streamlined comment system, replicated post_comment view so form errors redirect on the annotation page and not on a preview page + added revision detail view and links for specific revisions from the comments
authordurandn
Wed, 29 Jun 2016 14:56:27 +0200
changeset 42 51257e2701d9
parent 41 4fffc78a91b2
child 43 ccc449ef6f16
streamlined comment system, replicated post_comment view so form errors redirect on the annotation page and not on a preview page + added revision detail view and links for specific revisions from the comments
src/iconolab/forms/comments.py
src/iconolab/models.py
src/iconolab/templates/iconolab/change_annotation.html
src/iconolab/templates/iconolab/detail_annotation.html
src/iconolab/templates/iconolab/detail_revision.html
src/iconolab/urls.py
src/iconolab/views.py
--- a/src/iconolab/forms/comments.py	Wed Jun 29 14:54:16 2016 +0200
+++ b/src/iconolab/forms/comments.py	Wed Jun 29 14:56:27 2016 +0200
@@ -1,16 +1,30 @@
 from django import forms
 from django.utils.translation import ugettext_lazy as _
 from django_comments_xtd.forms import XtdCommentForm
+from django.contrib.contenttypes.models import ContentType
+from django.conf import settings
+from django.utils.encoding import force_text
+from django.utils import timezone
 from iconolab.models import MetaCategory
 
 class IconolabCommentForm(XtdCommentForm):
-    metacategories = forms.ModelMultipleChoiceField(queryset=MetaCategory.objects.all())
+    metacategories = forms.ModelMultipleChoiceField(queryset=MetaCategory.objects.all(), required=False)
+    email = forms.EmailField(required=False)
     
     def __init__(self, *args, **kwargs):
         super(IconolabCommentForm, self).__init__(*args, **kwargs)
         self.fields.pop('email')
     
     def get_comment_create_data(self):
-        data = super(IconolabCommentForm, self).get_comment_create_data()
-        data.update({'metacategories': self.cleaned_data['metacategories']})
-        return data
\ No newline at end of file
+        return dict(
+            content_type=ContentType.objects.get_for_model(self.target_object),
+            object_pk=force_text(self.target_object._get_pk_val()),
+            user_name=self.cleaned_data["name"],
+            user_email='',
+            user_url=self.cleaned_data["url"],
+            comment=self.cleaned_data["comment"],
+            submit_date=timezone.now(),
+            site_id=settings.SITE_ID,
+            is_public=True,
+            is_removed=False,
+        )
\ No newline at end of file
--- a/src/iconolab/models.py	Wed Jun 29 14:54:16 2016 +0200
+++ b/src/iconolab/models.py	Wed Jun 29 14:56:27 2016 +0200
@@ -193,8 +193,8 @@
 	
 	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='reverse_parent_revision', blank=True, null=True)
-	merge_parent_revision = models.ForeignKey('AnnotationRevision', related_name='reverse_merge_parent_revision', blank=True, null=True)
+	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)
@@ -233,7 +233,26 @@
 				accuracy = tag_accuracy,
 				relevancy = tag_relevancy
 			)
-
+		
+	def get_tags_json(self):
+		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
+				})
+			else:
+				#import label from dbpedia
+				final_list.append({
+					"tag_label": "",
+					"tag_link": tagging_info.tag.link,
+					"accuracy": tagging_info.accuracy,
+					"relevancy": tagging_info.relevancy
+				})
+		return json.dumps(final_list) 
 	
 class IconolabComment(XtdComment):
 	revision = models.ForeignKey('AnnotationRevision', related_name='creation_comment', null=True, blank=True)
--- a/src/iconolab/templates/iconolab/change_annotation.html	Wed Jun 29 14:54:16 2016 +0200
+++ b/src/iconolab/templates/iconolab/change_annotation.html	Wed Jun 29 14:56:27 2016 +0200
@@ -101,6 +101,7 @@
         			<input v-model="normalizePath" type="hidden" name="fragment"></input>
                     <input id="tags_input" type="hidden" name="tags" value="{{ form.tags_json }}"></input>
         			<button type="submit" class="save btn btn-default">Enregister</button>
+                    <a class="btn btn-default" href="{% if annotation %}{% url 'annotation_detail' collection_name image_guid annotation_guid %}{% else %}{% url 'image_detail' collection_name image_guid %}{% endif %}" role="button">Retour</a>
     			</form>
 			</div>
 			
