# HG changeset patch # User ymh # Date 1484754826 -3600 # Node ID 97b805fc88f0e1117ca842dafd6b88a28c5268fa # Parent ae36a174a3e5353433260beed1669f1a90ca28c6 force unix line ending diff -r ae36a174a3e5 -r 97b805fc88f0 .hgignore --- 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/ diff -r ae36a174a3e5 -r 97b805fc88f0 src/iconolab/apps.py --- 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 diff -r ae36a174a3e5 -r 97b805fc88f0 src/iconolab/auth/urls.py --- 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) +] diff -r ae36a174a3e5 -r 97b805fc88f0 src/iconolab/auth/views.py --- 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 diff -r ae36a174a3e5 -r 97b805fc88f0 src/iconolab/models.py --- 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 diff -r ae36a174a3e5 -r 97b805fc88f0 src/iconolab/settings/dev.py.tmpl --- 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 diff -r ae36a174a3e5 -r 97b805fc88f0 src/iconolab/static/iconolab/css/iconolab.css --- 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 diff -r ae36a174a3e5 -r 97b805fc88f0 src/iconolab/templates/iconolab/change_annotation.html --- 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 %} - -
- - - -
- -
-
- {% thumbnail image.media "500x360" crop=False as im %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - {% endthumbnail %} -
-
    -
  • Sélectionner le détail
  • -
  • Afficher la zone
  • -
  • Masquer la zone
  • - - -
-
- -
-
- {% if form.non_field_errors %} - - {% else %} - {% if not annotation %} - - {% endif %} - {% endif %} - - {% if form.errors %} - - {% endif %} - {% csrf_token %} -
- - {% if form.title.errors %} - - {% endif %} - -
-
- - {% if form.description.errors %} - - {% endif %} - -
-
- - {% if form.tags.errors %} - - {% endif %} - -
- -
- - {% if form.comment.errors %} - - {% endif %} - -
- - - Retour -

-
-
- -
-
- -{% endblock %} - -{% block footer_js %} - -{% endblock %} +{% extends 'iconolab_base.html' %} + +{% load staticfiles %} + +{% load thumbnail %} + +{% block content %} + +
+ + + +
+ +
+
+ {% thumbnail image.media "500x360" crop=False as im %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% endthumbnail %} +
+
    +
  • Sélectionner le détail
  • +
  • Afficher la zone
  • +
  • Masquer la zone
  • + + +
+
+ +
+
+ {% if form.non_field_errors %} + + {% else %} + {% if not annotation %} + + {% endif %} + {% endif %} + + {% if form.errors %} + + {% endif %} + {% csrf_token %} +
+ + {% if form.title.errors %} + + {% endif %} + +
+
+ + {% if form.description.errors %} + + {% endif %} + +
+
+ + {% if form.tags.errors %} + + {% endif %} + +
+ +
+ + {% if form.comment.errors %} + + {% endif %} + +
+ + + Retour +

+
+
+ +
+
+ +{% endblock %} + +{% block footer_js %} + +{% endblock %} diff -r ae36a174a3e5 -r 97b805fc88f0 src/iconolab/templates/iconolab/detail_annotation.html --- 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 %} -
- -
-
-
- {% thumbnail annotation.image.media "300x300" crop=False as im %} - - - - - - - - - - - {% endthumbnail %} -
-
-

- Voir l'objet de cette annotation - Voir les annotations sur l'image - -
-
-

Annotation créée par {{ annotation.author.username }}

-

Titre : {{ annotation.current_revision.title }}

-

Description : {{ annotation.current_revision.description }}

- {% if tags_data != "[]" %} -

Mot-clés :

- -
- {% endif %} - {% if user.is_authenticated %} - - {% if user == annotation.author %} - Modifier l'annotation - {% else %} - Proposer une révision - {% endif %} - -
- {% endif %} -
- {% include "partials/annotation_stats_panel.html" with annotation=annotation label="Statistiques sur cette annotation:" %} -
- - - -
-
-

Commentaires

- - -   - {% if comments.has_previous or comments.has_next %} -
    - {% if comments.has_previous %} -
  • - - - -
  • - {% endif %} - - {% for page in comments.paginator.page_range %} - - - {% endfor %} - - {% if comments.has_next %} -
  • - - - -
  • - {% endif %} -
- {% endif %} -
- - -
- {% if user.is_authenticated %} - {% if not comment_form %} - {% get_comment_form for annotation as comment_form %} - {% endif %} -
- {% csrf_token %} - {{ comment_form.content_type }} - {{ comment_form.object_pk }} - {{ comment_form.timestamp }} - {{ comment_form.security_hash }} - - -

Commenter ({{user.username}})

-
- -
-

-
-
-
-
- Annuler et répondre sur le fil principal

-
-
- {% if comment_form.comment.errors %} - - {% endif %} - -
-
-
- {% for metacategory in comment_form.metacategories %} - - {% endfor %} -
-
-

- -

