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

Revision 595, 16.7 KB checked in by pcosquer, 9 years ago (diff)

3D branch: merge changes from trunk rev 594

Line 
1############################################################################
2# openPLM - open source PLM
3# Copyright 2010 Philippe Joulaud, Pierre Cosquer
4#
5# This file is part of openPLM.
6#
7#    openPLM is free software: you can redistribute it and/or modify
8#    it under the terms of the GNU General Public License as published by
9#    the Free Software Foundation, either version 3 of the License, or
10#    (at your option) any later version.
11#
12#    openPLM is distributed in the hope that it will be useful,
13#    but WITHOUT ANY WARRANTY; without even the implied warranty of
14#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15#    GNU General Public License for more details.
16#
17#    You should have received a copy of the GNU General Public License
18#    along with Foobar.  If not, see <http://www.gnu.org/licenses/>.
19#
20# Contact :
21#    Philippe Joulaud : ninoo.fr@gmail.com
22#    Pierre Cosquer : pierre.cosquer@insa-rennes.fr
23################################################################################
24
25"""
26"""
27
28import re
29
30from django.conf import settings
31from django.core.exceptions import ObjectDoesNotExist
32
33import openPLM.plmapp.models as models
34from openPLM.plmapp.exceptions import RevisionError, PermissionError,\
35    PromotionError
36from openPLM.plmapp.utils import level_to_sign_str
37from openPLM.plmapp.controllers.base import Controller, permission_required
38
39rx_bad_ref = re.compile(r"[?/#\n\t\r\f]|\.\.")
40class PLMObjectController(Controller):
41    u"""
42    Object used to manage a :class:`~plmapp.models.PLMObject` and store his
43    modification in an history
44   
45    :attributes:
46        .. attribute:: object
47
48            The :class:`.PLMObject` managed by the controller
49        .. attribute:: _user
50
51            :class:`~django.contrib.auth.models.User` who modifies ``object``
52
53    :param obj: managed object
54    :type obj: a subinstance of :class:`.PLMObject`
55    :param user: user who modifies *obj*
56    :type user: :class:`~django.contrib.auth.models.User`
57    """
58
59    HISTORY = models.History
60
61    @classmethod
62    def create(cls, reference, type, revision, user, data={}, block_mails=False,
63            no_index=False):
64        u"""
65        This method builds a new :class:`.PLMObject` of
66        type *class_* and return a :class:`PLMObjectController` associated to
67        the created object.
68
69        Raises :exc:`ValueError` if *reference*, *type* or *revision* are
70        empty. Raises :exc:`ValueError` if *type* is not valid.
71
72        :param reference: reference of the objet
73        :param type: type of the object
74        :param revision: revision of the object
75        :param user: user who creates/owns the object
76        :param data: a dict<key, value> with informations to add to the plmobject
77        :rtype: :class:`PLMObjectController`
78        """
79       
80        profile = user.get_profile()
81        if not (profile.is_contributor or profile.is_administrator):
82            raise PermissionError("%s is not a contributor" % user)
83        if not reference or not type or not revision:
84            raise ValueError("Empty value not permitted for reference/type/revision")
85        if rx_bad_ref.search(reference) or rx_bad_ref.search(revision):
86            raise ValueError("Reference or revision contains a '/' or a '..'")
87        try:
88            class_ = models.get_all_plmobjects()[type]
89        except KeyError:
90            raise ValueError("Incorrect type")
91        # create an object
92        obj = class_(reference=reference, type=type, revision=revision,
93                     owner=user, creator=user)
94        if no_index:
95            obj.no_index = True
96        if data:
97            for key, value in data.iteritems():
98                if key not in ["reference", "type", "revision"]:
99                    setattr(obj, key, value)
100        obj.state = models.get_default_state(obj.lifecycle)
101        obj.save()
102        res = cls(obj, user)
103        if block_mails:
104            res.block_mails()
105        # record creation in history
106        infos = {"type" : type, "reference" : reference, "revision" : revision}
107        infos.update(data)
108        details = u",".join(u"%s : %s" % (k, v) for k, v in infos.items())
109        res._save_histo("Create", details)
110        # add links
111        models.PLMObjectUserLink.objects.create(plmobject=obj, user=user, role="owner")
112        try:
113            l = models.DelegationLink.objects.get(delegatee=user,
114                    role=models.ROLE_SPONSOR)
115            sponsor = l.delegator
116            if sponsor.username == settings.COMPANY:
117                sponsor = user
118        except models.DelegationLink.DoesNotExist:
119            sponsor = user
120        for i in range(len(obj.lifecycle.to_states_list()) - 1):
121            models.PLMObjectUserLink.objects.create(plmobject=obj, user=sponsor,
122                                                    role=level_to_sign_str(i))
123        return res
124       
125    @classmethod
126    def create_from_form(cls, form, user, block_mails=False, no_index=False):
127        u"""
128        Creates a :class:`PLMObjectController` from *form* and associates *user*
129        as the creator/owner of the PLMObject.
130       
131        This method raises :exc:`ValueError` if *form* is invalid.
132
133        :param form: a django form associated to a model
134        :param user: user who creates/owns the object
135        :rtype: :class:`PLMObjectController`
136        """
137        if form.is_valid():
138            ref = form.cleaned_data["reference"]
139            type = form.Meta.model.__name__
140            rev = form.cleaned_data["revision"]
141            obj = cls.create(ref, type, rev, user, form.cleaned_data,
142                    block_mails, no_index)
143            return obj
144        else:
145            raise ValueError("form is invalid")
146   
147    def promote(self):
148        u"""
149        Promotes :attr:`object` in his lifecycle and writes his promotion in
150        the history
151       
152        :raise: :exc:`.PromotionError` if :attr:`object` is not promotable
153        :raise: :exc:`.PermissionError` if the use can not sign :attr:`object`
154        """
155        if self.object.is_promotable():
156            state = self.object.state
157            lifecycle = self.object.lifecycle
158            lcl = lifecycle.to_states_list()
159            self.check_permission(level_to_sign_str(lcl.index(state.name)))
160            try:
161                new_state = lcl.next_state(state.name)
162                self.object.state = models.State.objects.get_or_create(name=new_state)[0]
163                self.object.save()
164                details = "change state from %(first)s to %(second)s" % \
165                                     {"first" :state.name, "second" : new_state}
166                self._save_histo("Promote", details, roles=["sign_"])
167                if self.object.state == lifecycle.official_state:
168                    cie = models.User.objects.get(username=settings.COMPANY)
169                    self.set_owner(cie)
170            except IndexError:
171                # FIXME raises it ?
172                pass
173        else:
174            raise PromotionError()
175
176    def demote(self):
177        u"""
178        Demotes :attr:`object` in his lifecycle and writes his demotion in the
179        history
180       
181        :raise: :exc:`.PermissionError` if the use can not sign :attr:`object`
182        """
183        if not self.is_editable:
184            raise PromotionError()
185        state = self.object.state
186        lifecycle = self.object.lifecycle
187        lcl = lifecycle.to_states_list()
188        try:
189            new_state = lcl.previous_state(state.name)
190            self.check_permission(level_to_sign_str(lcl.index(new_state)))
191            self.object.state = models.State.objects.get_or_create(name=new_state)[0]
192            self.object.save()
193            details = "change state from %(first)s to %(second)s" % \
194                    {"first" :state.name, "second" : new_state}
195            self._save_histo("Demote", details, roles=["sign_"])
196        except IndexError:
197            # FIXME raises it ?
198            pass
199
200    def _save_histo(self, action, details, blacklist=(), roles=(), users=()):
201        """
202        Records *action* with details *details* made by :attr:`_user` in
203        on :attr:`object` in the histories table.
204
205        *blacklist*, if given, should be a list of email whose no mail should
206        be sent (empty by default).
207
208        A mail is sent to all notified users. Moreover, more roles can be
209        notified by settings the *roles" argument.
210        """
211        roles = ["notified"] + list(roles)
212        super(PLMObjectController, self)._save_histo(action, details,
213                blacklist, roles, users)
214
215    def has_permission(self, role):
216        users = [self._user.id]
217        users.extend(models.DelegationLink.get_delegators(self._user, role))
218        qset = self.plmobjectuserlink_plmobject.filter(user__in=users,
219                                                          role=role)
220        return bool(qset)
221
222    def check_editable(self):
223        """
224        Raises a :exc:`.PermissionError` if :attr:`object` is not editable.
225        """
226        if not self.object.is_editable:
227            raise PermissionError("The object is not editable")
228
229    @permission_required(role="owner")
230    def revise(self, new_revision):
231        u"""
232        Makes a new revision : duplicates :attr:`object`. The duplicated
233        object's revision is *new_revision*.
234
235        Returns a controller of the new object.
236        """
237       
238        if not new_revision or new_revision == self.revision or \
239           rx_bad_ref.search(new_revision):
240            raise RevisionError("Bad value for new_revision")
241        if models.RevisionLink.objects.filter(old=self.object.pk):
242            raise RevisionError("a revision already exists for %s" % self.object)
243        data = {}
244        fields = self.get_modification_fields() + self.get_creation_fields()
245        for attr in fields:
246            if attr not in ("reference", "type", "revision"):
247                data[attr] = getattr(self.object, attr)
248        data["state"] = models.get_default_state(self.lifecycle)
249        new_controller = self.create(self.reference, self.type, new_revision,
250                                     self._user, data)
251        details = "old : %s, new : %s" % (self.object, new_controller.object)
252        self._save_histo(models.RevisionLink.ACTION_NAME, details)
253        models.RevisionLink.objects.create(old=self.object, new=new_controller.object)
254        return new_controller
255
256    def is_revisable(self, check_user=True):
257        """
258        Returns True if :attr:`object` is revisable : if :meth:`revise` can be
259        called safely.
260
261        If *check_user* is True (the default), it also checks if :attr:`_user` is
262        the *owner* of :attr:`object`.
263        """
264        # objects.get fails if a link does not exist
265        # we can revise if any links exist
266        try:
267            models.RevisionLink.objects.get(old=self.object.pk)
268            return False
269        except ObjectDoesNotExist:
270            return self.check_permission("owner", False)
271   
272    def get_previous_revisions(self):
273        try:
274            link = models.RevisionLink.objects.get(new=self.object.pk)
275            controller = type(self)(link.old, self._user)
276            return controller.get_previous_revisions() + [link.old]
277        except ObjectDoesNotExist:
278            return []
279
280    def get_next_revisions(self):
281        try:
282            link = models.RevisionLink.objects.get(old=self.object.pk)
283            controller = type(self)(link.new, self._user)
284            return [link.new] + controller.get_next_revisions()
285        except ObjectDoesNotExist:
286            return []
287
288    def get_all_revisions(self):
289        """
290        Returns a list of all revisions, ordered from less recent to most recent
291       
292        :rtype: list of :class:`.PLMObject`
293        """
294        return self.get_previous_revisions() + [self.object] +\
295               self.get_next_revisions()
296
297    def set_owner(self, new_owner):
298        """
299        Sets *new_owner* as current owner.
300       
301        :param new_owner: the new owner
302        :type new_owner: :class:`~django.contrib.auth.models.User`
303        :raise: :exc:`.PermissionError` if *new_owner* is not a contributor
304        """
305
306        self.check_contributor(new_owner)
307        links = models.PLMObjectUserLink.objects.filter(plmobject=self.object,
308                role="owner")
309        for link in links:
310            link.delete()
311        link = models.PLMObjectUserLink.objects.get_or_create(user=self.owner,
312               plmobject=self.object, role="owner")[0]
313        self.owner = new_owner
314        link.user = new_owner
315        link.save()
316        self.save()
317        # we do not need to write this event in an history since save() has
318        # already done it
319
320    def add_notified(self, new_notified):
321        """
322        Adds *new_notified* to the list of users notified when :attr:`object`
323        changes.
324       
325        :param new_notified: the new user who would be notified
326        :type new_notified: :class:`~django.contrib.auth.models.User`
327        :raise: :exc:`IntegrityError` if *new_notified* is already notified
328            when :attr:`object` changes
329        """
330        if new_notified != self._user:
331            self.check_permission("owner")
332        models.PLMObjectUserLink.objects.create(plmobject=self.object,
333            user=new_notified, role="notified")
334        details = "user: %s" % new_notified
335        self._save_histo("New notified", details)
336
337    def remove_notified(self, notified):
338        """
339        Removes *notified* to the list of users notified when :attr:`object`
340        changes.
341       
342        :param notified: the user who would be no more notified
343        :type notified: :class:`~django.contrib.auth.models.User`
344        :raise: :exc:`ObjectDoesNotExist` if *notified* is not notified
345            when :attr:`object` changes
346        """
347       
348        if notified != self._user:
349            self.check_permission("owner")
350        link = models.PLMObjectUserLink.objects.get(plmobject=self.object,
351                user=notified, role="notified")
352        link.delete()
353        details = "user: %s" % notified
354        self._save_histo("Notified removed", details)
355
356    def set_signer(self, signer, role):
357        """
358        Sets *signer* as current signer for *role*. *role* must be a valid
359        sign role (see :func:`.level_to_sign_str` to get a role from a
360        sign level (int))
361       
362        :param signer: the new signer
363        :type signer: :class:`~django.contrib.auth.models.User`
364        :param str role: the sign role
365        :raise: :exc:`.PermissionError` if *signer* is not a contributor
366        :raise: :exc:`.PermissionError` if *role* is invalid (level to high)
367        """
368        self.check_contributor(signer)
369        # remove old signer
370        old_signer = None
371        try:
372            link = models.PLMObjectUserLink.objects.get(plmobject=self.object,
373               role=role)
374            old_signer = link.user
375            link.delete()
376        except ObjectDoesNotExist:
377            pass
378        # check if the role is valid
379        max_level = len(self.lifecycle.to_states_list()) - 1
380        level = int(re.search(r"\d+", role).group(0))
381        if level > max_level:
382            # TODO better exception ?
383            raise PermissionError("bad role")
384        # add new signer
385        models.PLMObjectUserLink.objects.create(plmobject=self.object,
386                                                user=signer, role=role)
387        details = "signer: %s, level : %d" % (signer, level)
388        if old_signer:
389            details += ", old signer: %s" % old_signer
390        self._save_histo("New signer", details)
391
392    def set_role(self, user, role):
393        """
394        Sets role *role* (like `owner` or `notified`) for *user*
395
396        .. note::
397            If *role* is `owner` or a sign role, the old user who had
398            this role will lose it.
399
400            If *role* is notified, others roles are preserved.
401       
402        :raise: :exc:`ValueError` if *role* is invalid
403        :raise: :exc:`.PermissionError` if *user* is not allowed to has role
404            *role*
405        """
406        if role == "owner":
407            self.set_owner(user)
408        elif role == "notified":
409            self.add_notified(user)
410        elif role.startswith("sign"):
411            self.set_signer(user, role)
412        else:
413            raise ValueError("bad value for role")
414
415    def check_permission(self, role, raise_=True):
416        if not bool(self.group.user_set.filter(id=self._user.id)):
417            if raise_:
418                raise PermissionError("action not allowed for %s" % self._user)
419            else:
420                return False
421        return super(PLMObjectController, self).check_permission(role, raise_)
422
423    def check_readable(self, raise_=True):
424        if not self.is_editable:
425            return True
426        if bool(self.group.user_set.filter(id=self._user.id)):
427            return True
428        if raise_:
429            raise PermissionError("You can not see this object.")
430        return False
431
Note: See TracBrowser for help on using the repository browser.