django-photo-albums’s documentation

django-photo-albums is a pluggable django image gallery app.

Image galleries can be attached to any Django model. And thanks to django 1.1 url namespaces it is possible to have multiple ‘albums’ app instances (for example, for different models) that use different sets of templates, different permission rules, have dedicated integration test suites and are available from different urls.

Each image gallery provide functionality for image viewing, editing, uploading, uploading entire albums in one zip file, reordering, marking/unmarking as main and deleting.

django-photo-albums is an application based on django-generic-images . django-photo-albums requires Django >= 1.1 (or svn version with url namespaces), setuptools for installation, django-annoying for some utils and django-generic-images for image management and advanced admin image uploader. django-generic-images and django-annoying will be installed automatically if you install django-photo-albums via easy_install or pip.

django-photo-albums does not provide any thumbnail creation solution because there are external django apps (such as sorl-thumbnail) that would do this better.

Testing if app instance is integrated correctly (at least that templates don’t raise exceptions) is easy because base class for integration testcases is provided.

Installation

$ pip install django-photo-albums

or:

$ easy_install django-photo-albums

or:

$ hg clone http://bitbucket.org/kmike/django-photo-albums/
$ cd django-photo-albums
$ python setup.py install

Then add ‘photo_albums’ and ‘generic_images’ to your INSTALLED_APPS in settings.py and run ./manage.py syncdb (syncdb is not needed if django-generic-images was already installed).

Note: django-generic-images app provides admin image uploader (see more in django-generic-images docs ). For this admin uploader to work generic_images folder from generic_images/media/ should be copied to project’s MEDIA_ROOT.

Note: django-composition is required if you want to use ImageCountField or UserImageCountField. Run pip install django-composition to install django-composition.

Why another photo albums app? Сomparison with pinax’s photologue

There is the one conceptual difference between django-photo-albums and photologue: the data model.

Photologue data model

Image <- (Many To Many) <- Gallery [ <- (ManyToMany, FK) <- Object ]

or

Image <- (Many to Many) <- Object

django-photo-albums’ data model (provided by django-generic-images)

Image -> (GFK) -> Object

Several galleries for one object can also be implemented by introducing custom MyGallery model:

Image -> (GFK) -> MyGallery -> (FK, GFK) -> Object

This way images and galleries can be attached to any model and there is no need to change model to attach images or albums to it.

Please note that there is small performance penalty for extra flexibility provided by using generic foreign keys (1 extra query while selecting all images for an object + 1 extra join with contenttypes table).

Basic use

PhotoAlbumSite

To add image gallery for your model you should complete following steps:

  1. Create album site instance and plug it’s urls to urlconf:

    from photo_albums.urls import PhotoAlbumSite
    accounts_photo_site = PhotoAlbumSite(instance_name = 'user_images',
                             queryset = User.objects.all(),
                             template_object_name = 'album_user',
                             has_edit_permission = lambda request, obj: request.user==obj)
    urlpatterns += patterns('', url(r'^accounts/', include(accounts_photo_site.urls)),)
    

Please note that if you deploy multiple albums (ex. for different models), you must provide unique instance_name for each instance to make url reversing work.

Included urls looks like <object_id>/<app_name>/<action> or <object_id>/<app_name>/<image_id>/<action>, where object_id is the id of object which is gallery attached to, app_name is “album” by default (you can change it here), image_id is image id :-) and action is the performed action (view, edit, etc). It is possible to use slug instead of object’s id (look at object_regex and lookup_field parameters).

It is also possible to attach PhotoAlbumSite to any url using object_getter parameter.

  1. Create the necessary templates.
  2. Link people to image gallery using {% url .. %} template tags.

You can use these urls (assuming that user_images is an instance name, album_user is the object for which gallery is attached to, image is an image in gallery and slugs are not used):

{% url user_images:show_album album_user.id %}

{% url user_images:edit_album album_user.id %}

{% url user_images:upload_main_image album_user.id %}

{% url user_images:upload_images album_user.id %}

{% url user_images:upload_zip album_user.id %}

{% url user_images:show_image album_user.id image.id %}

{% url user_images:edit_image album_user.id image.id %}

{% url user_images:delete_image album_user.id image.id %}

{% url user_images:set_as_main_image album_user.id image.id %}

{% url user_images:clear_main_image album_user.id image.id %}

{% url user_images:reorder_images album_user.id %}

{% url user_images:set_image_order album_user.id %}
class photo_albums.urls.PhotoAlbumSite

Constructor parameters:

instance_name: String. Required. App instance name for url reversing. Must be unique.

queryset: QuerySet. Required. Albums will be attached to objects in this queryset.

object_regex: String. Optional, default is '\d+'. It should be a URL regular expression for object in URL. You should use smth. like '[\w\d-]+' for slugs.

