force unix line ending
authorymh <ymh.work@gmail.com>
Wed, 18 Jan 2017 16:53:46 +0100
changeset 298 97b805fc88f0
parent 297 ae36a174a3e5
child 299 fb07469bfb55
force unix line ending
.hgignore
src/iconolab/apps.py
src/iconolab/auth/urls.py
src/iconolab/auth/views.py
src/iconolab/models.py
src/iconolab/settings/dev.py.tmpl
src/iconolab/static/iconolab/css/iconolab.css
src/iconolab/templates/iconolab/change_annotation.html
src/iconolab/templates/iconolab/detail_annotation.html
src/iconolab/templates/iconolab_base.html
src/iconolab/templates/partials/header.html
src/iconolab/templates/registration/login.html
src/iconolab/templatetags/iconolab_tags.py
src/iconolab/utils/context_processors.py
src/iconolab/utils/utils.py
src/iconolab/views/objects.py
--- a/.hgignore	Wed Jan 18 16:50:59 2017 +0100
+++ b/.hgignore	Wed Jan 18 16:53:46 2017 +0100
@@ -1,38 +1,38 @@
-syntax: regexp
-
-^virtualenv/
-
-^__pychache__/
-^src/iconolab/settings/dev\.py$
-
-^src/iconolab/static/iconolab/js/node_modules/
-^src/iconolab/static/iconolab/js/iconolab-bundle/node_modules/
-^src/iconolab/static/iconolab/js/iconolab-bundle/dist/
-\.orig$
-
-^src_js/iconolab-bundle/node_modules/
-^src_js/iconolab-bundle/dist/
-
-\.log$
-^web/*
-^\.pydevproject$
-^\.project$
-^\.settings/org\.eclipse\.core\.resources\.prefs$
-^\.settings/org\.eclipse\.core\.runtime\.prefs$
-\.pyc$
-\.DS_Store$
-^src/iconolab/media/uploads/
-^src/iconolab/media/cache/
-^src/db.sqlite3$
-^src/log\.txt$
-^run/log/
-^log$
-^sbin/sync/config\.py$
-^src/MANIFEST
-^src/iconolab.egg-info
-^src/dist/
-^src/.vscode
-^src/requirements/custom.txt$
-^src/iconolab/db.sqlite3
-
-^data/
+syntax: regexp
+
+^virtualenv/
+
+^__pychache__/
+^src/iconolab/settings/dev\.py$
+
+^src/iconolab/static/iconolab/js/node_modules/
+^src/iconolab/static/iconolab/js/iconolab-bundle/node_modules/
+^src/iconolab/static/iconolab/js/iconolab-bundle/dist/
+\.orig$
+
+^src_js/iconolab-bundle/node_modules/
+^src_js/iconolab-bundle/dist/
+
+\.log$
+^web/*
+^\.pydevproject$
+^\.project$
+^\.settings/org\.eclipse\.core\.resources\.prefs$
+^\.settings/org\.eclipse\.core\.runtime\.prefs$
+\.pyc$
+\.DS_Store$
+^src/iconolab/media/uploads/
+^src/iconolab/media/cache/
+^src/db.sqlite3$
+^src/log\.txt$
+^run/log/
+^log$
+^sbin/sync/config\.py$
+^src/MANIFEST
+^src/iconolab.egg-info
+^src/dist/
+^src/.vscode
+^src/requirements/custom.txt$
+^src/iconolab/db.sqlite3
+
+^data/
--- a/src/iconolab/apps.py	Wed Jan 18 16:50:59 2017 +0100
+++ b/src/iconolab/apps.py	Wed Jan 18 16:53:46 2017 +0100
@@ -1,9 +1,9 @@
-from django.apps import AppConfig
-
-class IconolabApp(AppConfig):
-    name = 'iconolab'
-    verbose_name = 'Iconolab'
-
-    def ready(self):
-        import iconolab.signals.handlers
-        import iconolab.templatetags.iconolab_tags
+from django.apps import AppConfig
+
+class IconolabApp(AppConfig):
+    name = 'iconolab'
+    verbose_name = 'Iconolab'
+
+    def ready(self):
+        import iconolab.signals.handlers
+        import iconolab.templatetags.iconolab_tags
--- a/src/iconolab/auth/urls.py	Wed Jan 18 16:50:59 2017 +0100
+++ b/src/iconolab/auth/urls.py	Wed Jan 18 16:53:46 2017 +0100
@@ -1,14 +1,14 @@
-# -*- coding: utf-8 -*-
-
-from django.conf.urls import url
-from django.contrib.auth.views import login, logout, password_change
-from . import views
-
-urlpatterns = [
-    url(r'^register/$', views.RegisterView.as_view(), name='register'),
-    url(r'^register/created/$', views.UserCreatedView.as_view(), name='user_created'),
-    url(r'^login/$', views.LoginView.as_view(), name='login'),
-    url(r'^password/reset$', password_change, name='password_reset'),
- 	url(r'^logout/', views.LogoutView.as_view(), name='logout'),
- 	#url(r'^password/reset', view)
-]
+# -*- coding: utf-8 -*-
+
+from django.conf.urls import url
+from django.contrib.auth.views import login, logout, password_change
+from . import views
+
+urlpatterns = [
+    url(r'^register/$', views.RegisterView.as_view(), name='register'),
+    url(r'^register/created/$', views.UserCreatedView.as_view(), name='user_created'),
+    url(r'^login/$', views.LoginView.as_view(), name='login'),
+    url(r'^password/reset$', password_change, name='password_reset'),
+ 	url(r'^logout/', views.LogoutView.as_view(), name='logout'),
+ 	#url(r'^password/reset', view)
+]
--- a/src/iconolab/auth/views.py	Wed Jan 18 16:50:59 2017 +0100
+++ b/src/iconolab/auth/views.py	Wed Jan 18 16:53:46 2017 +0100
@@ -1,72 +1,72 @@
-from django.utils.http import is_safe_url
-from django.contrib.auth import authenticate, login, logout, get_user_model
-from django.contrib.auth.forms import UserCreationForm
-from django.shortcuts import redirect, render, HttpResponseRedirect
-from django.core.urlresolvers import reverse_lazy
-from django.contrib.auth.forms import AuthenticationForm
-from django.views.generic import FormView
-from django.views.generic.base import RedirectView, TemplateView
-from django.views.generic.edit import CreateView
-
-User = get_user_model()
-
-def login_form(request):
-	pass
-
-def login_view(request):
-	return HttpResponse('show show a login form')
-	
-def logout_view(request):
-	logout(request)
-
-
-class LoginView(FormView):
-	template_name = "registration/login.html"
-	form_class = AuthenticationForm
-	success_url = reverse_lazy('home')
-	redirect_field_name = "next"
-	
-	def get_context_data(self, **kwargs):
-		context = super(LoginView, self).get_context_data(**kwargs)
-		context["next"] = self.get_success_url()
-		return context
-	
-	def get(self, request, *args, **kwargs):
-		if request.user.is_authenticated():
-			return HttpResponseRedirect(self.get_success_url())
-		return super(LoginView, self).get(request, *args, **kwargs)
-	
-	def form_valid(self, form):
-		login(self.request, form.get_user())
-		return HttpResponseRedirect(self.get_success_url())
-	
-	def get_success_url(self):
-		redirect_to = self.request.POST.get(self.redirect_field_name, self.request.GET.get(self.redirect_field_name, self.success_url))
-		return redirect_to
-
-class LogoutView(RedirectView):
-	url = reverse_lazy("home")
-	
-	def get(self, request, *args, **kwargs):
-		logout(request)
-		return super(LogoutView, self).get(request, *args, **kwargs)
-	
-class RegisterView(CreateView):
-	model = User 
-	template_name = 'registration/register.html'
-	form_class = UserCreationForm
-	success_url = reverse_lazy("account:user_created")
-	
-	def post(self, request, *args, **kwargs):
-		self.object = None
-		form = self.get_form()
-		if form.is_valid():
-			form.save()
-			user = authenticate(username=request.POST["username"], password=request.POST["password1"])
-			login(request, user)
-			return HttpResponseRedirect(self.success_url)
-		else:
-			return self.form_invalid(form)
-
-class UserCreatedView(TemplateView):
+from django.utils.http import is_safe_url
+from django.contrib.auth import authenticate, login, logout, get_user_model
+from django.contrib.auth.forms import UserCreationForm
+from django.shortcuts import redirect, render, HttpResponseRedirect
+from django.core.urlresolvers import reverse_lazy
+from django.contrib.auth.forms import AuthenticationForm
+from django.views.generic import FormView
+from django.views.generic.base import RedirectView, TemplateView
+from django.views.generic.edit import CreateView
+
+User = get_user_model()
+
+def login_form(request):
+	pass
+
+def login_view(request):
+	return HttpResponse('show show a login form')
+	
+def logout_view(request):
+	logout(request)
+
+
+class LoginView(FormView):
+	template_name = "registration/login.html"
+	form_class = AuthenticationForm
+	success_url = reverse_lazy('home')
+	redirect_field_name = "next"
+	
+	def get_context_data(self, **kwargs):
+		context = super(LoginView, self).get_context_data(**kwargs)
+		context["next"] = self.get_success_url()
+		return context
+	
+	def get(self, request, *args, **kwargs):
+		if request.user.is_authenticated():
+			return HttpResponseRedirect(self.get_success_url())
+		return super(LoginView, self).get(request, *args, **kwargs)
+	
+	def form_valid(self, form):
+		login(self.request, form.get_user())
+		return HttpResponseRedirect(self.get_success_url())
+	
+	def get_success_url(self):
+		redirect_to = self.request.POST.get(self.redirect_field_name, self.request.GET.get(self.redirect_field_name, self.success_url))
+		return redirect_to
+
+class LogoutView(RedirectView):
+	url = reverse_lazy("home")
+	
+	def get(self, request, *args, **kwargs):
+		logout(request)
+		return super(LogoutView, self).get(request, *args, **kwargs)
+	
+class RegisterView(CreateView):
+	model = User 
+	template_name = 'registration/register.html'
+	form_class = UserCreationForm
+	success_url = reverse_lazy("account:user_created")
+	
+	def post(self, request, *args, **kwargs):
+		self.object = None
+		form = self.get_form()
+		if form.is_valid():
+			form.save()
+			user = authenticate(username=request.POST["username"], password=request.POST["password1"])
+			login(request, user)
+			return HttpResponseRedirect(self.success_url)
+		else:
+			return self.form_invalid(form)
+
+class UserCreatedView(TemplateView):
 	template_name='registration/created.html'
\ No newline at end of file
--- a/src/iconolab/models.py	Wed Jan 18 16:50:59 2017 +0100
+++ b/src/iconolab/models.py	Wed Jan 18 16:53:46 2017 +0100
@@ -1,933 +1,933 @@
-from django.db import models, transaction
-from django.conf import settings
-from django.contrib.auth.models import User
-from django.contrib.contenttypes.fields import GenericRelation
-from django.contrib.contenttypes.models import ContentType
-from django_comments_xtd.models import XtdComment
-from django.utils.text import slugify
-import iconolab.signals.handlers as iconolab_signals
-import uuid
-import json
-import re
-import requests
-import urllib
-import logging
-
-logger = logging.getLogger(__name__)
-
-
-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="")
-    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)
-    height = models.IntegerField(null=True, blank=True)
-    width = models.IntegerField(null=True, blank=True)
-    show_image_on_home = models.BooleanField(default=False)
-
-    def __str__(self):
-        return self.name
-
-
-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)
-
-    def __str__(self):
-        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
-    """
-    item = models.OneToOneField('Item', related_name='metadatas')
-    authors = models.CharField(max_length=255, default="")
-    school = models.CharField(max_length=255, default="")
-    field = models.CharField(max_length=255, default="")
-    designation = models.CharField(max_length=255, default="")
-    datation = models.CharField(max_length=255, default="")
-    technics = models.CharField(max_length=255, default="")
-    measurements = models.CharField(max_length=255, default="")
-    create_or_usage_location = models.CharField(max_length=255, default="")
-    discovery_context = models.CharField(max_length=255, default="")
-    conservation_location = models.CharField(max_length=255, default="")
-    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')
-
-    def __str__(self):
-        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)
-    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
-
-    @property
-    def wh_ratio(self):
-        return self.width / self.height
-
-    @property
-    def collection(self):
-        return self.item.collection.name
-
-    @property
-    def title(self):
-        return self.item.metadatas.designation
-
-    @property
-    def authors(self):
-        return self.item.metadatas.authors
-
-    @property
-    def school(self):
-        return self.item.metadatas.school
-
-    @property
-    def designation(self):
-        return self.item.metadatas.designation
-
-    @property
-    def datation(self):
-        return self.item.metadatas.datation
-
-    @property
-    def technics(self):
-        return self.item.metadatas.technics
-
-    @property
-    def measurements(self):
-        return self.item.metadatas.measurements
-
-    @property
-    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
-        return tag_list
-
-
-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()
-
-    @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,
-            author=author
-        )
-        new_annotation.save()
-
-        # Create initial revision
-        initial_revision = AnnotationRevision(
-            annotation=new_annotation,
-            author=author,
-            title=title,
-            description=description,
-            fragment=fragment,
-            state=AnnotationRevision.ACCEPTED
-        )
-        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)
-        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
-            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_revision_on_annotations = []
-        user_contributed_annotations = Annotation.objects.filter(revisions__author=user).exclude(author=user).prefetch_related(
-            'current_revision',
-            'revisions',
-            'image',
-            '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"))
-        contributed_list = []
-        if latest_revision_on_annotations:
-            latest_revision_on_annotations.sort(
-                key=lambda item: item.created, reverse=True)
-            contributed_list = [
-                {
-                    "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(),
-                    "latest_submitted_revision": revision.created
-                }
-                for revision in latest_revision_on_annotations
-            ]
-        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')
-        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]
-        unique_ordered_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})
-        commented_annotations = Annotation.objects.filter(id__in=[item["annotation_id"] for item in unique_ordered_comments_data]).prefetch_related(
-            'current_revision',
-            'revisions',
-            'image',
-            'image__item',
-            'image__item__collection'
-        ).distinct()
-        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"])
-            sorted_annotations_list.append(
-                {
-                    "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"]
-                }
-            )
-        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.
-
-        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)
-    """
-    UNVALIDATED = 0
-    VALIDATED = 1
-    VALIDATION_STATES = (
-        (UNVALIDATED, 'unvalidated'),
-        (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)
-    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)
-    validation_state_overriden = models.BooleanField(default=False)
-
-    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)
-    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'))
-
-    def __str__(self):
-        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)
-        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)
-        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()
-
-    @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
-        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()
-        # accepted_revisions_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,
-        ).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,
-            metacategories__collection=self.annotation.image.item.collection
-        )
-        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)
-                else:
-                    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)
-    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)
-
-
-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
-        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
-    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)
-    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'))
-    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
-
-    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:
-            pass
-        for tag_data in tags_dict:
-            tag_string = tag_data.get("tag_input")
-            tag_accuracy = tag_data.get("accuracy", 0)
-            tag_relevancy = tag_data.get("relevancy", 0)
-
-            # 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,
-                    )
-            else:
-                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
-                    )
-            tag_info = TaggingInfo.objects.create(
-                tag=tag_obj,
-                revision=self,
-                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
-                        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))
-            sparql_query_url = source + 'sparql'
-            try:
-                dbpedia_resp = requests.get(
-                    sparql_query_url,
-                    params={
-                        "query": sparql_query,
-                        "format": "json"
-                    }
-                )
-            except:
-                # dbpedia is down, will be handled with database label
-                pass
-            try:
-                results = json.loads(dbpedia_resp.text).get("results", {})
-            except:
-                # if error with json, results is empty
-                results = {}
-            variable_bindings = results.get("bindings", None)
-            label_data = {}
-            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():
-                final_list.append({
-                    "tag_label": tagging_info.tag.label,
-                    "tag_link": tagging_info.tag.link,
-                    "accuracy": tagging_info.accuracy,
-                    "relevancy": tagging_info.relevancy,
-                    "is_internal": tagging_info.tag.is_internal()
-                })
-            else:
-                tag_link = tagging_info.tag.link
-                # 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]))
-                    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
-                    else:
-                        tagging_info.tag.label = tag_label
-                        tagging_info.tag.save()
-                    final_list.append({
-                        "tag_label": tag_label,
-                        "tag_link": tag_link,
-                        "accuracy": tagging_info.accuracy,
-                        "relevancy": tagging_info.relevancy,
-                        "is_internal": tagging_info.tag.is_internal()
-                    })
-                except StopIteration:
-                    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'))
-
-    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
-
-
-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
-    COMMENTERS = 2
-    COLLECTION_ADMINS = 3
-    NOTIFIED_USERS = (
-        (NONE, 'none'),
-        (CONTRIBUTORS, 'contributors'),
-        (COMMENTERS, 'commenters'),
-        (COLLECTION_ADMINS, 'collection admins'),
-    )
-
-    NEUTRAL = 0
-    AGREEMENT = 1
-    DISAGREEMENT = 2
-    VALIDATION_VALUES = (
-        (NEUTRAL, 'neutral'),
-        (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)
-
-    def __str__(self):
-        return self.label + ":" + self.collection.name
-
-
-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
-    COMMENT_CHOICES = (
-        (LINK, 'link'),
-        (IMAGE, 'image'),
-        (PDF, 'pdf')
-    )
-
-    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)
-
-    def __str__(self):
-        return "profile:" + self.user.username
+from django.db import models, transaction
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.contrib.contenttypes.fields import GenericRelation
+from django.contrib.contenttypes.models import ContentType
+from django_comments_xtd.models import XtdComment
+from django.utils.text import slugify
+import iconolab.signals.handlers as iconolab_signals
+import uuid
+import json
+import re
+import requests
+import urllib
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+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="")
+    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)
+    height = models.IntegerField(null=True, blank=True)
+    width = models.IntegerField(null=True, blank=True)
+    show_image_on_home = models.BooleanField(default=False)
+
+    def __str__(self):
+        return self.name
+
+
+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)
+
+    def __str__(self):
+        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
+    """
+    item = models.OneToOneField('Item', related_name='metadatas')
+    authors = models.CharField(max_length=255, default="")
+    school = models.CharField(max_length=255, default="")
+    field = models.CharField(max_length=255, default="")
+    designation = models.CharField(max_length=255, default="")
+    datation = models.CharField(max_length=255, default="")
+    technics = models.CharField(max_length=255, default="")
+    measurements = models.CharField(max_length=255, default="")
+    create_or_usage_location = models.CharField(max_length=255, default="")
+    discovery_context = models.CharField(max_length=255, default="")
+    conservation_location = models.CharField(max_length=255, default="")
+    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')
+
+    def __str__(self):
+        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)
+    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
+
+    @property
+    def wh_ratio(self):
+        return self.width / self.height
+
+    @property
+    def collection(self):
+        return self.item.collection.name
+
+    @property
+    def title(self):
+        return self.item.metadatas.designation
+
+    @property
+    def authors(self):
+        return self.item.metadatas.authors
+
+    @property
+    def school(self):
+        return self.item.metadatas.school
+
+    @property
+    def designation(self):
+        return self.item.metadatas.designation
+
+    @property
+    def datation(self):
+        return self.item.metadatas.datation
+
+    @property
+    def technics(self):
+        return self.item.metadatas.technics
+
+    @property
+    def measurements(self):
+        return self.item.metadatas.measurements
+
+    @property
+    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
+        return tag_list
+
+
+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()
+
+    @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,
+            author=author
+        )
+        new_annotation.save()
+
+        # Create initial revision
+        initial_revision = AnnotationRevision(
+            annotation=new_annotation,
+            author=author,
+            title=title,
+            description=description,
+            fragment=fragment,
+            state=AnnotationRevision.ACCEPTED
+        )
+        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)
+        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
+            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_revision_on_annotations = []
+        user_contributed_annotations = Annotation.objects.filter(revisions__author=user).exclude(author=user).prefetch_related(
+            'current_revision',
+            'revisions',
+            'image',
+            '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"))
+        contributed_list = []
+        if latest_revision_on_annotations:
+            latest_revision_on_annotations.sort(
+                key=lambda item: item.created, reverse=True)
+            contributed_list = [
+                {
+                    "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(),
+                    "latest_submitted_revision": revision.created
+                }
+                for revision in latest_revision_on_annotations
+            ]
+        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')
+        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]
+        unique_ordered_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})
+        commented_annotations = Annotation.objects.filter(id__in=[item["annotation_id"] for item in unique_ordered_comments_data]).prefetch_related(
+            'current_revision',
+            'revisions',
+            'image',
+            'image__item',
+            'image__item__collection'
+        ).distinct()
+        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"])
+            sorted_annotations_list.append(
+                {
+                    "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"]
+                }
+            )
+        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.
+
+        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)
+    """
+    UNVALIDATED = 0
+    VALIDATED = 1
+    VALIDATION_STATES = (
+        (UNVALIDATED, 'unvalidated'),
+        (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)
+    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)
+    validation_state_overriden = models.BooleanField(default=False)
+
+    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)
+    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'))
+
+    def __str__(self):
+        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)
+        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)
+        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()
+
+    @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
+        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()
+        # accepted_revisions_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,
+        ).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,
+            metacategories__collection=self.annotation.image.item.collection
+        )
+        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)
+                else:
+                    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)
+    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)
+
+
+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
+        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
+    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)
+    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'))
+    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
+
+    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:
+            pass
+        for tag_data in tags_dict:
+            tag_string = tag_data.get("tag_input")
+            tag_accuracy = tag_data.get("accuracy", 0)
+            tag_relevancy = tag_data.get("relevancy", 0)
+
+            # 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,
+                    )
+            else:
+                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
+                    )
+            tag_info = TaggingInfo.objects.create(
+                tag=tag_obj,
+                revision=self,
+                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
+                        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))
+            sparql_query_url = source + 'sparql'
+            try:
+                dbpedia_resp = requests.get(
+                    sparql_query_url,
+                    params={
+                        "query": sparql_query,
+                        "format": "json"
+                    }
+                )
+            except:
+                # dbpedia is down, will be handled with database label
+                pass
+            try:
+                results = json.loads(dbpedia_resp.text).get("results", {})
+            except:
+                # if error with json, results is empty
+                results = {}
+            variable_bindings = results.get("bindings", None)
+            label_data = {}
+            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():
+                final_list.append({
+                    "tag_label": tagging_info.tag.label,
+                    "tag_link": tagging_info.tag.link,
+                    "accuracy": tagging_info.accuracy,
+                    "relevancy": tagging_info.relevancy,
+                    "is_internal": tagging_info.tag.is_internal()
+                })
+            else:
+                tag_link = tagging_info.tag.link
+                # 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]))
+                    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
+                    else:
+                        tagging_info.tag.label = tag_label
+                        tagging_info.tag.save()
+                    final_list.append({
+                        "tag_label": tag_label,
+                        "tag_link": tag_link,
+                        "accuracy": tagging_info.accuracy,
+                        "relevancy": tagging_info.relevancy,
+                        "is_internal": tagging_info.tag.is_internal()
+                    })
+                except StopIteration:
+                    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'))
+
+    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
+
+
+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
+    COMMENTERS = 2
+    COLLECTION_ADMINS = 3
+    NOTIFIED_USERS = (
+        (NONE, 'none'),
+        (CONTRIBUTORS, 'contributors'),
+        (COMMENTERS, 'commenters'),
+        (COLLECTION_ADMINS, 'collection admins'),
+    )
+
+    NEUTRAL = 0
+    AGREEMENT = 1
+    DISAGREEMENT = 2
+    VALIDATION_VALUES = (
+        (NEUTRAL, 'neutral'),
+        (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)
+
+    def __str__(self):
+        return self.label + ":" + self.collection.name
+
+
+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
+    COMMENT_CHOICES = (
+        (LINK, 'link'),
+        (IMAGE, 'image'),
+        (PDF, 'pdf')
+    )
+
+    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)
+
+    def __str__(self):
+        return "profile:" + self.user.username
--- a/src/iconolab/settings/dev.py.tmpl	Wed Jan 18 16:50:59 2017 +0100
+++ b/src/iconolab/settings/dev.py.tmpl	Wed Jan 18 16:53:46 2017 +0100
@@ -1,278 +1,278 @@
-"""
-Django settings for iconolab project.
-
-Generated by 'django-admin startproject' using Django 1.9.5.
-
-For more information on this file, see
-https://docs.djangoproject.com/en/1.9/topics/settings/
-
-For the full list of settings and their values, see
-https://docs.djangoproject.com/en/1.9/ref/settings/
-"""
-import logging
-import os
-
-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__)))
-
-STATIC_ROOT = os.path.join(BASE_DIR, '../../web/static/site')
-MEDIA_ROOT = os.path.join(BASE_DIR, '../../web/media')
-
-# dev_mode useful for src_js
-# We need to add 'iconolab.utils.context_processors.env' to context processor
-
-JS_DEV_MODE = True
-STATICFILES_DIRS = [
-    os.path.join(BASE_DIR, 'static'),
-    os.path.join(BASE_DIR, 'media'),
-]
-
-if JS_DEV_MODE:
-    SRC_JS_PATH = os.path.join(BASE_DIR, '..', '..', 'src_js')
-    STATICFILES_DIRS.append(SRC_JS_PATH)
-
-
-
-BASE_URL = ''
-STATIC_URL = '/static/'
-MEDIA_URL = '/media/'
-
-LOGIN_URL = '/account/login/'
-
-# Quick-start development settings - unsuitable for production
-# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
-
-# SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = '#8)+upuo3vc7fi15czxz53ml7*(1__q8hg=m&+9ylq&st1_kqv'
-
-# SECURITY WARNING: don't run with debug turned on in production!
-DEBUG = True
-THUMBNAIL_DEBUG = True
-
-ALLOWED_HOSTS = []
-
-
-# Application definition
-
-INSTALLED_APPS = [
-    'django.contrib.admin',
-    'django.contrib.auth',
-    'django.contrib.contenttypes',
-    'django.contrib.sessions',
-    'django.contrib.messages',
-    'django.contrib.staticfiles',
-    'django.contrib.sites',
-    'django_comments',
-    'django_comments_xtd',
-    'haystack',
-    'iconolab.apps.IconolabApp',
-    'sorl.thumbnail',
-    'notifications'
-]
-
-
-
-COMMENTS_APP = "django_comments_xtd"
-COMMENTS_XTD_MODEL = "iconolab.models.IconolabComment"
-COMMENTS_XTD_FORM_CLASS = 'iconolab.forms.comments.IconolabCommentForm'
-COMMENTS_XTD_MAX_THREAD_LEVEL = 100
-COMMENTS_PER_PAGE_DEFAULT = 10
-
-SITE_ID = 1
-
-MIDDLEWARE_CLASSES = [
-    'django.middleware.security.SecurityMiddleware',
-    'django.contrib.sessions.middleware.SessionMiddleware',
-    'django.middleware.common.CommonMiddleware',
-    'django.middleware.csrf.CsrfViewMiddleware',
-    'django.contrib.auth.middleware.AuthenticationMiddleware',
-    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
-    'django.contrib.messages.middleware.MessageMiddleware',
-    'django.middleware.clickjacking.XFrameOptionsMiddleware',
-]
-
-ROOT_URLCONF = 'iconolab.urls'
-
-TEMPLATES = [
-    {
-        'BACKEND': 'django.template.backends.django.DjangoTemplates',
-        'DIRS': [os.path.join(BASE_DIR,'iconolab','templates')],
-        'APP_DIRS': True,
-        'OPTIONS': {
-            'context_processors': [
-                'django.template.context_processors.debug',
-                'django.template.context_processors.request',
-                'django.contrib.auth.context_processors.auth',
-                'django.contrib.messages.context_processors.messages',
-                'django.template.context_processors.media',
-                'django.template.context_processors.static',
-                'django.template.context_processors.i18n',
-                'iconolab.utils.context_processors.env',
-            ],
-        },
-    },
-]
-
-WSGI_APPLICATION = 'iconolab.wsgi.application'
-
-
-# Database
-# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
-
-DATABASES = {
-    'default': {
-        'ENGINE': 'django.db.backends.postgresql_psycopg2', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
-        'NAME': '',                      # Or path to database file if using sqlite3.
-        'USER': '',                      # Not used with sqlite3.
-        'PASSWORD': '',                  # Not used with sqlite3.
-        'HOST': '',                      # Set to empty string for localhost. Not used with sqlite3.
-        'PORT': '',                      # Set to empty string for default. Not used with sqlite3.
-    }
-}
-
-# Logging
-
-LOG_FILE = os.path.abspath(os.path.join(BASE_DIR,"../../run/log/log.txt"))
-IMPORT_LOG_FILE = os.path.abspath(os.path.join(BASE_DIR,"../../run/log/import_log.txt"))
-IMPORT_LOGGER_NAME = "import_command"
-LOG_LEVEL = logging.DEBUG
-LOGGING = {
-    'version': 1,
-    'disable_existing_loggers': True,
-    'filters': {
-        'require_debug_false': {
-            '()': 'django.utils.log.RequireDebugFalse'
-        }
-    },
-    'formatters' : {
-        'simple' : {
-            'format': "%(asctime)s - %(levelname)s : %(message)s",
-        },
-        'semi-verbose': {
-            'format': '%(levelname)s %(asctime)s %(module)s %(message)s'
-        },
-    },
-    'handlers': {
-        'mail_admins': {
-            'level': 'ERROR',
-            'filters': ['require_debug_false'],
-            'class': 'django.utils.log.AdminEmailHandler'
-        },
-        'stream_to_console': {
-            'level': LOG_LEVEL,
-            'class': 'logging.StreamHandler'
-        },
-        'file': {
-            'level': LOG_LEVEL,
-            'class': 'logging.FileHandler',
-            'filename': LOG_FILE,
-            'formatter': 'semi-verbose',
-        },
-        'import_file': {
-            'level': LOG_LEVEL,
-            'class': 'logging.FileHandler',
-            'filename': IMPORT_LOG_FILE,
-            'formatter': 'semi-verbose',
-        }
-    },
-    'loggers': {
-        'django.request': {
-            'handlers': ['file'],
-            'level': LOG_LEVEL,
-            'propagate': True,
-        },
-        'iconolab': {
-            'handlers': ['file'],
-            'level': LOG_LEVEL,
-            'propagate': True,
-        },
-        'import_command': {
-            'handlers': ['import_file'],
-            'level': LOG_LEVEL,
-            'propagate': True,
-        },
-    }
-}
-
-# Haystack connection
-HAYSTACK_CONNECTIONS = {
-    'default': {
-        'ENGINE': 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine',
-        'URL': 'http://127.0.0.1:9200/',
-        'INDEX_NAME': 'haystack',
-    },
-}
-
-# HAYSTACK_SIGNAL_PROCESSOR
-HAYSTACK_SIGNAL_PROCESSOR = 'iconolab.search_indexes.signals.RevisionSignalProcessor'
-
-CACHES = {
-    'default': {
-        'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
-        'LOCATION': os.path.join(MEDIA_ROOT, 'cache'),
-#        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
-#        'LOCATION': 'unix:/var/run/memcached/memcached.socket',
-#        'KEY_PREFIX': 'ldt',
-    }
-}
-# Password validation
-# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
-
-AUTH_PASSWORD_VALIDATORS = [
-    {
-        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
-    },
-    {
-        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
-    },
-    {
-        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
-    },
-    {
-        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
-    },
-]
-
-
-# Internationalization
-# https://docs.djangoproject.com/en/1.9/topics/i18n/
-
-LANGUAGE_CODE = 'en-us'
-
-TIME_ZONE = 'UTC'
-
-USE_I18N = True
-
-USE_L10N = True
-
-USE_TZ = True
-
-
-IMPORT_FIELDS_DICT = {
-    "AUTR": [],
-    "ECOLE": [],
-    "TITR": ["Titre"],
-    "DENO": [],
-    "DOM": ["Domaine"],
-    "APPL": [],
-    "PERI": ["Période"],
-    "MILL": [],
-    "EPOCH": [],
-    "TECH": [],
-    "DIMS": ["Dimensions"],
-    "EPOCH": [],
-    "LIEUX": [],
-    "DECV": [],
-    "LOCA": ["Localisation"],
-    "PHOT": ["Photo"],
-    "INV": ["No inventaire",],
-    "REF": ["REFERENCE"],
-}
-
-INTERNAL_TAGS_URL = BASE_URL
-JOCONDE_NOTICE_BASE_URL = "http://www.culture.gouv.fr/public/mistral/joconde_fr?ACTION=CHERCHER&FIELD_98=REF&VALUE_98="
-
-RELEVANT_TAGS_MIN_SCORE = 3
-ACCURATE_TAGS_MIN_SCORE = 3
+"""
+Django settings for iconolab project.
+
+Generated by 'django-admin startproject' using Django 1.9.5.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.9/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/1.9/ref/settings/
+"""
+import logging
+import os
+
+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__)))
+
+STATIC_ROOT = os.path.join(BASE_DIR, '../../web/static/site')
+MEDIA_ROOT = os.path.join(BASE_DIR, '../../web/media')
+
+# dev_mode useful for src_js
+# We need to add 'iconolab.utils.context_processors.env' to context processor
+
+JS_DEV_MODE = True
+STATICFILES_DIRS = [
+    os.path.join(BASE_DIR, 'static'),
+    os.path.join(BASE_DIR, 'media'),
+]
+
+if JS_DEV_MODE:
+    SRC_JS_PATH = os.path.join(BASE_DIR, '..', '..', 'src_js')
+    STATICFILES_DIRS.append(SRC_JS_PATH)
+
+
+
+BASE_URL = ''
+STATIC_URL = '/static/'
+MEDIA_URL = '/media/'
+
+LOGIN_URL = '/account/login/'
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = '#8)+upuo3vc7fi15czxz53ml7*(1__q8hg=m&+9ylq&st1_kqv'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+THUMBNAIL_DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+    'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+    'django.contrib.sites',
+    'django_comments',
+    'django_comments_xtd',
+    'haystack',
+    'iconolab.apps.IconolabApp',
+    'sorl.thumbnail',
+    'notifications'
+]
+
+
+
+COMMENTS_APP = "django_comments_xtd"
+COMMENTS_XTD_MODEL = "iconolab.models.IconolabComment"
+COMMENTS_XTD_FORM_CLASS = 'iconolab.forms.comments.IconolabCommentForm'
+COMMENTS_XTD_MAX_THREAD_LEVEL = 100
+COMMENTS_PER_PAGE_DEFAULT = 10
+
+SITE_ID = 1
+
+MIDDLEWARE_CLASSES = [
+    'django.middleware.security.SecurityMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.middleware.common.CommonMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+    'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'iconolab.urls'
+
+TEMPLATES = [
+    {
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
+        'DIRS': [os.path.join(BASE_DIR,'iconolab','templates')],
+        'APP_DIRS': True,
+        'OPTIONS': {
+            'context_processors': [
+                'django.template.context_processors.debug',
+                'django.template.context_processors.request',
+                'django.contrib.auth.context_processors.auth',
+                'django.contrib.messages.context_processors.messages',
+                'django.template.context_processors.media',
+                'django.template.context_processors.static',
+                'django.template.context_processors.i18n',
+                'iconolab.utils.context_processors.env',
+            ],
+        },
+    },
+]
+
+WSGI_APPLICATION = 'iconolab.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.postgresql_psycopg2', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
+        'NAME': '',                      # Or path to database file if using sqlite3.
+        'USER': '',                      # Not used with sqlite3.
+        'PASSWORD': '',                  # Not used with sqlite3.
+        'HOST': '',                      # Set to empty string for localhost. Not used with sqlite3.
+        'PORT': '',                      # Set to empty string for default. Not used with sqlite3.
+    }
+}
+
+# Logging
+
+LOG_FILE = os.path.abspath(os.path.join(BASE_DIR,"../../run/log/log.txt"))
+IMPORT_LOG_FILE = os.path.abspath(os.path.join(BASE_DIR,"../../run/log/import_log.txt"))
+IMPORT_LOGGER_NAME = "import_command"
+LOG_LEVEL = logging.DEBUG
+LOGGING = {
+    'version': 1,
+    'disable_existing_loggers': True,
+    'filters': {
+        'require_debug_false': {
+            '()': 'django.utils.log.RequireDebugFalse'
+        }
+    },
+    'formatters' : {
+        'simple' : {
+            'format': "%(asctime)s - %(levelname)s : %(message)s",
+        },
+        'semi-verbose': {
+            'format': '%(levelname)s %(asctime)s %(module)s %(message)s'
+        },
+    },
+    'handlers': {
+        'mail_admins': {
+            'level': 'ERROR',
+            'filters': ['require_debug_false'],
+            'class': 'django.utils.log.AdminEmailHandler'
+        },
+        'stream_to_console': {
+            'level': LOG_LEVEL,
+            'class': 'logging.StreamHandler'
+        },
+        'file': {
+            'level': LOG_LEVEL,
+            'class': 'logging.FileHandler',
+            'filename': LOG_FILE,
+            'formatter': 'semi-verbose',
+        },
+        'import_file': {
+            'level': LOG_LEVEL,
+            'class': 'logging.FileHandler',
+            'filename': IMPORT_LOG_FILE,
+            'formatter': 'semi-verbose',
+        }
+    },
+    'loggers': {
+        'django.request': {
+            'handlers': ['file'],
+            'level': LOG_LEVEL,
+            'propagate': True,
+        },
+        'iconolab': {
+            'handlers': ['file'],
+            'level': LOG_LEVEL,
+            'propagate': True,
+        },
+        'import_command': {
+            'handlers': ['import_file'],
+            'level': LOG_LEVEL,
+            'propagate': True,
+        },
+    }
+}
+
+# Haystack connection
+HAYSTACK_CONNECTIONS = {
+    'default': {
+        'ENGINE': 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine',
+        'URL': 'http://127.0.0.1:9200/',
+        'INDEX_NAME': 'haystack',
+    },
+}
+
+# HAYSTACK_SIGNAL_PROCESSOR
+HAYSTACK_SIGNAL_PROCESSOR = 'iconolab.search_indexes.signals.RevisionSignalProcessor'
+
+CACHES = {
+    'default': {
+        'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
+        'LOCATION': os.path.join(MEDIA_ROOT, 'cache'),
+#        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
+#        'LOCATION': 'unix:/var/run/memcached/memcached.socket',
+#        'KEY_PREFIX': 'ldt',
+    }
+}
+# Password validation
+# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+    {
+        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+    },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/1.9/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+IMPORT_FIELDS_DICT = {
+    "AUTR": [],
+    "ECOLE": [],
+    "TITR": ["Titre"],
+    "DENO": [],
+    "DOM": ["Domaine"],
+    "APPL": [],
+    "PERI": ["Période"],
+    "MILL": [],
+    "EPOCH": [],
+    "TECH": [],
+    "DIMS": ["Dimensions"],
+    "EPOCH": [],
+    "LIEUX": [],
+    "DECV": [],
+    "LOCA": ["Localisation"],
+    "PHOT": ["Photo"],
+    "INV": ["No inventaire",],
+    "REF": ["REFERENCE"],
+}
+
+INTERNAL_TAGS_URL = BASE_URL
+JOCONDE_NOTICE_BASE_URL = "http://www.culture.gouv.fr/public/mistral/joconde_fr?ACTION=CHERCHER&FIELD_98=REF&VALUE_98="
+
+RELEVANT_TAGS_MIN_SCORE = 3
+ACCURATE_TAGS_MIN_SCORE = 3
--- a/src/iconolab/static/iconolab/css/iconolab.css	Wed Jan 18 16:50:59 2017 +0100
+++ b/src/iconolab/static/iconolab/css/iconolab.css	Wed Jan 18 16:53:46 2017 +0100
@@ -1,294 +1,294 @@
-body {padding-top: 20px; padding-bottom: 20px}
-
-.navbar-container{
-	vertical-align:middle;
-}
-.navbar-homepage {
-    width: calc(100% - 320px);
-    display: inline-block;
-    margin-top: 12px;
-    vertical-align:middle;
-}
-.homepage-logo{
-	display:inline-block;
-}
-
-.drawingModeBtn {border: 1px solid orange; cursor: pointer; height: 25px; margin-bottom: 10px}
-
-.form-drawing {border-bottom: 1px solid #C3C3C3; }
-
-.form-drawing-wrapper .selected {border: 1px solid orange; color: white; background-color: orange}
-.showPointer {cursor: pointer;}
-
-.zoom-action-list {
-	padding-left:21px;
-}
-
-.zoomTarget-wrapper {
-	padding: 0px;
-}
-
-#zoomTarget, .cut-canvas {
-	border: 1px solid #C3C3C3; 
-	padding-top: 2px; 
-	padding-bottom: 2px
-}
-
-.no-padding {
-	padding-left: 0; 
-	padding-right: 0;
-}
-
-.annotation-content{
-	margin-top: 15px;
-	margin-bottom: 15px;
-}
-
-.highlight {
-	border: 1px solid orange;
-}
-
-.revision-proposal {
-	background-color: #ECF0F1;
-}
-
-.collection-home-btn{
-	margin-top: 5px;
-}
-
-.img-stats-dt{
-	width: 250px !important;
-}
-
-.img-stats-dd{
-	margin-left: 270px !important;
-}
-
-.revision-link:hover{
-  	text-decoration: underline;
-}
-
-.item-image-thumbnail:hover{
-	cursor: pointer;
-}
-
-ul.annotation-comments{
-	background-color: #ededed;
-}
-li.list-group-item{
-	border: 1px solid #bbb
-}
-
-.comment-reply-link{
-	cursor: pointer;
-}
-.comment-subtext{
-	font-size:0.9em; 
-	display:inline;
-}
-.comment-metacategories{
-	font-size:0.9em; 
-	display:inline;
-}
-.comment-separator{
-	margin-top: 10px;
-	margin-bottom: 5px;
-}
-
-.pagination-shortcut{
-	cursor: pointer;	
-}
-
-/* BADGES */
-
-.badge-error {
-  background-color: #b94a48;
-}
-.badge-warning {
-  background-color: #f89406;
-}
-.badge-success {
-  background-color: #468847;
-}
-.badge-info {
-  background-color: #3a87ad;
-}
-.badge-inverse {
-  background-color: #333333;
-}
-
-.notif-badge{
-  margin-bottom: 5px;
-}
-
-/* USER PAGE */
-
-.show-all-notifications{
-  cursor: pointer;
-}
-.annotation-panel{
-	min-width: 535px;
-}
-.annotation-detail{
-	display:inline-block;
-	margin-right: 5px;
-	margin-left: 5px;
-	margin-top: 5px;
-	white-space: normal;
-	vertical-align: top;
-	padding: 10px;
-}
-
-.stats-annotation-userpage{
-	display:inline-block;
-	vertical-align: top;
-	padding: 10px;
-}
-
-.image-detail{
-	display:inline-block;
-	vertical-align: top;
-	width:150px;
-}
-.large-image-detail{
-	display:inline-block;
-	vertical-align: top;
-	width:400px;
-}
-
-
-.panel .dl-horizontal dt {
-    white-space: normal;
-	text-align: left;
-}
-
-.no-user-annotation{
-	margin-left: 15px;
-}
-.dt-annotation{
-	margin-bottom: 10px;
-}
-.userpage-annotation-btn{
-	display:inline-block; 
-	vertical-align: top; 
-	margin-bottom:5px;
-}
-
-/* GLOBAL HOME PAGE */
-
-.home-main-button-container{
-	position: relative; 
-	width:0px; 
-	height: 0px;
-}
-
-.home-main-button{
-	position: absolute; 
-	left: 15px; 
-	top: 15px
-}
-
-.collection-title{
-	margin-top:0px;
-}
-
-/* COLLECTION HOME PAGE */
-
-.collection-summary{
-	padding-bottom: 15px;
-	width: 100%
-}
-
-.collection-container{
-	width:100%;
-	padding-top: 15px; 
-	padding-bottom: 15px;
-}
-
-.collection-home-title{
-	padding-bottom: 15px;
-}
-
-.tab-selector{
-	margin-top: 15px;
-}
-
-.image-list-wrapper{
-	margin-top: 15px;
-}
-
-li.image-list-li{
-	margin-bottom: 5px; 
-	width: 370px; 
-	height: 430px; 
-	vertical-align:middle; 
-	padding:5px;
-}
-.image-list-image-container{
-	 position: relative;
-}
-.object-info{
-	margin-bottom:10px;
-}
-.collection-home-item-btn{
-	margin-bottom:5px;
-}
-.collection-home-tab{
-	cursor: pointer;
-}
-
-
-/* DIFF STYLE */
-
-.diff-viewer-wrapper {margin-top: 5px;}
-.diff-panel {border: 1px solid gray; width: 300px; heigth: 250px;}
-
-del { text-decoration: line-through;	color: #b30000;	background: #fadad7;}
-
-ins {	background: #eaf2c2; color: #406619; text-decoration: none; }
-
-.diff-panel .close-btn {
-	cursor: pointer;
-	margin-right: 5px;
-}
-
-/* FOOTER STYLE */
-
-footer div{
-}
-
-
-.partners-icons{
-	margin-left: 20px;
-}
-.footer-link{
-	margin-left: 20px;
-}
-
-.footer-info{
-	margin-left:15px;
-}
-
-/* STATIC PAGES STYLE */
-
-/* - LEGAL MENTIONS STYLE */
-
-dl.legals-dl dd{
-	margin-left: 10px;
-}
-
-/* HOME PAGE STYLE */
-
-.show-complete-link, .hide-complete-link{
-	cursor: pointer;
-}
-
-/* - COLLECTION HOME STYLE */
-
-.collection-summary{
-	margin-bottom: 15px;
-}
-
-.description-col{
-	padding: 10px;
+body {padding-top: 20px; padding-bottom: 20px}
+
+.navbar-container{
+	vertical-align:middle;
+}
+.navbar-homepage {
+    width: calc(100% - 320px);
+    display: inline-block;
+    margin-top: 12px;
+    vertical-align:middle;
+}
+.homepage-logo{
+	display:inline-block;
+}
+
+.drawingModeBtn {border: 1px solid orange; cursor: pointer; height: 25px; margin-bottom: 10px}
+
+.form-drawing {border-bottom: 1px solid #C3C3C3; }
+
+.form-drawing-wrapper .selected {border: 1px solid orange; color: white; background-color: orange}
+.showPointer {cursor: pointer;}
+
+.zoom-action-list {
+	padding-left:21px;
+}
+
+.zoomTarget-wrapper {
+	padding: 0px;
+}
+
+#zoomTarget, .cut-canvas {
+	border: 1px solid #C3C3C3; 
+	padding-top: 2px; 
+	padding-bottom: 2px
+}
+
+.no-padding {
+	padding-left: 0; 
+	padding-right: 0;
+}
+
+.annotation-content{
+	margin-top: 15px;
+	margin-bottom: 15px;
+}
+
+.highlight {
+	border: 1px solid orange;
+}
+
+.revision-proposal {
+	background-color: #ECF0F1;
+}
+
+.collection-home-btn{
+	margin-top: 5px;
+}
+
+.img-stats-dt{
+	width: 250px !important;
+}
+
+.img-stats-dd{
+	margin-left: 270px !important;
+}
+
+.revision-link:hover{
+  	text-decoration: underline;
+}
+
+.item-image-thumbnail:hover{
+	cursor: pointer;
+}
+
+ul.annotation-comments{
+	background-color: #ededed;
+}
+li.list-group-item{
+	border: 1px solid #bbb
+}
+
+.comment-reply-link{
+	cursor: pointer;
+}
+.comment-subtext{
+	font-size:0.9em; 
+	display:inline;
+}
+.comment-metacategories{
+	font-size:0.9em; 
+	display:inline;
+}
+.comment-separator{
+	margin-top: 10px;
+	margin-bottom: 5px;
+}
+
+.pagination-shortcut{
+	cursor: pointer;	
+}
+
+/* BADGES */
+
+.badge-error {
+  background-color: #b94a48;
+}
+.badge-warning {
+  background-color: #f89406;
+}
+.badge-success {
+  background-color: #468847;
+}
+.badge-info {
+  background-color: #3a87ad;
+}
+.badge-inverse {
+  background-color: #333333;
+}
+
+.notif-badge{
+  margin-bottom: 5px;
+}
+
+/* USER PAGE */
+
+.show-all-notifications{
+  cursor: pointer;
+}
+.annotation-panel{
+	min-width: 535px;
+}
+.annotation-detail{
+	display:inline-block;
+	margin-right: 5px;
+	margin-left: 5px;
+	margin-top: 5px;
+	white-space: normal;
+	vertical-align: top;
+	padding: 10px;
+}
+
+.stats-annotation-userpage{
+	display:inline-block;
+	vertical-align: top;
+	padding: 10px;
+}
+
+.image-detail{
+	display:inline-block;
+	vertical-align: top;
+	width:150px;
+}
+.large-image-detail{
+	display:inline-block;
+	vertical-align: top;
+	width:400px;
+}
+
+
+.panel .dl-horizontal dt {
+    white-space: normal;
+	text-align: left;
+}
+
+.no-user-annotation{
+	margin-left: 15px;
+}
+.dt-annotation{
+	margin-bottom: 10px;
+}
+.userpage-annotation-btn{
+	display:inline-block; 
+	vertical-align: top; 
+	margin-bottom:5px;
+}
+
+/* GLOBAL HOME PAGE */
+
+.home-main-button-container{
+	position: relative; 
+	width:0px; 
+	height: 0px;
+}
+
+.home-main-button{
+	position: absolute; 
+	left: 15px; 
+	top: 15px
+}
+
+.collection-title{
+	margin-top:0px;
+}
+
+/* COLLECTION HOME PAGE */
+
+.collection-summary{
+	padding-bottom: 15px;
+	width: 100%
+}
+
+.collection-container{
+	width:100%;
+	padding-top: 15px; 
+	padding-bottom: 15px;
+}
+
+.collection-home-title{
+	padding-bottom: 15px;
+}
+
+.tab-selector{
+	margin-top: 15px;
+}
+
+.image-list-wrapper{
+	margin-top: 15px;
+}
+
+li.image-list-li{
+	margin-bottom: 5px; 
+	width: 370px; 
+	height: 430px; 
+	vertical-align:middle; 
+	padding:5px;
+}
+.image-list-image-container{
+	 position: relative;
+}
+.object-info{
+	margin-bottom:10px;
+}
+.collection-home-item-btn{
+	margin-bottom:5px;
+}
+.collection-home-tab{
+	cursor: pointer;
+}
+
+
+/* DIFF STYLE */
+
+.diff-viewer-wrapper {margin-top: 5px;}
+.diff-panel {border: 1px solid gray; width: 300px; heigth: 250px;}
+
+del { text-decoration: line-through;	color: #b30000;	background: #fadad7;}
+
+ins {	background: #eaf2c2; color: #406619; text-decoration: none; }
+
+.diff-panel .close-btn {
+	cursor: pointer;
+	margin-right: 5px;
+}
+
+/* FOOTER STYLE */
+
+footer div{
+}
+
+
+.partners-icons{
+	margin-left: 20px;
+}
+.footer-link{
+	margin-left: 20px;
+}
+
+.footer-info{
+	margin-left:15px;
+}
+
+/* STATIC PAGES STYLE */
+
+/* - LEGAL MENTIONS STYLE */
+
+dl.legals-dl dd{
+	margin-left: 10px;
+}
+
+/* HOME PAGE STYLE */
+
+.show-complete-link, .hide-complete-link{
+	cursor: pointer;
+}
+
+/* - COLLECTION HOME STYLE */
+
+.collection-summary{
+	margin-bottom: 15px;
+}
+
+.description-col{
+	padding: 10px;
 }
\ No newline at end of file
--- a/src/iconolab/templates/iconolab/change_annotation.html	Wed Jan 18 16:50:59 2017 +0100
+++ b/src/iconolab/templates/iconolab/change_annotation.html	Wed Jan 18 16:53:46 2017 +0100
@@ -1,203 +1,203 @@
-{% extends 'iconolab_base.html' %}
-
-{% load staticfiles %}
-
-{% load thumbnail %}
-
-{% block content %}
-
-	<div id="drawing-zone" class="row" style="padding-top: 10px; padding-bottom: 10px; border:1px solid orange">
-
-		<div v-show='!formView' style="display:none" class="editor-wrapper col-md-12">
-			<div class="col-md-12 no-padding">
-				<p @click="cancel" class="pull-right btn btn-default btn-sm"><i class="fa fa-close"></i> Annuler</p>
-			</div>
-			<div class='col-md-2'>
-				<ul class="form-drawing-wrapper list-inline">
-					<p class='form-drawing pullright'><strong>Type de dessin</strong></p>
-					<li @click="setDrawingMode('RECT')" v-bind:class="{ 'selected': isRect }" class='pull-md-right drawingModeBtn'>Rect.</li>
-					<li @click="setDrawingMode('FREE')" v-bind:class="{ 'selected': !isRect }" class='pull-md-right drawingModeBtn'>Libre</li>
-				</ul>
-
-				{% thumbnail image.media "100x100" crop=False as im %}
-					<zoomview ref="zoomview" :image-url="'{{ im.url }}'" :image-width="{{ im.width }}" :image-height="{{ im.height }}">
-					</zoomview>
-				{% endthumbnail %}
-
-				<ul class='form-drawing-wrapper list-inline'>
-					<p class='form-drawing pullright'><strong>Actions</strong></p>
-
-					<li @click="clear" class='pull-md-right drawingModeBtn'><i class='fa fa-trash'></i> Effacer</li>
-
-					<li id="confirm-fragment-button" @click="showForm" class='pull-md-right drawingModeBtn infos info'><i class='fa fa-plus'></i> Valider</li>
-				</ul>
-			</div>
-
-			<div class="col-md-10">
-				<div ref="image" id="iconolab-image-wrapper">
-					{% with image.media as im %}
-						<svg class="cut-canvas" width="850" height="850">
-							<image class="main-image" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{{ im.url }}" x="0" y="0" width="{{ im.width }}" height="{{ im.height }}"></image>
-	    					<path class="image-path" d="{% if annotation %}{{ annotation.current_revision.fragment }}{% endif %}"></path>
-	    				</svg>
-                    {% endwith %}
-				</div>
-			</div>
-		</div>
-
-		<div v-show="formView" class="col-md-12">
-
-			<div class="col-md-6">
-				<div class="small-image-wrapper" style="position: relative">
-					{% thumbnail image.media "500x360" crop=False as im %}
-						<svg ref="smallSvgWrapper" width="{{ im.width }}" height="{{ im.height }}" version="1.1">
-							<image ref="smallImage" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{{ im.url }}" x="0" y="0"  width="{{ im.width }}" height="{{ im.height }}"></image>
-							<defs>
-								<mask id="smallImage">
-									<rect x="0" y="0" width="{{ im.width }}" height="{{ im.height }}" fill="white"/>
-									<g v-bind:transform="transformMatrix">
-										<path ref="currentPath" v-bind:d="fragmentPath"></path>
-									</g>
-								</mask>
-							</defs>
-
-							<g v-show="!displayMask" v-bind:transform="transformMatrix">
-								<path v-bind:d="fragmentPath" opacity="0.7" fill="orange"></path>
-							</g>
-
-							<rect v-show="displayMask" ref="smallMask" x="0" y="0" mask="url(#smallImage)" opacity="0.7" fill="white" width="{{ im.width }}" height="{{ im.height }}"></rect>
-						</svg>
-
-						<svg style="display:none" ref="zoomSvg" width="{{ im.width }}" height="{{ im.height }}" version="1.1">
-
-							<image xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{{ im.url }}" x="0" y="0" preserveAspectRatio="none" width="{{ im.width }}" height="{{ im.height }}"></image>
-
-							<g v-bind:transform="transformMatrix">
-								<path v-bind:d="fragmentPath" opacity="0.7" fill="orange"></path>
-							</g>
-						</svg>
-
-					{% endthumbnail %}
-				</div>
-				<ul class="list-inline list-unstyled">
-					<li @click="showEditor" class="showPointer btn btn-default btn-sm"> <i class='fa fa-edit'></i> Sélectionner le détail</li>
-					<li v-show="!displayMask" @click="highLightZone" class="showPointer show-zone btn btn-default btn-sm"> <i class='fa fa-eye-slash'></i> Afficher la zone</li>
-					<li v-show="displayMask" @click="highLightZone" class="showPointer hide-zone btn btn-default btn-sm"> <i class='fa fa-eye-slash'></i> Masquer la zone</li>
-					<li class="zoom-link btn btn-default btn-sm" v-if="canZoom" @click="zoom('in')"><i class="fa fa-zoom-in"></i>Zoomer</li>
-					<li class="zoom-link btn btn-default btn-sm" v-if="!canZoom" @click="zoom('out')"><i class="fa fa-zoom-out"></i>Dezoomer</li>
-				</ul>
-			</div>
-
-			<div class='col-md-6' style="">
-				 <form class="form" action="{% if annotation %}{% url 'annotation_edit' collection_name image_guid annotation_guid %}{% else %}{% url 'annotation_create' collection_name image_guid %}{% endif %}" method="POST">
-				 	{% if form.non_field_errors %}
-                        <div class="alert alert-danger" role="alert">
-                          <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
-                          <span class="sr-only">Erreur:</span>
-                            {{ form.non_field_errors | striptags }}
-                        </div>
-                    {% else %}
-                      {% if not annotation %}
-                        <div class="alert alert-info" role="alert">
-                          <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
-                          <span class="sr-only">Info:</span>
-                            Au moins un des trois champs, titre, description ou mots-clés, doit être renseigné.
-                        </div>
-                      {% endif %}
-                    {% endif %}
-
-                    {% if form.errors %}
-                    <div id="errors" style="display: none;">
-                        {% for field in form %}
-                          {% if field.errors %}
-                            * {{ field.name }}: {{ error|striptags }}
-                          {% endif %}
-                        {% endfor %}
-                    </div>
-                    {% endif %}
-                    {% csrf_token %}
-                    <fieldset class="form-group {% if form.title.errors %}has-error{% endif %}">
-                      <label class="control-label" for="id_{{ form.title.name }}">Titre</label>
-                      {% if form.title.errors %}
-                        <div class="alert alert-danger" role="alert">
-                          <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
-                          <span class="sr-only">Erreur:</span>
-                            {{ form.title.errors | striptags }}
-                        </div>
-                      {% endif %}
-                      <input type="text" class="form-control"
-                        name="{{ form.title.name }}"
-                        id="id_{{ form.title.name }}" value="{% if form.title.value %}{{ form.title.value}}{% endif %}">
-                    </fieldset>
-                    <fieldset class="form-group {% if form.description.errors %}has-error{% endif %}">
-                      <label class="control-label" for="id_{{ form.description.name }}">Description</label>
-                      {% if form.description.errors %}
-                        <div class="alert alert-danger" role="alert">
-                          <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
-                          <span class="sr-only">Erreur:</span>
-                            {{ form.description.errors | striptags }}
-                        </div>
-                      {% endif %}
-                      <textarea class="form-control"
-                        name="{{ form.description.name }}"
-                        id="id_{{ form.description.name }}" >{% if form.description.value %}{{ form.description.value}}{% endif %}</textarea>
-                    </fieldset>
-                    <fieldset class="form-group">
-                      <label class="control-label" for="id_{{ form.tags.name }}">Mots-Clés</label>
-                      {% if form.tags.errors %}
-                        <div class="alert alert-danger" role="alert">
-                          <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
-                          <span class="sr-only">Erreur:</span>
-                            {{ form.tags.errors | striptags }}
-                        </div>
-                      {% endif %}
-                      <typeahead :tags="{{ tags_data  }}"></typeahead>
-                    </fieldset>
-
-                    <fieldset class="form-group {% if form.comment.errors %}has-error{% endif %}">
-                      <label class="control-label" for="id_{{ form.comment.name }}">Commentaire sur mon annotation</label>
-                      {% if form.comment.errors %}
-                        <div class="alert alert-danger" role="alert">
-                          <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
-                          <span class="sr-only">Erreur:</span>
-                            {{ form.comment.errors | striptags }}
-                        </div>
-                      {% endif %}
-                      <textarea class="form-control"
-                        name="{{ form.comment.name }}"
-                        id="id_{{ form.comment.name }}" >{% if not annotation %} Commentaire de création de mon annotation.{% endif %}</textarea>
-                    </fieldset>
-        			<input id="fragment-hidden-field" v-model="normalizePath" type="hidden" name="fragment"></input>
-        			<button type="submit" class="save btn btn-default btn-sm">Enregister</button>
-                    <a class="btn btn-default btn-sm" href="{% if annotation %}{% url 'annotation_detail' collection_name image_guid annotation_guid %}{% else %}{% url 'item_detail' collection_name image.item.item_guid %}{% endif %}" role="button">Retour</a>
-    			    <br><br>
-                </form>
-			</div>
-
-		</div>
-	</div>
-
-{% endblock %}
-
-{% block footer_js %}
-	<script>
-		iconolab.Cutout.init();
-		$(document).ready(function(){
-		    if(($("#fragment-hidden-field").val()==";FREE")||!$("#fragment-hidden-field").val()){
-		        $(".zoom-link, .show-zone, .hide-zone").hide()
-			}
-			$("#confirm-fragment-button").on("click", function(){
-			    $("#fragment-hidden-field").trigger("change");
-			});
-			$("#fragment-hidden-field").on("change", function(){
-			    if($(this).val()==";FREE"){
-			        $(".zoom-link, .show-zone, .hide-zone").hide()
-			    }
-			    else {
-			        $(".zoom-link, .show-zone").show()
-			    }
-			});
-		});
-
-	</script>
-{% endblock %}
+{% extends 'iconolab_base.html' %}
+
+{% load staticfiles %}
+
+{% load thumbnail %}
+
+{% block content %}
+
+	<div id="drawing-zone" class="row" style="padding-top: 10px; padding-bottom: 10px; border:1px solid orange">
+
+		<div v-show='!formView' style="display:none" class="editor-wrapper col-md-12">
+			<div class="col-md-12 no-padding">
+				<p @click="cancel" class="pull-right btn btn-default btn-sm"><i class="fa fa-close"></i> Annuler</p>
+			</div>
+			<div class='col-md-2'>
+				<ul class="form-drawing-wrapper list-inline">
+					<p class='form-drawing pullright'><strong>Type de dessin</strong></p>
+					<li @click="setDrawingMode('RECT')" v-bind:class="{ 'selected': isRect }" class='pull-md-right drawingModeBtn'>Rect.</li>
+					<li @click="setDrawingMode('FREE')" v-bind:class="{ 'selected': !isRect }" class='pull-md-right drawingModeBtn'>Libre</li>
+				</ul>
+
+				{% thumbnail image.media "100x100" crop=False as im %}
+					<zoomview ref="zoomview" :image-url="'{{ im.url }}'" :image-width="{{ im.width }}" :image-height="{{ im.height }}">
+					</zoomview>
+				{% endthumbnail %}
+
+				<ul class='form-drawing-wrapper list-inline'>
+					<p class='form-drawing pullright'><strong>Actions</strong></p>
+
+					<li @click="clear" class='pull-md-right drawingModeBtn'><i class='fa fa-trash'></i> Effacer</li>
+
+					<li id="confirm-fragment-button" @click="showForm" class='pull-md-right drawingModeBtn infos info'><i class='fa fa-plus'></i> Valider</li>
+				</ul>
+			</div>
+
+			<div class="col-md-10">
+				<div ref="image" id="iconolab-image-wrapper">
+					{% with image.media as im %}
+						<svg class="cut-canvas" width="850" height="850">
+							<image class="main-image" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{{ im.url }}" x="0" y="0" width="{{ im.width }}" height="{{ im.height }}"></image>
+	    					<path class="image-path" d="{% if annotation %}{{ annotation.current_revision.fragment }}{% endif %}"></path>
+	    				</svg>
+                    {% endwith %}
+				</div>
+			</div>
+		</div>
+
+		<div v-show="formView" class="col-md-12">
+
+			<div class="col-md-6">
+				<div class="small-image-wrapper" style="position: relative">
+					{% thumbnail image.media "500x360" crop=False as im %}
+						<svg ref="smallSvgWrapper" width="{{ im.width }}" height="{{ im.height }}" version="1.1">
+							<image ref="smallImage" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{{ im.url }}" x="0" y="0"  width="{{ im.width }}" height="{{ im.height }}"></image>
+							<defs>
+								<mask id="smallImage">
+									<rect x="0" y="0" width="{{ im.width }}" height="{{ im.height }}" fill="white"/>
+									<g v-bind:transform="transformMatrix">
+										<path ref="currentPath" v-bind:d="fragmentPath"></path>
+									</g>
+								</mask>
+							</defs>
+
+							<g v-show="!displayMask" v-bind:transform="transformMatrix">
+								<path v-bind:d="fragmentPath" opacity="0.7" fill="orange"></path>
+							</g>
+
+							<rect v-show="displayMask" ref="smallMask" x="0" y="0" mask="url(#smallImage)" opacity="0.7" fill="white" width="{{ im.width }}" height="{{ im.height }}"></rect>
+						</svg>
+
+						<svg style="display:none" ref="zoomSvg" width="{{ im.width }}" height="{{ im.height }}" version="1.1">
+
+							<image xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{{ im.url }}" x="0" y="0" preserveAspectRatio="none" width="{{ im.width }}" height="{{ im.height }}"></image>
+
+							<g v-bind:transform="transformMatrix">
+								<path v-bind:d="fragmentPath" opacity="0.7" fill="orange"></path>
+							</g>
+						</svg>
+
+					{% endthumbnail %}
+				</div>
+				<ul class="list-inline list-unstyled">
+					<li @click="showEditor" class="showPointer btn btn-default btn-sm"> <i class='fa fa-edit'></i> Sélectionner le détail</li>
+					<li v-show="!displayMask" @click="highLightZone" class="showPointer show-zone btn btn-default btn-sm"> <i class='fa fa-eye-slash'></i> Afficher la zone</li>
+					<li v-show="displayMask" @click="highLightZone" class="showPointer hide-zone btn btn-default btn-sm"> <i class='fa fa-eye-slash'></i> Masquer la zone</li>
+					<li class="zoom-link btn btn-default btn-sm" v-if="canZoom" @click="zoom('in')"><i class="fa fa-zoom-in"></i>Zoomer</li>
+					<li class="zoom-link btn btn-default btn-sm" v-if="!canZoom" @click="zoom('out')"><i class="fa fa-zoom-out"></i>Dezoomer</li>
+				</ul>
+			</div>
+
+			<div class='col-md-6' style="">
+				 <form class="form" action="{% if annotation %}{% url 'annotation_edit' collection_name image_guid annotation_guid %}{% else %}{% url 'annotation_create' collection_name image_guid %}{% endif %}" method="POST">
+				 	{% if form.non_field_errors %}
+                        <div class="alert alert-danger" role="alert">
+                          <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
+                          <span class="sr-only">Erreur:</span>
+                            {{ form.non_field_errors | striptags }}
+                        </div>
+                    {% else %}
+                      {% if not annotation %}
+                        <div class="alert alert-info" role="alert">
+                          <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
+                          <span class="sr-only">Info:</span>
+                            Au moins un des trois champs, titre, description ou mots-clés, doit être renseigné.
+                        </div>
+                      {% endif %}
+                    {% endif %}
+
+                    {% if form.errors %}
+                    <div id="errors" style="display: none;">
+                        {% for field in form %}
+                          {% if field.errors %}
+                            * {{ field.name }}: {{ error|striptags }}
+                          {% endif %}
+                        {% endfor %}
+                    </div>
+                    {% endif %}
+                    {% csrf_token %}
+                    <fieldset class="form-group {% if form.title.errors %}has-error{% endif %}">
+                      <label class="control-label" for="id_{{ form.title.name }}">Titre</label>
+                      {% if form.title.errors %}
+                        <div class="alert alert-danger" role="alert">
+                          <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
+                          <span class="sr-only">Erreur:</span>
+                            {{ form.title.errors | striptags }}
+                        </div>
+                      {% endif %}
+                      <input type="text" class="form-control"
+                        name="{{ form.title.name }}"
+                        id="id_{{ form.title.name }}" value="{% if form.title.value %}{{ form.title.value}}{% endif %}">
+                    </fieldset>
+                    <fieldset class="form-group {% if form.description.errors %}has-error{% endif %}">
+                      <label class="control-label" for="id_{{ form.description.name }}">Description</label>
+                      {% if form.description.errors %}
+                        <div class="alert alert-danger" role="alert">
+                          <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
+                          <span class="sr-only">Erreur:</span>
+                            {{ form.description.errors | striptags }}
+                        </div>
+                      {% endif %}
+                      <textarea class="form-control"
+                        name="{{ form.description.name }}"
+                        id="id_{{ form.description.name }}" >{% if form.description.value %}{{ form.description.value}}{% endif %}</textarea>
+                    </fieldset>
+                    <fieldset class="form-group">
+                      <label class="control-label" for="id_{{ form.tags.name }}">Mots-Clés</label>
+                      {% if form.tags.errors %}
+                        <div class="alert alert-danger" role="alert">
+                          <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
+                          <span class="sr-only">Erreur:</span>
+                            {{ form.tags.errors | striptags }}
+                        </div>
+                      {% endif %}
+                      <typeahead :tags="{{ tags_data  }}"></typeahead>
+                    </fieldset>
+
+                    <fieldset class="form-group {% if form.comment.errors %}has-error{% endif %}">
+                      <label class="control-label" for="id_{{ form.comment.name }}">Commentaire sur mon annotation</label>
+                      {% if form.comment.errors %}
+                        <div class="alert alert-danger" role="alert">
+                          <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
+                          <span class="sr-only">Erreur:</span>
+                            {{ form.comment.errors | striptags }}
+                        </div>
+                      {% endif %}
+                      <textarea class="form-control"
+                        name="{{ form.comment.name }}"
+                        id="id_{{ form.comment.name }}" >{% if not annotation %} Commentaire de création de mon annotation.{% endif %}</textarea>
+                    </fieldset>
+        			<input id="fragment-hidden-field" v-model="normalizePath" type="hidden" name="fragment"></input>
+        			<button type="submit" class="save btn btn-default btn-sm">Enregister</button>
+                    <a class="btn btn-default btn-sm" href="{% if annotation %}{% url 'annotation_detail' collection_name image_guid annotation_guid %}{% else %}{% url 'item_detail' collection_name image.item.item_guid %}{% endif %}" role="button">Retour</a>
+    			    <br><br>
+                </form>
+			</div>
+
+		</div>
+	</div>
+
+{% endblock %}
+
+{% block footer_js %}
+	<script>
+		iconolab.Cutout.init();
+		$(document).ready(function(){
+		    if(($("#fragment-hidden-field").val()==";FREE")||!$("#fragment-hidden-field").val()){
+		        $(".zoom-link, .show-zone, .hide-zone").hide()
+			}
+			$("#confirm-fragment-button").on("click", function(){
+			    $("#fragment-hidden-field").trigger("change");
+			});
+			$("#fragment-hidden-field").on("change", function(){
+			    if($(this).val()==";FREE"){
+			        $(".zoom-link, .show-zone, .hide-zone").hide()
+			    }
+			    else {
+			        $(".zoom-link, .show-zone").show()
+			    }
+			});
+		});
+
+	</script>
+{% endblock %}
--- a/src/iconolab/templates/iconolab/detail_annotation.html	Wed Jan 18 16:50:59 2017 +0100
+++ b/src/iconolab/templates/iconolab/detail_annotation.html	Wed Jan 18 16:53:46 2017 +0100
@@ -1,280 +1,280 @@
-{% extends 'iconolab_base.html' %}
-
-{% load i18n %}
-{% load staticfiles %}
-{% load comments %}
-{% load comments_xtd %}
-{% load thumbnail %}
-{% load iconolab_tags %}
-
-{% block content %}
-	<div id="annotation-wrapper" class="row" style="border: 1px solid gray;padding-top: 10px;">
-
-		<div id="detail-annotation" class="col-md-12">
-			<div v-show="!showZoom" class="col-md-6">
-				<div class="small-image-wrapper" style="position: relative">
-					{% thumbnail annotation.image.media "300x300" crop=False as im %}
-						<img src="{{ im.url }}" width="{{ im.width }}" height="{{ im.height }}" />
-
-						<svg width="{{ im.width }}" height="{{ im.height }}" version="1.1" style="position:absolute; top:0px; left: 0px">
-
-							<g transform="matrix({% transform_matrix im_width=im.width im_height=im.height max_x=100 max_y=100 %})">
-								<path d="{{ annotation.current_revision.fragment|clean_path }}" opacity="0.7" fill="orange"></path>
-							</g>
-
-						</svg>
-
-					{% endthumbnail %}
-				</div>
-            <br>
-        	<p class="btn btn-default btn-sm" @click="toggleZoomView" style="padding-top:2px"><i class="fa fa-search-plus showPointer"></i></p>
-            <a class="btn btn-default btn-sm" href="{% url 'item_detail' collection_name image.item.item_guid %}"><i class="fa fa-eye" aria-hidden="true"></i> Voir l'objet de cette annotation</a>
-            <a class="btn btn-default btn-sm" href="{% url 'image_detail' collection_name image_guid %}"><i class="fa fa-picture-o" aria-hidden="true"></i> Voir les annotations sur l'image</a>
-
-            </div>
-        <div v-show="!showZoom" id="detail-annotation" class='col-md-6' style="">
-            <h4>Annotation créée par <a href="{% url 'user_home' annotation.author.id %}">{{ annotation.author.username }}</a></h4>
-  		    <p><strong>Titre :</strong> {{ annotation.current_revision.title }}</p>
-  		    <p><strong>Description :</strong> {{ annotation.current_revision.description }}</p>
-            {% if tags_data != "[]" %}
-              <p><strong>Mot-clés :</strong></p>
-              <typeahead :read-only="1" :tags="{{ tags_data }}"></typeahead>
-              <br>
-            {% endif %}
-            {% if user.is_authenticated %}
-  		      <a href="{% url 'annotation_edit' collection_name image_guid annotation_guid  %}" class="btn btn-default btn-sm">
-                {% if user == annotation.author %}
-                  <span class="glyphicon glyphicon-edit"></span> Modifier l'annotation
-                {% else %}
-                  <span class="glyphicon glyphicon-share"></span> Proposer une révision
-                {% endif %}
-              </a>
-              <br>
-	        {% endif %}
-            <br>
-            {% include "partials/annotation_stats_panel.html" with annotation=annotation label="Statistiques sur cette annotation:" %}
-        </div>
-        <!-- zoomView -->
-        <div class="col-md-12 zoom-view" style="display:none" v-show="showZoom">
-          <div class="col-md-12 no-padding"><p @click="toggleZoomView" class="btn btn-link pull-right"><i class="fa fa-close"></i></i> Fermer</p></div>
-          <div class="col-md-2">
-            {% thumbnail annotation.image.media "100x100" crop=False as im %}
-            <zoomview ref="zoomview" main-image-id="main-image" zoomtarget="zoomTarget" :image-url="'{{ im.url }}'" :image-width="{{ im.width }}" :image-height="{{ im.height }}"></zoomview>
-            {% endthumbnail %}
-          </div>
-          <div class="col-md-10 zoomTarget-wrapper">
-            {% with image.media as im %}
-              <svg id="zoomTarget" ref="zoomTarget" width="920" height="920">
-                <image id="main-image" class="main-image" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{{ im.url }}" x="0" y="0" width="{{ im.width }}" height="{{ im.height }}"></image>
-                <g transform="matrix({% transform_matrix im_width=im.width im_height=im.height max_x=100 max_y=100 %})">
-                  <path d="{{ annotation.current_revision.fragment|clean_path }}" opacity="0.7" fill="orange"></path>
-                </g>
-              </svg>
-            {% endwith %}
-            </div>
-          </div>
-
-		</div>
-        <div class='col-md-12'>
-          <h4 id="annotation-comments-header">Commentaires</h4>
-          <ul class="list-group annotation-comments" id="comments">
-            {% for comment in comments %}
-              <li class="list-group-item {% if comment.id in notifications_comments_ids %}list-group-item-warning{% endif %}" id="c{{ comment.id }}" style="margin-left:calc({{ comment.level }}*5px);">
-                <p id="c{{comment.id}}-content" class="comment-content">{{ comment.comment }}</p>
-                <hr class="comment-separator">
-                {% if comment.allow_thread and user.is_authenticated %}<div class="pull-right"><a class="btn btn-default btn-xs reply-to-btn" id="reply-to-{{comment.id}}" class="comment-reply-link">Répondre</a></div>{% endif %}
-                <div id="c{{comment.id}}-subtext" class="comment-subtext">{{ comment.submit_date|date:"d/m/Y" }} à {{ comment.submit_date|time:"H:i" }}&nbsp;-&nbsp; <b><a href="{% url 'user_home' comment.user.id %}">{{comment.name}}</a></b></div>
-                <div id="c{{comment.id}}-label-list" class="comment-metacategories">
-                {% if comment.revision or comment.metacategories.count > 0 %}&nbsp;-&nbsp;{% endif %}
-                {% if comment.revision %}
-                  <a href="{% url 'revision_detail' collection_name image_guid annotation_guid comment.revision.revision_guid %}">
-                    <span class="label
-                    {% if comment.revision.author == annotation.author %}
-                      label-success
-                    {% else %}
-                      {% if comment.revision.state == 0 %}
-                        label-warning
-                      {% elif comment.revision.state == 1 %}
-                        label-success
-                      {% elif comment.revision.state == 2 %}
-                        label-danger
-                      {% elif comment.revision.state == 3 %}
-                        label-primary
-                      {% endif %}
-                    {% endif %} revision-link">
-                    {% if comment.revision.author == annotation.author %}
-                      Voir révision {% if comment.revision.merge_parent_revision %}(fusion){% endif %}
-                    {% else %}
-                      Voir proposition
-                      {% if comment.revision.state == 0 %}
-                        (en attente)
-                      {% elif comment.revision.state == 1 %}
-                        (validée)
-                      {% elif comment.revision.state == 2 %}
-                        (rejetée)
-                      {% elif comment.revision.state == 3 %}
-                        (étudiée)
-                      {% endif %}
-                    {% endif %}</span>
-                  </a>
-                {% endif %}
-                {% for metacategory in comment.metacategories.all %}
-                  <span class="label label-info">{{metacategory.label}}</span>
-                {% endfor %}
-                </div>
-                </li>
-            {% endfor %}
-          </ul>
-          <ul class="pagination pull-right" style="margin-left: 15px;">
-            <li class="active pagination-label"><a>Commentaires par page : </a></li>
-            <li class="{% if comments.paginator.per_page == 5 %}active{% endif %}">
-              <a {% if comments.paginator.per_page != 5 %}href="{% url 'annotation_detail' collection_name image_guid annotation_guid %}?page=1&perpage=5"{% endif %}>5</a>
-            </li>
-            <li class="{% if comments.paginator.per_page == 10 %}active{% endif %}">
-              <a {% if comments.paginator.per_page != 10 %}href="{% url 'annotation_detail' collection_name image_guid annotation_guid %}?page=1&perpage=10"{% endif %}>10</a>
-            </li>
-            <li class="{% if comments.paginator.per_page == 25 %}active{% endif %}">
-              <a {% if comments.paginator.per_page != 25 %}href="{% url 'annotation_detail' collection_name image_guid annotation_guid %}?page=1&perpage=25"{% endif %}>25</a>
-            </li>
-            <li class="{% if comments.paginator.per_page == 100 %}active{% endif %}">
-              <a {% if comments.paginator.per_page != 100 %}href="{% url 'annotation_detail' collection_name image_guid annotation_guid %}?page=1&perpage=100"{% endif %}>100</a>
-            </li>
-          </ul>
-          &nbsp;
-          {% if comments.has_previous or comments.has_next %}
-            <ul class="pagination pull-right">
-              {% if comments.has_previous %}
-              <li>
-                <a href="{% url 'annotation_detail' collection_name image_guid annotation_guid %}?page={{comments.previous_page_number}}&perpage={{comments.paginator.per_page}}" aria-label="Précédent">
-                  <span aria-hidden="true">&laquo;</span>
-                </a>
-              </li>
-              {% endif %}
-
-              {% for page in comments.paginator.page_range %}
-                <li id="page-link-{{page}}" class="pagination-link {% if page == comments.number %}active{% endif %}">
-                  <a {% if page != comments.number %}href="{% url 'annotation_detail' collection_name image_guid annotation_guid %}?page={{page}}&perpage={{comments.paginator.per_page}}"{% endif %}>{{page}}</a>
-                </li>
-
-              {% endfor %}
-
-              {% if comments.has_next %}
-              <li>
-                <a href="{% url 'annotation_detail' collection_name image_guid annotation_guid %}?page={{comments.next_page_number}}&perpage={{comments.paginator.per_page}}" aria-label="Suivant">
-                  <span aria-hidden="true">&raquo;</span>
-                </a>
-              </li>
-              {% endif %}
-            </ul>
-          {% endif %}
-          </div>
-
-
-        <div class='col-md-12'>
-        {% if user.is_authenticated %}
-        {% if not comment_form %}
-          {% get_comment_form for annotation as comment_form %}
-        {% endif %}
-        <form class="form" action="{% url 'post_comment' %}" method="POST">
-          {% csrf_token %}
-          {{ comment_form.content_type }}
-          {{ comment_form.object_pk }}
-          {{ comment_form.timestamp }}
-          {{ comment_form.security_hash }}
-          <input id="reply-to-field" type="hidden" name="reply_to" value="0"></input>
-          <input type="hidden" name="next" value="{% url 'annotation_detail' collection_name image_guid annotation_guid %}">
-          <h4> Commenter ({{user.username}}) </h4>
-          <div class="reply-to-container">
-            <label id="form-reply-to-label" class="control-label reply-to-label" for="id_{{ comment_form.comment.name }}">En réponse à : </label>
-            <div class="alert alert-info reply-to-alert">
-               <p class="reply-to-content comment-content"></p>
-               <hr class="comment-separator">
-               <div class="reply-to-subtext comment-subtext"></div>
-               <div class="reply-to-label-list comment-metacategories"></div>
-            </div>
-            <a class="btn btn-default btn-sm reply-to-cancel">Annuler et répondre sur le fil principal</a><br><br>
-          </div>
-          <fieldset class="form-group {% if comment_form.comment.errors %}has-error{% endif %}">
-            {% if comment_form.comment.errors %}
-              <div class="alert alert-danger" role="alert">
-                <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
-                <span class="sr-only">Erreur :</span>
-                  {{ comment_form.comment.errors | striptags }}
-              </div>
-            {% endif %}
-            <textarea class="form-control"
-              name="{{ comment_form.comment.name }}"
-              id="id_{{ comment_form.comment.name }}" ></textarea>
-          </fieldset>
-          <fieldset class="form-group {% if comment_form.metacategories.errors %}has-error{% endif %}">
-            <div class="input-group">
-              {% for metacategory in comment_form.metacategories %}
-                <label class="checkbox-inline" for="{{metacategory.id_for_label}}">
-                    {{ metacategory.tag }} {{metacategory.choice_label}}
-                </label>
-              {% endfor %}
-            </div>
-          </fieldset>
-          <p class="submit">
-            <input class="btn btn-default" type="submit" name="post" class="submit-post" value="Publier"/>
-          </p>
-        </form>
-        {% endif %}
-
-	</div>
-{% endblock %}
-
-{% block footer_js %}
-  <script>
-    new Vue({
-      el: "#detail-annotation",
-
-      data: {
-        showZoom: false
-      },
-
-      methods: {
-
-        toggleZoomView: function () {
-          if (this.showZoom) {
-            this.showZoom = false;
-          }
-
-          else {
-            this.showZoom = true;
-          }
-        },
-
-
-      },
-
-      components: {
-        'Typeahead': iconolab.VueComponents.Typeahead,
-        'Zoomview': iconolab.VueComponents.Zoomview
-      }
-    });
-
-    $(".reply-to-container").hide()
-    $(".reply-to-btn").click(function(event){
-        $(".reply-to-container").hide()
-        var commentID = /reply\-to\-([0-9]+)/.exec($(this).attr("id"))[1];
-        var comment_content = $("#c"+commentID+"-content").html();
-        var comment_subtext = $("#c"+commentID+"-subtext").html();
-        var comment_labels = $("#c"+commentID+"-label-list").html();
-        $(".reply-to-alert .reply-to-content").html(comment_content);
-        $(".reply-to-alert .reply-to-subtext").html(comment_subtext);
-        $(".reply-to-alert .reply-to-label-list").html(comment_labels);
-        $(".reply-to-container").slideDown(complete=function(){
-			$("#id_comment").get(0).scrollIntoView();
-        });
-        $("#reply-to-field").val(commentID);
-    })
-    $(".reply-to-cancel").click(function(event){
-      $(".reply-to-container").hide()
-      $(".reply-to-alert .reply-to-content").html("");
-      $(".reply-to-alert .reply-to-subtext").html("");
-      $(".reply-to-alert .reply-to-label-list").html("");
-      $("#reply-to-field").val("0");
-    })
-  </script>
-{% endblock %}
+{% extends 'iconolab_base.html' %}
+
+{% load i18n %}
+{% load staticfiles %}
+{% load comments %}
+{% load comments_xtd %}
+{% load thumbnail %}
+{% load iconolab_tags %}
+
+{% block content %}
+	<div id="annotation-wrapper" class="row" style="border: 1px solid gray;padding-top: 10px;">
+
+		<div id="detail-annotation" class="col-md-12">
+			<div v-show="!showZoom" class="col-md-6">
+				<div class="small-image-wrapper" style="position: relative">
+					{% thumbnail annotation.image.media "300x300" crop=False as im %}
+						<img src="{{ im.url }}" width="{{ im.width }}" height="{{ im.height }}" />
+
+						<svg width="{{ im.width }}" height="{{ im.height }}" version="1.1" style="position:absolute; top:0px; left: 0px">
+
+							<g transform="matrix({% transform_matrix im_width=im.width im_height=im.height max_x=100 max_y=100 %})">
+								<path d="{{ annotation.current_revision.fragment|clean_path }}" opacity="0.7" fill="orange"></path>
+							</g>
+
+						</svg>
+
+					{% endthumbnail %}
+				</div>
+            <br>
+        	<p class="btn btn-default btn-sm" @click="toggleZoomView" style="padding-top:2px"><i class="fa fa-search-plus showPointer"></i></p>
+            <a class="btn btn-default btn-sm" href="{% url 'item_detail' collection_name image.item.item_guid %}"><i class="fa fa-eye" aria-hidden="true"></i> Voir l'objet de cette annotation</a>
+            <a class="btn btn-default btn-sm" href="{% url 'image_detail' collection_name image_guid %}"><i class="fa fa-picture-o" aria-hidden="true"></i> Voir les annotations sur l'image</a>
+
+            </div>
+        <div v-show="!showZoom" id="detail-annotation" class='col-md-6' style="">
+            <h4>Annotation créée par <a href="{% url 'user_home' annotation.author.id %}">{{ annotation.author.username }}</a></h4>
+  		    <p><strong>Titre :</strong> {{ annotation.current_revision.title }}</p>
+  		    <p><strong>Description :</strong> {{ annotation.current_revision.description }}</p>
+            {% if tags_data != "[]" %}
+              <p><strong>Mot-clés :</strong></p>
+              <typeahead :read-only="1" :tags="{{ tags_data }}"></typeahead>
+              <br>
+            {% endif %}
+            {% if user.is_authenticated %}
+  		      <a href="{% url 'annotation_edit' collection_name image_guid annotation_guid  %}" class="btn btn-default btn-sm">
+                {% if user == annotation.author %}
+                  <span class="glyphicon glyphicon-edit"></span> Modifier l'annotation
+                {% else %}
+                  <span class="glyphicon glyphicon-share"></span> Proposer une révision
+                {% endif %}
+              </a>
+              <br>
+	        {% endif %}
+            <br>
+            {% include "partials/annotation_stats_panel.html" with annotation=annotation label="Statistiques sur cette annotation:" %}
+        </div>
+        <!-- zoomView -->
+        <div class="col-md-12 zoom-view" style="display:none" v-show="showZoom">
+          <div class="col-md-12 no-padding"><p @click="toggleZoomView" class="btn btn-link pull-right"><i class="fa fa-close"></i></i> Fermer</p></div>
+          <div class="col-md-2">
+            {% thumbnail annotation.image.media "100x100" crop=False as im %}
+            <zoomview ref="zoomview" main-image-id="main-image" zoomtarget="zoomTarget" :image-url="'{{ im.url }}'" :image-width="{{ im.width }}" :image-height="{{ im.height }}"></zoomview>
+            {% endthumbnail %}
+          </div>
+          <div class="col-md-10 zoomTarget-wrapper">
+            {% with image.media as im %}
+              <svg id="zoomTarget" ref="zoomTarget" width="920" height="920">
+                <image id="main-image" class="main-image" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{{ im.url }}" x="0" y="0" width="{{ im.width }}" height="{{ im.height }}"></image>
+                <g transform="matrix({% transform_matrix im_width=im.width im_height=im.height max_x=100 max_y=100 %})">
+                  <path d="{{ annotation.current_revision.fragment|clean_path }}" opacity="0.7" fill="orange"></path>
+                </g>
+              </svg>
+            {% endwith %}
+            </div>
+          </div>
+
+		</div>
+        <div class='col-md-12'>
+          <h4 id="annotation-comments-header">Commentaires</h4>
+          <ul class="list-group annotation-comments" id="comments">
+            {% for comment in comments %}
+              <li class="list-group-item {% if comment.id in notifications_comments_ids %}list-group-item-warning{% endif %}" id="c{{ comment.id }}" style="margin-left:calc({{ comment.level }}*5px);">
+                <p id="c{{comment.id}}-content" class="comment-content">{{ comment.comment }}</p>
+                <hr class="comment-separator">
+                {% if comment.allow_thread and user.is_authenticated %}<div class="pull-right"><a class="btn btn-default btn-xs reply-to-btn" id="reply-to-{{comment.id}}" class="comment-reply-link">Répondre</a></div>{% endif %}
+                <div id="c{{comment.id}}-subtext" class="comment-subtext">{{ comment.submit_date|date:"d/m/Y" }} à {{ comment.submit_date|time:"H:i" }}&nbsp;-&nbsp; <b><a href="{% url 'user_home' comment.user.id %}">{{comment.name}}</a></b></div>
+                <div id="c{{comment.id}}-label-list" class="comment-metacategories">
+                {% if comment.revision or comment.metacategories.count > 0 %}&nbsp;-&nbsp;{% endif %}
+                {% if comment.revision %}
+                  <a href="{% url 'revision_detail' collection_name image_guid annotation_guid comment.revision.revision_guid %}">
+                    <span class="label
+                    {% if comment.revision.author == annotation.author %}
+                      label-success
+                    {% else %}
+                      {% if comment.revision.state == 0 %}
+                        label-warning
+                      {% elif comment.revision.state == 1 %}
+                        label-success
+                      {% elif comment.revision.state == 2 %}
+                        label-danger
+                      {% elif comment.revision.state == 3 %}
+                        label-primary
+                      {% endif %}
+                    {% endif %} revision-link">
+                    {% if comment.revision.author == annotation.author %}
+                      Voir révision {% if comment.revision.merge_parent_revision %}(fusion){% endif %}
+                    {% else %}
+                      Voir proposition
+                      {% if comment.revision.state == 0 %}
+                        (en attente)
+                      {% elif comment.revision.state == 1 %}
+                        (validée)
+                      {% elif comment.revision.state == 2 %}
+                        (rejetée)
+                      {% elif comment.revision.state == 3 %}
+                        (étudiée)
+                      {% endif %}
+                    {% endif %}</span>
+                  </a>
+                {% endif %}
+                {% for metacategory in comment.metacategories.all %}
+                  <span class="label label-info">{{metacategory.label}}</span>
+                {% endfor %}
+                </div>
+                </li>
+            {% endfor %}
+          </ul>
+          <ul class="pagination pull-right" style="margin-left: 15px;">
+            <li class="active pagination-label"><a>Commentaires par page : </a></li>
+            <li class="{% if comments.paginator.per_page == 5 %}active{% endif %}">
+              <a {% if comments.paginator.per_page != 5 %}href="{% url 'annotation_detail' collection_name image_guid annotation_guid %}?page=1&perpage=5"{% endif %}>5</a>
+            </li>
+            <li class="{% if comments.paginator.per_page == 10 %}active{% endif %}">
+              <a {% if comments.paginator.per_page != 10 %}href="{% url 'annotation_detail' collection_name image_guid annotation_guid %}?page=1&perpage=10"{% endif %}>10</a>
+            </li>
+            <li class="{% if comments.paginator.per_page == 25 %}active{% endif %}">
+              <a {% if comments.paginator.per_page != 25 %}href="{% url 'annotation_detail' collection_name image_guid annotation_guid %}?page=1&perpage=25"{% endif %}>25</a>
+            </li>
+            <li class="{% if comments.paginator.per_page == 100 %}active{% endif %}">
+              <a {% if comments.paginator.per_page != 100 %}href="{% url 'annotation_detail' collection_name image_guid annotation_guid %}?page=1&perpage=100"{% endif %}>100</a>
+            </li>
+          </ul>
+          &nbsp;
+          {% if comments.has_previous or comments.has_next %}
+            <ul class="pagination pull-right">
+              {% if comments.has_previous %}
+              <li>
+                <a href="{% url 'annotation_detail' collection_name image_guid annotation_guid %}?page={{comments.previous_page_number}}&perpage={{comments.paginator.per_page}}" aria-label="Précédent">
+                  <span aria-hidden="true">&laquo;</span>
+                </a>
+              </li>
+              {% endif %}
+
+              {% for page in comments.paginator.page_range %}
+                <li id="page-link-{{page}}" class="pagination-link {% if page == comments.number %}active{% endif %}">
+                  <a {% if page != comments.number %}href="{% url 'annotation_detail' collection_name image_guid annotation_guid %}?page={{page}}&perpage={{comments.paginator.per_page}}"{% endif %}>{{page}}</a>
+                </li>
+
+              {% endfor %}
+
+              {% if comments.has_next %}
+              <li>
+                <a href="{% url 'annotation_detail' collection_name image_guid annotation_guid %}?page={{comments.next_page_number}}&perpage={{comments.paginator.per_page}}" aria-label="Suivant">
+                  <span aria-hidden="true">&raquo;</span>
+                </a>
+              </li>
+              {% endif %}
+            </ul>
+          {% endif %}
+          </div>
+
+
+        <div class='col-md-12'>
+        {% if user.is_authenticated %}
+        {% if not comment_form %}
+          {% get_comment_form for annotation as comment_form %}
+        {% endif %}
+        <form class="form" action="{% url 'post_comment' %}" method="POST">
+          {% csrf_token %}
+          {{ comment_form.content_type }}
+          {{ comment_form.object_pk }}
+          {{ comment_form.timestamp }}
+          {{ comment_form.security_hash }}
+          <input id="reply-to-field" type="hidden" name="reply_to" value="0"></input>
+          <input type="hidden" name="next" value="{% url 'annotation_detail' collection_name image_guid annotation_guid %}">
+          <h4> Commenter ({{user.username}}) </h4>
+          <div class="reply-to-container">
+            <label id="form-reply-to-label" class="control-label reply-to-label" for="id_{{ comment_form.comment.name }}">En réponse à : </label>
+            <div class="alert alert-info reply-to-alert">
+               <p class="reply-to-content comment-content"></p>
+               <hr class="comment-separator">
+               <div class="reply-to-subtext comment-subtext"></div>
+               <div class="reply-to-label-list comment-metacategories"></div>
+            </div>
+            <a class="btn btn-default btn-sm reply-to-cancel">Annuler et répondre sur le fil principal</a><br><br>
+          </div>
+          <fieldset class="form-group {% if comment_form.comment.errors %}has-error{% endif %}">
+            {% if comment_form.comment.errors %}
+              <div class="alert alert-danger" role="alert">
+                <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
+                <span class="sr-only">Erreur :</span>
+                  {{ comment_form.comment.errors | striptags }}
+              </div>
+            {% endif %}
+            <textarea class="form-control"
+              name="{{ comment_form.comment.name }}"
+              id="id_{{ comment_form.comment.name }}" ></textarea>
+          </fieldset>
+          <fieldset class="form-group {% if comment_form.metacategories.errors %}has-error{% endif %}">
+            <div class="input-group">
+              {% for metacategory in comment_form.metacategories %}
+                <label class="checkbox-inline" for="{{metacategory.id_for_label}}">
+                    {{ metacategory.tag }} {{metacategory.choice_label}}
+                </label>
+              {% endfor %}
+            </div>
+          </fieldset>
+          <p class="submit">
+            <input class="btn btn-default" type="submit" name="post" class="submit-post" value="Publier"/>
+          </p>
+        </form>
+        {% endif %}
+
+	</div>
+{% endblock %}
+
+{% block footer_js %}
+  <script>
+    new Vue({
+      el: "#detail-annotation",
+
+      data: {
+        showZoom: false
+      },
+
+      methods: {
+
+        toggleZoomView: function () {
+          if (this.showZoom) {
+            this.showZoom = false;
+          }
+
+          else {
+            this.showZoom = true;
+          }
+        },
+
+
+      },
+
+      components: {
+        'Typeahead': iconolab.VueComponents.Typeahead,
+        'Zoomview': iconolab.VueComponents.Zoomview
+      }
+    });
+
+    $(".reply-to-container").hide()
+    $(".reply-to-btn").click(function(event){
+        $(".reply-to-container").hide()
+        var commentID = /reply\-to\-([0-9]+)/.exec($(this).attr("id"))[1];
+        var comment_content = $("#c"+commentID+"-content").html();
+        var comment_subtext = $("#c"+commentID+"-subtext").html();
+        var comment_labels = $("#c"+commentID+"-label-list").html();
+        $(".reply-to-alert .reply-to-content").html(comment_content);
+        $(".reply-to-alert .reply-to-subtext").html(comment_subtext);
+        $(".reply-to-alert .reply-to-label-list").html(comment_labels);
+        $(".reply-to-container").slideDown(complete=function(){
+			$("#id_comment").get(0).scrollIntoView();
+        });
+        $("#reply-to-field").val(commentID);
+    })
+    $(".reply-to-cancel").click(function(event){
+      $(".reply-to-container").hide()
+      $(".reply-to-alert .reply-to-content").html("");
+      $(".reply-to-alert .reply-to-subtext").html("");
+      $(".reply-to-alert .reply-to-label-list").html("");
+      $("#reply-to-field").val("0");
+    })
+  </script>
+{% endblock %}
--- a/src/iconolab/templates/iconolab_base.html	Wed Jan 18 16:50:59 2017 +0100
+++ b/src/iconolab/templates/iconolab_base.html	Wed Jan 18 16:53:46 2017 +0100
@@ -1,47 +1,47 @@
-{% load staticfiles %}
-
-{% load notifications_tags %}
-{% load iconolab_tags %}
-
-<!DOCTYPE html>
-<html>
-
-{% block head %}
-	<head>
-		<title>{% block title %} {% endblock %}</title>
-		
-		{% block main_js %}
-			{% if IS_JS_DEV_MODE %}
-				<script src="{% static 'iconolab-bundle/dist/iconolab.js' %}" type="text/javascript"></script>
-			{% else %}
-				<script src="{% static 'iconolab/js/iconolab.js' %}" type="text/javascript"></script>
-			{% endif %}
-			
-		{% endblock %}
-		{% block page_js %} {% endblock %}
-
-		{% block main_css %}		
-			<link rel="stylesheet" href="{% static 'iconolab/css/bootstrap/css/bootstrap.min.css' %}">
-			<link rel="stylesheet" href="{% static 'iconolab/css/font-awesome/css/font-awesome.min.css' %}">
-			<link rel="stylesheet" href="{% static 'iconolab/css/iconolab.css' %}">
-		{% endblock %}
-		
-		{% block page_css %} {% endblock %}
-
-	</head>
-{% endblock %}
-
-	<body>
-		<!-- navigation -->
-	    <div class="container">
-	    	{% include "partials/header.html"%}
-			{% block content %} {% endblock %}
-	    </div>
-	    {% block footer_js %}
-
-	    {% endblock %}
-	</body>
-    <footer>
-        {% include "partials/footer.html" %}
-    </footer>
+{% load staticfiles %}
+
+{% load notifications_tags %}
+{% load iconolab_tags %}
+
+<!DOCTYPE html>
+<html>
+
+{% block head %}
+	<head>
+		<title>{% block title %} {% endblock %}</title>
+		
+		{% block main_js %}
+			{% if IS_JS_DEV_MODE %}
+				<script src="{% static 'iconolab-bundle/dist/iconolab.js' %}" type="text/javascript"></script>
+			{% else %}
+				<script src="{% static 'iconolab/js/iconolab.js' %}" type="text/javascript"></script>
+			{% endif %}
+			
+		{% endblock %}
+		{% block page_js %} {% endblock %}
+
+		{% block main_css %}		
+			<link rel="stylesheet" href="{% static 'iconolab/css/bootstrap/css/bootstrap.min.css' %}">
+			<link rel="stylesheet" href="{% static 'iconolab/css/font-awesome/css/font-awesome.min.css' %}">
+			<link rel="stylesheet" href="{% static 'iconolab/css/iconolab.css' %}">
+		{% endblock %}
+		
+		{% block page_css %} {% endblock %}
+
+	</head>
+{% endblock %}
+
+	<body>
+		<!-- navigation -->
+	    <div class="container">
+	    	{% include "partials/header.html"%}
+			{% block content %} {% endblock %}
+	    </div>
+	    {% block footer_js %}
+
+	    {% endblock %}
+	</body>
+    <footer>
+        {% include "partials/footer.html" %}
+    </footer>
 </html>
\ No newline at end of file
--- a/src/iconolab/templates/partials/header.html	Wed Jan 18 16:50:59 2017 +0100
+++ b/src/iconolab/templates/partials/header.html	Wed Jan 18 16:53:46 2017 +0100
@@ -1,44 +1,44 @@
-{% load notifications_tags %}
-{% load staticfiles %}
-
-<div class="navbar-container">
-  {% if homepage %}
-    <a class="homepage-logo" href="{% url 'home' %}">
-      <img src="{% static 'iconolab/img/iconolab.png' %}" width="306" height="67"> 
-    </a>
-  {% endif %}
-  <nav class="navbar navbar-default {% if homepage %}navbar-homepage{% endif %}" {% if collection %} style="margin-bottom: 5px;" {% endif %}>
-    <div class="container-fluid">
-      <div class="navbar-header">
-          {% if not homepage %}
-            <a class="navbar-brand" href="{% url 'home' %}">
-              Iconolab
-            </a>
-          {% endif %}
-      </div>
-      <div id="navbar" class="navbar-collapse collapse">
-        <ul class="nav navbar-nav">
-          <li><a href="{% url 'iconolab_help' %}">Le projet</a></li>
-          {% if collection_name %}<li><a href="{% url 'collection_home' collection_name %}">Contribuer</a></li>{% endif %}
-        </ul>
-        
-        {% include "partials/header_search_form.html"%}
-        
-        <ul class="nav navbar-nav navbar-right">
-          {% if request.user.is_authenticated %}
-            {% notifications_unread as unread_count %}
-            <li><a href="{% url 'user_home' request.user.id %}" title="{{request.user.username}}: Mon espace - {{ unread_count }} notification(s)">
-              Mon espace <span class="badge {% if unread_count %}badge-warning{% endif %}">
-              {{ unread_count }} <i class="fa fa-envelope-o" aria-hidden="true"></i> </span>
-            </a></li>
-            <li><a href="{% url 'account:logout' %}">Se déconnecter</a></li>
-          {% else %}
-            <li><a href="{% url 'account:register' %}">Créer un compte</a></li>
-            <li><a href="{% url 'account:login' %}?next={{ request.path | urlencode }}">Se Connecter</a></li>
-          {% endif %}
-        </ul>
-      </div><!--/.nav-collapse -->
-    </div><!--/.container-fluid -->
-  </nav>
-</div>
-{% include "partials/header_breadcrumbs.html" %}
+{% load notifications_tags %}
+{% load staticfiles %}
+
+<div class="navbar-container">
+  {% if homepage %}
+    <a class="homepage-logo" href="{% url 'home' %}">
+      <img src="{% static 'iconolab/img/iconolab.png' %}" width="306" height="67"> 
+    </a>
+  {% endif %}
+  <nav class="navbar navbar-default {% if homepage %}navbar-homepage{% endif %}" {% if collection %} style="margin-bottom: 5px;" {% endif %}>
+    <div class="container-fluid">
+      <div class="navbar-header">
+          {% if not homepage %}
+            <a class="navbar-brand" href="{% url 'home' %}">
+              Iconolab
+            </a>
+          {% endif %}
+      </div>
+      <div id="navbar" class="navbar-collapse collapse">
+        <ul class="nav navbar-nav">
+          <li><a href="{% url 'iconolab_help' %}">Le projet</a></li>
+          {% if collection_name %}<li><a href="{% url 'collection_home' collection_name %}">Contribuer</a></li>{% endif %}
+        </ul>
+        
+        {% include "partials/header_search_form.html"%}
+        
+        <ul class="nav navbar-nav navbar-right">
+          {% if request.user.is_authenticated %}
+            {% notifications_unread as unread_count %}
+            <li><a href="{% url 'user_home' request.user.id %}" title="{{request.user.username}}: Mon espace - {{ unread_count }} notification(s)">
+              Mon espace <span class="badge {% if unread_count %}badge-warning{% endif %}">
+              {{ unread_count }} <i class="fa fa-envelope-o" aria-hidden="true"></i> </span>
+            </a></li>
+            <li><a href="{% url 'account:logout' %}">Se déconnecter</a></li>
+          {% else %}
+            <li><a href="{% url 'account:register' %}">Créer un compte</a></li>
+            <li><a href="{% url 'account:login' %}?next={{ request.path | urlencode }}">Se Connecter</a></li>
+          {% endif %}
+        </ul>
+      </div><!--/.nav-collapse -->
+    </div><!--/.container-fluid -->
+  </nav>
+</div>
+{% include "partials/header_breadcrumbs.html" %}
--- a/src/iconolab/templates/registration/login.html	Wed Jan 18 16:50:59 2017 +0100
+++ b/src/iconolab/templates/registration/login.html	Wed Jan 18 16:53:46 2017 +0100
@@ -1,27 +1,27 @@
-{% extends "iconolab_base.html" %}
-
-{% block content %}
-
-{% if form.errors %}
-<p>Combinaison identifiant/mot de passe incorrecte. Veuillez réessayer.</p>
-{% endif %}
-
-<div class='col-md-6 center centerer'>
-    <h3>Se connecter</h3>
-    <form class="form" action="{% url 'account:login' %}" method="POST">
-      {% csrf_token %}
-      {% for field in form %}
-      <fieldset class="form-group">
-        <label class="control-label" for="id_{{ field.name }}">{{ field.label }}</label>
-        <input type="{% if field.name == "username" %}text{% else %}password{% endif %}" class="form-control"
-          name="{{ field.name }}"
-          id="id_{{ field.name }}" > 
-        {% if field.errors %}{{ field.errors }}{% endif %}
-      </fieldset>
-      {% endfor %}
-      <input type="hidden" name="next" value="{{next}}">
-      <input type="submit" value="S'identifier"  class="btn btn-primary">
-    </form>
-</div>
-
+{% extends "iconolab_base.html" %}
+
+{% block content %}
+
+{% if form.errors %}
+<p>Combinaison identifiant/mot de passe incorrecte. Veuillez réessayer.</p>
+{% endif %}
+
+<div class='col-md-6 center centerer'>
+    <h3>Se connecter</h3>
+    <form class="form" action="{% url 'account:login' %}" method="POST">
+      {% csrf_token %}
+      {% for field in form %}
+      <fieldset class="form-group">
+        <label class="control-label" for="id_{{ field.name }}">{{ field.label }}</label>
+        <input type="{% if field.name == "username" %}text{% else %}password{% endif %}" class="form-control"
+          name="{{ field.name }}"
+          id="id_{{ field.name }}" > 
+        {% if field.errors %}{{ field.errors }}{% endif %}
+      </fieldset>
+      {% endfor %}
+      <input type="hidden" name="next" value="{{next}}">
+      <input type="submit" value="S'identifier"  class="btn btn-primary">
+    </form>
+</div>
+
 {% endblock %}
\ No newline at end of file
--- a/src/iconolab/templatetags/iconolab_tags.py	Wed Jan 18 16:50:59 2017 +0100
+++ b/src/iconolab/templatetags/iconolab_tags.py	Wed Jan 18 16:53:46 2017 +0100
@@ -1,56 +1,56 @@
-from django.template import Library
-import sys
-from iconolab import __version__
-
-register = Library()
-
-# {% transform_matrix 230 200 100 100 %}
-@register.simple_tag
-def transform_matrix(im_width=0, im_height=0, max_x=0, max_y=0):
-	try :
-		x_ratio = im_width / max_x
-		y_ratio = im_height / max_y
-		value_list = [x_ratio, 0, 0, y_ratio, 0, 0]
-		matrix = ",".join([str(v) for v in value_list])
-	except:
-		matrix = ""
-
-	return matrix
-
-@register.filter
-def clean_path(path):
-	result = ""
-	if not len(path):
-		return result
-	else:
-		path_infos = path.split(";")
-		if len(path_infos) > 0 :
-			result = path_infos[0] 
-	return result
-
-
-@register.filter
-def path_type(path):
-	result = ""
-	if not len(path):
-		return result
-	else:
-		path_infos = path.split(";")
-		if len(path_infos) > 1 :
-			result = path_infos[1]
-
-	return result
-
-@register.simple_tag
-def version():
-	return __version__
-
-@register.filter
-def get_item(dictionary, key):
-    return dictionary.get(key)
-   
-   
-@register.filter
-def addstr(arg1, arg2):
-    """concatenate arg1 & arg2"""
+from django.template import Library
+import sys
+from iconolab import __version__
+
+register = Library()
+
+# {% transform_matrix 230 200 100 100 %}
+@register.simple_tag
+def transform_matrix(im_width=0, im_height=0, max_x=0, max_y=0):
+	try :
+		x_ratio = im_width / max_x
+		y_ratio = im_height / max_y
+		value_list = [x_ratio, 0, 0, y_ratio, 0, 0]
+		matrix = ",".join([str(v) for v in value_list])
+	except:
+		matrix = ""
+
+	return matrix
+
+@register.filter
+def clean_path(path):
+	result = ""
+	if not len(path):
+		return result
+	else:
+		path_infos = path.split(";")
+		if len(path_infos) > 0 :
+			result = path_infos[0] 
+	return result
+
+
+@register.filter
+def path_type(path):
+	result = ""
+	if not len(path):
+		return result
+	else:
+		path_infos = path.split(";")
+		if len(path_infos) > 1 :
+			result = path_infos[1]
+
+	return result
+
+@register.simple_tag
+def version():
+	return __version__
+
+@register.filter
+def get_item(dictionary, key):
+    return dictionary.get(key)
+   
+   
+@register.filter
+def addstr(arg1, arg2):
+    """concatenate arg1 & arg2"""
     return str(arg1) + str(arg2)
\ No newline at end of file
--- a/src/iconolab/utils/context_processors.py	Wed Jan 18 16:50:59 2017 +0100
+++ b/src/iconolab/utils/context_processors.py	Wed Jan 18 16:53:46 2017 +0100
@@ -1,8 +1,8 @@
-from django.conf import settings
-
-def env(request):
-	try:
-		dev_mode = settings.JS_DEV_MODE
-	except AttributeError:
-		dev_mode = False
+from django.conf import settings
+
+def env(request):
+	try:
+		dev_mode = settings.JS_DEV_MODE
+	except AttributeError:
+		dev_mode = False
 	return {'IS_JS_DEV_MODE': dev_mode}
\ No newline at end of file
--- a/src/iconolab/utils/utils.py	Wed Jan 18 16:50:59 2017 +0100
+++ b/src/iconolab/utils/utils.py	Wed Jan 18 16:53:46 2017 +0100
@@ -1,35 +1,35 @@
-from django.db.models.signals import post_save
-from notifications.signals import notify
-
-class NotificationManager:
-
-	NEW_ANNOTATION = 'Nouvelle annotation' 
-	NEW_COMMENT = 'Nouveau commentaire'
-
-	class Notification:
-
-		def __init__(self, sender=None, verb=None):
-			self.sender = sender
-			self.verb = verb
-			self.recipient = None
-			self.target = None
-			self.description = None
-
-		def set_recipient(self, recipient):
-			self.recipient = recipient
-			
-		def set_target(self, target):
-			self.target = target
-
-		def set_description(self, description):
-			self.description = description
-
-	@classmethod
-	def create_notification(self, sender=None, verb=None):
-		annotation = NotificationManager.Notification(sender, verb=verb)
-		return annotation
-
-	@classmethod
-	def notify(self, notification=None):
-		#send to all users or a Group
+from django.db.models.signals import post_save
+from notifications.signals import notify
+
+class NotificationManager:
+
+	NEW_ANNOTATION = 'Nouvelle annotation' 
+	NEW_COMMENT = 'Nouveau commentaire'
+
+	class Notification:
+
+		def __init__(self, sender=None, verb=None):
+			self.sender = sender
+			self.verb = verb
+			self.recipient = None
+			self.target = None
+			self.description = None
+
+		def set_recipient(self, recipient):
+			self.recipient = recipient
+			
+		def set_target(self, target):
+			self.target = target
+
+		def set_description(self, description):
+			self.description = description
+
+	@classmethod
+	def create_notification(self, sender=None, verb=None):
+		annotation = NotificationManager.Notification(sender, verb=verb)
+		return annotation
+
+	@classmethod
+	def notify(self, notification=None):
+		#send to all users or a Group
 		notify.send(notification.sender, recipient=notification.sender, verb=notification.verb)
\ No newline at end of file
--- a/src/iconolab/views/objects.py	Wed Jan 18 16:50:59 2017 +0100
+++ b/src/iconolab/views/objects.py	Wed Jan 18 16:53:46 2017 +0100
@@ -1,821 +1,821 @@
-from django.shortcuts import HttpResponse, get_object_or_404, render, redirect
-from django.http import Http404
-from django.db.models import Count
-from django.contrib.auth.decorators import login_required
-from django.contrib.auth.models import User
-from django.views.generic import View, DetailView, RedirectView, TemplateView
-from django.views.generic.base import ContextMixin
-from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
-from django.core.urlresolvers import reverse
-from django.core.exceptions import ObjectDoesNotExist
-from django.contrib.contenttypes.models import ContentType
-from django.contrib.sites.models import Site
-from django.conf import settings
-from notifications.models import Notification
-from iconolab.models import Annotation, AnnotationRevision, Collection, Item, Image, IconolabComment, MetaCategory, MetaCategoryInfo
-from iconolab.forms.annotations import AnnotationRevisionForm
-import logging
-
-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()
-        context['homepage'] = True
-        return render(request, 'iconolab/home.html', context)
-
-class TestView(View):
-    template_name = 'iconolab/compare.html'
-
-    def get(self, request, *args, **kwargs):
-        return render(request, self.template_name)
-
-# 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 following this ordering:
-            (collection, item, image, annotation, revision)
-        '''
-
-        objects_tuple = ()
-        if 'collection_name' in kwargs.keys():
-            try:
-                objects_tuple += (Collection.objects.prefetch_related('items', 'items__images').get(name=kwargs.get('collection_name')),)
-            except (ValueError, Collection.DoesNotExist):
-                return False, RedirectView.as_view(url=reverse('404error'))
-        if 'item_guid' in kwargs.keys():
-            try:
-                objects_tuple += (Item.objects.prefetch_related('images', 'metadatas', 'images__stats').get(item_guid=kwargs.get('item_guid')),)
-            except (ValueError, Item.DoesNotExist):
-                return False, RedirectView.as_view(url=reverse('404error'))
-        if 'image_guid' in kwargs.keys():
-            try:
-                objects_tuple += (Image.objects.prefetch_related('annotations', 'item', 'stats').get(image_guid=kwargs.get('image_guid')),)
-            except (ValueError, Image.DoesNotExist):
-                return False, RedirectView.as_view(url=reverse('404error'))
-        if 'annotation_guid' in kwargs.keys():
-            try:
-                objects_tuple += (Annotation.objects.prefetch_related('current_revision', 'stats', 'image').get(annotation_guid=kwargs.get('annotation_guid')),)
-            except (ValueError, Annotation.DoesNotExist):
-                return False, RedirectView.as_view(url=reverse('404error'))
-        if 'revision_guid' in kwargs.keys():
-            try:
-                objects_tuple += (AnnotationRevision.objects.prefetch_related('parent_revision').get(revision_guid=kwargs.get('revision_guid')),)
-            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)
-                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
-        pagination_data["perpage"] = perpage
-        pagination_data["perpage_range"] = perpage_range
-        pagination_data["trailing_qarg"] = trailing_qarg
-        paginator = Paginator(list_to_paginate, perpage)
-        try:
-            pagination_data["list"] = paginator.page(page)
-        except PageNotAnInteger:
-            pagination_data["list"] = paginator.page(1)
-        except EmptyPage:
-            pagination_data["list"] = paginator.page(paginator.num_pages)
-        pagination_data["page_range"] = [
-            n for n in \
-            range(page - adjacent_pages_count, page + adjacent_pages_count + 1) \
-                if n > 0 and n <= paginator.num_pages
-        ]
-        pagination_data["show_first"] = page - adjacent_pages_count > 1
-        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
-
-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
-        else:
-            return result(request)
-        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'))
-        except ValueError:
-            items_page = 1
-        try:
-            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:
-            recent_page = 1
-        try:
-            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:
-            revised_page = 1
-        try:
-            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:
-            contributions_page = 1
-        try:
-            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,
-            adjacent_pages_count,
-            perpage_range=[6, 12, 48, 192],
-            trailing_qarg="&recent_page="+str(recent_page)
-            +"&recent_perpage="+str(recent_per_page)
-            +"&revised_page="+str(revised_page)
-            +"&revised_perpage="+str(revised_per_page)
-            +"&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,
-            adjacent_pages_count,
-            trailing_qarg="&items_page="+str(items_page)
-            +"&items_perpage="+str(items_per_page)
-            +"&revised_page="+str(revised_page)
-            +"&revised_perpage="+str(revised_per_page)
-            +"&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,
-            adjacent_pages_count,
-            trailing_qarg="&items_page="+str(items_page)
-            +"&items_perpage="+str(items_per_page)
-            +"&recent_page="+str(recent_page)
-            +"&recent_perpage="+str(recent_per_page)
-            +"&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,
-            metacategory__triggers_notifications=MetaCategory.CONTRIBUTORS
-        ).order_by('comment__submit_date').values_list('comment__object_pk', flat=True)))
-        collection_annotations = Annotation.objects.filter(id__in=contrib_calls_annotations_ids).all()
-        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,
-            adjacent_pages_count,
-            trailing_qarg="&items_page="+str(items_page)
-            +"&items_perpage="+str(items_per_page)
-            +"&recent_page="+str(recent_page)
-            +"&recent_perpage="+str(recent_per_page)
-            +"&revised_page="+str(revised_page)
-            +"&revised_perpage="+str(revised_per_page)
-        )
-
-        return render(request, 'iconolab/collection_home.html', context)
-
-
-
-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
-        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)]:
-            image_guid_to_display = str(item.images.first().image_guid)
-        context['display_image'] = image_guid_to_display
-        try:
-            displayed_annotations_page = int(request.GET.get('page', '1'))
-        except ValueError:
-            displayed_annotations_page = 1
-        try:
-            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
-        context['item'] = item
-        context['images'] = []
-        for image in item.images.all():
-            if str(image.image_guid) == image_guid_to_display:
-                page = displayed_annotations_page
-                per_page = displayed_annotations_per_page
-            else:
-                page = 1
-                per_page = 10
-            annotations_paginator = Paginator(image.annotations.all(), per_page)
-            try:
-                annotations = annotations_paginator.page(page)
-            except PageNotAnInteger:
-                annotations = annotations_paginator.page(1)
-            except EmptyPage:
-                annotations = annotations_paginator.page(recent_paginator.num_pages)
-            context['images'].append({
-                'obj' : image,
-                'annotations': annotations
-            })
-            image.stats.views_count += 1
-            image.stats.save()
-        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:
-            (collection, image) = result
-        else:
-            return result(request)
-        context = super(ShowImageView, self).get_context_data(**kwargs)
-        context['collection_name'] = self.kwargs.get('collection_name', '')
-        context['image_guid'] = self.kwargs.get('image_guid', '')
-        context['collection'] = collection
-        context['image'] = image
-        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', '')
-        context['image_guid'] = self.kwargs.get('image_guid', '')
-        return context
-
-    def get(self, request, *args, **kwargs):
-        success, result = self.check_kwargs(kwargs)
-        if success:
-            (collection, image,) = result
-        else:
-            return result(request)
-        annotation_form = AnnotationRevisionForm()
-        context = self.get_context_data(**kwargs)
-        context['image'] = image
-        context['form'] = annotation_form
-        context['tags_data'] = '[]'
-        return render(request, 'iconolab/change_annotation.html', context)
-
-    def post(self, request, *args, **kwargs):
-        success, result = self.check_kwargs(kwargs)
-        if success:
-            (collection, image) = result
-        else:
-            return result(request)
-        collection_name = kwargs['collection_name']
-        image_guid = kwargs['image_guid']
-        annotation_form = AnnotationRevisionForm(request.POST)
-        if annotation_form.is_valid():
-            author = request.user
-            title = annotation_form.cleaned_data['title']
-            description = annotation_form.cleaned_data['description']
-            fragment = annotation_form.cleaned_data['fragment']
-            tags_json = annotation_form.cleaned_data['tags']
-            new_annotation = Annotation.objects.create_annotation(author, image, title=title, description=description, fragment=fragment, tags_json=tags_json)
-            revision_comment = annotation_form.cleaned_data['comment']
-            IconolabComment.objects.create(
-                comment = revision_comment,
-                revision = new_annotation.current_revision,
-                content_type = ContentType.objects.get(app_label='iconolab', model='annotation'),
-                content_object = new_annotation,
-                site = Site.objects.get(id=settings.SITE_ID),
-                object_pk = new_annotation.id,
-                user = request.user,
-                user_name = request.user.username
-            )
-            return RedirectView.as_view(url=reverse('annotation_detail', kwargs={'collection_name': collection_name, 'image_guid': image_guid, 'annotation_guid': new_annotation.annotation_guid}))(request)
-        context = self.get_context_data(**kwargs)
-        context['image'] = image
-        context['form'] = annotation_form
-        context['tags_data'] = '[]'
-        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', '')
-        context['image_guid'] = self.kwargs.get('image_guid', '')
-        context['annotation_guid'] = self.kwargs.get('annotation_guid', '')
-        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
-        else:
-            return result(request)
-        context = self.get_context_data(**kwargs)
-        context['collection'] = collection
-        context['image'] = image
-        context['annotation'] = annotation
-        context['tags_data'] = annotation.current_revision.get_tags_json()
-
-        page = request.GET.get('page', 1)
-        per_page = request.GET.get('perpage', 10)
-        full_comments_list = IconolabComment.objects.for_app_models('iconolab.annotation').filter(object_pk = annotation.pk).order_by('thread_id', '-order')
-        paginator = Paginator(full_comments_list, per_page)
-        try:
-            comments_list = paginator.page(page)
-        except PageNotAnInteger:
-            comments_list = paginator.page(1)
-        except EmptyPage:
-            comments_list = paginator.page(paginator.num_pages)
-        context['comments'] = comments_list
-
-        if request.user.is_authenticated():
-            user_comment_notifications = Notification.objects.filter(
-                recipient=request.user,
-                action_object_content_type__app_label='iconolab',
-                action_object_content_type__model='iconolabcomment',
-                target_content_type__app_label='iconolab',
-                target_content_type__model='annotation',
-                target_object_id=annotation.id
-            ).unread()
-            context['notifications_comments_ids'] = [int(val) for val in user_comment_notifications.values_list('action_object_object_id', flat=True)]
-            comment_list_ids = [comment.id for comment in context['comments'] ]
-            for notification in user_comment_notifications.all():
-                if int(notification.action_object_object_id) in comment_list_ids:
-                    notification.mark_as_read()
-
-        image.stats.views_count += 1
-        image.stats.save()
-        annotation.stats.views_count += 1
-        annotation.stats.save()
-        return render(request, 'iconolab/detail_annotation.html', context)
-
-
-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', '')
-        context['image_guid'] = self.kwargs.get('image_guid', '')
-        context['annotation_guid'] = self.kwargs.get('annotation_guid', '')
-        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
-        else:
-            return result(request)
-        context = self.get_context_data(**kwargs)
-        context['collection'] = collection
-        context['image'] = image
-        context['annotation'] = annotation
-        context['tags_data'] = annotation.current_revision.get_tags_json()
-
-        image.stats.views_count += 1
-        image.stats.save()
-        annotation.stats.views_count += 1
-        annotation.stats.save()
-        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', '')
-        context['image_guid'] = self.kwargs.get('image_guid', '')
-        context['annotation_guid'] = self.kwargs.get('annotation_guid', '')
-        return context
-
-    def get(self, request, *args, **kwargs):
-        success, result = self.check_kwargs(kwargs)
-        if success:
-            (collection, image, annotation,) = result
-        else:
-            return result(request)
-        annotation_form = AnnotationRevisionForm(instance=annotation.current_revision)
-        context = self.get_context_data(**kwargs)
-        context['image'] = image
-        context['annotation'] = annotation
-        context['form'] = annotation_form
-        context['tags_data'] = annotation.current_revision.get_tags_json()
-        return render(request, 'iconolab/change_annotation.html', context)
-
-    def post(self, request, *args, **kwargs):
-        success, result = self.check_kwargs(kwargs)
-        if success:
-            (collection, image, annotation) = result
-        else:
-            return result(request)
-        collection_name = kwargs['collection_name']
-        image_guid = kwargs['image_guid']
-        annotation_guid = kwargs['annotation_guid']
-        annotation_form = AnnotationRevisionForm(request.POST)
-        if annotation_form.is_valid():
-            revision_author = request.user
-            revision_title = annotation_form.cleaned_data['title']
-            revision_description = annotation_form.cleaned_data['description']
-            revision_fragment = annotation_form.cleaned_data['fragment']
-            revision_tags_json = annotation_form.cleaned_data['tags']
-            new_revision = annotation.make_new_revision(revision_author, revision_title, revision_description, revision_fragment, revision_tags_json)
-            revision_comment = annotation_form.cleaned_data['comment']
-            comment = IconolabComment.objects.create(
-                comment = revision_comment,
-                revision = new_revision,
-                content_type = ContentType.objects.get(app_label='iconolab', model='annotation'),
-                content_object = annotation,
-                site = Site.objects.get(id=settings.SITE_ID),
-                object_pk = annotation.id,
-                user = request.user,
-                user_name = request.user.username
-            )
-            return RedirectView.as_view(url=reverse('annotation_detail', kwargs={'collection_name': collection_name, 'image_guid': image_guid, 'annotation_guid': annotation_guid}))(request)
-        context = self.get_context_data(**kwargs)
-        context['image'] = image
-        context['form'] = annotation_form
-        context['annotation'] = annotation
-        context['tags_data'] = annotation.current_revision.get_tags_json()
-        return render(request, 'iconolab/change_annotation.html', context)
-
-
-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', '')
-        context['image_guid'] = self.kwargs.get('image_guid', '')
-        context['annotation_guid'] = self.kwargs.get('annotation_guid', '')
-        context['revision_guid'] = self.kwargs.get('revision_guid', '')
-        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
-        else:
-            return result(request)
-        context = self.get_context_data(**kwargs)
-        context['collection'] = collection
-        context['image'] = image
-        context['annotation'] = annotation
-        context['revision'] = revision
-        context['tags_data'] = revision.get_tags_json()
-        context['comment'] = revision.creation_comment.first()
-        if request.user.is_authenticated() and annotation.author == request.user:
-            ann_author_notified = Notification.objects.filter(
-                    recipient=request.user,
-                    action_object_content_type__app_label='iconolab',
-                    action_object_content_type__model='annotationrevision',
-                    action_object_object_id=revision.id,
-                    target_content_type__app_label='iconolab',
-                    target_content_type__model='annotation',
-                    target_object_id=annotation.id
-                ).unread()
-            if ann_author_notified:
-                ann_author_notified.first().mark_as_read()
-                context['notified_revision'] = True
-        if request.user.is_authenticated() and revision.author == request.user:
-            rev_author_notified = Notification.objects.filter(
-                    recipient=request.user,
-                    action_object_content_type__app_label='iconolab',
-                    action_object_content_type__model='annotationrevision',
-                    action_object_object_id=revision.id,
-                    target_content_type__app_label='iconolab',
-                    target_content_type__model='annotation',
-                    target_object_id=annotation.id
-                ).unread()
-            if rev_author_notified:
-                rev_author_notified.first().mark_as_read()
-                context['notified_revision'] = True
-        return render(request, 'iconolab/detail_revision.html', context)
-
-
-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', '')
-        context['image_guid'] = self.kwargs.get('image_guid', '')
-        context['annotation_guid'] = self.kwargs.get('annotation_guid', '')
-        context['revision_guid'] = self.kwargs.get('revision_guid', '')
-        return context
-
-    def get(self, request, *args, **kwargs):
-        success, result = self.check_kwargs(kwargs)
-        if success:
-            (collection, image, annotation, revision,) = result
-        else:
-            return result(request)
-        # Only show merge form if there is a revision to merge AND the current user is the annotation author
-        if revision.state != AnnotationRevision.AWAITING or request.user != annotation.author:
-            return RedirectView.as_view(
-                url=reverse('revision_detail',
-                    kwargs={
-                        'collection_name': collection.name,
-                        'image_guid': image.image_guid,
-                        'annotation_guid': annotation.annotation_guid,
-                        'revision_guid': revision.revision_guid
-                    }
-                )
-            )(request)
-        # Auto-accepts the revision only if the proper query arg is set and only if the revision parent is the current revision
-        if 'auto_accept' in request.GET and request.GET['auto_accept'] in ['True', 'true', '1', 'yes'] and revision.parent_revision == annotation.current_revision:
-            annotation.validate_existing_revision(revision)
-            return RedirectView.as_view(
-                url=reverse('annotation_detail',
-                    kwargs={
-                        'collection_name': collection.name,
-                        'image_guid': image.image_guid,
-                        'annotation_guid': annotation.annotation_guid
-                    }
-                )
-            )(request)
-        # Auto-reject the revision only if the proper query arg is set
-        if 'auto_reject' in request.GET and request.GET['auto_reject'] in ['True', 'true', '1', 'yes']:
-            annotation.reject_existing_revision(revision)
-            return RedirectView.as_view(
-                url=reverse('annotation_detail',
-                    kwargs={
-                        'collection_name': collection.name,
-                        'image_guid': image.image_guid,
-                        'annotation_guid': annotation.annotation_guid
-                    }
-                )
-            )(request)
-
-        context = self.get_context_data(**kwargs)
-        context['collection'] = collection
-        context['image'] = image
-        context['annotation'] = annotation
-        # Proposal data
-        context['proposal_revision'] = revision
-        context['proposal_tags_data'] = revision.get_tags_json()
-        context['proposal_comment'] = revision.creation_comment.first()
-        # Parent data
-        context['parent_revision'] = revision.parent_revision
-        context['parent_tags_data'] = revision.parent_revision.get_tags_json()
-        context['parent_comment'] = revision.parent_revision.creation_comment.first()
-        # Current data
-        context['current_revision'] = annotation.current_revision
-        context['current_tags_data'] = annotation.current_revision.get_tags_json()
-        context['current_comment'] = annotation.current_revision.creation_comment.first()
-
-        merge_form = AnnotationRevisionForm(instance=revision)
-        context['merge_form'] = merge_form
-        return render(request, 'iconolab/merge_revision.html', context)
-
-    def post(self, request, *args, **kwargs):
-        # Handle merge form submit here
-        success, result = self.check_kwargs(kwargs)
-        if success:
-            (collection, image, annotation, revision) = result
-        else:
-            return result(request)
-        collection_name = kwargs['collection_name']
-        image_guid = kwargs['image_guid']
-        annotation_guid = kwargs['annotation_guid']
-        revision_guid = kwargs['revision_guid']
-
-        merge_revision_form = AnnotationRevisionForm(request.POST)
-        if merge_revision_form.is_valid():
-            revision_title = merge_revision_form.cleaned_data['title']
-            revision_description = merge_revision_form.cleaned_data['description']
-            revision_fragment = merge_revision_form.cleaned_data['fragment']
-            revision_tags_json = merge_revision_form.cleaned_data['tags']
-            new_revision = annotation.merge_existing_revision(revision_title, revision_description, revision_fragment, revision_tags_json, revision)
-            revision_comment = merge_revision_form.cleaned_data['comment']
-            comment = IconolabComment.objects.create(
-                comment = revision_comment,
-                revision = new_revision,
-                content_type = ContentType.objects.get(app_label='iconolab', model='annotation'),
-                content_object = annotation,
-                site = Site.objects.get(id=settings.SITE_ID),
-                object_pk = annotation.id,
-                user = request.user,
-                user_name = request.user.username
-            )
-            return RedirectView.as_view(url=reverse('annotation_detail', kwargs={'collection_name': collection_name, 'image_guid': image_guid, 'annotation_guid': annotation_guid}))(request)
-        context = self.get_context_data(**kwargs)
-        context['image'] = image
-        context['merge_form'] = merge_revision_form
-        context['annotation'] = annotation
-        # Proposal data
-        context['proposal_revision'] = revision
-        context['proposal_tags_data'] = revision.get_tags_json()
-        context['proposal_comment'] = revision.creation_comment.first()
-        # Parent data
-        context['parent_revision'] = revision.parent_revision
-        context['parent_tags_data'] = revision.parent_revision.get_tags_json()
-        context['parent_comment'] = revision.parent_revision.creation_comment.first()
-        # Current data
-        context['current_revision'] = annotation.current_revision
-        context['current_tags_data'] = annotation.current_revision.get_tags_json()
-        context['current_comment'] = annotation.current_revision.creation_comment.first()
-        return render(request, 'iconolab/merge_revision.html', context)
-
+from django.shortcuts import HttpResponse, get_object_or_404, render, redirect
+from django.http import Http404
+from django.db.models import Count
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.models import User
+from django.views.generic import View, DetailView, RedirectView, TemplateView
+from django.views.generic.base import ContextMixin
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+from django.core.urlresolvers import reverse
+from django.core.exceptions import ObjectDoesNotExist
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.sites.models import Site
+from django.conf import settings
+from notifications.models import Notification
+from iconolab.models import Annotation, AnnotationRevision, Collection, Item, Image, IconolabComment, MetaCategory, MetaCategoryInfo
+from iconolab.forms.annotations import AnnotationRevisionForm
+import logging
+
+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()
+        context['homepage'] = True
+        return render(request, 'iconolab/home.html', context)
+
+class TestView(View):
+    template_name = 'iconolab/compare.html'
+
+    def get(self, request, *args, **kwargs):
+        return render(request, self.template_name)
+
+# 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 following this ordering:
+            (collection, item, image, annotation, revision)
+        '''
+
+        objects_tuple = ()
+        if 'collection_name' in kwargs.keys():
+            try:
+                objects_tuple += (Collection.objects.prefetch_related('items', 'items__images').get(name=kwargs.get('collection_name')),)
+            except (ValueError, Collection.DoesNotExist):
+                return False, RedirectView.as_view(url=reverse('404error'))
+        if 'item_guid' in kwargs.keys():
+            try:
+                objects_tuple += (Item.objects.prefetch_related('images', 'metadatas', 'images__stats').get(item_guid=kwargs.get('item_guid')),)
+            except (ValueError, Item.DoesNotExist):
+                return False, RedirectView.as_view(url=reverse('404error'))
+        if 'image_guid' in kwargs.keys():
+            try:
+                objects_tuple += (Image.objects.prefetch_related('annotations', 'item', 'stats').get(image_guid=kwargs.get('image_guid')),)
+            except (ValueError, Image.DoesNotExist):
+                return False, RedirectView.as_view(url=reverse('404error'))
+        if 'annotation_guid' in kwargs.keys():
+            try:
+                objects_tuple += (Annotation.objects.prefetch_related('current_revision', 'stats', 'image').get(annotation_guid=kwargs.get('annotation_guid')),)
+            except (ValueError, Annotation.DoesNotExist):
+                return False, RedirectView.as_view(url=reverse('404error'))
+        if 'revision_guid' in kwargs.keys():
+            try:
+                objects_tuple += (AnnotationRevision.objects.prefetch_related('parent_revision').get(revision_guid=kwargs.get('revision_guid')),)
+            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)
+                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
+        pagination_data["perpage"] = perpage
+        pagination_data["perpage_range"] = perpage_range
+        pagination_data["trailing_qarg"] = trailing_qarg
+        paginator = Paginator(list_to_paginate, perpage)
+        try:
+            pagination_data["list"] = paginator.page(page)
+        except PageNotAnInteger:
+            pagination_data["list"] = paginator.page(1)
+        except EmptyPage:
+            pagination_data["list"] = paginator.page(paginator.num_pages)
+        pagination_data["page_range"] = [
+            n for n in \
+            range(page - adjacent_pages_count, page + adjacent_pages_count + 1) \
+                if n > 0 and n <= paginator.num_pages
+        ]
+        pagination_data["show_first"] = page - adjacent_pages_count > 1
+        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
+
+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
+        else:
+            return result(request)
+        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'))
+        except ValueError:
+            items_page = 1
+        try:
+            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:
+            recent_page = 1
+        try:
+            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:
+            revised_page = 1
+        try:
+            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:
+            contributions_page = 1
+        try:
+            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,
+            adjacent_pages_count,
+            perpage_range=[6, 12, 48, 192],
+            trailing_qarg="&recent_page="+str(recent_page)
+            +"&recent_perpage="+str(recent_per_page)
+            +"&revised_page="+str(revised_page)
+            +"&revised_perpage="+str(revised_per_page)
+            +"&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,
+            adjacent_pages_count,
+            trailing_qarg="&items_page="+str(items_page)
+            +"&items_perpage="+str(items_per_page)
+            +"&revised_page="+str(revised_page)
+            +"&revised_perpage="+str(revised_per_page)
+            +"&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,
+            adjacent_pages_count,
+            trailing_qarg="&items_page="+str(items_page)
+            +"&items_perpage="+str(items_per_page)
+            +"&recent_page="+str(recent_page)
+            +"&recent_perpage="+str(recent_per_page)
+            +"&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,
+            metacategory__triggers_notifications=MetaCategory.CONTRIBUTORS
+        ).order_by('comment__submit_date').values_list('comment__object_pk', flat=True)))
+        collection_annotations = Annotation.objects.filter(id__in=contrib_calls_annotations_ids).all()
+        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,
+            adjacent_pages_count,
+            trailing_qarg="&items_page="+str(items_page)
+            +"&items_perpage="+str(items_per_page)
+            +"&recent_page="+str(recent_page)
+            +"&recent_perpage="+str(recent_per_page)
+            +"&revised_page="+str(revised_page)
+            +"&revised_perpage="+str(revised_per_page)
+        )
+
+        return render(request, 'iconolab/collection_home.html', context)
+
+
+
+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
+        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)]:
+            image_guid_to_display = str(item.images.first().image_guid)
+        context['display_image'] = image_guid_to_display
+        try:
+            displayed_annotations_page = int(request.GET.get('page', '1'))
+        except ValueError:
+            displayed_annotations_page = 1
+        try:
+            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
+        context['item'] = item
+        context['images'] = []
+        for image in item.images.all():
+            if str(image.image_guid) == image_guid_to_display:
+                page = displayed_annotations_page
+                per_page = displayed_annotations_per_page
+            else:
+                page = 1
+                per_page = 10
+            annotations_paginator = Paginator(image.annotations.all(), per_page)
+            try:
+                annotations = annotations_paginator.page(page)
+            except PageNotAnInteger:
+                annotations = annotations_paginator.page(1)
+            except EmptyPage:
+                annotations = annotations_paginator.page(recent_paginator.num_pages)
+            context['images'].append({
+                'obj' : image,
+                'annotations': annotations
+            })
+            image.stats.views_count += 1
+            image.stats.save()
+        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:
+            (collection, image) = result
+        else:
+            return result(request)
+        context = super(ShowImageView, self).get_context_data(**kwargs)
+        context['collection_name'] = self.kwargs.get('collection_name', '')
+        context['image_guid'] = self.kwargs.get('image_guid', '')
+        context['collection'] = collection
+        context['image'] = image
+        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', '')
+        context['image_guid'] = self.kwargs.get('image_guid', '')
+        return context
+
+    def get(self, request, *args, **kwargs):
+        success, result = self.check_kwargs(kwargs)
+        if success:
+            (collection, image,) = result
+        else:
+            return result(request)
+        annotation_form = AnnotationRevisionForm()
+        context = self.get_context_data(**kwargs)
+        context['image'] = image
+        context['form'] = annotation_form
+        context['tags_data'] = '[]'
+        return render(request, 'iconolab/change_annotation.html', context)
+
+    def post(self, request, *args, **kwargs):
+        success, result = self.check_kwargs(kwargs)
+        if success:
+            (collection, image) = result
+        else:
+            return result(request)
+        collection_name = kwargs['collection_name']
+        image_guid = kwargs['image_guid']
+        annotation_form = AnnotationRevisionForm(request.POST)
+        if annotation_form.is_valid():
+            author = request.user
+            title = annotation_form.cleaned_data['title']
+            description = annotation_form.cleaned_data['description']
+            fragment = annotation_form.cleaned_data['fragment']
+            tags_json = annotation_form.cleaned_data['tags']
+            new_annotation = Annotation.objects.create_annotation(author, image, title=title, description=description, fragment=fragment, tags_json=tags_json)
+            revision_comment = annotation_form.cleaned_data['comment']
+            IconolabComment.objects.create(
+                comment = revision_comment,
+                revision = new_annotation.current_revision,
+                content_type = ContentType.objects.get(app_label='iconolab', model='annotation'),
+                content_object = new_annotation,
+                site = Site.objects.get(id=settings.SITE_ID),
+                object_pk = new_annotation.id,
+                user = request.user,
+                user_name = request.user.username
+            )
+            return RedirectView.as_view(url=reverse('annotation_detail', kwargs={'collection_name': collection_name, 'image_guid': image_guid, 'annotation_guid': new_annotation.annotation_guid}))(request)
+        context = self.get_context_data(**kwargs)
+        context['image'] = image
+        context['form'] = annotation_form
+        context['tags_data'] = '[]'
+        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', '')
+        context['image_guid'] = self.kwargs.get('image_guid', '')
+        context['annotation_guid'] = self.kwargs.get('annotation_guid', '')
+        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
+        else:
+            return result(request)
+        context = self.get_context_data(**kwargs)
+        context['collection'] = collection
+        context['image'] = image
+        context['annotation'] = annotation
+        context['tags_data'] = annotation.current_revision.get_tags_json()
+
+        page = request.GET.get('page', 1)
+        per_page = request.GET.get('perpage', 10)
+        full_comments_list = IconolabComment.objects.for_app_models('iconolab.annotation').filter(object_pk = annotation.pk).order_by('thread_id', '-order')
+        paginator = Paginator(full_comments_list, per_page)
+        try:
+            comments_list = paginator.page(page)
+        except PageNotAnInteger:
+            comments_list = paginator.page(1)
+        except EmptyPage:
+            comments_list = paginator.page(paginator.num_pages)
+        context['comments'] = comments_list
+
+        if request.user.is_authenticated():
+            user_comment_notifications = Notification.objects.filter(
+                recipient=request.user,
+                action_object_content_type__app_label='iconolab',
+                action_object_content_type__model='iconolabcomment',
+                target_content_type__app_label='iconolab',
+                target_content_type__model='annotation',
+                target_object_id=annotation.id
+            ).unread()
+            context['notifications_comments_ids'] = [int(val) for val in user_comment_notifications.values_list('action_object_object_id', flat=True)]
+            comment_list_ids = [comment.id for comment in context['comments'] ]
+            for notification in user_comment_notifications.all():
+                if int(notification.action_object_object_id) in comment_list_ids:
+                    notification.mark_as_read()
+
+        image.stats.views_count += 1
+        image.stats.save()
+        annotation.stats.views_count += 1
+        annotation.stats.save()
+        return render(request, 'iconolab/detail_annotation.html', context)
+
+
+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', '')
+        context['image_guid'] = self.kwargs.get('image_guid', '')
+        context['annotation_guid'] = self.kwargs.get('annotation_guid', '')
+        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
+        else:
+            return result(request)
+        context = self.get_context_data(**kwargs)
+        context['collection'] = collection
+        context['image'] = image
+        context['annotation'] = annotation
+        context['tags_data'] = annotation.current_revision.get_tags_json()
+
+        image.stats.views_count += 1
+        image.stats.save()
+        annotation.stats.views_count += 1
+        annotation.stats.save()
+        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', '')
+        context['image_guid'] = self.kwargs.get('image_guid', '')
+        context['annotation_guid'] = self.kwargs.get('annotation_guid', '')
+        return context
+
+    def get(self, request, *args, **kwargs):
+        success, result = self.check_kwargs(kwargs)
+        if success:
+            (collection, image, annotation,) = result
+        else:
+            return result(request)
+        annotation_form = AnnotationRevisionForm(instance=annotation.current_revision)
+        context = self.get_context_data(**kwargs)
+        context['image'] = image
+        context['annotation'] = annotation
+        context['form'] = annotation_form
+        context['tags_data'] = annotation.current_revision.get_tags_json()
+        return render(request, 'iconolab/change_annotation.html', context)
+
+    def post(self, request, *args, **kwargs):
+        success, result = self.check_kwargs(kwargs)
+        if success:
+            (collection, image, annotation) = result
+        else:
+            return result(request)
+        collection_name = kwargs['collection_name']
+        image_guid = kwargs['image_guid']
+        annotation_guid = kwargs['annotation_guid']
+        annotation_form = AnnotationRevisionForm(request.POST)
+        if annotation_form.is_valid():
+            revision_author = request.user
+            revision_title = annotation_form.cleaned_data['title']
+            revision_description = annotation_form.cleaned_data['description']
+            revision_fragment = annotation_form.cleaned_data['fragment']
+            revision_tags_json = annotation_form.cleaned_data['tags']
+            new_revision = annotation.make_new_revision(revision_author, revision_title, revision_description, revision_fragment, revision_tags_json)
+            revision_comment = annotation_form.cleaned_data['comment']
+            comment = IconolabComment.objects.create(
+                comment = revision_comment,
+                revision = new_revision,
+                content_type = ContentType.objects.get(app_label='iconolab', model='annotation'),
+                content_object = annotation,
+                site = Site.objects.get(id=settings.SITE_ID),
+                object_pk = annotation.id,
+                user = request.user,
+                user_name = request.user.username
+            )
+            return RedirectView.as_view(url=reverse('annotation_detail', kwargs={'collection_name': collection_name, 'image_guid': image_guid, 'annotation_guid': annotation_guid}))(request)
+        context = self.get_context_data(**kwargs)
+        context['image'] = image
+        context['form'] = annotation_form
+        context['annotation'] = annotation
+        context['tags_data'] = annotation.current_revision.get_tags_json()
+        return render(request, 'iconolab/change_annotation.html', context)
+
+
+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', '')
+        context['image_guid'] = self.kwargs.get('image_guid', '')
+        context['annotation_guid'] = self.kwargs.get('annotation_guid', '')
+        context['revision_guid'] = self.kwargs.get('revision_guid', '')
+        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
+        else:
+            return result(request)
+        context = self.get_context_data(**kwargs)
+        context['collection'] = collection
+        context['image'] = image
+        context['annotation'] = annotation
+        context['revision'] = revision
+        context['tags_data'] = revision.get_tags_json()
+        context['comment'] = revision.creation_comment.first()
+        if request.user.is_authenticated() and annotation.author == request.user:
+            ann_author_notified = Notification.objects.filter(
+                    recipient=request.user,
+                    action_object_content_type__app_label='iconolab',
+                    action_object_content_type__model='annotationrevision',
+                    action_object_object_id=revision.id,
+                    target_content_type__app_label='iconolab',
+                    target_content_type__model='annotation',
+                    target_object_id=annotation.id
+                ).unread()
+            if ann_author_notified:
+                ann_author_notified.first().mark_as_read()
+                context['notified_revision'] = True
+        if request.user.is_authenticated() and revision.author == request.user:
+            rev_author_notified = Notification.objects.filter(
+                    recipient=request.user,
+                    action_object_content_type__app_label='iconolab',
+                    action_object_content_type__model='annotationrevision',
+                    action_object_object_id=revision.id,
+                    target_content_type__app_label='iconolab',
+                    target_content_type__model='annotation',
+                    target_object_id=annotation.id
+                ).unread()
+            if rev_author_notified:
+                rev_author_notified.first().mark_as_read()
+                context['notified_revision'] = True
+        return render(request, 'iconolab/detail_revision.html', context)
+
+
+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', '')
+        context['image_guid'] = self.kwargs.get('image_guid', '')
+        context['annotation_guid'] = self.kwargs.get('annotation_guid', '')
+        context['revision_guid'] = self.kwargs.get('revision_guid', '')
+        return context
+
+    def get(self, request, *args, **kwargs):
+        success, result = self.check_kwargs(kwargs)
+        if success:
+            (collection, image, annotation, revision,) = result
+        else:
+            return result(request)
+        # Only show merge form if there is a revision to merge AND the current user is the annotation author
+        if revision.state != AnnotationRevision.AWAITING or request.user != annotation.author:
+            return RedirectView.as_view(
+                url=reverse('revision_detail',
+                    kwargs={
+                        'collection_name': collection.name,
+                        'image_guid': image.image_guid,
+                        'annotation_guid': annotation.annotation_guid,
+                        'revision_guid': revision.revision_guid
+                    }
+                )
+            )(request)
+        # Auto-accepts the revision only if the proper query arg is set and only if the revision parent is the current revision
+        if 'auto_accept' in request.GET and request.GET['auto_accept'] in ['True', 'true', '1', 'yes'] and revision.parent_revision == annotation.current_revision:
+            annotation.validate_existing_revision(revision)
+            return RedirectView.as_view(
+                url=reverse('annotation_detail',
+                    kwargs={
+                        'collection_name': collection.name,
+                        'image_guid': image.image_guid,
+                        'annotation_guid': annotation.annotation_guid
+                    }
+                )
+            )(request)
+        # Auto-reject the revision only if the proper query arg is set
+        if 'auto_reject' in request.GET and request.GET['auto_reject'] in ['True', 'true', '1', 'yes']:
+            annotation.reject_existing_revision(revision)
+            return RedirectView.as_view(
+                url=reverse('annotation_detail',
+                    kwargs={
+                        'collection_name': collection.name,
+                        'image_guid': image.image_guid,
+                        'annotation_guid': annotation.annotation_guid
+                    }
+                )
+            )(request)
+
+        context = self.get_context_data(**kwargs)
+        context['collection'] = collection
+        context['image'] = image
+        context['annotation'] = annotation
+        # Proposal data
+        context['proposal_revision'] = revision
+        context['proposal_tags_data'] = revision.get_tags_json()
+        context['proposal_comment'] = revision.creation_comment.first()
+        # Parent data
+        context['parent_revision'] = revision.parent_revision
+        context['parent_tags_data'] = revision.parent_revision.get_tags_json()
+        context['parent_comment'] = revision.parent_revision.creation_comment.first()
+        # Current data
+        context['current_revision'] = annotation.current_revision
+        context['current_tags_data'] = annotation.current_revision.get_tags_json()
+        context['current_comment'] = annotation.current_revision.creation_comment.first()
+
+        merge_form = AnnotationRevisionForm(instance=revision)
+        context['merge_form'] = merge_form
+        return render(request, 'iconolab/merge_revision.html', context)
+
+    def post(self, request, *args, **kwargs):
+        # Handle merge form submit here
+        success, result = self.check_kwargs(kwargs)
+        if success:
+            (collection, image, annotation, revision) = result
+        else:
+            return result(request)
+        collection_name = kwargs['collection_name']
+        image_guid = kwargs['image_guid']
+        annotation_guid = kwargs['annotation_guid']
+        revision_guid = kwargs['revision_guid']
+
+        merge_revision_form = AnnotationRevisionForm(request.POST)
+        if merge_revision_form.is_valid():
+            revision_title = merge_revision_form.cleaned_data['title']
+            revision_description = merge_revision_form.cleaned_data['description']
+            revision_fragment = merge_revision_form.cleaned_data['fragment']
+            revision_tags_json = merge_revision_form.cleaned_data['tags']
+            new_revision = annotation.merge_existing_revision(revision_title, revision_description, revision_fragment, revision_tags_json, revision)
+            revision_comment = merge_revision_form.cleaned_data['comment']
+            comment = IconolabComment.objects.create(
+                comment = revision_comment,
+                revision = new_revision,
+                content_type = ContentType.objects.get(app_label='iconolab', model='annotation'),
+                content_object = annotation,
+                site = Site.objects.get(id=settings.SITE_ID),
+                object_pk = annotation.id,
+                user = request.user,
+                user_name = request.user.username
+            )
+            return RedirectView.as_view(url=reverse('annotation_detail', kwargs={'collection_name': collection_name, 'image_guid': image_guid, 'annotation_guid': annotation_guid}))(request)
+        context = self.get_context_data(**kwargs)
+        context['image'] = image
+        context['merge_form'] = merge_revision_form
+        context['annotation'] = annotation
+        # Proposal data
+        context['proposal_revision'] = revision
+        context['proposal_tags_data'] = revision.get_tags_json()
+        context['proposal_comment'] = revision.creation_comment.first()
+        # Parent data
+        context['parent_revision'] = revision.parent_revision
+        context['parent_tags_data'] = revision.parent_revision.get_tags_json()
+        context['parent_comment'] = revision.parent_revision.creation_comment.first()
+        # Current data
+        context['current_revision'] = annotation.current_revision
+        context['current_tags_data'] = annotation.current_revision.get_tags_json()
+        context['current_comment'] = annotation.current_revision.creation_comment.first()
+        return render(request, 'iconolab/merge_revision.html', context)
+