from __future__ import unicode_literals
from __future__ import print_function
from __future__ import absolute_import
from django.conf import settings
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.db import models, transaction
from django.db.models import Q
from django.db.models.signals import post_save, m2m_changed, post_delete
from django.dispatch import receiver, Signal
from django.utils import timezone
from django.utils.module_loading import import_string
from django.utils.translation import ugettext_lazy as _
from model_utils.managers import InheritanceManager, InheritanceQuerySet
from model_utils.models import TimeStampedModel
from model_utils import Choices, FieldTracker
from aristotle_mdr.contrib.channels.utils import fire
import reversion # import revisions
import datetime
from ckeditor_uploader.fields import RichTextUploadingField as RichTextField
from aristotle_mdr import perms
from aristotle_mdr import messages
from aristotle_mdr.utils import (
url_slugify_concept,
url_slugify_workgroup,
url_slugify_registration_authoritity,
url_slugify_organization
)
from aristotle_mdr import comparators
from model_utils.fields import AutoLastModifiedField
import logging
logger = logging.getLogger(__name__)
logger.debug("Logging started for " + __name__)
"""
This is the core modelling for Aristotle mapping ISO/IEC 11179 classes to Python classes/Django models.
Docstrings are copied directly from the ISO/IEC 11179-3 documentation in their original form.
References to the originals is kept where possible using brackets and the dotted section numbers -
Eg. explanatory_comment (8.1.2.2.3.4)
"""
# 11179 States
# When used these MUST be used as IntegerFields to allow status comparison
STATES = Choices(
(0, 'notprogressed', _('Not Progressed')),
(1, 'incomplete', _('Incomplete')),
(2, 'candidate', _('Candidate')),
(3, 'recorded', _('Recorded')),
(4, 'qualified', _('Qualified')),
(5, 'standard', _('Standard')),
(6, 'preferred', _('Preferred Standard')),
(7, 'superseded', _('Superseded')),
(8, 'retired', _('Retired')),
)
VERY_RECENTLY_SECONDS = 15
concept_visibility_updated = Signal(providing_args=["concept"])
class baseAristotleObject(TimeStampedModel):
name = models.TextField(
help_text=_("The primary name used for human identification purposes.")
)
definition = RichTextField(
_('definition'),
help_text=_("Representation of a concept by a descriptive statement "
"which serves to differentiate it from related concepts. (3.2.39)")
)
objects = InheritanceManager()
class Meta:
# So the url_name works for items we can't determine
verbose_name = "item"
# Can't be abstract as we need unique app wide IDs.
abstract = True
def was_modified_very_recently(self):
return self.modified >= (
timezone.now() - datetime.timedelta(seconds=VERY_RECENTLY_SECONDS)
)
def was_modified_recently(self):
return self.modified >= timezone.now() - datetime.timedelta(days=1)
was_modified_recently.admin_order_field = 'modified'
was_modified_recently.boolean = True
was_modified_recently.short_description = 'Modified recently?'
def description_stub(self):
from django.utils.html import strip_tags
d = strip_tags(self.definition)
if len(d) > 150:
d = d[0:150] + "..."
return d
def __str__(self):
return "{name}".format(name=self.name).encode('utf-8')
def __unicode__(self):
return "{name}".format(name=self.name)
# Defined so we can access it during templates.
@classmethod
def get_verbose_name(cls):
return cls._meta.verbose_name.title()
@classmethod
def get_verbose_name_plural(cls):
return cls._meta.verbose_name_plural.title()
def can_edit(self, user):
# This should always be overridden
raise NotImplementedError # pragma: no cover
def can_view(self, user):
# This should always be overridden
raise NotImplementedError # pragma: no cover
@classmethod
def meta(self):
# I know what I'm doing, get out the way.
return self._meta
class unmanagedObject(baseAristotleObject):
class Meta:
abstract = True
def can_edit(self, user):
return user.is_superuser
def can_view(self, user):
return True
@property
def item(self):
return self
class aristotleComponent(models.Model):
class Meta:
abstract = True
def can_edit(self, user):
return self.parentItem.can_edit(user)
def can_view(self, user):
return self.parentItem.can_view(user)
class registryGroup(unmanagedObject):
managers = models.ManyToManyField(
User,
blank=True,
related_name="%(class)s_manager_in",
verbose_name=_('Managers')
)
class Meta:
abstract = True
def can_edit(self, user):
return user.is_superuser or self.managers.filter(pk=user.pk).exists()
@property
def help_name(self):
return self._meta.model_name
class Organization(registryGroup):
"""
6.3.6 - Organization is a class each instance of which models an organization (3.2.90),
a unique framework of authority within which individuals (3.2.65) act, or are designated to act,
towards some purpose.
"""
template = "aristotle_mdr/organization/organization.html"
uri = models.URLField( # 6.3.6.2.5
blank=True, null=True,
help_text="uri for Organization"
)
def promote_to_registration_authority(self):
ra = RegistrationAuthority(organization_ptr=self)
ra.save()
return ra
def get_absolute_url(self):
return url_slugify_organization(self)
class RegistrationAuthority(Organization):
"""
8.1.2.5 - Registration_Authority class
Registration_Authority is a class each instance of which models a registration authority (3.2.109),
an organization (3.2.90) responsible for maintaining a register (3.2.104).
A registration authority may register many administered items (3.2.2) as shown by the Registration
(8.1.5.1) association class.
"""
template = "aristotle_mdr/organization/registrationAuthority.html"
locked_state = models.IntegerField(
choices=STATES,
default=STATES.candidate
)
public_state = models.IntegerField(
choices=STATES,
default=STATES.recorded
)
registrars = models.ManyToManyField(
User,
blank=True,
related_name='registrar_in',
verbose_name=_('Registrars')
)
# The below text fields allow for brief descriptions of the context of each
# state for a particular Registration Authority
# For example:
# For a particular Registration Authority standard may mean"
# "Approved by a simple majority of the standing council of metadata
# standardisation"
# While "Preferred Standard" may mean:
# "Approved by a two-thirds majority of the standing council of metadata
# standardisation"
notprogressed = models.TextField(blank=True)
incomplete = models.TextField(blank=True)
candidate = models.TextField(blank=True)
recorded = models.TextField(blank=True)
qualified = models.TextField(blank=True)
standard = models.TextField(blank=True)
preferred = models.TextField(blank=True)
superseded = models.TextField(blank=True)
retired = models.TextField(blank=True)
tracker = FieldTracker()
class Meta:
verbose_name_plural = _("Registration Authorities")
def get_absolute_url(self):
return url_slugify_registration_authoritity(self)
def can_view(self, user):
return True
@property
def unlocked_states(self):
return range(STATES.notprogressed, self.locked_state)
@property
def locked_states(self):
return range(self.locked_state, self.public_state)
@property
def public_states(self):
return range(self.public_state, STATES.retired + 1)
def statusDescriptions(self):
descriptions = [
self.notprogressed,
self.incomplete,
self.candidate,
self.recorded,
self.qualified,
self.standard,
self.preferred,
self.superseded,
self.retired
]
unlocked = [
(i, STATES[i], descriptions[i]) for i in self.unlocked_states
]
locked = [
(i, STATES[i], descriptions[i]) for i in self.locked_states
]
public = [
(i, STATES[i], descriptions[i]) for i in self.public_states
]
return (
('unlocked', unlocked),
('locked', locked),
('public', public)
)
def cascaded_register(self, item, state, user, *args, **kwargs):
if not perms.user_can_change_status(user, item):
# Return a failure as this item isn't allowed
return {'success': [], 'failed': [item] + item.registry_cascade_items}
revision_message = _(
"Cascade registration of item '%(name)s' (id:%(iid)s)\n"
) % {
'name': item.name,
'iid': item.id
}
revision_message = revision_message + kwargs.get('changeDetails', "")
seen_items = {'success': [], 'failed': []}
with transaction.atomic(), reversion.revisions.create_revision():
reversion.revisions.set_user(user)
reversion.revisions.set_comment(revision_message)
for child_item in [item] + item.registry_cascade_items:
self._register(
child_item, state, user, *args, **kwargs
)
seen_items['success'] = seen_items['success'] + [child_item]
return seen_items
def register(self, item, state, user, *args, **kwargs):
if not perms.user_can_change_status(user, item):
# Return a failure as this item isn't allowed
return {'success': [], 'failed': [item]}
revision_message = kwargs.get('changeDetails', "")
with transaction.atomic(), reversion.revisions.create_revision():
reversion.revisions.set_user(user)
reversion.revisions.set_comment(revision_message)
self._register(item, state, user, *args, **kwargs)
return {'success': [item], 'failed': []}
def _register(self, item, state, user, *args, **kwargs):
changeDetails = kwargs.get('changeDetails', "")
# If registrationDate is None (like from a form), override it with
# todays date.
registrationDate = kwargs.get('registrationDate', None) \
or timezone.now().date()
until_date = kwargs.get('until_date', None)
Status.objects.create(
concept=item,
registrationAuthority=self,
registrationDate=registrationDate,
state=state,
changeDetails=changeDetails,
until_date=until_date
)
def giveRoleToUser(self, role, user):
if role == 'registrar':
self.registrars.add(user)
if role == "manager":
self.managers.add(user)
def removeRoleFromUser(self, role, user):
if role == 'registrar':
self.registrars.remove(user)
if role == "manager":
self.managers.remove(user)
@receiver(post_save, sender=RegistrationAuthority)
def update_registration_authority_states(sender, instance, created, **kwargs):
if not created:
if instance.tracker.has_changed('public_state') \
or instance.tracker.has_changed('locked_state'):
message = (
"Registration '{ra}' changed its public or locked status "
"level, items registered by this authority may have stale "
"visiblity states and need to be manually updated."
).format(ra=instance.name)
logger.critical(message)
class Workgroup(registryGroup):
"""
A workgroup is a collection of associated users given control to work on a
specific piece of work. Usually this work will be the creation of a
specific collection of objects, such as data elements, for a specific
topic.
Workgroup owners may choose to 'archive' a workgroup. All content remains
visible, but the workgroup is hidden in lists and new items cannot be
created in that workgroup.
"""
template = "aristotle_mdr/workgroup.html"
archived = models.BooleanField(
default=False,
help_text=_("Archived workgroups can no longer have new items or "
"discussions created within them."),
verbose_name=_('Archived'),
)
viewers = models.ManyToManyField(
User,
blank=True,
related_name='viewer_in',
verbose_name=_('Viewers')
)
submitters = models.ManyToManyField(
User,
blank=True,
related_name='submitter_in',
verbose_name=_('Submitters')
)
stewards = models.ManyToManyField(
User,
blank=True,
related_name='steward_in',
verbose_name=_('Stewards')
)
roles = {
'submitter': _("Submitter"),
'viewer': _("Viewer"),
'steward': _("Steward"),
'manager': _("Manager")
}
tracker = FieldTracker()
def get_absolute_url(self):
return url_slugify_workgroup(self)
@property
def members(self):
return self.viewers.all() \
| self.submitters.all() \
| self.stewards.all() \
| self.managers.all()
def can_view(self, user):
return self.members.filter(pk=user.pk).exists()
@property
def classedItems(self):
# Convenience class as we can't call functions in templates
return self.items.select_subclasses()
def giveRoleToUser(self, role, user):
if role == "manager":
self.managers.add(user)
if role == "viewer":
self.viewers.add(user)
if role == "submitter":
self.submitters.add(user)
if role == "steward":
self.stewards.add(user)
self.save()
def removeRoleFromUser(self, role, user):
if role == "manager":
self.managers.remove(user)
if role == "viewer":
self.viewers.remove(user)
if role == "submitter":
self.submitters.remove(user)
if role == "steward":
self.stewards.remove(user)
self.save()
def removeUser(self, user):
self.viewers.remove(user)
self.submitters.remove(user)
self.stewards.remove(user)
self.managers.remove(user)
class discussionAbstract(TimeStampedModel):
body = models.TextField()
author = models.ForeignKey(User)
class Meta:
abstract = True
@property
def edited(self):
return self.created != self.modified
class DiscussionPost(discussionAbstract):
workgroup = models.ForeignKey(Workgroup, related_name='discussions')
title = models.CharField(max_length=256)
relatedItems = models.ManyToManyField(
'_concept',
blank=True,
related_name='relatedDiscussions',
)
closed = models.BooleanField(default=False)
class Meta:
ordering = ['-modified']
@property
def active(self):
return not self.closed
class DiscussionComment(discussionAbstract):
post = models.ForeignKey(DiscussionPost, related_name='comments')
class Meta:
ordering = ['created']
# class ReferenceDocument(models.Model):
# url = models.URLField()
# definition = models.TextField()
# object = models.ForeignKey(managedObject)
[docs]class ConceptQuerySet(InheritanceQuerySet):
[docs] def visible(self, user):
"""
Returns a queryset that returns all items that the given user has
permission to view.
It is **chainable** with other querysets. For example, both of these
will work and return the same list::
ObjectClass.objects.filter(name__contains="Person").visible()
ObjectClass.objects.visible().filter(name__contains="Person")
"""
if user.is_superuser:
return self.all()
if user.is_anonymous():
return self.public()
q = Q(_is_public=True)
if user.is_active:
# User can see everything they've made.
q |= Q(submitter=user)
if user.profile.workgroups:
# User can see everything in their workgroups.
q |= Q(workgroup__in=user.profile.workgroups)
# q |= Q(workgroup__user__profile=user)
if user.profile.is_registrar:
# Registars can see items they have been asked to review
q |= Q(
Q(review_requests__registration_authority__registrars__profile__user=user) & ~Q(review_requests__status=REVIEW_STATES.cancelled)
)
# Registars can see items that have been registered in their registration authority
q |= Q(
Q(statuses__registrationAuthority__registrars__profile__user=user)
)
extra_q = settings.ARISTOTLE_SETTINGS.get('EXTRA_CONCEPT_QUERYSETS', {}).get('visible', None)
if extra_q:
for func in extra_q:
q |= import_string(func)(user)
return self.filter(q)
[docs] def editable(self, user):
"""
Returns a queryset that returns all items that the given user has
permission to edit.
It is **chainable** with other querysets. For example, both of these
will work and return the same list::
ObjectClass.objects.filter(name__contains="Person").editable()
ObjectClass.objects.editable().filter(name__contains="Person")
"""
if user.is_superuser:
return self.all()
if user.is_anonymous():
return self.none()
q = Q()
# User can edit everything they've made thats not locked
q |= Q(submitter=user, _is_locked=False)
if user.submitter_in.exists() or user.steward_in.exists():
if user.submitter_in.exists():
q |= Q(_is_locked=False, workgroup__submitters__profile__user=user)
if user.steward_in.exists():
q |= Q(workgroup__stewards__profile__user=user)
return self.filter(q)
[docs] def public(self):
"""
Returns a list of public items from the queryset.
This is a chainable query set, that filters on items which have the
internal `_is_public` flag set to true.
Both of these examples will work and return the same list::
ObjectClass.objects.filter(name__contains="Person").public()
ObjectClass.objects.public().filter(name__contains="Person")
"""
return self.filter(_is_public=True)
[docs]class ConceptManager(InheritanceManager):
"""
The ``ConceptManager`` is the default object manager for ``concept`` and
``_concept`` items, and extends from the django-model-utils
``InheritanceManager``.
It provides access to the ``ConceptQuerySet`` to allow for easy
permissions-based filtering of ISO 11179 Concept-based items.
"""
def get_queryset(self):
return ConceptQuerySet(self.model)
def __getattr__(self, attr, *args):
if attr in ['editable', 'visible', 'public']:
return getattr(self.get_queryset(), attr, *args)
else:
return getattr(self.__class__, attr, *args)
[docs]class _concept(baseAristotleObject):
"""
9.1.2.1 - Concept class
Concept is a class each instance of which models a concept (3.2.18),
a unit of knowledge created by a unique combination of characteristics (3.2.14).
A concept is independent of representation.
This is the base concrete class that ``Status`` items attach to, and to
which collection objects refer to. It is not marked abstract in the Django
Meta class, and **must not be inherited from**. It has relatively few
fields and is a convenience class to link with in relationships.
"""
objects = ConceptManager()
template = "aristotle_mdr/concepts/managedContent.html"
workgroup = models.ForeignKey(Workgroup, related_name="items", null=True, blank=True)
submitter = models.ForeignKey(
User, related_name="created_items",
null=True, blank=True,
help_text=_('This is the person who first created an item. Users can always see items they made.'))
# We will query on these, so want them cached with the items themselves
# To be usable these must be updated when statuses are changed
_is_public = models.BooleanField(default=False)
_is_locked = models.BooleanField(default=False)
short_name = models.CharField(max_length=100, blank=True)
version = models.CharField(max_length=20, blank=True)
synonyms = models.CharField(max_length=200, blank=True)
references = RichTextField(blank=True)
origin_URI = models.URLField(
blank=True,
help_text="If imported, the original location of the item"
)
comments = RichTextField(
help_text=_("Descriptive comments about the metadata item (8.1.2.2.3.4)"),
blank=True
)
submitting_organisation = models.CharField(max_length=256, blank=True)
responsible_organisation = models.CharField(max_length=256, blank=True)
superseded_by = models.ForeignKey(
'self',
related_name='supersedes',
blank=True,
null=True
)
tracker = FieldTracker()
comparator = comparators.Comparator
edit_page_excludes = None
admin_page_excludes = None
class Meta:
# So the url_name works for items we can't determine.
verbose_name = "item"
@property
def non_cached_fields_changed(self):
changed = self.tracker.changed()
public_changed = changed.pop('_is_public', False)
locked_changed = changed.pop('_is_locked', False)
return len(changed.keys()) > 0
@property
def changed_fields(self):
changed = self.tracker.changed()
public_changed = changed.pop('_is_public', False)
locked_changed = changed.pop('_is_locked', False)
return changed.keys()
def can_edit(self, user):
return _concept.objects.filter(pk=self.pk).editable(user).exists()
def can_view(self, user):
return _concept.objects.filter(pk=self.pk).visible(user).exists()
@property
def item(self):
"""
Performs a lookup using ``model_utils.managers.InheritanceManager`` to
find the subclassed item.
"""
return _concept.objects.get_subclass(pk=self.pk)
@property
def concept(self):
"""
Returns the parent _concept that an item is built on.
If the item type is _concept, return itself.
"""
return getattr(self, '_concept_ptr', self)
@classmethod
def get_autocomplete_name(self):
return 'Autocomplete' + "".join(
self._meta.verbose_name.title().split()
)
@staticmethod
def autocomplete_search_fields(self):
return ("name__icontains",)
def get_absolute_url(self):
return url_slugify_concept(self)
@property
def registry_cascade_items(self):
"""
This returns the items that can be registered along with the this item.
If a subclass of _concept defines this method, then when an instance
of that class is registered using a cascading method then that
instance, all instances returned by this method will all recieve the
same registration status.
Reimplementations of this MUST return iterables.
"""
return []
@property
def is_registered(self):
return self.statuses.count() > 0
@property
def is_superseded(self):
return all(
STATES.superseded == status.state for status in self.statuses.all()
) and self.superseded_by
@property
def is_retired(self):
return all(
STATES.retired == status.state for status in self.statuses.all()
) and self.statuses.count() > 0
def check_is_public(self, when=timezone.now()):
"""
A concept is public if any registration authority
has advanced it to a public state in that RA.
"""
statuses = self.statuses.all()
statuses = self.current_statuses(qs=statuses, when=when)
pub_state = True in [
s.state >= s.registrationAuthority.public_state for s in statuses
]
q = Q()
extra = False
extra_q = settings.ARISTOTLE_SETTINGS.get('EXTRA_CONCEPT_QUERYSETS', {}).get('public', None)
if extra_q:
for func in extra_q:
q |= import_string(func)()
extra = self.__class__.objects.filter(pk=self.pk).filter(q).exists()
return pub_state or extra
def is_public(self):
return self._is_public
is_public.boolean = True
is_public.short_description = 'Public'
def check_is_locked(self, when=timezone.now()):
"""
A concept is locked if any registration authority
has advanced it to a locked state in that RA.
"""
statuses = self.statuses.all()
statuses = self.current_statuses(qs=statuses, when=when)
return True in [
s.state >= s.registrationAuthority.locked_state for s in statuses
]
def is_locked(self):
return self._is_locked
is_locked.boolean = True
is_locked.short_description = 'Locked'
def recache_states(self):
self._is_public = self.check_is_public()
self._is_locked = self.check_is_locked()
self.save()
concept_visibility_updated.send(sender=self.__class__, concept=self)
def current_statuses(self, qs=None, when=timezone.now()):
if qs is None:
qs = self.statuses.all()
if hasattr(when, 'date'):
when = when.date()
registered_before_now = Q(registrationDate__lte=when)
registation_still_valid = (
Q(until_date__gte=when) |
Q(until_date__isnull=True)
)
states = qs.filter(
registered_before_now & registation_still_valid
).order_by("registrationAuthority", "-registrationDate", "-created")
from django.db import connection
if connection.vendor == 'postgresql':
states = states.distinct('registrationAuthority')
else:
current_ids = []
seen_ras = []
for s in states:
ra = s.registrationAuthority
if ra not in seen_ras:
current_ids.append(s.pk)
seen_ras.append(ra)
# We hit again so we can return this as a queryset
states = states.filter(pk__in=current_ids)
return states
def get_download_items(self):
"""
When downloading a concept, extra items can be included for download by
overriding the ``get_download_items`` method on your item. By default
this returns an empty list, but can be modified to include any number of
items that inherit from ``_concept``.
When overriding, each entry in the list must be a two item tuple, with
the first entry being the python class of the item or items being
included, and the second being the queryset of items to include.
"""
return []
[docs]class concept(_concept):
"""
This is an abstract class that all items that should behave like a 11179
Concept **must inherit from**. This model includes the definitions for many
long and optional text fields and the self-referential ``superseded_by``
field. It is not possible to include this model in a ``ForeignKey`` or
``ManyToManyField``.
"""
objects = ConceptManager()
class Meta:
abstract = True
@property
def help_name(self):
return self._meta.model_name
@property
def item(self):
"""
Return self, because we already have the correct item.
"""
return self
REVIEW_STATES = Choices(
(0, 'submitted', _('Submitted')),
(5, 'cancelled', _('Cancelled')),
(10, 'accepted', _('Accepted')),
(15, 'rejected', _('Rejected')),
)
class ReviewRequestQuerySet(models.QuerySet):
def visible(self, user):
"""
Returns a queryset that returns all reviews that the given user has
permission to view.
It is **chainable** with other querysets. For example, both of these
will work and return the same list::
ObjectClass.objects.filter(name__contains="Person").visible()
ObjectClass.objects.visible().filter(name__contains="Person")
"""
if user.is_superuser:
return self.all()
if user.is_anonymous():
return self.none()
q = Q(requester=user) # Users can always see reviews they requested
if user.profile.is_registrar:
# Registars can see reviews for the registration authority
q |= Q(
Q(registration_authority__registrars__profile__user=user) & ~Q(status=REVIEW_STATES.cancelled)
)
return self.filter(q)
class ReviewRequestManager(models.Manager):
def get_queryset(self):
return ReviewRequestQuerySet(self.model, using=self._db)
def __getattr__(self, attr, *args):
if attr in ['visible']:
return getattr(self.get_queryset(), attr, *args)
else:
return getattr(self.__class__, attr, *args)
class ReviewRequest(TimeStampedModel):
objects = ReviewRequestManager()
concepts = models.ManyToManyField(_concept, related_name="review_requests")
registration_authority = models.ForeignKey(
RegistrationAuthority,
help_text=_("The registration authority the requester wishes to endorse the metadata item")
)
requester = models.ForeignKey(User, help_text=_("The user requesting a review"), related_name='requested_reviews')
message = models.TextField(blank=True, null=True, help_text=_("An optional message accompanying a request"))
reviewer = models.ForeignKey(User, null=True, help_text=_("The user performing a review"), related_name='reviewed_requests')
response = models.TextField(blank=True, null=True, help_text=_("An optional message responding to a request"))
status = models.IntegerField(
choices=REVIEW_STATES,
default=REVIEW_STATES.submitted,
help_text=_('Status of a review')
)
state = models.IntegerField(
choices=STATES,
blank=True, null=True,
help_text=_("The state at which a user wishes a metadata item to be endorsed")
)
class Status(TimeStampedModel):
"""
8.1.2.6 - Registration_State class
A Registration_State is a collection of information about the Registration (8.1.5.1) of an Administered Item (8.1.2.2).
The attributes of the Registration_State class are summarized here and specified more formally in 8.1.2.6.2.
"""
concept = models.ForeignKey(_concept, related_name="statuses")
registrationAuthority = models.ForeignKey(RegistrationAuthority)
changeDetails = models.TextField(blank=True, null=True)
state = models.IntegerField(
choices=STATES,
default=STATES.incomplete,
help_text=_("Designation (3.2.51) of the status in the registration life-cycle of an Administered_Item")
)
# TODO: Below should be changed to 'effective_date' to match ISO IEC
# 11179-6 (Section 8.1.2.6.2.2)
registrationDate = models.DateField(
_('Date registration effective'),
help_text=_("date and time an Administered_Item became/becomes available to registry users")
)
until_date = models.DateField(
_('Date registration expires'),
blank=True,
null=True,
help_text=_("date and time the Registration of an Administered_Item by a Registration_Authority in a registry is no longer effective")
)
tracker = FieldTracker()
class Meta:
verbose_name_plural = "Statuses"
@property
def state_name(self):
return STATES[self.state]
def __unicode__(self):
return "{obj} is {stat} for {ra} on {date} - {desc}".format(
obj=self.concept.name,
stat=self.state_name,
ra=self.registrationAuthority,
desc=self.changeDetails,
date=self.registrationDate
)
def recache_concept_states(sender, instance, *args, **kwargs):
instance.concept.recache_states()
post_save.connect(recache_concept_states, sender=Status)
post_delete.connect(recache_concept_states, sender=Status)
class ObjectClass(concept):
"""
Set of ideas, abstractions or things in the real world that are
identified with explicit boundaries and meaning and whose properties and
behaviour follow the same rules (3.2.88)
"""
template = "aristotle_mdr/concepts/objectClass.html"
class Meta:
verbose_name_plural = "Object Classes"
class Property(concept):
"""
Quality common to all members of an :model:`aristotle_mdr.ObjectClass`
(3.2.100)
"""
template = "aristotle_mdr/concepts/property.html"
class Meta:
verbose_name_plural = "Properties"
class Measure(unmanagedObject):
"""
Measure_Class is a class each instance of which models a measure class (3.2.72),
a set of equivalent units of measure (3.2.138) that may be shared across multiple
dimensionalities (3.2.58). Measure_Class allows a grouping of units of measure to
be specified once, and reused by multiple dimensionalities.
NB. A measure is not defined as a concept in ISO 11179 (11.4.2.2)
"""
template = "aristotle_mdr/unmanaged/measure.html"
class UnitOfMeasure(concept):
"""
actual units in which the associated values are measured
:model:`aristotle_mdr.ValueDomain` (3.2.138)
"""
class Meta:
verbose_name_plural = "Units Of Measure"
template = "aristotle_mdr/concepts/unitOfMeasure.html"
measure = models.ForeignKey(Measure, blank=True, null=True)
symbol = models.CharField(max_length=20, blank=True)
class DataType(concept):
"""
set of distinct values, characterized by properties of those values and
by operations on those values (3.1.9)
"""
template = "aristotle_mdr/concepts/dataType.html"
class ConceptualDomain(concept):
"""
Concept that expresses its description or valid instance meanings (3.2.21)
"""
# Implementation note: Since a Conceptual domain "must be either one or
# both an Enumerated Conceptual or a Described_Conceptual_Domain" there is
# no reason to model them separately.
template = "aristotle_mdr/concepts/conceptualDomain.html"
description = models.TextField(
_('description'),
blank=True,
help_text=('Description or specification of a rule, reference, or '
'range for a set of all value meanings for a Conceptual '
'Domain')
)
class ValueMeaning(aristotleComponent):
"""
Value_Meaning is a class each instance of which models a value meaning (3.2.141),
which provides semantic content of a possible value (11.3.2.3.2).
"""
class Meta:
ordering = ['order']
meaning = models.CharField( # 3.2.141
max_length=255,
help_text=_('The semantic content of a possible value (3.2.141)')
)
conceptual_domain = models.ForeignKey(ConceptualDomain)
order = models.PositiveSmallIntegerField("Position")
start_date = models.DateField(
blank=True,
null=True,
help_text='Date at which the value meaning became valid'
)
end_date = models.DateField(
blank=True,
null=True,
help_text='Date at which the value meaning ceased to be valid'
)
def __unicode__(self):
return "%s: %s - %s" % (
self.conceptual_domain.name,
self.value,
self.meaning
)
@property
def parentItem(self):
return self.conceptual_domain
class ValueDomain(concept):
"""
Value_Domain is a class each instance of which models a value domain (3.2.140),
a set of permissible values (3.2.96) (11.3.2.5).
"""
# Implementation note: Since a Value domain "must be either one or
# both an Enumerated Valued or a Described_Value_Domain" there is
# no reason to model them separately.
template = "aristotle_mdr/concepts/valueDomain.html"
data_type = models.ForeignKey( # 11.3.2.5.2.1
DataType,
blank=True,
null=True,
help_text=_('Datatype used in a Value Domain')
)
format = models.CharField( # 11.3.2.5.2.1
max_length=100,
blank=True,
null=True,
help_text=_('template for the structure of the presentation of the value(s)')
)
maximum_length = models.PositiveIntegerField( # 11.3.2.5.2.3
blank=True,
null=True,
help_text=_('maximum number of characters available to represent the Data Element value')
)
unit_of_measure = models.ForeignKey( # 11.3.2.5.2.3
UnitOfMeasure,
blank=True,
null=True,
help_text=_('Unit of Measure used in a Value Domain')
)
conceptual_domain = models.ForeignKey(
ConceptualDomain,
blank=True,
null=True,
help_text=_('The Conceptual Domain that this Value Domain which provides representation.')
)
description = models.TextField(
_('description'),
blank=True,
help_text=('Description or specification of a rule, reference, or '
'range for a set of all values for a Value Domain.')
)
# Below is a dirty, dirty hack that came from re-designing permissible
# values
# TODO: Fix references to permissible and supplementary values
@property
def permissibleValues(self):
return self.permissiblevalue_set.all()
@property
def supplementaryValues(self):
return self.supplementaryvalue_set.all()
class AbstractValue(aristotleComponent):
"""
Implementation note: Not the best name, but there will be times to
subclass a "value" when its not just a permissible value.
"""
class Meta:
abstract = True
ordering = ['order']
value = models.CharField( # 11.3.2.7.2.1 - Renamed from permitted value for abstracts
max_length=32,
help_text=_("the actual value of the Value")
)
meaning = models.CharField( # 11.3.2.7.1
max_length=255,
help_text=_("A textual designation of a value, where a relation to a Value meaning doesn't exist")
)
value_meaning = models.ForeignKey( # 11.3.2.7.1
ValueMeaning,
blank=True,
null=True,
help_text=_('A reference to the value meaning that this designation relates to')
)
# Below will generate exactly the same related name as django, but reversion-compare
# needs an explicit related_name for some actions.
valueDomain = models.ForeignKey(
ValueDomain,
related_name="%(class)s_set",
help_text=_("Enumerated Value Domain that this value meaning relates to")
)
order = models.PositiveSmallIntegerField("Position")
start_date = models.DateField(
blank=True,
null=True,
help_text='Date at which the value became valid'
)
end_date = models.DateField(
blank=True,
null=True,
help_text='Date at which the value ceased to be valid'
)
def __unicode__(self):
return "%s: %s - %s" % (
self.valueDomain.name,
self.value,
self.meaning
)
@property
def parentItem(self):
return self.value_domain
class PermissibleValue(AbstractValue):
"""
Permissible Value is a class each instance of which models a permissible value (3.2.96),
the designation (3.2.51) of a value meaning (3.2.141).
"""
pass
class SupplementaryValue(AbstractValue):
pass
class DataElementConcept(concept):
"""
Data Element Concept is a class each instance of which models a data element concept (3.2.29).
A data element concept is a specification of a concept (3.2.18) independent of any particular representation.
A data element concept can be represented in the form of a data element (3.2.28).
Concept that is an association of a :model:`aristotle_mdr.Property`
with an :model:`aristotle_mdr.ObjectClass` (3.2.29) (11.2.2.3)
"""
# Redefine in this context as we need 'property' for the 11179 terminology.
property_ = property
template = "aristotle_mdr/concepts/dataElementConcept.html"
objectClass = models.ForeignKey( # 11.2.3.3
ObjectClass, blank=True, null=True,
help_text=_('references an Object_Class that is part of the specification of the Data_Element_Concept')
)
property = models.ForeignKey( # 11.2.3.1
Property, blank=True, null=True,
help_text=_('references a Property that is part of the specification of the Data_Element_Concept')
)
conceptualDomain = models.ForeignKey( # 11.2.3.2
ConceptualDomain, blank=True, null=True,
help_text=_('references a Conceptual_Domain that is part of the specification of the Data_Element_Concept')
)
@property_
def registry_cascade_items(self):
out = []
if self.objectClass:
out.append(self.objectClass)
if self.property:
out.append(self.property)
return out
def get_download_items(self):
return [
(ObjectClass, ObjectClass.objects.filter(dataelementconcept=self)),
(Property, Property.objects.filter(dataelementconcept=self)),
]
# Yes this name looks bad - blame 11179:3:2013 for renaming "administered item"
# to "concept".
class DataElement(concept):
"""
Unit of data that is considered in context to be indivisible (3.2.28)"""
template = "aristotle_mdr/concepts/dataElement.html"
dataElementConcept = models.ForeignKey( # 11.5.3.2
DataElementConcept,
verbose_name="Data Element Concept",
blank=True,
null=True,
help_text=_("binds with a Value_Domain that describes a set of possible values that may be recorded in an instance of the Data_Element")
)
valueDomain = models.ForeignKey( # 11.5.3.1
ValueDomain,
verbose_name="Value Domain",
blank=True,
null=True,
help_text=_("binds with a Data_Element_Concept that provides the meaning for the Data_Element")
)
@property
def registry_cascade_items(self):
out = []
if self.valueDomain:
out.append(self.valueDomain)
if self.dataElementConcept:
out.append(self.dataElementConcept)
out += self.dataElementConcept.registry_cascade_items
return out
def get_download_items(self):
return [
(ObjectClass, ObjectClass.objects.filter(dataelementconcept=self.dataElementConcept)),
(Property, Property.objects.filter(dataelementconcept=self.dataElementConcept)),
(DataElementConcept, DataElementConcept.objects.filter(dataelement=self)),
(ValueDomain, ValueDomain.objects.filter(dataelement=self)),
]
class DataElementDerivation(concept):
"""
Application of a derivation rule to one or more
input :model:`aristotle_mdr.DataElement`\s to derive one or more
output :model:`aristotle_mdr.DataElement`\s (3.2.33)
"""
derives = models.ForeignKey( # 11.5.3.5
DataElement,
related_name="derived_from",
blank=True,
null=True,
help_text=_("binds with one or more output Data_Elements that are the result of the application of the Data_Element_Derivation.")
)
inputs = models.ManyToManyField( # 11.5.3.4
DataElement,
related_name="input_to_derivation",
blank=True,
help_text=_("binds one or more input Data_Element(s) with a Data_Element_Derivation.")
)
derivation_rule = models.TextField(
blank=True,
help_text=_("text of a specification of a data element Derivation_Rule")
)
# Create a 1-1 user profile so we don't need to extend user
# Thanks to http://stackoverflow.com/a/965883/764357
class PossumProfile(models.Model):
user = models.OneToOneField(
User,
related_name='profile'
)
savedActiveWorkgroup = models.ForeignKey(
Workgroup,
blank=True,
null=True
)
favourites = models.ManyToManyField(
_concept,
related_name='favourited_by',
blank=True
)
# Override save for inline creation of objects.
# http://stackoverflow.com/questions/2813189/django-userprofile-with-unique-foreign-key-in-django-admin
def save(self, *args, **kwargs):
try:
existing = PossumProfile.objects.get(user=self.user)
self.id = existing.id # Force update instead of insert.
except PossumProfile.DoesNotExist: # pragma: no cover
pass
models.Model.save(self, *args, **kwargs)
@property
def activeWorkgroup(self):
return self.savedActiveWorkgroup or None
@property
def workgroups(self):
if self.user.is_superuser:
return Workgroup.objects.all()
else:
return (
self.user.viewer_in.all() |
self.user.submitter_in.all() |
self.user.steward_in.all() |
self.user.workgroup_manager_in.all()
).distinct()
@property
def myWorkgroups(self):
return self.workgroups.filter(archived=False)
@property
def editable_workgroups(self):
if self.user.is_superuser:
return Workgroup.objects.all()
else:
return (
self.user.submitter_in.all() |
self.user.steward_in.all()
).distinct().filter(archived=False)
@property
def is_registrar(self):
return perms.user_is_registrar(self.user)
@property
def discussions(self):
return DiscussionPost.objects.filter(
workgroup__in=self.myWorkgroups.all()
)
@property
def registrarAuthorities(self):
"NOTE: This is a list of Authorities the user is a *registrar* in!."
if self.user.is_superuser:
return RegistrationAuthority.objects.all()
else:
return self.user.registrar_in.all()
def is_workgroup_manager(self, wg=None):
return perms.user_is_workgroup_manager(self.user, wg)
def is_favourite(self, item):
return self.favourites.filter(pk=item.pk).exists()
def toggleFavourite(self, item):
if self.is_favourite(item):
self.favourites.remove(item)
else:
self.favourites.add(item)
def create_user_profile(sender, instance, created, **kwargs):
if created:
profile, created = PossumProfile.objects.get_or_create(user=instance)
post_save.connect(create_user_profile, sender=User)
@receiver(post_save)
def concept_saved(sender, instance, **kwargs):
if not issubclass(sender, _concept):
return
if not instance.non_cached_fields_changed:
# If the only thing that has changed is a cached public/locked status
# then don't notify.
return
if kwargs.get('raw'):
# Don't run during loaddata
return
kwargs['changed_fields'] = instance.changed_fields
fire("concept_changes.concept_saved", obj=instance, **kwargs)
@receiver(post_save, sender=DiscussionComment)
def new_comment_created(sender, **kwargs):
comment = kwargs['instance']
post = comment.post
if kwargs.get('raw'):
# Don't run during loaddata
return
if not kwargs['created']:
return # We don't need to notify a topic poster of an edit.
if comment.author == post.author:
return # We don't need to tell someone they replied to themselves
fire("concept_changes.new_comment_created", obj=comment)
@receiver(post_save, sender=DiscussionPost)
def new_post_created(sender, **kwargs):
post = kwargs['instance']
if kwargs.get('raw'):
# Don't run during loaddata
return
if not kwargs['created']:
return # We don't need to notify a topic poster of an edit.
fire("concept_changes.new_post_created", obj=post, **kwargs)
@receiver(post_save, sender=Status)
def states_changed(sender, instance, *args, **kwargs):
item = instance.concept
kwargs['status_id'] = instance.pk
fire("concept_changes.status_changed", obj=item, **kwargs)