lookup_field: String. Optional, default is 'pk'. It is a field name to lookup. It may contain __ and follow relations (ex.: userprofile__slug).

app_name: String. Optional, default value is 'album'. Used by url namespaces stuff.

extra_context: Dict. Optional. Extra context that will be passed to each view.

template_object_name: String. Optional. The name of template context variable with object for which album is attached. Default is 'object'.

has_edit_permission: Optional. Function that accepts request and object and returns True if user is allowed to edit album for object and False otherwise. Default behaviour is to always return True.

context_processors: Optional. A list of callables that will be used as additional context_processors in each view.

object_getter: special function that returns object that PhotoAlbumSite is attached to. It is special because it must have explicitly assigned ‘regex’ attribute. This regex will be passed to django URL system. Parameters from this regex will be then passed to object_getter function.

Example:

def get_place(city_slug, place_slug):
    return Place.objects.get(city__slug=city_slug, slug=place_slug)
get_place.regex = r'(?P<city_slug>[\w\d-]+)/(?P<place_slug>[\w\d-]+)'

edit_form_class: Optional, default is ImageEditForm. ModelForm subclass to be used in edit_image() view.

upload_form_class: Optional, default is AttachedImageForm (defined in generic_images.forms module). ModelForm subclass to be used in upload_main_image() view.

upload_formset_class: Optional, default is PhotoFormSet. ModelFormSet to be used in upload_images() view.

upload_zip_form_class: Optional, default is UploadZipAlbumForm. Form to be used in upload_zip() view.

Templates used by django-photo-albums

Templates usually should be placed in templates/albums/<app_name>/ folder. App_name should be the name of queryset model’s app as it appears in contenttypes table (e.g. ‘auth’ for User). It is possible to override templates per-model (by placing them in templates/albums/<app_name>/<model_name>/ folder) or to have a kind of default fallback templates for several apps (by placing them in templates/albums/ folder).

Common context

Each view have at least 2 variables in context:

  • <template_object_name>: object for which gallery is attached to (the name

    of variable is set in PhotoAlbumsSite constructor (here), default is 'object')

  • current_app: app name, 'albums' by default

Templates

The views included in django-photo-albums make use of these 9 templates:

  • show_album.html displays entire album

  • edit_album.html displays entire album. Used by edit_album view.

  • reorder_images.html displays entire album. Used by reorder_images view.

    These 3 templates have images variable in context with iterable of all images in gallery.

Example:

{% for image in images %}
    <img src='{{ image.image }}' alt='{{image.caption}}'>
{% endfor %}

With sorl-thumbnail:

{% for image in images %}
    <img src='{% thumbnail image.image 100x50 %}' alt='{{ image.caption }}'>
{% endfor %}
  • show_image.html - displays one image. Has image, prev and next

    variables in context. prev and next are id’s of previous and next (by image.order field) images in gallery.

  • edit_image.html - displays one image for editing purposes. Has form,

    image, prev and next variables in context. prev and next are id’s of previous and next (by image.order field) images in gallery. form is a form of edit_form_class class.

Example:

<img src='{{ image.image }}' alt='{{image.caption}}'>

<a href='{% url user_images:edit_image album_user.id prev %}'>previous image</a>
<a href='{% url user_images:edit_image album_user.id next %}'>next image</a>

<form action='' method='POST'>
    {{ form }}
    <input type='submit' value='Save'>
</form>
  • upload_images.html - displays the formset for bulk image upload.

    Formset is of upload_formset_class class and is available as formset context variable.

Example:

<form action="" method="POST" enctype="multipart/form-data">
    {{ formset }}
    <input type="submit" value="Upload images">
</form>
  • upload_main_image.html - displays form for uploading one image. Uploaded

    image becomes main in gallery. Has form in context, it’s a form of type upload_form_class.

  • upload_zip.html - displays form for uploading zip archive with images.

    Has form in context, it’s a form of type upload_zip_form_class

  • confirm_delete.html - displays confirmation dialog for deleting image.

    Has image in context. Should have a form that do POST request to delete view on submit.

Views

Views used by PhotoAlbumSite.

photo_albums.views.get_prepared_errors(form)
photo_albums.views.show_album(request, **kwargs)
Show album for object using show_album.html template
photo_albums.views.show_image(request, **kwargs)
Show one image
photo_albums.views.clear_main_image
Mark image as not main and redirect to show_image view
photo_albums.views.delete_image
Delete image if request method is POST, displays confirm_delete.html template otherwise
photo_albums.views.edit_album
Show album for object using edit_album.html template, with permission checks.
photo_albums.views.edit_image
Show one image. Checks permissions and provides edit form.
photo_albums.views.set_as_main_image
Mark image as main and redirect to show_image view

