Commits

b7w committed 0f832de Merge

merge dev

Comments (0)

Files changed (21)

 097dbda132f98f2042611511af8801138e7640f1 v1.0.4
 6479e8dbb02a3d639d260c543ab665702758f996 v1.0.5
 cc9fd36f3c33bf84c44b5b186c32ffa7595db37d v1.0.6
+b60ff5c397e38ff8b108066bf7df5b7cf060fbb7 v1.0.7

bviewer/archive/views.py

 # -*- coding: utf-8 -*-
 import json
-
 import logging
-
 from django.core.urlresolvers import reverse
 from django.http import Http404, HttpResponse, HttpResponseRedirect
 from django.shortcuts import render
 
 logger = logging.getLogger(__name__)
 
+NO_USER_DEFINED = 'No user defined'
+NO_GALLERY_FOUND = 'No gallery found'
+NOT_ALBUM = 'It is not album with images'
+NOT_ALLOW_ARCHIVING = 'Archiving is disabled for this gallery'
+
 
 def index_view(request, gid):
     """
     """
     holder = get_gallery_user(request)
     if not holder:
-        return message_view(request, message='No user defined')
+        return message_view(request, message=NO_USER_DEFINED)
 
     controller = GalleryController(holder, request.user, gid)
     main = controller.get_object()
     if not main:
-        return message_view(request, message='No gallery found')
+        return message_view(request, message=NO_GALLERY_FOUND)
 
     if not controller.is_album():
-        return message_view(request, message='It is not album with images')
+        return message_view(request, message=NOT_ALBUM)
+
+    if not controller.is_archiving_allowed():
+        return message_view(request, message=NOT_ALLOW_ARCHIVING)
 
     image_paths = [i.path for i in controller.get_images()]
     z = ZipArchiveController(image_paths, holder)
     """
     holder = get_gallery_user(request)
     if not holder:
-        raise Http404('No user defined')
+        raise Http404(NO_USER_DEFINED)
 
     controller = GalleryController(holder, request.user, gid)
     main = controller.get_object()
     if not main:
-        return HttpResponse(json.dumps(dict(error='No gallery found')))
+        return HttpResponse(json.dumps(dict(error=NO_GALLERY_FOUND)))
 
     if not controller.is_album():
-        return HttpResponse(json.dumps(dict(error='It is not album with images')))
+        return HttpResponse(json.dumps(dict(error=NOT_ALBUM)))
+
+    if not controller.is_archiving_allowed():
+        return HttpResponse(json.dumps(dict(error=NOT_ALLOW_ARCHIVING)))
 
     image_paths = [i.path for i in controller.get_images()]
     z = ZipArchiveController(image_paths, holder, name=uid)
     """
     holder = get_gallery_user(request)
     if not holder:
-        raise Http404('No user defined')
+        raise Http404(NO_USER_DEFINED)
 
     controller = GalleryController(holder, request.user, gid)
     main = controller.get_object()
     if not main:
-        raise Http404('No gallery found')
+        raise Http404(NO_GALLERY_FOUND)
 
     if not controller.is_album():
-        return message_view(request, message='It is not album with images')
+        return message_view(request, message=NOT_ALBUM)
+
+    if not controller.is_archiving_allowed():
+        return message_view(request, message=NOT_ALLOW_ARCHIVING)
 
     image_paths = [i.path for i in controller.get_images()]
     z = ZipArchiveController(image_paths, holder, name=uid)

bviewer/core/controllers.py

 # -*- coding: utf-8 -*-
 import logging
 import re
-
 from django.conf import settings
 from django.core.cache import cache
 from django.db.models import Q
                 result.append(gallery)
         return result
 