--- a/src/iconolab/templates/iconolab/detail_annotation.html	Wed Jun 29 14:54:16 2016 +0200
+++ b/src/iconolab/templates/iconolab/detail_annotation.html	Wed Jun 29 14:56:27 2016 +0200
@@ -30,49 +30,60 @@
 			</div>
 
 			<div class='col-xs-6' style="">
+                <h4>Annotation</h4>
 				<p> <strong>Title:</strong> {{ annotation.current_revision.title }}</p>
 				<p> <strong>Description:</strong> {{ annotation.current_revision.description }}</p>
-				<p> <strong>Tags:</strong> {{ annotation.current_revision.tags }}</p>
+				<p> <strong>Tags:</strong> {{ tags_data }}</p>
 
 				<a href="{% url 'annotation_edit' collection_name image_guid annotation_guid  %}">Editer</a> | 
 				<a href="{% url 'annotation_edit' collection_name image_guid annotation_guid %}">Proposer une révision</a>
 			</div>
 		</div>
         <div class='col-md-12'>
+          <h4>Comments</h4>
           {% get_comment_list for annotation as comment_list %}
-          <dl class="dl-horizontal" id="comments">
+          <ul class="list-group" id="comments">
             {% for comment in comment_list %}
-              <dt id="c{{ comment.id }}">
-                {{ comment.name }}
-              </dt>
-              <dd>
+              <li class="list-group-item" id="c{{ comment.id }}">
+                <p><p>
                 <p>{{ comment.comment }} </p>
-                <div style="font-size:0.9em">{{ comment.submit_date }}&nbsp;-&nbsp;
-                {% if comment.allow_thread %}&nbsp;-&nbsp;<a href="{{ comment.get_reply_url }}">{% trans "Reply" %}</a>{% endif %}</div>
-              </dd>
+                <div style="font-size:0.9em">{{ comment.submit_date }}&nbsp;-&nbsp; <b>{{ comment.name }}</b>
+                {% if comment.revision %}&nbsp;-&nbsp; <a href="{% url 'revision_detail' collection_name image_guid annotation_guid comment.revision.revision_guid %}">Show revision</a>{% endif %}
+                {% if comment.allow_thread %}&nbsp;-&nbsp;  <a href="{{ comment.get_reply_url }}">{% trans "Reply" %}</a>{% endif %}</div>
+              </li>
             {% endfor %}
-          </dl>
+          </ul>
         </div>
         <div class='col-md-12'>
         {% if user.is_authenticated %}
-        {% get_comment_form for annotation as comment_form %}
-        <form class="form" action="{% comment_form_target %}" method="POST">
+        {% if not comment_form %}
+          {% get_comment_form for annotation as comment_form %}
+        {% endif %}
+        <form class="form" action="{% url 'post_comment' %}" method="POST">
           {% csrf_token %}
           {{ comment_form.content_type }}
           {{ comment_form.object_pk }}
           {{ comment_form.timestamp }}
           {{ comment_form.security_hash }}
-          <input type="hidden" name="next" value="{% url 'annotation_create' collection_name image_guid %}">
-          <fieldset class="form-group">
-            <label class="control-label" for="id_{{ comment_form.name.name }}">Commenting as {{ user.username }}</label>
-          </fieldset>
-          <fieldset class="form-group">
+          {{ comment_form.reply_to }}
+          <input type="hidden" name="next" value="{% url 'annotation_detail' collection_name image_guid annotation_guid %}">
+          <h4> Commenting as {{user.username}} </h4>
+          <fieldset class="form-group {% if comment_form.comment.errors %}has-error{% endif %}">
             <label class="control-label" for="id_{{ comment_form.comment.name }}">{{ comment_form.comment.label }}</label>
+            {% if comment_form.comment.errors %}
+              <div class="alert alert-danger" role="alert">
+                <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
+                <span class="sr-only">Error:</span>
+                  {{ comment_form.comment.errors | striptags }}
+              </div>
+            {% endif %}
             <textarea class="form-control"
               name="{{ comment_form.comment.name }}"
               id="id_{{ comment_form.comment.name }}" ></textarea>
           </fieldset>
-          <button type="submit" class="save btn btn-default">Poster</button>
+          <p class="submit">
+            <input class="btn btn-default" type="submit" name="post" class="submit-post" value="{% trans "Post" %}"/>
+          </p>
         </form>
         {% endif %}
         <div></div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/iconolab/templates/iconolab/detail_revision.html	Wed Jun 29 14:56:27 2016 +0200
