clean README text, remove unnecesary settings, refresh base requirements
authorymh <ymh.work@gmail.com>
Wed, 18 Jan 2017 14:11:30 +0100
changeset 288 9273f1f2c827
parent 287 959cbaad2076
child 289 74040a8ee852
clean README text, remove unnecesary settings, refresh base requirements
.hgignore
src/README.md
src/iconolab/apps.py
src/iconolab/models.py
src/iconolab/settings/dev.py.tmpl
src/iconolab/urls.py
src/iconolab/views/objects.py
src/requirements/base.txt
--- a/.hgignore	Fri Dec 16 13:24:55 2016 +0100
+++ b/.hgignore	Wed Jan 18 14:11:30 2017 +0100
@@ -31,3 +31,5 @@
 ^src/MANIFEST
 ^src/iconolab.egg-info
 ^src/dist/
+^src/.vscode
+^src/requirements/custom.txt$
--- a/src/README.md	Fri Dec 16 13:24:55 2016 +0100
+++ b/src/README.md	Wed Jan 18 14:11:30 2017 +0100
@@ -1,32 +1,3 @@
-# How to start?
-
-1. Make sure PIP is installed then install Django and others dependencies with 
-
-```
-pip install -r requirements.txt
-
-```
-
-2. Move to src/iconolab/static/js/iconolab-bundle to install js dependencies.
-Make sure your have installed nodejs then run the command bellow
-
-```
-npm install
-
-```
-3. To recreate the bundle file that lives in dist/
-
-```
-npm run build
-
-```
-
-4. To add a new js module, you can add it to the js/components folder and then run
-
-```
-npm start
-```
-
 ## ICONOLAB ##
 
 ### 1. Configuration and setup
@@ -37,23 +8,23 @@
 - Create a virtualenv for the project (using virtualenvwrapper is a good idea if possible). Python version is 3.5.1
 - Run
 
-	pip install -r requirements.txt
+    pip install -r requirements.txt
 
 
 #### node.js
 
 - Make sure nodejs is installed
 - cd into iconolab/src/iconolab/static/iconolab/js and run
-	
-	npm install
+
+    npm install
 
 - To recreate the bundle file that lives in dist/
 
-	npm build
+    npm build
 
 - To add a new js module, you can add it to the js/components folder and then run
-	
-	npm start
+    
+    npm start
 
 #### Django project setup
 
@@ -67,14 +38,14 @@
 - Run
 
     python manage.py createsuperuser
-    
+
 to create an admin user
 
 #### Elasticsearch
 
 Some objects in Iconolab are indexed and searched using ElasticSearch. You need to configure Haystack (see dev.py.tmpl, HAYSTACK_CONNECTIONS) and run:
 
-	python manage.py rebuild_index
+    python manage.py rebuild_index
 
 
 ### 2. Development server
@@ -83,17 +54,17 @@
 
 - cd into the iconolab/src folder and run
 
-	python manage.py runserver
-	
+    python manage.py runserver
+
 By default, the app is accessible through http://127.0.0.1:8000/home
 
 #### 2.2 Javascript development
 
 - cd into the iconolab/src_js/iconolab-bundle folder and run
 
-	npm install
-	npm run start
-	
+    npm install
+    npm run start
+
 This will serve the iconolab.js file in the iconolab/src/iconolab/static/js and update it on changes you make in the js code in src_js so you can
 edit the code and debug it live in your browser
 
@@ -109,20 +80,22 @@
 
 The following django manage.py command is used to import collection data and images:
 
-	python manage.py importimages <:export-csv-path> --delimiter <:delimiter> --encoding <:encoding> --collection-json <:collection_fixture_FILENAME> (OR --collection-id <:collection_id> if collection already exists in db) --metacategories-json <:metacategories_json_FILENAME> 
+```
+python manage.py importimages <:export-csv-path> --delimiter <:delimiter> --encoding <:encoding> --collection-json <:collection_fixture_FILENAME> (OR --collection-id <:collection_id> if collection already exists in db) --metacategories-json <:metacategories_json_FILENAME>
+```
 
 Options:
- --delimiter: the delimiter for the csv file. For special ascii characters add a # before the code. Supported special chars are 9 (tab), 29 (Group separator), 30 (Record separator), 31 (Unit separator)
- --encoding: the encoding provided if the csv is not in utf-8. Exemple: 8859 for ISO-8859
- --collection-json: the json file to create the collection from
- --collection-id: the id of the collection to import into, it must already exist
- --metacategories-json: the json file to create metacategories on the collection we're importing into
- --jpeg-quality: the jpeg quality: default to the setting IMG_JPG_DEFAULT_QUALITY
- --no-jpg-conversion: set to True so the command will not convert the images to jpg. Useful for pre-converted jpeg and especially when importing large image banks
- --img-filename-identifier: the column from which the command will try to find images in the folder: use keys from the setting IMPORT_FIELDS_DICT. Default is "INV".
- --filename-regexp-prefix: allows you to customize the way the command try to find images by specifying a regexp pattern to match *before* the identifier provided in img-filename-identifier. Defaults to .*
- --filename-regexp-suffix: allows you to customize the way the command try to find images by specifying a regexp pattern to match *after* the identifier provided in img-filename-identifier. Defaults to [\.\-_].*
-	
+- ```--delimiter```: the delimiter for the csv file. For special ascii characters add a # before the code. Supported special chars are 9 (tab), 29 (Group separator), 30 (Record separator), 31 (Unit separator)
+- ```--encoding```: the encoding provided if the csv is not in utf-8. Exemple: 8859 for ISO-8859
+- ```--collection-json```: the json file to create the collection from
+- ```--collection-id```: the id of the collection to import into, it must already exist
+- ```--metacategories-json```: the json file to create metacategories on the collection we're importing into
+- ```--jpeg-quality```: the jpeg quality: default to the setting IMG_JPG_DEFAULT_QUALITY
+- ```--no-jpg-conversion```: set to True so the command will not convert the images to jpg. Useful for pre-converted jpeg and especially when importing large image banks
+- ```--img-filename-identifier```: the column from which the command will try to find images in the folder: use keys from the setting IMPORT_FIELDS_DICT. Default is "INV".
+- ```--filename-regexp-prefix```: allows you to customize the way the command try to find images by specifying a regexp pattern to match *before* the identifier provided in img-filename-identifier. Defaults to .*
+- ```--filename-regexp-suffix```: allows you to customize the way the command try to find images by specifying a regexp pattern to match *after* the identifier provided in img-filename-identifier. Defaults to [\.\-_].*
+    
 Notes: 
 * The export csv path will be used to find everything else (images and fixtures files). 
 * If the csv file is not encoded in utf-8, you MUST provide --encoding so the csv file can be read
