diff options
Diffstat (limited to 'bn_django/photos')
-rw-r--r-- | bn_django/photos/__init__.py | 1 | ||||
-rw-r--r-- | bn_django/photos/models.py | 283 | ||||
-rw-r--r-- | bn_django/photos/templates/photos/base.html | 1 | ||||
-rw-r--r-- | bn_django/photos/templates/photos/gallery_detail.html | 61 | ||||
-rw-r--r-- | bn_django/photos/templates/photos/gallery_list.html | 56 | ||||
-rw-r--r-- | bn_django/photos/templates/photos/import_form.html | 59 | ||||
-rw-r--r-- | bn_django/photos/templates/photos/photo_detail.html | 96 | ||||
-rw-r--r-- | bn_django/photos/urls.py | 21 | ||||
-rw-r--r-- | bn_django/photos/views.py | 156 |
9 files changed, 734 insertions, 0 deletions
diff --git a/bn_django/photos/__init__.py b/bn_django/photos/__init__.py new file mode 100644 index 0000000..52af441 --- /dev/null +++ b/bn_django/photos/__init__.py @@ -0,0 +1 @@ +"""Strongly based on code from the stockphoto project (google it?)""" diff --git a/bn_django/photos/models.py b/bn_django/photos/models.py new file mode 100644 index 0000000..33b2e0a --- /dev/null +++ b/bn_django/photos/models.py @@ -0,0 +1,283 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ +import django.contrib.auth.models as auth +from django.conf import settings +from django.dispatch import dispatcher +from django.db.models import signals + +import os, os.path +import Image + +# Handle settings here +try: + STOCKPHOTO_BASE = settings.STOCKPHOTO_BASE.strip('/') +except AttributeError: + STOCKPHOTO_BASE='photos' + +try: + STOCKPHOTO_URL = settings.STOCKPHOTO_URL +except AttributeError: + STOCKPHOTO_URL='/photos' +if STOCKPHOTO_URL[-1] == '/': + STOCKPHOTO_URL=STOCKPHOTO_URL[:-1] + +try: + ADMIN_URL = settings.ADMIN_URL +except AttributeError: + ADMIN_URL='/admin' +if ADMIN_URL[-1] == '/': + ADMIN_URL=ADMIN_URL[:-1] + +# Create your models here. +class Gallery(models.Model): + title = models.CharField(_("title"), maxlength=80) + slug = models.SlugField(prepopulate_from=("title",)) + date = models.DateField(_("publication date"), auto_now_add=True) + created = models.ForeignKey(auth.User, + verbose_name=_("gallery created by") + ) + display_width = models.IntegerField( + _("width to display full images"), + default=640) + display_height = models.IntegerField( + _("height to display full images"), + default=480) + thumbnail_width = models.IntegerField( + _("width to display thumbnails"), + default=150) + thumbnail_height = models.IntegerField( + _("height to display thumbnails"), + default=100) + + class Meta: + get_latest_by = 'date' + + class Admin: + ordering = ['date'] + + def __str__(self): + return self.title + def get_absolute_url(self): + return "%s/%d/" % (STOCKPHOTO_URL, self.id) + def get_admin_url(self): + return "%s/photos/gallery/%d/" % (ADMIN_URL, self.id) + def was_published_today(self): + return self.date.date() == datetime.date.today() + + def first(self): + try: + return self.photo_set.all()[0] + except IndexError: + return None + def update_thumbs(self): + for photo in self.photo_set.all(): + photo.create_disp() + photo.create_thumb() + +class Photo(models.Model): + # import os, os.path, Image + image = models.ImageField(_("Photograph"), + upload_to= STOCKPHOTO_BASE + "/%Y/%m/%d/") + title = models.CharField(_("title"), maxlength=80) + desc = models.TextField(_("description"), blank=True) + gallery = models.ForeignKey(Gallery) + photographer = models.CharField(_("photographer"), maxlength=80, + blank=True) + date = models.DateField(_("date photographed"), blank=True, null=True) + extra = models.TextField(_("any extra information about the photo"), + blank=True) + class META: + get_latest_by = ['date'] + + class Admin: + ordering = ['date'] + + def __str__(self): + return self.title + + def delete_thumbnails(self): + """Remove thumbnail and display-sized images when deleting. + + This may fail if, for example, they don't exist, so it should + fail silently. It may not be a good idea to delete the + original, as the user may not understand that deleting it from + the gallery deletes it from the filesystem, so currently we + don't do that. + + """ + try: + os.unlink(self.thumbpath()) + except (IOError, OSError): + pass + try: + os.unlink(self.disppath()) + except (IOError, OSError): + pass + # Deleting the original might be a bad thing. + #os.unlink(self.fullpath()) + + def get_absolute_url(self): + return "%s/detail/%d/" % (STOCKPHOTO_URL, self.id) + + def get_admin_url(self): + return "%s/photos/photos/%d/" % (ADMIN_URL, self.id) + + def thumbpath(self): + """Path to the thumbnail + """ + photobase = self.image[len(STOCKPHOTO_BASE)+1:] + return os.path.join( settings.MEDIA_ROOT, STOCKPHOTO_BASE, + "cache", "thumbs", photobase) + + def thumburl(self): + """URL to the thumbnail + """ + photobase = self.image[len(STOCKPHOTO_BASE)+1:] + if settings.MEDIA_URL.endswith('/'): + return settings.MEDIA_URL + STOCKPHOTO_BASE + \ + "/cache/thumbs/" + photobase + return settings.MEDIA_URL + '/' + STOCKPHOTO_BASE + \ + "/cache/thumbs/" + photobase + + + def disppath(self): + photobase = self.image[len(STOCKPHOTO_BASE)+1:] + return os.path.join( settings.MEDIA_ROOT, STOCKPHOTO_BASE, + "cache", photobase) + + def dispurl(self): + photobase = self.image[len(STOCKPHOTO_BASE)+1:] + if settings.MEDIA_URL.endswith('/'): + return settings.MEDIA_URL + STOCKPHOTO_BASE + "/cache/" \ + + photobase + return settings.MEDIA_URL + '/' + STOCKPHOTO_BASE + \ + "/cache/" + photobase + + def fullpath(self): + if self.image.startswith(os.path.sep): + return self.image + return os.path.join(settings.MEDIA_ROOT, self.image) + + def fullurl(self): + if self.image.startswith(os.path.sep): + # Shouldn't happen anymore + return (settings.MEDIA_URL + + self.image[len(settings.MEDIA_ROOT):]) + else: + if settings.MEDIA_URL.endswith('/'): + return settings.MEDIA_URL + self.image + return settings.MEDIA_URL + '/' + self.image + + + def next(self): + '''Return id of 'next' photo in the same gallery or None if at + the end.''' + # we could probably be more clever here by using the new nifty + # db access filters and queries, but for now, this is good enough + photo_list = [x for x in self.gallery.photo_set.all()] + ind = photo_list.index(self) + if (ind +1) == len(photo_list): + return None + else: + return photo_list[ind + 1] + + def prev(self): + """Return id of 'previous' photo in the same gallery or None + if at the beginning + """ + photo_list = [x for x in self.gallery.photo_set.all()] + ind = photo_list.index(self) + if ind == 0: + return False + else: + return photo_list[ind - 1] + + def full_exists(self): + return os.path.exists( self.fullpath() ) + + def disp_exists(self): + return os.path.exists( self.disppath() ) + + def thumb_exists(self): + return os.path.exists( self.thumbpath() ) + + def create_disp(self): + im = Image.open( self.fullpath() ) + format = im.format + # create the path for the display image + disp_path = self.disppath() + disp_dir = os.path.dirname(disp_path) + if not os.path.exists(disp_dir): + os.makedirs(disp_dir, 0775) + + # Make a copy of the image, scaled, if needed. + maxwidth = self.gallery.display_width + maxheight = self.gallery.display_height + width, height = im.size + if (width > maxwidth) and width > height: + scale = float(maxwidth)/width + width = int(width * scale) + height = int(height * scale) + newim = im.resize( (width, height), Image.ANTIALIAS ) + elif (height > maxheight) and height >= width: + scale = float(maxheight)/height + width = int(width * scale) + height = int(height * scale) + newim = im.resize( (width, height), Image.ANTIALIAS ) + else: + newim = im + newim.save(disp_path, format) + + def create_thumb(self): + im = Image.open( self.fullpath() ) + format = im.format + # create the path for the thumbnail image + thumb_path = self.thumbpath() + thumb_dir = os.path.dirname(thumb_path) + if not os.path.exists(thumb_dir): + os.makedirs(thumb_dir, 0775) + + # Make a copy of the image, scaled, if needed. + maxwidth = self.gallery.thumbnail_width + maxheight = self.gallery.thumbnail_height + width, height = im.size + if (width > maxwidth) and (width > height): + scale = float(maxwidth)/width + width = int(width * scale) + height = int(height * scale) + newim = im.resize( (width, height), Image.ANTIALIAS ) + elif (height > maxheight): + scale = float(maxheight)/height + width = int(width * scale) + height = int(height * scale) + newim = im.resize( (width, height), Image.ANTIALIAS ) + else: + newim = im + newim.save(thumb_path, format) + + def build_display_images(self): + """Make thumbnail and display-sized images after saving. + + For some reason, this may fail on a first pass (self.image may + be empty when this is called), but if we just let it fail + silently, it will apparently get called again and succeed. + """ + if self.image: + if not self.thumb_exists(): + self.create_thumb() + if not self.disp_exists(): + self.create_disp() + +def build_display_images(sender, instance, signal, *args, **kwargs): + """Simple hook for save-after trigger + """ + instance.build_display_images() +def delete_thumbnails(sender, instance, signal, *args, **kwargs): + """Simple hook for pre-delete trigger. + """ + instance.delete_thumbnails() + +dispatcher.connect(build_display_images, signal=signals.post_save, + sender=Photo) +dispatcher.connect(delete_thumbnails, signal=signals.pre_delete, + sender=Photo) diff --git a/bn_django/photos/templates/photos/base.html b/bn_django/photos/templates/photos/base.html new file mode 100644 index 0000000..94d9808 --- /dev/null +++ b/bn_django/photos/templates/photos/base.html @@ -0,0 +1 @@ +{% extends "base.html" %} diff --git a/bn_django/photos/templates/photos/gallery_detail.html b/bn_django/photos/templates/photos/gallery_detail.html new file mode 100644 index 0000000..f9d1bd6 --- /dev/null +++ b/bn_django/photos/templates/photos/gallery_detail.html @@ -0,0 +1,61 @@ +{% extends "photos/base.html" %} +{# {% load markup %} #} + +{% block path %} + <a href="../">photos</a> » +{% if object %} + <a href="../{{ object.id }}">{{ object.title }}</a> +{% endif %} +{% endblock %} + +{% block title %} +{% if object %} +Gallery: {{ object.title }} +{% endif %} +{% endblock %} + +{% block content %} +{% if object %} +<br /> +{% if object.photo_set.count %} + +{% for item in object.photo_set.all %} + +<span class="photo_thumb"> + <a href="../detail/{{ item.id }}"> + <img src="{{ item.thumburl }}" + alt="{{ item.title }}" /> + <!--<a href="../detail/{{ item.id }}" class="caption">{{ item.title }}</a> +</a> --> + +</span> +{% endfor %} + +{% else %} +<p>There are no photos in this gallery. If you just uploaded a batch +of photos, try hitting your browser's reload button to see if they +show up.</p> +{% endif %} + +<!-- +{% if not user.is_anonymous %} +<p> + <a href="{{ object.get_admin_url }}">Edit this gallery.</a><br/> + <a href="{{ admin_url }}/photos/photo/add/">Add a photo to + this gallery.</a><br/> + <a href="{{photos_url}}/import/{{ object.id }}/">Add a bunch + of photos to this gallery.</a> +</p> +{% else %} +<p> + <a href="/accounts/login/?next={{ request.path }}"> + Login</a> to add or edit photos +</p> +{% endif %} +--> + +{% else %} +<p>This is not the gallery you are looking for.</p> +{% endif %} + +{% endblock %} diff --git a/bn_django/photos/templates/photos/gallery_list.html b/bn_django/photos/templates/photos/gallery_list.html new file mode 100644 index 0000000..61225c4 --- /dev/null +++ b/bn_django/photos/templates/photos/gallery_list.html @@ -0,0 +1,56 @@ +{% extends "photos/base.html" %} +{# {% load markup %} #} + +{% block path %}photos{% endblock %} +{% block title %}Photo Galleries{% endblock %} + +{% block content %} +{% if object_list %} +<div id="thumbnail-box"> + <div id="thumbnails"> + {% for item in object_list %} + <div class="lefty"> + + <a href="{{ item.id }}/"> + {% if item.first %} + <img src="{{ item.first.thumburl }}" + alt="{{ item.first.title }}" /> + {% else %} + No photo available + {% endif %} + </a> + </div> + <br /> <br /><a href="{{ item.id }}/"><h3> {{ item.title }}</h3></a> + <br /> <br /> <br /> <br /> + {% endfor %} + </div> +</div> +{% else %} +<p>No galleries have been set up yet.</p> +{% endif %} + + +{% if is_paginated %} + +{% if has_previous %} +<a href="./?page={{ previous }}">« previous</a> | +{% endif %} +{% if has_next %} +<a href="./?page={{ next }}">next »</a> +{% endif %} +{% endif %} + +<!-- +{% if not user.is_anonymous %} +<p> + <a href="{{admin_url}}/photos/gallery/add/">Create a new gallery.</a> +</p> +{% else %} +<p> + <a href="/accounts/login/?next={{ request.path }}"> + Login</a> to create a new gallery. +</p> +{% endif %} +--> + +{% endblock %} diff --git a/bn_django/photos/templates/photos/import_form.html b/bn_django/photos/templates/photos/import_form.html new file mode 100644 index 0000000..0d4f19e --- /dev/null +++ b/bn_django/photos/templates/photos/import_form.html @@ -0,0 +1,59 @@ +{% extends "photos/base.html" %} +{# {% load markup %} #} +{% block title %}Import photos into a gallery +{% if gallery%} ({{gallery.title}}){% endif %}{% endblock %} + +{% block content %} +{% if gallery %} + +<form action="../../import/{{ gallery.id }}/" method="post" + enctype="multipart/form-data"> + <div> + {%if form.zipfile.errors %} + <span style="color: red;"> + {{ form.zipfile.errors|join:", " }} + </span><br/> + {% endif %} + + <label class="fortextinput" for="id_zipfile"> + ZIP archive to upload: + </label> + {{ form.zipfile }}<br/><br/> + + {%if form.photographer.errors %} + <span style="color: red;"> + {{ form.photographer.errors|join:", " }} + </span><br/> + {% endif %} + + <label class="fortextinput" for="id_photographer"> + Name of photographer: + </label> + {{ form.photographer }}<br/><br/> + + {%if form.date.errors %} + <span style="color: red;"> + {{ form.date.errors|join:", " }} + </span><br/> + {% endif %} + + <label class="fortextinput" for="id_date"> + Date photos were taken: + </label> + {{ form.date }}<br/><br/> + <input type="submit" value="Upload"/> + </div> +</form> +<p> + When you upload a batch of photos in a zipfile, it will give each of + them a title based on its filename, and assigns them all the same + photographer and date. That's probably not always what you want, so + you can change any of these settings on a per-photo basis + <em>after</em> you upload the images. +</p> + +{% else %} +<p>Oops! No gallery here!</p> +{% endif %} + +{% endblock %} diff --git a/bn_django/photos/templates/photos/photo_detail.html b/bn_django/photos/templates/photos/photo_detail.html new file mode 100644 index 0000000..d9c2177 --- /dev/null +++ b/bn_django/photos/templates/photos/photo_detail.html @@ -0,0 +1,96 @@ +{% extends "photos/base.html" %} +{# {% load markup %} #} + +{% block path %} + <a href="../..">photos</a> » + <a href="../../{{ object.gallery.id }}">{{ object.gallery.title }}</a> +{% endblock %} + +{% block title %} +{% if object %} +{{ object.title }} +{% endif %} +{% endblock %} + +{% block content %} + +{% if object %} +{% if object.next %} +<div class="right_stuff"> +<div class="small-image-box"> +<br /> <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br /> + <a href="../{{ object.next.id }}/"> + Next:<br/> + <img src="{{ object.next.thumburl}}" alt="{{ object.next.title }}"/> + </a> <!-- + <a href="../{{ object.next.id }}/"> + <span>{{ object.next.title }} »</span></a> --> +</div></div> + +{% endif %} + +{% if object.prev %} +<div class="right_stuff"> +<div class="small-image-box"> + <a href="../{{ object.prev.id }}/"> + Previous:<br /> + <img src="{{ object.prev.thumburl}}" alt="{{ object.prev.title }}"/> + </a> <!-- + <a href="../{{ object.prev.id }}/"> + <span>« {{ object.prev.title }} </span></a> --> +</div> +</div> +{% endif %} +<center> +<div id="centerize"> + <a href="{{ object.fullurl }}"> + <img src="{{ object.dispurl }}" + alt="{{ object.title }}" /> + </a> + <div> + <p class="photodesc"> {{ object.desc }} </p> + {% if object.photographer %} + <p class="photographer">Photo by {{ object.photographer }}.</p> + {% endif %} + {% if object.date %} + <p class="photodate">Photo taken {{ object.date }}.</p> + {% endif %} + {% if object.extra %} + <h3 class="photoextra">More information</h3> + <p class="photoextra">{{ object.extra }}</p> + {% endif %} + </div> +</div> +</center> +{% if object.prev %} + <a href="../{{ object.prev.id }}/" class="lefty"> + « previous + </a> +{% endif %} +{% if object.next %} + <a href="../{{ object.next.id }}/" class="righty"> + next » + </a> +{% endif %} +<br /> + +<!-- +{% if not user.is_anonymous %} +<p> + <a href="{{ admin_url }}/photos/photo/{{ object.id }}/"> + Edit this image. + </a> +</p> +{%else %} +<p> + <a href="/accounts/login/?next={{ request.path }}"> + Login</a> to edit this image. +</p> +{% endif %} +--> +{% else %} +<p>This is not the photo you are looking for.</p> +{% endif %} + + +{% endblock %} diff --git a/bn_django/photos/urls.py b/bn_django/photos/urls.py new file mode 100644 index 0000000..a413c3f --- /dev/null +++ b/bn_django/photos/urls.py @@ -0,0 +1,21 @@ +from django.conf.urls.defaults import * +from django.conf import settings + +from models import Gallery, Photo, ADMIN_URL, STOCKPHOTO_URL + +info_dict = { 'extra_context': { 'admin_url': ADMIN_URL, + 'stockphoto_url': STOCKPHOTO_URL} } + +urlpatterns = patterns('django.views.generic.list_detail', + (r'^$', 'object_list', + dict(info_dict, queryset=Gallery.objects.all(), + paginate_by= 10, allow_empty= True)), + (r'^(?P<object_id>\d+)/$', 'object_detail', + dict(info_dict, queryset=Gallery.objects.all())), + (r'^detail/(?P<object_id>\d+)/$', 'object_detail', + dict(info_dict, queryset=Photo.objects.all())), +) +urlpatterns += patterns('bn_django.photos.views', + (r'^import/(\d+)/$', 'import_photos'), + (r'^export/(\d+)/$', 'export'), +) diff --git a/bn_django/photos/views.py b/bn_django/photos/views.py new file mode 100644 index 0000000..7cc5332 --- /dev/null +++ b/bn_django/photos/views.py @@ -0,0 +1,156 @@ +# Create your views here. + +# django imports +from django.conf import settings +from django import forms, http, template +from django.contrib.auth.decorators import login_required +from django.shortcuts import get_object_or_404, render_to_response +from django.http import HttpResponse + +# other imports +import zipfile +import os +import stat +import shutil +from datetime import datetime +from tempfile import NamedTemporaryFile, mkdtemp +import Image +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +# Handling settings here +try: + STOCKPHOTO_BASE = settings.STOCKPHOTO_BASE.strip('/') +except AttributeError: + STOCKPHOTO_BASE = 'photos' + +# models +from bn_django.photos.models import Gallery, Photo + +# views + +class ImportManipulator(forms.Manipulator): + def __init__(self): + self.fields = ( + forms.FileUploadField(field_name="zipfile", + is_required=True, + validator_list=[self.valid_zipfile,]), + forms.TextField(field_name="photographer"), + forms.DateField(field_name="date"), + ) + def valid_zipfile(self, field_data, all_data): + zip_file = StringIO(field_data['content']) + zip = zipfile.ZipFile(zip_file) + return not zip.testzip() + + + +@login_required +def import_photos(request, thegallery): + """Import a batch of photographs uploaded by the user. + + Import a batch of photographs uploaded by the user, all with + the same information for gallery, photographer and date. The + title will be set from the filename, and the description will be + blank. Self-respecting photographers will edit the fields for + each photograph; this is just a way to get a bunch of photographs + uploaded quickly. + + The photographs should be wrapped up in a zip archive. The + archive will be unpacked (and flattened) in a temporary directory, + and all image files will be identified and imported into the + gallery. Other files in the archive will be silently ignored. + + After importing the images, the view will display a page which may + contain the number of images imported, and a link to the gallery + into which the images were imported. + """ + # Check if the gallery is valid + gallery = get_object_or_404(Gallery, pk=thegallery) + # And that the user has permission to add photos + if not request.user.has_perm('gallery.add_photo'): + return http.HttpResponseForbidden("No permission to add photos") + + manipulator = ImportManipulator() + if request.POST: + new_data = request.POST.copy() + new_data.update(request.FILES) + errors = manipulator.get_validation_errors(new_data) + if not errors: + # So now everything is okay + f = StringIO(new_data['zipfile']['content']) # the zip"file" + zip = zipfile.ZipFile(f) + manipulator.do_html2python(new_data) + date = new_data['date'] + if not date: + date = datetime.date(datetime.now()) + + destdir= os.path.join(settings.MEDIA_ROOT, STOCKPHOTO_BASE, + datetime.strftime(datetime.now(), + "%Y/%m/%d/")) + if not os.path.isdir(destdir): + os.makedirs(destdir, 0775) + for filename in zip.namelist(): + photopath = os.path.join(destdir, os.path.basename(filename)) + data = zip.read(filename) + file_data = StringIO(data) + try: + Image.open(file_data) + except: + # don't save and process non Image files + continue + photo = file(photopath, "wb") + photo.write(data) + + # Create the object + if photopath.startswith(os.path.sep): + photopath = photopath[len(settings.MEDIA_ROOT):] + photo = Photo(image=photopath, date=date, + photographer=new_data['photographer'], + title = os.path.basename(filename), + gallery_id = thegallery) + # Save it -- the thumbnails etc. get created. + photo.save() + + # And jump to the directory for this gallery + response = http.HttpResponseRedirect(gallery.get_absolute_url()) + response['Pragma'] = 'no cache' + response['Cache-Control'] = 'no-cache' + return response + else: + errors = new_data = {} + form = forms.FormWrapper(manipulator, new_data, errors) + return render_to_response('photos/import_form.html', + dict(form=form, gallery=gallery)) + # request, + +@login_required +def export(request, thegallery): + """Export a gallery to a zip file and send it to the user. + """ + # Check if the gallery is valid + gallery = get_object_or_404(Gallery, pk=thegallery) + + # gather up the photos into a new directory + tmpdir = mkdtemp() + for photo in gallery.photo_set.all(): + shutil.copy(photo.get_image_filename(), + tmpdir) + files = [ os.path.join(tmpdir, ff) for ff in os.listdir(tmpdir) ] + outfile = NamedTemporaryFile() + zf = zipfile.ZipFile(outfile, "w", + compression=zipfile.ZIP_DEFLATED) + for filename in files: + zf.write(filename, arcname=os.path.basename(filename)) + zf.close() + outfile.flush() + outfile.seek(0) + shutil.rmtree(tmpdir) + response = HttpResponse(outfile) + response['Content-Type'] = "application/zip" + response['Content-Length'] = str(os.stat(outfile.name)[stat.ST_SIZE]) + response['Content-Disposition'] = "attachment; filename=photos.zip" + return response + |