--- 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" }} - <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 %} - {% 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>
-
- {% 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">«</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">»</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" }} - <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 %} - {% 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>
+
+ {% 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">«</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">»</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)
+