@@ -134,36 +107,38 @@
 
 Another management command allows for editing data using only a .csv file. The command will go through the csv and update the metadatas for every objects it finds in the database with the csv row content.
 
-	python manage.py updatecollection --collection-id=<:id> --delimiter=<:delimiter> --encoding=<:encoding>
-	
+```
+python manage.py updatecollection --collection-id=<:id> --delimiter=<:delimiter> --encoding=<:encoding>
+```
+
 Options:
- --delimiter: the delimiter for the csv file. For special ascii characters add a # before the code. Supported special chars are 9 (tab), 29 (Group separator), 30 (Record separator), 31 (Unit separator)
- --encoding: the encoding provided if the csv is not in utf-8. Exemple: 8859 for ISO-8859
- --collection-id: the id of the collection to import into, it must already exist
+- ```--delimiter```: the delimiter for the csv file. For special ascii characters add a # before the code. Supported special chars are 9 (tab), 29 (Group separator), 30 (Record separator), 31 (Unit separator)
+- ```--encoding```: the encoding provided if the csv is not in utf-8. Exemple: 8859 for ISO-8859
+- ```--collection-id```: the id of the collection to import into, it must already exist
 
- 
+
  ## TO-DOs
  
  * Add a stat object for items with the following stats (at least?)
- 	- contributors_count
- 	- annotations_count
- 	- comments_count
- 	- contribution_calls_count
- 	
+     - contributors_count
+     - annotations_count
+     - comments_count
+     - contribution_calls_count
+
  * Annotation validation: there is an example handler in signals/handler.py for validation an annotation
  
  * Admin interface: add a way to extract data for one or more annotation as .csv
  
  * Django admin:
- 	- Search annotation/item/image by guid
- 	- More complete infos per row for object lists
- 
+     - Search annotation/item/image by guid
+     - More complete infos per row for object lists
+
  * History view: to be able to visualize the history of a given annotation
- 
+
  * Zoomed images:	
- 	- Zoomed images on annotation pages and item list pages (thumbnail sizes must be sorted out to be pre-generated for the list pages)
- 
+     - Zoomed images on annotation pages and item list pages (thumbnail sizes must be sorted out to be pre-generated for the list pages)
+
  * Fragment editor:
- 	- Identify usability issues
- 	- Rectangle selection as default
- 	- Add a way to define two or more shapes for one fragment 
\ No newline at end of file
+     - Identify usability issues
+     - Rectangle selection as default
+     - Add a way to define two or more shapes for one fragment 
\ No newline at end of file
--- a/src/iconolab/apps.py	Fri Dec 16 13:24:55 2016 +0100
+++ b/src/iconolab/apps.py	Wed Jan 18 14:11:30 2017 +0100
@@ -1,9 +1,9 @@
 from django.apps import AppConfig
 
 class IconolabApp(AppConfig):
-	name = 'iconolab'
-	verbose_name = 'Iconolab'
+    name = 'iconolab'
+    verbose_name = 'Iconolab'
 
-	def ready(self):
-		import iconolab.signals.handlers
-		import iconolab.templatetags.iconolab_tags
\ No newline at end of file
+    def ready(self):
+        import iconolab.signals.handlers
+        import iconolab.templatetags.iconolab_tags
--- a/src/iconolab/models.py	Fri Dec 16 13:24:55 2016 +0100
+++ b/src/iconolab/models.py	Wed Jan 18 14:11:30 2017 +0100
@@ -6,7 +6,12 @@
 from django_comments_xtd.models import XtdComment
 from django.utils.text import slugify
 import iconolab.signals.handlers as iconolab_signals
-import uuid, json, re, requests, urllib, logging
+import uuid
+import json
+import re
+import requests
+import urllib
+import logging
 
 logger = logging.getLogger(__name__)
 
@@ -14,19 +19,24 @@
 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
+            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="")
     complete_description = models.TextField(null=True, blank=True, default="")
-    image = models.ImageField(upload_to='uploads/', height_field='height', width_field='width', null=True, blank=True)
+    image = models.ImageField(
+        upload_to='uploads/', height_field='height', width_field='width', null=True, blank=True)
     height = models.IntegerField(null=True, blank=True)
     width = models.IntegerField(null=True, blank=True)
     show_image_on_home = models.BooleanField(default=False)
@@ -37,21 +47,23 @@
 
 class Item(models.Model):
     """
-        Item objects belong to a collection, are linked to a metadata item, and to one or more images
+        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)
-    
+
     def __str__(self):
-        return str(self.item_guid)+":from:"+self.collection.name
-    
+        return str(self.item_guid) + ":from:" + self.collection.name
+
     @property
     def images_sorted_by_name(self):
         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 
+        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="")
@@ -67,35 +79,37 @@
     photo_credits = models.CharField(max_length=255, default="")
     inventory_number = models.CharField(max_length=255, default="")
     joconde_ref = models.CharField(max_length=255, default="")
-    
+
     @property
     def get_joconde_url(self):
-        return settings.JOCONDE_NOTICE_BASE_URL+self.joconde_ref.rjust(11, '0')
-    
+        return settings.JOCONDE_NOTICE_BASE_URL + self.joconde_ref.rjust(11, '0')
+
     def __str__(self):
-        return "metadatas:for:"+str(self.item.item_guid)
+        return "metadatas:for:" + str(self.item.item_guid)
 
 
 class Image(models.Model):
     """
         Each image object is linked to one item, users can create annotations on images
     """
-    
+
     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')
-    item = models.ForeignKey('Item', related_name='images', null=True, blank=True)
+    media = models.ImageField(upload_to='uploads/',
+                              height_field='height', width_field='width')
+    item = models.ForeignKey(
+        'Item', related_name='images', null=True, blank=True)
     height = models.IntegerField(null=False, blank=False)
     width = models.IntegerField(null=False, blank=False)
     created = models.DateTimeField(auto_now_add=True, null=True)
-    
+
     def __str__(self):
-        return str(self.image_guid)+":"+self.name
-    
+        return str(self.image_guid) + ":" + self.name
+
     @property
     def wh_ratio(self):
         return self.width / self.height
-    
+
     @property
     def collection(self):
         return self.item.collection.name
@@ -111,7 +125,7 @@
     @property
     def school(self):
         return self.item.metadatas.school
-        
+
     @property
     def designation(self):
         return self.item.metadatas.designation
@@ -119,7 +133,7 @@
     @property
     def datation(self):
         return self.item.metadatas.datation
-    
+
     @property
     def technics(self):
         return self.item.metadatas.technics
@@ -132,8 +146,10 @@
     def tag_labels(self):
         tag_list = []
         for annotation in self.annotations.all():
-            revision_tags = json.loads(annotation.current_revision.get_tags_json())
-            tag_list += [tag_infos['tag_label'] for tag_infos in revision_tags if tag_infos.get('tag_label') is not None] #deal with
+            revision_tags = json.loads(
+                annotation.current_revision.get_tags_json())
+            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
 
 
@@ -141,20 +157,24 @@
     """
         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)
