# HG changeset patch # User Alexandre Segura # Date 1487608195 -3600 # Node ID 55c024fc7c60ad40238f77202bcdb0ab81905f47 # Parent 1da12bcb989469625d9a06f76fd3772302c61607 Roughly implement annotation navigator. diff -r 1da12bcb9894 -r 55c024fc7c60 src/iconolab/forms/annotations.py --- a/src/iconolab/forms/annotations.py Mon Feb 20 17:14:08 2017 +0100 +++ b/src/iconolab/forms/annotations.py Mon Feb 20 17:29:55 2017 +0100 @@ -8,28 +8,27 @@ pass class AnnotationRevisionForm(forms.ModelForm): - tags = forms.CharField() - comment = forms.CharField(widget=forms.Textarea) - + description = forms.CharField(required=False) + fragment = forms.CharField(required=False) + tags = forms.CharField(widget=forms.Textarea, required=False) + comment = forms.CharField(widget=forms.Textarea, required=False) + class Meta: model = AnnotationRevision fields = ('title', 'description', 'fragment') widgets = { - 'fragment': forms.HiddenInput(), - 'tags': forms.HiddenInput() + 'fragment': forms.Textarea(), + 'tags': forms.Textarea() } - + def __init__(self, *args, **kwargs): super(AnnotationRevisionForm, self).__init__(*args, **kwargs) - for key in self.fields: - if key != "comment": - self.fields[key].required = False - + def clean(self, *args, **kwargs): cleaned_data = super(AnnotationRevisionForm, self).clean(*args, **kwargs) if not (cleaned_data.get("title", "") or cleaned_data.get("description", "") or json.loads(cleaned_data.get("tags", "[]"))): raise forms.ValidationError("Au moins un de ces champs doit être renseigné: description, titre ou mot-clé", code="missing_fields") - + def tags_json(self): if self.instance: tags_list = [] @@ -41,4 +40,4 @@ }) return json.dumps(tags_list) else: - return '[]' \ No newline at end of file + return '[]' diff -r 1da12bcb9894 -r 55c024fc7c60 src/iconolab/forms/comments.py --- a/src/iconolab/forms/comments.py Mon Feb 20 17:14:08 2017 +0100 +++ b/src/iconolab/forms/comments.py Mon Feb 20 17:29:55 2017 +0100 @@ -19,17 +19,15 @@ class IconolabCommentForm(XtdCommentForm): email = forms.EmailField(required=False) metacategories = MetaCategoryMultipleChoiceFields(widget=forms.CheckboxSelectMultiple, queryset=None, required=False) - + def __init__(self, *args, **kwargs): super(IconolabCommentForm, self).__init__(*args, **kwargs) self.collection = self.target_object.image.item.collection - logger.debug(self.fields) self.fields["metacategories"].queryset = self.collection.metacategories.all() - logger.debug(self.fields["metacategories"].queryset) self.fields.pop('email') - + def get_comment_create_data(self): self.cleaned_data['email'] = '' data = super(IconolabCommentForm, self).get_comment_create_data() data.update({'user_email': ''}) - return data \ No newline at end of file + return data diff -r 1da12bcb9894 -r 55c024fc7c60 src/iconolab/models.py --- a/src/iconolab/models.py Mon Feb 20 17:14:08 2017 +0100 +++ b/src/iconolab/models.py Mon Feb 20 17:29:55 2017 +0100 @@ -9,6 +9,7 @@ from django.contrib.auth.models import User from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.models import ContentType +from rest_framework import serializers from django.db import models, transaction from django.utils.text import slugify from django_comments_xtd.models import XtdComment @@ -146,6 +147,10 @@ return self.item.metadatas.measurements @property + def latest_annotations(self): + return self.annotations.all().order_by('-created') + + @property def tag_labels(self): tag_list = [] for annotation in self.annotations.all(): @@ -287,7 +292,6 @@ } for revision in latest_revision_on_annotations ] - logger.debug(contributed_list) return contributed_list @transaction.atomic @@ -310,9 +314,7 @@ 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 = [] @@ -328,7 +330,6 @@ '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"]) @@ -677,6 +678,7 @@ pass for tag_data in tags_dict: tag_string = tag_data.get("tag_input") + tag_label = tag_data.get("tag_label") tag_accuracy = tag_data.get("accuracy", 0) tag_relevancy = tag_data.get("relevancy", 0) @@ -688,6 +690,7 @@ else: tag_obj = Tag.objects.create( link=tag_string, + label=tag_label ) else: new_tag_link = settings.BASE_URL + '/' + slugify(tag_string) @@ -710,6 +713,7 @@ relevancy=tag_relevancy ) + # FIXME Avoid calling DBPedia all the time def get_tags_json(self): """ This method returns the json data that will be sent to the js to display @@ -796,6 +800,27 @@ pass return json.dumps(final_list) +class AnnotationRevisionSerializer(serializers.ModelSerializer): + tags = serializers.SerializerMethodField('get_normalized_tags') + annotation_guid = serializers.SerializerMethodField() + + def get_normalized_tags(self, obj): + tags = [] + for tagging_info in obj.tagginginfo_set.all(): + tags.append({ + "tag_label": tagging_info.tag.label, + "tag_link": tagging_info.tag.link, + "accuracy": tagging_info.accuracy, + "relevancy": tagging_info.relevancy, + }) + return tags + + def get_annotation_guid(self, obj): + return obj.annotation.annotation_guid + + class Meta: + model = AnnotationRevision + fields = ('annotation_guid', 'title', 'description', 'fragment', 'tags') class Tag(models.Model): """ @@ -814,7 +839,7 @@ return self.link.startswith(settings.INTERNAL_TAGS_URL) def __str__(self): - return self.label_slug + ":" + self.label + return "Tag:" + self.label class TaggingInfo(models.Model): @@ -829,7 +854,7 @@ relevancy = models.IntegerField() def __str__(self): - return str(str(self.tag.label_slug) + ":to:" + str(self.revision.revision_guid)) + return str(str(self.tag.label) + ":to:" + str(self.revision.revision_guid)) class IconolabComment(XtdComment): @@ -869,6 +894,12 @@ ).filter(thread_id__gte=self.thread_id).filter(order__lte=self.order).count() + 1) // settings.COMMENTS_PER_PAGE_DEFAULT + 1 +class IconolabCommentSerializer(serializers.ModelSerializer): + class Meta: + model = IconolabComment + fields = '__all__' + + class MetaCategory(models.Model): """ Metacategories are objects that can be linked to a comment to augment it diff -r 1da12bcb9894 -r 55c024fc7c60 src/iconolab/settings/dev.py.tmpl --- a/src/iconolab/settings/dev.py.tmpl Mon Feb 20 17:14:08 2017 +0100 +++ b/src/iconolab/settings/dev.py.tmpl Mon Feb 20 17:29:55 2017 +0100 @@ -36,7 +36,7 @@ -BASE_URL = '' +BASE_URL = 'http://localhost:8000' if JS_DEV_MODE: STATIC_URL = 'http://localhost:8001/static/' else: @@ -73,7 +73,8 @@ 'haystack', 'iconolab.apps.IconolabApp', 'sorl.thumbnail', - 'notifications' + 'notifications', + 'rest_framework', ] diff -r 1da12bcb9894 -r 55c024fc7c60 src/iconolab/templates/iconolab/detail_image.html --- a/src/iconolab/templates/iconolab/detail_image.html Mon Feb 20 17:14:08 2017 +0100 +++ b/src/iconolab/templates/iconolab/detail_image.html Mon Feb 20 17:29:55 2017 +0100 @@ -1,26 +1,160 @@ {% extends 'iconolab_base.html' %} {% load staticfiles %} - {% load thumbnail %} - {% load iconolab_tags %} -{% block content %} -
-
- Retour à la liste d'objets - Voir l'objet de cette image - Annoter l'image -

