Introduce bookmarks feature.
authorAlexandre Segura <mex.zktk@gmail.com>
Wed, 17 May 2017 16:38:08 +0200
changeset 506 4e18e1f69db9
parent 505 bf5439a77a8d
child 507 1bae3da99830
Introduce bookmarks feature.
src/iconolab/forms/bookmarks.py
src/iconolab/migrations/0022_auto_20170516_1538.py
src/iconolab/models.py
src/iconolab/templates/iconolab/collection_home.html
src/iconolab/templates/iconolab/user_base.html
src/iconolab/templates/iconolab/user_bookmarks.html
src/iconolab/templates/partials/item_images_preview.html
src/iconolab/urls.py
src/iconolab/views/objects.py
src/iconolab/views/userpages.py
src_js/iconolab-bundle/src/iconolab.scss
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/iconolab/forms/bookmarks.py	Wed May 17 16:38:08 2017 +0200
@@ -0,0 +1,52 @@
+from django import forms
+from iconolab.models import Bookmark, BookmarkCategory
+import json, logging
+
+logger = logging.getLogger(__name__)
+
+class BookmarkForm(forms.ModelForm):
+
+    bookmark_category = forms.ChoiceField(required=False)
+    bookmark_category_new = forms.CharField(required=False)
+
+    class Meta:
+        model = Bookmark
+        fields = ('bookmark_category', 'bookmark_category_new')
+
+    def __init__(self, *args, **kwargs):
+
+        user = kwargs.pop('user', None)
+        super(BookmarkForm, self).__init__(*args, **kwargs)
+
+        if user.is_authenticated():
+
+            self.user = user
+
+            # Create the default category if not exists
+            try:
+                default_category = BookmarkCategory.objects.get(user=user, name='Favoris')
+            except BookmarkCategory.DoesNotExist:
+                default_category = BookmarkCategory(
+                    user=user,
+                    name='Favoris'
+                )
+                default_category.save()
+
+            bookmark_categories = BookmarkCategory.objects.filter(user=user).all()
+            choices = [(bookmark_category.id, bookmark_category.name) for bookmark_category in bookmark_categories]
+            for choice in choices:
+                self.fields['bookmark_category'].choices.append(choice)
+
+    def clean(self, *args, **kwargs):
+        cleaned_data = super(BookmarkForm, self).clean(*args, **kwargs)
+
+        if cleaned_data['bookmark_category_new']:
+            bookmark_category = BookmarkCategory(
+                user=self.user,
+                name=cleaned_data['bookmark_category_new']
+            )
+            bookmark_category.save()
+        else:
+            bookmark_category = BookmarkCategory.objects.get(id=cleaned_data['bookmark_category'])
+
+        cleaned_data['category'] = bookmark_category
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/iconolab/migrations/0022_auto_20170516_1538.py	Wed May 17 16:38:08 2017 +0200
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-05-16 15:38
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('iconolab', '0021_folder_display_image'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Bookmark',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('created', models.DateTimeField(auto_now_add=True)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='BookmarkCategory',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=255)),
+                ('created', models.DateTimeField(auto_now_add=True)),
+                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+            ],
+        ),
+        migrations.AddField(
+            model_name='bookmark',
+            name='category',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='iconolab.BookmarkCategory'),
+        ),
+        migrations.AddField(
+            model_name='bookmark',
+            name='image',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='iconolab.Image'),
+        ),
+    ]
--- a/src/iconolab/models.py	Tue May 16 12:36:38 2017 +0200
+++ b/src/iconolab/models.py	Wed May 17 16:38:08 2017 +0200
@@ -1004,3 +1004,14 @@
 
     def __str__(self):
         return "profile:" + self.user.username
+
+
+class BookmarkCategory(models.Model):
+    user = models.ForeignKey(User)
+    name = models.CharField(max_length=255)
+    created = models.DateTimeField(auto_now_add=True)
+
+class Bookmark(models.Model):
+    category = models.ForeignKey(BookmarkCategory)
+    image = models.ForeignKey(Image)
+    created = models.DateTimeField(auto_now_add=True)
--- a/src/iconolab/templates/iconolab/collection_home.html	Tue May 16 12:36:38 2017 +0200
+++ b/src/iconolab/templates/iconolab/collection_home.html	Wed May 17 16:38:08 2017 +0200
@@ -139,4 +139,83 @@
 
     </div>
   </div>
