import os
import string
import random
import hashlib
from django.utils import timezone
from django.db import models
from django.conf import settings
from django.contrib.auth.models import User
from django.core.files.storage import FileSystemStorage
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext_noop
from django.utils.functional import memoize
from openPLM.plmapp.files.formats import native_to_standards
from openPLM.plmapp.utils import memoize_noarg
from .lifecycle import Lifecycle
from .plmobject import (PLMObject, get_all_subclasses,
get_all_subclasses_with_level)
# document stuff
class DocumentStorage(FileSystemStorage):
[docs] """
File system storage which stores files with a specific name
"""
def get_available_name(self, name):
[docs] """
Returns a path for a file *name*, the path always refers to a file
which does not already exist.
The path is computed as follow:
#. a directory which name is the last extension of *name*.
For example, it is :file:`.gz` if *name* is :file:`.a.tar.gz`.
If *name* does not have an extension, the directory is
:file:`.no_ext/`.
#. a file name with 4 parts:
#. the md5 sum of *name*
#. a dash separator: ``-``
#. a random part with 7 characters in ``[a-z0-9]``
#. the extension, like :file:`.gz`
For example, if *name* is :file:`.my_file.tar.gz`,
a possible output is:
:file:`.gz/c7bfe8d00ea6e7138215ebfafff187af-jj6789g.gz`
If *name* is :file:`.my_file`, a possible output is:
:file:`.no_ext/59c211e8fc0f14b21c78c87eafe1ab72-dhh5555`
"""
def rand():
r = ""
for i in xrange(7):
r += random.choice(string.ascii_lowercase + string.digits)
return r
basename = os.path.basename(name)
base, ext = os.path.splitext(basename)
ext2 = ext.lstrip(".").lower() or "no_ext"
md5 = hashlib.md5()
md5.update(basename)
md5_value = md5.hexdigest() + "-%s" + ext
path = os.path.join(ext2, md5_value % rand())
while os.path.exists(os.path.join(self.location, path)):
path = os.path.join(ext2, md5_value % rand())
return path
#: :class:`.DocumentStorage` instance which stores files in :const:`.settings.DOCUMENTS_DIR`
docfs = DocumentStorage(location=settings.DOCUMENTS_DIR)
#: :class:`.FileSystemStorage` instance which stores thumbnails in :const:`.settings.THUMBNAILS_DIR`
thumbnailfs = FileSystemStorage(location=settings.THUMBNAILS_DIR,
base_url=settings.THUMBNAILS_URL)
class DocumentFile(models.Model):
[docs] """
.. versionchanged:: 1.2
New attributes: :attr:`.ctime`, :attr:`.end_time`, :attr:`.deleted`,
:attr:`.revision`, :attr:`.previous_revision`, :attr:`.last_revision`
Model which stores informations of a file bounded to a :class:`.Document`
:model attributes:
.. attribute:: filename
original filename
.. attribute:: file
file stored in :obj:`.docfs`
.. attribute:: size
size of the file in Byte
.. attribute:: locked
True if the file is locked
.. attribute:: locker
:class:`~django.contrib.auth.models.User` who locked the file,
None, if the file is not locked
.. attribute:: document
:class:`.Document` bounded to the file (required)
.. attribute:: ctime
date of creation of the document file
.. attribute:: end_time
date of creation of the next revision or None if it is
the last revision
.. attribute:: revision
revision number (starts at 1)
.. attribute:: previous_revision
:class:`.DocumentFile` or None if it's the first revision
.. attribute:: last_revision
foreign key to the last revision (:class:`.DocumentFile`)
or None it it'is the last_revision
.. attribute:: deleted
True if the file has been physically removed
"""
class Meta:
app_label = "plmapp"
filename = models.CharField(max_length=200)
file = models.FileField(upload_to=".", storage=docfs)
size = models.PositiveIntegerField()
thumbnail = models.ImageField(upload_to=".", storage=thumbnailfs,
blank=True, null=True)
locked = models.BooleanField(default=lambda: False)
locker = models.ForeignKey(User, null=True, blank=True,
default=lambda: None)
document = models.ForeignKey('Document')
deprecated = models.BooleanField(default=lambda: False)
ctime = models.DateTimeField(auto_now_add=False, default=timezone.now)
end_time = models.DateTimeField(blank=True, null=True, default=lambda: None)
deleted = models.BooleanField(default=False)
revision = models.IntegerField(default=1)
previous_revision = models.OneToOneField('self',
related_name="next_revision", default=None, null=True)
last_revision = models.ForeignKey('self',
related_name="older_files", default=None, null=True)
@property
def native_related(self):
def checkout_valid(self):
[docs] """
Returns False if DocumentFile has a native related *locked* file
and :const:`.settings.ENABLE_NATIVE_FILE_MANAGEMENT` is True.
"""
if getattr(settings, 'ENABLE_NATIVE_FILE_MANAGEMENT', False):
name, ext = os.path.splitext(self.filename)
ext = ext.lower()
doc_files = DocumentFile.objects.filter(document__id=self.document_id, locked=True)\
.exclude(deprecated=True, id=self.id).values_list("filename", flat=True)
for filename in doc_files:
native, native_ext = os.path.splitext(filename)
if native == name and ext in native_to_standards[native_ext.lower()]:
return False
return True
def __unicode__(self):
return u"DocumentFile<%s, %s>" % (self.filename, self.document)
class PrivateFile(models.Model):
[docs] """
.. versionadded:: 1.2
Model which stores informations of a private file only readable by its
creator.
There are no revision, locker, deleted or similar attributes.
A private file is not shared, and it is temporary. It is created to
store a file before a document creation.
Private files are created when a user uploads files and *then* creates
a document containing these files.
:model attributes:
.. attribute:: filename
original filename
.. attribute:: file
file stored in :obj:`.docfs`
.. attribute:: size
size of the file in Byte
.. attribute:: creator
:class:`~django.contrib.auth.models.User` who created the file,
.. attribute:: ctime
date of creation of the file
"""
class Meta:
app_label = "plmapp"
filename = models.CharField(max_length=200)
file = models.FileField(upload_to=".", storage=docfs)
size = models.PositiveIntegerField()
creator = models.ForeignKey(User, related_name="files")
ctime = models.DateTimeField(auto_now_add=False, default=timezone.now)
def __unicode__(self):
return u"PrivateFile<%s, %s>" % (self.filename, self.creator)
class Document(PLMObject):
[docs] """
Model for documents
"""
class Meta:
app_label = "plmapp"
ACCEPT_FILES = True
template = models.ForeignKey('self', related_name='tpl_instances',
null=True, default=None, editable=False)
@property
def files(self):
[docs] "Queryset of all non deprecated :class:`.DocumentFile` linked to self"
return self.documentfile_set.exclude(deprecated=True)
@property
def deprecated_files(self):
[docs] "Queryset of all deprecated :class:`.DocumentFile` linked to self"
return self.documentfile_set.filter(deprecated=True)
def get_content_and_size(self, doc_file):
return open(doc_file.file.path, "rb"), doc_file.file.size
def is_promotable(self):
def menu_items(self):
items = list(super(Document, self).menu_items)
items.insert(0, ugettext_noop("files"))
items.append(ugettext_noop("parts"))
return items
@property
def is_part(self):
return False
@property
def is_document(self):
return True
@classmethod
def get_creation_score(cls, files):
[docs] """
.. versionadded:: 2.0
Returns a score (an integer) computed from *files*.
*files* is a list of :class:`PrivateFile` uploaded by a user
who wants to create a document with all files.
The class which returns the highest score is chosen has the
default type after an upload.
The default implementation returns 10 if the current class is *Document*
and 0 otherwise.
For example, :class:`~Document3D` returns 50 if a CAD file has been
uploaded. Document3D is so preferred when a STEP file is uploaded.
.. seealso::
:func:`get_best_document_type`
"""
if cls == Document:
return 10
return 0
@property
def is_template(self):
return self.lifecycle.type == Lifecycle.TEMPLATE
@memoize_noarg
def get_all_documents():
[docs] u"""
Returns a dict<doc_name, doc_class> of all available :class:`.Document` classes
"""
res = {}
get_all_subclasses(Document, res)
return res
def get_all_subtype_documents(subtype):
[docs] u"""
Returns a dict<doc_name, doc_class> of all available **subtype** classes
"""
res = {}
get_all_subclasses(subtype, res)
return res
def get_all_documents_with_level(only_accept_files=False):
lst = []
level="=>"
get_all_subclasses_with_level(Document, lst, level)
classes = get_all_documents()
if only_accept_files:
return [(n, l) for n, l in lst if classes[n].ACCEPT_FILES]
return lst
get_all_documents_with_level = memoize(get_all_documents_with_level, {}, 1)
def get_best_document_type(files):
[docs] """
.. versionadded:: 2.0
Returns the document type (str) which returns the highest
creation score for the uploaded files (list of :class:`PrivateFile`).
.. seealso::
:meth:`Document.get_creation_score`
"""
dtype = "Document"
max_score = 0
for name, cls in get_all_documents().iteritems():
score = cls.get_creation_score(files)
if score > max_score:
max_score = score
dtype = name
return dtype