+    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)
+    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)
+    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)
-    
+        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()
-    
+        self.tag_count = Tag.objects.filter(
+            tagginginfo__revision__annotation__image=self.image).distinct().count()
+
     @transaction.atomic
     def update_stats(self):
         self.annotations_count = 0
@@ -168,18 +188,19 @@
         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,
+                object_pk=annotation.pk,
             ).count()
         # tag_count
-        self.tag_count = Tag.objects.filter(tagginginfo__revision__annotation__image = self.image).distinct().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 
+        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
     """
@@ -190,14 +211,14 @@
         """
         # Create annotation object
         new_annotation = Annotation(
-            image=image, 
+            image=image,
             author=author
         )
         new_annotation.save()
-        
+
         # Create initial revision
         initial_revision = AnnotationRevision(
-            annotation=new_annotation, 
+            annotation=new_annotation,
             author=author,
             title=title,
             description=description,
@@ -206,33 +227,36 @@
         )
         initial_revision.save()
         initial_revision.set_tags(tags_json)
-        
+
         # Create stats object
         new_annotation_stats = AnnotationStats(annotation=new_annotation)
         new_annotation_stats.set_tags_stats()
         new_annotation_stats.save()
-        
+
         # Link everything to parent
         new_annotation.current_revision = initial_revision
         new_annotation.stats = new_annotation_stats
         new_annotation.save()
-        iconolab_signals.revision_created.send(sender=AnnotationRevision, instance=initial_revision)
+        iconolab_signals.revision_created.send(
+            sender=AnnotationRevision, instance=initial_revision)
         return new_annotation
 
     @transaction.atomic
     def get_annotations_contributed_for_user(self, user):
         """
             user is the user whom we want to get the contributed annotations
-            
-            Returns the list of all the annotations on which the user submitted a revision but did not create the annotation
+
+            Returns the list of all the annotations on which the user submitted
+            a revision but did not create the annotation
             List of dict in the format:
-            
+
             {
                 "annotation_obj": annotation object,
                 "revisions_count": revisions count for user
                 "awaiting_count": awaiting revisions for user on this annotation
                 "accepted_count": accepted revisions for user
-                "latest_submitted_revision": date of the latest submitted revision from user on annotation
+                "latest_submitted_revision": date of the latest submitted revision
+                    from user on annotation
             }
         """
         latest_revision_on_annotations = []
@@ -243,13 +267,15 @@
             'image__item',
             'image__item__collection').distinct()
         for annotation in user_contributed_annotations.all():
-            latest_revision_on_annotations.append(annotation.revisions.filter(author=user).latest(field_name="created"))
+            latest_revision_on_annotations.append(
+                annotation.revisions.filter(author=user).latest(field_name="created"))
         contributed_list = []
         if latest_revision_on_annotations:
