This is a quick demo of the Photologue application - just click on
12 | the menu options above.
13 |
It uses the built-in Bootstrap-compatible
14 | templates that you can use to get you quickly up and running, or completely
15 | replace with your own templates.
39 | {% if form.errors|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}
40 |
41 | {{ form.non_field_errors }}
42 | {% endif %}
43 |
44 |
56 |
57 | {% endblock %}
58 |
--------------------------------------------------------------------------------
/photologue/tests/test_views_gallery.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase, override_settings
2 |
3 | from .factories import GalleryFactory
4 |
5 |
6 | @override_settings(ROOT_URLCONF='photologue.tests.test_urls')
7 | class RequestGalleryTest(TestCase):
8 |
9 | def setUp(self):
10 | super().setUp()
11 | self.gallery = GalleryFactory(slug='test-gallery')
12 |
13 | def test_archive_gallery_url_works(self):
14 | response = self.client.get('/ptests/gallery/')
15 | self.assertEqual(response.status_code, 200)
16 |
17 | def test_archive_gallery_empty(self):
18 | """If there are no galleries to show, tell the visitor - don't show a
19 | 404."""
20 |
21 | self.gallery.is_public = False
22 | self.gallery.save()
23 |
24 | response = self.client.get('/ptests/gallery/')
25 | self.assertEqual(response.status_code, 200)
26 |
27 | self.assertEqual(response.context['latest'].count(),
28 | 0)
29 |
30 | def test_paginated_gallery_url_works(self):
31 | response = self.client.get('/ptests/gallerylist/')
32 | self.assertEqual(response.status_code, 200)
33 |
34 | def test_gallery_works(self):
35 | response = self.client.get('/ptests/gallery/test-gallery/')
36 | self.assertEqual(response.status_code, 200)
37 |
38 | def test_archive_year_gallery_works(self):
39 | response = self.client.get('/ptests/gallery/2011/')
40 | self.assertEqual(response.status_code, 200)
41 |
42 | def test_archive_month_gallery_works(self):
43 | response = self.client.get('/ptests/gallery/2011/12/')
44 | self.assertEqual(response.status_code, 200)
45 |
46 | def test_archive_day_gallery_works(self):
47 | response = self.client.get('/ptests/gallery/2011/12/23/')
48 | self.assertEqual(response.status_code, 200)
49 |
50 | def test_detail_gallery_works(self):
51 | response = self.client.get('/ptests/gallery/2011/12/23/test-gallery/')
52 | self.assertEqual(response.status_code, 200)
53 |
54 | def test_redirect_to_list(self):
55 | """Trivial test - if someone requests the root url of the app
56 | (i.e. /ptests/'), redirect them to the gallery list page."""
57 | response = self.client.get('/ptests/')
58 | self.assertRedirects(response, '/ptests/gallery/', 301, 200)
59 |
--------------------------------------------------------------------------------
/photologue/tests/test_gallery.py:
--------------------------------------------------------------------------------
1 | from .. import models
2 | from .factories import GalleryFactory, PhotoFactory
3 | from .helpers import PhotologueBaseTest
4 |
5 |
6 | class GalleryTest(PhotologueBaseTest):
7 |
8 | def setUp(self):
9 | """Create a test gallery with 2 photos."""
10 | super().setUp()
11 | self.test_gallery = GalleryFactory()
12 | self.pl2 = PhotoFactory()
13 | self.test_gallery.photos.add(self.pl)
14 | self.test_gallery.photos.add(self.pl2)
15 |
16 | def tearDown(self):
17 | super().tearDown()
18 | self.pl2.delete()
19 |
20 | def test_public(self):
21 | """Method 'public' should only return photos flagged as public."""
22 | self.assertEqual(self.test_gallery.public().count(), 2)
23 | self.pl.is_public = False
24 | self.pl.save()
25 | self.assertEqual(self.test_gallery.public().count(), 1)
26 |
27 | def test_photo_count(self):
28 | """Method 'photo_count' should return the count of the photos in this
29 | gallery."""
30 | self.assertEqual(self.test_gallery.photo_count(), 2)
31 | self.pl.is_public = False
32 | self.pl.save()
33 | self.assertEqual(self.test_gallery.photo_count(), 1)
34 |
35 | # Method takes an optional 'public' kwarg.
36 | self.assertEqual(self.test_gallery.photo_count(public=False), 2)
37 |
38 | def test_sample(self):
39 | """Method 'sample' should return a random queryset of photos from the
40 | gallery."""
41 |
42 | # By default we return all photos from the gallery (but ordered at random).
43 | _current_sample_size = models.SAMPLE_SIZE
44 | models.SAMPLE_SIZE = 5
45 | self.assertEqual(len(self.test_gallery.sample()), 2)
46 |
47 | # We can state how many photos we want.
48 | self.assertEqual(len(self.test_gallery.sample(count=1)), 1)
49 |
50 | # If only one photo is public then the sample cannot have more than one
51 | # photo.
52 | self.pl.is_public = False
53 | self.pl.save()
54 | self.assertEqual(len(self.test_gallery.sample(count=2)), 1)
55 |
56 | self.pl.is_public = True
57 | self.pl.save()
58 |
59 | # We can limit the number of photos by changing settings.
60 | models.SAMPLE_SIZE = 1
61 | self.assertEqual(len(self.test_gallery.sample()), 1)
62 |
63 | models.SAMPLE_SIZE = _current_sample_size
64 |
--------------------------------------------------------------------------------
/docs/pages/usage.rst:
--------------------------------------------------------------------------------
1 | #####
2 | Usage
3 | #####
4 |
5 | Now that you've installed Photologue, here are a few suggestions on how to use it:
6 |
7 | Upload some photos in the admin
8 | -------------------------------
9 | The ``Photo`` model in the admin allows you to add new photos to Photologue. You can add photos one by one - and
10 | it the top-right corner there is a 'Upload a Zip archive' button that will allow you to upload many photos at once.
11 |
12 | Define some Photosizes
13 | ----------------------
14 | Photologue will create thumbnails of the photos that you upload, and the thumbnails are what is displayed in the
15 | public website. By default Photologue comes with a few Photosizes to get you started - feel free to tweak them, or
16 | to create new ones.
17 |
18 | Just note that the ``admin_thumbnail`` size is used by the admin pages, so it's not a good idea to delete it!
19 |
20 | Built-in pages and templates
21 | ----------------------------
22 |
23 | If you've followed all the instructions in the installation page, you will have included Photologue's
24 | urls at ``/photologue/`` - you can use these, tweak them, or discard them if they do not fit in with your website's
25 | requirements.
26 |
27 | Custom usage
28 | ------------
29 | The base of Photologue is the ``Photo`` model. When an instance is created, we automatically add methods to retrieve
30 | photos at various photosizes. E.g. if you have an instance of ``Photo`` called ``photo``, then the
31 | following methods will have been added automatically::
32 |
33 | photo.get_thumbnail_url()
34 | photo.get_display_url()
35 | photo.get_admin_thumbnail_url()
36 |
37 | These can be used in a custom template to display a thumbnail, e.g.::
38 |
39 |
40 |
41 |
42 |
43 | This will display an image, sized to the dimensions specified in the Photosize ``display``,
44 | and provide a clickable link to the raw image. Please refer to the example templates for ideas on how to use
45 | ``Photo`` and ``Gallery`` instances!
46 |
47 | Data integrity
48 | --------------
49 | Photologue will store 'as-is' any data stored for galleries and photos.
50 | You may want to enforce some data integrity rules - e.g. to sanitise
51 | any javascript injected into a ``Photo`` ``caption`` field. An easy way to do this
52 | would be to add extra processing on a ``post-save`` signal.
53 |
54 | Photologue does not sanitise data itself as you may legitimately want to store html and
55 | javascript in a caption field e.g. if you use a rich-text editor.
56 |
57 |
--------------------------------------------------------------------------------
/docs/pages/customising/templates.rst:
--------------------------------------------------------------------------------
1 | ##################################
2 | Customisation: extending templates
3 | ##################################
4 |
5 | Photologue comes with a set of basic templates to get you started quickly - you
6 | can of course replace them with your own. That said, it is possible to extend the basic templates in
7 | your own project and override various blocks, for example to add css classes.
8 | Often this will be enough.
9 |
10 | The trick to extending the templates is not special to Photologue, it's used
11 | in other projects such as `Oscar `_.
12 |
13 | First, set up your template configuration as so:
14 |
15 | .. code-block:: python
16 |
17 | # for Django versions < 1.8
18 | TEMPLATE_LOADERS = (
19 | 'django.template.loaders.filesystem.Loader',
20 | 'django.template.loaders.app_directories.Loader',
21 | )
22 |
23 | from photologue import PHOTOLOGUE_APP_DIR
24 | TEMPLATE_DIRS = (
25 | ...other template folders...,
26 | PHOTOLOGUE_APP_DIR
27 | )
28 |
29 | # for Django versions >= 1.8
30 | TEMPLATES = [
31 | {
32 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
33 | 'DIRS': [os.path.join(BASE_DIR, 'templates')],
34 | # note: if you set APP_DIRS to True, you won't need to add 'loaders' under OPTIONS
35 | # proceeding as if APP_DIRS is False
36 | 'APP_DIRS': False,
37 | 'OPTIONS': {
38 | 'context_processors': [
39 | ... context processors ...,
40 | ],
41 | # start - please add only if APP_DIRS is False
42 | 'loaders': [
43 | 'django.template.loaders.filesystem.Loader',
44 | 'django.template.loaders.app_directories.Loader',
45 | ],
46 | # end - please add only if APP_DIRS is False
47 | },
48 | },
49 | ]
50 |
51 | The ``PHOTOLOGUE_APP_DIR`` points to the directory above Photologue's normal
52 | templates directory. This means that ``path/to/photologue/template.html`` can also
53 | be reached via ``templates/path/to/photologue/template.html``.
54 |
55 | For example, to customise ``photologue/gallery_list.html``, you can have an implementation like:
56 |
57 | .. code-block:: html+django
58 |
59 | # Create your own photologue/gallery_list.html
60 | {% extends "templates/photologue/gallery_list.html" %}
61 |
62 | ... we are now extending the built-in gallery_list.html and we can override
63 | the content blocks that we want to customise ...
64 |
65 |
66 |
--------------------------------------------------------------------------------
/photologue/tests/test_views_photo.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase, override_settings
2 |
3 | from ..models import Photo
4 | from .factories import PhotoFactory
5 |
6 |
7 | @override_settings(ROOT_URLCONF='photologue.tests.test_urls')
8 | class RequestPhotoTest(TestCase):
9 |
10 | def setUp(self):
11 | super().setUp()
12 | self.photo = PhotoFactory(slug='fake-photo')
13 |
14 | def tearDown(self):
15 | super().tearDown()
16 | self.photo.delete()
17 |
18 | def test_archive_photo_url_works(self):
19 | response = self.client.get('/ptests/photo/')
20 | self.assertEqual(response.status_code, 200)
21 |
22 | def test_archive_photo_empty(self):
23 | """If there are no photo to show, tell the visitor - don't show a
24 | 404."""
25 |
26 | Photo.objects.all().update(is_public=False)
27 |
28 | response = self.client.get('/ptests/photo/')
29 | self.assertEqual(response.status_code, 200)
30 |
31 | self.assertEqual(response.context['latest'].count(),
32 | 0)
33 |
34 | def test_paginated_photo_url_works(self):
35 | response = self.client.get('/ptests/photolist/')
36 | self.assertEqual(response.status_code, 200)
37 |
38 | def test_photo_works(self):
39 | response = self.client.get('/ptests/photo/fake-photo/')
40 | self.assertEqual(response.status_code, 200)
41 |
42 | def test_archive_year_photo_works(self):
43 | response = self.client.get('/ptests/photo/2011/')
44 | self.assertEqual(response.status_code, 200)
45 |
46 | def test_archive_month_photo_works(self):
47 | response = self.client.get('/ptests/photo/2011/12/')
48 | self.assertEqual(response.status_code, 200)
49 |
50 | def test_archive_day_photo_works(self):
51 | response = self.client.get('/ptests/photo/2011/12/23/')
52 | self.assertEqual(response.status_code, 200)
53 |
54 | def test_detail_photo_works(self):
55 | response = self.client.get('/ptests/photo/2011/12/23/fake-photo/')
56 | self.assertEqual(response.status_code, 200)
57 |
58 | def test_detail_photo_xss(self):
59 | """Check that the default templates handle XSS."""
60 | self.photo.title = ''
61 | self.photo.caption = ''
62 | self.photo.save()
63 | response = self.client.get('/ptests/photo/2011/12/23/fake-photo/')
64 | self.assertContains(response, 'Photologue Demo - <img src=x onerror=alert("title")>')
65 | self.assertNotContains(response, '')
66 | self.assertContains(response, '<img src=x onerror=alert(origin)>')
67 | self.assertNotContains(response, '')
68 |
--------------------------------------------------------------------------------
/example_project/example_project/templates/base.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
3 |
4 |
5 |
6 | Photologue Demo - {% block title %}{% endblock title %}
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
49 |
50 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/example_project/README.rst:
--------------------------------------------------------------------------------
1 | #######################
2 | Photologue Demo Project
3 | #######################
4 |
5 | About
6 | =====
7 | This project serves 3 purposes:
8 |
9 | - It's a quick demo of django-photologue for people who wish to try it out.
10 | - It's an easy way for contributors to the project to have both django-photologue,
11 | and a project that uses it.
12 | - It's used for Travis CI testing of django-photologue.
13 |
14 | It uses the Bootstrap-friendly templates that come with Photologue.
15 |
16 | The rest of the README will assume that you want to set up the test project in
17 | order to work on django-photologue itself.
18 |
19 | Prerequisites
20 | =============
21 |
22 | - python 3.
23 | - virtualenvwrapper makes it easy to manage your virtualenvs. Strongly recommended!
24 |
25 | Installation
26 | ============
27 | **Note**: the project is configured so that it can run immediately with zero configuration
28 | (especially of settings files).
29 |
30 | Create a virtual python environment for the project. The use of virtualenvwrapper
31 | is strongly recommended::
32 |
33 | mkproject --no-site-packages django-photologue
34 | or for more sophisticated setups:
35 | mkvirtualenv --no-site-packages django-photologue
36 |
37 |
38 | Clone this code into your project folder::
39 |
40 | (cd to the new virtualenv)
41 | git clone https://github.com/richardbarran/django-photologue.git .
42 |
43 | **Note**: if you plan to contribute code back to django-photologue, then you'll
44 | probably want instead to fork the project on Github, and clone your fork instead.
45 |
46 | Install requirements::
47 |
48 | cd example_project
49 | pip install -r requirements.txt
50 |
51 | **Note**: this will install Pillow, which is not always straightforward; sometimes it
52 | will install smoothly out of the box, sometimes you can spend hours figuring it out - installation
53 | issues vary from platform to platform, and from one OS release to the next. Google
54 | is your friend here, and it's worth noting that Pillow is a fork of PIL,
55 | so googling 'PIL installation ' can also help.
56 |
57 | The project is set up to run SQLite in dev so that it can be quickly started
58 | with no configuration required (you can of course specify another database in
59 | the settings file). To setup the database::
60 |
61 | ./manage.py migrate
62 |
63 | Follow the instructions to configure photologue here: `Photologue Docs `_
64 |
65 | And finally run the project (it defaults to a safe set of settings for a dev
66 | environment)::
67 |
68 | ./manage.py runserver
69 |
70 | Open browser to http://127.0.0.1:8000
71 |
72 | Thank you
73 | =========
74 | This example project is based on the earlier `photologue_demo project `_.
75 | This project included contributions and input from: crainbf, tomkingston, bmcorser.
76 |
77 |
78 | ..
79 | Note: this README is formatted as reStructuredText so that it's in the same
80 | format as the Sphinx docs.
81 |
--------------------------------------------------------------------------------
/docs/pages/customising/admin.rst:
--------------------------------------------------------------------------------
1 | .. _customisation-admin-label:
2 |
3 | ####################
4 | Customisation: Admin
5 | ####################
6 |
7 | The Photologue admin can easily be customised to your project's requirements. The technique described on this page
8 | is not specific to Photologue - it can be applied to any 3rd party library.
9 |
10 | Create a customisation application
11 | ----------------------------------
12 | For clarity, it's best to put our customisation code in a new application; let's call it
13 | ``photologue_custom``; create the application and add it to your ``INSTALLED_APPS`` setting.
14 |
15 |
16 | Changing the admin
17 | ------------------
18 | In the new ``photologue_custom`` application, create a new empty ``admin.py`` file. In this file we
19 | can replace the admin configuration supplied by Photologue, with a configuration specific to your project.
20 | For example:
21 |
22 | .. code-block:: python
23 |
24 | from django import forms
25 | from django.contrib import admin
26 |
27 | from photologue.admin import GalleryAdmin as GalleryAdminDefault
28 | from photologue.models import Gallery
29 |
30 |
31 | class GalleryAdminForm(forms.ModelForm):
32 | """Users never need to enter a description on a gallery."""
33 |
34 | class Meta:
35 | model = Gallery
36 | exclude = ['description']
37 |
38 |
39 | class GalleryAdmin(GalleryAdminDefault):
40 | form = GalleryAdminForm
41 |
42 | admin.site.unregister(Gallery)
43 | admin.site.register(Gallery, GalleryAdmin)
44 |
45 |
46 | This snippet will define a new Gallery admin class based on Photologue's own. The only change we make
47 | is to exclude the ``description`` field from the change form.
48 |
49 | We then unregister the default admin for the Gallery model and replace it with our new class.
50 |
51 | Possible uses
52 | -------------
53 |
54 | The technique outlined above can be used to make many changes to the admin; here are a couple of suggestions.
55 |
56 | Custom rich text editors
57 | ~~~~~~~~~~~~~~~~~~~~~~~~
58 | The description field on the Gallery model (and the caption field on the Photo model) are plain text fields.
59 | With the above technique, it's easy to use a rich text editor to manage these fields in the admin. For example,
60 | if you have `django-ckeditor `_ installed:
61 |
62 | .. code-block:: python
63 |
64 | from django import forms
65 | from django.contrib import admin
66 |
67 | from ckeditor.widgets import CKEditorWidget
68 | from photologue.admin import GalleryAdmin as GalleryAdminDefault
69 | from photologue.models import Gallery
70 |
71 |
72 | class GalleryAdminForm(forms.ModelForm):
73 | """Replace the default description field, with one that uses a custom widget."""
74 |
75 | description = forms.CharField(widget=CKEditorWidget())
76 |
77 | class Meta:
78 | model = Gallery
79 | exclude = ['']
80 |
81 |
82 | class GalleryAdmin(GalleryAdminDefault):
83 | form = GalleryAdminForm
84 |
85 | admin.site.unregister(Gallery)
86 | admin.site.register(Gallery, GalleryAdmin)
87 |
88 |
--------------------------------------------------------------------------------
/docs/pages/customising/settings.rst:
--------------------------------------------------------------------------------
1 | #######################
2 | Customisation: Settings
3 | #######################
4 |
5 |
6 | Photologue has several settings to customise behaviour.
7 |
8 | PHOTOLOGUE_GALLERY_LATEST_LIMIT
9 | -------------------------------
10 |
11 | Default: ``None``
12 |
13 | Default limit for gallery.latest
14 |
15 |
16 | PHOTOLOGUE_GALLERY_SAMPLE_SIZE
17 | ------------------------------
18 |
19 | Default: ``5``
20 |
21 | Number of random images from the gallery to display.
22 |
23 |
24 | PHOTOLOGUE_IMAGE_FIELD_MAX_LENGTH
25 | ---------------------------------
26 |
27 | Default: ``100``
28 |
29 | max_length setting for the ImageModel ImageField
30 |
31 |
32 | PHOTOLOGUE_SAMPLE_IMAGE_PATH
33 | ----------------------------
34 |
35 | Default: ``os.path.join(os.path.dirname(__file__), 'res', 'sample.jpg'))``
36 |
37 | Path to sample image
38 |
39 |
40 | PHOTOLOGUE_MAXBLOCK
41 | -------------------
42 |
43 | Default: ``256 * 2 ** 10``
44 |
45 | Modify image file buffer size.
46 |
47 |
48 | PHOTOLOGUE_DIR
49 | --------------
50 |
51 | Default: ``'photologue'``
52 |
53 | The relative path from your ``MEDIA_ROOT`` setting where Photologue will save image files. If your ``MEDIA_ROOT`` is set to "/home/user/media", photologue will upload your images to "/home/user/media/photologue"
54 |
55 |
56 | PHOTOLOGUE_PATH
57 | ---------------
58 |
59 | Default: ``None``
60 |
61 | Look for user function to define file paths. Specifies a "callable" that takes a model instance and the original uploaded filename and returns a relative path from your ``MEDIA_ROOT`` that the file will be saved. This function can be set directly.
62 |
63 | For example you could use the following code in a util module::
64 |
65 | # myapp/utils.py:
66 |
67 | import os
68 |
69 | def get_image_path(instance, filename):
70 | return os.path.join('path', 'to', 'my', 'files', filename)
71 |
72 | Then set in settings::
73 |
74 | # settings.py:
75 |
76 | from utils import get_image_path
77 |
78 | PHOTOLOGUE_PATH = get_image_path
79 |
80 | Or instead, pass a string path::
81 |
82 | # settings.py:
83 |
84 | PHOTOLOGUE_PATH = 'myapp.utils.get_image_path'
85 |
86 | .. _settings-photologue-multisite-label:
87 |
88 | PHOTOLOGUE_MULTISITE
89 | --------------------
90 |
91 | Default: ``False``
92 |
93 | Photologue can integrate galleries and photos with `Django's site framework`_.
94 | The default is for this feature to be switched off, and new galleries and photos to be automatically
95 | linked to the current site (``SITE_ID = 1``). The Sites many-to-many field is hidden is the admin, as there is no
96 | need for a user to see it.
97 |
98 | If the setting is ``True``, the admin interface is slightly changed:
99 |
100 | * The Sites many-to-many field is displayed on Gallery and Photos models.
101 | * The Gallery Upload allows you to associate one more sites to the uploaded photos (and gallery).
102 |
103 | .. note:: Gallery Uploads (zip archives) are always associated with the current site. Pull requests to
104 | fix this would be welcome!
105 |
106 | .. _Django's site framework: http://django.readthedocs.org/en/latest/ref/contrib/sites.html
107 |
--------------------------------------------------------------------------------
/photologue/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path, re_path, reverse_lazy
2 | from django.views.generic import RedirectView
3 |
4 | from .views import (GalleryArchiveIndexView, GalleryDateDetailView, GalleryDayArchiveView, GalleryDetailView,
5 | GalleryListView, GalleryMonthArchiveView, GalleryYearArchiveView, PhotoArchiveIndexView,
6 | PhotoDateDetailView, PhotoDayArchiveView, PhotoDetailView, PhotoListView, PhotoMonthArchiveView,
7 | PhotoYearArchiveView)
8 |
9 | """NOTE: the url names are changing. In the long term, I want to remove the 'pl-'
10 | prefix on all urls, and instead rely on an application namespace 'photologue'.
11 |
12 | At the same time, I want to change some URL patterns, e.g. for pagination. Changing the urls
13 | twice within a few releases, could be confusing, so instead I am updating URLs bit by bit.
14 |
15 | The new style will coexist with the existing 'pl-' prefix for a couple of releases.
16 |
17 | """
18 |
19 | app_name = 'photologue'
20 | urlpatterns = [
21 | re_path(r'^gallery/(?P\d{4})/(?P[0-9]{2})/(?P\w{1,2})/(?P[\-\d\w]+)/$',
22 | GalleryDateDetailView.as_view(month_format='%m'),
23 | name='gallery-detail'),
24 | re_path(r'^gallery/(?P\d{4})/(?P[0-9]{2})/(?P\w{1,2})/$',
25 | GalleryDayArchiveView.as_view(month_format='%m'),
26 | name='gallery-archive-day'),
27 | re_path(r'^gallery/(?P\d{4})/(?P[0-9]{2})/$',
28 | GalleryMonthArchiveView.as_view(month_format='%m'),
29 | name='gallery-archive-month'),
30 | re_path(r'^gallery/(?P\d{4})/$',
31 | GalleryYearArchiveView.as_view(),
32 | name='pl-gallery-archive-year'),
33 | path('gallery/',
34 | GalleryArchiveIndexView.as_view(),
35 | name='pl-gallery-archive'),
36 | path('',
37 | RedirectView.as_view(
38 | url=reverse_lazy('photologue:pl-gallery-archive'), permanent=True),
39 | name='pl-photologue-root'),
40 | re_path(r'^gallery/(?P[\-\d\w]+)/$',
41 | GalleryDetailView.as_view(), name='pl-gallery'),
42 | path('gallerylist/',
43 | GalleryListView.as_view(),
44 | name='gallery-list'),
45 |
46 | re_path(r'^photo/(?P\d{4})/(?P[0-9]{2})/(?P\w{1,2})/(?P[\-\d\w]+)/$',
47 | PhotoDateDetailView.as_view(month_format='%m'),
48 | name='photo-detail'),
49 | re_path(r'^photo/(?P\d{4})/(?P[0-9]{2})/(?P\w{1,2})/$',
50 | PhotoDayArchiveView.as_view(month_format='%m'),
51 | name='photo-archive-day'),
52 | re_path(r'^photo/(?P\d{4})/(?P[0-9]{2})/$',
53 | PhotoMonthArchiveView.as_view(month_format='%m'),
54 | name='photo-archive-month'),
55 | re_path(r'^photo/(?P\d{4})/$',
56 | PhotoYearArchiveView.as_view(),
57 | name='pl-photo-archive-year'),
58 | path('photo/',
59 | PhotoArchiveIndexView.as_view(),
60 | name='pl-photo-archive'),
61 |
62 | re_path(r'^photo/(?P[\-\d\w]+)/$',
63 | PhotoDetailView.as_view(),
64 | name='pl-photo'),
65 | path('photolist/',
66 | PhotoListView.as_view(),
67 | name='photo-list'),
68 | ]
69 |
--------------------------------------------------------------------------------
/scripts/releaser_hooks.py:
--------------------------------------------------------------------------------
1 | import os
2 | import subprocess
3 |
4 | try:
5 | import polib
6 | except ImportError:
7 | print('Msg to the package releaser: prerelease hooks will not work as you have not installed polib.')
8 | raise
9 | import codecs
10 | import copy
11 |
12 |
13 | def prereleaser_before(data):
14 | """
15 | 1. Run the unit tests one last time before we make a release.
16 | 2. Update the CONTRIBUTORS.txt file.
17 |
18 | Note: Install * polib (https://pypi.python.org/pypi/polib).
19 | * pep8.
20 |
21 | """
22 | print('Running unit tests.')
23 | subprocess.check_output(["python", "example_project/manage.py", "test", "photologue"])
24 |
25 | print('Checking that we have no outstanding DB migrations.')
26 | output = subprocess.check_output(["python", "example_project/manage.py", "makemigrations", "--dry-run",
27 | "photologue"])
28 | if not output == b"No changes detected in app 'photologue'\n":
29 | raise Exception('There are outstanding migrations for Photologue.')
30 |
31 | print('Updating CONTRIBUTORS.txt')
32 |
33 | # This command will get the author of every commit.
34 | output = subprocess.check_output(["git", "log", "--format='%aN'"])
35 |
36 | # Convert to a list.
37 | contributors_list = [contributor.strip("'") for contributor in output.decode('utf-8').split('\n')]
38 |
39 | # Now add info from the translator files. This is incomplete, we can only list
40 | # the 'last contributor' to each translation.
41 | for language in os.listdir('photologue/locale/'):
42 | filename = f'photologue/locale/{language}/LC_MESSAGES/django.po'
43 | po = polib.pofile(filename)
44 | last_translator = po.metadata['Last-Translator']
45 | contributors_list.append(last_translator[:last_translator.find('<') - 1])
46 |
47 | # Now we want to only show each contributor once, and to list them by how many
48 | # contributions they have made - a rough guide to the effort they have put in.
49 | contributors_dict = {}
50 |
51 | for author in contributors_list:
52 | author_copy = copy.copy(author)
53 |
54 | if author_copy in ('', '(no author)', 'FULL NAME'):
55 | # Skip bad data.
56 | continue
57 |
58 | # The creator of this project should always appear first in the list - so
59 | # don't add him to this list, but hard-code his name.
60 | if author_copy in ('Justin Driscoll', 'justin.driscoll'):
61 | continue
62 |
63 | # Handle contributors who appear under multiple names.
64 | if author_copy == 'richardbarran':
65 | author_copy = 'Richard Barran'
66 |
67 | if author_copy in contributors_dict:
68 | contributors_dict[author_copy] += 1
69 | else:
70 | contributors_dict[author_copy] = 1
71 |
72 | with codecs.open('CONTRIBUTORS.txt', 'w', encoding='utf8') as f:
73 | f.write('Photologue is made possible by all the people who have contributed'
74 | ' to it. A non-exhaustive list follows:\n\n')
75 | f.write('Justin Driscoll\n')
76 | for i in sorted(contributors_dict, key=contributors_dict.get, reverse=True):
77 | f.write(i + '\n')
78 |
79 | # And commit the new contributors file.
80 | if subprocess.check_output(["git", "diff", "CONTRIBUTORS.txt"]):
81 | subprocess.check_output(["git", "commit", "-m", "Updated the list of contributors.", "CONTRIBUTORS.txt"])
82 |
--------------------------------------------------------------------------------
/docs/pages/customising/models.rst:
--------------------------------------------------------------------------------
1 | .. _customising-models-label:
2 |
3 | #####################
4 | Customisation: Models
5 | #####################
6 |
7 | The photologue models can be extended to better suit your project. The technique described on this page
8 | is not specific to Photologue - it can be applied to any 3rd party library.
9 |
10 | The models within Photologue cannot be directly modified (unlike, for example, Django's own User model).
11 | There are a number of reasons behind this decision, including:
12 |
13 | - If code within a project modifies directly the Photologue models' fields, it leaves the Photologue
14 | schema migrations in an ambiguous state.
15 | - Likewise, model methods can no longer be trusted to behave as intended (as fields on which they
16 | depend may have been overridden).
17 |
18 | However, it's easy to create new models linked by one-to-one relationships to Photologue's own
19 | ``Gallery`` and ``Photo`` models.
20 |
21 | On this page we will show how you can add tags to the ``Gallery`` model. For this we will use
22 | the popular 3rd party application `django-taggit `_.
23 |
24 | .. note::
25 |
26 | The ``Gallery`` and ``Photo`` models currently have tag fields, however these are based on the
27 | abandonware `django-tagging `_ application. Instead,
28 | tagging is being entirely removed from Photologue, as it is a non-core functionality of a
29 | gallery application, and is easy to add back in - as this page shows!
30 |
31 | Create a customisation application
32 | ----------------------------------
33 | For clarity, it's best to put our customisation code in a new application; let's call it
34 | ``photologue_custom``; create the application and add it to your ``INSTALLED_APPS`` setting.
35 |
36 | Extending
37 | ---------
38 |
39 | Within the ``photologue_custom`` application, we will edit 2 files:
40 |
41 | Models.py
42 | ~~~~~~~~~
43 |
44 | .. code-block:: python
45 |
46 | from django.db import models
47 |
48 | from taggit.managers import TaggableManager
49 |
50 | from photologue.models import Gallery
51 |
52 |
53 | class GalleryExtended(models.Model):
54 |
55 | # Link back to Photologue's Gallery model.
56 | gallery = models.OneToOneField(Gallery, related_name='extended')
57 |
58 | # This is the important bit - where we add in the tags.
59 | tags = TaggableManager(blank=True)
60 |
61 | # Boilerplate code to make a prettier display in the admin interface.
62 | class Meta:
63 | verbose_name = u'Extra fields'
64 | verbose_name_plural = u'Extra fields'
65 |
66 | def __str__(self):
67 | return self.gallery.title
68 |
69 |
70 | Admin.py
71 | ~~~~~~~~
72 |
73 | .. code-block:: python
74 |
75 | from django.contrib import admin
76 |
77 | from photologue.admin import GalleryAdmin as GalleryAdminDefault
78 | from photologue.models import Gallery
79 | from .models import GalleryExtended
80 |
81 |
82 | class GalleryExtendedInline(admin.StackedInline):
83 | model = GalleryExtended
84 | can_delete = False
85 |
86 |
87 | class GalleryAdmin(GalleryAdminDefault):
88 |
89 | """Define our new one-to-one model as an inline of Photologue's Gallery model."""
90 |
91 | inlines = [GalleryExtendedInline, ]
92 |
93 | admin.site.unregister(Gallery)
94 | admin.site.register(Gallery, GalleryAdmin)
95 |
96 | The above code is enough to start entering tags in the admin interface. To use/display them in the front
97 | end, you will also need to override Photologue's own templates - as the templates are likely to be
98 | heavily customised for your specific project, an example is not included here.
99 |
100 |
101 |
--------------------------------------------------------------------------------
/photologue/utils/reflection.py:
--------------------------------------------------------------------------------
1 | """ Function for generating web 2.0 style image reflection effects.
2 |
3 | Copyright (c) 2007, Justin C. Driscoll
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without modification,
7 | are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice,
10 | this list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright
13 | notice, this list of conditions and the following disclaimer in the
14 | documentation and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of reflection.py nor the names of its contributors may be used
17 | to endorse or promote products derived from this software without
18 | specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
24 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
25 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
26 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
27 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
31 | """
32 |
33 | try:
34 | import Image
35 | import ImageColor
36 | except ImportError:
37 | try:
38 | from PIL import Image, ImageColor
39 | except ImportError:
40 | raise ImportError("The Python Imaging Library was not found.")
41 |
42 |
43 | def add_reflection(im, bgcolor="#00000", amount=0.4, opacity=0.6):
44 | """ Returns the supplied PIL Image (im) with a reflection effect
45 |
46 | bgcolor The background color of the reflection gradient
47 | amount The height of the reflection as a percentage of the orignal image
48 | opacity The initial opacity of the reflection gradient
49 |
50 | Originally written for the Photologue image management system for Django
51 | and Based on the original concept by Bernd Schlapsi
52 |
53 | """
54 | # convert bgcolor string to rgb value
55 | background_color = ImageColor.getrgb(bgcolor)
56 |
57 | # copy orignial image and flip the orientation
58 | reflection = im.copy().transpose(Image.FLIP_TOP_BOTTOM)
59 |
60 | # create a new image filled with the bgcolor the same size
61 | background = Image.new("RGB", im.size, background_color)
62 |
63 | # calculate our alpha mask
64 | start = int(255 - (255 * opacity)) # The start of our gradient
65 | steps = int(255 * amount) # the number of intermedite values
66 | increment = (255 - start) / float(steps)
67 | mask = Image.new('L', (1, 255))
68 | for y in range(255):
69 | if y < steps:
70 | val = int(y * increment + start)
71 | else:
72 | val = 255
73 | mask.putpixel((0, y), val)
74 | alpha_mask = mask.resize(im.size)
75 |
76 | # merge the reflection onto our background color using the alpha mask
77 | reflection = Image.composite(background, reflection, alpha_mask)
78 |
79 | # crop the reflection
80 | reflection_height = int(im.size[1] * amount)
81 | reflection = reflection.crop((0, 0, im.size[0], reflection_height))
82 |
83 | # create new image sized to hold both the original image and the reflection
84 | composite = Image.new("RGB", (im.size[0], im.size[1] + reflection_height), background_color)
85 |
86 | # paste the orignal image and the reflection into the composite image
87 | composite.paste(im, (0, 0))
88 | composite.paste(reflection, (0, im.size[1]))
89 |
90 | # return the image complete with reflection effect
91 | return composite
92 |
--------------------------------------------------------------------------------
/docs/pages/customising/views.rst:
--------------------------------------------------------------------------------
1 | .. _customisation-views-label:
2 |
3 | #############################
4 | Customisation: Views and Urls
5 | #############################
6 |
7 | The photologue views and urls can be tweaked to better suit your project. The technique described on this page
8 | is not specific to Photologue - it can be applied to any 3rd party library.
9 |
10 | Create a customisation application
11 | ----------------------------------
12 | For clarity, it's best to put our customisation code in a new application; let's call it
13 | ``photologue_custom``; create the application and add it to your ``INSTALLED_APPS`` setting.
14 |
15 | We will also want to customise urls:
16 |
17 | 1. Create a urls.py that will contain our customised urls:
18 |
19 |
20 | .. code-block:: python
21 |
22 | from django.conf.urls import *
23 |
24 | urlpatterns = [
25 | # Nothing to see here... for now.
26 | ]
27 |
28 |
29 | 2. These custom urls will override the main Photologue urls, so place them just before Photologue
30 | in the project's main urls.py file:
31 |
32 | .. code-block:: python
33 |
34 | ... other code
35 | (r'^photologue/', include('photologue_custom.urls')),
36 | url(r'^photologue/', include('photologue.urls', namespace='photologue')),
37 |
38 | ... other code
39 |
40 | Now we're ready to make some changes.
41 |
42 | Changing pagination from our new urls.py
43 | ----------------------------------------
44 |
45 | The list pages of Photologue (both Gallery and Photo) display 20 objects per page. Let's change this value.
46 | Edit our new urls.py file, and add:
47 |
48 |
49 | .. code-block:: python
50 |
51 | from django.conf.urls import *
52 |
53 | from photologue.views import GalleryListView
54 |
55 | urlpatterns = [
56 | url(r'^gallerylist/$',
57 | GalleryListView.as_view(paginate_by=5),
58 | name='photologue_custom-gallery-list'),
59 | ]
60 |
61 |
62 | We've copied the urlpattern for
63 | `the gallery list view from Photologue itself `_,
64 | and changed it slightly by passing in ``paginate_by=5``.
65 |
66 | And that's it - now when that page is requested, our customised urls.py will be called first, with pagination
67 | set to 5 items.
68 |
69 | Values that can be overridden from urls.py
70 | ------------------------------------------
71 |
72 | GalleryListView
73 | ~~~~~~~~~~~~~~~
74 |
75 | * paginate_by: number of items to display per page.
76 |
77 | PhotoListView
78 | ~~~~~~~~~~~~~
79 |
80 | * paginate_by: number of items to display per page.
81 |
82 | Changing views.py to create a RESTful api
83 | -----------------------------------------
84 | More substantial customisation can be carried out by writing custom views. For example,
85 | it's easy to change a Photologue view to return JSON objects rather than html webpages. For this
86 | quick demo, we'll use the
87 | `django-braces library `_
88 | to override the view returning a list of all photos.
89 |
90 | Add the following code to views.py in ``photologue_custom``:
91 |
92 | .. code-block:: python
93 |
94 | from photologue.views import PhotoListView
95 |
96 | from braces.views import JSONResponseMixin
97 |
98 |
99 | class PhotoJSONListView(JSONResponseMixin, PhotoListView):
100 |
101 | def render_to_response(self, context, **response_kwargs):
102 | return self.render_json_object_response(context['object_list'],
103 | **response_kwargs)
104 |
105 | And call this new view from urls.py; here we are replacing the standard Photo list page provided by Photologue:
106 |
107 | .. code-block:: python
108 |
109 | from .views import PhotoJSONListView
110 |
111 | urlpatterns = [
112 | # Other urls...
113 | url(r'^photolist/$',
114 | PhotoJSONListView.as_view(),
115 | name='photologue_custom-photo-json-list'),
116 | # Other urls as required...
117 | ]
118 |
119 |
120 | And that's it! Of course, this is simply a demo and a real RESTful api would be rather more complex.
121 |
122 |
123 |
124 |
125 |
126 |
--------------------------------------------------------------------------------
/photologue/tests/factories.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import os
3 |
4 | from django.conf import settings
5 | from django.utils.text import slugify
6 |
7 | try:
8 | import factory
9 | except ImportError:
10 | raise ImportError(
11 | "No module named factory. To run photologue's tests you need to install factory-boy.")
12 |
13 | from ..models import Gallery, ImageModel, Photo, PhotoEffect, PhotoSize
14 |
15 | RES_DIR = os.path.join(os.path.dirname(__file__), '../res')
16 | LANDSCAPE_IMAGE_PATH = os.path.join(RES_DIR, 'test_photologue_landscape.jpg')
17 | PORTRAIT_IMAGE_PATH = os.path.join(RES_DIR, 'test_photologue_portrait.jpg')
18 | SQUARE_IMAGE_PATH = os.path.join(RES_DIR, 'test_photologue_square.jpg')
19 | QUOTING_IMAGE_PATH = os.path.join(RES_DIR, 'test_photologue_"ing.jpg')
20 | UNICODE_IMAGE_PATH = os.path.join(RES_DIR, 'test_unicode_®.jpg')
21 | NONSENSE_IMAGE_PATH = os.path.join(RES_DIR, 'test_nonsense.jpg')
22 | SAMPLE_ZIP_PATH = os.path.join(RES_DIR, 'zips/sample.zip')
23 | SAMPLE_NOT_IMAGE_ZIP_PATH = os.path.join(RES_DIR, 'zips/not_image.zip')
24 | IGNORED_FILES_ZIP_PATH = os.path.join(RES_DIR, 'zips/ignored_files.zip')
25 |
26 |
27 | class GalleryFactory(factory.django.DjangoModelFactory):
28 |
29 | class Meta:
30 | model = Gallery
31 |
32 | title = factory.Sequence(lambda n: f'gallery{n:0>3}')
33 | slug = factory.LazyAttribute(lambda a: slugify(a.title))
34 |
35 | @factory.sequence
36 | def date_added(n):
37 | # Have to cater projects being non-timezone aware.
38 | if settings.USE_TZ:
39 | sample_date = datetime.datetime(
40 | year=2011, month=12, day=23, hour=17, minute=40, tzinfo=datetime.timezone.utc)
41 | else:
42 | sample_date = datetime.datetime(year=2011, month=12, day=23, hour=17, minute=40)
43 | return sample_date + datetime.timedelta(minutes=n)
44 |
45 | @factory.post_generation
46 | def sites(self, create, extracted, **kwargs):
47 | """
48 | Associates the object with the current site unless ``sites`` was passed,
49 | in which case the each item in ``sites`` is associated with the object.
50 |
51 | Note that if PHOTOLOGUE_MULTISITE is False, all Gallery/Photos are automatically
52 | associated with the current site - bear this in mind when writing tests.
53 | """
54 | if not create:
55 | return
56 | if extracted:
57 | for site in extracted:
58 | self.sites.add(site)
59 |
60 |
61 | class ImageModelFactory(factory.django.DjangoModelFactory):
62 |
63 | class Meta:
64 | model = ImageModel
65 | abstract = True
66 |
67 |
68 | class PhotoFactory(ImageModelFactory):
69 |
70 | """Note: after creating Photo instances for tests, remember to manually
71 | delete them.
72 | """
73 |
74 | class Meta:
75 | model = Photo
76 |
77 | title = factory.Sequence(lambda n: f'photo{n:0>3}')
78 | slug = factory.LazyAttribute(lambda a: slugify(a.title))
79 | image = factory.django.ImageField(from_path=LANDSCAPE_IMAGE_PATH)
80 |
81 | @factory.sequence
82 | def date_added(n):
83 | # Have to cater projects being non-timezone aware.
84 | if settings.USE_TZ:
85 | sample_date = datetime.datetime(
86 | year=2011, month=12, day=23, hour=17, minute=40, tzinfo=datetime.timezone.utc)
87 | else:
88 | sample_date = datetime.datetime(year=2011, month=12, day=23, hour=17, minute=40)
89 | return sample_date + datetime.timedelta(minutes=n)
90 |
91 | @factory.post_generation
92 | def sites(self, create, extracted, **kwargs):
93 | """
94 | Associates the object with the current site unless ``sites`` was passed,
95 | in which case the each item in ``sites`` is associated with the object.
96 |
97 | Note that if PHOTOLOGUE_MULTISITE is False, all Gallery/Photos are automatically
98 | associated with the current site - bear this in mind when writing tests.
99 | """
100 | if not create:
101 | return
102 | if extracted:
103 | for site in extracted:
104 | self.sites.add(site)
105 |
106 |
107 | class PhotoSizeFactory(factory.django.DjangoModelFactory):
108 |
109 | class Meta:
110 | model = PhotoSize
111 |
112 | name = factory.Sequence(lambda n: f'name{n:0>3}')
113 |
114 |
115 | class PhotoEffectFactory(factory.django.DjangoModelFactory):
116 |
117 | class Meta:
118 | model = PhotoEffect
119 |
120 | name = factory.Sequence(lambda n: f'effect{n:0>3}')
121 |
--------------------------------------------------------------------------------
/photologue/templatetags/photologue_tags.py:
--------------------------------------------------------------------------------
1 | import random
2 |
3 | from django import template
4 |
5 | from ..models import Gallery, Photo
6 |
7 | register = template.Library()
8 |
9 |
10 | @register.inclusion_tag('photologue/tags/next_in_gallery.html')
11 | def next_in_gallery(photo, gallery):
12 | return {'photo': photo.get_next_in_gallery(gallery)}
13 |
14 |
15 | @register.inclusion_tag('photologue/tags/prev_in_gallery.html')
16 | def previous_in_gallery(photo, gallery):
17 | return {'photo': photo.get_previous_in_gallery(gallery)}
18 |
19 |
20 | @register.simple_tag
21 | def cycle_lite_gallery(gallery_title, height, width):
22 | """Generate image tags for jquery slideshow gallery.
23 | See http://malsup.com/jquery/cycle/lite/"""
24 | html = ""
25 | first = 'class="first"'
26 | for p in Gallery.objects.get(title=gallery_title).public():
27 | html += ''.format(
28 | p.get_display_url(), p.title, height, width, first)
29 | first = None
30 | return html
31 |
32 |
33 | @register.tag
34 | def get_photo(parser, token):
35 | """Get a single photo from the photologue library and return the img tag to display it.
36 |
37 | Takes 3 args:
38 | - the photo to display. This can be either the slug of a photo, or a variable that holds either a photo instance or
39 | a integer (photo id)
40 | - the photosize to use.
41 | - a CSS class to apply to the img tag.
42 | """
43 | try:
44 | # Split the contents of the tag, i.e. tag name + argument.
45 | tag_name, photo, photosize, css_class = token.split_contents()
46 | except ValueError:
47 | msg = '%r tag requires 3 arguments' % token.contents[0]
48 | raise template.TemplateSyntaxError(msg)
49 | return PhotoNode(photo, photosize[1:-1], css_class[1:-1])
50 |
51 |
52 | class PhotoNode(template.Node):
53 |
54 | def __init__(self, photo, photosize, css_class):
55 | self.photo = photo
56 | self.photosize = photosize
57 | self.css_class = css_class
58 |
59 | def render(self, context):
60 | try:
61 | a = template.Variable(self.photo).resolve(context)
62 | except:
63 | a = self.photo
64 | if isinstance(a, Photo):
65 | p = a
66 | else:
67 | try:
68 | p = Photo.objects.get(slug=a)
69 | except Photo.DoesNotExist:
70 | # Ooops. Fail silently
71 | return None
72 | if not p.is_public:
73 | return None
74 | func = getattr(p, 'get_%s_url' % (self.photosize), None)
75 | if func is None:
76 | return 'A "%s" photo size has not been defined.' % (self.photosize)
77 | else:
78 | return f''
79 |
80 |
81 | @register.tag
82 | def get_rotating_photo(parser, token):
83 | """Pick at random a photo from a given photologue gallery and return the img tag to display it.
84 |
85 | Takes 3 args:
86 | - the gallery to pick a photo from. This can be either the slug of a gallery, or a variable that holds either a
87 | gallery instance or a gallery slug.
88 | - the photosize to use.
89 | - a CSS class to apply to the img tag.
90 | """
91 | try:
92 | # Split the contents of the tag, i.e. tag name + argument.
93 | tag_name, gallery, photosize, css_class = token.split_contents()
94 | except ValueError:
95 | msg = '%r tag requires 3 arguments' % token.contents[0]
96 | raise template.TemplateSyntaxError(msg)
97 | return PhotoGalleryNode(gallery, photosize[1:-1], css_class[1:-1])
98 |
99 |
100 | class PhotoGalleryNode(template.Node):
101 |
102 | def __init__(self, gallery, photosize, css_class):
103 | self.gallery = gallery
104 | self.photosize = photosize
105 | self.css_class = css_class
106 |
107 | def render(self, context):
108 | try:
109 | a = template.resolve_variable(self.gallery, context)
110 | except:
111 | a = self.gallery
112 | if isinstance(a, Gallery):
113 | g = a
114 | else:
115 | try:
116 | g = Gallery.objects.get(slug=a)
117 | except Gallery.DoesNotExist:
118 | return None
119 | photos = g.public()
120 | if len(photos) > 1:
121 | r = random.randint(0, len(photos) - 1)
122 | p = photos[r]
123 | elif len(photos) == 1:
124 | p = photos[0]
125 | else:
126 | return None
127 | func = getattr(p, 'get_%s_url' % (self.photosize), None)
128 | if func is None:
129 | return 'A "%s" photo size has not been defined.' % (self.photosize)
130 | else:
131 | return f''
132 |
--------------------------------------------------------------------------------
/example_project/example_project/settings.py:
--------------------------------------------------------------------------------
1 | # Global settings for photologue example project.
2 |
3 | import os
4 | import sys
5 |
6 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
7 |
8 |
9 | SECRET_KEY = '=_v6sfp8u2uuhdncdz9t1_nu8(#8q4=40$f$4rorj4q3)f-nlc'
10 |
11 | DEBUG = True
12 |
13 | ALLOWED_HOSTS = ['*']
14 |
15 |
16 | INSTALLED_APPS = (
17 | 'django.contrib.admin',
18 | 'django.contrib.auth',
19 | 'django.contrib.contenttypes',
20 | 'django.contrib.sessions',
21 | 'django.contrib.sites',
22 | 'django.contrib.messages',
23 | 'django.contrib.staticfiles',
24 | # Note: added sitemaps to the INSTALLED_APPS just so that unit tests run,
25 | # but not actually added a sitemap in urls.py.
26 | 'django.contrib.sitemaps',
27 | 'photologue',
28 | 'sortedm2m',
29 | 'example_project',
30 | )
31 |
32 | MIDDLEWARE = (
33 | 'django.contrib.sessions.middleware.SessionMiddleware',
34 | 'django.middleware.common.CommonMiddleware',
35 | 'django.middleware.csrf.CsrfViewMiddleware',
36 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
37 | 'django.contrib.messages.middleware.MessageMiddleware',
38 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
39 | )
40 |
41 | ROOT_URLCONF = 'example_project.urls'
42 |
43 | TEMPLATES = [
44 | {
45 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
46 | 'DIRS': [os.path.join(BASE_DIR, 'example_project/templates'), ],
47 | 'APP_DIRS': True,
48 | 'OPTIONS': {
49 | 'context_processors': [
50 | 'django.template.context_processors.debug',
51 | 'django.template.context_processors.request',
52 | 'django.contrib.auth.context_processors.auth',
53 | 'django.contrib.messages.context_processors.messages',
54 | 'django.template.context_processors.i18n',
55 | 'django.template.context_processors.media',
56 | 'django.template.context_processors.static',
57 | ],
58 | 'debug': True,
59 | },
60 | },
61 | ]
62 |
63 | WSGI_APPLICATION = 'example_project.wsgi.application'
64 |
65 |
66 | # Database
67 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases
68 |
69 | DATABASES = {
70 | 'default': {
71 | 'ENGINE': 'django.db.backends.sqlite3',
72 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
73 | }
74 | }
75 |
76 |
77 | # Internationalization
78 | # https://docs.djangoproject.com/en/1.8/topics/i18n/
79 |
80 | LANGUAGE_CODE = 'en-gb'
81 |
82 | TIME_ZONE = 'UTC'
83 |
84 | USE_I18N = True
85 |
86 | USE_TZ = True
87 |
88 |
89 | # Static files (CSS, JavaScript, Images)
90 | # https://docs.djangoproject.com/en/1.8/howto/static-files/
91 |
92 | STATIC_ROOT = os.path.join(BASE_DIR, 'public', 'static')
93 | STATIC_URL = '/static/'
94 |
95 | MEDIA_ROOT = os.path.join(BASE_DIR, 'public', 'media')
96 | MEDIA_URL = '/media/'
97 |
98 | STATICFILES_DIRS = (
99 | os.path.join(BASE_DIR, 'example_project/static'),
100 | )
101 |
102 | SITE_ID = 1
103 |
104 | # LOGGING CONFIGURATION
105 | # A logging configuration that writes log messages to the console.
106 | LOGGING = {
107 | 'version': 1,
108 | 'disable_existing_loggers': True,
109 | # Formatting of messages.
110 | 'formatters': {
111 | # Don't need to show the time when logging to console.
112 | 'console': {
113 | 'format': '%(levelname)s %(name)s.%(funcName)s (%(lineno)d) %(message)s'
114 | }
115 | },
116 | # The handlers decide what we should do with a logging message - do we email
117 | # it, ditch it, or write it to a file?
118 | 'handlers': {
119 | # Writing to console. Use only in dev.
120 | 'console': {
121 | 'level': 'DEBUG',
122 | 'class': 'logging.StreamHandler',
123 | 'formatter': 'console'
124 | },
125 | # Send logs to /dev/null.
126 | 'null': {
127 | 'level': 'DEBUG',
128 | 'class': 'logging.NullHandler',
129 | },
130 | },
131 | # Loggers decide what is logged.
132 | 'loggers': {
133 | '': {
134 | # Default (suitable for dev) is to log to console.
135 | 'handlers': ['console'],
136 | 'level': 'INFO',
137 | 'propagate': False,
138 | },
139 | 'photologue': {
140 | # Default (suitable for dev) is to log to console.
141 | 'handlers': ['console'],
142 | 'level': 'DEBUG',
143 | 'propagate': False,
144 | },
145 | # logging of SQL statements. Default is to ditch them (send them to
146 | # null). Note that this logger only works if DEBUG = True.
147 | 'django.db.backends': {
148 | 'handlers': ['null'],
149 | 'level': 'DEBUG',
150 | 'propagate': False,
151 | },
152 | }
153 | }
154 |
155 | # Don't display logging messages to console during unit test runs.
156 | if len(sys.argv) > 1 and sys.argv[1] == 'test':
157 | LOGGING['loggers']['']['handlers'] = ['null']
158 | LOGGING['loggers']['photologue']['handlers'] = ['null']
159 |
160 | # Uncomment this for Amazon S3 file storage
161 | # from example_storages.settings_s3boto import *
162 |
--------------------------------------------------------------------------------
/docs/pages/contributing.rst:
--------------------------------------------------------------------------------
1 | ##########################
2 | Contributing to Photologue
3 | ##########################
4 |
5 | Contributions are always very welcome. Even if you have never contributed to an
6 | open-source project before - please do not hesitate to offer help. Fixes for typos in the
7 | documentation, extra unit tests, etc... are welcome. And look in the issues
8 | list for anything tagged "easy_win".
9 |
10 | Example project
11 | ---------------
12 | Photologue includes an example project under ``/example_project/`` to get you quickly ready for
13 | contributing to the project - do not hesitate to use it! Please refer to ``/example_project/README.rst``
14 | for installation instructions.
15 |
16 | You'll probably also want to manually install
17 | `Sphinx `_ if you're going to update the documentation.
18 |
19 | Workflow
20 | --------
21 | Photologue is hosted on Github, so if you have not already done so, read the excellent
22 | `Github help pages `_. We try to keep the workflow
23 | as simple as possible; most pull requests are merged straight into the master branch. Please
24 | ensure your pull requests are on separate branches, and please try to only include one new
25 | feature per pull request!
26 |
27 | Features that will take a while to develop might warrant a separate branch in the project;
28 | at present only the ImageKit integration project is run on a separate branch.
29 |
30 | Coding style
31 | ------------
32 | No surprises here - just try to `follow the conventions used by Django itself
33 | `_.
34 |
35 | Unit tests
36 | ----------
37 | Including unit tests with your contributions will earn you bonus points, maybe even a beer. So write
38 | plenty of tests, and run them from the ``/example_project/`` with a
39 | ``python manage.py test photologue``.
40 |
41 | There's also a `Tox `_ configuration file - so if
42 | you have tox installed, run ``tox`` from the ``/example_project/`` folder, and it will run the entire
43 | test suite against all versions of Python and Django that are supported.
44 |
45 | Documentation
46 | -------------
47 | Keeping the documentation up-to-date is very important - so if your code changes
48 | how Photologue works, or adds a new feature, please check that the documentation is still accurate, and
49 | update it if required.
50 |
51 | We use `Sphinx `_ to prepare the documentation; please refer to the excellent docs
52 | on that site for help.
53 |
54 | .. note::
55 |
56 | The CHANGELOG is part of the documentation, so if your patch needs the
57 | end user to do something - e.g. run a database migration - don't forget to update
58 | it!
59 |
60 | Translations
61 | ------------
62 | `Photologue manages string translations with Transifex
63 | `_. The easiest way to help is
64 | to add new/updated translations there.
65 |
66 | Once you've added translations, give the maintainer a wave and he will pull the updated
67 | translations into the master branch, so that you can install Photologue directly from the
68 | Github repository (see :ref:`installing-photologue-label`) and use your translations straight away. Or you can do nothing - just before a release
69 | any new/updated translations get pulled from Transifex and added to the Photologue project.
70 |
71 | New features
72 | ------------
73 | In the wiki there is a `wishlist of new features already planned
74 | for Photologue `_ - you are welcome to suggest other useful improvements.
75 |
76 | If you’re interested in developing a new feature, it is recommended that you first
77 | discuss it on the `mailing list `_
78 | or open a new ticket in Github, in order to avoid working on a feature that will
79 | not get accepted as it is judged to not fit in with the goals of Photologue.
80 |
81 | A bit of history
82 | ~~~~~~~~~~~~~~~~
83 | Photologue was started by Justin Driscoll in 2007. He quickly built it into a powerful
84 | photo gallery and image processing application, and it became successful.
85 |
86 | Justin then moved onto other projects, and no longer had the time required to maintain
87 | Photologue - there was only one commit between August 2009 and August 2012, and
88 | approximately 70 open tickets on the Google Code project page.
89 |
90 | At this point Richard Barran took over as maintainer of the project. First priority
91 | was to improve the infrastructure of the project: moving to Github, adding South,
92 | Sphinx for documentation, Transifex for translations, Travis for continuous integration,
93 | zest.releaser.
94 |
95 | The codebase has not changed much so far - and it needs quite a bit of TLC
96 | (Tender Loving Care), and new features are waiting to be added. This is where you step in...
97 |
98 | And finally...
99 | --------------
100 | Please remember that the maintainer looks after Photologue in his spare time -
101 | so it might be a few weeks before your pull request gets looked at... and the pull
102 | requests that are nicely formatted, with code, tests and docs included, will
103 | always get reviewed first ;-)
104 |
--------------------------------------------------------------------------------
/photologue/tests/test_resize.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from django.core.exceptions import ValidationError
4 |
5 | from ..models import PhotoSize, PhotoSizeCache
6 | from .factories import PORTRAIT_IMAGE_PATH, SQUARE_IMAGE_PATH, PhotoFactory
7 | from .helpers import PhotologueBaseTest
8 |
9 |
10 | class PhotoSizeTest(unittest.TestCase):
11 |
12 | def test_clean_wont_allow_zero_dimension_and_crop(self):
13 | """Tests if ValidationError is raised by clean method if with or height
14 | is set to 0 and crop is set to true"""
15 | s = PhotoSize(name='test', width=400, crop=True)
16 | self.assertRaises(ValidationError, s.clean)
17 |
18 |
19 | class ImageResizeTest(PhotologueBaseTest):
20 |
21 | def setUp(self):
22 | super().setUp()
23 | # Portrait.
24 | self.pp = PhotoFactory(image__from_path=PORTRAIT_IMAGE_PATH)
25 | # Square.
26 | self.ps = PhotoFactory(image__from_path=SQUARE_IMAGE_PATH)
27 |
28 | def tearDown(self):
29 | super().tearDown()
30 | self.pp.delete()
31 | self.ps.delete()
32 |
33 | def test_resize_to_fit(self):
34 | self.assertEqual(self.pl.get_testPhotoSize_size(), (100, 75))
35 | self.assertEqual(self.pp.get_testPhotoSize_size(), (75, 100))
36 | self.assertEqual(self.ps.get_testPhotoSize_size(), (100, 100))
37 |
38 | def test_resize_to_fit_width(self):
39 | self.s.size = (100, 0)
40 | self.s.save()
41 | self.assertEqual(self.pl.get_testPhotoSize_size(), (100, 75))
42 | self.assertEqual(self.pp.get_testPhotoSize_size(), (100, 133))
43 | self.assertEqual(self.ps.get_testPhotoSize_size(), (100, 100))
44 |
45 | def test_resize_to_fit_width_enlarge(self):
46 | self.s.size = (400, 0)
47 | self.s.upscale = True
48 | self.s.save()
49 | self.assertEqual(self.pl.get_testPhotoSize_size(), (400, 300))
50 | self.assertEqual(self.pp.get_testPhotoSize_size(), (400, 533))
51 | self.assertEqual(self.ps.get_testPhotoSize_size(), (400, 400))
52 |
53 | def test_resize_to_fit_height(self):
54 | self.s.size = (0, 100)
55 | self.s.save()
56 | self.assertEqual(self.pl.get_testPhotoSize_size(), (133, 100))
57 | self.assertEqual(self.pp.get_testPhotoSize_size(), (75, 100))
58 | self.assertEqual(self.ps.get_testPhotoSize_size(), (100, 100))
59 |
60 | def test_resize_to_fit_height_enlarge(self):
61 | self.s.size = (0, 400)
62 | self.s.upscale = True
63 | self.s.save()
64 | self.assertEqual(self.pl.get_testPhotoSize_size(), (533, 400))
65 | self.assertEqual(self.pp.get_testPhotoSize_size(), (300, 400))
66 | self.assertEqual(self.ps.get_testPhotoSize_size(), (400, 400))
67 |
68 | def test_resize_and_crop(self):
69 | self.s.crop = True
70 | self.s.save()
71 | self.assertEqual(self.pl.get_testPhotoSize_size(), self.s.size)
72 | self.assertEqual(self.pp.get_testPhotoSize_size(), self.s.size)
73 | self.assertEqual(self.ps.get_testPhotoSize_size(), self.s.size)
74 |
75 | def test_resize_rounding_to_fit(self):
76 | self.s.size = (113, 113)
77 | self.s.save()
78 | self.assertEqual(self.pl.get_testPhotoSize_size(), (113, 85))
79 | self.assertEqual(self.pp.get_testPhotoSize_size(), (85, 113))
80 | self.assertEqual(self.ps.get_testPhotoSize_size(), (113, 113))
81 |
82 | def test_resize_rounding_cropped(self):
83 | self.s.size = (113, 113)
84 | self.s.crop = True
85 | self.s.save()
86 | self.assertEqual(self.pl.get_testPhotoSize_size(), self.s.size)
87 | self.assertEqual(self.pp.get_testPhotoSize_size(), self.s.size)
88 | self.assertEqual(self.ps.get_testPhotoSize_size(), self.s.size)
89 |
90 | def test_resize_one_dimension_width(self):
91 | self.s.size = (100, 150)
92 | self.s.save()
93 | self.assertEqual(self.pl.get_testPhotoSize_size(), (100, 75))
94 |
95 | def test_resize_one_dimension_height(self):
96 | self.s.size = (200, 75)
97 | self.s.save()
98 | self.assertEqual(self.pl.get_testPhotoSize_size(), (100, 75))
99 |
100 | def test_resize_no_upscale(self):
101 | self.s.size = (1000, 1000)
102 | self.s.save()
103 | self.assertEqual(self.pl.get_testPhotoSize_size(), (200, 150))
104 |
105 | def test_resize_no_upscale_mixed_height(self):
106 | self.s.size = (400, 75)
107 | self.s.save()
108 | self.assertEqual(self.pl.get_testPhotoSize_size(), (100, 75))
109 |
110 | def test_resize_no_upscale_mixed_width(self):
111 | self.s.size = (100, 300)
112 | self.s.save()
113 | self.assertEqual(self.pl.get_testPhotoSize_size(), (100, 75))
114 |
115 | def test_resize_no_upscale_crop(self):
116 | self.s.size = (1000, 1000)
117 | self.s.crop = True
118 | self.s.save()
119 | self.assertEqual(self.pl.get_testPhotoSize_size(), (1000, 1000))
120 |
121 | def test_resize_upscale(self):
122 | self.s.size = (1000, 1000)
123 | self.s.upscale = True
124 | self.s.save()
125 | self.assertEqual(self.pl.get_testPhotoSize_size(), (1000, 750))
126 | self.assertEqual(self.pp.get_testPhotoSize_size(), (750, 1000))
127 | self.assertEqual(self.ps.get_testPhotoSize_size(), (1000, 1000))
128 |
129 |
130 | class PhotoSizeCacheTest(PhotologueBaseTest):
131 |
132 | def test(self):
133 | cache = PhotoSizeCache()
134 | self.assertEqual(cache.sizes['testPhotoSize'], self.s)
135 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | REM Command file for Sphinx documentation
4 |
5 | if "%SPHINXBUILD%" == "" (
6 | set SPHINXBUILD=sphinx-build
7 | )
8 | set BUILDDIR=_build
9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
10 | set I18NSPHINXOPTS=%SPHINXOPTS% .
11 | if NOT "%PAPER%" == "" (
12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
14 | )
15 |
16 | if "%1" == "" goto help
17 |
18 | if "%1" == "help" (
19 | :help
20 | echo.Please use `make ^` where ^ is one of
21 | echo. html to make standalone HTML files
22 | echo. dirhtml to make HTML files named index.html in directories
23 | echo. singlehtml to make a single large HTML file
24 | echo. pickle to make pickle files
25 | echo. json to make JSON files
26 | echo. htmlhelp to make HTML files and a HTML help project
27 | echo. qthelp to make HTML files and a qthelp project
28 | echo. devhelp to make HTML files and a Devhelp project
29 | echo. epub to make an epub
30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
31 | echo. text to make text files
32 | echo. man to make manual pages
33 | echo. texinfo to make Texinfo files
34 | echo. gettext to make PO message catalogs
35 | echo. changes to make an overview over all changed/added/deprecated items
36 | echo. linkcheck to check all external links for integrity
37 | echo. doctest to run all doctests embedded in the documentation if enabled
38 | goto end
39 | )
40 |
41 | if "%1" == "clean" (
42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
43 | del /q /s %BUILDDIR%\*
44 | goto end
45 | )
46 |
47 | if "%1" == "html" (
48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
49 | if errorlevel 1 exit /b 1
50 | echo.
51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html.
52 | goto end
53 | )
54 |
55 | if "%1" == "dirhtml" (
56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
57 | if errorlevel 1 exit /b 1
58 | echo.
59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
60 | goto end
61 | )
62 |
63 | if "%1" == "singlehtml" (
64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
65 | if errorlevel 1 exit /b 1
66 | echo.
67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
68 | goto end
69 | )
70 |
71 | if "%1" == "pickle" (
72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
73 | if errorlevel 1 exit /b 1
74 | echo.
75 | echo.Build finished; now you can process the pickle files.
76 | goto end
77 | )
78 |
79 | if "%1" == "json" (
80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
81 | if errorlevel 1 exit /b 1
82 | echo.
83 | echo.Build finished; now you can process the JSON files.
84 | goto end
85 | )
86 |
87 | if "%1" == "htmlhelp" (
88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
89 | if errorlevel 1 exit /b 1
90 | echo.
91 | echo.Build finished; now you can run HTML Help Workshop with the ^
92 | .hhp project file in %BUILDDIR%/htmlhelp.
93 | goto end
94 | )
95 |
96 | if "%1" == "qthelp" (
97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
98 | if errorlevel 1 exit /b 1
99 | echo.
100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^
101 | .qhcp project file in %BUILDDIR%/qthelp, like this:
102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-photologue.qhcp
103 | echo.To view the help file:
104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-photologue.ghc
105 | goto end
106 | )
107 |
108 | if "%1" == "devhelp" (
109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
110 | if errorlevel 1 exit /b 1
111 | echo.
112 | echo.Build finished.
113 | goto end
114 | )
115 |
116 | if "%1" == "epub" (
117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
118 | if errorlevel 1 exit /b 1
119 | echo.
120 | echo.Build finished. The epub file is in %BUILDDIR%/epub.
121 | goto end
122 | )
123 |
124 | if "%1" == "latex" (
125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
126 | if errorlevel 1 exit /b 1
127 | echo.
128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
129 | goto end
130 | )
131 |
132 | if "%1" == "text" (
133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
134 | if errorlevel 1 exit /b 1
135 | echo.
136 | echo.Build finished. The text files are in %BUILDDIR%/text.
137 | goto end
138 | )
139 |
140 | if "%1" == "man" (
141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
142 | if errorlevel 1 exit /b 1
143 | echo.
144 | echo.Build finished. The manual pages are in %BUILDDIR%/man.
145 | goto end
146 | )
147 |
148 | if "%1" == "texinfo" (
149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
150 | if errorlevel 1 exit /b 1
151 | echo.
152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
153 | goto end
154 | )
155 |
156 | if "%1" == "gettext" (
157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
158 | if errorlevel 1 exit /b 1
159 | echo.
160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
161 | goto end
162 | )
163 |
164 | if "%1" == "changes" (
165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
166 | if errorlevel 1 exit /b 1
167 | echo.
168 | echo.The overview file is in %BUILDDIR%/changes.
169 | goto end
170 | )
171 |
172 | if "%1" == "linkcheck" (
173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
174 | if errorlevel 1 exit /b 1
175 | echo.
176 | echo.Link check complete; look for any errors in the above output ^
177 | or in %BUILDDIR%/linkcheck/output.txt.
178 | goto end
179 | )
180 |
181 | if "%1" == "doctest" (
182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
183 | if errorlevel 1 exit /b 1
184 | echo.
185 | echo.Testing of doctests in the sources finished, look at the ^
186 | results in %BUILDDIR%/doctest/output.txt.
187 | goto end
188 | )
189 |
190 | :end
191 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 |
10 | # Internal variables.
11 | PAPEROPT_a4 = -D latex_paper_size=a4
12 | PAPEROPT_letter = -D latex_paper_size=letter
13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
14 | # the i18n builder cannot share the environment and doctrees with the others
15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
16 |
17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
18 |
19 | help:
20 | @echo "Please use \`make ' where is one of"
21 | @echo " html to make standalone HTML files"
22 | @echo " dirhtml to make HTML files named index.html in directories"
23 | @echo " singlehtml to make a single large HTML file"
24 | @echo " pickle to make pickle files"
25 | @echo " json to make JSON files"
26 | @echo " htmlhelp to make HTML files and a HTML help project"
27 | @echo " qthelp to make HTML files and a qthelp project"
28 | @echo " devhelp to make HTML files and a Devhelp project"
29 | @echo " epub to make an epub"
30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
31 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
32 | @echo " text to make text files"
33 | @echo " man to make manual pages"
34 | @echo " texinfo to make Texinfo files"
35 | @echo " info to make Texinfo files and run them through makeinfo"
36 | @echo " gettext to make PO message catalogs"
37 | @echo " changes to make an overview of all changed/added/deprecated items"
38 | @echo " linkcheck to check all external links for integrity"
39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
40 |
41 | clean:
42 | -rm -rf $(BUILDDIR)/*
43 |
44 | html:
45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
46 | @echo
47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
48 |
49 | dirhtml:
50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
51 | @echo
52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
53 |
54 | singlehtml:
55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
56 | @echo
57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
58 |
59 | pickle:
60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
61 | @echo
62 | @echo "Build finished; now you can process the pickle files."
63 |
64 | json:
65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
66 | @echo
67 | @echo "Build finished; now you can process the JSON files."
68 |
69 | htmlhelp:
70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
71 | @echo
72 | @echo "Build finished; now you can run HTML Help Workshop with the" \
73 | ".hhp project file in $(BUILDDIR)/htmlhelp."
74 |
75 | qthelp:
76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
77 | @echo
78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-photologue.qhcp"
81 | @echo "To view the help file:"
82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-photologue.qhc"
83 |
84 | devhelp:
85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
86 | @echo
87 | @echo "Build finished."
88 | @echo "To view the help file:"
89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-photologue"
90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-photologue"
91 | @echo "# devhelp"
92 |
93 | epub:
94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
95 | @echo
96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
97 |
98 | latex:
99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
100 | @echo
101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
103 | "(use \`make latexpdf' here to do that automatically)."
104 |
105 | latexpdf:
106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
107 | @echo "Running LaTeX files through pdflatex..."
108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
110 |
111 | text:
112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
113 | @echo
114 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
115 |
116 | man:
117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
118 | @echo
119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
120 |
121 | texinfo:
122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
123 | @echo
124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
125 | @echo "Run \`make' in that directory to run these through makeinfo" \
126 | "(use \`make info' here to do that automatically)."
127 |
128 | info:
129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
130 | @echo "Running Texinfo files through makeinfo..."
131 | make -C $(BUILDDIR)/texinfo info
132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
133 |
134 | gettext:
135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
136 | @echo
137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
138 |
139 | changes:
140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
141 | @echo
142 | @echo "The overview file is in $(BUILDDIR)/changes."
143 |
144 | linkcheck:
145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
146 | @echo
147 | @echo "Link check complete; look for any errors in the above output " \
148 | "or in $(BUILDDIR)/linkcheck/output.txt."
149 |
150 | doctest:
151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
152 | @echo "Testing of doctests in the sources finished, look at the " \
153 | "results in $(BUILDDIR)/doctest/output.txt."
154 |
--------------------------------------------------------------------------------
/photologue/tests/test_sites.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from django.conf import settings
4 | from django.contrib.sites.models import Site
5 | from django.test import TestCase, override_settings
6 |
7 | from .factories import GalleryFactory, PhotoFactory
8 |
9 |
10 | @override_settings(ROOT_URLCONF='photologue.tests.test_urls')
11 | class SitesTest(TestCase):
12 |
13 | def setUp(self):
14 | """
15 | Create two example sites that we can use to test what gets displayed
16 | where.
17 | """
18 | super().setUp()
19 |
20 | self.site1, created1 = Site.objects.get_or_create(
21 | domain="example.com", name="example.com")
22 | self.site2, created2 = Site.objects.get_or_create(
23 | domain="example.org", name="example.org")
24 |
25 | with self.settings(PHOTOLOGUE_MULTISITE=True):
26 | # Be explicit about linking Galleries/Photos to Sites."""
27 | self.gallery1 = GalleryFactory(slug='test-gallery', sites=[self.site1])
28 | self.gallery2 = GalleryFactory(slug='not-on-site-gallery')
29 | self.photo1 = PhotoFactory(slug='test-photo', sites=[self.site1])
30 | self.photo2 = PhotoFactory(slug='not-on-site-photo')
31 | self.gallery1.photos.add(self.photo1, self.photo2)
32 |
33 | # I'd like to use factory_boy's mute_signal decorator but that
34 | # will only available once factory_boy 2.4 is released. So long
35 | # we'll have to remove the site association manually
36 | self.photo2.sites.clear()
37 |
38 | def tearDown(self):
39 | super().tearDown()
40 | self.gallery1.delete()
41 | self.gallery2.delete()
42 | self.photo1.delete()
43 | self.photo2.delete()
44 |
45 | def test_basics(self):
46 | """ See if objects were added automatically (by the factory) to the current site. """
47 | self.assertEqual(list(self.gallery1.sites.all()), [self.site1])
48 | self.assertEqual(list(self.photo1.sites.all()), [self.site1])
49 |
50 | def test_auto_add_sites(self):
51 | """
52 | Objects should not be automatically associated with a particular site when
53 | ``PHOTOLOGUE_MULTISITE`` is ``True``.
54 | """
55 |
56 | with self.settings(PHOTOLOGUE_MULTISITE=False):
57 | gallery = GalleryFactory()
58 | photo = PhotoFactory()
59 | self.assertEqual(list(gallery.sites.all()), [self.site1])
60 | self.assertEqual(list(photo.sites.all()), [self.site1])
61 |
62 | photo.delete()
63 |
64 | with self.settings(PHOTOLOGUE_MULTISITE=True):
65 | gallery = GalleryFactory()
66 | photo = PhotoFactory()
67 | self.assertEqual(list(gallery.sites.all()), [])
68 | self.assertEqual(list(photo.sites.all()), [])
69 |
70 | photo.delete()
71 |
72 | def test_gallery_list(self):
73 | response = self.client.get('/ptests/gallerylist/')
74 | self.assertEqual(list(response.context['object_list']), [self.gallery1])
75 |
76 | def test_gallery_detail(self):
77 | response = self.client.get('/ptests/gallery/test-gallery/')
78 | self.assertEqual(response.context['object'], self.gallery1)
79 |
80 | response = self.client.get('/ptests/gallery/not-on-site-gallery/')
81 | self.assertEqual(response.status_code, 404)
82 |
83 | def test_photo_list(self):
84 | response = self.client.get('/ptests/photolist/')
85 | self.assertEqual(list(response.context['object_list']), [self.photo1])
86 |
87 | def test_photo_detail(self):
88 | response = self.client.get('/ptests/photo/test-photo/')
89 | self.assertEqual(response.context['object'], self.photo1)
90 |
91 | response = self.client.get('/ptests/photo/not-on-site-photo/')
92 | self.assertEqual(response.status_code, 404)
93 |
94 | def test_photo_archive(self):
95 | response = self.client.get('/ptests/photo/')
96 | self.assertEqual(list(response.context['object_list']), [self.photo1])
97 |
98 | def test_photos_in_gallery(self):
99 | """
100 | Only those photos are supposed to be shown in a gallery that are
101 | also associated with the current site.
102 | """
103 | response = self.client.get('/ptests/gallery/test-gallery/')
104 | self.assertEqual(list(response.context['object'].public()), [self.photo1])
105 |
106 | @unittest.skipUnless('django.contrib.sitemaps' in settings.INSTALLED_APPS,
107 | 'Sitemaps not installed in this project, nothing to test.')
108 | def test_sitemap(self):
109 | """A sitemap should only show objects associated with the current site."""
110 | response = self.client.get('/sitemap.xml')
111 |
112 | # Check photos.
113 | self.assertContains(response,
114 | 'http://example.com/ptests/photo/test-photo/'
115 | '2011-12-23')
116 | self.assertNotContains(response,
117 | 'http://example.com/ptests/photo/not-on-site-photo/'
118 | '2011-12-23')
119 |
120 | # Check galleries.
121 | self.assertContains(response,
122 | 'http://example.com/ptests/gallery/test-gallery/'
123 | '2011-12-23')
124 | self.assertNotContains(response,
125 | 'http://example.com/ptests/gallery/not-on-site-gallery/'
126 | '2011-12-23')
127 |
128 | def test_orphaned_photos(self):
129 | self.assertEqual(list(self.gallery1.orphaned_photos()), [self.photo2])
130 |
131 | self.gallery2.photos.add(self.photo2)
132 | self.assertEqual(list(self.gallery1.orphaned_photos()), [self.photo2])
133 |
134 | self.gallery1.sites.clear()
135 | self.assertEqual(list(self.gallery1.orphaned_photos()), [self.photo1, self.photo2])
136 |
137 | self.photo1.sites.clear()
138 | self.photo2.sites.clear()
139 | self.assertEqual(list(self.gallery1.orphaned_photos()), [self.photo1, self.photo2])
140 |
--------------------------------------------------------------------------------
/docs/pages/installation.rst:
--------------------------------------------------------------------------------
1 | ############################
2 | Installation & configuration
3 | ############################
4 |
5 |
6 | .. _installing-photologue-label:
7 |
8 | Installation
9 | ------------
10 | The easiest way to install Photologue is with `pip `_; this will give you the latest
11 | version available on `PyPi `_::
12 |
13 | pip install django-photologue
14 |
15 | You can also take risks and install the latest code directly from the
16 | Github repository::
17 |
18 | pip install -e git+https://github.com/richardbarran/django-photologue.git#egg=django-photologue
19 |
20 | This code should work ok - like `Django `_
21 | itself, we try to keep the master branch bug-free. However, we strongly recommend that you
22 | stick with a release from the PyPi repository, unless if you're confident in your abilities
23 | to fix any potential bugs on your own!
24 |
25 | Python 3
26 | ~~~~~~~~
27 | Photologue is compatible with Python 3 (3.3 or later).
28 |
29 | Dependencies
30 | ------------
31 | 3 apps that will be installed automatically if required.
32 |
33 | * `Django `_.
34 | * `Pillow `_.
35 | * `Django-sortedm2m `_.
36 |
37 | And 1 dependency that you will have to manage yourself:
38 |
39 | * `Pytz `_. See the Django release notes `for more information
40 | `_.
41 |
42 | .. note::
43 |
44 | Photologue tries to support the same Django version as are supported by the Django
45 | project itself.
46 |
47 | That troublesome Pillow...
48 | ~~~~~~~~~~~~~~~~~~~~~~~~~~
49 | Pillow can be tricky to install; sometimes it will install smoothly
50 | out of the box, sometimes you can spend hours figuring it out - installation
51 | issues vary from platform to platform, and from one OS release to the next, so listing
52 | them all here would not be realistic. Google
53 | is your friend!
54 |
55 | #. Pillow is a fork of PIL; you should not have installed both - this can cause strange bugs.
56 |
57 | #. Sometimes Pillow will install... but is not actually installed. This 'undocumented feature' has been
58 | reported by a user on Windows. If you can't get Photologue to display any images, check
59 | that you can actually import Pillow::
60 |
61 | $ python manage.py shell
62 | Python 3.3.1 (default, Sep 25 2013, 19:29:01)
63 | [GCC 4.7.3] on linux
64 | Type "help", "copyright", "credits" or "license" for more information.
65 | (InteractiveConsole)
66 | >>> from PIL import Image
67 | >>>
68 |
69 |
70 | Configure Your Django Settings file
71 | -----------------------------------
72 |
73 | Follow these 4 steps:
74 |
75 | #. Add to your ``INSTALLED_APPS`` setting::
76 |
77 | INSTALLED_APPS = (
78 | # ...other installed applications...
79 | 'photologue',
80 | 'sortedm2m',
81 | )
82 |
83 | #. Confirm that your `MEDIA_ROOT `_ and
84 | `MEDIA_URL `_ settings
85 | are correct (Photologue will store uploaded files in a folder called 'photologue' under your ``MEDIA_ROOT``).
86 |
87 | #. `Enable the admin app `_ if you have not already done so.
88 |
89 | #. `Enable the Django sites framework `_
90 | if you have not already done so. This is not enabled by default in Django, but is required by Photologue.
91 |
92 | Add the urls
93 | ------------
94 |
95 | Add photologue to your projects urls.py file::
96 |
97 | urlpatterns += [
98 | ...
99 | url(r'^photologue/', include('photologue.urls', namespace='photologue')),
100 | ]
101 |
102 | Sync Your Database
103 | ------------------
104 |
105 | You can now sync your database::
106 |
107 | python manage.py migrate photologue
108 |
109 | If you are installing Photologue for the first time, this will set up some
110 | default PhotoSizes to get you started - you are free to change them of course!
111 |
112 | Instant templates
113 | -----------------
114 |
115 | Photologue comes with basic templates for galleries and photos, which are designed
116 | to work well with `Twitter-Bootstrap `_.
117 | You can of course use them, or override them, or completely replace them. Note that all
118 | Photologue templates inherit from ``photologue/root.html``, which itself expects your site's
119 | base template to be called ``base.html`` - you can change this to use a different base template.
120 |
121 | Sitemap
122 | -------
123 |
124 | .. automodule:: photologue.sitemaps
125 |
126 | Sites
127 | -----
128 |
129 | Photologue supports `Django's site framework`_ since version 2.8. That means
130 | that each Gallery and each Photo can be displayed on one or more sites.
131 |
132 | Please bear in mind that photos don't necessarily have to be assigned to the
133 | same sites as the gallery they're belonging to: each gallery will only display
134 | the photos that are on its site. When a gallery does not belong the current site
135 | but a single photo is, that photo is only accessible directly as the gallery
136 | won't be shown in the index.
137 |
138 | .. note:: If you're upgrading from a version earlier than 2.8 you don't need to
139 | worry about the assignment of already existing objects to a site because a
140 | datamigration will assign all your objects to the current site automatically.
141 |
142 | .. note:: This feature is switched off by default. :ref:`See here to enable it
143 | ` and for more information.
144 |
145 | .. _Django's site framework: http://django.readthedocs.org/en/latest/ref/contrib/sites.html
146 |
147 | Amazon S3
148 | ---------
149 |
150 | Photologue can use a custom file storage system, for example
151 | `Amazon's S3 `_.
152 |
153 | You will need to configure your Django project to use Amazon S3 for storing files; a full discussion of
154 | how to do this is outside the scope of this page.
155 |
156 | However, there is a quick demo of using Photologue with S3 in the ``example_project`` directory; if you look
157 | at these files:
158 |
159 | * ``example_project/example_project/settings.py``
160 | * ``example_project/requirements.txt``
161 |
162 | At the end of each file you will commented-out lines for configuring S3 functionality. These point to extra files
163 | stored under ``example_project/example_storages/``. Uncomment these lines, run the example
164 | project, then study these files for inspiration! After that, setting up S3 will consist of
165 | (at minimum) the following steps:
166 |
167 | #. Signup for Amazon AWS S3 at http://aws.amazon.com/s3/.
168 | #. Create a Bucket on S3 to store your media and static files.
169 | #. Set the environment variables:
170 |
171 | * ``AWS_ACCESS_KEY_ID`` - issued to your account by S3.
172 | * ``AWS_SECRET_ACCESS_KEY`` - issued to your account by S3.
173 | * ``AWS_STORAGE_BUCKET_NAME`` - name of your bucket on S3.
174 |
175 | #. To copy your static files into your S3 Bucket, type ``python manage.py collectstatic`` in the ``example_project`` directory.
176 |
177 | .. note:: This simple setup does not handle S3 regions.
178 |
179 |
180 |
181 |
182 |
183 |
184 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | #
2 | # django-photologue documentation build configuration file, created by
3 | # sphinx-quickstart on Mon Sep 3 16:31:44 2012.
4 | #
5 | # This file is execfile()d with the current directory set to its containing dir.
6 | #
7 | # Note that not all possible configuration values are present in this
8 | # autogenerated file.
9 | #
10 | # All configuration values have a default; values that are commented out
11 | # serve to show the default.
12 |
13 | import datetime
14 | import os
15 | import sys
16 |
17 | import django
18 |
19 | # If extensions (or modules to document with autodoc) are in another directory,
20 | # add these directories to sys.path here. If the directory is relative to the
21 | # documentation root, use os.path.abspath to make it absolute, like shown here.
22 | sys.path.insert(0, os.path.abspath('..'))
23 | sys.path.append('../example_project/')
24 | os.environ['DJANGO_SETTINGS_MODULE'] = 'example_project.settings'
25 | django.setup()
26 |
27 | # -- General configuration -----------------------------------------------------
28 |
29 | # If your documentation needs a minimal Sphinx version, state it here.
30 | #needs_sphinx = '1.0'
31 |
32 | # Add any Sphinx extension module names here, as strings. They can be extensions
33 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
34 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest']
35 |
36 | # Add any paths that contain templates here, relative to this directory.
37 | templates_path = ['_templates']
38 |
39 | # The suffix of source filenames.
40 | source_suffix = '.rst'
41 |
42 | # The encoding of source files.
43 | #source_encoding = 'utf-8-sig'
44 |
45 | # The master toctree document.
46 | master_doc = 'index'
47 |
48 | # General information about the project.
49 | project = 'django-photologue'
50 | copyright = f'{datetime.datetime.now().year}, Justin Driscoll/Richard Barran'
51 |
52 | # The version info for the project you're documenting, acts as replacement for
53 | # |version| and |release|, also used in various other places throughout the
54 | # built documents.
55 | #
56 |
57 | # Dynamically get the version number from the photologue package.
58 | parent_dir = os.path.join(os.path.dirname(__file__), '..')
59 | sys.path.append(parent_dir)
60 | import photologue
61 |
62 | # The short X.Y version.
63 | version = '.'.join(photologue.__version__.split('.')[:1])
64 | # The full version, including alpha/beta/rc tags.
65 | release = photologue.__version__
66 |
67 | # The language for content autogenerated by Sphinx. Refer to documentation
68 | # for a list of supported languages.
69 | #language = None
70 |
71 | # There are two options for replacing |today|: either, you set today to some
72 | # non-false value, then it is used:
73 | #today = ''
74 | # Else, today_fmt is used as the format for a strftime call.
75 | #today_fmt = '%B %d, %Y'
76 |
77 | # List of patterns, relative to source directory, that match files and
78 | # directories to ignore when looking for source files.
79 | exclude_patterns = ['_build']
80 |
81 | # The reST default role (used for this markup: `text`) to use for all documents.
82 | #default_role = None
83 |
84 | # If true, '()' will be appended to :func: etc. cross-reference text.
85 | #add_function_parentheses = True
86 |
87 | # If true, the current module name will be prepended to all description
88 | # unit titles (such as .. function::).
89 | #add_module_names = True
90 |
91 | # If true, sectionauthor and moduleauthor directives will be shown in the
92 | # output. They are ignored by default.
93 | #show_authors = False
94 |
95 | # The name of the Pygments (syntax highlighting) style to use.
96 | pygments_style = 'sphinx'
97 |
98 | # A list of ignored prefixes for module index sorting.
99 | #modindex_common_prefix = []
100 |
101 |
102 | # -- Options for HTML output ---------------------------------------------------
103 |
104 | # The theme to use for HTML and HTML Help pages. See the documentation for
105 | # a list of builtin themes.
106 | html_theme = 'sphinx_rtd_theme'
107 |
108 | # Theme options are theme-specific and customize the look and feel of a theme
109 | # further. For a list of options available for each theme, see the
110 | # documentation.
111 | #html_theme_options = {}
112 |
113 | # Add any paths that contain custom themes here, relative to this directory.
114 | #html_theme_path = []
115 |
116 | # The name for this set of Sphinx documents. If None, it defaults to
117 | # " v documentation".
118 | #html_title = None
119 |
120 | # A shorter title for the navigation bar. Default is the same as html_title.
121 | #html_short_title = None
122 |
123 | # The name of an image file (relative to this directory) to place at the top
124 | # of the sidebar.
125 | #html_logo = None
126 |
127 | # The name of an image file (within the static path) to use as favicon of the
128 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
129 | # pixels large.
130 | #html_favicon = None
131 |
132 | # Add any paths that contain custom static files (such as style sheets) here,
133 | # relative to this directory. They are copied after the builtin static files,
134 | # so a file named "default.css" will overwrite the builtin "default.css".
135 | #html_static_path = ['_static']
136 |
137 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
138 | # using the given strftime format.
139 | #html_last_updated_fmt = '%b %d, %Y'
140 |
141 | # If true, SmartyPants will be used to convert quotes and dashes to
142 | # typographically correct entities.
143 | #html_use_smartypants = True
144 |
145 | # Custom sidebar templates, maps document names to template names.
146 | #html_sidebars = {}
147 |
148 | # Additional templates that should be rendered to pages, maps page names to
149 | # template names.
150 | #html_additional_pages = {}
151 |
152 | # If false, no module index is generated.
153 | #html_domain_indices = True
154 |
155 | # If false, no index is generated.
156 | #html_use_index = True
157 |
158 | # If true, the index is split into individual pages for each letter.
159 | #html_split_index = False
160 |
161 | # If true, links to the reST sources are added to the pages.
162 | #html_show_sourcelink = True
163 |
164 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
165 | #html_show_sphinx = True
166 |
167 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
168 | #html_show_copyright = True
169 |
170 | # If true, an OpenSearch description file will be output, and all pages will
171 | # contain a tag referring to it. The value of this option must be the
172 | # base URL from which the finished HTML is served.
173 | #html_use_opensearch = ''
174 |
175 | # This is the file name suffix for HTML files (e.g. ".xhtml").
176 | #html_file_suffix = None
177 |
178 | # Output file base name for HTML help builder.
179 | htmlhelp_basename = 'django-photologuedoc'
180 |
181 |
182 | # -- Options for LaTeX output --------------------------------------------------
183 |
184 | latex_elements = {
185 | # The paper size ('letterpaper' or 'a4paper').
186 | #'papersize': 'letterpaper',
187 |
188 | # The font size ('10pt', '11pt' or '12pt').
189 | #'pointsize': '10pt',
190 |
191 | # Additional stuff for the LaTeX preamble.
192 | #'preamble': '',
193 | }
194 |
195 | # Grouping the document tree into LaTeX files. List of tuples
196 | # (source start file, target name, title, author, documentclass [howto/manual]).
197 | latex_documents = [
198 | ('index', 'django-photologue.tex', 'django-photologue Documentation',
199 | 'Justin Driscoll/Richard Barran', 'manual'),
200 | ]
201 |
202 | # The name of an image file (relative to this directory) to place at the top of
203 | # the title page.
204 | #latex_logo = None
205 |
206 | # For "manual" documents, if this is true, then toplevel headings are parts,
207 | # not chapters.
208 | #latex_use_parts = False
209 |
210 | # If true, show page references after internal links.
211 | #latex_show_pagerefs = False
212 |
213 | # If true, show URL addresses after external links.
214 | #latex_show_urls = False
215 |
216 | # Documents to append as an appendix to all manuals.
217 | #latex_appendices = []
218 |
219 | # If false, no module index is generated.
220 | #latex_domain_indices = True
221 |
222 |
223 | # -- Options for manual page output --------------------------------------------
224 |
225 | # One entry per manual page. List of tuples
226 | # (source start file, name, description, authors, manual section).
227 | man_pages = [
228 | ('index', 'django-photologue', 'django-photologue Documentation',
229 | ['Justin Driscoll/Richard Barran'], 1)
230 | ]
231 |
232 | # If true, show URL addresses after external links.
233 | #man_show_urls = False
234 |
235 |
236 | # -- Options for Texinfo output ------------------------------------------------
237 |
238 | # Grouping the document tree into Texinfo files. List of tuples
239 | # (source start file, target name, title, author,
240 | # dir menu entry, description, category)
241 | texinfo_documents = [
242 | ('index', 'django-photologue', 'django-photologue Documentation',
243 | 'Justin Driscoll/Richard Barran', 'django-photologue', 'One line description of project.',
244 | 'Miscellaneous'),
245 | ]
246 |
247 | # Documents to append as an appendix to all manuals.
248 | #texinfo_appendices = []
249 |
250 | # If false, no module index is generated.
251 | #texinfo_domain_indices = True
252 |
253 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
254 | #texinfo_show_urls = 'footnote'
255 |
--------------------------------------------------------------------------------
/photologue/tests/test_zipupload.py:
--------------------------------------------------------------------------------
1 | import copy
2 |
3 | from django import VERSION
4 | from django.contrib.auth.models import User
5 | from django.test import TestCase
6 |
7 | from ..models import Gallery, Photo
8 | from .factories import (IGNORED_FILES_ZIP_PATH, LANDSCAPE_IMAGE_PATH, SAMPLE_NOT_IMAGE_ZIP_PATH, SAMPLE_ZIP_PATH,
9 | GalleryFactory, PhotoFactory)
10 |
11 |
12 | class GalleryUploadTest(TestCase):
13 |
14 | """Testing the admin page that allows users to upload zips."""
15 |
16 | def setUp(self):
17 | super().setUp()
18 | user = User.objects.create_user('john.doe',
19 | 'john.doe@example.com',
20 | 'secret')
21 | user.is_staff = True
22 | user.save()
23 | self.assertTrue(self.client.login(username='john.doe', password='secret'))
24 |
25 | self.zip_file = open(SAMPLE_ZIP_PATH, mode='rb')
26 |
27 | self.sample_form_data = {'zip_file': self.zip_file,
28 | 'title': 'This is a test title'}
29 |
30 | def tearDown(self):
31 | super().tearDown()
32 | self.zip_file.close()
33 | for photo in Photo.objects.all():
34 | photo.delete()
35 |
36 | def test_get(self):
37 | """We can get the custom admin page."""
38 |
39 | response = self.client.get('/admin/photologue/photo/upload_zip/')
40 | self.assertEqual(response.status_code, 200)
41 | self.assertTemplateUsed(response, 'admin/photologue/photo/upload_zip.html')
42 |
43 | self.assertContains(response, 'Upload a zip archive of photos')
44 |
45 | def test_breadcrumbs(self):
46 | """Quick check that the breadcrumbs are generated correctly."""
47 |
48 | response = self.client.get('/admin/photologue/photo/upload_zip/')
49 | self.assertContains(
50 | response, """