+    def is_archiving_allowed(self):
+        obj = self.get_object()
+        return obj and obj.allow_archiving
+
     def is_album(self):
         """
         Is no any sub albums

bviewer/core/files/storage.py

                 while sum(map(os.path.getsize, cache_paths)) > self._max_cache_size:
                     os.remove(cache_paths.pop())
 
+
+    @io_call
+    def cache_size(self):
+        abs_cache = self._abs_cache_path
+        if os.path.exists(abs_cache):
+            cache_paths = [os.path.join(abs_cache, i) for i in os.listdir(abs_cache)]
+            cache_paths = [i for i in cache_paths if not os.path.islink(i)]
+            return sum(map(os.path.getsize, cache_paths))
+        return 0
+
     @io_call
     def rename_cache(self, path_from, path_to):
         abs_from = os.path.join(self._abs_cache_path, path_from)

bviewer/core/images.py

 import os
 import random
 import logging
-
+from django.utils.timezone import utc
 from django.conf import settings
 from PIL import Image, ImageDraw, ImageFont
 from PIL.ExifTags import TAGS
         time = self._data.get('DateTimeOriginal', None)
         if time:
             try:
-                return datetime.strptime(time, '%Y:%m:%d %H:%M:%S')
+                return datetime.strptime(time, '%Y:%m:%d %H:%M:%S').replace(tzinfo=utc)
             except ValueError:
                 logger.warning('Wrong datetime "%s" in "%s" file', time, self.image_path)
 

bviewer/core/models.py

 
 from bviewer.core.files.storage import ImageStorage
 from bviewer.core.exceptions import HttpError, ViewerError
+from bviewer.core.utils import set_time_from_exif
+
 
 logger = logging.getLogger(__name__)
 
     home = models.CharField(max_length=256, blank=True, default='')
 
     cache_size = models.PositiveIntegerField(default=32,
-        validators=[MinValueValidator(CACHE_SIZE_MIN), MaxValueValidator(CACHE_SIZE_MAX)])
+                                             validators=[MinValueValidator(CACHE_SIZE_MIN), MaxValueValidator(CACHE_SIZE_MAX)])
     cache_archive_size = models.PositiveIntegerField(default=256,
-        validators=[MinValueValidator(CACHE_ARCHIVE_SIZE_MIN), MaxValueValidator(CACHE_ARCHIVE_SIZE_MAX)])
+                                                     validators=[MinValueValidator(CACHE_ARCHIVE_SIZE_MIN),
+                                                                 MaxValueValidator(CACHE_ARCHIVE_SIZE_MAX)])
 
     top_gallery = models.ForeignKey('Gallery', related_name='top', null=True, blank=True, on_delete=models.DO_NOTHING)
     about_title = models.CharField(max_length=256, blank=True)
     title = models.CharField(max_length=256)
     user = models.ForeignKey(ProxyUser)
     visibility = models.SmallIntegerField(max_length=1, choices=VISIBILITY_CHOICE, default=VISIBLE,
-        help_text='HIDDEN - not shown on page for anonymous, PRIVATE - available only to the holder')
+                                          help_text='HIDDEN - not shown on page for anonymous, '
+                                                    'PRIVATE - available only to the holder')
     gallery_sorting = models.SmallIntegerField(max_length=1, choices=SORT_CHOICE, default=ASK,
-        help_text='How to sort galleries inside')
+                                               help_text='How to sort galleries inside')
+    allow_archiving = models.BooleanField(default=True)
     description = models.TextField(max_length=512, null=True, blank=True)
     thumbnail = models.ForeignKey('Image', null=True, blank=True, related_name='thumbnail', on_delete=models.SET_NULL)
     time = models.DateTimeField(default=date_now)
     """
     if created:
         storage = ImageStorage(instance.gallery.user)
-        image_path = storage.get_path(instance.path)
-        if image_path.is_image and image_path.exif.ctime:
-            instance.time = image_path.exif.ctime
-            instance.save()
+        set_time_from_exif(storage, instance, save=True)
 
 
 post_save.connect(update_time_from_exif, sender=Image)

bviewer/core/static/core/css/core.css

 }
 
 .gallery-years a:hover, .gallery-years .selected {
-    color: #CCC;
+    color: #EEE;
     border: solid 1px #3A3A3A;
 }
 

bviewer/core/templates/core/base.html

         <p>
             Copyright &copy; {% now "Y" %} <a href="mailto:{{ holder.email }}">{{ holder.username }}</a>
         </p>
+
         <p>
             <a href="https://bitbucket.org/b7w/bviewer">Project bviewer</a>
         </p>
     <script src="{% static 'core/js/jquery.min.js' %}" type="text/javascript"></script>
 {% endblock %}
 
-{{ EXTRA_HTML }}
+{{ EXTRA_HTML|safe }}
 
 </body>
 </html>

bviewer/core/templates/core/gallery.html

     <li class="view-full">
         <a href="#" onclick="playSlideshow()">Slideshow</a>
     </li>
+    {% if main.allow_archiving %}
     <li class="view-full">
         <a href="{% url 'archive.archive' main.id %}" title="Download all images in archive">Download</a>
     </li>