+  <div class="modal fade" id="bookmark-modal" tabindex="-1" role="dialog" aria-labelledby="bookmark-modal-label">
+    <form method="post" class="form-horizontal" action="{% url 'image_bookmark' collection_name ':image_guid' %}">
+      {% csrf_token %}
+      <div class="modal-dialog" role="document">
+        <div class="modal-content">
+          <div class="modal-header">
+            <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+            <h4 class="modal-title" id="bookmark-modal-label">Ajouter à mes favoris</h4>
+          </div>
+          <div class="modal-body">
+              <div class="form-group">
+                <label class="col-sm-2 control-label">Objet</label>
+                <div class="col-sm-10">
+                  <input ype="text" class="form-control bookmark-title" disabled="disabled" value="">
+                </div>
+              </div>
+              <div class="form-group bookmark-folder">
+                <label for="bookmark_folder" class="col-sm-2 control-label">Dossier</label>
+                <div class="col-sm-10">
+                  <select class="form-control" name="{{ bookmark_form.bookmark_category.name }}">
+                    {% for category_id, category_name in bookmark_form.bookmark_category.field.choices %}
+                    <option value="{{ category_id }}">{{ category_name }}</option>
+                    {% endfor %}
+                  </select>
+                </div>
+              </div>
+              <div class="form-group bookmark-folder-new" style="display: none;">
+                <label for="bookmark_folder" class="col-sm-2 control-label">Nouveau dossier</label>
+                <div class="col-sm-10">
+                  <input type="text" class="form-control" name="{{ bookmark_form.bookmark_category_new.name }}">
+                </div>
+              </div>
+              <div class="form-group">
+                <div class="col-sm-offset-2 col-sm-10">
+                  <button class="btn btn-sm btn-default bookmark-folder-add">Ajouter un dossier</button>
+                  <button class="btn btn-sm btn-default bookmark-folder-cancel" style="display: none;">Annuler</button>
+                </div>
+              </div>
+          </div>
+          <div class="modal-footer">
+            <button type="button" class="btn btn-default" data-dismiss="modal">Annuler</button>
+            <button type="submit" class="btn btn-primary">Valider</button>
+          </div>
+        </div>
+      </div>
+    </form>
+  </div>
 {% endblock %}
+
+{% block footer_js %}
+<script>
+var $modal = $('#bookmark-modal');
+
+function toggleAddFolder(e) {
+  e.preventDefault();
+  $modal.find('.bookmark-folder-new').toggle();
+  $modal.find('.bookmark-folder-cancel').toggle();
+  $modal.find('.bookmark-folder-add').toggle();
+  $modal.find('.bookmark-folder').toggle();
+}
+
+$modal.find('.bookmark-folder-add').on('click', toggleAddFolder);
+$modal.find('.bookmark-folder-cancel').on('click', toggleAddFolder);
+
+$('.bookmark').on('click', function(e) {
+  e.preventDefault();
+
+  var $target = $(e.currentTarget);
+
+  $modal.find('.bookmark-title').val($target.data('item'));
+  $modal.find('.bookmark-image').val($target.data('image-id'));
+
+  var $form = $modal.find('form');
+  $form.attr('action', $form.attr('action').replace(':image_guid', $target.data('image-guid')));
+
+  $modal.modal();
+
+});
+</script>
+{% endblock %}
--- a/src/iconolab/templates/iconolab/user_base.html	Tue May 16 12:36:38 2017 +0200
+++ b/src/iconolab/templates/iconolab/user_base.html	Wed May 17 16:38:08 2017 +0200
@@ -17,6 +17,10 @@
           class="list-group-item {% if request.resolver_match.url_name == 'user_annotations' %}active{% endif%}">
           Annotations
         </a>
