"""
Models for the :mod:`annotations` app.
Texts and projects
------------------
.. autosummary::
:nosignatures:
Text
TextCollection
Annotations
-----------
.. autosummary::
:nosignatures:
Annotation
Appellation
DateAppellation
Interpreted
QuadrigaAccession
Relation
RelationSet
RelationTemplate
RelationTemplatePart
Users and groups
----------------
.. autosummary::
:nosignatures:
GroupManager
VogonGroup
VogonUser
VogonUserManager
Detailed descriptions
---------------------
"""
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from concepts.models import Concept
from django.conf import settings
import ast
from annotations.managers import repositoryManagers
from annotations.utils import help_text
from django.contrib.auth.models import (
BaseUserManager, AbstractBaseUser, PermissionsMixin, Permission
)
from django.utils.translation import ugettext_lazy as _
from concepts.models import Concept, Type
from annotations.managers import repositoryManagers
import ast
import networkx as nx
[docs]class VogonUserManager(BaseUserManager):
[docs] def create_user(self, username, email, password=None, full_name=None, affiliation=None, location=None, link=None):
if not email:
raise ValueError('Users must have an email address')
user = self.model(
username=username,
email=self.normalize_email(email),
full_name=full_name,
affiliation=affiliation,
location=location,
link=link
)
user.set_password(password)
user.save(using=self._db)
return user
[docs] def create_superuser(self, username, email, password):
user = self.create_user(
username,
email,
password=password,
)
user.is_admin = True
user.save(using=self._db)
return user
[docs]class VogonUser(AbstractBaseUser, PermissionsMixin):
username = models.CharField(max_length=255, unique=True)
email = models.EmailField(
verbose_name='email address',
max_length=255,
)
affiliation = models.CharField(max_length=255, blank=True, null=True,
help_text="Your home institution or employer.")
"""The user's home institution or employer."""
location = models.CharField(max_length=255, blank=True, null=True,
help_text="Your current geographical location.")
"""The user's current geographical location."""
link = models.URLField(max_length=500, blank=True, null=True,
help_text="The location of your online bio or homepage.")
"""The location of the user's online bio or homepage."""
full_name = models.CharField(max_length=255, blank=True, null=True)
conceptpower_uri = models.URLField(max_length=500, blank=True, null=True,
help_text=help_text("""Advanced: if you
have an entry for yourself in the
Conceptpower authority service, please
enter it here."""))
"""
Ideally, each :class:`.VogonUser` will have a corresponding record in
Conceptpower that we can submit to Quadriga along with annotations. This is
not typicaly at the moment, but we should create a mechanism to make this
easy.
"""
imagefile = models.URLField(blank=True, null=True,
help_text="Upload a profile picture.")
"""
Location of the user's profile picture. This will usually be in our AWS S3
bucket.
"""
is_active = models.BooleanField(default=True, help_text=help_text("""Un-set
this field to deactivate a user. This is
extremely preferable to deletion."""))
"""If this field is ``False``, the user will not be able to log in."""
is_admin = models.BooleanField(default=False)
objects = VogonUserManager()
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['email']
[docs] def get_full_name(self):
return self.username
[docs] def get_short_name(self):
return self.username
def __unicode__(self):
return self.username
[docs] def has_perm(self, perm, obj=None):
"""
Does the user have a specific permission?
"""
# Simplest possible answer: Yes, always
return True
[docs] def has_module_perms(self, app_label):
"""
Does the user have permissions to view the app `app_label`?
"""
# Simplest possible answer: Yes, always
return True
@property
def is_staff(self):
"""
Is the user a member of staff?
Returns
-------
bool
"""
# Simplest possible answer: All admins are staff
return self.is_admin
@property
def uri(self):
"""
The Uniform Resource Identifier (URI) for this :class:`.VogonUser`\.
This is required for accessioning annotations into Quadriga.
Returns
-------
unicode
"""
if self.conceptpower_uri:
return self.conceptpower_uri
return settings.BASE_URI_NAMESPACE + reverse('user_details', args=[self.id])
[docs]class GroupManager(models.Manager):
"""
The manager for the auth's Group model.
"""
use_in_migrations = True
[docs] def get_by_natural_key(self, name):
return self.get(name=name)
[docs]class VogonGroup(models.Model):
name = models.CharField(_('name'), max_length=80, unique=True)
permissions = models.ManyToManyField(
Permission,
verbose_name=_('permissions'),
blank=True,
)
objects = GroupManager()
class Meta:
verbose_name = _('group')
verbose_name_plural = _('groups')
def __init__(self):
return self.name
[docs] def natural_key(self):
return (self.name,)
[docs]class TupleField(models.TextField):
__metaclass__ = models.SubfieldBase
description = "Stores a Python tuple of instances of built-in types"
def __init__(self, *args, **kwargs):
super(TupleField, self).__init__(*args, **kwargs)
[docs] def to_python(self, value):
if not value:
value = tuple()
if isinstance(value, tuple):
return value
try:
value = ast.literal_eval(value)
except ValueError:
pass
return value
[docs] def get_prep_value(self, value):
if value is None:
return value
return unicode(value)
[docs] def value_to_string(self, obj):
value = self._get_val_from_obj(obj)
return self.get_prep_value(value)
[docs]class QuadrigaAccession(models.Model):
"""
Records the event that a set of :class:`.RelationSet`\s are accessioned to
Quadriga.
"""
created = models.DateTimeField(auto_now_add=True)
createdBy = models.ForeignKey('VogonUser', related_name='accessions')
project_id = models.CharField(max_length=255, blank=True, null=True)
workspace_id = models.CharField(max_length=255, blank=True, null=True)
network_id = models.CharField(max_length=255, blank=True, null=True)
[docs]class TextCollection(models.Model):
"""
This is referred to as a "Project" in most cases.
"""
name = models.CharField(max_length=255)
description = models.TextField()
ownedBy = models.ForeignKey(VogonUser, related_name='collections')
texts = models.ManyToManyField('Text', related_name='partOf',
blank=True, null=True)
created = models.DateTimeField(auto_now_add=True)
participants = models.ManyToManyField(VogonUser,
related_name='contributes_to')
quadriga_id = models.CharField(max_length=255, blank=True, null=True,
help_text=help_text("""Use this field to
specify the ID of an existing project in
Quadriga with which this project should be
associated."""))
"""
This ID will be used when submitting :class:`.RelationSet`\s to Quadriga.
If not set, the default value (see ``QUADRIGA_PROJECT`` in settings) will
be used instead.
"""
def __unicode__(self):
return self.name
[docs]class Text(models.Model):
"""
Represents a document that is available for annotation.
.. todo:: Add a field to store arbitrary metadata about the document.
"""
uri = models.CharField(max_length=255, unique=True, help_text=help_text(
"""
Uniform Resource Identifier. This should be sufficient to retrieve text
from a repository.
"""))
"""
This identifier is used when submitting :class:`.RelationSet`\s to
Quadriga.
.. todo:: Make this field non-changeable once it is set.
"""
tokenizedContent = models.TextField()
"""
Text should already be tagged, with <word> elements delimiting tokens.
"""
title = models.CharField(max_length=1000, help_text=help_text("""The
original title of the document."""))
"""The original title of the document."""
created = models.DateField(blank=True, null=True, help_text=help_text("""The
publication or creation date of the original
document."""))
"""The publication or creation date of the original document."""
added = models.DateTimeField(auto_now_add=True)
"""The date and time when the text was added to VogonWeb."""
addedBy = models.ForeignKey(VogonUser, related_name="addedTexts")
"""The user who added the text to VogonWeb."""
source = models.ForeignKey("Repository", blank=True, null=True,
related_name="loadedTexts")
"""
The repository (if applicable) from which the text was retrieved.
.. todo:: This should target :class:`repository.Repository` rather than
:class:`annotations.Repository`\.
"""
originalResource = models.URLField(blank=True, null=True)
"""
The (online) location of the original resource, or its digital
surrogate.
"""
annotators = models.ManyToManyField(VogonUser, related_name="userTexts")
"""
If a text is non-public, these users are authorized to access and
annotate that text.
"""
public = models.BooleanField(default=True)
"""
If ``True`` (default), the full content of this text will be made publicly
available.
"""
@property
def annotation_count(self):
"""
The combined number of :class:`.Appellation`\s and :class:`.Relation`\s
that have been created using this text.
"""
return self.appellation_set.count() + self.relation_set.count()
@property
def relation_count(self):
"""
The number of :class:`.RelationSet`\s that have been created using this
text.
"""
return self.relation_set.count()
def __unicode__(self):
return self.title
class Meta:
permissions = (
('view_text', 'View text'),
)
[docs]class Repository(models.Model):
"""
Represents an online repository from which :class:`.Text`\s can be
retrieved.
.. deprecated:: 0.5
Use :class:`repository.models.Repository` instead.
We assume that there is a manager (see :mod:`annotations.managers`\) for
each :class:`.Repository` that provides CRUD methods.
.. todo:: Can we gracefully remove this without breaking migrations?
"""
name = models.CharField(max_length=255)
"""The human-readable name that will be presented to end users."""
manager = models.CharField(max_length=255, choices=repositoryManagers)
"""The name of the manager class for this repository."""
endpoint = models.CharField(max_length=255)
"""The base URL for the repository API."""
oauth_client_id = models.CharField(max_length=255)
"""
.. todo:: This should be moved to a more general formatted configuration
in :mod:`repository`\.
"""
oauth_secret_key = models.CharField(max_length=255)
"""
.. todo:: This should be moved to a more general formatted configuration
in :mod:`repository`\.
"""
def __unicode__(self):
return unicode(self.name)
[docs]class Authorization(models.Model):
"""
Represents an authorization token for an external service.
.. deprecated:: 0.5
Repository-related models and methods should be implemented in
:mod:`repository`\.
"""
repository = models.ForeignKey('Repository')
user = models.ForeignKey(VogonUser, related_name='authorizations')
access_token = models.CharField(max_length=255)
token_type = models.CharField(max_length=255)
lifetime = models.IntegerField(default=0)
refresh_token = models.CharField(max_length=255, blank=True)
[docs]class Annotation(models.Model):
"""
Mixin (abstract) for text-based annotations.
Provides fields for :class:`.Text` association, the creation event, and
Quadriga accession.
"""
occursIn = models.ForeignKey("Text")
"""The :class:`.Text` to which the :class:`.Annotation` refers."""
created = models.DateTimeField(auto_now_add=True)
"""The date and time that the :class:`.Annotation` was created."""
createdBy = models.ForeignKey(VogonUser)
"""The :class:`.VogonUser` who created the :class:`.Annotation`\."""
submitted = models.BooleanField(default=False)
"""
Indicates whether or not the :class:`.Annotation` has been accessioned to
Quadriga.
"""
submittedOn = models.DateTimeField(null=True, blank=True)
"""
The date and time that the :class:`.Annotation` was accessioned to Quadriga.
"""
submittedWith = models.ForeignKey('QuadrigaAccession', blank=True,
null=True)
"""
If the :class:`.Annotation` has been added to Quadriga, this refers to the
:class:`.QuadrigaAccession` with which it was submitted.
"""
[docs]class Interpreted(models.Model):
"""
Mixin for :class:`.Annotation`\s that refer to a :class:`concepts.Concept`\.
.. todo:: Should this subclass :class:`Annotation`\? Does it matter?
"""
interpretation = models.ForeignKey(Concept)
"""The :class:`.Concept` to which the :class:`.Annotation` refers."""
@property
def interpretation_type(self):
"""
The primary-key identifier of the referenced
:class:`concepts.Concept`\s associated :class:`concepts.Type`\.
If the :class:`concepts.Concept` has no type, returns ``None``.
Returns
-------
int or None
"""
if self.interpretation.typed:
return self.interpretation.typed.id
return None
@property
def interpretation_label(self):
"""
The referenced :class:`concepts.Concept`'s lemma/label.
"""
return self.interpretation.label
@property
def interpretation_type_label(self):
"""
The lemma/label of the referenced :class:`concepts.Concept`\'s
associated :class:`concepts.Type`\.
If the :class:`concepts.Concept` has no type, returns ``None``.
Returns
-------
unicode or None
"""
if self.interpretation.typed:
return self.interpretation.typed.label
return None
[docs]class DateAppellation(Annotation):
"""
Dates can be represented as ISO-8601 literals, with variable precision.
"""
year = models.PositiveIntegerField(default=1)
month = models.IntegerField(default=0)
day = models.IntegerField(default=0)
def __unicode__(self):
"""
Returns an ISO-8601 compliant unicode representation of the date.
"""
return u'-'.join([unicode(getattr(self, 'part'))
for part in ['year', 'month', 'day']
if getattr(self, 'part')])
@property
def precision(self):
"""
This is mainly for display. Indicates the precision of the
:class:`.DateAppellation`\: 'year', 'month', or 'day'. Precision will
vary depending on the confidence/interpretation of the user.
"""
if self.day > 0:
return 'day'
elif self.month > 0:
return 'month'
return 'year'
[docs]class Appellation(Annotation, Interpreted):
"""
An Appellation represents a user's interpretation of a specific passage of
text. In particular, it captures the user's belief that the passage in
question refers to a specific concept (e.g. of a person, place, etc).
Notes
-----
``startPos`` and ``endPos`` are deprecated -- these can be created
on-the-fly, and don't apply to non-plain-text use-cases.
``controlling_verb`` is deprecated as of v0.3. This is no longer necessary,
since we now implement the full quadruple model in VogonWeb.
"""
tokenIds = models.TextField()
"""
IDs of words (in the tokenizedContent) selected for this Appellation.
"""
stringRep = models.TextField()
"""
Plain-text snippet spanning the selected text.
"""
startPos = models.IntegerField(blank=True, null=True)
"""
Character offset from the beginning of the (plain text) document.
.. deprecated:: 0.5
Text positions will be represented using :class:`.DocumentPosition`\.
"""
endPos = models.IntegerField(blank=True, null=True)
"""
Character offset from the end of the (plain text) document.
.. deprecated:: 0.5
Text positions will be represented using :class:`.DocumentPosition`\.
"""
# Reverse generic relations to Relation.
relationsFrom = GenericRelation('Relation',
content_type_field='source_content_type',
object_id_field='source_object_id',
related_query_name='source_appellations')
relationsTo = GenericRelation('Relation',
content_type_field='object_content_type',
object_id_field='object_object_id',
related_query_name='object_appellations')
asPredicate = models.BooleanField(default=False)
"""
Indicates whether this Appellation should function as a predicate for a
Relation. As of version 0.3, this basically just controls whether or not
the Appellation should be displayed in the text annotation view.
"""
IS = 'is'
HAS = 'has'
NONE = None
VCHOICES = (
(NONE, ''),
(IS, 'is/was'),
(HAS, 'has/had'),
)
controlling_verb = models.CharField(max_length=4, choices=VCHOICES,
null=True, blank=True)
"""
.. deprecated:: 0.4
We now fully implement the quadruple data model, so this is no longer
relevant.
"""
[docs]class RelationSet(models.Model):
"""
A :class:`.RelationSet` organizes :class:`.Relation`\s into complete
statements.
"""
template = models.ForeignKey('RelationTemplate', blank=True, null=True,
related_name='instantiations')
"""
If this RelationSet was created from a RelationTemplate, we can use the
template to make decisions about display.
"""
created = models.DateTimeField(auto_now_add=True)
createdBy = models.ForeignKey('VogonUser')
"""The user who created the RelationSet."""
occursIn = models.ForeignKey('Text', related_name='relationsets')
"""The text on which this RelationSet is based."""
pending = models.BooleanField(default=False)
"""
A :class:`.RelationSet` is pending if it has been selected for submission,
but the submission process has not yet completed. The primary purpose of
this field is to prevent duplicate submissions.
"""
submitted = models.BooleanField(default=False)
"""
Whether or not the :class:`.RelationSet` has been accessioned to Quadriga.
This is set ``True`` only if the :class:`.RelationSet` was added
successfully.
"""
submittedOn = models.DateTimeField(null=True, blank=True)
"""
The date/time when the :class:`.RelationSet` was (successfully) accessioned
to Quadriga.
"""
submittedWith = models.ForeignKey('QuadrigaAccession', blank=True)
"""
The :class:`.QuadrigaAccession` tracks the entire set of RelationSets that
were accessioned together in a single query.
"""
@property
def root(self):
"""
Identifies and retrieves the highest-level or "starting"
:class:`.Relation` in the :class:`.RelationSet`\.
"""
# If this RelationSet is not nested, then there will by only one
# Relation -- that's all we need.
if self.constituents.count() == 1:
return self.constituents.first()
relation_type = ContentType.objects.get_for_model(Relation)
# The "starting" Relation will be the only Relation that is not
# referenced by any other Relation. If we represent the RelationSet
# as a directed graph, this will be the node with an in-degree of 0.
dg = nx.DiGraph()
for relation in self.constituents.all():
for part in ['source', 'object']:
target_type = getattr(relation, '%s_content_type' % part)
if target_type.id == relation_type.id:
target = getattr(relation, '%s_object_id' % part)
# Each node represents a Relation by its primary key ID.
dg.add_edge(relation.id, target)
# Topological sort is supposed to be faster than calculating in-degree
# and searching for the 0-valued node.
return Relation.objects.get(pk=nx.topological_sort(dg)[0])
@property
def label(self):
"""
The label displayed in lists of :class:`RelationSet`\s.
Returns
-------
unicode
"""
if self.template:
return self.template.name
label = u'Untemplated relation created by %s at %s' % (self.createdBy,
self.created)
return label
[docs] def ready(self):
"""
Check whether or not the constituent :class:`.Concept`\s in this
:class:`.RelationSet` have been resolved (or merged).
This aids the process of submitting annotations to Quadriga: all
:class:`.Concept`\s must be present in Conceptpower prior to submission.
Returns
-------
bool
"""
criteria = lambda s: s[0] == Concept.RESOLVED or s[1]
values = self.concepts().values_list('concept_state', 'merged_with')
return all(map(criteria, values))
ready.boolean = True # So that we can display a nifty icon in changelist.
[docs] def appellations(self):
"""
Get all non-predicate appellations in child :class:`.Relation`\s.
Returns
-------
:class:`django.db.models.query.QuerySet`
"""
appellation_type = ContentType.objects.get_for_model(Appellation)
appellation_ids = []
for relation in self.constituents.all():
if relation.source_content_type == appellation_type:
appellation_ids.append(relation.source_object_id)
if relation.object_content_type == appellation_type:
appellation_ids.append(relation.object_object_id)
return Appellation.objects.filter(pk__in=appellation_ids)
[docs] def concepts(self):
"""
Get all of the Concept instances connected to non-predicate
Appellation instances.
Returns
-------
:class:`django.db.models.query.QuerySet`
"""
qs = self.appellations().values_list('interpretation_id', flat=True)
interpretation_ids = list(qs) # <-- DB hit.
return Concept.objects.filter(pk__in=interpretation_ids)
[docs]class Relation(Annotation):
"""
A :class:`.Relation` captures a user's assertion that a passage of text
implies a specific relation between two concepts.
The ``source`` and/or ``object`` of the :class:`.Relation` can be a
:class:`.Appellation`\, :class:`.DateAppellation`\, or another
:class:`.Relation`\.
"""
part_of = models.ForeignKey('RelationSet', blank=True, null=True,
related_name='constituents')
source_content_type = models.ForeignKey(ContentType,
on_delete=models.CASCADE,
related_name='as_source_in_relation',
null=True, blank=True)
source_object_id = models.PositiveIntegerField(null=True, blank=True)
source_content_object = GenericForeignKey('source_content_type',
'source_object_id')
predicate = models.ForeignKey("Appellation", related_name="relationsAs")
object_content_type = models.ForeignKey(ContentType,
on_delete=models.CASCADE,
related_name='as_object_in_relation',
null=True, blank=True)
object_object_id = models.PositiveIntegerField(null=True, blank=True)
object_content_object = GenericForeignKey('object_content_type',
'object_object_id')
bounds = models.ForeignKey("TemporalBounds", blank=True, null=True)
"""
.. deprecated:: 0.5
We now fully implement the quadruple model in VogonWeb.
"""
[docs]class RelationTemplate(models.Model):
"""
Provides a template for complex relations, allowing the user to simply
fill in fields without worrying about the structure of the quadruple.
.. todo:: Add ``created_by`` field, perhaps others.
"""
name = models.CharField(max_length=255)
"""A descriptive name used in menus in the annotation interface."""
description = models.TextField()
"""A longer-form description of the relation."""
expression = models.TextField(null=True)
"""Pattern for representing the relation in normal language."""
@property
def fields(self):
"""
The fields that we need the user to fill to create a
:class:`.RelationSet` from this :class:`.RelationSet`\.
"""
fields = []
for tpart in self.template_parts.all():
for field in ['source', 'predicate', 'object']:
evidenceRequired = getattr(tpart, '%s_prompt_text' % field)
nodeType = getattr(tpart, '%s_node_type' % field)
# The user needs to provide specific concepts for TYPE fields.
if nodeType == RelationTemplatePart.TYPE:
part_type = getattr(tpart, '%s_type' % field)
part_label = getattr(tpart, '%s_label' % field)
part_description = getattr(tpart, '%s_description' % field)
concept_id = getattr(part_type, 'id', None)
concept_label = getattr(part_type, 'label', None)
fields.append({
'type': 'TP',
'part_id': tpart.id,
'part_field': field,
'concept_id': concept_id,
'label': part_label,
'concept_label': concept_label,
'evidence_required': evidenceRequired,
'description': part_description,
})
# Even if there is an explicit concept, we may require textual
# evidence from the user.
elif evidenceRequired and nodeType == RelationTemplatePart.CONCEPT:
part_concept = getattr(tpart, '%s_concept' % field)
concept_id = getattr(part_concept, 'id', None)
concept_label = getattr(part_concept, 'label', None)
part_label = getattr(tpart, '%s_label' % field)
part_description = getattr(tpart, '%s_description' % field)
fields.append({
'type': 'CO',
'part_id': tpart.id,
'part_field': field,
'concept_id': concept_id,
'label': part_label,
'concept_label': concept_label,
'evidence_required': evidenceRequired,
'description': part_description,
})
return fields
[docs]class RelationTemplatePart(models.Model):
"""
Template for a :class:`.Relation` constituent to a :class:`.RelationSet`\.
"""
TYPE = 'TP'
CONCEPT = 'CO'
TOBE = 'IS'
HAS = 'HA'
RELATION = 'RE'
NODE_CHOICES = (
(TYPE, 'Concept type'),
(CONCEPT, 'Specific concept'),
(RELATION, 'Relation'),
)
PRED_CHOICES = (
(TYPE, 'Concept type'),
(CONCEPT, 'Specific concept'),
(TOBE, 'Is/was'),
(HAS, 'Has/had'),
)
part_of = models.ForeignKey('RelationTemplate',
related_name="template_parts")
internal_id = models.IntegerField(default=-1)
source_node_type = models.CharField(choices=NODE_CHOICES, max_length=2,
null=True, blank=True)
source_label = models.CharField(max_length=100, null=True, blank=True)
source_type = models.ForeignKey(Type, blank=True, null=True,
related_name='used_as_type_for_source')
source_concept = models.ForeignKey(Concept, blank=True, null=True,
related_name='used_as_concept_for_source')
source_relationtemplate = models.ForeignKey('RelationTemplatePart',
blank=True, null=True,
related_name='used_as_source')
source_relationtemplate_internal_id = models.IntegerField(default=-1)
source_prompt_text = models.BooleanField(default=True)
"""Indicates whether the user should be asked for evidence for source."""
source_description = models.TextField(blank=True, null=True)
predicate_node_type = models.CharField(choices=PRED_CHOICES, max_length=2,
null=True, blank=True)
predicate_label = models.CharField(max_length=100, null=True, blank=True)
predicate_type = models.ForeignKey(Type, blank=True, null=True,
related_name='used_as_type_for_predicate')
predicate_concept = models.ForeignKey(Concept, blank=True, null=True,
related_name='used_as_concept_for_predicate')
predicate_prompt_text = models.BooleanField(default=True)
"""
Indicates whether the user should be asked for evidence for predicate.
"""
predicate_description = models.TextField(blank=True, null=True)
object_node_type = models.CharField(choices=NODE_CHOICES, max_length=2,
null=True, blank=True)
object_label = models.CharField(max_length=100, null=True, blank=True)
object_type = models.ForeignKey(Type, blank=True, null=True,
related_name='used_as_type_for_object')
object_concept = models.ForeignKey(Concept, blank=True, null=True,
related_name='used_as_concept_for_object')
object_relationtemplate = models.ForeignKey('RelationTemplatePart',
blank=True, null=True,
related_name='used_as_object')
object_relationtemplate_internal_id = models.IntegerField(default=-1)
object_prompt_text = models.BooleanField(default=True)
"""Indicates whether the user should be asked for evidence for object."""
object_description = models.TextField(blank=True, null=True)
[docs]class TemporalBounds(models.Model):
"""
.. deprecated:: 0.5
We now fully implement the Quadruple model in VogonWeb. See
:class:`.DateAppellation`\.
"""
start = TupleField(blank=True, null=True)
occur = TupleField(blank=True, null=True)
end = TupleField(blank=True, null=True)