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

Revision 319, 29.8 KB checked in by pcosquer, 8 years ago (diff)

update documentation

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