Forms

class photo_albums.forms.ImageEditForm
class ImageEditForm(forms.ModelForm):
    class Meta:
        model = AttachedImage
        fields = ['caption']
class photo_albums.forms.PhotoFormSet
modelformset_factory(AttachedImage, extra=3, fields = ['image', 'caption'])
class photo_albums.forms.UploadZipForm

A base form class for uploading several files packed as one .zip file. Extract files and provides hook for processing extracted files. During extraction it loads uncompressed files to memory by chunks so it is safe to process zip archives with big files inside.

clean_zip_file()
Checks if zip file is not corrupted, stores in-memory uploaded file to disk and returns path to stored file.
needs_unpacking(name, info)
Returns True is file should be extracted from zip and False otherwise. Override in subclass to customize behaviour. Default is to unpack all files except directories and meta files (names starts with ‘__’) .
process_file(path, name, info, file_num, files_count)

Override this in subclass to do something useful with files extracted from uploaded zip archive.

Params:

  • path: path to temporary file. It’s on developer to delete this file.
  • name: name of file in zip archive, returned by ZipFile.namelist()
  • info: file info, returned by ZipFile.infolist()
  • file_num: file’s order number
  • files_count: total files count
process_zip_file(chunksize=65536)

Extract all files to temporary place and call process_file method for each.

chunksize is the size of block in which compressed files are read. Default is 64k. Do not set it below 64k because data from compressed files will be read in blocks >= 64k anyway.

class photo_albums.forms.UploadZipAlbumForm(user, obj, *args, **kwargs)

Bases: photo_albums.forms.UploadZipForm

Form for uploading several images packed as one .zip file. Only valid images are stored. Uploaded images are marked as uploaded by user and are attached to obj model.

is_valid_image(path)
Check if file is readable by PIL.
needs_unpacking(name, info)
Returns True is file should be extracted from zip and False otherwise. Override in subclass to customize behaviour. Default is to skip directories, meta files (names starts with '__') and files with non-image extensions.
process_file(path, name, info, file_num, files_count)
Create AttachedImage instance if file is a valid image.

Example:

if request.method == 'POST':
    form = UploadZipAlbumForm(request.user, obj, request.POST, request.FILES)
    if form.is_valid():
        form.process_zip_file()
        success_url = album_site.reverse('show_album', args=[object_id])
        return HttpResponseRedirect(success_url)
else:
    form = UploadZipAlbumForm(request.user, obj)

Testing

Integration testing

django-photo-albums provides base class (photo_albums.test_utils.AlbumTest) for writing integration tests for app instances.

The example usage:

from accounts.urls import accounts_photo_site
from photo_albums import test_utils

class UserAlbumTest(test_utils.AlbumTest):
    # existing user's data
    username = 'obiwanus' 
    password = 'vasia'
    
    # fixtures to be loaded (at least with users, images and 
    # objects with galleries)
    fixtures = ['my_fixtures']
    
    # app instance which is to be tested
    album_site = accounts_photo_site
    
    # we don't need edit_image view and don't create template for it
    # so it should be excluded from testing
    excluded_views = ['edit_image']
    
    # id of object for which album is attached
    album_for_id = 4
                
    # if slugs are in use:
    # album_for_id = 'my_object_slug'
    
    # if object_getter is in use:
    # album_for_kwargs = {'year': 2009, 'month': 12, 'day': 5, 'slug': 'wow'}                        
    
    # id's of various images: 2 images in album (second is nedded if you
    # want to test reordering) and one image in other album to test
    # permission checks
    image_in_album_id = 48
    image2_in_album_id = 66
    image_in_other_album_id = 42    

If you don’t use fixtures you can override setUp method and create necessery objects there.

class photo_albums.test_utils.AlbumTest(*args, **kwargs)

Bases: generic_utils.test_helpers.ViewTest

check(view, status, kwargs=None)
test_auth_views()
test_forbidden_views()
test_public_views()
test_reorder()
album_for_id
object id (or slug) for which album is attached to
album_for_kwargs
object url resolver kwargs (for more complex object urls)
album_site
PhotoAlbumSite instance to be tested
excluded_views
a list of names of excluded views. Excluded views won’t be tested.
fixtures
fixtures to be loaded (at least with users, images and objects with galleries)
image2_in_album_id
image_in_album_id
image_in_other_album_id
id’s of various images: 2 images in album (second is nedded if tou want to test reordering) and one image in other album to test permission checks
password
existing user’s password
username
existing user’s username

Bugs

Issue tracker is here: http://bitbucket.org/kmike/django-photo-albums/issues/

Bug reports, feature requests, enhancement suggestions are always welcome.