dms/dms/models.py

300 lines
9.0 KiB
Python

"""
This module defines the structure and properties of documents and versions.
"""
from django.db import models
from django.contrib.auth.models import Group, User
from markdown import Markdown
from closuretree.models import ClosureModel
import reversion
import pdfkit
from process.models import Approval
from process.exceptions import ActorException
from writer.models import Section
class Audience(ClosureModel):
name = models.CharField(max_length=50)
parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
group = models.ForeignKey(Group)
def __str__(self):
return self.name
class Meta:
ordering = ('name',)
class Site(models.Model):
name = models.CharField(max_length=50)
def __str__(self):
return self.name
class Meta:
ordering = ['name']
class Node(ClosureModel):
name = models.CharField(max_length=100, unique=True)
acronym = models.CharField(max_length=20, null=True, blank=True)
parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
def __str__(self):
return self.name
class Meta:
ordering = ['name']
class Keyword(ClosureModel):
name = models.CharField(max_length=255)
parent = models.ForeignKey('self', null=True, blank=True)
selectable = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
class DocumentType(models.Model):
name = models.CharField(max_length=50)
level = models.IntegerField()
template = models.FileField(upload_to='documents_templates/', null=True, blank=True)
description = models.TextField(max_length=255, blank=True)
def __str__(self):
return self.name
class Meta:
ordering = ['level']
@reversion.register()
class Document(models.Model):
title = models.CharField(max_length=255)
overview = models.TextField(blank=True, null=True)
type = models.ForeignKey(DocumentType)
created_at = models.DateTimeField(auto_now_add=True)
manager = models.ForeignKey(User)
jci_standard = models.ForeignKey('jci.Standard', related_name='documents', null=True, blank=True)
@property
def last_published_version(self):
try:
return self.versions.filter(is_published__isnull=True).first()
except Version.DoesNotExist:
return 'None'
except Exception:
return 'None'
@property
def last_working_version(self):
try:
return self.versions.filter(is_published__isnull=False).first()
except Version.DoesNotExist:
return 'None'
def create_new_version(self):
"""Creates a new version for this document.
By default, version 0.0 is created with the chosen template for this document.
:raises
IndexError if there is a current version still in draft
:returns
A new `Version` object if it needs to be created.
Raises an `IndexError` exception instead.
"""
latest_version = self.versions.last()
if not latest_version or latest_version.is_published:
latest_version = Version(
document= self,
file=self.type.template,
revision=1,
major=self.major,
)
latest_version.save()
return latest_version
if not latest_version.is_published:
raise IndexError('The latest version is still in draft.\n '
'Publish this first before creating a new one.')
@property
def major(self):
latest_version = self.versions.filter(is_published=True).last()
if latest_version:
return latest_version.major
return 0
@property
def revision(self):
latest_version = self.versions.last()
if latest_version:
return latest_version.revision
return 0
@reversion.create_revision()
def save(self, *args, **kwargs):
# todo : should create a default version based on the document type template.
if not self.pk:
super().save() # obtains a new id for this document
self.create_new_version() # create a new empty version
for model_section in self.type.model_sections.all(): # create default sections
Section.create_from_model_section(model_section, self)
else:
super().save()
def to_pdf(self):
x = ""
x += '# Tables des matières \n [TOC]\n'
for section in self.sections.all():
x += '# ' + section.title
x += '\n'
x += section.content
x += '\n'
md = Markdown(extensions=['markdown.extensions.toc', 'markdown.extensions.tables'])
html = md.convert(x)
options = {
'page-size': 'Letter',
'encoding': "UTF-8",
'custom-header': [
('Accept-Encoding', 'gzip')
],
'no-outline': None,
'footer-right': '[page]/[topage]'
}
return pdfkit.from_string(html, False, options=options)
def __str__(self):
return self.title
ROLE_CHOICES = (
(1, 'Validator'),
(2, 'Reviewer'),
(3, 'Author'),
)
class Role(models.Model):
"""Defines possible roles for users.
These roles will (or can) be used for running processes.
:attributes
name (str): the name of the role.
"""
type = models.IntegerField(choices=ROLE_CHOICES)
actor = models.ForeignKey(User)
version = models.ForeignKey('Version')
@reversion.register()
class Version(models.Model):
"""A version represents the writing steps of a document.
System fields::
major (+Int)
minor (+Int)
is_published (bool)
created_at (DateTime)
Properties:
audiences
keywords
sites
nodes
restricted
Actors:
actors
"""
# instance structure
document = models.ForeignKey(Document, related_name='versions')
file = models.FileField(upload_to='revisions/', null=True, blank=True)
snapshot = models.TextField(blank=True, null=True)
# system fields
major = models.PositiveIntegerField()
revision = models.PositiveIntegerField()
is_published = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
# properties
keywords = models.ManyToManyField(Keyword, blank=True, related_name='versions')
audiences = models.ManyToManyField(Audience, blank=True, related_name='versions')
restricted = models.BooleanField(default=False)
sites = models.ManyToManyField(Site, blank=True)
nodes = models.ManyToManyField(Node, blank=True)
# actors
actors = models.ManyToManyField(User, related_name='versions', through='Role')
@property
def validators(self):
return User.objects.filter(role__type=1)
@reversion.create_revision()
def publish(self):
"""Starts a new publication process, based on the people set with a role `validator`.
If the current version has not been published yet, this method checks that
there are people in the role `validator` and assign a task to each one of them.
:exception
ActorException: when no validator has been set.
:returns
A new instance of type `process.Approval`.
"""
if not self.is_published:
if self.validators.count() == 0:
raise ActorException('There are no actors for this version. \n'
'Please, add at least one of them.')
return Approval.objects.create(
document_version=self
)
@reversion.create_revision()
def update(self, *args, **kwargs):
"""Update the current version by bumping its minor version by one.
This update can only occur when the current version has never been published.
:exception
ValueError when the current version has already been published.
:returns
super().save()"""
if not self.is_published:
self.revision = self.revision + 1
return super().save(*args, **kwargs)
raise ValueError('A published version cannot be updated.')
@reversion.create_revision()
def save(self, *args, **kwargs):
super().save()
class Meta:
unique_together = ('document', 'major')
@property
def status(self):
return 'Published' if self.is_published else 'Draft'
def __str__(self):
return '{} v{}.{} ({})'.format(self.document, self.major, self.revision, self.status)