+        <a href="{% url 'user_bookmarks' profile_user.username %}"
+          class="list-group-item {% if request.resolver_match.url_name == 'user_bookmarks' %}active{% endif%}">
+          Favoris
+        </a>
       </div>
       <div class="list-group">
       {% if profile_user == request.user %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/iconolab/templates/iconolab/user_bookmarks.html	Wed May 17 16:38:08 2017 +0200
@@ -0,0 +1,61 @@
+{% extends 'iconolab/user_base.html' %}
+
+{% load thumbnail %}
+{% load humanize %}
+
+{% block user_content %}
+  <table class="table">
+    <thead>
+      <th></th>
+      <th>Collection</th>
+      <th>Catégorie</th>
+      <th></th>
+      <th></th>
+    </thead>
+    <tbody>
+    {% for bookmark in bookmarks %}
+    <tr>
+      <td width="10%">
+        {% thumbnail bookmark.image.media "100x100" crop=False as thumbnail %}
+        <a href="{% url 'image_detail' bookmark.image.item.collection.name bookmark.image.image_guid %}">
+          <img src="{{ thumbnail.url }}" width="{{ thumbnail.width }}" height="{{ thumbnail.height }}" />
+        </a>
+        {% endthumbnail %}
+      </td>
+      <td>{{ bookmark.image.item.collection.verbose_name }}</td>
+      <td>
+        {% if request.user.is_authenticated and profile_user == request.user %}
+        <form class="bookmark-edit-form" method="post" action="{% url 'bookmark_edit' bookmark.id %}">
+          {% csrf_token %}
+          <select name="category">
+          {% for bookmark_category in bookmark_categories %}
+            <option value="{{ bookmark_category.id }}" {% if bookmark_category == bookmark.category %}selected="selected"{% endif %}>{{ bookmark_category.name }}</option>
+          {% endfor %}
+          </select>
+        </form>
+        {% else %}
+        {{ bookmark.category.name }}
+        {% endif %}
+      </td>
+      <td>{{ bookmark.created|naturaltime }}</td>
+      <td class="text-right">
+        {% if request.user.is_authenticated and profile_user == request.user %}
+        <form method="post" action="{% url 'bookmark_delete' bookmark.id %}">
+          {% csrf_token %}
+          <button type="submit" class="btn btn-danger btn-xs">Supprimer</button>
+        </form>
+        {% endif %}
+      </td>
+    </tr>
+    {% endfor %}
+    </tbody>
+  </table>
+{% endblock %}
+
+{% block footer_js %}
+<script>
+$('.bookmark-edit-form').find('select[name="category"]').on('change', function() {
+  $(this).closest('form').trigger('submit');
+});
+</script>
+{% endblock %}
--- a/src/iconolab/templates/partials/item_images_preview.html	Tue May 16 12:36:38 2017 +0200
+++ b/src/iconolab/templates/partials/item_images_preview.html	Wed May 17 16:38:08 2017 +0200
@@ -10,6 +10,11 @@
           <img v-el:small-image src="{{ im.url }}" width="{{ im.width }}" height="{{ im.height }}" />
         </a>
       {% endthumbnail %}
+      <a href="#" class="bookmark"
+        title="Ajouter à mes favoris"
+        data-item="{{ item.metadatas.joconde_ref }}"
+        data-image-id="{{ first_image.id }}"
+        data-image-guid="{{ first_image.image_guid }}"><i class="fa fa-lg fa-star"></i></a>
     </div>
 
     {% if item.images.count > 1 %}
--- a/src/iconolab/urls.py	Tue May 16 12:36:38 2017 +0200
+++ b/src/iconolab/urls.py	Wed May 17 16:38:08 2017 +0200
@@ -36,6 +36,7 @@
     url(r'^collections/(?P<collection_name>[a-z0-9\-]+)/items/?$', django_views.generic.RedirectView.as_view(pattern_name="collection_home")),
     url(r'^collections/(?P<collection_name>[a-z0-9\-]+)/images/?$', django_views.generic.RedirectView.as_view(pattern_name="collection_home")),
     url(r'^collections/(?P<collection_name>[a-z0-9\-]+)/images/(?P<image_guid>[^/]+)$', views.objects.ShowImageView.as_view(), name='image_detail'),
+    url(r'^collections/(?P<collection_name>[a-z0-9\-]+)/images/(?P<image_guid>[^/]+)/bookmark/?$', views.objects.BookmarkImageView.as_view(), name='image_bookmark'),
     url(r'^collections/(?P<collection_name>[a-z0-9\-]+)/images/(?P<image_guid>[^/]+)/annotations/?$', django_views.generic.RedirectView.as_view(pattern_name="image_detail")),
     url(r'^collections/(?P<collection_name>[a-z0-9\-]+)/images/(?P<image_guid>[^/]+)/annotations/create$', login_required(views.objects.CreateAnnotationView.as_view()), name='annotation_create'),
     url(r'^collections/(?P<collection_name>[a-z0-9\-]+)/images/(?P<image_guid>[^/]+)/annotations/(?P<annotation_guid>[^/]+)/?$', views.objects.ShowAnnotationView.as_view(), name='annotation_detail'),
@@ -53,6 +54,9 @@
     url(r'^user/(?P<slug>[a-z0-9\-]+)/commented/?$', views.userpages.UserCommentedView.as_view(), name="user_commented"),
     url(r'^user/(?P<slug>[a-z0-9\-]+)/contributed/?$', views.userpages.UserContributedView.as_view(), name="user_contributed"),
     url(r'^user/(?P<slug>[a-z0-9\-]+)/annotations/?$', views.userpages.UserAnnotationsView.as_view(), name="user_annotations"),
+    url(r'^user/(?P<slug>[a-z0-9\-]+)/bookmarks/?$', views.userpages.UserBookmarksView.as_view(), name="user_bookmarks"),
+    url(r'^bookmarks/(?P<bookmark>[0-9]+)/delete/?$', views.userpages.BookmarkDeleteView.as_view(), name="bookmark_delete"),
+    url(r'^bookmarks/(?P<bookmark>[0-9]+)/edit/?$', views.userpages.BookmarkEditView.as_view(), name="bookmark_edit"),
     url(r'^user/adminpanel/(?P<collection_name>[a-z0-9\-]+)/$', views.userpages.UserCollectionAdminView.as_view(), name="user_admin_panel"),
     url(r'^user/notifications/all/?$', login_required(views.userpages.UserNotificationsView.as_view()), name="user_notifications"),
 
--- a/src/iconolab/views/objects.py	Tue May 16 12:36:38 2017 +0200
+++ b/src/iconolab/views/objects.py	Wed May 17 16:38:08 2017 +0200
@@ -13,8 +13,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, Folder, Item, Image, IconolabComment, MetaCategory, MetaCategoryInfo
+from iconolab.models import Annotation, AnnotationRevision, Collection, Folder, Item, Image, IconolabComment, MetaCategory, MetaCategoryInfo, BookmarkCategory, Bookmark
 from iconolab.forms.annotations import AnnotationRevisionForm
+from iconolab.forms.bookmarks import BookmarkForm
 from iconolab.serializers import AnnotationRevisionSerializer
 import logging
 
@@ -309,6 +310,8 @@
             +"&revised_perpage="+str(revised_per_page)
         )
 