+    {% endif %}
 {% endblock %}
 
 {% block main %}

bviewer/core/tests/test_images.py

 # -*- coding: utf-8 -*-
 from datetime import datetime
+from django.utils.timezone import utc
 
 from bviewer.core.images import CacheImage
 from bviewer.core.tests.base import BaseImageStorageTestCase
 
         exif = self.storage.exif(self.path)
         exif._data = dict(DateTimeOriginal='2013:04:14 12:00:01')
-        self.assertEqual(exif.ctime, datetime(2013, 4, 14, 12, 0, 1))
+        self.assertEqual(exif.ctime, datetime(2013, 4, 14, 12, 0, 1, tzinfo=utc))

bviewer/core/utils.py

 import mockredis
 import time
 import logging
-
 from django.conf import settings
 from django.utils.encoding import smart_text, smart_bytes
 from django.utils.functional import wraps
     value = request.GET.get('year', '')
     if len(value) == 4 and value.isdigit():
         return int(value)
-    return default
+    return default
+
+
+def set_time_from_exif(storage, image, save=False):
+    """
+    :type storage: bviewer.core.files.storage.ImageStorage
+    :type image: bviewer.core.model.Image
+    """
+    image_path = storage.get_path(image.path)
+    if image_path.is_image and image_path.exif.ctime:
+        image.time = image_path.exif.ctime
+        if save:
+            image.save()

bviewer/profile/actions.py

+# -*- coding: utf-8 -*-
+import logging
+from django.contrib import admin
+from django.db.models import F
+from django.shortcuts import redirect, render
+
+from bviewer.core.utils import set_time_from_exif
+from bviewer.core.files.storage import ImageStorage
+from bviewer.profile.forms import BulkTimeUpdateForm
+
+
+logger = logging.getLogger(__name__)
+
+
+def bulk_time_update(model_admin, request, queryset):
+    form = None
+    if 'apply' in request.POST:
+        form = BulkTimeUpdateForm(request.POST)
+        if form.is_valid():
+            method = form.cleaned_data['method']
+            interval = form.cleaned_data['interval']
+            if interval and method == BulkTimeUpdateForm.ADD:
+                queryset.update(time=F('time') + interval)
+            if interval and method == BulkTimeUpdateForm.SUBTRACT:
+                queryset.update(time=F('time') - interval)
+
+            model_admin.message_user(request, '{0} {1}'.format(method.capitalize(), interval))
+            return redirect(request.get_full_path())
+
+    if not form:
+        form = BulkTimeUpdateForm(initial={'_selected_action': request.POST.getlist(admin.ACTION_CHECKBOX_NAME)})
+    return render(request, 'profile/bulk_time_update.html', {
+        'title': 'Bulk time update',
+        'form': form,
+        'dimensions': BulkTimeUpdateForm.DIMENSIONS,
+    })
+
+
+def update_time_from_exif(model_admin, request, queryset):
+    for image in queryset:
+        storage = ImageStorage(image.gallery.user)
+        set_time_from_exif(storage, image, save=True)

bviewer/profile/admin.py

 # -*- coding: utf-8 -*-
+import os
 from collections import Counter
-import os
-
 from django.contrib.admin import AdminSite, ModelAdmin
 from django.contrib.auth.admin import UserAdmin
 from django.core.urlresolvers import reverse
 from django.utils.encoding import smart_text
 
 from bviewer.core.admin import ProxyUserForm
+from bviewer.core.files.storage import ImageStorage
 from bviewer.core.models import Gallery, Image, ProxyUser, Video
+from bviewer.profile.actions import bulk_time_update, update_time_from_exif
+from bviewer.profile.forms import AdminGalleryForm
 
 
 class ProfileSite(AdminSite):
 
 class ProfileGalleryAdmin(ProfileModelAdmin):
     list_select_related = True
+    form = AdminGalleryForm
 
     list_display = ('title', 'parent', 'visibility', 'images', 'time',)
     list_filter = ('parent__title', 'time', )
     search_fields = ('title', 'description',)
 
     readonly_fields = ('images', 'thumbnails',)