-
- {% endif %} - -
-{% endblock %} - -{% block footer_js %} - -{% endblock %} +{% extends 'iconolab_base.html' %} + +{% load i18n %} +{% load staticfiles %} +{% load comments %} +{% load comments_xtd %} +{% load thumbnail %} +{% load iconolab_tags %} + +{% block content %} +
+ +
+
+
+ {% thumbnail annotation.image.media "300x300" crop=False as im %} + + + + + + + + + + + {% endthumbnail %} +
+
+

+ Voir l'objet de cette annotation + Voir les annotations sur l'image + +
+
+

Annotation créée par {{ annotation.author.username }}

+

Titre : {{ annotation.current_revision.title }}

+

Description : {{ annotation.current_revision.description }}

+ {% if tags_data != "[]" %} +

Mot-clés :

+ +
+ {% endif %} + {% if user.is_authenticated %} + + {% if user == annotation.author %} + Modifier l'annotation + {% else %} + Proposer une révision + {% endif %} + +
+ {% endif %} +
+ {% include "partials/annotation_stats_panel.html" with annotation=annotation label="Statistiques sur cette annotation:" %} +
+ + + +
+
+

Commentaires

+ + +   + {% if comments.has_previous or comments.has_next %} +
    + {% if comments.has_previous %} +
  • + + + +
  • + {% endif %} + + {% for page in comments.paginator.page_range %} + + + {% endfor %} + + {% if comments.has_next %} +
  • + + + +
  • + {% endif %} +
+ {% endif %} +
+ + +
+ {% if user.is_authenticated %} + {% if not comment_form %} + {% get_comment_form for annotation as comment_form %} + {% endif %} +
+ {% csrf_token %} + {{ comment_form.content_type }} + {{ comment_form.object_pk }} + {{ comment_form.timestamp }} + {{ comment_form.security_hash }} + + +

Commenter ({{user.username}})

+
+ +
+

+
+
+
+
+ Annuler et répondre sur le fil principal

+
+
+ {% if comment_form.comment.errors %} + + {% endif %} + +
+
+
+ {% for metacategory in comment_form.metacategories %} + + {% endfor %} +
+
+

+ +

+
+ {% endif %} + +
+{% endblock %} + +{% block footer_js %} + +{% endblock %} diff -r ae36a174a3e5 -r 97b805fc88f0 src/iconolab/templates/iconolab_base.html --- 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 %} - - - - -{% block head %} - - {% block title %} {% endblock %} - - {% block main_js %} - {% if IS_JS_DEV_MODE %} - - {% else %} - - {% endif %} - - {% endblock %} - {% block page_js %} {% endblock %} - - {% block main_css %} - - - - {% endblock %} - - {% block page_css %} {% endblock %} - - -{% endblock %} - - - -
- {% include "partials/header.html"%} - {% block content %} {% endblock %} -
- {% block footer_js %} - - {% endblock %} - -
- {% include "partials/footer.html" %} -
+{% load staticfiles %} + +{% load notifications_tags %} +{% load iconolab_tags %} + + + + +{% block head %} + + {% block title %} {% endblock %} + + {% block main_js %} + {% if IS_JS_DEV_MODE %} + + {% else %} + + {% endif %} + + {% endblock %} + {% block page_js %} {% endblock %} + + {% block main_css %} + + + + {% endblock %} + + {% block page_css %} {% endblock %} + + +{% endblock %} + + + +
+ {% include "partials/header.html"%} + {% block content %} {% endblock %} +
+ {% block footer_js %} + + {% endblock %} + +
+ {% include "partials/footer.html" %} +
\ No newline at end of file diff -r ae36a174a3e5 -r 97b805fc88f0 src/iconolab/templates/partials/header.html --- 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 %} - - -{% include "partials/header_breadcrumbs.html" %} +{% load notifications_tags %} +{% load staticfiles %} + + +{% include "partials/header_breadcrumbs.html" %} diff -r ae36a174a3e5 -r 97b805fc88f0 src/iconolab/templates/registration/login.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 %} -

Combinaison identifiant/mot de passe incorrecte. Veuillez réessayer.

-{% endif %} - -
-

Se connecter

-
- {% csrf_token %} - {% for field in form %} -
- - - {% if field.errors %}{{ field.errors }}{% endif %} -
- {% endfor %} - - -
-
- +{% extends "iconolab_base.html" %} + +{% block content %} + +{% if form.errors %} +

Combinaison identifiant/mot de passe incorrecte. Veuillez réessayer.

+{% endif %} + +
+

Se connecter

+
+ {% csrf_token %} + {% for field in form %} +
+ + + {% if field.errors %}{{ field.errors }}{% endif %} +
+ {% endfor %} + + +
+
+ {% endblock %} \ No newline at end of file diff -r ae36a174a3e5 -r 97b805fc88f0 src/iconolab/templatetags/iconolab_tags.py --- 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 diff -r ae36a174a3e5 -r 97b805fc88f0 src/iconolab/utils/context_processors.py --- 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 diff -r ae36a174a3e5 -r 97b805fc88f0 src/iconolab/utils/utils.py --- 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 diff -r ae36a174a3e5 -r 97b805fc88f0 src/iconolab/views/objects.py --- 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) +