-            latest_revision_on_annotations.sort(key=lambda item:item.created, reverse=True)
-            contributed_list= [
+            latest_revision_on_annotations.sort(
+                key=lambda item: item.created, reverse=True)
+            contributed_list = [
                 {
-                    "annotation_obj": revision.annotation, 
+                    "annotation_obj": revision.annotation,
                     "revisions_count": revision.annotation.revisions.filter(author=user).count(),
                     "awaiting_count": revision.annotation.revisions.filter(author=user, state=AnnotationRevision.AWAITING).count(),
                     "accepted_count": revision.annotation.revisions.filter(author=user, state=AnnotationRevision.ACCEPTED).count(),
@@ -259,33 +285,36 @@
             ]
         logger.debug(contributed_list)
         return contributed_list
-    
+
     @transaction.atomic
     def get_annotations_commented_for_user(self, user, ignore_revisions_comments=True):
         """
             user is the user for which we want to get the commented annotations
             ignore_revisions_comment allows to filter comments that are associated with a revision
-        
-        
+
+
             Returns a list of all annotations on which a given user commented with user-comments-related data
             List of dict in the format:
-            
+
             {
                 "annotation_obj": annotation object,
                 "comment_count": comment count for user
                 "latest_comment_date": date of the latest comment from user on annotation
             }
         """
-        user_comments = IconolabComment.objects.filter(user=user, content_type__app_label='iconolab', content_type__model='annotation').order_by('-submit_date')
+        user_comments = IconolabComment.objects.filter(
+            user=user, content_type__app_label='iconolab', content_type__model='annotation').order_by('-submit_date')
         if ignore_revisions_comments:
             logger.debug(user_comments.count())
             user_comments = user_comments.filter(revision__isnull=True)
             logger.debug(user_comments.count())
-        all_user_comments_data = [(comment.object_pk, comment.submit_date) for comment in user_comments]
+        all_user_comments_data = [
+            (comment.object_pk, comment.submit_date) for comment in user_comments]
         unique_ordered_comments_data = []
-        for (id, submit_date) in all_user_comments_data: 
+        for (id, submit_date) in all_user_comments_data:
             if id not in [item["annotation_id"] for item in unique_ordered_comments_data]:
-                unique_ordered_comments_data.append({"annotation_id": id, "latest_comment_date": submit_date})
+                unique_ordered_comments_data.append(
+                    {"annotation_id": id, "latest_comment_date": submit_date})
         commented_annotations = Annotation.objects.filter(id__in=[item["annotation_id"] for item in unique_ordered_comments_data]).prefetch_related(
             'current_revision',
             'revisions',
@@ -296,11 +325,12 @@
         sorted_annotations_list = []
         logger.debug(unique_ordered_comments_data)
         for comment_data in unique_ordered_comments_data:
-            annotation_obj = commented_annotations.get(id=comment_data["annotation_id"])
+            annotation_obj = commented_annotations.get(
+                id=comment_data["annotation_id"])
             sorted_annotations_list.append(
                 {
-                    "annotation_obj": annotation_obj, 
-                    "comment_count_for_user": user_comments.filter(object_pk=annotation_obj.id).count(), 
+                    "annotation_obj": annotation_obj,
+                    "comment_count_for_user": user_comments.filter(object_pk=annotation_obj.id).count(),
                     "latest_comment_date": comment_data["latest_comment_date"]
                 }
             )
@@ -309,11 +339,11 @@
 
 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 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.
-        
+
         Annotations can be considered validated or not depending on the metacategories posted in their comments through the attribute validation_state. Their validation state
         can also be overriden and in such case we can use validation_state_overriden attribute to remember it in the model (so for instance if an admin un-validates an annotation
         we could block it from being validated again)
@@ -325,55 +355,61 @@
         (VALIDATED, 'validated'),
     )
     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)
+    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')
-    validation_state = models.IntegerField(choices=VALIDATION_STATES, default=UNVALIDATED)
+    comments = GenericRelation(
+        'IconolabComment', content_type_field='content_type_id', object_id_field='object_pk')
+    validation_state = models.IntegerField(
+        choices=VALIDATION_STATES, default=UNVALIDATED)
     validation_state_overriden = models.BooleanField(default=False)
-    
+
     objects = AnnotationManager()
-    
+
     def __str__(self):
-        return str(self.annotation_guid)+":"+self.current_revision.title
-        
+        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 ]
-    
+        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):
         """
@@ -386,7 +422,7 @@
             # Revision will require validation
             new_revision_state = AnnotationRevision.AWAITING
         new_revision = AnnotationRevision(
-            annotation = self,
+            annotation=self,
             parent_revision=self.current_revision,
             title=title,
             description=description,
@@ -399,9 +435,10 @@
         if new_revision.state == AnnotationRevision.ACCEPTED:
             self.current_revision = new_revision
             self.save()
-        iconolab_signals.revision_created.send(sender=AnnotationRevision, instance=new_revision)
+        iconolab_signals.revision_created.send(
+            sender=AnnotationRevision, instance=new_revision)
         return new_revision
-    
+
     @transaction.atomic
     def validate_existing_revision(self, revision_to_validate):
         """
@@ -412,8 +449,9 @@
             revision_to_validate.state = AnnotationRevision.ACCEPTED
             revision_to_validate.save()
             self.save()
-            iconolab_signals.revision_accepted.send(sender=AnnotationRevision, instance=revision_to_validate)
-    
+            iconolab_signals.revision_accepted.send(
+                sender=AnnotationRevision, instance=revision_to_validate)
+
     @transaction.atomic
     def reject_existing_revision(self, revision_to_reject):
         """
@@ -422,20 +460,23 @@
         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)
-    
+            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 = 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
+        iconolab_signals.revision_accepted.send(
+            sender=AnnotationRevision, instance=revision_to_merge)
+        self.current_revision = merged_revision
         self.save()
         return merged_revision
 
@@ -444,100 +485,114 @@
     """
         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)
-    accepted_revisions_count = models.IntegerField(blank=True, null=True, default=1)
+    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)
+    accepted_revisions_count = models.IntegerField(
+        blank=True, null=True, default=1)
     contributors_count = models.IntegerField(blank=True, null=True, default=1)
     views_count = models.IntegerField(blank=True, null=True, default=0)
     comments_count = models.IntegerField(blank=True, null=True, default=0)
     tag_count = models.IntegerField(blank=True, null=True, default=0)
-    metacategories = models.ManyToManyField('MetaCategory', through='MetaCategoriesCountInfo', through_fields=('annotation_stats_obj', 'metacategory'))
-    
+    metacategories = models.ManyToManyField(
+        'MetaCategory', through='MetaCategoriesCountInfo', through_fields=('annotation_stats_obj', 'metacategory'))
+
     def __str__(self):
-        return "stats:for:"+str(self.annotation.annotation_guid)
-    
+        return "stats:for:" + str(self.annotation.annotation_guid)
+
     @property
     def contributors(self):
-        user_ids_list = self.annotation.revisions.filter(state__in=[AnnotationRevision.ACCEPTED, AnnotationRevision.STUDIED]).values_list("author__id", flat=True)
+        user_ids_list = self.annotation.revisions.filter(state__in=[
+                                                         AnnotationRevision.ACCEPTED, AnnotationRevision.STUDIED]).values_list("author__id", flat=True)
         return User.objects.filter(id__in=user_ids_list).distinct()
-    
+
     @property
     def commenters(self):
-        user_ids_list = IconolabComment.objects.filter(content_type__app_label="iconolab", content_type__model="annotation", object_pk=self.annotation.id).values_list("user__id", flat=True)
+        user_ids_list = IconolabComment.objects.filter(
+            content_type__app_label="iconolab", content_type__model="annotation", object_pk=self.annotation.id).values_list("user__id", flat=True)
         return User.objects.filter(id__in=user_ids_list).distinct()
-    
+
     def set_tags_stats(self):
-        self.tag_count = Tag.objects.filter(tagginginfo__revision = self.annotation.current_revision).distinct().count()
-    
+        self.tag_count = Tag.objects.filter(
+            tagginginfo__revision=self.annotation.current_revision).distinct().count()
+
     @property
     def relevant_tags_count(self, score=settings.RELEVANT_TAGS_MIN_SCORE):
         return TaggingInfo.objects.filter(revision=self.annotation.current_revision, relevancy__gte=score).distinct().count()
-    
+
     @property
     def accurate_tags_count(self, score=settings.ACCURATE_TAGS_MIN_SCORE):
         return TaggingInfo.objects.filter(revision=self.annotation.current_revision, accuracy__gte=score).distinct().count()
-    
+
     @transaction.atomic
     def update_stats(self):
         # views_count - Can't do much about views count
