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.
$ 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.
There is the one conceptual difference between django-photo-albums and photologue: the data model.
Image <- (Many To Many) <- Gallery [ <- (ManyToMany, FK) <- Object ]
or
Image <- (Many to Many) <- Object
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).
To add image gallery for your model you should complete following steps:
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.
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 %}
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 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).
Each view have at least 2 variables in context:
of variable is set in PhotoAlbumsSite constructor (here), default is 'object')
current_app: app name, 'albums' by default
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.
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 %}
variables in context. prev and next are id’s of previous and next (by image.order field) images in gallery.
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>
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>
image becomes main in gallery. Has form in context, it’s a form of type upload_form_class.
Has form in context, it’s a form of type upload_zip_form_class
Has image in context. Should have a form that do POST request to delete view on submit.
Views used by PhotoAlbumSite.
class ImageEditForm(forms.ModelForm):
class Meta:
model = AttachedImage
fields = ['caption']
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.
Override this in subclass to do something useful with files extracted from uploaded zip archive.
Params:
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.
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.
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)
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.
Bases: generic_utils.test_helpers.ViewTest
Issue tracker is here: http://bitbucket.org/kmike/django-photo-albums/issues/
Bug reports, feature requests, enhancement suggestions are always welcome.