- - {% thumbnail image.media "800x800" crop=False as im %} - - {% endthumbnail %} -
-
+{% block content_fluid %} + +
+
+
+ {% if image.annotations %} + + {% else %} + Pas d'annotation pour cette image + {% endif %} +
+
+
+ {% with image.media as img %} + + {% endwith %} +
+ {% csrf_token %} + + + + +
+
+
+
+
+ + {% csrf_token %} + + +
+
+ +
+ +
+
+
+
+
+{% endblock %} -{% include "partials/image_annotations_list.html" with annotation_list=image.annotations.all header="Annotations de l'image" %} +{% block footer_js %} + +{% endblock %} + diff -r 1da12bcb9894 -r 55c024fc7c60 src/iconolab/templates/partials/comment_form.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/iconolab/templates/partials/comment_form.html Mon Feb 20 17:29:55 2017 +0100 @@ -0,0 +1,67 @@ +
+ {% csrf_token %} + {{ comment_form.content_type }} + {{ comment_form.object_pk }} + {{ comment_form.timestamp }} + {{ comment_form.security_hash }} + + + + +
+ {% if comment_form.comment.errors %} + + {% endif %} + +
+ + + {% comment %} + {# + + +

Commenter ({{user.username}})

+
+ +
+

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

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

+ +

+ #} + {% endcomment %} +
diff -r 1da12bcb9894 -r 55c024fc7c60 src/iconolab/templatetags/iconolab_tags.py --- a/src/iconolab/templatetags/iconolab_tags.py Mon Feb 20 17:14:08 2017 +0100 +++ b/src/iconolab/templatetags/iconolab_tags.py Mon Feb 20 17:29:55 2017 +0100 @@ -1,5 +1,8 @@ from django.template import Library -import sys +from django.core.serializers import serialize +from iconolab.models import AnnotationRevision, AnnotationRevisionSerializer +from rest_framework.renderers import JSONRenderer + from iconolab import __version__ register = Library() @@ -25,7 +28,7 @@ else: path_infos = path.split(";") if len(path_infos) > 0 : - result = path_infos[0] + result = path_infos[0] return result @@ -48,9 +51,16 @@ @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 + return str(arg1) + str(arg2) + +@register.filter(name='json') +def json_dumps(data): + if isinstance(data, AnnotationRevision) : + serializer = AnnotationRevisionSerializer(data) + return JSONRenderer().render(serializer.data) + return serialize('json', [data]) diff -r 1da12bcb9894 -r 55c024fc7c60 src/iconolab/urls.py --- a/src/iconolab/urls.py Mon Feb 20 17:14:08 2017 +0100 +++ b/src/iconolab/urls.py Mon Feb 20 17:29:55 2017 +0100 @@ -64,6 +64,8 @@ url(r'^search/', include('iconolab.search_indexes.urls', namespace='search_indexes')), url(r'^comments/', include('django_comments_xtd.urls')), url(r'^comments/annotation/post', views.comments.post_comment_iconolab, name="post_comment"), + url(r'^comments/annotation/(?P[^/]+)/comment-form$', views.comments.get_comment_form, name="get_comment_form"), + url(r'^comments/annotation/(?P[^/]+)/comments.json$', views.comments.get_annotation_comments_json, name="get_annotation_comments_json"), url(r'^compare/$', views.objects.TestView.as_view(), name="compare_view") #url(r'^search/', include('haystack.urls'), name="search_iconolab"), diff -r 1da12bcb9894 -r 55c024fc7c60 src/iconolab/views/comments.py --- a/src/iconolab/views/comments.py Mon Feb 20 17:14:08 2017 +0100 +++ b/src/iconolab/views/comments.py Mon Feb 20 17:29:55 2017 +0100 @@ -1,19 +1,22 @@ from django.apps import apps +from django.http import HttpResponse from django.views.decorators.csrf import csrf_protect -from django.views.decorators.http import require_POST +from django.views.decorators.http import require_POST, require_GET from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.shortcuts import render import datetime import django_comments from django_comments import signals from django_comments.views.utils import next_redirect, confirmation_view from django_comments.views.comments import CommentPostBadRequest -from iconolab.models import MetaCategoryInfo +from iconolab.models import MetaCategoryInfo, Annotation, IconolabComment, IconolabCommentSerializer +from rest_framework.renderers import JSONRenderer @csrf_protect @require_POST def post_comment_iconolab(request, next=None, using=None): ''' - Rewriting of a django_comments method to link Iconolab metacategories on comment posting + Rewriting of a django_comments method to link Iconolab metacategories on comment posting ''' # Fill out some initial data fields from an authenticated user, if present data = request.POST.copy() @@ -90,13 +93,13 @@ # Save the comment and signal that it was saved comment.save() - + signals.comment_was_posted.send( sender=comment.__class__, comment=comment, request=request ) - + # Creating metacategories here as apparently there is no way to make it work easily woth django_comments_xtd for metacategory in form.cleaned_data.get("metacategories", []): if 'xtd_comment' in comment: @@ -106,4 +109,23 @@ ) return next_redirect(request, fallback=next or 'comments-comment-done', - c=comment._get_pk_val()) \ No newline at end of file + c=comment._get_pk_val()) + +@require_GET +def get_comment_form(request, annotation_guid): + # TODO Manage 404 + annotation = Annotation.objects.get(annotation_guid=annotation_guid) + next_url = request.GET.get('next') + form = django_comments.get_form()(annotation) + return render(request, 'partials/comment_form.html', { + 'comment_form': form, + 'next': next_url + }) + +@require_GET +def get_annotation_comments_json(request, annotation_guid): + # TODO Manage 404 + annotation = Annotation.objects.get(annotation_guid=annotation_guid) + comments = IconolabComment.objects.for_app_models('iconolab.annotation').filter(object_pk = annotation.pk).order_by('thread_id', '-order') + serializer = IconolabCommentSerializer(comments, many=True) + return HttpResponse(JSONRenderer().render(serializer.data), content_type="application/json") diff -r 1da12bcb9894 -r 55c024fc7c60 src/iconolab/views/objects.py --- a/src/iconolab/views/objects.py Mon Feb 20 17:14:08 2017 +0100 +++ b/src/iconolab/views/objects.py Mon Feb 20 17:29:55 2017 +0100 @@ -12,8 +12,9 @@ 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.models import Annotation, AnnotationRevision, AnnotationRevisionSerializer, Collection, Item, Image, IconolabComment, MetaCategory, MetaCategoryInfo from iconolab.forms.annotations import AnnotationRevisionForm +from rest_framework import serializers import logging logger = logging.getLogger(__name__) @@ -325,6 +326,7 @@ """ success, result = self.check_kwargs(kwargs) + if success: (collection, item) = result else: @@ -386,6 +388,7 @@ context['image_guid'] = self.kwargs.get('image_guid', '') context['collection'] = collection context['image'] = image + context['form'] = AnnotationRevisionForm() return render(request, 'iconolab/detail_image.html', context) class CreateAnnotationView(View, ContextMixin, IconolabObjectView): @@ -427,18 +430,8 @@ 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) + redirect_url = reverse('image_detail', kwargs={'collection_name': collection_name, 'image_guid': image_guid}) + return RedirectView.as_view(url=redirect_url)(request) context = self.get_context_data(**kwargs) context['image'] = image context['form'] = annotation_form @@ -611,7 +604,8 @@ 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) + redirect_url = reverse('image_detail', kwargs={'collection_name': collection_name, 'image_guid': image_guid}) + return RedirectView.as_view(url=redirect_url)(request) context = self.get_context_data(**kwargs) context['image'] = image context['form'] = annotation_form diff -r 1da12bcb9894 -r 55c024fc7c60 src/requirements/base.txt --- a/src/requirements/base.txt Mon Feb 20 17:14:08 2017 +0100 +++ b/src/requirements/base.txt Mon Feb 20 17:29:55 2017 +0100 @@ -5,6 +5,7 @@ django-haystack==2.6.0 django-model-utils==2.6.1 django-notifications-hq==1.2 +djangorestframework==3.5.4 elasticsearch==5.1.0 jsonfield==1.0.3 olefile==0.44 diff -r 1da12bcb9894 -r 55c024fc7c60 src_js/iconolab-bundle/src/components/editor/AnnotationForm.vue --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src_js/iconolab-bundle/src/components/editor/AnnotationForm.vue Mon Feb 20 17:29:55 2017 +0100 @@ -0,0 +1,99 @@ + + + + + diff -r 1da12bcb9894 -r 55c024fc7c60 src_js/iconolab-bundle/src/components/editor/Canvas.vue --- a/src_js/iconolab-bundle/src/components/editor/Canvas.vue Mon Feb 20 17:14:08 2017 +0100 +++ b/src_js/iconolab-bundle/src/components/editor/Canvas.vue Mon Feb 20 17:29:55 2017 +0100 @@ -39,9 +39,15 @@ import ShapeFree from './ShapeFree.vue' export default { - props: [ - 'image', - ], + props: { + image: String, + tooltip: { + type: Boolean, + default: function () { + return false; + } + }, + }, components: { shapeRect: ShapeRect, shapeFree: ShapeFree @@ -75,25 +81,8 @@ if (!loaded) { return; } if (this.annotation) { - - var pieces = this.annotation.fragment.split(';'); - var path = pieces[0]; - var mode = pieces[1].toLowerCase(); - - this.mode = mode; - - this.$nextTick(() => { - path = this.denormalizePath(path); - if (mode === 'free') { - this.$refs.free.fromSVGPath(path); - } - if (mode === 'rect') { - this.$refs.rect.fromSVGPath(path); - } - }); - + this.loadAnnotation(); } else { - if (this.mode === 'free') { this.handleDrawFree(); } @@ -101,6 +90,11 @@ this.handleDrawRect(); } } + }, + annotation: function(annotation) { + this.reset(); + if (!this.annotation) { return; } + this.loadAnnotation(); } }, mounted() { @@ -154,7 +148,17 @@ this.$refs.rect.clear(); this.$refs.free.clear(); - // Remove event handlers + this.removeEventHandlers(); + + if (this.mode === 'free') { + this.handleDrawFree(); + } + if (this.mode === 'rect') { + this.handleDrawRect(); + } + }, + + removeEventHandlers: function() { this.paper.unmousedown(); this.paper.unmousemove(); this.paper.unmouseup(); @@ -165,6 +169,26 @@ this.annotation = annotation; }, + loadAnnotation: function() { + if (this.annotation.fragment.length > 0) { + var pieces = this.annotation.fragment.split(';'); + var path = pieces[0]; + var mode = pieces[1].toLowerCase(); + + this.mode = mode; + + this.$nextTick(() => { + path = this.denormalizePath(path); + if (mode === 'free') { + this.$refs.free.fromSVGPath(path, this.tooltip); + } + if (mode === 'rect') { + this.$refs.rect.fromSVGPath(path, this.tooltip); + } + }); + } + }, + getCenter: function() { return { x: this.viewBox[0] + (this.viewBox[2] / 2), @@ -301,8 +325,19 @@ return path; }, + toSVGPath: function() { + if (this.mode === 'free') { + this.$refs.free.toSVGPath(); + } + if (this.mode === 'rect') { + this.$refs.rect.toSVGPath(); + } + }, + handleDrawFree: function() { + this.removeEventHandlers(); + var clickTimeout; var clickHandler = function (offsetX, offsetY) { @@ -325,6 +360,8 @@ handleDrawRect: function() { + this.removeEventHandlers(); + var startPosition = { x: 0, y: 0 }; var currentPosition = { x: 0, y: 0 }; var canDraw = false; @@ -429,7 +466,7 @@ } .cut-canvas { width: 100%; - height: 800px; + height: 600px; } .canvas--rect:hover { cursor: crosshair; diff -r 1da12bcb9894 -r 55c024fc7c60 src_js/iconolab-bundle/src/components/editor/Comment.vue --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src_js/iconolab-bundle/src/components/editor/Comment.vue Mon Feb 20 17:29:55 2017 +0100 @@ -0,0 +1,49 @@ + + + + + diff -r 1da12bcb9894 -r 55c024fc7c60 src_js/iconolab-bundle/src/components/editor/CommentList.vue --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src_js/iconolab-bundle/src/components/editor/CommentList.vue Mon Feb 20 17:29:55 2017 +0100 @@ -0,0 +1,27 @@ + + + diff -r 1da12bcb9894 -r 55c024fc7c60 src_js/iconolab-bundle/src/components/editor/ShapeFree.vue --- a/src_js/iconolab-bundle/src/components/editor/ShapeFree.vue Mon Feb 20 17:14:08 2017 +0100 +++ b/src_js/iconolab-bundle/src/components/editor/ShapeFree.vue Mon Feb 20 17:29:55 2017 +0100 @@ -1,6 +1,8 @@