@@ -0,0 +1,47 @@
+{% extends 'iconolab_base.html' %}
+
+{% load i18n %}
+{% load staticfiles %}
+{% load comments %}
+{% load comments_xtd %}
+{% load thumbnail %}
+{% load iconolab_tags %}
+
+{% block content %}
+	<a href="{% url 'image_detail' collection_name image_guid %}"><i class="fa fa-list"></i> Revoir l'image </a>
+	<div id="annotation-wrapper" class="row" style="border: 1px solid gray">
+		
+		<div v-show="formView" class="col-md-12">
+			<div class="col-xs-6">
+				<div class="small-image-wrapper" style="position: relative">
+					{% thumbnail image.media "x300" crop="center" as im %}
+						<img v-el:small-image src="{{ im.url }}" width="{{ im.width }}" height="{{ im.height }}" />
+
+						<svg width="{{ im.width }}" height="{{ im.height }}" version="1.1" style="position:absolute; top:0px; left: 0px">
+
+							<g transform="matrix({% transform_matrix im_width=im.width im_height=im.height max_x=100 max_y=100 %})">
+								<path d="{{ revision.fragment|clean_path }}" opacity="0.7" fill="orange"></path>
+							</g>
+
+						</svg>
+						
+					{% endthumbnail %}
+				</div>
+			</div>
+
+			<div class='col-xs-6' style="">
+                <h4>Annotation</h4>
+				<p> <strong>Title:</strong> {{ revision.title }}</p>
+				<p> <strong>Description:</strong> {{ revision.description }}</p>
+				<p> <strong>Tags:</strong> {{ tags_data }}</p>
+                <div class="alert alert-info" role="alert">
+                  <span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
+                  <span class="sr-only">Info:</span>
+                    <b>Revision created by {{ revision.author }} on the {{ revision.created }}</b><br>
+                    "{{ revision.creation_comment.first.comment }}"
+                </div>
+				<a href="{% url 'annotation_detail' collection_name image_guid annotation_guid  %}">Retour sur l'annotation</a>
+				{% if revision.parent_revision %} | <a href="{% url 'revision_detail' collection_name image_guid annotation_guid revision.parent_revision.revision_guid %}">Voir révision précédente</a>{% endif %}
+			</div>
+		</div>
+{% endblock %}
--- a/src/iconolab/urls.py	Wed Jun 29 14:54:16 2016 +0200
+++ b/src/iconolab/urls.py	Wed Jun 29 14:56:27 2016 +0200
@@ -30,11 +30,13 @@
     url(r'^collections/(?P<collection_name>[a-z]+)/images/(?P<image_guid>[^/]+)/annotations/create$', login_required(views.CreateAnnotationView.as_view()), name='annotation_create'),
     url(r'^collections/(?P<collection_name>[a-z]+)/images/(?P<image_guid>[^/]+)/annotations/(?P<annotation_guid>[^/]+)/detail$', views.ShowAnnotationView.as_view(), name='annotation_detail'),
     url(r'^collections/(?P<collection_name>[a-z]+)/images/(?P<image_guid>[^/]+)/annotations/(?P<annotation_guid>[^/]+)/edit$', login_required(views.EditAnnotationView.as_view()), name='annotation_edit'),
-    url(r'^collections/(?P<collection_name>[a-z]+)/images/(?P<image_guid>[^/]+)/annotations/(?P<annotation_guid>[^/]+)/revisions/(?P<revision_guid>[0-9]+)/merge$', login_required(views.MergeProposalView.as_view()), name='annotation_merge'),
+    url(r'^collections/(?P<collection_name>[a-z]+)/images/(?P<image_guid>[^/]+)/annotations/(?P<annotation_guid>[^/]+)/revisions/(?P<revision_guid>[^/]+)/detail', login_required(views.ShowRevisionView.as_view()), name='revision_detail'),
+    url(r'^collections/(?P<collection_name>[a-z]+)/images/(?P<image_guid>[^/]+)/annotations/(?P<annotation_guid>[^/]+)/revisions/(?P<revision_guid>[^/]+)/merge$', login_required(views.MergeProposalView.as_view()), name='annotation_merge'),
     url(r'errors/404', views.NotFoundErrorView.as_view(), name="404error"),
     url(r'^rest', include('restapi.urls')),
     url(r'^account/', include('iconolab.auth.urls', namespace='account')),
