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
--- 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 }} -
- {% if comment.allow_thread %} - <a href="{{ comment.get_reply_url }}">{% trans "Reply" %}</a>{% endif %}</div>
- </dd>
+ <div style="font-size:0.9em">{{ comment.submit_date }} - <b>{{ comment.name }}</b>
+ {% if comment.revision %} - <a href="{% url 'revision_detail' collection_name image_guid annotation_guid comment.revision.revision_guid %}">Show revision</a>{% endif %}
+ {% if comment.allow_thread %} - <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