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

Revision 662, 16.8 KB checked in by pcosquer, 9 years ago (diff)

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

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(obj.lifecycle.nb_states - 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        if role == models.ROLE_OWNER and self.owner == self._user:
217            return True
218        if self.plmobjectuserlink_plmobject.filter(user=self._user, role=role).exists():
219            return True
220
221        users = models.DelegationLink.get_delegators(self._user, role)
222        qset = self.plmobjectuserlink_plmobject.filter(user__in=users,
223                                                          role=role)
224        return bool(qset)
225
226    def check_editable(self):
227        """
228        Raises a :exc:`.PermissionError` if :attr:`object` is not editable.
229        """
230        if not self.object.is_editable:
231            raise PermissionError("The object is not editable")
232
233    def revise(self, new_revision):
234        u"""
235        Makes a new revision: duplicates :attr:`object`. The duplicated
236        object's revision is *new_revision*.
237
238        Returns a controller of the new object.
239        """
240        self.check_readable()
241        if not new_revision or new_revision == self.revision or \
242           rx_bad_ref.search(new_revision):
243            raise RevisionError("Bad value for new_revision")
244        if models.RevisionLink.objects.filter(old=self.object.pk):
245            raise RevisionError("a revision already exists for %s" % self.object)
246        data = {}
247        fields = self.get_modification_fields() + self.get_creation_fields()
248        for attr in fields:
249            if attr not in ("reference", "type", "revision"):
250                data[attr] = getattr(self.object, attr)
251        data["state"] = models.get_default_state(self.lifecycle)
252        new_controller = self.create(self.reference, self.type, new_revision,
253                                     self._user, data)
254        details = "old : %s, new : %s" % (self.object, new_controller.object)
255        self._save_histo(models.RevisionLink.ACTION_NAME, details)
256        models.RevisionLink.objects.create(old=self.object, new=new_controller.object)
257        return new_controller
258
259    def is_revisable(self, check_user=True):
260        """
261        Returns True if :attr:`object` is revisable: if :meth:`revise` can be
262        called safely.
263
264        If *check_user* is True (the default), it also checks if :attr:`_user` can
265        see the objects.
266        """
267        # objects.get fails if a link does not exist
268        # we can revise if any links exist
269        try:
270            models.RevisionLink.objects.get(old=self.object.pk)
271            return False
272        except ObjectDoesNotExist:
273            return self.check_readable(False)
274   
275    def get_previous_revisions(self):
276        try:
277            link = models.RevisionLink.objects.get(new=self.object.pk)
278            controller = type(self)(link.old, self._user)
279            return controller.get_previous_revisions() + [link.old]
280        except ObjectDoesNotExist:
281            return []
282
283    def get_next_revisions(self):
284        try:
285            link = models.RevisionLink.objects.get(old=self.object.pk)
286            controller = type(self)(link.new, self._user)
287            return [link.new] + controller.get_next_revisions()
288        except ObjectDoesNotExist:
289            return []
290
291    def get_all_revisions(self):
292        """
293        Returns a list of all revisions, ordered from less recent to most recent
294       
295        :rtype: list of :class:`.PLMObject`
296        """
297        return self.get_previous_revisions() + [self.object] +\
298               self.get_next_revisions()
299
300    def set_owner(self, new_owner):
301        """
302        Sets *new_owner* as current owner.
303       
304        :param new_owner: the new owner
305        :type new_owner: :class:`~django.contrib.auth.models.User`
306        :raise: :exc:`.PermissionError` if *new_owner* is not a contributor
307        """
308
309        self.check_contributor(new_owner)
310        links = models.PLMObjectUserLink.objects.filter(plmobject=self.object,
311                role="owner")
312        for link in links:
313            link.delete()
314        link = models.PLMObjectUserLink.objects.get_or_create(user=self.owner,
315               plmobject=self.object, role="owner")[0]
316        self.owner = new_owner
317        link.user = new_owner
318        link.save()
319        self.save()
320        # we do not need to write this event in an history since save() has
321        # already done it
322
323    def add_notified(self, new_notified):
324        """
325        Adds *new_notified* to the list of users notified when :attr:`object`
326        changes.
327       
328        :param new_notified: the new user who would be notified
329        :type new_notified: :class:`~django.contrib.auth.models.User`
330        :raise: :exc:`IntegrityError` if *new_notified* is already notified
331            when :attr:`object` changes
332        """
333        if new_notified != self._user:
334            self.check_permission("owner")
335        models.PLMObjectUserLink.objects.create(plmobject=self.object,
336            user=new_notified, role="notified")
337        details = "user: %s" % new_notified
338        self._save_histo("New notified", details)
339
340    def remove_notified(self, notified):
341        """
342        Removes *notified* to the list of users notified when :attr:`object`
343        changes.
344       
345        :param notified: the user who would be no more notified
346        :type notified: :class:`~django.contrib.auth.models.User`
347        :raise: :exc:`ObjectDoesNotExist` if *notified* is not notified
348            when :attr:`object` changes
349        """
350       
351        if notified != self._user:
352            self.check_permission("owner")
353        link = models.PLMObjectUserLink.objects.get(plmobject=self.object,
354                user=notified, role="notified")
355        link.delete()
356        details = "user: %s" % notified
357        self._save_histo("Notified removed", details)
358
359    def set_signer(self, signer, role):
360        """
361        Sets *signer* as current signer for *role*. *role* must be a valid
362        sign role (see :func:`.level_to_sign_str` to get a role from a
363        sign level (int))
364       
365        :param signer: the new signer
366        :type signer: :class:`~django.contrib.auth.models.User`
367        :param str role: the sign role
368        :raise: :exc:`.PermissionError` if *signer* is not a contributor
369        :raise: :exc:`.PermissionError` if *role* is invalid (level to high)
370        """
371        self.check_contributor(signer)
372        # remove old signer
373        old_signer = None
374        try:
375            link = models.PLMObjectUserLink.objects.get(plmobject=self.object,
376               role=role)
377            old_signer = link.user
378            link.delete()
379        except ObjectDoesNotExist:
380            pass
381        # check if the role is valid
382        max_level = self.lifecycle.nb_states - 1
383        level = int(re.search(r"\d+", role).group(0))
384        if level > max_level:
385            # TODO better exception ?
386            raise PermissionError("bad role")
387        # add new signer
388        models.PLMObjectUserLink.objects.create(plmobject=self.object,
389                                                user=signer, role=role)
390        details = "signer: %s, level : %d" % (signer, level)
391        if old_signer:
392            details += ", old signer: %s" % old_signer
393        self._save_histo("New signer", details)
394
395    def set_role(self, user, role):
396        """
397        Sets role *role* (like `owner` or `notified`) for *user*
398
399        .. note::
400            If *role* is `owner` or a sign role, the old user who had
401            this role will lose it.
402
403            If *role* is notified, others roles are preserved.
404       
405        :raise: :exc:`ValueError` if *role* is invalid
406        :raise: :exc:`.PermissionError` if *user* is not allowed to has role
407            *role*
408        """
409        if role == "owner":
410            self.set_owner(user)
411        elif role == "notified":
412            self.add_notified(user)
413        elif role.startswith("sign"):
414            self.set_signer(user, role)
415        else:
416            raise ValueError("bad value for role")
417
418    def check_permission(self, role, raise_=True):
419        if not bool(self.group.user_set.filter(id=self._user.id)):
420            if raise_:
421                raise PermissionError("action not allowed for %s" % self._user)
422            else:
423                return False
424        return super(PLMObjectController, self).check_permission(role, raise_)
425
426    def check_readable(self, raise_=True):
427        if not self.is_editable:
428            return True
429        if bool(self.group.user_set.filter(id=self._user.id)):
430            return True
431        if raise_:
432            raise PermissionError("You can not see this object.")
433        return False
434
Note: See TracBrowser for help on using the repository browser.