-    url(r'^comments/', include('django_comments_xtd.urls'))
+    url(r'^comments/', include('django_comments_xtd.urls')),
+    url(r'^comments/annotation/post', views.post_comment_iconolab, name="post_comment")
 ]
 
 urlpatterns += staticfiles_urlpatterns()
--- a/src/iconolab/views.py	Wed Jun 29 14:54:16 2016 +0200
+++ b/src/iconolab/views.py	Wed Jun 29 14:56:27 2016 +0200
@@ -1,35 +1,23 @@
+from django.apps import apps
 from django.shortcuts import HttpResponse, get_object_or_404, render
 from django.http import Http404
 from django.contrib.auth.decorators import login_required
 from django.views.generic import View, RedirectView
 from django.views.generic.base import ContextMixin
+from django.views.decorators.csrf import csrf_protect
+from django.views.decorators.http import require_POST
 from django.core.urlresolvers import reverse
+from django.core.exceptions import ObjectDoesNotExist, ValidationError
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.sites.models import Site
 from django.conf import settings
-from iconolab.models import Annotation, Collection, Image, IconolabComment
+from iconolab.models import Annotation, AnnotationRevision, Collection, Image, IconolabComment
 from iconolab.forms.annotations import AnnotationRevisionForm
-import json, datetime
+import datetime
+import django_comments
+from django_comments import signals
+from django_comments.views.utils import next_redirect, confirmation_view
 
-def make_tags_json(annotation_revision):
-	final_list = []
-	for tagging_info in annotation_revision.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
-			})
-		else:
-			#import label from dbpedia
-			final_list.append({
-				"tag_label": "",
-				"tag_link": tagging_info.tag.link,
-				"accuracy": tagging_info.accuracy,
-				"relevancy": tagging_info.relevancy
-			})
-	return json.dumps(final_list) 
 
 class GlobalHomepageView(View):
 	def get(self, request, *args, **kwargs):
@@ -65,7 +53,7 @@
 		context["collection"] = collection
 		context["image"] = image
 		return render(request, 'iconolab/detail_image.html', context);
-    
+	
 class CreateAnnotationView(View, ContextMixin):
 	
 	def get_context_data(self, **kwargs):
@@ -105,6 +93,17 @@
 			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)
 
 
@@ -137,8 +136,9 @@
 		self.check_kwargs(kwargs)
 		context = self.get_context_data(**kwargs)
 		context["collection"] = collection
-		context["image"] = collection
+		context["image"] = image
 		context["annotation"] = annotation
+		context["tags_data"] = annotation.current_revision.get_tags_json()
 		return render(request, 'iconolab/detail_annotation.html', context)
 
 
@@ -173,7 +173,7 @@
 		context["image"] = image
 		context["annotation"] = annotation
 		context["form"] = annotation_form
-		context["tags_data"] = make_tags_json(annotation.current_revision)
+		context["tags_data"] = annotation.current_revision.get_tags_json()
 		return render(request, 'iconolab/change_annotation.html', context) 
 	
 	def post(self, request, *args, **kwargs):
@@ -190,7 +190,7 @@
 			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"]
