--- a/src/iconolab/models.py Mon Dec 12 17:27:33 2016 +0100
+++ b/src/iconolab/models.py Thu Dec 15 16:35:01 2016 +0100
@@ -10,30 +10,18 @@
logger = logging.getLogger(__name__)
-class Tag(models.Model):
- label = models.CharField(max_length=255, blank=True, null=True)
- label_slug = models.SlugField(blank=True, null=True)
- link = models.URLField(unique=True)
- description = models.CharField(max_length=255, blank=True, null=True)
- collection = models.ForeignKey('Collection', blank=True, null=True)
-
- def is_internal(self):
- return self.link.startswith(settings.INTERNAL_TAGS_URL)
-
- def __str__(self):
- return self.label_slug+":"+self.label
-
-class TaggingInfo(models.Model):
- revision = models.ForeignKey('AnnotationRevision', on_delete=models.CASCADE)
- tag = models.ForeignKey('Tag', on_delete=models.CASCADE)
- accuracy = models.IntegerField()
- relevancy = models.IntegerField()
-
- def __str__(self):
- return str(str(self.tag.label_slug)+":to:"+str(self.revision.revision_guid))
-
class Collection(models.Model):
+ """
+ Collection objects are the thematic item repositories in Iconolab
+
+ name: the name displayed in the url and also used to identify the collection
+ verbose_name: the name displayed in the text of the pages
+ description: the short description of the collection that will be displayed by default in pages
+ complete_description: the complete description that will be shown with a "view more" button/link
+ image/height/width: the collection image that will be shown in the collection description
+ show_image_on_home: if True, the collection will appear by default on the homepage as one of the bigger images
+ """
name = models.SlugField(max_length=50, unique=True)
verbose_name = models.CharField(max_length=50, null=True, blank=True)
description = models.TextField(null=True, blank=True, default="")
@@ -48,6 +36,9 @@
class Item(models.Model):
+ """
+ Item objects belong to a collection, are linked to a metadata item, and to one or more images
+ """
collection = models.ForeignKey(Collection, related_name="items")
item_guid = models.UUIDField(default=uuid.uuid4, editable=False)
@@ -59,6 +50,9 @@
return self.images.order_by("-name").all()
class ItemMetadata(models.Model):
+ """
+ Metadata object for the item class. Each field represents what we can import from the provided .csv files
+ """
item = models.OneToOneField('Item', related_name='metadatas')
authors = models.CharField(max_length=255, default="")
school = models.CharField(max_length=255, default="")
@@ -82,44 +76,11 @@
return "metadatas:for:"+str(self.item.item_guid)
-class ImageStats(models.Model):
- image = models.OneToOneField('Image', related_name='stats', blank=False, null=False)
- views_count = models.IntegerField(blank=True, null=True, default=0)
- annotations_count = models.IntegerField(blank=True, null=True, default=0)
- submitted_revisions_count = models.IntegerField(blank=True, null=True, default=0)
- comments_count = models.IntegerField(blank=True, null=True, default=0)
- folders_inclusion_count = models.IntegerField(blank=True, null=True, default=0)
- tag_count = models.IntegerField(blank=True, null=True, default=0)
-
- def __str__(self):
- return "stats:for:"+str(self.image.image_guid)
-
- def set_tags_stats(self):
- self.tag_count = Tag.objects.filter(tagginginfo__revision__annotation__image = self.image).distinct().count()
+class Image(models.Model):
+ """
+ Each image object is linked to one item, users can create annotations on images
+ """
- @transaction.atomic
- def update_stats(self):
- self.annotations_count = 0
- self.submitted_revisions_count = 0
- self.comments_count = 0
- image_annotations = Annotation.objects.filter(image=self.image)
- # views_count - Can't do much about views count
- # annotations_count
- self.annotations_count = image_annotations.count()
- # submitted_revisions_count & comment_count
- for annotation in image_annotations.all():
- annotation_revisions = annotation.revisions
- self.submitted_revisions_count += annotation_revisions.count()
-
- self.comments_count += XtdComment.objects.for_app_models("iconolab.annotation").filter(
- object_pk = annotation.pk,
- ).count()
- # tag_count
- self.tag_count = Tag.objects.filter(tagginginfo__revision__annotation__image = self.image).distinct().count()
- self.save()
-
-
-class Image(models.Model):
image_guid = models.UUIDField(default=uuid.uuid4, editable=False)
name = models.CharField(max_length=200)
media = models.ImageField(upload_to='uploads/', height_field='height', width_field='width')
@@ -175,11 +136,58 @@
tag_list += [tag_infos['tag_label'] for tag_infos in revision_tags if tag_infos.get('tag_label') is not None] #deal with
return tag_list
-class AnnotationManager(models.Manager):
+
+class ImageStats(models.Model):
+ """
+ Stats objects for a given image, keep count of several values to be displayed in image and item pages
+ """
+ image = models.OneToOneField('Image', related_name='stats', blank=False, null=False)
+ views_count = models.IntegerField(blank=True, null=True, default=0)
+ annotations_count = models.IntegerField(blank=True, null=True, default=0)
+ submitted_revisions_count = models.IntegerField(blank=True, null=True, default=0)
+ comments_count = models.IntegerField(blank=True, null=True, default=0)
+ folders_inclusion_count = models.IntegerField(blank=True, null=True, default=0)
+ tag_count = models.IntegerField(blank=True, null=True, default=0)
+
+ def __str__(self):
+ return "stats:for:"+str(self.image.image_guid)
+
+ def set_tags_stats(self):
+ self.tag_count = Tag.objects.filter(tagginginfo__revision__annotation__image = self.image).distinct().count()
- # Call Annotation.objects.create_annotation to initialize a new Annotation with its associated AnnotationStats and initial AnnotationRevision
+ @transaction.atomic
+ def update_stats(self):
+ self.annotations_count = 0
+ self.submitted_revisions_count = 0
+ self.comments_count = 0
+ image_annotations = Annotation.objects.filter(image=self.image)
+ # views_count - Can't do much about views count
+ # annotations_count
+ self.annotations_count = image_annotations.count()
+ # submitted_revisions_count & comment_count
+ for annotation in image_annotations.all():
+ annotation_revisions = annotation.revisions
+ self.submitted_revisions_count += annotation_revisions.count()
+
+ self.comments_count += XtdComment.objects.for_app_models("iconolab.annotation").filter(
+ object_pk = annotation.pk,
+ ).count()
+ # tag_count
+ self.tag_count = Tag.objects.filter(tagginginfo__revision__annotation__image = self.image).distinct().count()
+ self.save()
+
+
+class AnnotationManager(models.Manager):
+ """
+ Manager class for annotation, it handles annotation creation (with initial revision creation, and
+ has methods to get a list of annotation commented for a given user, and a list of annotations contributed for a
+ given user
+ """
@transaction.atomic
def create_annotation(self, author, image, title='', description='', fragment='', tags_json='[]'):
+ """
+ Creates a new Annotation with its associated AnnotationStats and initial AnnotationRevision
+ """
# Create annotation object
new_annotation = Annotation(
image=image,
@@ -299,7 +307,131 @@
return sorted_annotations_list
+class Annotation(models.Model):
+ """
+ Annotation objects are created on a given image, each annotation have a list of revisions to keep track of its history, the latest revision is the 'current revision'
+ that will be displayed by default in most pages.
+
+ Annotation data (title, description, fragment) is thus stored in the revision.
+ """
+ annotation_guid = models.UUIDField(default=uuid.uuid4, editable=False)
+ image = models.ForeignKey('Image', related_name='annotations', on_delete=models.CASCADE)
+ source_revision = models.ForeignKey('AnnotationRevision', related_name='source_related_annotation', blank=True, null=True)
+ current_revision = models.OneToOneField('AnnotationRevision', related_name='current_for_annotation', blank=True, null=True)
+ author = models.ForeignKey(User, null=True)
+ created = models.DateTimeField(auto_now_add=True, null=True)
+ comments = GenericRelation('IconolabComment', content_type_field='content_type_id', object_id_field='object_pk')
+
+ objects = AnnotationManager()
+
+ def __str__(self):
+ return str(self.annotation_guid)+":"+self.current_revision.title
+
+ @property
+ def awaiting_revisions_count(self):
+ return self.revisions.filter(state=AnnotationRevision.AWAITING).distinct().count()
+
+ @property
+ def accepted_revisions_count(self):
+ return self.revisions.filter(state=AnnotationRevision.ACCEPTED).distinct().count()
+
+ @property
+ def rejected_revisions_count(self):
+ return self.revisions.filter(state=AnnotationRevision.REJECTED).distinct().count()
+
+ @property
+ def studied_revisions_count(self):
+ return self.revisions.filter(state=AnnotationRevision.STUDIED).distinct().count()
+
+ @property
+ def total_revisions_count(self):
+ return self.revisions.distinct().count()
+
+ @property
+ def collection(self):
+ return self.image.collection
+
+ @property
+ def tag_labels(self):
+ current_revision_tags = json.loads(self.current_revision.get_tags_json())
+ return [tag_infos['tag_label'] for tag_infos in current_revision_tags if tag_infos.get('tag_label') is not None ]
+
+ def latest_revision_for_user(self, user):
+ user_revisions = self.revisions.filter(creator=user)
+ if user_revisions.exists():
+ return user_revisions.filter(creator=author).order_by("-created").first()
+ return None
+
+ @transaction.atomic
+ def make_new_revision(self, author, title, description, fragment, tags_json):
+ """
+ Called to create a new revision, potentially from a merge
+ """
+ if author == self.author:
+ # We're creating an automatically accepted revision
+ new_revision_state = AnnotationRevision.ACCEPTED
+ else:
+ # Revision will require validation
+ new_revision_state = AnnotationRevision.AWAITING
+ new_revision = AnnotationRevision(
+ annotation = self,
+ parent_revision=self.current_revision,
+ title=title,
+ description=description,
+ author=author,
+ fragment=fragment,
+ state=new_revision_state
+ )
+ new_revision.save()
+ new_revision.set_tags(tags_json)
+ if new_revision.state == AnnotationRevision.ACCEPTED:
+ self.current_revision = new_revision
+ self.save()
+ iconolab_signals.revision_created.send(sender=AnnotationRevision, instance=new_revision)
+ return new_revision
+
+ @transaction.atomic
+ def validate_existing_revision(self, revision_to_validate):
+ """
+ Called when we're validating an awaiting revision whose parent is the current revision AS IT WAS CREATED
+ """
+ if revision_to_validate.parent_revision == self.current_revision and revision_to_validate.state == AnnotationRevision.AWAITING:
+ self.current_revision = revision_to_validate
+ revision_to_validate.state = AnnotationRevision.ACCEPTED
+ revision_to_validate.save()
+ self.save()
+ iconolab_signals.revision_accepted.send(sender=AnnotationRevision, instance=revision_to_validate)
+
+ @transaction.atomic
+ def reject_existing_revision(self, revision_to_reject):
+ """
+ Called when we reject a revision
+ """
+ if revision_to_reject.state == AnnotationRevision.AWAITING:
+ revision_to_reject.state = AnnotationRevision.REJECTED
+ revision_to_reject.save()
+ iconolab_signals.revision_rejected.send(sender=AnnotationRevision, instance=revision_to_reject)
+
+ @transaction.atomic
+ def merge_existing_revision(self, title, description, fragment, tags, revision_to_merge):
+ """
+ Called when we're validating an awaiting revision whose parent isn't the current revision or if the awaiting revision was modified by the annotation author
+ """
+ merged_revision = self.make_new_revision(author=self.author, title=title, description=description, fragment=fragment, tags_json=tags)
+ merged_revision.merge_parent_revision = revision_to_merge
+ merged_revision.save()
+ revision_to_merge.state = AnnotationRevision.STUDIED
+ revision_to_merge.save()
+ iconolab_signals.revision_accepted.send(sender=AnnotationRevision, instance=revision_to_merge)
+ self.current_revision=merged_revision
+ self.save()
+ return merged_revision
+
+
class AnnotationStats(models.Model):
+ """
+ Stats objects for a given annotation, keep count of several values to be displayed in annotation pages
+ """
annotation = models.OneToOneField('Annotation', related_name='stats', blank=False, null=False)
submitted_revisions_count = models.IntegerField(blank=True, null=True, default=1)
awaiting_revisions_count = models.IntegerField(blank=True, null=True, default=0)
@@ -372,8 +504,12 @@
m2m_object.save()
self.set_tags_stats()
self.save()
-
+
+
class MetaCategoriesCountInfo(models.Model):
+ """
+ M2M class to keep a count of a given metacategory on a given annotation. metacategories are linked to comments, themselve linked to an annotation
+ """
annotation_stats_obj = models.ForeignKey('AnnotationStats', on_delete=models.CASCADE)
metacategory = models.ForeignKey('MetaCategory', on_delete=models.CASCADE)
count = models.IntegerField(default=1, blank=False, null=False)
@@ -382,115 +518,20 @@
return "metacategory_count_for:"+self.metacategory.label+":on:"+str(self.annotation_stats_obj.annotation.annotation_guid)
-class Annotation(models.Model):
- annotation_guid = models.UUIDField(default=uuid.uuid4, editable=False)
- image = models.ForeignKey('Image', related_name='annotations', on_delete=models.CASCADE)
- source_revision = models.ForeignKey('AnnotationRevision', related_name='source_related_annotation', blank=True, null=True)
- current_revision = models.OneToOneField('AnnotationRevision', related_name='current_for_annotation', blank=True, null=True)
- author = models.ForeignKey(User, null=True)
- created = models.DateTimeField(auto_now_add=True, null=True)
- comments = GenericRelation('IconolabComment', content_type_field='content_type_id', object_id_field='object_pk')
-
- objects = AnnotationManager()
-
- def __str__(self):
- return str(self.annotation_guid)+":"+self.current_revision.title
+class AnnotationRevision(models.Model):
+ """
+ AnnotationRevisions objects are linked to an annotation and store the data of the annotation at a given time
+
+ A revision is always in one out of multiple states:
- @property
- def awaiting_revisions_count(self):
- return self.revisions.filter(state=AnnotationRevision.AWAITING).distinct().count()
-
- @property
- def accepted_revisions_count(self):
- return self.revisions.filter(state=AnnotationRevision.ACCEPTED).distinct().count()
-
- @property
- def rejected_revisions_count(self):
- return self.revisions.filter(state=AnnotationRevision.REJECTED).distinct().count()
-
- @property
- def studied_revisions_count(self):
- return self.revisions.filter(state=AnnotationRevision.STUDIED).distinct().count()
-
- @property
- def total_revisions_count(self):
- return self.revisions.distinct().count()
-
- @property
- def collection(self):
- return self.image.collection
-
- @property
- def tag_labels(self):
- current_revision_tags = json.loads(self.current_revision.get_tags_json())
- return [tag_infos['tag_label'] for tag_infos in current_revision_tags if tag_infos.get('tag_label') is not None ]
-
- def latest_revision_for_user(self, user):
- user_revisions = self.revisions.filter(creator=user)
- if user_revisions.exists():
- return user_revisions.filter(creator=author).order_by("-created").first()
- return None
-
- # Call to create a new revision, possibly from a merge
- @transaction.atomic
- def make_new_revision(self, author, title, description, fragment, tags_json):
- if author == self.author:
- # We're creating an automatically accepted revision
- new_revision_state = AnnotationRevision.ACCEPTED
- else:
- # Revision will require validation
- new_revision_state = AnnotationRevision.AWAITING
- new_revision = AnnotationRevision(
- annotation = self,
- parent_revision=self.current_revision,
- title=title,
- description=description,
- author=author,
- fragment=fragment,
- state=new_revision_state
- )
- new_revision.save()
- new_revision.set_tags(tags_json)
- if new_revision.state == AnnotationRevision.ACCEPTED:
- self.current_revision = new_revision
- self.save()
- iconolab_signals.revision_created.send(sender=AnnotationRevision, instance=new_revision)
- return new_revision
-
- # Call when we're validating an awaiting revision whose parent is the current revision AS IT WAS CREATED
- @transaction.atomic
- def validate_existing_revision(self, revision_to_validate):
- if revision_to_validate.parent_revision == self.current_revision and revision_to_validate.state == AnnotationRevision.AWAITING:
- self.current_revision = revision_to_validate
- revision_to_validate.state = AnnotationRevision.ACCEPTED
- revision_to_validate.save()
- self.save()
- iconolab_signals.revision_accepted.send(sender=AnnotationRevision, instance=revision_to_validate)
-
- # Call to reject a
- @transaction.atomic
- def reject_existing_revision(self, revision_to_reject):
- if revision_to_reject.state == AnnotationRevision.AWAITING:
- revision_to_reject.state = AnnotationRevision.REJECTED
- revision_to_reject.save()
- iconolab_signals.revision_rejected.send(sender=AnnotationRevision, instance=revision_to_reject)
-
- # Call when we're validating an awaiting revision whose parent isn't the current revision OR IF IT WAS CHANGED BY THE ANNOTATION AUTHOR
- @transaction.atomic
- def merge_existing_revision(self, title, description, fragment, tags, revision_to_merge):
- merged_revision = self.make_new_revision(author=self.author, title=title, description=description, fragment=fragment, tags_json=tags)
- merged_revision.merge_parent_revision = revision_to_merge
- merged_revision.save()
- revision_to_merge.state = AnnotationRevision.STUDIED
- revision_to_merge.save()
- iconolab_signals.revision_accepted.send(sender=AnnotationRevision, instance=revision_to_merge)
- self.current_revision=merged_revision
- self.save()
- return merged_revision
-
-
-class AnnotationRevision(models.Model):
-
+ - Awaiting: the revision has been submitted but must be validated by the original author of the related annotation
+ - Accepted: the revision has been accepted *as-is* by the author of the related annotation (this happens automatically
+ if the revision is created by the author of the annotation)
+ - Rejected: the revision has been rejected by the author of the related annotation
+ - Studied: the revision has been studied by the author of the related annotation and was either modified or at the very least compared with the current state
+ through the merge interface, thus creating a new revision merging the current state with the proposal. At this point the proposal is flagged as "studied" to show
+ that the author of the original annotation has considered it
+ """
AWAITING = 0
ACCEPTED = 1
REJECTED = 2
@@ -519,6 +560,20 @@
return str(self.revision_guid)+":"+self.title
def set_tags(self, tags_json_string):
+ """
+ This method creates tags object and links them to the revision, from a given json that has the following format:
+
+ [
+ {
+ "tag_input": the tag string that has been provided. If it is an http(s?):// pattern, it means the tag is external, else it means it is a custom tag
+ "accuracy": the accuracy value provided by the user
+ "relevancy": the relevancy value provided by the user
+ },
+ {
+ ...
+ }
+ ]
+ """
try:
tags_dict = json.loads(tags_json_string)
except ValueError:
@@ -556,7 +611,25 @@
)
def get_tags_json(self):
-
+ """
+ This method returns the json data that will be sent to the js to display tags for the revision.
+
+ The json data returned will be of the following format:
+
+ [
+ {
+ "tag_label": the tag label for display purposes,
+ "tag_link": the link of the tag, for instance for dbpedia links,
+ "accuracy": the accuracy value of the tag,
+ "relevancy": the relevancy value of the tag,
+ "is_internal": will be True if the tag is 'internal', meaning specific to Iconolab and
+ not an external tag like a dbpedia reference for instance
+ },
+ {
+ ...
+ }
+ ]
+ """
def fetch_from_dbpedia(uri, lang, source):
sparql_template = 'select distinct * where { <<%uri%>> rdfs:label ?l FILTER( langMatches( lang(?l), "<%lang%>" ) ) }'
sparql_query = re.sub("<%uri%>", uri, re.sub("<%lang%>", lang, sparql_template))
@@ -619,8 +692,47 @@
pass
return json.dumps(final_list)
+
+class Tag(models.Model):
+ """
+ Tag objects that are linked to revisions.
+
+ Each tag is linked to a specific collection, this is important for internal tags
+ so each collection can build its own vocabulary
+ """
+ label = models.CharField(max_length=255, blank=True, null=True)
+ label_slug = models.SlugField(blank=True, null=True)
+ link = models.URLField(unique=True)
+ description = models.CharField(max_length=255, blank=True, null=True)
+ collection = models.ForeignKey('Collection', blank=True, null=True)
+
+ def is_internal(self):
+ return self.link.startswith(settings.INTERNAL_TAGS_URL)
+
+ def __str__(self):
+ return self.label_slug+":"+self.label
+
+
+class TaggingInfo(models.Model):
+ """
+ M2M object for managing tag relation to a revision with its associated relevancy and accuracy
+ """
+ revision = models.ForeignKey('AnnotationRevision', on_delete=models.CASCADE)
+ tag = models.ForeignKey('Tag', on_delete=models.CASCADE)
+ accuracy = models.IntegerField()
+ relevancy = models.IntegerField()
+
+ def __str__(self):
+ return str(str(self.tag.label_slug)+":to:"+str(self.revision.revision_guid))
+
class IconolabComment(XtdComment):
+ """
+ Comment objects that extends XtdComment model, itself extending the django-contrib-comments model.
+
+ Each comment can have 0 or 1 revision, if it is a comment created alongside a revision
+ Each comment can have a set of metacategories
+ """
revision = models.ForeignKey('AnnotationRevision', related_name='creation_comment', null=True, blank=True)
metacategories = models.ManyToManyField('MetaCategory', through='MetaCategoryInfo', through_fields=('comment', 'metacategory'))
@@ -638,18 +750,30 @@
return Annotation.objects.get(pk=self.object_pk)
return None
- # Get page for considered comment, with COMMENTS_PER_PAGE_DEFAULT comments per page
def get_comment_page(self):
+ """
+ Shortcut function to get page for considered comment, with COMMENTS_PER_PAGE_DEFAULT comments per page, used for notifications links generation
+ """
return (IconolabComment.objects.for_app_models("iconolab.annotation").filter(
object_pk=self.object_pk,
).filter(thread_id__gte=self.thread_id).filter(order__lte=self.order).count() +1) // settings.COMMENTS_PER_PAGE_DEFAULT + 1
class MetaCategory(models.Model):
- NONE = 0 # Notifies nobody
- CONTRIBUTORS = 1 # Notifies contributors (revision owners) on target annotation
- COMMENTERS = 2 # Notifies commenters (contributors + comment owners) on target annotation
- COLLECTION_ADMINS = 3 # Notifies collection admins
+ """
+ Metacategories are objects that can be linked to a comment to augment it with meaning (depending on the metacategories defined for a given collection)
+
+ Metacategories can trigger notifications when they are linked to a given coment depending on their trigger_notifications property:
+
+ - NONE : Notifies nobody
+ - CONTRIBUTORS : Notifies contributors (revision owners) on target annotation
+ - COMMENTERS : Notifies commenters (contributors + comment owners) on target annotation
+ - COLLECTION_ADMINS : Notifies collection admins
+ """
+ NONE = 0
+ CONTRIBUTORS = 1
+ COMMENTERS = 2
+ COLLECTION_ADMINS = 3
NOTIFIED_USERS = (
(NONE, 'none'),
@@ -667,14 +791,21 @@
class MetaCategoryInfo(models.Model):
+ """
+ M2M class linking comments and metacategories
+ """
comment = models.ForeignKey('IconolabComment', on_delete=models.CASCADE)
metacategory = models.ForeignKey('MetaCategory', on_delete=models.CASCADE)
def __str__(self):
return "metacategory:"+self.metacategory.label+":on:"+self.comment.id
+
class CommentAttachement(models.Model):
-
+ """
+ This class is supposed to represent added resources to a given comment
+ Not implemented as of v0.0.19
+ """
LINK = 0
IMAGE = 1
PDF = 2
@@ -687,8 +818,14 @@
comment = models.ForeignKey('IconolabComment', related_name='attachments', on_delete=models.CASCADE)
attachment_type = models.IntegerField(choices=COMMENT_CHOICES, default=0)
data = models.TextField(blank=False)
-
+
+
class UserProfile(models.Model):
+ """
+ UserProfile objects are extensions of user model
+
+ As of v0.0.19 they are used to define collection admins. Each user can thus managed 0-N collections.
+ """
user = models.OneToOneField(User, related_name='profile', on_delete=models.CASCADE)
managed_collections = models.ManyToManyField('Collection', related_name='admins', blank=True)
--- a/src/iconolab/signals/handlers.py Mon Dec 12 17:27:33 2016 +0100
+++ b/src/iconolab/signals/handlers.py Thu Dec 15 16:35:01 2016 +0100
@@ -13,6 +13,9 @@
def increment_stats_on_new_revision(sender, instance, **kwargs):
+ """
+ Signal to increment stats on annotation when a revision is created
+ """
from iconolab.models import AnnotationRevision
if sender == AnnotationRevision:
if instance.parent_revision:
@@ -34,6 +37,9 @@
image.stats.save()
def increment_stats_on_new_comment(sender, instance, created, **kwargs):
+ """
+ Signal to increment stats on annotation when a comment is posted
+ """
from iconolab.models import IconolabComment
if created and sender == IconolabComment:
model = apps.get_model(instance.content_type.app_label,instance.content_type.model)
@@ -45,6 +51,9 @@
annotation.image.stats.save()
def increment_stats_on_new_metacategory(sender, instance, created, **kwargs):
+ """
+ Signal to increment stats on annotation when a metacategory is linked to a comment
+ """
from iconolab.models import MetaCategoryInfo, MetaCategoriesCountInfo
if created and sender == MetaCategoryInfo:
metacategory = instance.metacategory
@@ -59,6 +68,9 @@
logger.debug("NEW METACATEGORY %r on comment %r on annotation %r", metacategory, comment, annotation)
def increment_stats_on_accepted_revision(sender, instance, **kwargs):
+ """
+ Signal to increment stats on annotation when a revision is accepted
+ """
from iconolab.models import AnnotationRevision
if sender == AnnotationRevision:
annotation = instance.annotation
@@ -67,6 +79,9 @@
annotation.stats.save()
def increment_stats_on_rejected_revision(sender, instance, **kwargs):
+ """
+ Signal to increment stats on annotation when a comment is rejected
+ """
from iconolab.models import AnnotationRevision
if sender == AnnotationRevision:
annotation = instance.annotation
@@ -74,6 +89,9 @@
annotation.stats.save()
def increment_annotations_count(sender, instance, created, **kwargs):
+ """
+ Signal to increment stats on image when an annotation is created
+ """
from iconolab.models import Annotation
if created and sender == Annotation:
image = instance.image
--- a/src/iconolab/views/comments.py Mon Dec 12 17:27:33 2016 +0100
+++ b/src/iconolab/views/comments.py Thu Dec 15 16:35:01 2016 +0100
@@ -13,9 +13,7 @@
@require_POST
def post_comment_iconolab(request, next=None, using=None):
'''
- Post a comment.
- HTTP POST is required. If ``POST['submit'] == 'preview'`` or if there are
- errors a preview template, ``comments/preview.html``, will be rendered.
+ Rewriting of a django_comments method to link Iconolab metacategories on comment posting
'''
# Fill out some initial data fields from an authenticated user, if present
data = request.POST.copy()
--- a/src/iconolab/views/objects.py Mon Dec 12 17:27:33 2016 +0100
+++ b/src/iconolab/views/objects.py Thu Dec 15 16:35:01 2016 +0100
@@ -19,7 +19,18 @@
logger = logging.getLogger(__name__)
class GlobalHomepageView(View):
+ """
+ View for the opening page of Iconolab.
+ """
def get(self, request, *args, **kwargs):
+ """
+ Template is iconolab/home.html
+
+ Context variables provided to the template are:
+ collections_primary: list of collections to display as big images
+ collections_secondary: list of collections to display as small links at the bottom
+ homepage = True: used to pass checks in the partials/header.html template to adjust the navbar to the homepage
+ """
context = {}
context['collections_primary'] = Collection.objects.filter(show_image_on_home=True).all()
context['collections_secondary'] = Collection.objects.filter(show_image_on_home=False).all()
@@ -34,6 +45,9 @@
# Class with check_kwargs method to fetch objects from database depending on what level in the app we're currently at
class IconolabObjectView(object):
+ """
+ Superclass that defines method used in all object display views.
+ """
def check_kwargs(self, kwargs):
'''
Returns a boolean depending on wether (True) or not (False) the objects were found and a tuple containing the objects, with a select_related/prefetch_related on relevant related objects
@@ -70,7 +84,22 @@
def get_pagination_data(self, list_to_paginate, page, perpage, adjacent_pages_count, perpage_range=[5, 10, 25, 100], trailing_qarg=""):
"""
- Takes a queryset or a list and returns a dict with pagination data
+ Takes a queryset or a list and returns a dict with pagination data for display purposes
+
+ Dict will be of the format:
+ {
+ page: the page to load (integer)
+ perpage_range: a list of the page links to display (list of integers)
+ perpage: the item count per page (integer)
+ perpage_range: a list of the perpage values to display next to the page list (list of integers)
+ trailing_qarg: optional trailing qarg for the paginations links (used in collection home to remember the state of each list between page loads) (string)
+ list: the item list to display (list of objects)
+ show_first: used in template to display links, will be True if 1 is not in page_range
+ show_last: used in template to display links, will be True if page_count is not in page_range
+ ellipsis_first: used in template to display links, will be True if page_range starts at 3 or more
+ ellipsis_last: used in template to display links, will be True if page_range ends at last_page - 2 or less
+
+ }
"""
pagination_data = {}
pagination_data["page"] = page
@@ -96,7 +125,40 @@
return pagination_data
class CollectionHomepageView(View, ContextMixin, IconolabObjectView):
+ """
+ View that displays a collection and four panels to show relevant paginated lists for collection:
+ * item lists
+ * annotations ordered by creation date
+ * annotations ordered by revisions count
+ * annotations where a metacategory that notifies contributors was called
+ """
def get(self, request, *args, **kwargs):
+ """
+ Template is iconolab/collection_home.html
+
+ Url args are:
+ - collection_name: 'name' attribute of the requested collection
+
+ Queryargs understood by the view are:
+ - show : panel that will be shown on page load, one of ['items', 'recent', 'revised', 'contributions'], default to "items"
+ - items_page : item list page to load
+ - items_perpage : item count per page
+ - recent_page : recent annotations list page to load
+ - recent_perpage : recent annotations count per page
+ - revised_page : most revised annotations list page to load
+ - revised_perpage : most revised annotations count per page
+ - contributions_page : annotations with the most contribution calls list page to load
+ - contributions_perpage : annotations with the most contribution calls count per page for item list
+
+ Context variables provided to the template are:
+ - collection: the collection object for the requested collection
+ - collection_name : the collection_name url arg
+ - items_pagination_data: pagination data dict in the format of the IconolabObjectView.get_pagination_data() method for the items list
+ - recent_pagination_data: pagination data dict in the format of the IconolabObjectView.get_pagination_data() method for the recent annotations list
+ - revised_pagination_data: pagination data dict in the format of the IconolabObjectView.get_pagination_data() method for the revised annotations list
+ - contributions_pagination_data: pagination data dict in the format of the IconolabObjectView.get_pagination_data() method for the contribution calls annotations list
+ """
+
success, result = self.check_kwargs(kwargs)
if success:
(collection,) = result
@@ -230,7 +292,35 @@
class ShowItemView(View, ContextMixin, IconolabObjectView):
+ """
+ View that displays informations on an item with associated metadatas and stats. Also displays images and annotation list for each image.
+ """
def get(self, request, *args, **kwargs):
+ """
+ Template is iconolab/item_detail.html
+
+ Url args are:
+ - collection_name : name of the collection
+ - item_guid: 'item_guid' attribute of the requested item
+
+ Queryargs understood by the view are:
+ - show: image_guid for the image to show on load
+ - page: annotation list page on load for displayed image
+ - perpage: annotation count per page on load for displayed image
+
+ Context variables provided to the template are:
+ - collection_name : the collection_name url arg
+ - item_guid: the item_guid url arg
+ - collection: the collection object for the requested collection
+ - item: the item object for the requested item
+ - display_image: the image_guid for the image to display on load
+ - images: a list of dict for the item images data in the format:
+ {
+ 'obj': the image object,
+ 'annotations': the list of annotations on that image
+ }
+ """
+
success, result = self.check_kwargs(kwargs)
if success:
(collection, item) = result
@@ -279,6 +369,9 @@
return render(request, 'iconolab/detail_item.html', context);
class ShowImageView(View, ContextMixin, IconolabObjectView):
+ """
+ View that only displays an image and the associated annotations
+ """
def get(self, request, *args, **kwargs):
success, result = self.check_kwargs(kwargs)
if success:
@@ -293,7 +386,9 @@
return render(request, 'iconolab/detail_image.html', context)
class CreateAnnotationView(View, ContextMixin, IconolabObjectView):
-
+ """
+ View that displays annotation forms and handles annotation creation
+ """
def get_context_data(self, **kwargs):
context = super(CreateAnnotationView, self).get_context_data(**kwargs)
context['collection_name'] = self.kwargs.get('collection_name', '')
@@ -348,7 +443,11 @@
return render(request, 'iconolab/change_annotation.html', context)
class ShowAnnotationView(View, ContextMixin, IconolabObjectView):
-
+ """
+ View that show a given annotation with the corresponding data, links to submit new revisions and the paginated comments thread.
+ """
+
+
def get_context_data(self, **kwargs):
context = super(ShowAnnotationView, self).get_context_data(**kwargs)
context['collection_name'] = self.kwargs.get('collection_name', '')
@@ -357,6 +456,27 @@
return context
def get(self, request, *args, **kwargs):
+ """
+ Template is iconolab/detail_annotations.html
+
+ Url args are:
+ - collection_name: 'name' attribute of the requested collection
+ - item_guid: 'item_guid' attribute of the requested item
+ - annotation_guid: 'annotation_guid' attribute of the requested annotation
+
+ Queryargs understood by the view are:
+ - page: comment thread page on load
+ - perpage: comment count per page on load
+
+ Context variables provided to the template are:
+ - collection: the collection object for the requested collection
+ - image: the image object for the requested image
+ - annotation: the annotation object for the requested annotation
+ - tags_data: a json string describing tags for the annotation current revision
+ - comments: the paginated comments list for the annotation according page and perpage queryargs
+ - notification_comments_ids: the ids of the comments that are referenced by a notification for the authenticated user; This allows
+ us to highlight comments that triggered a notification in the page
+ """
success, result = self.check_kwargs(kwargs)
if success:
(collection, image, annotation,) = result
@@ -403,7 +523,9 @@
class ReadonlyAnnotationView(View, ContextMixin, IconolabObjectView):
-
+ """
+ Same view as ShowAnnotationView but without the comments and links to the forms
+ """
def get_context_data(self, **kwargs):
context = super(ReadonlyAnnotationView, self).get_context_data(**kwargs)
context['collection_name'] = self.kwargs.get('collection_name', '')
@@ -412,6 +534,9 @@
return context
def get(self, request, *args, **kwargs):
+ """
+ Exactly the same as ShowAnnotationView but without all the data around comments
+ """
success, result = self.check_kwargs(kwargs)
if success:
(collection, image, annotation,) = result
@@ -430,7 +555,9 @@
return render(request, 'iconolab/detail_annotation_readonly.html', context)
class EditAnnotationView(View, ContextMixin, IconolabObjectView):
-
+ """
+ View that handles displaying the edition form and editing an annotation
+ """
def get_context_data(self, **kwargs):
context = super(EditAnnotationView, self).get_context_data(**kwargs)
context['collection_name'] = self.kwargs.get('collection_name', '')
@@ -490,7 +617,9 @@
class ShowRevisionView(View, ContextMixin, IconolabObjectView):
-
+ """
+ View that displays a given revision with its associated data and comment
+ """
def get_context_data(self, **kwargs):
context = super(ShowRevisionView, self).get_context_data(**kwargs)
context['collection_name'] = self.kwargs.get('collection_name', '')
@@ -500,6 +629,25 @@
return context
def get(self, request, *args, **kwargs):
+ """
+ Template is iconolab/detail_annotations.html
+
+ Url args are:
+ - collection_name: 'name' attribute of the requested collection
+ - item_guid: 'item_guid' attribute of the requested item
+ - annotation_guid: 'annotation_guid' attribute of the requested annotation
+ - revision_guid: 'revision_guid' attribute of the requested revision
+
+ Context variables provided to the template are:
+ - collection: the collection object for the requested collection
+ - image: the image object for the requested image
+ - annotation: the annotation object for the requested annotation
+ - revision: the revision object for the requested annotation
+ - tags_data: a json string describing tags for the annotation current revision
+ - comment: the comment that was posted alongside the revision
+ - notified_revision: if True, the revision is linked from one or more unread notifications for the
+ current user, allowing us to highlight it in the template.
+ """
success, result = self.check_kwargs(kwargs)
if success:
(collection, image, annotation, revision,) = result
@@ -542,7 +690,10 @@
class MergeProposalView(View, ContextMixin, IconolabObjectView):
-
+ """
+ View that displays the merge form, used when a user wants to "study" a revision because it was submitted from an older revision than the current revision (thus
+ the two revisions don't have the same parents and there is a conflict)
+ """
def get_context_data(self, **kwargs):
context = super(MergeProposalView, self).get_context_data(**kwargs)
context['collection_name'] = self.kwargs.get('collection_name', '')
--- a/src/iconolab/views/userpages.py Mon Dec 12 17:27:33 2016 +0100
+++ b/src/iconolab/views/userpages.py Thu Dec 15 16:35:01 2016 +0100
@@ -16,6 +16,10 @@
logger = logging.getLogger(__name__)
class UserHomeView(DetailView):
+ """
+ Homepage for user account, displays latest unread notifications, latest annotations created recap, latest contributions on annotations recap,
+ latest annotations commented recap, also provides access to admin interface.
+ """
model = User
slug_field = 'id'
@@ -48,7 +52,9 @@
return render(request, 'iconolab/user_home.html', context)
class UserNotificationsView(View):
-
+ """
+ View that displays the notifications the user received
+ """
def get(self, request, *args, **kwargs):
context = {}
notifications = Notification.objects.filter(recipient=request.user)
@@ -65,6 +71,9 @@
return render(request, 'iconolab/user_notifications.html', context)
class UserAnnotationsView(DetailView):
+ """
+ View that displays the full paginated list of annotations created for the considered user
+ """
model = User
slug_field = 'id'
@@ -96,6 +105,9 @@
return render(request, 'iconolab/user_annotations.html', context)
class UserCommentedView(DetailView):
+ """
+ View that displays the full paginated list of annotations on which the considered user has commented
+ """
model = User
slug_field = 'id'
@@ -121,6 +133,9 @@
return render(request, 'iconolab/user_commented.html', context)
class UserContributedView(DetailView):
+ """
+ View that displays the full paginated list of annotations on which the considered user has submitted at least one revision
+ """
model = User
slug_field = 'id'
@@ -146,6 +161,9 @@
return render(request, 'iconolab/user_contributed.html', context)
class UserCollectionAdminView(DetailView):
+ """
+ View that displays the admin panel, allowing collection admin to filter and order annotations on several criterias
+ """
model = User
slug_field = 'id'