source: main/trunk/openPLM/plmapp/controllers/part.py @ 1565

Revision 1565, 33.2 KB checked in by zali, 7 years ago (diff)

Controllers (plmobject) : save histo when a plmobject is cloned

Line 
1############################################################################
2# openPLM - open source PLM
3# Copyright 2010 Philippe Joulaud, Pierre Cosquer
4#
5# This file is part of openPLM.
6#
7#    openPLM is free software: you can redistribute it and/or modify
8#    it under the terms of the GNU General Public License as published by
9#    the Free Software Foundation, either version 3 of the License, or
10#    (at your option) any later version.
11#
12#    openPLM is distributed in the hope that it will be useful,
13#    but WITHOUT ANY WARRANTY; without even the implied warranty of
14#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15#    GNU General Public License for more details.
16#
17#    You should have received a copy of the GNU General Public License
18#    along with openPLM.  If not, see <http://www.gnu.org/licenses/>.
19#
20# Contact :
21#    Philippe Joulaud : ninoo.fr@gmail.com
22#    Pierre Cosquer : pierre.cosquer@insa-rennes.fr
23################################################################################
24
25"""
26"""
27
28import datetime
29from collections import namedtuple
30
31from django.db.models.query import Q
32
33import openPLM.plmapp.models as models
34from openPLM.plmapp.units import DEFAULT_UNIT
35from openPLM.plmapp.controllers.plmobject import PLMObjectController
36from openPLM.plmapp.controllers.base import get_controller
37from openPLM.plmapp.fileformats import is_cad_file
38
39from openPLM.plmapp.exceptions import PermissionError
40
41Child = namedtuple("Child", "level link")
42Parent = namedtuple("Parent", "level link")
43
44class PartController(PLMObjectController):
45    u"""
46    Controller for :class:`.Part`.
47
48    This controller adds methods to manage Parent-Child links between two
49    Parts.
50    """
51
52    def check_add_child(self, child):
53        """
54        Checks if *child* can be added to *self*.
55        If *child* can not be added, an exception is raised.
56       
57        :param child: child to be added
58        :type child: :class:`.Part`
59       
60        :raises: :exc:`ValueError` if *child* is already a child or a parent.
61        :raises: :exc:`.PermissionError` if :attr:`_user` is not the owner of
62            :attr:`object`.   
63        """
64        self.check_permission("owner")
65        self.check_editable()
66        if child.is_cancelled:
67            raise ValueError("Can not add child: child is cancelled.")
68        if child.is_deprecated:
69            raise ValueError("Can not add child: child is deprecated.")
70        if not child.is_part:
71            raise TypeError("Can not add child: not a Part")
72        # check if child is not a parent
73        if child.id == self.object.id:
74            raise ValueError("Can not add child: child is current object")
75        get_controller(child.type)(child, self._user).check_readable()
76        if self.is_ancestor(child):
77            raise ValueError("Can not add child %s to %s, it is a parent" %
78                                (child, self.object))
79        link = self.parentchildlink_parent.filter(child=child, end_time=None)
80        if link.exists():
81            raise ValueError("Can not add child, %s is already a child of %s" %
82                                (child, self.object))
83
84    def can_add_child(self, child):
85        """
86        Returns True if *child* can be added to *self*.
87        """
88
89        can_add = False
90        try:
91            self.check_add_child(child)
92            can_add = True
93        except StandardError:
94            pass
95        return can_add
96
97    def add_child(self, child, quantity, order, unit=DEFAULT_UNIT, **extension_data):
98        """
99        Adds *child* to *self*.
100
101        :param child: added child
102        :type child: :class:`.Part`
103        :param quantity: amount of *child*
104        :type quantity: positive float
105        :param order: order
106        :type order: positive int
107        :param unit: a valid unit
108
109        Extra arguments are used to create relevant :class:`.ParentChildLinkExtension`.
110       
111        :raises: :exc:`ValueError` if *child* is already a child or a parent.
112        :raises: :exc:`ValueError` if *quantity* or *order* are negative.
113        :raises: :exc:`.PermissionError` if :attr:`_user` is not the owner of
114            :attr:`object`.
115        :raises: :exc:`.PermissionError` if :attr:`object` is not editable.
116        """
117
118        if isinstance(child, PLMObjectController):
119            child = child.object
120        self.check_add_child(child)
121        if order < 0 or quantity < 0:
122            raise ValueError("Quantity or order is negative")
123        # data are valid : create the link
124        link = models.ParentChildLink()
125        link.parent = self.object
126        link.child = child
127        link.quantity = quantity
128        link.order = order
129        link.unit = unit
130        link.save()
131        # handle plces
132        for PCLE in models.get_PCLEs(self.object):
133            name = PCLE._meta.module_name
134            if name in extension_data and PCLE.one_per_link():
135                ext = PCLE(link=link, **extension_data[name])
136                ext.save()
137        # records creation in history
138        self._save_histo(link.ACTION_NAME,
139                         "parent : %s\nchild : %s" % (self.object, child))
140        return link
141
142    def delete_child(self, child):
143        u"""
144        Deletes *child* from current children and records this action in the
145        history.
146
147        .. note::
148            The link is not destroyed: its :attr:`.ParentChildLink.end_time`
149            is set to now.
150       
151        :raises: :exc:`.PermissionError` if :attr:`_user` is not the owner of
152            :attr:`object`.
153        :raises: :exc:`.PermissionError` if :attr:`object` is not editable.
154        """
155
156        self.check_permission("owner")
157        self.check_editable()
158        if isinstance(child, PLMObjectController):
159            child = child.object
160        link = self.parentchildlink_parent.get(child=child, end_time=None)
161        link.end_time = datetime.datetime.today()
162        link.save()
163        self._save_histo("Delete - %s" % link.ACTION_NAME, "child : %s" % child)
164
165    def modify_child(self, child, new_quantity, new_order, new_unit,
166            **extension_data):
167        """
168        Modifies information about *child*.
169
170        :param child: added child
171        :type child: :class:`.Part`
172        :param new_quantity: amount of *child*
173        :type new_quantity: positive float
174        :param new_order: order
175        :type new_order: positive int
176       
177        Extra arguments are used to modify relevant :class:`.ParentChildLinkExtension`.
178       
179        :raises: :exc:`.PermissionError` if :attr:`_user` is not the owner of
180            :attr:`object`.
181        :raises: :exc:`.PermissionError` if :attr:`object` is not editable.
182        """
183       
184        self.check_permission("owner")
185        self.check_editable()
186        if isinstance(child, PLMObjectController):
187            child = child.object
188        if new_order < 0 or new_quantity < 0:
189            raise ValueError("Quantity or order is negative")
190        link = models.ParentChildLink.objects.get(parent=self.object,
191                                                  child=child, end_time=None)
192        original_extension_data = link.get_extension_data()
193
194        if (link.quantity == new_quantity and link.order == new_order and
195            link.unit == new_unit and original_extension_data == extension_data):
196            # do not make an update if it is useless
197            return
198        link.end_time = datetime.datetime.today()
199        link.save()
200        # make a new link
201        link2, extensions = link.clone(quantity=new_quantity, order=new_order,
202                       unit=new_unit, end_time=None, extension_data=extension_data)
203        details = ""
204        if link.quantity != new_quantity:
205            details += "quantity changes from %f to %f\n" % (link.quantity, new_quantity)
206        if link.order != new_order:
207            details += "order changes from %d to %d" % (link.order, new_order)
208        if link.unit != new_unit:
209            details += "unit changes from %s to %s" % (link.unit, new_unit)
210
211        # TODO: details of extension changes
212
213        self._save_histo("Modify - %s" % link.ACTION_NAME, details)
214        link2.save(force_insert=True)
215        # save cloned extensions
216        for ext in extensions:
217            ext.link = link2
218            ext.save(force_insert=True)
219        # add new extensions
220        for PCLE in models.get_PCLEs(self.object):
221            name = PCLE._meta.module_name
222            if (name in extension_data and name not in original_extension_data
223                and PCLE.one_per_link()):
224                ext = PCLE(link=link2, **extension_data[name])
225                ext.save()
226        return link2
227
228    def replace_child(self, link, new_child):
229        """
230        Replaces a child by another one.
231
232        :param link: link being replaced, its data (extensions included)
233                     are copied
234        :type link: :class:`.ParentChildLink`
235        :param new_child: the new child
236        :type new_child: :class:`.Part`
237
238        :raises: :exc:`ValueError` if the link is invalid (already completed
239                 or its parent is not the current object)
240        :raises: all permission errors raised by :meth:`check_add_child`
241        """
242        if link.end_time != None or link.parent_id != self.id:
243            raise ValueError("Invalid link")
244        if isinstance(new_child, PLMObjectController):
245            new_child = new_child.object
246        if link.child == new_child:
247            return link
248        self.check_add_child(new_child)
249        link.end_time = datetime.datetime.today()
250        link.save()
251        # make a new link
252        link2, extensions = link.clone(child=new_child, end_time=None)
253        details = u"Child changes from %s to %s" % (link.child, new_child)
254        self._save_histo("Modify - %s" % link.ACTION_NAME, details)
255        link2.save(force_insert=True)
256        # save cloned extensions
257        for ext in extensions:
258            ext.link = link2
259            ext.save(force_insert=True)
260        return link2       
261
262    def get_children(self, max_level=1, date=None,
263            related=("child", "child__state", "child__lifecycle"),
264            only_official=False, only=None):
265        """
266        Returns a list of all children at time *date*.
267       
268        :param max_level: maximum level of children, ``-1``
269            returns all descendants, ``1`` returns direct children
270        :param related: a list of related fields that are given
271            to retrieve the :class:`.ParentChildLink`
272        :param only_official: True if the result should be pruned to
273            only include official children
274        :param only: a list of fields that are given to limit the
275            retrieved field of the :class:`.ParentChildLink`
276        :rtype: list of :class:`Child`
277        """
278       
279        objects = models.ParentChildLink.objects.order_by("-order")\
280                .select_related(*related)
281        if date is None:
282            links = objects.filter(end_time__exact=None)
283        else:
284            links = objects.filter(ctime__lte=date).exclude(end_time__lt=date)
285        if only is not None:
286            links = links.only(*only)
287        res = []
288        parents = [self.object.id]
289        level = 1
290        last_children = []
291        children_ids = []
292        while parents and (max_level < 0 or level <= max_level):
293            qs = links.filter(parent__in=parents)
294            parents = []
295            last = []
296            for link in qs.iterator():
297                parents.append(link.child_id)
298                child = Child(level, link)
299                last.append(child)
300                children_ids.append(link.child_id)
301                if level == 1:
302                    res.insert(0, child)
303                else:
304                    for c in last_children:
305                        if c.link.child_id == link.parent_id:
306                            res.insert(res.index(c) +1, child)
307                            break
308            last_children = last
309            level += 1
310        if only_official:
311            # retrieves all official children at *date* and then prunes the
312            # tree so that we only run one query
313            res2 = []
314            sh = models.StateHistory.objects.filter(plmobject__in=children_ids,
315                    state_category=models.StateHistory.OFFICIAL)
316            if date is None:
317                sh = sh.filter(end_time__exact=None)
318            else:
319                sh = sh.filter(start_time__lte=date).exclude(end_time__lt=date)
320            valid_children = set(sh.values_list("plmobject_id", flat=True))
321            # level_threshold is used to cut a "branch" of the tree
322            level_threshold = len(res) + 1 # all levels are inferior to this value
323            for child in res:
324                if child.level > level_threshold:
325                    continue
326                if child.link.child_id in valid_children:
327                    res2.append(child)
328                    level_threshold = len(res) + 1
329                else:
330                    level_threshold = child.level
331            res = res2
332        return res
333
334    def is_ancestor(self, part):
335        """
336        Returns True if *part* is an ancestor of the current object.
337        """
338        links = models.ParentChildLink.objects.filter(end_time__exact=None)
339        parents = [part.id]
340        last_children = []
341        while parents:
342            parents = links.filter(parent__in=parents).values_list("child",
343                    flat=True)
344            if self.id in parents:
345                return True
346        return False
347   
348    def get_parents(self, max_level=1, date=None,
349            related=("parent", "parent__state", "parent__lifecycle"),
350            only_official=False, only=None):
351        """
352        Returns a list of all parents at time *date*.
353       
354        :param max_level: maximum level of parents, ``-1``
355            returns all ancestors, ``1`` returns direct parents
356        :param related: a list of related fields that are given
357            to retrieve the :class:`.ParentChildLink`
358        :param only_official: True if the result should be pruned to
359            only include official parents
360        :param only: a list of fields that are given to limit the
361            retrieved field of the :class:`.ParentChildLink`
362        :rtype: list of :class:`Parent`
363        """
364
365        objects = models.ParentChildLink.objects.order_by("-order")\
366                .select_related(*related)
367        if not date:
368            links = objects.filter(end_time__exact=None)
369        else:
370            links = objects.filter(ctime__lte=date).exclude(end_time__lt=date)
371        if only is not None:
372            links = links.only(*only)
373        res = []
374        children = [self.object.id]
375        level = 1
376        last_parents = []
377        parents_ids = []
378        while children and (max_level < 0 or level <= max_level):
379            qs = links.filter(child__in=children)
380            children = []
381            last = []
382            for link in qs.iterator():
383                children.append(link.parent_id)
384                parent = Parent(level, link)
385                last.append(parent)
386                parents_ids.append(link.parent_id)
387                if level == 1:
388                    res.insert(0, parent)
389                else:
390                    for c in last_parents:
391                        if c.link.parent_id == link.child_id:
392                            res.insert(res.index(c) +1, parent)
393                            break
394            last_parents = last
395            level += 1
396        if only_official:
397            # retrieves all official children at *date* and then prunes the
398            # tree so that we only run one query
399            res2 = []
400            sh = models.StateHistory.objects.filter(plmobject__in=parents_ids,
401                    state_category=models.StateHistory.OFFICIAL)
402            if date is None:
403                sh = sh.filter(end_time__exact=None)
404            else:
405                sh = sh.filter(start_time__lte=date).exclude(end_time__lt=date)
406            valid_parents = set(sh.values_list("plmobject_id", flat=True))
407            # level_threshold is used to cut a "branch" of the tree
408            level_threshold = len(res) + 1 # all levels are inferior to this value
409            for parent in res:
410                if parent.level > level_threshold:
411                    continue
412                if parent.link.parent_id in valid_parents:
413                    res2.append(parent)
414                    level_threshold = len(res) + 1
415                else:
416                    level_threshold = parent.level
417            res = res2
418        return res
419
420    def update_children(self, formset):
421        u"""
422        Updates children informations with data from *formset*
423       
424        :param formset:
425        :type formset: a modelfactory_formset of
426                        :class:`~plmapp.forms.ModifyChildForm`
427       
428        :raises: :exc:`.PermissionError` if :attr:`_user` is not the owner of
429            :attr:`object`.
430        :raises: :exc:`.PermissionError` if :attr:`object` is not editable.
431        """
432
433        self.check_permission("owner")
434        self.check_editable()
435        if formset.is_valid():
436            for form in formset.forms:
437                parent = form.cleaned_data["parent"]
438                if parent.pk != self.object.pk:
439                    raise ValueError("Bad parent %s (%s expected)" % (parent, self.object))
440                delete = form.cleaned_data["delete"]
441                child = form.cleaned_data["child"]
442                if delete:
443                    self.delete_child(child)
444                else:
445                    quantity = form.cleaned_data["quantity"]
446                    order = form.cleaned_data["order"]
447                    unit = form.cleaned_data["unit"]
448                    self.modify_child(child, quantity, order, unit,
449                            **form.extensions)
450
451    def revise(self, new_revision, child_links=None, documents=(),
452            parents=()):
453        """
454        Revises the part. Does the same thing as :meth:`.PLMObjectController.revise`
455        and:
456           
457            * copies all :class:`.ParentChildLink` of *child_links*, with the
458              new revision as the new parent. If *child_links* is None (the
459              default), all current children are copied. If an empty sequence
460              is given, no links are copied.
461
462            * attaches all document of *documents*, by default, no documents
463              are attached. The method :meth:`get_suggested_documents` returns a
464              list of documents that should be interesting.
465
466            * replaces all parent links in *parents*. This arguments must be
467              a list of tuples (link (an instance of :class:`.ParentChildLink`),
468              parent (an instance of :class:`.PLMObject`)) where *parent* is
469              the parent whose the bom will be modified and *link* is the
470              source of data (quantity, unit, order...). *link* will be
471              ended if *parent* is a parent of the current part.
472              The method :meth:`get_suggested_parents` returns a list of
473              tuples that may interest the user who revises this part.
474        """
475        # same as PLMObjectController + add children
476        new_controller = super(PartController, self).revise(new_revision)
477        # adds the children
478        if child_links is None:
479            child_links = (x.link for x in self.get_children(1))
480        for link in child_links:
481            link.clone(save=True, parent=new_controller.object)
482        # attach the documents
483        for doc in documents:
484            models.DocumentPartLink.objects.create(part=new_controller.object,
485                    document=doc)
486        # for each parent, replace its child with the new revision
487        now = datetime.datetime.today()
488        for link, parent in parents:
489            link.clone(save=True, parent=parent, child=new_controller.object)
490            if link.parent_id == parent.id:
491                link.end_time = now
492                link.save()
493        return new_controller
494
495    def get_suggested_documents(self):
496        """
497        Returns a QuerySet of documents that should be suggested when the
498        user revises the part.
499
500        A document is suggested if:
501       
502            a. it is attached to the current part and:
503             
504                1. it is a *draft* and its superior revisions, if they exist,
505                   are *not* attached to the part
506
507                   or
508
509                2. it is *official* and its superior revisions, if they exist,
510                   are *not* attached to the part
511
512                   or
513
514                3. it is *official* and a superior revision is attached *and*
515                   another superior revision is not attached to the part
516
517            b. it is *not* attached to the current part, an inferior revision
518               is attached to the part and:
519
520                1. it is a draft
521
522                   or
523
524                2. it is official
525               
526        """
527        docs = []
528        links = self.get_attached_documents()
529        attached_documents = set(link.document_id for link in links)
530        for link in links:
531            document = link.document
532            ctrl = PLMObjectController(document, self._user)
533            revisions = ctrl.get_next_revisions()
534            attached_revisions = [d for d in revisions if d.id in attached_documents]
535            other_revisions = set(revisions).difference(attached_revisions)
536            if not attached_revisions:
537                if document.is_draft or document.is_official:
538                    docs.append(document.id)
539            else:
540                if document.is_official and not other_revisions:
541                    docs.append(document.id)
542            for rev in other_revisions:
543                if rev.is_official or rev.is_draft:
544                    docs.append(rev.id)
545        return models.Document.objects.filter(id__in=docs)
546
547    def get_suggested_parents(self):
548        """
549        Returns a list of suggested parents that should be suggested
550        when the part is revised.
551
552        This method returns a list of tuple (link (an instance of
553        :class:`.ParentChildLink`), parent (an instance of :class:`.PLMObject`)).
554        It does not returns a list of links, since it may suggest a part
555        that is not a parent but whose one of its previous revision is a parent.
556        We need a link to copy its data (order, quantity, unit and extensions).
557
558        A part is suggested as a parent if:
559
560            a. it is already a parent and:
561
562                1. no superior revisions are a parent and its state is draft
563                   or official
564   
565                   or
566               
567                2. no superior revisions exist and its state is proposed.
568
569            b. it is not a parent, a previous revision is a parent, its state
570               is a draft or a parent. In that case, the link of the most
571               superior parent revision is used.
572
573        """
574        parents = self.get_parents(1)
575        links = []
576        ids = set(p.link.parent_id for p in parents)
577        for level, link in parents:
578            parent = link.parent
579            ctrl = PLMObjectController(parent, self._user)
580            revisions = ctrl.get_next_revisions()
581            attached_revisions = [d for d in revisions if d.id in ids]
582            other_revisions = set(revisions).difference(attached_revisions)
583            if not attached_revisions:
584                if parent.is_draft or parent.is_official or \
585                    (parent.is_proposed and not other_revisions):
586                    links.append((link, parent))
587            for p in other_revisions:
588                if p.is_draft or p.is_official:
589                    links.append((link, p.part))
590        # it is possible that some parts are suggested twice or more
591        # if they are not a parent (they are a superior revision of a parent)
592        # so we must clean up links
593        links2 = dict() # id -> (link, parent)
594        for link, parent in links:
595            if parent.id in ids:
596                links2[parent.id] = (link, parent)
597            else:
598                # it is not a parent
599                try:
600                    l, p = links2[parent.id]
601                    if l.parent.ctime < link.parent.ctime:
602                        # true if parent is a superior revision
603                        links2[parent.id] = (link, parent)
604                except KeyError:
605                    links2[parent.id] = (link, parent)
606        return links2.values()
607
608    def attach_to_document(self, document):
609        """
610        Links *document* (a :class:`.Document`) with
611        :attr:`~PLMObjectController.object`.
612       
613        :raises: :exc:`.PermissionError` if :attr:`_user` is not the owner of
614            :attr:`object`.
615        """
616       
617        self.check_attach_document(document)
618        if isinstance(document, PLMObjectController):
619            document = document.object
620        self.documentpartlink_part.create(document=document)
621        self._save_histo(models.DocumentPartLink.ACTION_NAME,
622                         "Part : %s - Document : %s" % (self.object, document))
623
624    def detach_document(self, document):
625        """
626        Delete link between *document* (a :class:`.Document`)
627        and :attr:`~PLMObjectController.object`.
628       
629        :raises: :exc:`.PermissionError` if :attr:`_user` is not the owner of
630            :attr:`object`.
631        """
632       
633        self.check_attach_document(document, True)
634        if isinstance(document, PLMObjectController):
635            document = document.object
636        link = self.documentpartlink_part.get(document=document)
637        link.delete()
638        self._save_histo(models.DocumentPartLink.ACTION_NAME + " - delete",
639                         "Part : %s - Document : %s" % (self.object, document))
640
641    def get_attached_documents(self):
642        """
643        Returns all :class:`.Document` attached to
644        :attr:`~PLMObjectController.object`.
645        """
646        return self.documentpartlink_part.all()
647
648    def get_detachable_documents(self):
649        """
650        Returns all attached documents the user can detach.
651        """
652        links = []
653        for link in self.get_attached_documents().select_related("document"):
654            doc = link.document
655            if self.can_detach_document(doc):
656                links.append(link.id)
657        return self.documentpartlink_part.filter(id__in=links)
658     
659    def is_document_attached(self, document):
660        """
661        Returns True if *document* is attached to the current part.
662        """
663
664        if isinstance(document, PLMObjectController):
665            document = document.object
666        return self.documentpartlink_part.filter(document=document).exists()
667   
668    def check_attach_document(self, document, detach=False):
669        if not hasattr(document, "is_document") or not document.is_document:
670            raise TypeError("%s is not a document" % document)
671        self.check_contributor()
672        if not (self.is_draft or document.is_draft):
673            raise ValueError("Can not attach: one of the part or document's state must be draft.")
674        if self.is_cancelled:
675            raise ValueError("Can not attach: part is cancelled.")
676        if self.is_deprecated:
677            raise ValueError("Can not attach: part is deprecated.")
678        if document.is_cancelled:
679            raise ValueError("Can not attach: document is cancelled.")
680        if document.is_deprecated:
681            raise ValueError("Can not attach: document is deprecated.")
682        if self.is_proposed:
683            raise ValueError("Can not attach: part's state is %s" % self.state.name)
684        if isinstance(document, PLMObjectController):
685            document.check_readable()
686            ctrl = document
687            document = document.object
688        else:
689            ctrl = get_controller(document.type)(document, self._user)
690            ctrl.check_readable()
691        self.check_readable()
692        if document.is_draft and self.is_draft:
693            owner_ok = True
694        elif document.is_draft or document.is_proposed:
695            owner_ok = ctrl.check_permission("owner", raise_=False)
696        else:
697            self.check_editable()
698            owner_ok = False
699        if not owner_ok:
700            self.check_permission("owner")
701       
702        if self.is_document_attached(document):
703            if not detach:
704                raise ValueError("Document is already attached to the part.")
705        elif detach:
706            raise ValueError("Document is not attached to the part.")
707
708    def can_attach_document(self, document):
709        """
710        Returns True if *document* can be attached to the current part.
711        """
712        can_attach = False
713        try:
714            self.check_attach_document(document)
715            can_attach = True
716        except StandardError:
717            pass
718        return can_attach
719
720    def can_detach_document(self, document):
721        """
722        Returns True if *document* can be detached.
723        """
724        can_detach = False
725        try:
726            self.check_attach_document(document, True)
727            can_detach = True
728        except StandardError:
729            pass
730        return can_detach
731
732    def update_doc_cad(self, formset):
733        u"""
734        Updates doc_cad informations with data from *formset*
735       
736        :param formset:
737        :type formset: a modelfactory_formset of
738                        :class:`~plmapp.forms.ModifyChildForm`
739       
740        :raises: :exc:`ValueError` if one of the document is not detachable.
741        """
742         
743        docs = set()
744        if formset.is_valid():
745            for form in formset.forms:
746                part = form.cleaned_data["part"]
747                if part.pk != self.object.pk:
748                    raise ValueError("Bad part %s (%s expected)" % (part, self.object))
749                delete = form.cleaned_data["delete"]
750                document = form.cleaned_data["document"]
751                if delete:
752                    docs.add(document)
753            if docs:
754                for doc in docs:
755                    self.check_attach_document(doc, True)
756                ids = (d.id for d in docs)
757                self.documentpartlink_part.filter(document__in=ids).delete()
758
759    def cancel(self):
760        """
761        Cancels the object:
762
763            * calls :meth:`.PLMObjectController.cancel`
764            * removes all :class:`.DocumentPartLink` related to the object
765            * removes all children/parents link (set their end_time)
766        """
767        super(PartController, self).cancel()
768        self.get_attached_documents().delete()
769        q = Q(parent=self.object) | Q(child=self.object)
770        now = datetime.datetime.today()
771        models.ParentChildLink.objects.filter(q, end_time=None).update(end_time=now)
772       
773    def check_cancel(self,raise_=True):
774        res = super(PartController, self).check_cancel(raise_=raise_)
775        if res :
776            q = Q(parent=self.object) | Q(child=self.object)
777            res = res and not models.ParentChildLink.objects.filter(q, end_time=None).exists()
778            if (not res) and raise_ :
779                raise PermissionError("This part is related to an other part.")
780            res = res and not self.get_attached_documents()
781            if (not res) and raise_ :
782                raise PermissionError("This part has a document related to it.")
783        return res
784
785    def clone(self,form, user, child_links, documents, block_mails=False, no_index=False):
786        """
787        Clones the object :
788
789        calls PLMObjectController.clone()
790
791        :param child_links: list of :class:`.ParentChildLink` selected to be cloned with the new part as parent
792        :param documents: list of :class:`.Document` selected to be attached to the new part
793        """
794        new_ctrl = super(PartController, self).clone(form, user, block_mails, no_index)
795        if child_links :
796            for link in child_links:
797                link.clone(save=True, parent=new_ctrl.object)
798        if documents :
799            for doc in documents:
800                models.DocumentPartLink.objects.create(part=new_ctrl.object,
801                    document=doc)
802        details = "to %s//%s//%s//%s " %(new_ctrl.type, new_ctrl.reference, new_ctrl.revision, new_ctrl.name)
803        self._save_histo("Clone", details)
804        return new_ctrl
805
806    def has_links(self):
807        """
808        Return true if the part :
809       
810            * is a parent or a child
811            * is attached to at least one document
812        """
813        q = Q(parent=self.object) | Q(child=self.object)
814        res = not models.ParentChildLink.objects.filter(q, end_time=None).exists()
815        res = res and not self.get_attached_documents().exists()
816        return res
817
818   
819    def get_cad_files(self):
820        """
821        Returns an iterable of all :class:`.DocumentFile` related
822        to *part* that contain a CAD file. It retrieves all non deprecated
823        files of all documents parts to *part* and its children and
824        filters these files according to their extension (see :meth:`.is_cad_file`).
825        """
826        children = self.get_children(-1, related=("child",))
827        children_ids = set(c.link.child_id for c in children)
828        children_ids.add(self.id)
829        links = models.DocumentPartLink.objects.filter(part__in=children_ids)
830        docs = links.values_list("document", flat=True)
831        d_o_u = "document__owner__username"
832        files = models.DocumentFile.objects.filter(deprecated=False,
833                    document__in=set(docs))
834        # XXX : maybe its faster to build a complex query than retrieving
835        # each file and testing their extension
836        return (df for df in files.select_related(d_o_u) if is_cad_file(df.filename))
837
Note: See TracBrowser for help on using the repository browser.