-        # submitted_revisions_count 
+        # submitted_revisions_count
         annotation_revisions = self.annotation.revisions
         self.submitted_revisions_count = annotation_revisions.count()
         # aawaiting_revisions_count
-        self.awaiting_revisions_count = annotation_revisions.filter(state=AnnotationRevision.AWAITING).count()
+        self.awaiting_revisions_count = annotation_revisions.filter(
+            state=AnnotationRevision.AWAITING).count()
         # accepted_revisions_count
-        self.accepted_revisions_count = annotation_revisions.filter(state=AnnotationRevision.ACCEPTED).count() + annotation_revisions.filter(state=AnnotationRevision.STUDIED).count()
+        self.accepted_revisions_count = annotation_revisions.filter(state=AnnotationRevision.ACCEPTED).count(
+        ) + annotation_revisions.filter(state=AnnotationRevision.STUDIED).count()
         # comment_count
         self.comments_count = XtdComment.objects.for_app_models("iconolab.annotation").filter(
-            object_pk = self.annotation.pk,
+            object_pk=self.annotation.pk,
         ).count()
         # contributors_count
         self.contributors_count = len(self.contributors)
         # tag_count
-        
+
         annotation_comments_with_metacategories = IconolabComment.objects.filter(
-            content_type__app_label="iconolab", 
-            content_type__model="annotation", 
-            object_pk=self.annotation.id, 
+            content_type__app_label="iconolab",
+            content_type__model="annotation",
+            object_pk=self.annotation.id,
             metacategories__collection=self.annotation.image.item.collection
         )
-        m2m_objects = MetaCategoriesCountInfo.objects.filter(annotation_stats_obj=self)
+        m2m_objects = MetaCategoriesCountInfo.objects.filter(
+            annotation_stats_obj=self)
         for obj in m2m_objects.all():
             obj.count = 0
             obj.save()
         for comment in annotation_comments_with_metacategories.all():
             for metacategory in comment.metacategories.all():
                 if metacategory not in self.metacategories.all():
-                    MetaCategoriesCountInfo.objects.create(annotation_stats_obj=self, metacategory=metacategory, count=1)
+                    MetaCategoriesCountInfo.objects.create(
+                        annotation_stats_obj=self, metacategory=metacategory, count=1)
                 else:
-                    m2m_object = MetaCategoriesCountInfo.objects.filter(annotation_stats_obj=self, metacategory=metacategory).first()
+                    m2m_object = MetaCategoriesCountInfo.objects.filter(
+                        annotation_stats_obj=self, metacategory=metacategory).first()
                     m2m_object.count += 1
                     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)
+    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)
-    
+
     def __str__(self):
-        return "metacategory_count_for:"+self.metacategory.label+":on:"+str(self.annotation_stats_obj.annotation.annotation_guid)
+        return "metacategory_count_for:" + self.metacategory.label + ":on:" + str(self.annotation_stats_obj.annotation.annotation_guid)
 
 
 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:
-        
+
         - 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 
+        - 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
@@ -548,33 +603,37 @@
     ACCEPTED = 1
     REJECTED = 2
     STUDIED = 3
-    
+
     REVISION_STATES = (
         (AWAITING, 'awaiting'),
         (ACCEPTED, 'accepted'),
         (REJECTED, 'rejected'),
         (STUDIED, 'studied'),
     )
-    
+
     revision_guid = models.UUIDField(default=uuid.uuid4)
-    annotation = models.ForeignKey('Annotation', related_name='revisions', null=False, blank=False)
-    parent_revision = models.ForeignKey('AnnotationRevision', related_name='child_revisions', blank=True, null=True)
-    merge_parent_revision = models.ForeignKey('AnnotationRevision', related_name='child_revisions_merge', blank=True, null=True)
+    annotation = models.ForeignKey(
+        'Annotation', related_name='revisions', null=False, blank=False)
+    parent_revision = models.ForeignKey(
+        'AnnotationRevision', related_name='child_revisions', blank=True, null=True)
+    merge_parent_revision = models.ForeignKey(
+        'AnnotationRevision', related_name='child_revisions_merge', blank=True, null=True)
     author = models.ForeignKey(User, null=True)
     title = models.CharField(max_length=255)
     description = models.TextField(null=True)
     fragment = models.TextField()
-    tags = models.ManyToManyField('Tag', through='TaggingInfo', through_fields=('revision', 'tag'))
+    tags = models.ManyToManyField(
+        'Tag', through='TaggingInfo', through_fields=('revision', 'tag'))
     state = models.IntegerField(choices=REVISION_STATES, default=AWAITING)
     created = models.DateTimeField(auto_now_add=True, null=True)
 
     def __str__(self):
-        return str(self.revision_guid)+":"+self.title
+        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
@@ -594,47 +653,50 @@
             tag_string = tag_data.get("tag_input")
             tag_accuracy = tag_data.get("accuracy", 0)
             tag_relevancy = tag_data.get("relevancy", 0)
-            
-            if tag_string.startswith("http://") or tag_string.startswith("https://"): #check if url
-                if Tag.objects.filter(link=tag_string).exists(): #check if tag already exists
+
+            # check if url
+            if tag_string.startswith("http://") or tag_string.startswith("https://"):
+                # check if tag already exists
+                if Tag.objects.filter(link=tag_string).exists():
                     tag_obj = Tag.objects.get(link=tag_string)
                 else:
                     tag_obj = Tag.objects.create(
-                        link = tag_string,
+                        link=tag_string,
                     )
             else:
-                new_tag_link = settings.BASE_URL+'/'+slugify(tag_string)
+                new_tag_link = settings.BASE_URL + '/' + slugify(tag_string)
                 if Tag.objects.filter(link=new_tag_link).exists():
                     # Somehow we received a label for an existing tag
                     tag_obj = Tag.objects.get(link=new_tag_link)
                 else:
                     tag_obj = Tag.objects.create(
-                        label = tag_string,
-                        label_slug = slugify(tag_string),
-                        description = "",
-                        link = settings.INTERNAL_TAGS_URL+'/'+slugify(tag_string),
-                        collection = self.annotation.image.item.collection
+                        label=tag_string,
+                        label_slug=slugify(tag_string),
+                        description="",
+                        link=settings.INTERNAL_TAGS_URL +
+                        '/' + slugify(tag_string),
+                        collection=self.annotation.image.item.collection
                     )
             tag_info = TaggingInfo.objects.create(
-                tag=tag_obj, 
+                tag=tag_obj,
                 revision=self,
-                accuracy = tag_accuracy,
-                relevancy = tag_relevancy
+                accuracy=tag_accuracy,
+                relevancy=tag_relevancy
             )
