source: main/trunk/openPLM/plmapp/controllers/document.py @ 1543

Revision 1543, 23.4 KB checked in by pcosquer, 8 years ago (diff)

thumbnails: add stuff to restrict access to authenticated users
see the admin doc for more details
a little migration of plmapp is required (it just creates some symlinks, it does not change the database schema)

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 openPLM.  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 os
29import shutil
30
31import Image
32from django.conf import settings
33
34import openPLM.plmapp.models as models
35from openPLM.plmapp.exceptions import LockError, UnlockError, DeleteFileError, PermissionError
36from openPLM.plmapp.controllers.plmobject import PLMObjectController
37from openPLM.plmapp.controllers.base import get_controller
38from openPLM.plmapp.thumbnailers import generate_thumbnail
39from openPLM.plmapp.fileformats import native_to_standards
40
41
42class DocumentController(PLMObjectController):
43    """
44    A :class:`.PLMObjectController` which manages
45    :class:`.Document`
46   
47    It provides methods to add or delete files, (un)lock them and attach a
48    :class:`.Document` to a :class:`.Part`.
49    """
50   
51    def has_standard_related_locked(self, new_filename):
52        """
53        Returns True if :const:`settings.ENABLE_NATIVE_FILE_MANAGEMENT` is True
54        and exists a document that contains a standard locked file related to the
55        file we want to add.
56
57        We use it to avoid to add a native file while a related standard locked
58        file is present in the document.
59         
60        :param new_filename: name of the added file
61        """   
62        if getattr(settings, 'ENABLE_NATIVE_FILE_MANAGEMENT', False):
63            name, ext = os.path.splitext(new_filename)
64            ext = ext.lower()
65            doc_files = self.files.filter(locked=True)
66            for doc in doc_files:
67                standard, standard_ext = os.path.splitext(doc.filename)           
68                if standard == name and standard_ext.lower() in native_to_standards[ext]:
69                    return True
70        return False
71           
72    def lock(self, doc_file):
73        """
74        Lock *doc_file* so that it can not be modified or deleted
75        if *doc_file* has a native related file this will be deprecated
76         
77        :exceptions raised:
78            * :exc:`ValueError` if *doc_file*.document is not self.object
79            * :exc:`.PermissionError` if :attr:`_user` is not the owner of
80              :attr:`object`
81            * :exc:`.PermissionError` if :attr:`object` is not editable.
82            * :exc:`.LockError` if *doc_file* is already locked
83            * :exc:`ValueError` if *doc_file* has a native related file locked
84
85        :param doc_file:
86        :type doc_file: :class:`.DocumentFile`
87        """
88        self.check_permission("owner")
89        self.check_editable()
90        if doc_file.document.pk != self.object.pk:
91            raise ValueError("Bad file's document")
92        if not doc_file.checkout_valid:
93            raise LockError("Check-out impossible, native related file is locked")
94        if doc_file.deprecated:
95            raise LockError("Check-out impossible,  file is deprecated") 
96        if not doc_file.locked:
97            doc_file.locked = True
98            doc_file.locker = self._user
99            doc_file.save()
100            self._save_histo("Locked",
101                             "%s locked by %s" % (doc_file.filename, self._user))
102                             
103            doc_to_deprecated=doc_file.native_related
104            if doc_to_deprecated:
105                doc_to_deprecated.deprecated = True
106                doc_to_deprecated.save()
107                self._save_histo("Deprecated",
108                                 "file : %s" % doc_to_deprecated.filename)                 
109        else:
110            raise LockError("File already locked")
111
112    def unlock(self, doc_file):
113        """
114        Unlock *doc_file* so that it can be modified or deleted
115       
116        :exceptions raised:
117            * :exc:`ValueError` if *doc_file*.document is not self.object
118            * :exc:`plmapp.exceptions.UnlockError` if *doc_file* is already
119              unlocked or *doc_file.locker* is not the current user
120
121        :param doc_file:
122        :type doc_file: :class:`.DocumentFile`
123        """
124
125        if doc_file.document.pk != self.object.pk:
126            raise ValueError("Bad file's document")
127        if not doc_file.locked:
128            raise UnlockError("File already unlocked")
129        if doc_file.locker != self._user:
130            raise UnlockError("Bad user")
131        doc_file.locked = False
132        doc_file.locker = None
133        doc_file.save()
134        self._save_histo("Unlocked",
135                         "%s unlocked by %s" % (doc_file.filename, self._user))
136
137       
138    def add_file(self, f, update_attributes=True, thumbnail=True):
139        """
140        Adds file *f* to the document. *f* should be a :class:`~django.core.files.File`
141        with an attribute *name* (like an :class:`UploadedFile`).
142
143        If *update_attributes* is True (the default), :meth:`handle_added_file`
144        will be called with *f* as parameter.
145
146        :return: the :class:`.DocumentFile` created.
147        :raises: :exc:`.PermissionError` if :attr:`_user` is not the owner of
148              :attr:`object`
149        :raises: :exc:`.PermissionError` if :attr:`object` is not editable.
150        :raises: :exc:`ValueError` if the file size is superior to
151                 :attr:`settings.MAX_FILE_SIZE`
152        :raises: :exc:`ValueError` if we try to add a native file while a relate standar file locked is present in the Document
153        """   
154        self.check_permission("owner")
155        self.check_editable()
156
157        if settings.MAX_FILE_SIZE != -1 and f.size > settings.MAX_FILE_SIZE:
158            raise ValueError("File too big, max size : %d bytes" % settings.MAX_FILE_SIZE)
159
160        f.name = f.name.encode("utf-8")
161        if self.has_standard_related_locked(f.name):
162            raise ValueError("Native file has a standard related locked file.")
163
164        f.seek(0, os.SEEK_END)
165        size = f.tell()
166        f.seek(0)
167        doc_file = models.DocumentFile.objects.create(filename=f.name, size=size,
168                        file=models.docfs.save(f.name,f), document=self.object)
169        self.save(False)
170        # set read only file
171        os.chmod(doc_file.file.path, 0400)
172        self._save_histo("File added", "file : %s" % f.name)
173        if update_attributes:
174            self.handle_added_file(doc_file)
175        if thumbnail:
176           generate_thumbnail.delay(doc_file.id)
177        return doc_file
178
179    def add_thumbnail(self, doc_file, thumbnail_file):
180        """
181        Sets *thumnail_file* as the thumbnail of *doc_file*. *thumbnail_file*
182        should be a :class:`~django.core.files.File` with an attribute *name*
183        (like an :class:`UploadedFile`).
184       
185        :exceptions raised:
186            * :exc:`ValueError` if *doc_file*.document is not self.object
187            * :exc:`ValueError` if the file size is superior to
188              :attr:`settings.MAX_FILE_SIZE`
189            * :exc:`.PermissionError` if :attr:`_user` is not the owner of
190              :attr:`object`
191            * :exc:`.PermissionError` if :attr:`object` is not editable.
192        """
193        self.check_permission("owner")
194        self.check_editable()
195        if doc_file.document.pk != self.object.pk:
196            raise ValueError("Bad file's document")
197        if settings.MAX_FILE_SIZE != -1 and thumbnail_file.size > settings.MAX_FILE_SIZE:
198            raise ValueError("File too big, max size : %d bytes" % settings.MAX_FILE_SIZE)
199        if doc_file.deprecated:
200            raise ValueError("File is deprecated") 
201        basename = os.path.basename(thumbnail_file.name)
202        name = "%d%s" % (doc_file.id, os.path.splitext(basename)[1])
203        if doc_file.thumbnail:
204            doc_file.thumbnail.delete(save=False)
205        doc_file.thumbnail = models.thumbnailfs.save(name, thumbnail_file)
206        doc_file.save()
207        image = Image.open(doc_file.thumbnail.path)
208        image.thumbnail((150, 150), Image.ANTIALIAS)
209        image.save(doc_file.thumbnail.path)
210
211    def delete_file(self, doc_file):
212        """
213        Deletes *doc_file*, the file attached to *doc_file* is physically
214        removed.
215
216        :exceptions raised:
217            * :exc:`ValueError` if *doc_file*.document is not self.object
218            * :exc:`plmapp.exceptions.DeleteFileError` if *doc_file* is
219              locked
220            * :exc:`.PermissionError` if :attr:`_user` is not the owner of
221              :attr:`object`
222            * :exc:`.PermissionError` if :attr:`object` is not editable.
223
224        :param doc_file: the file to be deleted
225        :type doc_file: :class:`.DocumentFile`
226        """
227
228        self.check_permission("owner")
229        self.check_editable()
230        if doc_file.document.pk != self.object.pk:
231            raise ValueError("Bad file's document")
232        if doc_file.locked:
233            raise DeleteFileError("File is locked")
234        if doc_file.deprecated:
235            raise ValueError("File is deprecated") 
236        path = os.path.realpath(doc_file.file.path)
237        if not path.startswith(settings.DOCUMENTS_DIR):
238            raise DeleteFileError("Bad path : %s" % path)
239        filename = doc_file.filename
240        if getattr(settings, "KEEP_ALL_FILES", False):
241            doc_file.deprecated = True
242            doc_file.save()
243        else:
244            os.chmod(path, 0700)
245            os.remove(path)
246            if doc_file.thumbnail:
247                doc_file.thumbnail.delete(save=False)
248            doc_file.delete()
249        self._save_histo("File deleted", "file : %s" % filename)
250
251    def handle_added_file(self, doc_file):
252        """
253        Method called when adding a file (method :meth:`add_file`) with
254        *updates_attributes* set to True.
255
256        This method may be overridden to updates attributes with data from
257        *doc_file*. The default implementation does nothing.
258       
259        :param doc_file:
260        :type doc_file: :class:`.DocumentFile`
261        """
262        pass
263
264    def attach_to_part(self, part):
265        """
266        Links *part* (a :class:`.Part`) with
267        :attr:`~PLMObjectController.object`.
268        """
269
270        self.check_attach_part(part)       
271        if isinstance(part, PLMObjectController):
272            part = part.object
273        self.documentpartlink_document.create(part=part)
274        self._save_histo(models.DocumentPartLink.ACTION_NAME,
275                         "Part : %s - Document : %s" % (part, self.object))
276
277    def detach_part(self, part):
278        """
279        Deletes link between *part* (a :class:`.Part`) and
280        :attr:`~PLMObjectController.object`.
281        """
282       
283        self.check_attach_part(part, True)
284        if isinstance(part, PLMObjectController):
285            part = part.object
286        link = self.documentpartlink_document.get(part=part)
287        link.delete()
288        self._save_histo(models.DocumentPartLink.ACTION_NAME + " - delete",
289                         "Part : %s - Document : %s" % (part, self.object))
290
291    def get_attached_parts(self):
292        """
293        Returns all parts attached to
294        :attr:`~PLMObjectController.object`.
295        """
296        return self.object.documentpartlink_document.all()
297   
298    def get_detachable_parts(self):
299        """
300        Returns all attached parts the user can detach.
301        """
302        links = []
303        for link in self.get_attached_parts().select_related("parts"):
304            part = link.part
305            if self.can_detach_part(part):
306                links.append(link.id)
307        return self.documentpartlink_document.filter(id__in=links)
308   
309    def is_part_attached(self, part):
310        """
311        Returns True if *part* is attached to the current document.
312        """
313
314        if isinstance(part, PLMObjectController):
315            part = part.object
316        return self.documentpartlink_document.filter(part=part).exists()
317
318    def check_attach_part(self, part, detach=False):
319        if not (hasattr(part, "is_part") and part.is_part):
320            raise TypeError("%s is not a part" % part)
321        if not isinstance(part, PLMObjectController):
322            part = get_controller(part.type)(part, self._user)
323        part.check_attach_document(self, detach)
324       
325    def can_attach_part(self, part):
326        """
327        Returns True if *part* can be attached to the current document.
328        """
329        can_attach = False
330        try:
331            self.check_attach_part(part)
332            can_attach = True
333        except StandardError:
334            pass
335        return can_attach
336       
337    def can_detach_part(self, part):
338        """
339        Returns True if *part* can be detached.
340        """
341        can_detach = False
342        try:
343            self.check_attach_part(part, True)
344            can_detach = True
345        except StandardError:
346            pass
347        return can_detach
348
349    def get_suggested_parts(self):
350        """
351        Returns a QuerySet of parts a user may want to attach to
352        a future revision.
353        """
354        attached_parts = self.get_attached_parts().select_related("part",
355                "part__state", "part__lifecycle").only("part")
356        parts = []
357        for link in attached_parts:
358            part = link.part
359            try:
360                new = models.RevisionLink.objects.get(old=part).new
361                if new.is_draft:
362                    parts.append(new)
363                while models.RevisionLink.objects.filter(old=new).exists():
364                    new = models.RevisionLink.objects.get(old=new).new
365                    if new.is_draft:
366                        parts.append(new)
367            except models.RevisionLink.DoesNotExist:
368                if not part.is_deprecated:
369                    parts.append(part)
370        qs = models.Part.objects.filter(id__in=(p.id for p in parts))
371        qs = qs.select_related('type', 'reference', 'revision', 'name')
372        return qs
373
374    def revise(self, new_revision, selected_parts=()):
375        # same as PLMObjectController + duplicate files (and their thumbnails)
376        rev = super(DocumentController, self).revise(new_revision)
377        for doc_file in self.object.files.all():
378            filename = doc_file.filename
379            path = models.docfs.get_available_name(filename)
380            shutil.copy(doc_file.file.path, models.docfs.path(path))
381            new_doc = models.DocumentFile.objects.create(file=path,
382                filename=filename, size=doc_file.size, document=rev.object)
383            new_doc.thumbnail = doc_file.thumbnail
384            if doc_file.thumbnail:
385                ext = os.path.splitext(doc_file.thumbnail.path)[1]
386                thumb = "%d%s" %(new_doc.id, ext)
387                dirname = os.path.dirname(doc_file.thumbnail.path)
388                thumb_path = os.path.join(dirname, thumb)
389                shutil.copy(doc_file.thumbnail.path, thumb_path)
390                new_doc.thumbnail = os.path.basename(thumb_path)
391            new_doc.locked = False
392            new_doc.locker = None
393            new_doc.save()
394        # attach the given parts
395        for part in selected_parts:
396            rev.documentpartlink_document.create(part=part)
397
398        return rev
399
400    def checkin(self, doc_file, new_file, update_attributes=True,
401            thumbnail=True):
402        """
403        Updates *doc_file* with data from *new_file*. *doc_file*.thumbnail
404        is deleted if it is present.
405       
406        :exceptions raised:
407            * :exc:`ValueError` if *doc_file*.document is not self.object
408            * :exc:`ValueError` if the file size is superior to
409              :attr:`settings.MAX_FILE_SIZE`
410            * :exc:`plmapp.exceptions.UnlockError` if *doc_file* is locked
411              but *doc_file.locker* is not the current user
412            * :exc:`.PermissionError` if :attr:`_user` is not the owner of
413              :attr:`object`
414            * :exc:`.PermissionError` if :attr:`object` is not editable.
415
416        :param doc_file:
417        :type doc_file: :class:`.DocumentFile`
418        :param new_file: file with new data, same parameter as *f*
419                         in :meth:`add_file`
420        :param update_attributes: True if :meth:`handle_added_file` should be
421                                  called
422        """
423        self.check_permission("owner")
424        self.check_editable()
425        if doc_file.document.pk != self.object.pk:
426            raise ValueError("Bad file's document")
427        if doc_file.filename != new_file.name:
428            raise ValueError("Checkin document and document already in plm have different names")
429        if settings.MAX_FILE_SIZE != -1 and new_file.size > settings.MAX_FILE_SIZE:
430            raise ValueError("File too big, max size : %d bytes" % settings.MAX_FILE_SIZE)
431        if doc_file.deprecated:
432            raise ValueError("File is deprecated") 
433           
434        if doc_file.locked:
435            self.unlock(doc_file)   
436        if getattr(settings, "KEEP_ALL_FILES", False):
437            deprecated_df = models.DocumentFile.objects.create(
438                    document=self.object,
439                    deprecated=True,
440                    size=doc_file.size,
441                    filename=doc_file.filename,
442                    file=models.docfs.save(new_file.name, doc_file.file),
443                    thumbnail=doc_file.thumbnail)
444        else:
445            os.chmod(doc_file.file.path, 0700)
446            os.remove(doc_file.file.path)
447            if doc_file.thumbnail:
448                doc_file.thumbnail.delete(save=False)
449        doc_file.file = models.docfs.save(new_file.name, new_file)
450        doc_file.size = new_file.size
451        os.chmod(doc_file.file.path, 0400)
452        doc_file.save()
453        self._save_histo("Check-in", doc_file.filename)
454        if update_attributes:
455            self.handle_added_file(doc_file)
456        if thumbnail:
457            generate_thumbnail.delay(doc_file.id)
458           
459    def update_rel_part(self, formset):
460        u"""
461        Updates related part informations with data from *formset*
462       
463        :param formset:
464        :type formset: a modelfactory_formset of
465                        :class:`~plmapp.forms.ModifyRelPartForm`
466        """
467        parts = set()
468        if formset.is_valid():
469            for form in formset.forms:
470                document = form.cleaned_data["document"]
471                if document.pk != self.document.pk:
472                    raise ValueError("Bad document %s (%s expected)" % (document, self.object))
473                delete = form.cleaned_data["delete"]
474                part = form.cleaned_data["part"]
475                if delete:
476                    parts.add(part)
477            if parts:
478                for part in parts:
479                    self.detach_part(part)
480                ids = (p.id for p in parts)
481                self.documentpartlink_document.filter(part__in=ids).delete()
482
483    def update_file(self, formset):
484        u"""
485        Updates uploaded file informations with data from *formset*
486       
487        :param formset:
488        :type formset: a modelfactory_formset of
489                        :class:`~plmapp.forms.ModifyFileForm`
490        :raises: :exc:`.PermissionError` if :attr:`_user` is not the owner of
491              :attr:`object`
492        :raises: :exc:`.PermissionError` if :attr:`object` is not editable.
493        """
494       
495        self.check_permission("owner")
496        self.check_editable()
497        if formset.is_valid():
498            for form in formset.forms:
499                document = form.cleaned_data["document"]
500                if document.pk != self.document.pk:
501                    raise ValueError("Bad document %s (%s expected)" % (document, self.object))
502                delete = form.cleaned_data["delete"]
503                filename = form.cleaned_data["id"]
504                if delete:
505                    self.delete_file(filename)
506
507    def cancel(self):
508        """
509        Cancels the object:
510
511            * calls :meth:`.PLMObjectController.cancel`
512            * removes all :class:`.DocumentPartLink` related to the object
513        """
514        super(DocumentController, self).cancel()
515        self.get_attached_parts().delete()
516
517    def check_cancel(self, raise_=True):
518        res = super(DocumentController, self).check_cancel(raise_=raise_)
519        if res :
520            res = res and not self.get_attached_parts()
521            if (not res) and raise_ :
522                raise PermissionError("This document is related to a part.")
523        return res
524       
525    def clone(self,form, user, parts, block_mails=False, no_index=False):
526        """
527        Clones the object :
528       
529            * calls :meth:`.PLMObjectController.clone`
530            * duplicates all :class:`.DocumentFile` in self.object
531           
532        :param parts: list of :class:`.Part` selected to be attached to the new document
533        """   
534        new_ctrl = super(DocumentController, self).clone(form, user, block_mails, no_index)
535       
536        for doc_file in self.object.files.all():
537            filename = doc_file.filename
538            path = models.docfs.get_available_name(filename)
539            shutil.copy(doc_file.file.path, models.docfs.path(path))
540            new_doc = models.DocumentFile.objects.create(file=path,
541                filename=filename, size=doc_file.size, document=new_ctrl.object)
542            new_doc.thumbnail = doc_file.thumbnail
543            if doc_file.thumbnail:
544                ext = os.path.splitext(doc_file.thumbnail.path)[1]
545                thumb = "%d%s" %(new_doc.id, ext)
546                dirname = os.path.dirname(doc_file.thumbnail.path)
547                thumb_path = os.path.join(dirname, thumb)
548                shutil.copy(doc_file.thumbnail.path, thumb_path)
549                new_doc.thumbnail = os.path.basename(thumb_path)
550            new_doc.locked = False
551            new_doc.locker = None
552            new_doc.save()
553           
554        if parts :
555            # attach the given parts
556            for part in parts:
557                models.DocumentPartLink.objects.create(part=part,
558                    document=new_ctrl.object)
559        return new_ctrl
560       
561    def has_links(self):
562        """
563        Return true if the document is attached to at least one part.
564        """
565        res = not self.get_attached_parts()
566        return res
567
568    def publish(self):
569        super(DocumentController, self).publish()
570        # publish all thumbnails
571        input_dir = settings.THUMBNAILS_DIR
572        output_dir = os.path.join(input_dir, "..", "public", "thumbnails")
573        for path in self.files.exclude(thumbnail="").values_list("thumbnail", flat=True):
574            os.symlink(os.path.join(input_dir, path),
575                       os.path.join(output_dir, path))
576   
577    def unpublish(self):
578        super(DocumentController, self).unpublish()
579        # unpublish all thumbnails
580        input_dir = settings.THUMBNAILS_DIR
581        output_dir = os.path.join(input_dir, "..", "public", "thumbnails")
582        for path in self.files.exclude(thumbnail="").values_list("thumbnail", flat=True):
583            try:
584                os.unlink(os.path.join(output_dir, path))
585            except OSError:
586                pass
587
Note: See TracBrowser for help on using the repository browser.