added/corrected method strings on models, views and signal handlers
authordurandn
Thu, 15 Dec 2016 16:35:01 +0100
changeset 283 d8fcfac848ed
parent 282 7204cfdde3be
child 284 f52b0f6e2cd9
added/corrected method strings on models, views and signal handlers
src/iconolab/models.py
src/iconolab/signals/handlers.py
src/iconolab/views/comments.py
src/iconolab/views/objects.py
src/iconolab/views/userpages.py
--- 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'