source: main/trunk/openPLM/plmapp/models.py @ 270

Revision 270, 29.8 KB checked in by pjoulaud, 9 years ago (diff)

Possibility to drag the graph in navigate function

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