source: main/branches/3D/openPLM/plmapp/models.py @ 662

Revision 662, 38.6 KB checked in by pcosquer, 8 years ago (diff)

3D branch: merge changes from trunk (rev [661])

Line 
1#! -*- coding:utf-8 -*-
2
3############################################################################
4# openPLM - open source PLM
5# Copyright 2010 Philippe Joulaud, Pierre Cosquer
6#
7# This file is part of openPLM.
8#
9#    openPLM is free software: you can redistribute it and/or modify
10#    it under the terms of the GNU General Public License as published by
11#    the Free Software Foundation, either version 3 of the License, or
12#    (at your option) any later version.
13#
14#    openPLM is distributed in the hope that it will be useful,
15#    but WITHOUT ANY WARRANTY; without even the implied warranty of
16#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17#    GNU General Public License for more details.
18#
19#    You should have received a copy of the GNU General Public License
20#    along with Foobar.  If not, see <http://www.gnu.org/licenses/>.
21#
22# Contact :
23#    Philippe Joulaud : ninoo.fr@gmail.com
24#    Pierre Cosquer : pierre.cosquer@insa-rennes.fr
25################################################################################
26
27u"""
28Introduction
29=============
30
31Models for openPLM
32
33This module contains openPLM's main models.
34
35There are 5 kinds of models:
36    * :class:`UserProfile`
37    * Lifecycle related models :
38        - :class:`Lifecycle`
39        - :class:`State`
40        - :class:`LifecycleStates`
41        - there are some functions that may be useful:
42            - :func:`get_default_lifecycle`
43            - :func:`get_default_state`
44    * History models:
45        - :class:`AbstractHistory` model
46        - :class:`History` model
47        - :class:`UserHistory` model
48    * PLMObject models:
49        - :class:`PLMObject` is the base class
50        - :class:`Part`
51        - :class:`Document` and related classes:
52            - :class:`DocumentStorage` (see also :obj:`docfs`)
53            - :class:`DocumentFile`
54        - functions:
55            - :func:`get_all_plmobjects`
56            - :func:`get_all_parts`
57            - :func:`get_all_documents`
58            - :func:`import_models`
59    * :class:`Link` models:
60        - :class:`RevisionLink`
61        - :class:`ParentChildLink`
62        - :class:`DocumentPartLink`
63        - :class:`DelegationLink`
64        - :class:`PLMObjectUserLink`
65
66
67Inheritance diagram
68=====================
69
70.. inheritance-diagram:: openPLM.plmapp.models
71    :parts: 1
72
73Classes and functions
74========================
75"""
76
77import os
78import string
79import random
80import hashlib
81import fnmatch
82import datetime
83
84import kjbuckets
85from django.db import models
86from django.db.models.signals import post_save
87from django.conf import settings
88from django.contrib.auth.models import User, Group
89from django.core.files.storage import FileSystemStorage
90from django.utils.encoding import iri_to_uri
91from django.utils.translation import ugettext_lazy as _
92from django.utils.translation import ugettext_noop
93from django.forms.util import ErrorList
94
95from openPLM.plmapp.units import UNITS, DEFAULT_UNIT
96from openPLM.plmapp.lifecycle import LifecycleList
97from openPLM.plmapp.utils import level_to_sign_str, memoize_noarg
98
99
100# user stuff
101
102class UserProfile(models.Model):
103    """
104    Profile for a :class:`~django.contrib.auth.models.User`
105    """
106    user = models.ForeignKey(User, unique=True)
107    #: True if user is an administrator
108    is_administrator = models.BooleanField(default=False, blank=True)
109    #: True if user is a contributor
110    is_contributor = models.BooleanField(default=False, blank=True)
111   
112    @property
113    def is_viewer(self):
114        u"""
115        True if user is just a viewer, i.e: not an adminstrator and not a
116        contributor.
117        """
118        return not (self.is_administrator or self.is_contributor)
119
120    def __unicode__(self):
121        return u"UserProfile<%s>" % self.user.username
122
123    @property
124    def plmobject_url(self):
125        return iri_to_uri("/user/%s/" % self.user.username)
126
127    @property
128    def rank(self):
129        u""" Rank of the user: "adminstrator", "contributor" or "viewer" """
130        if self.is_administrator:
131            return _("administrator")
132        elif self.is_contributor:
133            return _("contributor")
134        else:
135            return _("viewer")
136
137    @property
138    def attributes(self):
139        u"Attributes to display in `Attributes view`"
140        return ["first_name", "last_name", "email",  "creator", "owner",
141                "ctime", "mtime", "rank"]
142
143    @property
144    def menu_items(self):
145        "menu items to choose a view"
146        return ["attributes", "history", "parts-doc-cad", "delegation",
147                "groups"]
148
149    @classmethod
150    def excluded_creation_fields(cls):
151        "Returns fields which should not be available in a creation form"
152        return ["owner", "creator", "ctime", "mtime"]
153   
154
155def add_profile(sender, instance, created, **kwargs):
156    """ function called when an user is created to add his profile """
157    if sender == User and created:
158        profile = UserProfile(user=instance)
159        profile.save()
160
161if __name__ == "openPLM.plmapp.models":
162    post_save.connect(add_profile, sender=User)
163
164
165class GroupInfo(Group):
166    u"""
167    Class that stores additional data on a :class:`Group`.
168    """
169
170    description = models.TextField(blank=True)
171    creator = models.ForeignKey(User, related_name="%(class)s_creator")
172   
173    owner = models.ForeignKey(User, verbose_name=_("owner"),
174                              related_name="%(class)s_owner")
175    ctime = models.DateTimeField(_("date of creation"), default=datetime.datetime.today,
176                                 auto_now_add=False)
177    mtime = models.DateTimeField(_("date of last modification"), auto_now=True)
178
179    def __init__(self, *args, **kwargs):
180        if "__fake__" not in kwargs:
181            super(GroupInfo, self).__init__(*args, **kwargs)
182
183    @property
184    def plmobject_url(self):
185        return iri_to_uri("/group/%s/" % self.name)
186
187    @property
188    def attributes(self):
189        u"Attributes to display in `Attributes view`"
190        return ["name", "description", "creator", "owner",
191                "ctime", "mtime"]
192
193    @property
194    def menu_items(self):
195        "menu items to choose a view"
196        return ["attributes", "history", "users", "objects"]
197
198    @classmethod
199    def excluded_creation_fields(cls):
200        "Returns fields which should not be available in a creation form"
201        return ["owner", "creator", "ctime", "mtime"]
202
203    @classmethod
204    def get_creation_fields(cls):
205        """
206        Returns fields which should be displayed in a creation form.
207
208        By default, it returns :attr:`attributes` less attributes returned by
209        :meth:`excluded_creation_fields`
210        """
211        fields = []
212        for field in cls(__fake__=True).attributes:
213            if field not in cls.excluded_creation_fields():
214                fields.append(field)
215        return fields
216
217    @classmethod
218    def excluded_modification_fields(cls):
219        """
220        Returns fields which should not be available in a modification form
221       
222        By default, it returns :attr:`attributes` less attributes returned by
223        :meth:`excluded_modification_fields`
224        """
225        return [ugettext_noop("name"), ugettext_noop("creator"),
226                ugettext_noop("owner"), ugettext_noop("ctime"),
227                ugettext_noop("mtime")]
228
229    @classmethod
230    def get_modification_fields(cls):
231        "Returns fields which should be displayed in a modification form"
232        fields = []
233        for field in cls(__fake__=True).attributes:
234            if field not in cls.excluded_modification_fields():
235                fields.append(field)
236        return fields
237
238    @property
239    def is_editable(self):
240        return True
241
242    def get_attributes_and_values(self):
243        return [(attr, getattr(self, attr)) for attr in self.attributes]
244
245
246# lifecycle stuff
247
248class State(models.Model):
249    u"""
250    State : object which represents a state in a lifecycle
251   
252    .. attribute:: name
253
254        name of the state, must be unique
255    """
256    name = models.CharField(max_length=50, primary_key=True)
257
258    def __unicode__(self):
259        return u'State<%s>' % self.name
260
261class Lifecycle(models.Model):
262    u"""
263    Lifecycle : object which represents a lifecycle
264   
265    .. attribute:: name
266
267        name of the lifecycle, must be unique
268
269    .. attribute:: official_state
270
271        *official* :class:`State` of the lifecycle
272
273    .. note::
274        A Lifecycle is iterable and each iteration returns a string of
275        the next state.
276
277    .. seealso:: :class:`~plmapp.lifecycle.LifecycleList`
278        A class that simplifies the usage of a LifeCycle
279
280    """
281
282    name = models.CharField(max_length=50, primary_key=True)
283    official_state = models.ForeignKey(State)
284
285    def __init__(self, *args, **kwargs):
286        super(Lifecycle, self).__init__(*args, **kwargs)
287        # keep a cache of some values: Lifecycle are most of the time
288        # read-only objects, and there are no valid reasons to modify a
289        # lifecycle in a production environment
290        self._first_state = None
291        self._last_state = None
292        self._states_list = None
293
294    def __unicode__(self):
295        return u'Lifecycle<%s>' % self.name
296
297    def to_states_list(self):
298        u"""
299        Converts a Lifecycle to a :class:`LifecycleList` (a list of strings)
300        """
301        if self._states_list is None:
302            lcs = self.lifecyclestates_set.order_by("rank")
303            self._states_list = LifecycleList(self.name, self.official_state.name,
304                    *lcs.values_list("state__name", flat=True))
305        return LifecycleList(self.name, self.official_state, *self._states_list)
306
307    @property
308    def first_state(self):
309        if self._first_state is None:
310            self._first_state = self.lifecyclestates_set.order_by('rank')[0].state
311        return self._first_state
312   
313    @property
314    def last_state(self):
315        if self._last_state is None:
316            self._last_state = self.lifecyclestates_set.order_by('-rank')[0].state
317        return self._last_state
318
319    @property
320    def nb_states(self):
321        return self.lifecyclestates_set.count()
322
323    def __iter__(self):
324        return iter(self.to_states_list())
325
326    @classmethod
327    def from_lifecyclelist(cls, cycle):
328        u"""
329        Builds a Lifecycle from *cycle*. The built object is save in the database.
330        This function creates states which were not in the database
331       
332        :param cycle: the cycle use to build the :class:`Lifecycle`
333        :type cycle: :class:`~plmapp.lifecycle.LifecycleList`
334        :return: a :class:`Lifecycle`
335        """
336       
337        lifecycle = cls(name=cycle.name,
338            official_state=State.objects.get_or_create(name=cycle.official_state)[0])
339        lifecycle.save()
340        for i, state_name in enumerate(cycle):
341            state = State.objects.get_or_create(name=state_name)[0]
342            lcs = LifecycleStates(lifecycle=lifecycle, state=state, rank=i)
343            lcs.save()
344        return lifecycle
345               
346class LifecycleStates(models.Model):
347    u"""
348    A LifecycleStates links a :class:`Lifecycle` and a :class:`State`.
349   
350    The link is made with a field *rank* to order the states.
351    """
352    lifecycle = models.ForeignKey(Lifecycle)
353    state = models.ForeignKey(State)
354    rank = models.PositiveSmallIntegerField()
355
356    class Meta:
357        unique_together = (('lifecycle', 'state'),)
358
359    def __unicode__(self):
360        return u"LifecycleStates<%s, %s, %d>" % (unicode(self.lifecycle),
361                                                 unicode(self.state),
362                                                 self.rank)
363
364@memoize_noarg
365def get_default_lifecycle():
366    u"""
367    Returns the default :class:`Lifecycle` used when instanciate a :class:`PLMObject`
368    """
369    return Lifecycle.objects.all()[0]
370
371_default_states_cache = {}
372def get_default_state(lifecycle=None):
373    u"""
374    Returns the default :class:`State` used when instanciate a :class:`PLMObject`.
375    It's the first state of the default lifecycle.
376    """
377
378    if not lifecycle:
379        lifecycle = get_default_lifecycle()
380    state = _default_states_cache.get(lifecycle.name, None)
381    if state is None:
382        state = lifecycle.first_state
383        _default_states_cache[lifecycle.name] = state
384    return state
385
386# PLMobjects
387
388class PLMObject(models.Model):
389    u"""
390    Base class for :class:`Part` and :class:`Document`.
391
392    A PLMObject is identified by a triplet reference/type/revision
393
394    :key attributes:
395        .. attribute:: reference
396
397            Reference of the :class:`PLMObject`, for example ``YLTG00``
398        .. attribute:: type
399
400            Type of the :class:`PLMObject`, for example ``Game``
401        .. attribute:: revision
402           
403            Revision of the :class:`PLMObject`, for example ``a``
404
405    :other attributes:
406        .. attribute:: name
407
408            Name of the product, for example ``Game of life``
409        .. attribute:: creator
410
411            :class:`~django.contrib.auth.models.User` who created the :class:`PLMObject`
412        .. attribute:: creator
413
414            :class:`~django.contrib.auth.models.User` who owns the :class:`PLMObject`
415        .. attribute:: ctime
416
417            date of creation of the object (default value : current time)
418        .. attribute:: mtime
419
420            date of last modification of the object (automatically field at each save)
421        .. attribute:: lifecycle
422           
423            :class:`Lifecycle` of the object
424        .. attribute:: state
425           
426            Current :class:`State` of the object
427        .. attribute:: group
428
429            :class:`GroupInfo` that owns the object
430
431    .. note::
432
433        This class is abstract, to create a PLMObject, see :class:`Part` and
434        :class:`Document`.
435
436    """
437
438    # key attributes
439    reference = models.CharField(_("reference"), max_length=50, db_index=True)
440    type = models.CharField(_("type"), max_length=50)
441    revision = models.CharField(_("revision"), max_length=50)
442
443    # other attributes
444    name = models.CharField(_("name"), max_length=100, blank=True,
445                            help_text=_(u"Name of the product"))
446
447    creator = models.ForeignKey(User, verbose_name=_("creator"),
448                                related_name="%(class)s_creator")
449    owner = models.ForeignKey(User, verbose_name=_("owner"),
450                              related_name="%(class)s_owner")
451    ctime = models.DateTimeField(_("date of creation"), default=datetime.datetime.today,
452                                 auto_now_add=False)
453    mtime = models.DateTimeField(_("date of last modification"), auto_now=True)
454    group = models.ForeignKey(GroupInfo, related_name="%(class)s_group")
455
456    # state and lifecycle
457    lifecycle = models.ForeignKey(Lifecycle, verbose_name=_("lifecycle"),
458                                  related_name="%(class)s_lifecyle",
459                                  default=get_default_lifecycle)
460    state = models.ForeignKey(State, verbose_name=_("state"),
461                              related_name="%(class)s_lifecyle",
462                              default=get_default_state)
463   
464   
465    class Meta:
466        # keys in the database
467        unique_together = (('reference', 'type', 'revision'),)
468        ordering = ["type", "reference", "revision"]
469
470    def __init__(self, *args, **kwargs):
471        # little hack:
472        # get_creation_fields is a class method but it needs to create
473        # an instance, this hacks avoids calls to default value functions
474        if "__fake__" not in kwargs:
475            super(PLMObject, self).__init__(*args, **kwargs)
476        self._promotion_errors = None
477
478    def __unicode__(self):
479        return u"%s<%s/%s/%s>" % (type(self).__name__, self.reference, self.type,
480                                  self.revision)
481
482    def _is_promotable(self):
483        """
484        Returns True if the object's state is the last state of its lifecycle.
485        """
486        self._promotion_errors = ErrorList()
487        if self.lifecycle.last_state == self.state:
488            self._promotion_errors.append(_(u"The object is at its last state."))
489            return False
490        return True
491
492    def is_promotable(self):
493        u"""
494        Returns True if object is promotable
495
496        .. note::
497            This method is abstract and raises :exc:`NotImplementedError`.
498            This method must be overriden.
499        """
500        raise NotImplementedError()
501
502    def _get_promotion_errors(self):
503        """ Returns an :class:`ErrorList` of promotion errors.
504        Calls :meth:`is_promotable()` if it has not already been called.
505        """
506        if self._promotion_errors is None:
507            self.is_promotable()
508        return self._promotion_errors
509    promotion_errors = property(_get_promotion_errors)
510
511    @property
512    def is_editable(self):
513        """
514        True if the object is not in a non editable state
515        (for example, in an official or deprecated state).
516        """
517        lcs = self.lifecycle.lifecyclestates_set.only("rank")
518        current_rank = lcs.get(state=self.state).rank
519        official_rank = lcs.get(state=self.lifecycle.official_state).rank
520        return current_rank < official_rank
521   
522    def get_current_sign_level(self):
523        """
524        Returns the current sign level that an user must have to promote this
525        object.
526        """
527        rank = LifecycleStates.objects.get(state=self.state,
528                            lifecycle=self.lifecycle).rank
529        return level_to_sign_str(rank)
530   
531    def get_previous_sign_level(self):
532        """
533        Returns the current sign level that an user must have to demote this
534        object.
535        """
536        rank = LifecycleStates.objects.get(state=self.state,
537                            lifecycle=self.lifecycle).rank
538        return level_to_sign_str(rank - 1)
539   
540    @property
541    def is_part(self):
542        if self.type in get_all_plmobjects():
543            return issubclass(get_all_plmobjects()[self.type], Part)
544        return False
545
546    @property
547    def is_document(self):
548        if self.type in get_all_plmobjects():
549            return issubclass(get_all_plmobjects()[self.type], Document)
550        return False
551   
552    @property
553    def is_official(self):
554        u"Returns True if document's state is official."""
555        return self.state == self.lifecycle.official_state
556
557    @property
558    def attributes(self):
559        u"Attributes to display in `Attributes view`"
560        return ["type", "reference", "revision", "name", "creator", "owner",
561                "group", "ctime", "mtime"]
562
563    @property
564    def menu_items(self):
565        "Menu items to choose a view"
566        return [ugettext_noop("attributes"), ugettext_noop("lifecycle"),
567                ugettext_noop("revisions"), ugettext_noop("history"),
568                ugettext_noop("management")]
569
570    @classmethod
571    def excluded_creation_fields(cls):
572        "Returns fields which should not be available in a creation form"
573        return ["owner", "creator", "ctime", "mtime", "state"]
574
575    @property
576    def plmobject_url(self):
577        url = u"/object/%s/%s/%s/" % (self.type, self.reference, self.revision)
578        return iri_to_uri(url)
579   
580    @classmethod
581    def get_creation_fields(cls):
582        """
583        Returns fields which should be displayed in a creation form.
584
585        By default, it returns :attr:`attributes` less attributes returned by
586        :meth:`excluded_creation_fields`
587        """
588        fields = ["reference", "type", "revision", "lifecycle"]
589        for field in cls(__fake__=True).attributes:
590            if field not in cls.excluded_creation_fields():
591                fields.append(field)
592        return fields
593
594    @classmethod
595    def excluded_modification_fields(cls):
596        """
597        Returns fields which should not be available in a modification form
598        """
599        return [ugettext_noop("type"), ugettext_noop("reference"),
600                ugettext_noop("revision"),
601                ugettext_noop("ctime"), ugettext_noop("creator"),
602                ugettext_noop("owner"), ugettext_noop("ctime"),
603                ugettext_noop("mtime"), ugettext_noop("group")]
604
605    @classmethod
606    def get_modification_fields(cls):
607        """
608        Returns fields which should be displayed in a modification form
609       
610        By default, it returns :attr:`attributes` less attributes returned by
611        :meth:`excluded_modification_fields`
612        """
613        fields = []
614        for field in cls(__fake__=True).attributes:
615            if field not in cls.excluded_modification_fields():
616                fields.append(field)
617        return fields
618
619    def get_attributes_and_values(self):
620        return [(attr, getattr(self, attr)) for attr in self.attributes]
621
622# parts stuff
623
624class Part(PLMObject):
625    """
626    Model for parts
627    """
628
629    @property
630    def menu_items(self):
631        items = list(super(Part, self).menu_items)
632        items.extend([ugettext_noop("BOM-child"), ugettext_noop("parents"),
633                      ugettext_noop("doc-cad")])
634        return items
635
636    def is_promotable(self):
637        """
638        Returns True if the part is promotable.
639       
640        A part is promotable if:
641           
642            #. its state is not the last state of its lifecycle
643           
644            #. if the part is not editable (its state is official).
645           
646            #. the part is editable and:
647
648                #. there is a next state in its lifecycle and if its children
649                    which have the same lifecycle are in a state as mature as
650                    the object's state. 
651
652                #. if the part has no children, there is at least one official
653                   document attached to it.
654        """
655        if not self._is_promotable():
656            return False
657        if not self.is_editable:
658            return True
659        # check children
660        children = self.parentchildlink_parent.filter(end_time__exact=None).only("child")
661        lcs = self.lifecycle.to_states_list()
662        rank = lcs.index(self.state.name)
663        for link in children:
664            child = link.child
665            if child.lifecycle == self.lifecycle:
666                rank_c = lcs.index(child.state.name)
667                if rank_c == 0 or rank_c < rank:
668                    self._promotion_errors.append(_("Some children are at a lower or draft state."))
669                    return False
670        if not children:
671            # check that at least one document is attached and its state is official
672            # see ticket #57
673            found = False
674            links = self.documentpartlink_part.all()
675            for link in links:
676                found = link.document.is_official
677                if found:
678                    break
679            if not found:
680                self._promotion_errors.append(_("There are no official documents attached."))
681            return found
682        return True
683
684    @property
685    def is_part(self):
686        return True
687   
688    @property
689    def is_document(self):
690        return False
691
692def _get_all_subclasses(base, d):
693    if base.__name__ not in d:
694        d[base.__name__] = base
695    for part in base.__subclasses__():
696        _get_all_subclasses(part, d)
697
698@memoize_noarg
699def get_all_parts():
700    u"""
701    Returns a dict<part_name, part_class> of all available :class:`Part` classes
702    """
703    res = {}
704    _get_all_subclasses(Part, res)
705    return res
706
707# document stuff
708class DocumentStorage(FileSystemStorage):
709    """
710    File system storage which stores files with a specific name
711    """
712    def get_available_name(self, name):
713        """
714        Returns a path for a file *name*, the path always refers to a file
715        which do not exist.
716       
717        The path is computed as follow:
718            #. a directory which name is the last extension of *name*.
719               For example, it is :file:`gz` if *name* is :file:`a.tar.gz`.
720               If *name* does not have an extension, the directory is
721               :file:`no_ext/`.
722            #. a file name with 4 parts:
723                #. the md5 sum of *name*
724                #. a dash separator: ``-``
725                #. a random part with 3 characters in ``[a-z0-9]``
726                #. the extension, like :file:`.gz`
727           
728            For example, if *name* is :file:`my_file.tar.gz`,
729            a possible output is:
730
731                :file:`gz/c7bfe8d00ea6e7138215ebfafff187af-jj6.gz`
732
733            If *name* is :file:`my_file`, a possible output is:
734
735                :file:`no_ext/59c211e8fc0f14b21c78c87eafe1ab72-dhh`
736        """
737       
738        def rand():
739            r = ""
740            for i in xrange(3):
741                r += random.choice(string.ascii_lowercase + string.digits)
742            return r
743        basename = os.path.basename(name)
744        base, ext = os.path.splitext(basename)
745        ext2 = ext.lstrip(".").lower() or "no_ext"
746        md5 = hashlib.md5()
747        md5.update(basename)
748        md5_value = md5.hexdigest() + "-%s" + ext
749        path = os.path.join(ext2, md5_value % rand())
750        while os.path.exists(os.path.join(self.location, path)):
751            path = os.path.join(ext2, md5_value % rand())
752        return path
753
754#: :class:`DocumentStorage` instance which stores files in :const:`settings.DOCUMENTS_DIR`
755docfs = DocumentStorage(location=settings.DOCUMENTS_DIR)
756#: :class:`FileSystemStorage` instance which stores thumbnails in :const:`settings.THUMBNAILS_DIR`
757thumbnailfs = FileSystemStorage(location=settings.THUMBNAILS_DIR,
758        base_url=settings.THUMBNAILS_URL)
759
760class DocumentFile(models.Model):
761    """
762    Model which stores informations of a file link to a :class:`Document`
763   
764    :model attributes:
765        .. attribute:: filename
766           
767            original filename
768        .. attribute:: file
769           
770            file stored in :obj:`docfs`
771        .. attribute:: size
772           
773            size of the file in Byte
774        .. attribute:: locked
775
776            True if the file is locked
777        .. attribute:: locker
778           
779            :class:`~django.contrib.auth.models.User` who locked the file,
780            None, if the file is not locked
781        .. attribute document
782
783            :class:`Document` linked to the file
784    """
785    filename = models.CharField(max_length=200)
786    file = models.FileField(upload_to=".", storage=docfs)
787    size = models.PositiveIntegerField()
788    thumbnail = models.ImageField(upload_to=".", storage=thumbnailfs,
789                                 blank=True, null=True)
790    locked = models.BooleanField(default=lambda: False)
791    locker = models.ForeignKey(User, null=True, blank=True,
792                               default=lambda: None)
793    document = models.ForeignKey('Document')
794
795    def __unicode__(self):
796        return u"DocumentFile<%s, %s>" % (self.filename, self.document)
797
798class Document(PLMObject):
799    """
800    Model for documents
801    """
802
803    @property
804    def files(self):
805        "Queryset of all :class:`DocumentFile` linked to self"
806        return self.documentfile_set.all()
807
808    def is_promotable(self):
809        """
810        Returns True if the object is promotable. A documentt is promotable
811        if there is a next state in its lifecycle and if it has at least
812        one file and if none of its files are locked.
813        """
814        if not self._is_promotable():
815            return False
816        if not bool(self.files):
817            self._promotion_errors.append(_("This document has no files."))
818            return False
819        if bool(self.files.filter(locked=True)):
820            self._promotion_errors.append(_("Some files are locked."))
821            return False
822        return True
823
824    @property
825    def menu_items(self):
826        items = list(super(Document, self).menu_items)
827        items.insert(0, ugettext_noop("files"))
828        items.append(ugettext_noop("parts"))
829        return items
830
831    @property
832    def is_part(self):
833        return False
834   
835    @property
836    def is_document(self):
837        return True
838
839
840@memoize_noarg
841def get_all_documents():
842    u"""
843    Returns a dict<doc_name, doc_class> of all available :class:`Document` classes
844    """
845    res = {}
846    _get_all_subclasses(Document, res)
847    return res
848
849@memoize_noarg
850def get_all_plmobjects():
851    u"""
852    Returns a dict<name, class> of all available :class:`PLMObject` subclasses
853    """
854
855    res = {}
856    _get_all_subclasses(PLMObject, res)
857    res["Group"] = GroupInfo
858    del res["PLMObject"]
859    return res
860
861@memoize_noarg
862def get_all_users_and_plmobjects():
863    res = {}
864    _get_all_subclasses(User, res)
865    res.update(get_all_plmobjects())
866    return res
867
868@memoize_noarg
869def get_all_userprofiles_and_plmobjects():
870    res = {}
871    _get_all_subclasses(UserProfile, res)
872    res.update(get_all_plmobjects())
873    return res
874
875# history stuff
876class AbstractHistory(models.Model):
877    u"""
878    History model.
879    This model records all events related to :class:`PLMObject`
880
881    :model attributes:
882        .. attribute:: plmobject
883
884            :class:`PLMObject` of the event
885        .. attribute:: action
886
887            type of action (see :attr:`ACTIONS`)
888        .. attribute:: details
889       
890            type of action (see :attr:`ACTIONS`)
891        .. attribute:: date
892       
893            date of the event
894        .. attribute:: user
895       
896            :class:`~django.contrib.auth.models.User` who maded the event
897
898    :class attribute:
899    """
900    #: some actions available in the admin interface
901    ACTIONS = (
902        ("Create", "Create"),
903        ("Delete", "Delete"),
904        ("Modify", "Modify"),
905        ("Revise", "Revise"),
906        ("Promote", "Promote"),
907        ("Demote", "Demote"),
908    )
909   
910    class Meta:
911        abstract = True
912
913    action = models.CharField(max_length=50, choices=ACTIONS)
914    details = models.TextField()
915    date = models.DateTimeField(auto_now=True)
916    user = models.ForeignKey(User, related_name="%(class)s_user")
917
918    def __unicode__(self):
919        return "History<%s, %s, %s>" % (self.plmobject, self.date, self.action)
920
921    def get_day(self):
922        return datetime.date(self.date.year, self.date.month, self.date.day)
923
924class History(AbstractHistory):
925    plmobject = models.ForeignKey(PLMObject)
926
927class UserHistory(AbstractHistory):
928    plmobject = models.ForeignKey(User)
929
930class GroupHistory(AbstractHistory):
931    plmobject = models.ForeignKey(Group)
932
933# link stuff
934
935class Link(models.Model):
936    u"""
937    Abstract link base class.
938
939    This class represents a link between two :class:`PLMObject`
940   
941    :model attributes:
942        .. attribute:: ctime
943
944            date of creation of the link (automatically set)
945
946    :class attributes:
947        .. attribute:: ACTION_NAME
948
949            an identifier used to set :attr:`History.action` field
950    """
951
952    ctime = models.DateTimeField(auto_now_add=True)
953
954    ACTION_NAME = "Link"
955    class Meta:
956        abstract = True
957
958class RevisionLink(Link):
959    """
960    Link between two revisions of a :class:`PLMObject`
961   
962    :model attributes:
963        .. attribute:: old
964
965            old revision (a :class:`PLMObject`)
966        .. attribute:: new
967
968            new revision (a :class:`PLMObject`)
969    """
970   
971    ACTION_NAME = "Link : revision"
972    old = models.ForeignKey(PLMObject, related_name="%(class)s_old")   
973    new = models.ForeignKey(PLMObject, related_name="%(class)s_new")
974   
975    class Meta:
976        unique_together = ("old", "new")
977
978    def __unicode__(self):
979        return u"RevisionLink<%s, %s>" % (self.old, self.new)
980   
981
982class ParentChildLink(Link):
983    """
984    Link between two :class:`Part`: a parent and a child
985   
986    :model attributes:
987        .. attribute:: parent
988
989            a :class:`Part`
990        .. attribute:: child
991
992            a :class:`Part`
993        .. attribute:: quantity
994           
995            amount of child (a positive float)
996        .. attribute:: order
997           
998            positive integer
999        .. attribute:: end_time
1000           
1001            date of end of the link, None if the link is still alive
1002    """
1003
1004    ACTION_NAME = "Link : parent-child"
1005
1006    parent = models.ForeignKey(Part, related_name="%(class)s_parent")   
1007    child = models.ForeignKey(Part, related_name="%(class)s_child")   
1008    quantity = models.FloatField(default=lambda: 1)
1009    unit = models.CharField(max_length=4, choices=UNITS,
1010            default=lambda: DEFAULT_UNIT)
1011    order = models.PositiveSmallIntegerField(default=lambda: 1)
1012    end_time = models.DateTimeField(blank=True, null=True, default=lambda: None)
1013   
1014    class Meta:
1015        unique_together = ("parent", "child", "end_time")
1016
1017    def __unicode__(self):
1018        return u"ParentChildLink<%s, %s, %f, %d>" % (self.parent, self.child,
1019                                                     self.quantity, self.order)
1020    def get_shortened_unit(self):
1021        if self.unit == "-":
1022            return u""
1023        return self.get_unit_display()
1024
1025class DocumentPartLink(Link):
1026    """
1027    Link between a :class:`Part` and a :class:`Document`
1028   
1029    :model attributes:
1030        .. attribute:: part
1031
1032            a :class:`Part`
1033        .. attribute:: document
1034
1035            a :class:`Document`
1036    """
1037
1038    ACTION_NAME = "Link : document-part"
1039
1040    document = models.ForeignKey(Document, related_name="%(class)s_document")   
1041    part = models.ForeignKey(Part, related_name="%(class)s_part")   
1042
1043    class Meta:
1044        unique_together = ("document", "part")
1045
1046    def __unicode__(self):
1047        return u"DocumentPartLink<%s, %s>" % (self.document, self.part)
1048
1049
1050# abstraction stuff
1051ROLE_NOTIFIED = "notified"
1052ROLE_SIGN = "sign_"
1053ROLE_OWNER = "owner"
1054ROLE_SPONSOR = "sponsor"
1055
1056ROLES = [ROLE_OWNER, ROLE_NOTIFIED, ROLE_SPONSOR]
1057for i in range(10):
1058    level = level_to_sign_str(i)
1059    ROLES.append(level)
1060
1061class DelegationLink(Link):
1062    """
1063    Link between two :class:`~.django.contrib.auth.models.User` to delegate
1064    his rights (abstract class)
1065   
1066    :model attributes:
1067        .. attribute:: delegator
1068
1069            :class:`~django.contrib.auth.models.User` who gives his role
1070        .. attribute:: delegatee
1071
1072            :class:`~django.contrib.auth.models.User` who receives the role
1073        .. attribute:: role
1074           
1075            right that is delegated
1076    """
1077
1078    ACTION_NAME = "Link : delegation"
1079   
1080    delegator = models.ForeignKey(User, related_name="%(class)s_delegator")   
1081    delegatee = models.ForeignKey(User, related_name="%(class)s_delegatee")   
1082    role = models.CharField(max_length=30, choices=zip(ROLES, ROLES),
1083            db_index=True)
1084
1085    class Meta:
1086        unique_together = ("delegator", "delegatee", "role")
1087
1088    def __unicode__(self):
1089        return u"DelegationLink<%s, %s, %s>" % (self.delegator, self.delegatee,
1090                                                self.role)
1091   
1092    @classmethod
1093    def get_delegators(cls, user, role):
1094        """
1095        Returns the list of user's id of the delegators of *user* for the role
1096        *role*.
1097        """
1098        links = cls.objects.filter(role=role).values_list("delegatee", "delegator")
1099        gr = kjbuckets.kjGraph(tuple(links))
1100        return gr.reachable(user.id).items()
1101
1102
1103class PLMObjectUserLink(Link):
1104    """
1105    Link between a :class:`~.django.contrib.auth.models.User` and a
1106    :class:`PLMObject`
1107   
1108    :model attributes:
1109        .. attribute:: plmobject
1110
1111            a :class:`PLMObject`
1112        .. attribute:: user
1113
1114            a :class:`User`
1115        .. attribute:: role
1116           
1117            role of *user* for *plmobject* (like `owner` or `notified`)
1118    """
1119
1120    ACTION_NAME = "Link : PLMObject-user"
1121
1122    plmobject = models.ForeignKey(PLMObject, related_name="%(class)s_plmobject")   
1123    user = models.ForeignKey(User, related_name="%(class)s_user")   
1124    role = models.CharField(max_length=30, choices=zip(ROLES, ROLES),
1125            db_index=True)
1126
1127    class Meta:
1128        unique_together = ("plmobject", "user", "role")
1129        ordering = ["user", "role", "plmobject__type", "plmobject__reference",
1130                "plmobject__revision"]
1131
1132    def __unicode__(self):
1133        return u"PLMObjectUserLink<%s, %s, %s>" % (self.plmobject, self.user, self.role)
1134
1135
1136def _get_all_subclasses_with_level(base, lst, level):
1137    level = "=" + level
1138    if base.__name__ not in lst:
1139        lst.append((base.__name__,level[3:] + base.__name__))
1140    for part in base.__subclasses__():
1141        _get_all_subclasses_with_level(part, lst, level)
1142
1143@memoize_noarg
1144def get_all_plmobjects_with_level():
1145    u"""
1146    Returns a list<name, class> of all available :class:`PLMObject` subclasses
1147    with 1 or more "=>" depending on the level
1148    """
1149
1150    lst = []
1151    level=">"
1152    _get_all_subclasses_with_level(PLMObject, lst, level)
1153    if lst: del lst[0]
1154    lst.append(("Group", "Group"))
1155    return lst
1156
1157@memoize_noarg
1158def get_all_users_and_plmobjects_with_level():
1159    list_of_choices = list(get_all_plmobjects_with_level())
1160    level=">"
1161    _get_all_subclasses_with_level(User, list_of_choices, level)
1162    return list_of_choices
1163
1164
1165class Invitation(models.Model):
1166    PENDING = "p"
1167    ACCEPTED = "a"
1168    REFUSED = "r"
1169    STATES = ((PENDING, "Pending"),
1170              (ACCEPTED, "Accepted"),
1171              (REFUSED, "Refused"))
1172    group = models.ForeignKey(GroupInfo)
1173    owner = models.ForeignKey(User, related_name="%(class)s_inv_owner")
1174    guest = models.ForeignKey(User, related_name="%(class)s_inv_guest")
1175    state = models.CharField(max_length=1, choices=STATES, default=PENDING)
1176    ctime = models.DateTimeField(_("date of creation"), default=datetime.datetime.today,
1177                                 auto_now_add=False)
1178    validation_time = models.DateTimeField(_("date of validation"), null=True)
1179    guest_asked = models.BooleanField(_("True if guest created the invitation"))
1180    token = models.CharField(max_length=155, primary_key=True,
1181            default=lambda:str(random.getrandbits(512)))
1182   
1183   
1184# import_models should be the last function
1185
1186def import_models(force_reload=False):
1187    u"""
1188    Imports recursively all modules in directory *plmapp/customized_models*
1189    """
1190
1191    MODELS_DIR = "customized_models"
1192    IMPORT_ROOT = "openPLM.plmapp.%s" % MODELS_DIR
1193    if __name__ != "openPLM.plmapp.models":
1194        # this avoids to import models twice
1195        return
1196    if force_reload or not hasattr(import_models, "done"):
1197        import_models.done = True
1198        models_dir = os.path.join(os.path.split(__file__)[0], MODELS_DIR)
1199        # we browse recursively models_dir
1200        for root, dirs, files in os.walk(models_dir):
1201            # we only look at python files
1202            for module in sorted(fnmatch.filter(files, "*.py")):
1203                if module == "__init__.py":
1204                    # these files are empty
1205                    continue
1206                # import_name should respect the format
1207                # 'openPLM.plmapp.customized_models.{module_name}'
1208                module_name = os.path.splitext(os.path.basename(module))[0]
1209                import_dir = root.split(MODELS_DIR, 1)[-1].replace(os.path.sep, ".")
1210                import_name = "%s.%s.%s" % (IMPORT_ROOT, import_dir, module_name)
1211                import_name = import_name.replace("..", ".")
1212                try:
1213                    __import__(import_name, globals(), locals(), [], -1)
1214                except ImportError, exc:
1215                    print "Exception in import_models", module_name, exc
1216                except StandardError, exc:
1217                    print "Exception in import_models", module_name, type(exc), exc
1218import_models()
1219
Note: See TracBrowser for help on using the repository browser.