-        
+
     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 
+                        "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
                 },
                 {
@@ -643,15 +705,16 @@
             ]
         """
         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))
-            sparql_query_url = source+'sparql'
+            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))
+            sparql_query_url = source + 'sparql'
             try:
                 dbpedia_resp = requests.get(
-                    sparql_query_url, 
+                    sparql_query_url,
                     params={
-                            "query": sparql_query,
-                            "format": "json"
+                        "query": sparql_query,
+                        "format": "json"
                     }
                 )
             except:
@@ -667,7 +730,7 @@
             if variable_bindings:
                 label_data = variable_bindings.pop()
             return label_data.get("l", {"value": False}).get("value")
-        
+
         final_list = []
         for tagging_info in self.tagginginfo_set.select_related("tag").all():
             if tagging_info.tag.is_internal():
@@ -680,16 +743,17 @@
                 })
             else:
                 tag_link = tagging_info.tag.link
-                #import label from external
+                # import label from external
                 externaL_repos_fetch_dict = {
                     "http://dbpedia.org/": fetch_from_dbpedia,
                     "http://fr.dbpedia.org/": fetch_from_dbpedia
                 }
                 try:
-                    (source, fetch_label) = next(item for item in externaL_repos_fetch_dict.items() if tag_link.startswith(item[0]))
+                    (source, fetch_label) = next(
+                        item for item in externaL_repos_fetch_dict.items() if tag_link.startswith(item[0]))
                     tag_label = fetch_label(tag_link, "fr", source)
-                    if not tag_label: # Error happened and we got False as a fetch return
-                        tag_label = tagging_info.tag.label 
+                    if not tag_label:  # Error happened and we got False as a fetch return
+                        tag_label = tagging_info.tag.label
                     else:
                         tagging_info.tag.label = tag_label
                         tagging_info.tag.save()
@@ -702,13 +766,13 @@
                     })
                 except StopIteration:
                     pass
-        return json.dumps(final_list) 
+        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
     """
@@ -717,77 +781,80 @@
     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
+        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)
+    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))
+        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'))
-    
+    revision = models.ForeignKey(
+        'AnnotationRevision', related_name='creation_comment', null=True, blank=True)
+    metacategories = models.ManyToManyField(
+        'MetaCategory', through='MetaCategoryInfo', through_fields=('comment', 'metacategory'))
+
     objects = XtdComment.objects
-    
+
     def __str__(self):
         return str(self.id)
-    
+
     class Meta:
         ordering = ["thread_id", "id"]
-    
+
     @property
     def annotation(self):
         if self.content_type.app_label == "iconolab" and self.content_type.model == "annotation":
             return Annotation.objects.get(pk=self.object_pk)
         return None
-            
+
     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
+        ).filter(thread_id__gte=self.thread_id).filter(order__lte=self.order).count() + 1) // settings.COMMENTS_PER_PAGE_DEFAULT + 1
 
-    
+
 class MetaCategory(models.Model):
     """
         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
-            
+
         Metacategories can be used to consider an annotation as "validated" if a certain agreement threshold is reached using their validation_value property
-        
+
             - NEUTRAL : The metacategory doesn't affect the validation state
             - AGREEMENT : The metacategory can be used to validate the annotation when linked to a comment on said annotation
             - DISAGREEMENT : The metacategory can be used to unvalidate the annotation when linked to a comment on said annotation
-   
+
     """
     NONE = 0
     CONTRIBUTORS = 1
@@ -799,7 +866,7 @@
         (COMMENTERS, 'commenters'),
         (COLLECTION_ADMINS, 'collection admins'),
     )
-    
+
     NEUTRAL = 0
     AGREEMENT = 1
     DISAGREEMENT = 2
@@ -808,15 +875,17 @@
         (AGREEMENT, 'agreement'),
         (DISAGREEMENT, 'disagreement'),
     )
-    
+
     collection = models.ForeignKey(Collection, related_name="metacategories")
     label = models.CharField(max_length=255)
-    triggers_notifications = models.IntegerField(choices=NOTIFIED_USERS, default=NONE)
-    validation_value = models.IntegerField(choices=VALIDATION_VALUES, default=NEUTRAL)
-    
+    triggers_notifications = models.IntegerField(
+        choices=NOTIFIED_USERS, default=NONE)
+    validation_value = models.IntegerField(
+        choices=VALIDATION_VALUES, default=NEUTRAL)
+
     def __str__(self):
-        return self.label+":"+self.collection.name
-    
+        return self.label + ":" + self.collection.name
+
 
 class MetaCategoryInfo(models.Model):
     """
@@ -824,9 +893,9 @@
     """
     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
+        return "metacategory:" + self.metacategory.label + ":on:" + self.comment.id
 
 
 class CommentAttachement(models.Model):
@@ -842,20 +911,23 @@
         (IMAGE, 'image'),
         (PDF, 'pdf')
     )
-    
-    comment = models.ForeignKey('IconolabComment', related_name='attachments', on_delete=models.CASCADE)
+
+    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)
-    
+    user = models.OneToOneField(
+        User, related_name='profile', on_delete=models.CASCADE)
+    managed_collections = models.ManyToManyField(
+        'Collection', related_name='admins', blank=True)
+
     def __str__(self):
