300 lines
9.0 KiB
Python
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)
|