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

Revision 595, 34.8 KB checked in by pcosquer, 8 years ago (diff)

3D branch: merge changes from trunk rev 594

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