-    fields = ('parent', 'title', 'visibility', 'gallery_sorting', 'images', 'description', 'time', 'thumbnails', )
+    fields = ('parent', 'title', 'visibility', 'gallery_sorting', 'allow_archiving',
+              'images', 'description', 'time', 'thumbnails', )
 
     def images(self, obj):
         if Gallery.objects.safe_get(id=obj.id):
             obj.thumbnail_id = thumbnail_id
         else:
             obj.thumbnail = None
-        obj.save()
+        super(ProfileGalleryAdmin, self).save_model(request, obj, form, change)
 
-    def add_view(self, request, form_url='', extra_context=None):
+    def get_form(self, request, obj=None, **kwargs):
         # Add default parent Welcome gallery
+        user = ProxyUser.objects.get(pk=request.user.pk)
         data = request.GET.copy()
-        data['parent'] = ProxyUser.objects.get(pk=request.user.pk).top_gallery_id
+        data['parent'] = user.top_gallery_id
         request.GET = data
-        return super(ProfileGalleryAdmin, self).add_view(request, form_url, extra_context)
+        # Add default user
+        data = request.POST.copy()
+        data['user'] = user.id
+        request.POST = data
+        return super(ProfileGalleryAdmin, self).get_form(request, obj=None, **kwargs)
 
     def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
         """
 
 class ProfileImageAdmin(ProfileModelAdmin):
     list_select_related = True
+    actions = [bulk_time_update, update_time_from_exif, ]
 
     list_display = ('path', 'file_name', 'gallery_title', 'time', )
     list_filter = ('gallery__title', 'time',)
     ordering = ('-time', 'gallery', )
 
+    readonly_fields = ('image_thumbnail',)
     search_fields = ('gallery__title', 'path',)
 
     def file_name(self, obj):
     def gallery_title(self, obj):
         return obj.gallery.title
 
+    def image_thumbnail(self, obj):
+        url = reverse('core.download', kwargs=dict(size='small', uid=obj.id))
+        return smart_text('<img class="thumbnail" src="{0}">').format(url)
+
+    image_thumbnail.allow_tags = True
+
     def queryset(self, request):
         return super(ProfileImageAdmin, self).queryset(request).filter(gallery__user=request.user)
 
     fieldsets = (
         ('Account info', {'fields': ('username', 'password', )}),
         ('Personal info', {'fields': ('email', 'first_name', 'last_name', )}),
-        ('Viewer info', {'fields': ('url', 'top_gallery', 'cache_size', 'cache_archive_size', )}),
+        ('Viewer info', {'fields': ('url', 'top_gallery', 'cache_size', 'cache_archive_size', 'cache_info', )}),
         ('Additional info', {'fields': ('about_title', 'about_text', )}),
         ('Important dates', {'fields': ('last_login', 'date_joined', )}),
     )
-    readonly_fields = ('last_login', 'date_joined', )
+    readonly_fields = ('last_login', 'date_joined', 'cache_info', )
 
     form = ProxyUserForm
 
+    def cache_info(self, user):
+        storage = ImageStorage(user)
+        images_size = storage.cache_size() / 2 ** 20
+        storage = ImageStorage(user, archive_cache=True)
+        archive_size = storage.cache_size() / 2 ** 20
+        return 'Images size: {0} MB, archives size: {1} MB'.format(images_size, archive_size)
+
     def has_add_permission(self, request):
         return False
 

bviewer/profile/forms.py

 # -*- coding: utf-8 -*-
-
-from django.forms import ModelForm
+import re
+from datetime import timedelta
+from django.core.exceptions import ValidationError
+from django.forms import Form, ModelForm, ChoiceField, CharField, MultipleHiddenInput
 
 from bviewer.core.models import Gallery, Video
 
 
+class BulkTimeUpdateForm(Form):
+    ADD = 'add'
+    SUBTRACT = 'subtract'
+    CHOICES = (
+        (ADD, 'Add'),
+        (SUBTRACT, 'Subtract')
+    )
+    DIMENSIONS = ('days', 'seconds', 'minutes', 'hours', 'weeks')
+    _selected_action = CharField(widget=MultipleHiddenInput)
+    method = ChoiceField(choices=CHOICES)
+    interval = CharField()
+
+    def clean_interval(self):
+        interval = self.cleaned_data['interval']
+        dimensions_symbol = tuple(i[0] for i in self.DIMENSIONS)
+        if not re.match(r'[\d{0}]'.format(''.join(dimensions_symbol)), interval):
+            raise ValidationError('Wrong format, support only {0}'.format(self.DIMENSIONS))
+        kwargs = {}
+        for symbol, dimension in zip(dimensions_symbol, self.DIMENSIONS):
+            match = re.search(r'\d+{0}'.format(symbol), interval)
+            if match:
+                kwargs[dimension] = int(match.group()[:-1])
+        return timedelta(**kwargs)
+
+
+class AdminGalleryForm(ModelForm):
+    def clean_title(self):
+        title = self.cleaned_data['title']
+        user_id = self.data['user']
+        if Gallery.objects.filter(title=title, user_id=user_id).exclude(id=self.instance.id).count() > 0:
+            raise ValidationError('Title must be unique')
+        return title
+
+    class Meta(object):
+        model = Gallery
+
+
 class GalleryForm(ModelForm):
     class Meta(object):
         model = Gallery

bviewer/profile/templates/profile/bulk_time_update.html

+{% extends "admin/base_site.html" %}
+
+
+{% block content %}
+    <p>Interval dimensions: {{ dimensions|join:', ' }}</p>
+    <p>Example: 10d 4h</p>
+    <form action="" method="post">{% csrf_token %}
+        {{ form }}
+        <input type="hidden" name="action" value="bulk_time_update"/>
+        <input type="submit" name="apply" value="Save"/>
+    </form>
+{% endblock %}

bviewer/profile/templates/profile/thumbnails.html

 {% load staticfiles %}
 
 <script type="text/javascript">
-    django.jQuery('#thumbnails-open').live('click', function (e) {
+    django.jQuery(document).on('click', '#thumbnails-open', function (e) {
         e.preventDefault();
         django.jQuery('#thumbnails-open').css('display', 'none');
         django.jQuery('#thumbnails').css('display', 'block');
     {% for item in images %}
         <li>
             <span class="thumbnail">
-                <input type="radio" name="thumbnail_id" value="{{ item.id }}" {% if obj.thumbnail_id == item.id %}checked{% endif %}>
+                <input type="radio" name="thumbnail_id" value="{{ item.id }}"
+                       {% if obj.thumbnail_id == item.id %}checked{% endif %}>
                 <img class="thumbnail" data-src="{% url 'core.download' 'tiny' item.id %}">
             </span>
         </li>

bviewer/settings/debug.py

 )
 
 DEBUG_TOOLBAR_CONFIG = {
-    'INTERCEPT_REDIRECTS': False,
     'SHOW_TOOLBAR_CALLBACK': 'bviewer.core.utils.show_toolbar',
 }
 # The short X.Y version.
 version = '1.0'
 # The full version, including alpha/beta/rc tags.
-release = '1.0.7'
+release = '1.0.8'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.

docs/ref/architecture.rst

   *If parent is None it will be hidden from gallery tree for holder too.*
   **Gallery sorting** - Sort order of the nested galleries on time.
   ASK - Ascending, DESK - Descending.
+  **allow_archiving** - Allow users to download images in archive
   **Thumbnail** - image of gallery tile.
 
 .. code-block:: python
         user = models.ForeignKey(ProxyUser)
         visibility = models.SmallIntegerField(max_length=1, choices=VISIBILITY_CHOICE, default=VISIBLE)
         gallery_sorting = models.SmallIntegerField(max_length=1, choices=SORT_CHOICE, default=ASK)
+        allow_archiving = models.BooleanField(default=True)
         description = models.TextField(max_length=512, null=True)
         thumbnail = models.ForeignKey('Image', null=True)
         time = models.DateTimeField(default=datetime.now)

docs/releases/notes.rst

 
 .. index:: Release notes
 
-| **v1.0.7**
+| **v1.0.8** - *20.05.1014*
+| Add some small features to profile. Need database scheme update.
+
+| ``ALTER TABLE core_gallery ADD COLUMN allow_archiving boolean NOT NULL DEFAULT true;``
+
+* Fix bug not safe HTML_EXTRA
+* Fix bug admin gallery title unique integrity error
+* Add bulk time edit and update from exif actions for images in profile
+* Add option to allow/disallow gallery archiving
+* Add disk cache info in user profile
+
+
+| **v1.0.7** - *26.01.1014*
 | Fix some bugs. Move project to django 1.6
 
 * Fix exception in getting image cache name on wrong file path
 
 setup(
     name='bviewer',
-    version='1.0.7',
+    version='1.0.8',
     install_requires=[
         'django>=1.6,<1.7',
         'django-rq',