-        return "profile:"+self.user.username
\ No newline at end of file
+        return "profile:" + self.user.username
--- a/src/iconolab/settings/dev.py.tmpl	Fri Dec 16 13:24:55 2016 +0100
+++ b/src/iconolab/settings/dev.py.tmpl	Wed Jan 18 14:11:30 2017 +0100
@@ -9,9 +9,10 @@
 For the full list of settings and their values, see
 https://docs.djangoproject.com/en/1.9/ref/settings/
 """
-from iconolab.settings import *
+import logging
+import os
 
-import os, logging
+from iconolab.settings import *
 
 # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
--- a/src/iconolab/urls.py	Fri Dec 16 13:24:55 2016 +0100
+++ b/src/iconolab/urls.py	Wed Jan 18 14:11:30 2017 +0100
@@ -43,7 +43,7 @@
     url(r'^collections/(?P<collection_name>[a-z0-9\-]+)/images/(?P<image_guid>[^/]+)/annotations/(?P<annotation_guid>[^/]+)/revisions/?$', django_views.generic.RedirectView.as_view(pattern_name="annotation_detail")),
     url(r'^collections/(?P<collection_name>[a-z0-9\-]+)/images/(?P<image_guid>[^/]+)/annotations/(?P<annotation_guid>[^/]+)/revisions/(?P<revision_guid>[^/]+)/detail', views.objects.ShowRevisionView.as_view(), name='revision_detail'),
     url(r'^collections/(?P<collection_name>[a-z0-9\-]+)/images/(?P<image_guid>[^/]+)/annotations/(?P<annotation_guid>[^/]+)/revisions/(?P<revision_guid>[^/]+)/merge$', login_required(views.objects.MergeProposalView.as_view()), name='annotation_merge'),
-    
+
     url(r'^user/(?P<slug>[a-z0-9\-]+)/home/?$', views.userpages.UserHomeView.as_view(), name="user_home"),
     url(r'^user/(?P<slug>[a-z0-9\-]+)/commented/?$', views.userpages.UserCommentedView.as_view(), name="user_commented"),
     url(r'^user/(?P<slug>[a-z0-9\-]+)/contributed/?$', views.userpages.UserContributedView.as_view(), name="user_contributed"),
@@ -51,20 +51,20 @@
     url(r'^user/(?P<slug>[a-z0-9\-]+)/adminpanel/(?P<collection_name>[a-z0-9\-]+)/$', views.userpages.UserCollectionAdminView.as_view(), name="user_admin_panel"),
     url(r'^user/notifications/all/?$', login_required(views.userpages.UserNotificationsView.as_view()), name="user_notifications"),
     url(r'^user/notifications/', include(notifications.urls, namespace='notifications')),
-    
+
     url(r'^errors/404', views.misc.NotFoundErrorView.as_view(), name="404error"),
-    
+
     url(r'^help/', views.misc.HelpView.as_view(), name="iconolab_help"),
     url(r'^glossary/', views.misc.GlossaryView.as_view(), name="iconolab_glossary"),
     url(r'^credits/', views.misc.CreditsView.as_view(), name="iconolab_credits"),
     url(r'^contributioncharter/', views.misc.ContributionCharterView.as_view(), name="iconolab_charter"),
     url(r'^legalmentions/', views.misc.LegalMentionsView.as_view(), name="iconolab_legals"),
-    
+
     url(r'^account/', include('iconolab.auth.urls', namespace='account')),
     url(r'^search/', include('iconolab.search_indexes.urls', namespace='search_indexes')),
     url(r'^comments/', include('django_comments_xtd.urls')),
     url(r'^comments/annotation/post', views.comments.post_comment_iconolab, name="post_comment"),
-    
+
     url(r'^compare/$', views.objects.TestView.as_view(), name="compare_view")
     #url(r'^search/', include('haystack.urls'), name="search_iconolab"),
 ]
--- a/src/iconolab/views/objects.py	Fri Dec 16 13:24:55 2016 +0100
+++ b/src/iconolab/views/objects.py	Wed Jan 18 14:11:30 2017 +0100
@@ -25,11 +25,12 @@
     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
+                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()
@@ -50,8 +51,10 @@
     """
     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