+        context['bookmark_form'] = BookmarkForm(user=request.user)
+
         return render(request, 'iconolab/collection_home.html', context)
 
 
@@ -410,6 +413,30 @@
         context['form'] = AnnotationRevisionForm()
         return render(request, 'iconolab/detail_image.html', context)
 
+class BookmarkImageView(View, ContextMixin, IconolabObjectView):
+    def post(self, request, *args, **kwargs):
+        success, result = self.check_kwargs(kwargs)
+        if success:
+            (collection, image) = result
+        else:
+            return result(request)
+
+        context = super(BookmarkImageView, self).get_context_data(**kwargs)
+
+        bookmark_form = BookmarkForm(request.POST, user=request.user)
+        if bookmark_form.is_valid():
+            bookmark = Bookmark(
+                image=image,
+                category=bookmark_form.cleaned_data['category']
+            )
+            bookmark.save()
+
+        context['bookmark_form'] = bookmark_form
+
+        redirect_url = reverse('collection_home', kwargs={'collection_name': self.kwargs.get('collection_name', '')})
+
+        return redirect(redirect_url)
+
 class CreateAnnotationView(View, ContextMixin, IconolabObjectView):
     """
         View that displays annotation forms and handles annotation creation
--- a/src/iconolab/views/userpages.py	Tue May 16 12:36:38 2017 +0200
+++ b/src/iconolab/views/userpages.py	Wed May 17 16:38:08 2017 +0200
@@ -9,7 +9,7 @@
 from django.conf import settings
 from django.urls import reverse
 from notifications.models import Notification
-from iconolab.models import Collection, Annotation, AnnotationRevision, IconolabComment, Image, MetaCategoriesCountInfo
+from iconolab.models import Collection, Annotation, AnnotationRevision, IconolabComment, Image, MetaCategoriesCountInfo, Bookmark, BookmarkCategory
 from iconolab.auth.forms import UserForm
 from itertools import chain
 from uuid import UUID
@@ -113,6 +113,61 @@
         context['profile_user'] = profile_user
         return render(request, 'iconolab/user_annotations.html', context)
 
+class UserBookmarksView(DetailView):
+
+    model = User
+    slug_field = 'username'
+
+    def get_context_data(self, **kwargs):
+        context = super(UserBookmarksView, self).get_context_data(**kwargs)
+        return context
+
+    def get(self, request, *args, **kwargs):
+        self.object = self.get_object()
+        profile_user = self.object
+        context = self.get_context_data()
+
+        context['profile_user'] = profile_user
+        context['bookmarks'] = Bookmark.objects.filter(category__user=profile_user).all()
+
+        if (request.user.is_authenticated()):
+            context['bookmark_categories'] = BookmarkCategory.objects.filter(user=request.user).all()
+
+        return render(request, 'iconolab/user_bookmarks.html', context)
+
+class BookmarkDeleteView(View):
+
+    def post(self, request, *args, **kwargs):
+
+        if request.user.is_authenticated():
+            bookmark_id = kwargs.get('bookmark')
+            bookmark = Bookmark.objects.get(id=bookmark_id)
+            if bookmark.category.user == request.user:
+                bookmark.delete()
+            else:
+                return redirect(reverse('home'))
+        else:
+            return redirect(reverse('home'))
+
+        return redirect(reverse('user_bookmarks', kwargs={'slug': request.user.username}))
+
+class BookmarkEditView(View):
+
+    def post(self, request, *args, **kwargs):
+
+        if request.user.is_authenticated():
+            bookmark_id = kwargs.get('bookmark')
+            bookmark = Bookmark.objects.get(id=bookmark_id)
+            if bookmark.category.user == request.user:
+                bookmark.category = BookmarkCategory.objects.get(id=request.POST.get('category'))
+                bookmark.save()
+            else:
+                return redirect(reverse('home'))
+        else:
+            return redirect(reverse('home'))
+
+        return redirect(reverse('user_bookmarks', kwargs={'slug': request.user.username}))
+
 class UserCommentedView(DetailView):
     """
         View that displays the full paginated list of annotations on which the considered user has commented
--- a/src_js/iconolab-bundle/src/iconolab.scss	Tue May 16 12:36:38 2017 +0200
+++ b/src_js/iconolab-bundle/src/iconolab.scss	Wed May 17 16:38:08 2017 +0200
@@ -40,9 +40,20 @@
             }
         }
         .main-image {
+            position: relative;
             a {
                 display: block;
             }
+            .bookmark {
+                color: #E67E22;
+                position: absolute;
+                top: 10px;
+                right: 10px;
+                &:hover,
+                &:active {
+                    color: darken(#E67E22, 10%);
+                }
+            }
         }
         .item-image-stats {
             display: flex;