photo-objects 0.4.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {photo_objects-0.4.0 → photo_objects-0.5.0}/PKG-INFO +2 -2
- {photo_objects-0.4.0 → photo_objects-0.5.0}/README.md +1 -1
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/config.py +6 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/admin.py +2 -1
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/api/album.py +1 -22
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/api/auth.py +3 -3
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/api/photo.py +4 -4
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/api/utils.py +2 -2
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/forms.py +2 -2
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/management/commands/clean-scaled-photos.py +1 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/management/commands/create-initial-admin-account.py +6 -4
- photo_objects-0.5.0/photo_objects/django/migrations/0005_sitesettings.py +27 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/models.py +42 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/signals.py +1 -32
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/templatetags/photo_objects_extras.py +7 -5
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/tests/test_album.py +7 -30
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/tests/test_auth.py +2 -2
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/tests/test_commands.py +12 -44
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/tests/test_img.py +3 -3
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/tests/test_photo.py +3 -3
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/tests/test_utils.py +24 -3
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/tests/utils.py +35 -1
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/urls.py +1 -2
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/views/api/photo.py +1 -1
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/views/ui/album.py +9 -15
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/views/ui/configuration.py +29 -42
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/views/ui/photo.py +8 -4
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/views/ui/users.py +4 -4
- photo_objects-0.5.0/photo_objects/django/views/utils.py +60 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/img.py +1 -1
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects.egg-info/PKG-INFO +2 -2
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects.egg-info/SOURCES.txt +1 -1
- {photo_objects-0.4.0 → photo_objects-0.5.0}/pyproject.toml +1 -1
- photo_objects-0.4.0/photo_objects/django/management/commands/create-site-albums.py +0 -28
- photo_objects-0.4.0/photo_objects/django/views/utils.py +0 -12
- {photo_objects-0.4.0 → photo_objects-0.5.0}/LICENSE +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/__init__.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/__init__.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/api/__init__.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/apps.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/conf.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/context_processors.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/management/__init__.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/management/commands/__init__.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/migrations/0001_initial.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/migrations/0002_created_at_updated_at.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/migrations/0003_admin_visibility.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/migrations/0004_camera_setup_and_settings.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/migrations/__init__.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/objsto.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/templatetags/__init__.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/tests/__init__.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/views/__init__.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/views/api/__init__.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/views/api/album.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/views/api/auth.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/views/api/utils.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/views/ui/__init__.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/views/ui/utils.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/error.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/utils.py +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects.egg-info/dependency_links.txt +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects.egg-info/requires.txt +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects.egg-info/top_level.txt +0 -0
- {photo_objects-0.4.0 → photo_objects-0.5.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: photo-objects
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Application for storing photos in S3 compatible object-storage.
|
|
5
5
|
Author: Toni Kangas
|
|
6
6
|
License: MIT License
|
|
@@ -74,7 +74,7 @@ autopep8 -aaar --in-place --exclude back/api/settings.py,*/migrations/*.py back
|
|
|
74
74
|
Run static analysis with:
|
|
75
75
|
|
|
76
76
|
```sh
|
|
77
|
-
pylint
|
|
77
|
+
pylint back/api photo_objects
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
### Integration tests
|
|
@@ -26,7 +26,7 @@ autopep8 -aaar --in-place --exclude back/api/settings.py,*/migrations/*.py back
|
|
|
26
26
|
Run static analysis with:
|
|
27
27
|
|
|
28
28
|
```sh
|
|
29
|
-
pylint
|
|
29
|
+
pylint back/api photo_objects
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
### Integration tests
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import re
|
|
2
|
-
|
|
3
|
-
from django.contrib.sites.models import Site
|
|
4
1
|
from django.db.models.deletion import ProtectedError
|
|
5
2
|
from django.http import HttpRequest
|
|
6
3
|
|
|
@@ -16,24 +13,6 @@ from .utils import (
|
|
|
16
13
|
)
|
|
17
14
|
|
|
18
15
|
|
|
19
|
-
def get_site_album(site: Site) -> Album:
|
|
20
|
-
album_key = f'_site_{site.id}'
|
|
21
|
-
return Album.objects.get_or_create(
|
|
22
|
-
key=album_key,
|
|
23
|
-
defaults={
|
|
24
|
-
'title': site.name,
|
|
25
|
-
'visibility': Album.Visibility.ADMIN,
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def parse_site_id(album_key: str) -> int | None:
|
|
30
|
-
key_match = re.match(r'_site_([0-9]+)', album_key)
|
|
31
|
-
if not key_match:
|
|
32
|
-
return None
|
|
33
|
-
|
|
34
|
-
return int(key_match.group(1))
|
|
35
|
-
|
|
36
|
-
|
|
37
16
|
def get_albums(request: HttpRequest):
|
|
38
17
|
if not request.user.is_authenticated:
|
|
39
18
|
return Album.objects.filter(visibility=Album.Visibility.PUBLIC)
|
|
@@ -89,4 +68,4 @@ def delete_album(request: HttpRequest, album_key: str):
|
|
|
89
68
|
f"Album with {album_key} key can not be deleted because it "
|
|
90
69
|
"contains photos.",
|
|
91
70
|
409,
|
|
92
|
-
)
|
|
71
|
+
) from None
|
|
@@ -16,7 +16,7 @@ def check_album_access(request: HttpRequest, album_key: str):
|
|
|
16
16
|
try:
|
|
17
17
|
album = Album.objects.get(key=album_key)
|
|
18
18
|
except Album.DoesNotExist:
|
|
19
|
-
raise AlbumNotFound(album_key)
|
|
19
|
+
raise AlbumNotFound(album_key) from None
|
|
20
20
|
|
|
21
21
|
if not request.user.is_authenticated:
|
|
22
22
|
if album.visibility == Album.Visibility.PRIVATE:
|
|
@@ -37,12 +37,12 @@ def check_photo_access(
|
|
|
37
37
|
try:
|
|
38
38
|
size = PhotoSize(size_key)
|
|
39
39
|
except ValueError:
|
|
40
|
-
raise InvalidSize(size_key)
|
|
40
|
+
raise InvalidSize(size_key) from None
|
|
41
41
|
|
|
42
42
|
try:
|
|
43
43
|
photo = Photo.objects.get(key=join_key(album_key, photo_key))
|
|
44
44
|
except Photo.DoesNotExist:
|
|
45
|
-
raise PhotoNotFound(album_key, photo_key)
|
|
45
|
+
raise PhotoNotFound(album_key, photo_key) from None
|
|
46
46
|
|
|
47
47
|
if not request.user.is_authenticated:
|
|
48
48
|
if photo.album.visibility == Album.Visibility.PRIVATE:
|
|
@@ -36,7 +36,7 @@ def _upload_photo(album_key: str, photo_file: UploadedFile):
|
|
|
36
36
|
raise JsonProblem(
|
|
37
37
|
"Could not open photo file.",
|
|
38
38
|
400,
|
|
39
|
-
)
|
|
39
|
+
) from None
|
|
40
40
|
|
|
41
41
|
f = CreatePhotoForm(dict(
|
|
42
42
|
key=f"{album_key}/{slugify(photo_file.name)}",
|
|
@@ -59,7 +59,7 @@ def _upload_photo(album_key: str, photo_file: UploadedFile):
|
|
|
59
59
|
msg = objsto.with_error_code(
|
|
60
60
|
"Could not save photo to object storage", e)
|
|
61
61
|
logger.error(f"{msg}: {str(e)}")
|
|
62
|
-
raise JsonProblem(f"{msg}.", 500)
|
|
62
|
+
raise JsonProblem(f"{msg}.", 500) from e
|
|
63
63
|
|
|
64
64
|
return photo
|
|
65
65
|
|
|
@@ -125,11 +125,11 @@ def delete_photo(request: HttpRequest, album_key: str, photo_key: str):
|
|
|
125
125
|
msg = objsto.with_error_code(
|
|
126
126
|
"Could not delete photo from object storage", e)
|
|
127
127
|
logger.error(f"{msg}: {str(e)}")
|
|
128
|
-
raise JsonProblem("{msg}.", 500)
|
|
128
|
+
raise JsonProblem(f"{msg}.", 500) from e
|
|
129
129
|
|
|
130
130
|
try:
|
|
131
131
|
photo.delete()
|
|
132
132
|
except Exception as e:
|
|
133
133
|
msg = "Could not delete photo from database"
|
|
134
134
|
logger.error(f"{msg}: {str(e)}")
|
|
135
|
-
raise JsonProblem(f"{msg}.", 500)
|
|
135
|
+
raise JsonProblem(f"{msg}.", 500) from e
|
|
@@ -50,7 +50,7 @@ class JsonProblem(PhotoObjectsError):
|
|
|
50
50
|
return render(request, "photo_objects/problem.html", {
|
|
51
51
|
"title": "Error",
|
|
52
52
|
"back": BackLink(
|
|
53
|
-
|
|
53
|
+
'Back to albums',
|
|
54
54
|
reverse_lazy('photo_objects:list_albums')),
|
|
55
55
|
"problem_title": self.title,
|
|
56
56
|
"status": self.status
|
|
@@ -153,7 +153,7 @@ def parse_json_body(request: HttpRequest):
|
|
|
153
153
|
raise JsonProblem(
|
|
154
154
|
"Could not parse JSON data from request body.",
|
|
155
155
|
400,
|
|
156
|
-
)
|
|
156
|
+
) from None
|
|
157
157
|
|
|
158
158
|
|
|
159
159
|
def parse_input_data(request: HttpRequest):
|
|
@@ -23,9 +23,9 @@ KEY_POSTFIX_CHARS = 'bcdfghjklmnpqrstvwxz2456789'
|
|
|
23
23
|
KEY_POSTFIX_LEN = 5
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def slugify(
|
|
26
|
+
def slugify(title: str, lower=False, replace_leading_underscores=False) -> str:
|
|
27
27
|
key = unicodedata.normalize(
|
|
28
|
-
'NFKD',
|
|
28
|
+
'NFKD', title).encode(
|
|
29
29
|
'ascii', 'ignore').decode('ascii')
|
|
30
30
|
if lower:
|
|
31
31
|
key = key.lower()
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
# pylint: disable=invalid-name
|
|
2
|
+
from secrets import token_urlsafe
|
|
3
|
+
|
|
1
4
|
from django.core.management.base import BaseCommand
|
|
2
5
|
from django.contrib.auth import get_user_model
|
|
3
|
-
from secrets import token_urlsafe
|
|
4
6
|
|
|
5
7
|
from photo_objects.config import write_to_home_directory
|
|
6
8
|
|
|
@@ -9,13 +11,13 @@ class Command(BaseCommand):
|
|
|
9
11
|
help = "Create initial admin user account."
|
|
10
12
|
|
|
11
13
|
def handle(self, *args, **options):
|
|
12
|
-
|
|
13
|
-
superuser_count =
|
|
14
|
+
user = get_user_model()
|
|
15
|
+
superuser_count = user.objects.filter(is_superuser=True).count()
|
|
14
16
|
|
|
15
17
|
if superuser_count == 0:
|
|
16
18
|
username = 'admin'
|
|
17
19
|
password = token_urlsafe(32)
|
|
18
|
-
|
|
20
|
+
user.objects.create_superuser(username, password=password)
|
|
19
21
|
|
|
20
22
|
write_to_home_directory("initial_admin_password", password)
|
|
21
23
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Generated by Django 5.2 on 2025-08-26 18:39
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
dependencies = [
|
|
10
|
+
('photo_objects', '0004_camera_setup_and_settings'),
|
|
11
|
+
('sites', '0002_alter_domain_unique'),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.CreateModel(
|
|
16
|
+
name='SiteSettings',
|
|
17
|
+
fields=[
|
|
18
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
19
|
+
('description', models.TextField(blank=True)),
|
|
20
|
+
('preview_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='photo_objects.photo')),
|
|
21
|
+
('site', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='settings', to='sites.site')),
|
|
22
|
+
],
|
|
23
|
+
options={
|
|
24
|
+
'verbose_name_plural': 'site settings',
|
|
25
|
+
},
|
|
26
|
+
),
|
|
27
|
+
]
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
from django.db import models
|
|
2
|
+
from django.db.models.signals import pre_delete, pre_save
|
|
3
|
+
from django.contrib.sites.models import Site
|
|
2
4
|
from django.core.validators import RegexValidator
|
|
3
5
|
from django.utils.translation import gettext_lazy as _
|
|
4
6
|
|
|
@@ -146,3 +148,43 @@ class Photo(BaseModel):
|
|
|
146
148
|
exposure_time=self.exposure_time,
|
|
147
149
|
iso_speed=self.iso_speed,
|
|
148
150
|
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
SETTINGS_CACHE = {}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class SiteSettingsManager(models.Manager):
|
|
157
|
+
def get(self, site: Site):
|
|
158
|
+
cached = SETTINGS_CACHE.get(site.id)
|
|
159
|
+
if cached:
|
|
160
|
+
return cached
|
|
161
|
+
settings, _ = self.get_or_create(site=site)
|
|
162
|
+
SETTINGS_CACHE[site.id] = settings
|
|
163
|
+
return settings
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class SiteSettings(models.Model):
|
|
167
|
+
class Meta:
|
|
168
|
+
verbose_name_plural = "site settings"
|
|
169
|
+
|
|
170
|
+
site = models.OneToOneField(
|
|
171
|
+
Site, on_delete=models.CASCADE, related_name="settings")
|
|
172
|
+
|
|
173
|
+
description = models.TextField(blank=True)
|
|
174
|
+
preview_image = models.ForeignKey(
|
|
175
|
+
Photo, blank=True, null=True, on_delete=models.SET_NULL)
|
|
176
|
+
|
|
177
|
+
objects = SiteSettingsManager()
|
|
178
|
+
|
|
179
|
+
def __str__(self):
|
|
180
|
+
return f"Settings for {self.site.name}"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def clear_cached_settings(sender, **kwargs):
|
|
184
|
+
site_id = kwargs.get("instance").site.id
|
|
185
|
+
if site_id in SETTINGS_CACHE:
|
|
186
|
+
del SETTINGS_CACHE[site_id]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
pre_save.connect(clear_cached_settings, sender=SiteSettings)
|
|
190
|
+
pre_delete.connect(clear_cached_settings, sender=SiteSettings)
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import re
|
|
2
|
-
|
|
3
|
-
from django.contrib.sites.models import Site
|
|
4
1
|
from django.db.models.signals import post_save, post_delete
|
|
5
2
|
from django.dispatch import receiver
|
|
6
3
|
|
|
7
|
-
from .
|
|
8
|
-
from .models import Album, Photo
|
|
4
|
+
from .models import Photo
|
|
9
5
|
|
|
10
6
|
|
|
11
7
|
@receiver(post_save, sender=Photo)
|
|
@@ -60,30 +56,3 @@ def update_album_on_photo_delete(sender, **kwargs):
|
|
|
60
56
|
|
|
61
57
|
if needs_save:
|
|
62
58
|
album.save()
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
@receiver(post_save, sender=Site)
|
|
66
|
-
def update_album_on_site_save(sender, **kwargs):
|
|
67
|
-
site = kwargs.get('instance', None)
|
|
68
|
-
album, created = get_site_album(site)
|
|
69
|
-
|
|
70
|
-
if not created and album.title != site.name:
|
|
71
|
-
album.title = site.name
|
|
72
|
-
album.save()
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
@receiver(post_save, sender=Album)
|
|
76
|
-
def update_site_on_album_save(sender, **kwargs):
|
|
77
|
-
album = kwargs.get('instance', None)
|
|
78
|
-
site_id = parse_site_id(album.key)
|
|
79
|
-
if site_id is None:
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
try:
|
|
83
|
-
site = Site.objects.get(id=site_id)
|
|
84
|
-
except Site.DoesNotExist:
|
|
85
|
-
return
|
|
86
|
-
|
|
87
|
-
if site.name != album.title:
|
|
88
|
-
site.name = album.title
|
|
89
|
-
site.save()
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
from django import template
|
|
3
3
|
|
|
4
|
-
from photo_objects.django.
|
|
4
|
+
from photo_objects.django.models import SiteSettings
|
|
5
|
+
from photo_objects.django.views.utils import meta_description
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
register = template.Library()
|
|
@@ -47,13 +48,14 @@ def meta_og(context):
|
|
|
47
48
|
try:
|
|
48
49
|
request = context.get("request")
|
|
49
50
|
site = request.site
|
|
50
|
-
|
|
51
|
+
|
|
52
|
+
settings = SiteSettings.objects.get(site)
|
|
51
53
|
|
|
52
54
|
return {
|
|
53
55
|
'request': request,
|
|
54
|
-
"title":
|
|
55
|
-
"description":
|
|
56
|
-
"photo":
|
|
56
|
+
"title": site.name,
|
|
57
|
+
"description": meta_description(request, settings.description),
|
|
58
|
+
"photo": settings.preview_image,
|
|
57
59
|
}
|
|
58
60
|
except Exception:
|
|
59
61
|
return context
|
|
@@ -3,7 +3,6 @@ from time import sleep
|
|
|
3
3
|
|
|
4
4
|
from django.contrib.auth import get_user_model
|
|
5
5
|
from django.contrib.auth.models import Permission
|
|
6
|
-
from django.contrib.sites.models import Site
|
|
7
6
|
|
|
8
7
|
from photo_objects.django.models import Album
|
|
9
8
|
from photo_objects.img import utcnow
|
|
@@ -22,9 +21,9 @@ PHOTOS_DIRECTORY = "photos"
|
|
|
22
21
|
class ViewVisibilityTests(TestCase):
|
|
23
22
|
@classmethod
|
|
24
23
|
def setUpTestData(cls):
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
user = get_user_model()
|
|
25
|
+
user.objects.create_user(username='test-visibility', password='test')
|
|
26
|
+
user.objects.create_user(
|
|
28
27
|
username='test-staff-visibility',
|
|
29
28
|
password='test',
|
|
30
29
|
is_staff=True)
|
|
@@ -91,16 +90,16 @@ class ViewVisibilityTests(TestCase):
|
|
|
91
90
|
|
|
92
91
|
class AlbumViewTests(TestCase):
|
|
93
92
|
def setUp(self):
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
user = get_user_model()
|
|
94
|
+
user.objects.create_user(username='no_permission', password='test')
|
|
96
95
|
|
|
97
|
-
|
|
96
|
+
user.objects.create_user(
|
|
98
97
|
username='superuser',
|
|
99
98
|
password='test',
|
|
100
99
|
is_staff=True,
|
|
101
100
|
is_superuser=True)
|
|
102
101
|
|
|
103
|
-
has_permission =
|
|
102
|
+
has_permission = user.objects.create_user(
|
|
104
103
|
username='has_permission', password='test')
|
|
105
104
|
permissions = [
|
|
106
105
|
'add_album',
|
|
@@ -376,25 +375,3 @@ class AlbumViewTests(TestCase):
|
|
|
376
375
|
|
|
377
376
|
response = self.client.delete("/api/albums/copenhagen")
|
|
378
377
|
self.assertStatus(response, 204)
|
|
379
|
-
|
|
380
|
-
def test_site_config_album_title_change(self):
|
|
381
|
-
site = Site.objects.get(id=1)
|
|
382
|
-
album, _ = Album.objects.get_or_create(
|
|
383
|
-
key="_site_1",
|
|
384
|
-
defaults={
|
|
385
|
-
"title": "Site 1",
|
|
386
|
-
"visibility": Album.Visibility.ADMIN,
|
|
387
|
-
}
|
|
388
|
-
)
|
|
389
|
-
|
|
390
|
-
site.name = "Changed in site"
|
|
391
|
-
site.save()
|
|
392
|
-
|
|
393
|
-
album.refresh_from_db()
|
|
394
|
-
self.assertEqual(album.title, "Changed in site")
|
|
395
|
-
|
|
396
|
-
album.title = "Changed in album"
|
|
397
|
-
album.save()
|
|
398
|
-
|
|
399
|
-
site.refresh_from_db()
|
|
400
|
-
self.assertEqual(site.name, "Changed in album")
|
|
@@ -12,8 +12,8 @@ def _path_fn(album, photo):
|
|
|
12
12
|
class AuthViewTests(TestCase):
|
|
13
13
|
@classmethod
|
|
14
14
|
def setUpTestData(cls):
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
user = get_user_model()
|
|
16
|
+
user.objects.create_user(username='test-auth', password='test')
|
|
17
17
|
|
|
18
18
|
public_album = Album.objects.create(
|
|
19
19
|
key="test-auth-public", visibility=Album.Visibility.PUBLIC)
|
|
@@ -2,9 +2,7 @@ from io import StringIO
|
|
|
2
2
|
|
|
3
3
|
from django.contrib.auth import get_user_model
|
|
4
4
|
from django.core.management import call_command
|
|
5
|
-
from minio import S3Error
|
|
6
5
|
|
|
7
|
-
from photo_objects.django import objsto
|
|
8
6
|
from photo_objects.django.conf import CONFIGURABLE_PHOTO_SIZES
|
|
9
7
|
from photo_objects.django.models import Album
|
|
10
8
|
|
|
@@ -13,8 +11,8 @@ from .utils import TestCase, open_test_photo
|
|
|
13
11
|
|
|
14
12
|
class PhotoViewTests(TestCase):
|
|
15
13
|
def setUp(self):
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
user = get_user_model()
|
|
15
|
+
user.objects.create_user(
|
|
18
16
|
username='superuser',
|
|
19
17
|
password='test',
|
|
20
18
|
is_staff=True,
|
|
@@ -30,36 +28,6 @@ class PhotoViewTests(TestCase):
|
|
|
30
28
|
f"/api/albums/{album_key}/photos/{photo_key}/img?size={size}")
|
|
31
29
|
self.assertStatus(response, 200)
|
|
32
30
|
|
|
33
|
-
def assertPhotoFound(self, album_key, photo_key, sizes):
|
|
34
|
-
if not isinstance(sizes, list):
|
|
35
|
-
sizes = [sizes]
|
|
36
|
-
|
|
37
|
-
for size in sizes:
|
|
38
|
-
try:
|
|
39
|
-
objsto.get_photo(album_key, photo_key, size)
|
|
40
|
-
except S3Error as e:
|
|
41
|
-
if e.code == "NoSuchKey":
|
|
42
|
-
raise AssertionError(
|
|
43
|
-
f"Photo not found: {size}/{album_key}/{photo_key}")
|
|
44
|
-
else:
|
|
45
|
-
raise e
|
|
46
|
-
|
|
47
|
-
def assertPhotoNotFound(self, album_key, photo_key, sizes):
|
|
48
|
-
if not isinstance(sizes, list):
|
|
49
|
-
sizes = [sizes]
|
|
50
|
-
|
|
51
|
-
for size in sizes:
|
|
52
|
-
with self.assertRaises(
|
|
53
|
-
S3Error,
|
|
54
|
-
msg=f"Photo found: {size}/{album_key}/{photo_key}"
|
|
55
|
-
) as e:
|
|
56
|
-
objsto.get_photo(album_key, photo_key, size)
|
|
57
|
-
|
|
58
|
-
self.assertEqual(
|
|
59
|
-
e.exception.code,
|
|
60
|
-
"NoSuchKey",
|
|
61
|
-
f"Photo not found: {size}/{album_key}/{photo_key}")
|
|
62
|
-
|
|
63
31
|
def test_clean_scaled_photos(self):
|
|
64
32
|
login_success = self.client.login(
|
|
65
33
|
username='superuser', password='test')
|
|
@@ -73,23 +41,23 @@ class PhotoViewTests(TestCase):
|
|
|
73
41
|
self.assertStatus(response, 201)
|
|
74
42
|
|
|
75
43
|
self._scale_image("test-photo-sizes", "tower.jpg")
|
|
76
|
-
self.
|
|
77
|
-
|
|
44
|
+
self.assertPhotoInObjsto(
|
|
45
|
+
"test-photo-sizes", "tower.jpg", ["sm", "md", "lg", "og"])
|
|
78
46
|
|
|
79
47
|
out = StringIO()
|
|
80
48
|
call_command('clean-scaled-photos', stdout=out)
|
|
81
49
|
output = out.getvalue()
|
|
82
50
|
self.assertIn("No previous photo sizes configuration found", output)
|
|
83
51
|
self.assertIn("Total deleted photos: 3", output)
|
|
84
|
-
self.
|
|
52
|
+
self.assertPhotoNotInObjsto(
|
|
85
53
|
"test-photo-sizes",
|
|
86
54
|
"tower.jpg",
|
|
87
55
|
CONFIGURABLE_PHOTO_SIZES)
|
|
88
|
-
self.
|
|
56
|
+
self.assertPhotoInObjsto("test-photo-sizes", "tower.jpg", "og")
|
|
89
57
|
|
|
90
58
|
self._scale_image("test-photo-sizes", "tower.jpg")
|
|
91
|
-
self.
|
|
92
|
-
|
|
59
|
+
self.assertPhotoInObjsto(
|
|
60
|
+
"test-photo-sizes", "tower.jpg", ["sm", "md", "lg", "og"])
|
|
93
61
|
|
|
94
62
|
with self.settings(PHOTO_OBJECTS_PHOTO_SIZES=dict(
|
|
95
63
|
sm=dict(max_width=256, max_height=256),
|
|
@@ -101,12 +69,12 @@ class PhotoViewTests(TestCase):
|
|
|
101
69
|
"Found changes in photo sizes configuration for sm sizes.",
|
|
102
70
|
output)
|
|
103
71
|
self.assertIn("Total deleted photos: 1", output)
|
|
104
|
-
self.
|
|
105
|
-
self.
|
|
106
|
-
|
|
72
|
+
self.assertPhotoNotInObjsto("test-photo-sizes", "tower.jpg", "sm")
|
|
73
|
+
self.assertPhotoInObjsto(
|
|
74
|
+
"test-photo-sizes", "tower.jpg", ["md", "lg", "og"])
|
|
107
75
|
|
|
108
76
|
response = self.client.delete(
|
|
109
77
|
"/api/albums/test-photo-sizes/photos/tower.jpg")
|
|
110
78
|
self.assertStatus(response, 204)
|
|
111
|
-
self.
|
|
79
|
+
self.assertPhotoNotInObjsto(
|
|
112
80
|
"test-photo-sizes", "tower.jpg", ["sm", "md", "lg", "og"])
|
|
@@ -16,10 +16,10 @@ class ImgTests(TestCase):
|
|
|
16
16
|
((1000, 1000), (512, 512)),
|
|
17
17
|
]
|
|
18
18
|
|
|
19
|
-
for
|
|
20
|
-
with self.subTest(w=
|
|
19
|
+
for size, expected in testdata:
|
|
20
|
+
with self.subTest(w=size[0], h=size[1]):
|
|
21
21
|
original = BytesIO()
|
|
22
|
-
image = Image.new("RGB",
|
|
22
|
+
image = Image.new("RGB", size, color="red")
|
|
23
23
|
image.save(original, format="JPEG")
|
|
24
24
|
original.seek(0)
|
|
25
25
|
|
|
@@ -18,10 +18,10 @@ from .utils import TestCase, open_test_photo, parse_timestamps
|
|
|
18
18
|
|
|
19
19
|
class PhotoViewTests(TestCase):
|
|
20
20
|
def setUp(self):
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
user = get_user_model()
|
|
22
|
+
user.objects.create_user(username='no_permission', password='test')
|
|
23
23
|
|
|
24
|
-
has_permission =
|
|
24
|
+
has_permission = user.objects.create_user(
|
|
25
25
|
username='has_permission', password='test')
|
|
26
26
|
permissions = [
|
|
27
27
|
'add_photo',
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from unittest import TestCase
|
|
2
|
+
from unittest.mock import MagicMock
|
|
2
3
|
|
|
3
4
|
from minio import S3Error
|
|
4
5
|
|
|
5
6
|
from photo_objects.django import objsto
|
|
6
7
|
from photo_objects.django.forms import slugify
|
|
8
|
+
from photo_objects.django.views.utils import meta_description
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
class TestUtils(TestCase):
|
|
@@ -16,9 +18,9 @@ class TestUtils(TestCase):
|
|
|
16
18
|
("album__photo_-key", "album-photo-key"),
|
|
17
19
|
]
|
|
18
20
|
|
|
19
|
-
for
|
|
20
|
-
with self.subTest(input=
|
|
21
|
-
self.assertEqual(slugify(
|
|
21
|
+
for title, expected in checks:
|
|
22
|
+
with self.subTest(input=title, expected=expected):
|
|
23
|
+
self.assertEqual(slugify(title), expected)
|
|
22
24
|
|
|
23
25
|
def test_slugify_lower(self):
|
|
24
26
|
self.assertEqual(slugify("QwErTy!", True), "qwerty-")
|
|
@@ -41,3 +43,22 @@ class TestUtils(TestCase):
|
|
|
41
43
|
objsto.with_error_code("Failed", e),
|
|
42
44
|
"Failed (Test)",
|
|
43
45
|
)
|
|
46
|
+
|
|
47
|
+
def test_meta_description(self):
|
|
48
|
+
md_multi_p = (
|
|
49
|
+
"Description with **bold** and *italics*...\n\n"
|
|
50
|
+
"...and multiple paragraphs")
|
|
51
|
+
testdata = [
|
|
52
|
+
("Plain text description",
|
|
53
|
+
"Plain text description"),
|
|
54
|
+
(md_multi_p,
|
|
55
|
+
"Description with bold and italics..."),
|
|
56
|
+
(None,
|
|
57
|
+
"A simple self-hosted photo server."),
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
for description, expected in testdata:
|
|
61
|
+
with self.subTest(expected=expected):
|
|
62
|
+
self.assertEqual(
|
|
63
|
+
meta_description(MagicMock(), description),
|
|
64
|
+
expected)
|
|
@@ -5,7 +5,9 @@ from django.conf import settings
|
|
|
5
5
|
from django.test import TestCase as DjangoTestCase, override_settings
|
|
6
6
|
from django.utils import timezone
|
|
7
7
|
from django.utils.dateparse import parse_datetime
|
|
8
|
+
from minio import S3Error
|
|
8
9
|
|
|
10
|
+
from photo_objects.django import objsto
|
|
9
11
|
from photo_objects.django.models import Album, Photo
|
|
10
12
|
from photo_objects.django.objsto import _objsto_access
|
|
11
13
|
|
|
@@ -38,8 +40,9 @@ def _objsto_test_settings():
|
|
|
38
40
|
|
|
39
41
|
@override_settings(PHOTO_OBJECTS_OBJSTO=_objsto_test_settings())
|
|
40
42
|
class TestCase(DjangoTestCase):
|
|
43
|
+
# pylint: disable=invalid-name
|
|
41
44
|
@classmethod
|
|
42
|
-
def tearDownClass(
|
|
45
|
+
def tearDownClass(cls):
|
|
43
46
|
client, bucket = _objsto_access()
|
|
44
47
|
|
|
45
48
|
for i in client.list_objects(bucket, recursive=True):
|
|
@@ -47,6 +50,37 @@ class TestCase(DjangoTestCase):
|
|
|
47
50
|
|
|
48
51
|
client.remove_bucket(bucket)
|
|
49
52
|
|
|
53
|
+
def assertPhotoInObjsto(self, album_key, photo_key, sizes):
|
|
54
|
+
if not isinstance(sizes, list):
|
|
55
|
+
sizes = [sizes]
|
|
56
|
+
|
|
57
|
+
for size in sizes:
|
|
58
|
+
try:
|
|
59
|
+
objsto.get_photo(album_key, photo_key, size)
|
|
60
|
+
except S3Error as e:
|
|
61
|
+
if e.code == "NoSuchKey":
|
|
62
|
+
raise AssertionError(
|
|
63
|
+
f"Photo not found: {size}/{album_key}/{photo_key}"
|
|
64
|
+
) from None
|
|
65
|
+
else:
|
|
66
|
+
raise e
|
|
67
|
+
|
|
68
|
+
def assertPhotoNotInObjsto(self, album_key, photo_key, sizes):
|
|
69
|
+
if not isinstance(sizes, list):
|
|
70
|
+
sizes = [sizes]
|
|
71
|
+
|
|
72
|
+
for size in sizes:
|
|
73
|
+
with self.assertRaises(
|
|
74
|
+
S3Error,
|
|
75
|
+
msg=f"Photo found: {size}/{album_key}/{photo_key}"
|
|
76
|
+
) as e:
|
|
77
|
+
objsto.get_photo(album_key, photo_key, size)
|
|
78
|
+
|
|
79
|
+
self.assertEqual(
|
|
80
|
+
e.exception.code,
|
|
81
|
+
"NoSuchKey",
|
|
82
|
+
f"Photo not found: {size}/{album_key}/{photo_key}")
|
|
83
|
+
|
|
50
84
|
def assertTimestampLess(self, a, b, **kwargs):
|
|
51
85
|
'''Assert a is less than b. Automatically parses strings to datetime
|
|
52
86
|
objects.
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
from django.contrib.auth import views as auth_views
|
|
2
2
|
from django.http import HttpResponseRedirect
|
|
3
|
-
from django.urls import path
|
|
3
|
+
from django.urls import path
|
|
4
4
|
|
|
5
5
|
from .views import api, ui
|
|
6
|
-
from .views.utils import BackLink
|
|
7
6
|
|
|
8
7
|
app_name = "photo_objects"
|
|
9
8
|
urlpatterns = [
|
|
@@ -82,7 +82,7 @@ def get_img(request: HttpRequest, album_key: str, photo_key: str):
|
|
|
82
82
|
album_key, photo_key, PhotoSize.ORIGINAL.value)
|
|
83
83
|
except (S3Error, HTTPError) as e:
|
|
84
84
|
msg = objsto.with_error_code(
|
|
85
|
-
|
|
85
|
+
"Could not fetch photo from object storage", e)
|
|
86
86
|
logger.error(f"{msg}: {str(e)}")
|
|
87
87
|
|
|
88
88
|
code = objsto.get_error_code(e)
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import re
|
|
2
|
-
|
|
3
1
|
from django.http import HttpRequest, HttpResponseRedirect
|
|
4
2
|
from django.shortcuts import render
|
|
5
3
|
from django.urls import reverse
|
|
6
4
|
from django.utils.translation import gettext_lazy as _
|
|
7
5
|
|
|
8
6
|
from photo_objects.django import api
|
|
9
|
-
from photo_objects.django.api.album import parse_site_id
|
|
10
7
|
from photo_objects.django.api.utils import FormValidationFailed
|
|
11
8
|
from photo_objects.django.forms import CreateAlbumForm, ModifyAlbumForm
|
|
12
9
|
from photo_objects.django.models import Album
|
|
13
|
-
from photo_objects.django.views.utils import
|
|
10
|
+
from photo_objects.django.views.utils import (
|
|
11
|
+
BackLink,
|
|
12
|
+
meta_description,
|
|
13
|
+
render_markdown,
|
|
14
|
+
)
|
|
14
15
|
|
|
15
16
|
from .utils import json_problem_as_html
|
|
16
17
|
|
|
@@ -49,15 +50,8 @@ def new_album(request: HttpRequest):
|
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
def get_info(request: HttpRequest, album_key: str):
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return render_markdown(
|
|
55
|
-
"This is a special album for configuring site metadata for "
|
|
56
|
-
f"**{request.site.name}**. Use album title to change the site "
|
|
57
|
-
"name, albums cover photo to configure the preview image, and "
|
|
58
|
-
"album description to configure the site description. The album "
|
|
59
|
-
"title is automatically updated when the related sites name is "
|
|
60
|
-
"changed and vice versa.")
|
|
53
|
+
# TODO: Remove this later if not needed
|
|
54
|
+
return None
|
|
61
55
|
|
|
62
56
|
|
|
63
57
|
@json_problem_as_html
|
|
@@ -75,7 +69,7 @@ def show_album(request: HttpRequest, album_key: str):
|
|
|
75
69
|
"album": album,
|
|
76
70
|
"photos": photos,
|
|
77
71
|
"title": album.title or album.key,
|
|
78
|
-
"description": album
|
|
72
|
+
"description": meta_description(request, album),
|
|
79
73
|
"back": back,
|
|
80
74
|
"details": details,
|
|
81
75
|
"photo": album.cover_photo,
|
|
@@ -147,7 +141,7 @@ def delete_album(request: HttpRequest, album_key: str):
|
|
|
147
141
|
'This album is managed by the system and can not be deleted.')}
|
|
148
142
|
|
|
149
143
|
return render(request, 'photo_objects/delete.html', {
|
|
150
|
-
"title":
|
|
144
|
+
"title": "Delete album",
|
|
151
145
|
"back": back,
|
|
152
146
|
"photo": album.cover_photo,
|
|
153
147
|
"resource": target,
|
|
@@ -5,7 +5,7 @@ from django.shortcuts import render
|
|
|
5
5
|
from django.urls import reverse
|
|
6
6
|
from django.utils.translation import gettext_lazy as _
|
|
7
7
|
|
|
8
|
-
from photo_objects.django.
|
|
8
|
+
from photo_objects.django.api.utils import JsonProblem
|
|
9
9
|
from photo_objects.django.views.utils import BackLink, render_markdown
|
|
10
10
|
|
|
11
11
|
from .utils import json_problem_as_html
|
|
@@ -44,7 +44,7 @@ def uses_https(request: HttpRequest) -> Validation:
|
|
|
44
44
|
detail += _(
|
|
45
45
|
' If you are running the API server behind a reverse proxy or '
|
|
46
46
|
'a load-balancer, ensure that HTTPS termination is configured '
|
|
47
|
-
|
|
47
|
+
'correctly.')
|
|
48
48
|
|
|
49
49
|
return Validation(
|
|
50
50
|
check=_("Site is served over HTTPS"),
|
|
@@ -108,25 +108,20 @@ def domain_matches_request(request: HttpRequest) -> Validation:
|
|
|
108
108
|
)
|
|
109
109
|
|
|
110
110
|
|
|
111
|
-
def site_preview_configured(
|
|
112
|
-
request: HttpRequest,
|
|
113
|
-
album: Album | Exception) -> Validation:
|
|
111
|
+
def site_preview_configured(request: HttpRequest) -> Validation:
|
|
114
112
|
detail = None
|
|
115
113
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
detail =
|
|
114
|
+
ok = request.site.settings.preview_image is not None
|
|
115
|
+
if ok:
|
|
116
|
+
detail = (
|
|
117
|
+
f'The site settings for `{request.site.domain}` configure a '
|
|
118
|
+
'preview image.'
|
|
119
|
+
)
|
|
119
120
|
else:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
'photo will be used as the site preview image.'
|
|
125
|
-
)
|
|
126
|
-
else:
|
|
127
|
-
detail = (
|
|
128
|
-
f'Set cover photo for `{album.key}` album to configure '
|
|
129
|
-
'the preview image.')
|
|
121
|
+
detail = (
|
|
122
|
+
'Configure a preview image in site settings for '
|
|
123
|
+
f'`{request.site.domain}`.'
|
|
124
|
+
)
|
|
130
125
|
|
|
131
126
|
return Validation(
|
|
132
127
|
check=_("Site has a default preview image"),
|
|
@@ -135,25 +130,21 @@ def site_preview_configured(
|
|
|
135
130
|
)
|
|
136
131
|
|
|
137
132
|
|
|
138
|
-
def site_description_configured(
|
|
139
|
-
request: HttpRequest,
|
|
140
|
-
album: Album | Exception) -> Validation:
|
|
133
|
+
def site_description_configured(request: HttpRequest) -> Validation:
|
|
141
134
|
detail = None
|
|
142
135
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
136
|
+
settings = request.site.settings
|
|
137
|
+
ok = settings.description is not None and len(settings.description) > 0
|
|
138
|
+
if ok:
|
|
139
|
+
detail = (
|
|
140
|
+
f'The site settings for `{request.site.domain}` configure a '
|
|
141
|
+
'description.'
|
|
142
|
+
)
|
|
146
143
|
else:
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
'will be used as the site description.'
|
|
152
|
-
)
|
|
153
|
-
else:
|
|
154
|
-
detail = (
|
|
155
|
-
f'Set description for `{album.key}` album to configure '
|
|
156
|
-
'the site description.')
|
|
144
|
+
detail = (
|
|
145
|
+
'Configure a description in site settings for '
|
|
146
|
+
f'`{request.site.domain}`.'
|
|
147
|
+
)
|
|
157
148
|
|
|
158
149
|
return Validation(
|
|
159
150
|
check=_("Site has a default description"),
|
|
@@ -164,19 +155,15 @@ def site_description_configured(
|
|
|
164
155
|
|
|
165
156
|
@json_problem_as_html
|
|
166
157
|
def configuration(request: HttpRequest):
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
album_key = f"_site_{site_id}"
|
|
170
|
-
album = Album.objects.get(key=album_key)
|
|
171
|
-
except Exception as e:
|
|
172
|
-
album = e
|
|
158
|
+
if not request.user.is_staff:
|
|
159
|
+
raise JsonProblem("Page not found", status=404)
|
|
173
160
|
|
|
174
161
|
validations = [
|
|
175
162
|
uses_https(request),
|
|
176
163
|
site_is_configured(request),
|
|
177
164
|
domain_matches_request(request),
|
|
178
|
-
site_preview_configured(request
|
|
179
|
-
site_description_configured(request
|
|
165
|
+
site_preview_configured(request),
|
|
166
|
+
site_description_configured(request),
|
|
180
167
|
]
|
|
181
168
|
|
|
182
169
|
back = BackLink("Back to albums", reverse('photo_objects:list_albums'))
|
|
@@ -6,7 +6,11 @@ from photo_objects.django import api
|
|
|
6
6
|
from photo_objects.django.api.utils import AlbumNotFound, FormValidationFailed
|
|
7
7
|
from photo_objects.django.forms import ModifyPhotoForm, UploadPhotosForm
|
|
8
8
|
from photo_objects.django.models import Photo
|
|
9
|
-
from photo_objects.django.views.utils import
|
|
9
|
+
from photo_objects.django.views.utils import (
|
|
10
|
+
BackLink,
|
|
11
|
+
meta_description,
|
|
12
|
+
render_markdown,
|
|
13
|
+
)
|
|
10
14
|
|
|
11
15
|
from .utils import json_problem_as_html
|
|
12
16
|
|
|
@@ -40,8 +44,8 @@ def upload_photos(request: HttpRequest, album_key: str):
|
|
|
40
44
|
})
|
|
41
45
|
|
|
42
46
|
|
|
43
|
-
def _lower(
|
|
44
|
-
return
|
|
47
|
+
def _lower(value: str):
|
|
48
|
+
return value.lower() if value else ''
|
|
45
49
|
|
|
46
50
|
|
|
47
51
|
def _camera(photo: Photo):
|
|
@@ -128,7 +132,7 @@ def show_photo(request: HttpRequest, album_key: str, photo_key: str):
|
|
|
128
132
|
"previous_filename": previous_filename,
|
|
129
133
|
"next_filename": next_filename,
|
|
130
134
|
"title": photo.title or photo.filename,
|
|
131
|
-
"description": photo
|
|
135
|
+
"description": meta_description(request, photo),
|
|
132
136
|
"back": back,
|
|
133
137
|
"details": details,
|
|
134
138
|
})
|
|
@@ -2,21 +2,21 @@ from django.contrib.auth import views as auth_views
|
|
|
2
2
|
from django.http import HttpRequest
|
|
3
3
|
from django.urls import reverse_lazy
|
|
4
4
|
|
|
5
|
-
from photo_objects.django.
|
|
5
|
+
from photo_objects.django.models import SiteSettings
|
|
6
6
|
from photo_objects.django.views.utils import BackLink
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def login(request: HttpRequest):
|
|
10
|
-
|
|
10
|
+
settings = SiteSettings.objects.get(request.site)
|
|
11
11
|
|
|
12
12
|
return auth_views.LoginView.as_view(
|
|
13
13
|
template_name="photo_objects/form.html",
|
|
14
14
|
extra_context={
|
|
15
15
|
"title": "Login",
|
|
16
|
-
"photo":
|
|
16
|
+
"photo": settings.preview_image,
|
|
17
17
|
"action": "Login",
|
|
18
18
|
"back": BackLink(
|
|
19
|
-
|
|
19
|
+
'Back to albums',
|
|
20
20
|
reverse_lazy('photo_objects:list_albums')),
|
|
21
21
|
"class": "login"
|
|
22
22
|
},
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from xml.etree import ElementTree as ET
|
|
2
|
+
|
|
3
|
+
from django.http import HttpRequest
|
|
4
|
+
from django.utils.dateformat import format as format_date
|
|
5
|
+
from django.utils.safestring import mark_safe
|
|
6
|
+
from markdown import markdown
|
|
7
|
+
|
|
8
|
+
from photo_objects.django.models import Album, Photo
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BackLink:
|
|
12
|
+
def __init__(self, text, url):
|
|
13
|
+
self.text = text
|
|
14
|
+
self.url = url
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def render_markdown(value):
|
|
18
|
+
return mark_safe(markdown(value))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _first_paragraph_textcontent(raw) -> str | None:
|
|
22
|
+
html = render_markdown(raw)
|
|
23
|
+
root = ET.fromstring(f"<root>{html}</root>")
|
|
24
|
+
|
|
25
|
+
first = root.find("p")
|
|
26
|
+
if first is None:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
return ''.join(first.itertext())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _default_album_description(request: HttpRequest, album: Album) -> str:
|
|
33
|
+
count = album.photo_set.count()
|
|
34
|
+
plural = 's' if count != 1 else ''
|
|
35
|
+
return f"Album with {count} photo{plural} in {request.site.name}."
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _default_photo_description(photo: Photo) -> str:
|
|
39
|
+
date_str = format_date(photo.timestamp, "F Y")
|
|
40
|
+
return f"Photo from {date_str} in {photo.album.title} album."
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def meta_description(
|
|
44
|
+
request: HttpRequest,
|
|
45
|
+
resource: Album | Photo | str | None) -> str:
|
|
46
|
+
text = None
|
|
47
|
+
if isinstance(resource, Album):
|
|
48
|
+
text = (
|
|
49
|
+
_first_paragraph_textcontent(resource.description) or
|
|
50
|
+
_default_album_description(request, resource))
|
|
51
|
+
|
|
52
|
+
if isinstance(resource, Photo):
|
|
53
|
+
text = (
|
|
54
|
+
_first_paragraph_textcontent(resource.description) or
|
|
55
|
+
_default_photo_description(resource))
|
|
56
|
+
|
|
57
|
+
if isinstance(resource, str):
|
|
58
|
+
text = _first_paragraph_textcontent(resource)
|
|
59
|
+
|
|
60
|
+
return text or "A simple self-hosted photo server."
|
|
@@ -30,6 +30,7 @@ class ExifReader:
|
|
|
30
30
|
value = d.get(key)
|
|
31
31
|
if value is not None:
|
|
32
32
|
return value
|
|
33
|
+
return None
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
def _read_original_datetime(image: Image) -> datetime:
|
|
@@ -63,7 +64,6 @@ def _read_camera_setup_and_settings(image: Image) -> dict:
|
|
|
63
64
|
)
|
|
64
65
|
except Exception as e:
|
|
65
66
|
raise e
|
|
66
|
-
return dict()
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
def _image_format(filename):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: photo-objects
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Application for storing photos in S3 compatible object-storage.
|
|
5
5
|
Author: Toni Kangas
|
|
6
6
|
License: MIT License
|
|
@@ -74,7 +74,7 @@ autopep8 -aaar --in-place --exclude back/api/settings.py,*/migrations/*.py back
|
|
|
74
74
|
Run static analysis with:
|
|
75
75
|
|
|
76
76
|
```sh
|
|
77
|
-
pylint
|
|
77
|
+
pylint back/api photo_objects
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
### Integration tests
|
|
@@ -30,11 +30,11 @@ photo_objects/django/management/__init__.py
|
|
|
30
30
|
photo_objects/django/management/commands/__init__.py
|
|
31
31
|
photo_objects/django/management/commands/clean-scaled-photos.py
|
|
32
32
|
photo_objects/django/management/commands/create-initial-admin-account.py
|
|
33
|
-
photo_objects/django/management/commands/create-site-albums.py
|
|
34
33
|
photo_objects/django/migrations/0001_initial.py
|
|
35
34
|
photo_objects/django/migrations/0002_created_at_updated_at.py
|
|
36
35
|
photo_objects/django/migrations/0003_admin_visibility.py
|
|
37
36
|
photo_objects/django/migrations/0004_camera_setup_and_settings.py
|
|
37
|
+
photo_objects/django/migrations/0005_sitesettings.py
|
|
38
38
|
photo_objects/django/migrations/__init__.py
|
|
39
39
|
photo_objects/django/templatetags/__init__.py
|
|
40
40
|
photo_objects/django/templatetags/photo_objects_extras.py
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
from django.core.management.base import BaseCommand
|
|
2
|
-
from django.contrib.sites.models import Site
|
|
3
|
-
|
|
4
|
-
from photo_objects.django.api.album import get_site_album
|
|
5
|
-
from photo_objects.django.models import Album
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class Command(BaseCommand):
|
|
9
|
-
help = "Create albums for configuring site metadata."
|
|
10
|
-
|
|
11
|
-
def handle(self, *args, **options):
|
|
12
|
-
sites = Site.objects.all()
|
|
13
|
-
|
|
14
|
-
for site in sites:
|
|
15
|
-
album, created = get_site_album(site)
|
|
16
|
-
if created:
|
|
17
|
-
self.stdout.write(
|
|
18
|
-
self.style.SUCCESS(
|
|
19
|
-
f'Album for site {site.domain} created:') +
|
|
20
|
-
f'\n Key: {album.key}'
|
|
21
|
-
f'\n Title: {album.title}')
|
|
22
|
-
else:
|
|
23
|
-
self.stdout.write(
|
|
24
|
-
self.style.NOTICE(
|
|
25
|
-
f'Album creation for site {site.domain} skipped: '
|
|
26
|
-
'Album already exists.'
|
|
27
|
-
)
|
|
28
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/management/commands/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{photo_objects-0.4.0 → photo_objects-0.5.0}/photo_objects/django/migrations/0003_admin_visibility.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|