-			IconolabComment.objects.create(
+			comment = IconolabComment.objects.create(
 				comment = revision_comment,
 				revision = new_revision,
 				content_type = ContentType.objects.get(app_label="iconolab", model="annotation"),
@@ -200,9 +200,55 @@
 				user = request.user,
 				user_name = request.user.username
 			)
+			print(comment.revision)
+			print(new_revision.creation_comment.first())
 			return RedirectView.as_view(url=reverse("annotation_detail", kwargs={'collection_name': collection_name, 'image_guid': image_guid, 'annotation_guid': annotation_guid}))(request)
 		print(annotation_form.errors)
 		return HttpResponse("wow errors")
+
+
+class ShowRevisionView(View, ContextMixin):
+	
+	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 check_kwargs(self, kwargs):
+		try:
+			collection = Collection.objects.get(name=kwargs.get("collection_name", ""))
+		except Collection.DoesNotExist:
+			return RedirectView.as_view(url=reverse("404error"))
+		try:
+			image = Image.objects.get(image_guid=kwargs.get("image_guid", ""))
+		except Image.DoesNotExist:
+			return RedirectView.as_view(url=reverse("404error"))
+		try:
+			annotation = Annotation.objects.select_related("current_revision").get(annotation_guid=kwargs.get("annotation_guid", ""))
+		except Annotation.DoesNotExist:
+			return RedirectView.as_view(url=reverse("404error"))
+		try:
+			revision = AnnotationRevision.objects.select_related("parent_revision").get(revision_guid=kwargs.get("revision_guid", ""))
+		except AnnotationRevision.DoesNotExist:
+			return RedirectView.as_view(url=reverse("404error"))
+		return collection, image, annotation, revision
+	
+	def get(self, request, *args, **kwargs):
+		collection, image, annotation, revision = self.check_kwargs(kwargs)
+		self.check_kwargs(kwargs)
+		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()
+		print(revision.creation_comment)
+		context["comment"] = revision.creation_comment.first()
+		return render(request, 'iconolab/detail_revision.html', context)
+
 		
 class MergeProposalView(View):
 	
@@ -218,4 +264,96 @@
 class NotFoundErrorView(View):
 	def get(self, request, *args, **kwargs):
 		# Handle image display here
-		pass
\ No newline at end of file
+		pass
+	
+@csrf_protect
+@require_POST
+def post_comment_iconolab(request, next=None, using=None):
+	"""
+	Post a comment.
+	HTTP POST is required. If ``POST['submit'] == "preview"`` or if there are
+	errors a preview template, ``comments/preview.html``, will be rendered.
+	"""
+	# Fill out some initial data fields from an authenticated user, if present
+	data = request.POST.copy()
+	if request.user.is_authenticated():
+		if not data.get('name', ''):
+			data["name"] = request.user.get_full_name() or request.user.get_username()
+		if not data.get('email', ''):
+			data["email"] = request.user.email
+
+	# Look up the object we're trying to comment about
+	ctype = data.get("content_type")
+	object_pk = data.get("object_pk")
+	if ctype is None or object_pk is None:
+		return CommentPostBadRequest("Missing content_type or object_pk field.")
+	try:
+		model = apps.get_model(*ctype.split(".", 1))
+		target = model._default_manager.using(using).get(pk=object_pk)
+	except TypeError:
+		return CommentPostBadRequest(
+			"Invalid content_type value: %r" % escape(ctype))
+	except AttributeError:
+		return CommentPostBadRequest(
+			"The given content-type %r does not resolve to a valid model." % escape(ctype))
+	except ObjectDoesNotExist:
+		return CommentPostBadRequest(
+			"No object matching content-type %r and object PK %r exists." % (
+				escape(ctype), escape(object_pk)))
+	except (ValueError, ValidationError) as e:
+		return CommentPostBadRequest(
+			"Attempting go get content-type %r and object PK %r exists raised %s" % (
+				escape(ctype), escape(object_pk), e.__class__.__name__))
+
+	# Do we want to preview the comment?
+	preview = "preview" in data
+
+	# Construct the comment form
+	form = django_comments.get_form()(target, data=data)
+
+	# Check security information
+	if form.security_errors():
+		return CommentPostBadRequest(
+			"The comment form failed security verification: %s" % escape(str(form.security_errors())))
+
+	# If there are errors or if we requested a preview show the comment
+	if form.errors:
+		return render(request, "iconolab/detail_annotation.html", {
+				"comment_form": form,
+				"next": data.get("next", next),
+				"annotation": target,
+				"annotation_guid": target.annotation_guid,
+				"image_guid": target.image.image_guid,
+				"collection_name": target.image.item.collection.name,
+				"tags_data": target.current_revision.get_tags_json()
+			},
+		)
+
+	# Otherwise create the comment
+	comment = form.get_comment_object()
+	comment.ip_address = request.META.get("REMOTE_ADDR", None)
+	if request.user.is_authenticated():
+		comment.user = request.user
+
+	# Signal that the comment is about to be saved
+	responses = signals.comment_will_be_posted.send(
+		sender=comment.__class__,
+		comment=comment,
+		request=request
+	)
+
+	for (receiver, response) in responses:
+		if response is False:
+			return CommentPostBadRequest(
+				"comment_will_be_posted receiver %r killed the comment" % receiver.__name__)
+
+	# Save the comment and signal that it was saved
+	comment.save()
+	signals.comment_was_posted.send(
+		sender=comment.__class__,
+		comment=comment,
+		request=request
+	)
+
+	return next_redirect(request, fallback=next or 'comments-comment-done',
+						 c=comment._get_pk_val())
\ No newline at end of file