-            following this ordering: (collection, item, image, annotation, revision)
+            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 following this ordering:
+            (collection, item, image, annotation, revision)
         '''
 
         objects_tuple = ()
@@ -81,11 +84,11 @@
             except (ValueError, AnnotationRevision.DoesNotExist):
                 return False, RedirectView.as_view(url=reverse('404error'))
         return True, objects_tuple
-        
+
     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 for display purposes
-            
+
             Dict will be of the format:
             {
                 page: the page to load (integer)
@@ -98,7 +101,7 @@
                 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 = {}
@@ -122,11 +125,11 @@
         pagination_data["ellipsis_first"] = pagination_data["show_first"] and (page - adjacent_pages_count != 2)
         pagination_data["show_last"] = page + adjacent_pages_count < paginator.num_pages
         pagination_data["ellipsis_last"] = pagination_data["show_last"] and (page + adjacent_pages_count != paginator.num_pages - 1)
-        return pagination_data        
-            
+        return pagination_data
+
 class CollectionHomepageView(View, ContextMixin, IconolabObjectView):
     """
-        View that displays a collection and four panels to show relevant paginated lists for collection: 
+        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
@@ -135,10 +138,10 @@
     def get(self, request, *args, **kwargs):
         """
             Template is iconolab/collection_home.html
-            
-            Url args are: 
-                - collection_name: 'name' attribute of the requested collection 
-            
+
+            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
@@ -149,7 +152,7 @@
                 - 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
@@ -158,7 +161,7 @@
                 - 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
@@ -167,7 +170,7 @@
         context = super(CollectionHomepageView, self).get_context_data(**kwargs)
         context['collection_name'] = self.kwargs.get('collection_name', '')
         context['collection'] = collection
-        
+
         # get Pagination and navigation query args
         try:
             items_page = int(request.GET.get('items_page', '1'))
@@ -177,7 +180,7 @@
             items_per_page = int(request.GET.get('items_perpage', '12'))
         except ValueError:
             items_per_page = 12
-        
+
         try:
             recent_page = int(request.GET.get('recent_page', '1'))
         except ValueError:
@@ -186,7 +189,7 @@
             recent_per_page = int(request.GET.get('recent_perpage', '10'))
         except ValueError:
             recent_per_page = 10
-        
+
         try:
             revised_page = int(request.GET.get('revised_page', '1'))
         except ValueError:
@@ -195,7 +198,7 @@
             revised_per_page = int(request.GET.get('revised_perpage', '10'))
         except ValueError:
             revised_per_page = 10
-        
+
         try:
             contributions_page = int(request.GET.get('contributions_page', '1'))
         except ValueError:
@@ -204,22 +207,22 @@
             contributions_per_page = int(request.GET.get('contributions_perpage', '10'))
         except ValueError:
             contributions_per_page = 10
-        
+
         active_list = request.GET.get('show', 'items')
         if active_list not in ['items', 'recent', 'revised', 'contributions']:
             active_list = 'items'
         context["active_list"] = active_list
-        
-        
+
+
         # Pagination values
         adjacent_pages_count = 2
-        
+
         # Paginated objects list
         items_list = collection.items.order_by("metadatas__inventory_number").all()
         context["items_pagination_data"] = self.get_pagination_data(
-            items_list, 
-            items_page, 
-            items_per_page, 
+            items_list,
+            items_page,
+            items_per_page,
             adjacent_pages_count,
             perpage_range=[6, 12, 48, 192],
             trailing_qarg="&recent_page="+str(recent_page)
@@ -229,16 +232,16 @@
             +"&contributions_page="+str(contributions_page)
             +"&contributions_perpage="+str(contributions_per_page)
         )
-        
+
         # Paginated recent annotations list
         recent_annotations = Annotation.objects.filter(image__item__collection__name=collection.name).prefetch_related(
             'current_revision',
             'stats'
         ).order_by('-current_revision__created')
         context["recent_pagination_data"] = self.get_pagination_data(
-            recent_annotations, 
-            recent_page, 
-            recent_per_page, 
+            recent_annotations,
+            recent_page,
+            recent_per_page,
             adjacent_pages_count,
             trailing_qarg="&items_page="+str(items_page)
             +"&items_perpage="+str(items_per_page)
@@ -247,16 +250,16 @@
             +"&contributions_page="+str(contributions_page)
             +"&contributions_perpage="+str(contributions_per_page)
         )
-        
+
         # Paginated revised annotations list
         revised_annotations = Annotation.objects.filter(image__item__collection__name=collection.name).prefetch_related(
             'current_revision',
             'stats'
         ).annotate(revision_count=Count('revisions')).order_by('-revision_count')
         context["revised_pagination_data"] = self.get_pagination_data(
-            revised_annotations, 
-            revised_page, 
-            revised_per_page, 
+            revised_annotations,
+            revised_page,
+            revised_per_page,
             adjacent_pages_count,
             trailing_qarg="&items_page="+str(items_page)
             +"&items_perpage="+str(items_per_page)
@@ -265,7 +268,7 @@
             +"&contributions_page="+str(contributions_page)
             +"&contributions_perpage="+str(contributions_per_page)
         )
-        
+
         # Paginated contribution calls annotation list
         contrib_calls_annotations_ids = list(set(MetaCategoryInfo.objects.filter(
             metacategory__collection__name=collection.name,
@@ -275,9 +278,9 @@
         collection_ann_dict = dict([(str(annotation.id), annotation) for annotation in collection_annotations])
         contributions_annotations = [collection_ann_dict[id] for id in contrib_calls_annotations_ids]
         context["contributions_pagination_data"] = self.get_pagination_data(
-            contributions_annotations, 
-            contributions_page, 
-            contributions_per_page, 
+            contributions_annotations,
+            contributions_page,
+            contributions_per_page,
             adjacent_pages_count,
             trailing_qarg="&items_page="+str(items_page)
             +"&items_perpage="+str(items_per_page)
@@ -286,7 +289,7 @@
             +"&revised_page="+str(revised_page)
             +"&revised_perpage="+str(revised_per_page)
         )
-            
+
         return render(request, 'iconolab/collection_home.html', context)
 
 
@@ -298,16 +301,16 @@
     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 
-                
+                - 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
@@ -320,13 +323,13 @@
                         'annotations': the list of annotations on that image
                     }
         """
-        
+
         success, result = self.check_kwargs(kwargs)
         if success:
             (collection, item) = result
         else:
             return result(request)
-        
+
         context = super(ShowItemView, self).get_context_data(**kwargs)
         image_guid_to_display = request.GET.get("show", str(item.images.first().image_guid))
         if image_guid_to_display not in [str(guid) for guid in item.images.all().values_list("image_guid", flat=True)]:
@@ -340,7 +343,7 @@
             displayed_annotations_per_page = int(request.GET.get('perpage', '10'))
         except ValueError:
             displayed_annotations_per_page = 10
-        
+
         context['collection_name'] = self.kwargs.get('collection_name', '')
         context['item_guid'] = self.kwargs.get('image_guid', '')
         context['collection'] = collection
@@ -359,7 +362,7 @@
             except PageNotAnInteger:
                 annotations = annotations_paginator.page(1)
             except EmptyPage:
-                annotations = annotations_paginator.page(recent_paginator.num_pages) 
+                annotations = annotations_paginator.page(recent_paginator.num_pages)
             context['images'].append({
                 'obj' : image,
                 'annotations': annotations
@@ -444,10 +447,11 @@
 
 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.
+        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', '')
@@ -458,23 +462,23 @@
     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 
-            
+
+            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 
+                - 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)
@@ -535,7 +539,7 @@
 
     def get(self, request, *args, **kwargs):
         """
-            Exactly the same as ShowAnnotationView but without all the data around comments 
+            Exactly the same as ShowAnnotationView but without all the data around comments
         """
         success, result = self.check_kwargs(kwargs)
         if success:
@@ -556,7 +560,7 @@
 
 class EditAnnotationView(View, ContextMixin, IconolabObjectView):
     """
-        View that handles displaying the edition form and editing an annotation 
+        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)
@@ -631,13 +635,13 @@
     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 
+
+            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
--- a/src/requirements/base.txt	Fri Dec 16 13:24:55 2016 +0100
+++ b/src/requirements/base.txt	Wed Jan 18 14:11:30 2017 +0100
@@ -1,15 +1,17 @@
-Django==1.10
-django-comments-xtd==1.6.0
-django-contrib-comments==1.7.2
-django-haystack==2.5.0
-django-model-utils==2.5.2
+Django==1.10.5
+django-comments-xtd==1.6.3
+django-contrib-comments==1.7.3
+django-haystack==2.6.0
+django-model-utils==2.6.1
 django-notifications-hq==1.2
-elasticsearch==2.4.0
+elasticsearch==5.1.0
+iconolab==0.0.19
 jsonfield==1.0.3
-Pillow==3.3.1
+olefile==0.44
+Pillow==4.0.0
 psycopg2==2.6.2
-pytz==2016.6.1
-requests==2.11.1
+pytz==2016.10
+requests==2.12.4
 six==1.10.0
 sorl-thumbnail==12.4a1
-urllib3==1.16
+urllib3==1.19.1