photo-objects 0.0.2__py3-none-any.whl → 0.0.3__py3-none-any.whl
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/django/api/album.py +7 -0
- photo_objects/django/forms.py +24 -3
- photo_objects/django/management/commands/create-initial-admin-account.py +1 -3
- photo_objects/django/management/commands/create-site-albums.py +32 -0
- photo_objects/django/migrations/0004_camera_setup_and_settings.py +63 -0
- photo_objects/django/models.py +20 -2
- photo_objects/django/templatetags/photo_objects_extras.py +30 -0
- photo_objects/django/tests/test_album.py +1 -1
- photo_objects/django/tests/test_photo.py +8 -0
- photo_objects/django/tests/test_utils.py +7 -0
- photo_objects/django/urls.py +5 -0
- photo_objects/django/views/ui/__init__.py +1 -0
- photo_objects/django/views/ui/album.py +16 -0
- photo_objects/django/views/ui/configuration.py +117 -0
- photo_objects/django/views/ui/photo.py +39 -0
- photo_objects/img.py +38 -6
- {photo_objects-0.0.2.dist-info → photo_objects-0.0.3.dist-info}/METADATA +1 -1
- {photo_objects-0.0.2.dist-info → photo_objects-0.0.3.dist-info}/RECORD +21 -18
- {photo_objects-0.0.2.dist-info → photo_objects-0.0.3.dist-info}/WHEEL +0 -0
- {photo_objects-0.0.2.dist-info → photo_objects-0.0.3.dist-info}/licenses/LICENSE +0 -0
- {photo_objects-0.0.2.dist-info → photo_objects-0.0.3.dist-info}/top_level.txt +0 -0
|
@@ -54,6 +54,13 @@ def delete_album(request: HttpRequest, album_key: str):
|
|
|
54
54
|
check_permissions(request, 'photo_objects.delete_album')
|
|
55
55
|
album = check_album_access(request, album_key)
|
|
56
56
|
|
|
57
|
+
if album.key.startswith('_'):
|
|
58
|
+
raise JsonProblem(
|
|
59
|
+
f"Album with {album_key} key is managed by the system and can not "
|
|
60
|
+
"be deleted.",
|
|
61
|
+
409,
|
|
62
|
+
)
|
|
63
|
+
|
|
57
64
|
try:
|
|
58
65
|
album.delete()
|
|
59
66
|
except ProtectedError:
|
photo_objects/django/forms.py
CHANGED
|
@@ -23,14 +23,19 @@ KEY_POSTFIX_CHARS = 'bcdfghjklmnpqrstvwxz2456789'
|
|
|
23
23
|
KEY_POSTFIX_LEN = 5
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def slugify(input: str, lower=False) -> str:
|
|
26
|
+
def slugify(input: str, lower=False, replace_leading_underscores=False) -> str:
|
|
27
27
|
key = unicodedata.normalize(
|
|
28
28
|
'NFKD', input).encode(
|
|
29
29
|
'ascii', 'ignore').decode('ascii')
|
|
30
30
|
if lower:
|
|
31
31
|
key = key.lower()
|
|
32
|
+
|
|
32
33
|
key = re.sub(r'[^a-zA-Z0-9._-]', '-', key)
|
|
33
34
|
key = re.sub(r'[-_]{2,}', '-', key)
|
|
35
|
+
|
|
36
|
+
if replace_leading_underscores:
|
|
37
|
+
key = re.sub(r'^_+', '-', key)
|
|
38
|
+
|
|
34
39
|
return key
|
|
35
40
|
|
|
36
41
|
|
|
@@ -87,6 +92,13 @@ class CreateAlbumForm(ModelForm):
|
|
|
87
92
|
|
|
88
93
|
# If key is set to _new, generate a key from the title.
|
|
89
94
|
if key != '_new':
|
|
95
|
+
if key.startswith('_'):
|
|
96
|
+
self.add_error(
|
|
97
|
+
'key',
|
|
98
|
+
ValidationError(
|
|
99
|
+
_('Keys starting with underscore are reserved for '
|
|
100
|
+
'system albums.'),
|
|
101
|
+
code='invalid'))
|
|
90
102
|
return
|
|
91
103
|
|
|
92
104
|
if title == '':
|
|
@@ -97,7 +109,7 @@ class CreateAlbumForm(ModelForm):
|
|
|
97
109
|
code='required'))
|
|
98
110
|
return
|
|
99
111
|
|
|
100
|
-
key = slugify(title, True)
|
|
112
|
+
key = slugify(title, lower=True, replace_leading_underscores=True)
|
|
101
113
|
|
|
102
114
|
postfix_iter = _postfix_generator()
|
|
103
115
|
try:
|
|
@@ -170,7 +182,16 @@ class CreatePhotoForm(ModelForm):
|
|
|
170
182
|
'timestamp',
|
|
171
183
|
'height',
|
|
172
184
|
'width',
|
|
173
|
-
'tiny_base64'
|
|
185
|
+
'tiny_base64',
|
|
186
|
+
'camera_make',
|
|
187
|
+
'camera_model',
|
|
188
|
+
'lens_make',
|
|
189
|
+
'lens_model',
|
|
190
|
+
'focal_length',
|
|
191
|
+
'f_number',
|
|
192
|
+
'exposure_time',
|
|
193
|
+
'iso_speed',
|
|
194
|
+
]
|
|
174
195
|
error_messages = {
|
|
175
196
|
'album': {
|
|
176
197
|
'invalid_choice': _('Album with %(value)s key does not exist.')
|
|
@@ -19,13 +19,11 @@ class Command(BaseCommand):
|
|
|
19
19
|
|
|
20
20
|
write_to_home_directory("initial_admin_password", password)
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
self.stdout.write(
|
|
23
23
|
self.style.SUCCESS('Initial admin account created:') +
|
|
24
24
|
f'\n Username: {username}'
|
|
25
25
|
f'\n Password: {password}'
|
|
26
26
|
)
|
|
27
|
-
|
|
28
|
-
self.stdout.write(msg)
|
|
29
27
|
else:
|
|
30
28
|
self.stdout.write(
|
|
31
29
|
self.style.NOTICE(
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from django.core.management.base import BaseCommand
|
|
2
|
+
from django.contrib.sites.models import Site
|
|
3
|
+
|
|
4
|
+
from photo_objects.django.models import Album
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Command(BaseCommand):
|
|
8
|
+
help = "Create albums for configuring site metadata."
|
|
9
|
+
|
|
10
|
+
def handle(self, *args, **options):
|
|
11
|
+
sites = Site.objects.all()
|
|
12
|
+
|
|
13
|
+
for site in sites:
|
|
14
|
+
album_key = f'_site_{site.id}'
|
|
15
|
+
_, created = Album.objects.get_or_create(
|
|
16
|
+
key=album_key,
|
|
17
|
+
defaults={
|
|
18
|
+
'visibility': Album.Visibility.ADMIN,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
if created:
|
|
22
|
+
self.stdout.write(
|
|
23
|
+
self.style.SUCCESS(
|
|
24
|
+
f'Album for site {site.domain} created:') +
|
|
25
|
+
f'\n Key: {album_key}')
|
|
26
|
+
else:
|
|
27
|
+
self.stdout.write(
|
|
28
|
+
self.style.NOTICE(
|
|
29
|
+
f'Album creation for site {site.domain} skipped: '
|
|
30
|
+
'Album already exists.'
|
|
31
|
+
)
|
|
32
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Generated by Django 5.0.7 on 2025-04-05 21:45
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('photo_objects', '0003_admin_visibility'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='photo',
|
|
15
|
+
name='camera_make',
|
|
16
|
+
field=models.CharField(blank=True),
|
|
17
|
+
),
|
|
18
|
+
migrations.AddField(
|
|
19
|
+
model_name='photo',
|
|
20
|
+
name='camera_model',
|
|
21
|
+
field=models.CharField(blank=True),
|
|
22
|
+
),
|
|
23
|
+
migrations.AddField(
|
|
24
|
+
model_name='photo',
|
|
25
|
+
name='exposure_time',
|
|
26
|
+
field=models.FloatField(blank=True, null=True),
|
|
27
|
+
),
|
|
28
|
+
migrations.AddField(
|
|
29
|
+
model_name='photo',
|
|
30
|
+
name='f_number',
|
|
31
|
+
field=models.FloatField(blank=True, null=True),
|
|
32
|
+
),
|
|
33
|
+
migrations.AddField(
|
|
34
|
+
model_name='photo',
|
|
35
|
+
name='focal_length',
|
|
36
|
+
field=models.FloatField(blank=True, null=True),
|
|
37
|
+
),
|
|
38
|
+
migrations.AddField(
|
|
39
|
+
model_name='photo',
|
|
40
|
+
name='iso_speed',
|
|
41
|
+
field=models.IntegerField(blank=True, null=True),
|
|
42
|
+
),
|
|
43
|
+
migrations.AddField(
|
|
44
|
+
model_name='photo',
|
|
45
|
+
name='lens_make',
|
|
46
|
+
field=models.CharField(blank=True),
|
|
47
|
+
),
|
|
48
|
+
migrations.AddField(
|
|
49
|
+
model_name='photo',
|
|
50
|
+
name='lens_model',
|
|
51
|
+
field=models.CharField(blank=True),
|
|
52
|
+
),
|
|
53
|
+
migrations.AlterField(
|
|
54
|
+
model_name='album',
|
|
55
|
+
name='first_timestamp',
|
|
56
|
+
field=models.DateTimeField(blank=True, null=True),
|
|
57
|
+
),
|
|
58
|
+
migrations.AlterField(
|
|
59
|
+
model_name='album',
|
|
60
|
+
name='last_timestamp',
|
|
61
|
+
field=models.DateTimeField(blank=True, null=True),
|
|
62
|
+
),
|
|
63
|
+
]
|
photo_objects/django/models.py
CHANGED
|
@@ -65,8 +65,8 @@ class Album(BaseModel):
|
|
|
65
65
|
null=True,
|
|
66
66
|
on_delete=models.SET_NULL,
|
|
67
67
|
related_name="+")
|
|
68
|
-
first_timestamp = models.DateTimeField(null=True)
|
|
69
|
-
last_timestamp = models.DateTimeField(null=True)
|
|
68
|
+
first_timestamp = models.DateTimeField(blank=True, null=True)
|
|
69
|
+
last_timestamp = models.DateTimeField(blank=True, null=True)
|
|
70
70
|
|
|
71
71
|
def __str__(self):
|
|
72
72
|
return _str(self.key, title=self.title, visibility=self.visibility)
|
|
@@ -96,6 +96,16 @@ class Photo(BaseModel):
|
|
|
96
96
|
width = models.PositiveIntegerField()
|
|
97
97
|
tiny_base64 = models.TextField(blank=True)
|
|
98
98
|
|
|
99
|
+
camera_make = models.CharField(blank=True)
|
|
100
|
+
camera_model = models.CharField(blank=True)
|
|
101
|
+
lens_make = models.CharField(blank=True)
|
|
102
|
+
lens_model = models.CharField(blank=True)
|
|
103
|
+
|
|
104
|
+
focal_length = models.FloatField(blank=True, null=True)
|
|
105
|
+
f_number = models.FloatField(blank=True, null=True)
|
|
106
|
+
exposure_time = models.FloatField(blank=True, null=True)
|
|
107
|
+
iso_speed = models.IntegerField(blank=True, null=True)
|
|
108
|
+
|
|
99
109
|
def __str__(self):
|
|
100
110
|
return _str(
|
|
101
111
|
self.key,
|
|
@@ -127,4 +137,12 @@ class Photo(BaseModel):
|
|
|
127
137
|
height=self.height,
|
|
128
138
|
width=self.width,
|
|
129
139
|
tiny_base64=self.tiny_base64,
|
|
140
|
+
camera_make=self.camera_make,
|
|
141
|
+
camera_model=self.camera_model,
|
|
142
|
+
lens_make=self.lens_make,
|
|
143
|
+
lens_model=self.lens_model,
|
|
144
|
+
focal_length=self.focal_length,
|
|
145
|
+
f_number=self.f_number,
|
|
146
|
+
exposure_time=self.exposure_time,
|
|
147
|
+
iso_speed=self.iso_speed,
|
|
130
148
|
)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from django import template
|
|
2
2
|
|
|
3
|
+
from photo_objects.django.models import Album
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
register = template.Library()
|
|
5
7
|
|
|
@@ -21,3 +23,31 @@ def display_name(user):
|
|
|
21
23
|
if user.first_name or user.last_name:
|
|
22
24
|
return f'{user.first_name} {user.last_name}'.strip()
|
|
23
25
|
return user.username
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@register.filter
|
|
29
|
+
def is_list(value):
|
|
30
|
+
return isinstance(value, list)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@register.inclusion_tag("photo_objects/meta-og.html", takes_context=True)
|
|
34
|
+
def meta_og(context):
|
|
35
|
+
photo = context.get("photo")
|
|
36
|
+
title = context.get("title")
|
|
37
|
+
|
|
38
|
+
if photo and title:
|
|
39
|
+
return context
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
request = context.get("request")
|
|
43
|
+
site = request.site
|
|
44
|
+
album = Album.objects.get(key=f'_site_{site.id}')
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
'request': request,
|
|
48
|
+
"title": album.title or site.name,
|
|
49
|
+
"description": album.description,
|
|
50
|
+
"photo": album.cover_photo,
|
|
51
|
+
}
|
|
52
|
+
except Exception:
|
|
53
|
+
return context
|
|
@@ -249,7 +249,7 @@ class AlbumViewTests(TestCase):
|
|
|
249
249
|
username='has_permission', password='test')
|
|
250
250
|
self.assertTrue(login_success)
|
|
251
251
|
|
|
252
|
-
for key in ["", "#invalid", "()"]:
|
|
252
|
+
for key in ["", "#invalid", "()", "_reserved"]:
|
|
253
253
|
response = self.client.post(
|
|
254
254
|
"/api/albums",
|
|
255
255
|
content_type="application/json",
|
|
@@ -245,6 +245,14 @@ class PhotoViewTests(TestCase):
|
|
|
245
245
|
self.assertEqual(data.get("timestamp"), "2024-03-20T14:28:04+00:00")
|
|
246
246
|
self.assertEqual(data.get("height"), 512)
|
|
247
247
|
self.assertEqual(data.get("width"), 341)
|
|
248
|
+
self.assertEqual(data.get("camera_make"), "FUJIFILM")
|
|
249
|
+
self.assertEqual(data.get("camera_model"), "X-E3")
|
|
250
|
+
self.assertEqual(data.get("lens_make"), "FUJIFILM")
|
|
251
|
+
self.assertEqual(data.get("lens_model"), "XF23mmF2 R WR")
|
|
252
|
+
self.assertEqual(data.get("focal_length"), 23.0)
|
|
253
|
+
self.assertEqual(data.get("f_number"), 8.0)
|
|
254
|
+
self.assertEqual(data.get("exposure_time"), 0.00025)
|
|
255
|
+
self.assertEqual(data.get("iso_speed"), 800)
|
|
248
256
|
|
|
249
257
|
tic = utcnow()
|
|
250
258
|
sleep(0.1)
|
|
@@ -23,6 +23,13 @@ class TestUtils(TestCase):
|
|
|
23
23
|
def test_slugify_lower(self):
|
|
24
24
|
self.assertEqual(slugify("QwErTy!", True), "qwerty-")
|
|
25
25
|
|
|
26
|
+
def test_slugify_replace_leading_underscores(self):
|
|
27
|
+
self.assertEqual(
|
|
28
|
+
slugify(
|
|
29
|
+
"__SecretAlbum",
|
|
30
|
+
replace_leading_underscores=True),
|
|
31
|
+
"-SecretAlbum")
|
|
32
|
+
|
|
26
33
|
def test_with_error_code(self):
|
|
27
34
|
self.assertEqual(
|
|
28
35
|
objsto.with_error_code("Failed", Exception('TEST')),
|
photo_objects/django/urls.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
1
3
|
from django.http import HttpRequest, HttpResponseRedirect
|
|
2
4
|
from django.shortcuts import render
|
|
3
5
|
from django.urls import reverse
|
|
@@ -45,6 +47,15 @@ def new_album(request: HttpRequest):
|
|
|
45
47
|
})
|
|
46
48
|
|
|
47
49
|
|
|
50
|
+
def get_info(request: HttpRequest, album_key: str):
|
|
51
|
+
if re.match(r'_site_[0-9]+', album_key) and request.site:
|
|
52
|
+
return (
|
|
53
|
+
"This is a special album for configuring site metadata for "
|
|
54
|
+
f"{request.site.name}. Use album title to override the site name, "
|
|
55
|
+
"albums cover photo to configure the preview image, and album "
|
|
56
|
+
"description to configure the site description.")
|
|
57
|
+
|
|
58
|
+
|
|
48
59
|
@json_problem_as_html
|
|
49
60
|
def show_album(request: HttpRequest, album_key: str):
|
|
50
61
|
album = api.check_album_access(request, album_key)
|
|
@@ -64,6 +75,7 @@ def show_album(request: HttpRequest, album_key: str):
|
|
|
64
75
|
"back": back,
|
|
65
76
|
"details": details,
|
|
66
77
|
"photo": album.cover_photo,
|
|
78
|
+
"info": get_info(request, album_key),
|
|
67
79
|
})
|
|
68
80
|
|
|
69
81
|
|
|
@@ -102,6 +114,7 @@ def edit_album(request: HttpRequest, album_key: str):
|
|
|
102
114
|
"title": "Edit album",
|
|
103
115
|
"back": back,
|
|
104
116
|
"photo": album.cover_photo,
|
|
117
|
+
"info": get_info(request, album_key),
|
|
105
118
|
})
|
|
106
119
|
|
|
107
120
|
|
|
@@ -125,6 +138,9 @@ def delete_album(request: HttpRequest, album_key: str):
|
|
|
125
138
|
error = {'error': _(
|
|
126
139
|
'Album can not be deleted because it contains photos. Delete '
|
|
127
140
|
'all photos from the album to be able to delete the album.')}
|
|
141
|
+
if album.key.startswith('_'):
|
|
142
|
+
error = {'error': _(
|
|
143
|
+
'This album is managed by the system and can not be deleted.')}
|
|
128
144
|
|
|
129
145
|
return render(request, 'photo_objects/delete.html', {
|
|
130
146
|
"title": f"Delete album",
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from django.http import HttpRequest
|
|
4
|
+
from django.shortcuts import render
|
|
5
|
+
from django.urls import reverse
|
|
6
|
+
from django.utils.translation import gettext_lazy as _
|
|
7
|
+
|
|
8
|
+
from photo_objects.django.models import Album
|
|
9
|
+
from photo_objects.django.views.utils import BackLink
|
|
10
|
+
|
|
11
|
+
from .utils import json_problem_as_html
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Validation:
|
|
16
|
+
check: str
|
|
17
|
+
status: str
|
|
18
|
+
detail: str = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def status(b: bool) -> str:
|
|
22
|
+
return _("OK") if b else _("Error")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def uses_https(request: HttpRequest) -> Validation:
|
|
26
|
+
return Validation(
|
|
27
|
+
check=_("Site is served over HTTPS"),
|
|
28
|
+
status=status(request.is_secure()),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def site_is_configured(request: HttpRequest) -> Validation:
|
|
33
|
+
detail = None
|
|
34
|
+
try:
|
|
35
|
+
ok = request.site.domain != "example.com"
|
|
36
|
+
if not ok:
|
|
37
|
+
detail = (
|
|
38
|
+
f'Site domain is set to "example.com". This is a placeholder '
|
|
39
|
+
'domain and should be changed to the actual domain of the '
|
|
40
|
+
'site.')
|
|
41
|
+
except Exception as e:
|
|
42
|
+
ok = False
|
|
43
|
+
detail = (
|
|
44
|
+
f"Failed to resolve site domain: {str(e)}. Check that sites "
|
|
45
|
+
"framework is installed, site middleware is configured, and that "
|
|
46
|
+
"the site exists in the database.")
|
|
47
|
+
|
|
48
|
+
return Validation(
|
|
49
|
+
check=_("Site is configured"),
|
|
50
|
+
status=status(ok),
|
|
51
|
+
detail=detail,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def domain_matches_request(request: HttpRequest) -> Validation:
|
|
56
|
+
detail = None
|
|
57
|
+
try:
|
|
58
|
+
host = request.get_host().lower()
|
|
59
|
+
domain = request.site.domain.lower()
|
|
60
|
+
ok = request.get_host() == request.site.domain
|
|
61
|
+
if not ok:
|
|
62
|
+
detail = (
|
|
63
|
+
f'Host in the request does not match domain configured for '
|
|
64
|
+
'the site: expected "{domain}", got "{host}".')
|
|
65
|
+
except Exception as e:
|
|
66
|
+
ok = False
|
|
67
|
+
detail = (
|
|
68
|
+
f"Failed to resolve host or domain: {str(e)}. Check that "
|
|
69
|
+
"sites framework is installed, site middleware is configured, "
|
|
70
|
+
"and that the site exists in the database.")
|
|
71
|
+
|
|
72
|
+
return Validation(
|
|
73
|
+
check=_("Configured domain matches host in request"),
|
|
74
|
+
status=status(ok),
|
|
75
|
+
detail=detail,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def site_preview_configured(request: HttpRequest) -> Validation:
|
|
80
|
+
detail = None
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
site_id = request.site.id
|
|
84
|
+
album_key = f"_site_{site_id}"
|
|
85
|
+
album = Album.objects.get(key=album_key)
|
|
86
|
+
ok = album.cover_photo is not None
|
|
87
|
+
if not ok:
|
|
88
|
+
detail = (
|
|
89
|
+
f'Set cover photo for "{album_key}" album to configure '
|
|
90
|
+
'the preview image.')
|
|
91
|
+
except Exception as e:
|
|
92
|
+
ok = False
|
|
93
|
+
detail = f'Failed to resolve site or album: {str(e)}'
|
|
94
|
+
|
|
95
|
+
return Validation(
|
|
96
|
+
check=_("Site has a default preview image"),
|
|
97
|
+
status=status(ok),
|
|
98
|
+
detail=detail,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@json_problem_as_html
|
|
103
|
+
def configuration(request: HttpRequest):
|
|
104
|
+
validations = [
|
|
105
|
+
uses_https(request),
|
|
106
|
+
site_is_configured(request),
|
|
107
|
+
domain_matches_request(request),
|
|
108
|
+
site_preview_configured(request),
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
back = BackLink("Back to albums", reverse('photo_objects:list_albums'))
|
|
112
|
+
|
|
113
|
+
return render(request, "photo_objects/configuration.html", {
|
|
114
|
+
"title": "Configuration",
|
|
115
|
+
"validations": validations,
|
|
116
|
+
"back": back,
|
|
117
|
+
})
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from django.http import HttpRequest, HttpResponseRedirect
|
|
2
2
|
from django.shortcuts import render
|
|
3
3
|
from django.urls import reverse
|
|
4
|
+
from django.utils.safestring import mark_safe
|
|
4
5
|
|
|
5
6
|
from photo_objects.django import api
|
|
6
7
|
from photo_objects.django.api.utils import AlbumNotFound, FormValidationFailed
|
|
7
8
|
from photo_objects.django.forms import ModifyPhotoForm, UploadPhotosForm
|
|
9
|
+
from photo_objects.django.models import Photo
|
|
8
10
|
from photo_objects.django.views.utils import BackLink, render_markdown
|
|
9
11
|
|
|
10
12
|
from .utils import json_problem_as_html
|
|
@@ -39,6 +41,41 @@ def upload_photos(request: HttpRequest, album_key: str):
|
|
|
39
41
|
})
|
|
40
42
|
|
|
41
43
|
|
|
44
|
+
def _camera_setup(photo: Photo):
|
|
45
|
+
r = []
|
|
46
|
+
if photo.camera_make or photo.camera_model:
|
|
47
|
+
r.append(
|
|
48
|
+
" ".join(i for i in [
|
|
49
|
+
photo.camera_make,
|
|
50
|
+
photo.camera_model,
|
|
51
|
+
] if i))
|
|
52
|
+
if photo.lens_make or photo.lens_model:
|
|
53
|
+
r.append(" ".join(i for i in [photo.lens_make, photo.lens_model] if i))
|
|
54
|
+
return r
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _exposure_time_to_string(exposure_time: float | None):
|
|
58
|
+
if exposure_time is None:
|
|
59
|
+
return None
|
|
60
|
+
if exposure_time < 1:
|
|
61
|
+
return f"1/{int(1 / exposure_time)}\u202Fs"
|
|
62
|
+
else:
|
|
63
|
+
return f"{int(exposure_time)}\u202Fs"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _camera_settings(photo: Photo):
|
|
67
|
+
r = []
|
|
68
|
+
if photo.focal_length:
|
|
69
|
+
r.append(f"{round(photo.focal_length)}\u202Fmm")
|
|
70
|
+
if photo.f_number:
|
|
71
|
+
r.append(f"f/{photo.f_number}")
|
|
72
|
+
if photo.exposure_time:
|
|
73
|
+
r.append(_exposure_time_to_string(photo.exposure_time))
|
|
74
|
+
if photo.iso_speed:
|
|
75
|
+
r.append(f"ISO\u202F{photo.iso_speed}")
|
|
76
|
+
return r
|
|
77
|
+
|
|
78
|
+
|
|
42
79
|
@json_problem_as_html
|
|
43
80
|
def show_photo(request: HttpRequest, album_key: str, photo_key: str):
|
|
44
81
|
photo = api.check_photo_access(request, album_key, photo_key, "lg")
|
|
@@ -70,6 +107,8 @@ def show_photo(request: HttpRequest, album_key: str, photo_key: str):
|
|
|
70
107
|
details = {
|
|
71
108
|
"Description": render_markdown(photo.description),
|
|
72
109
|
"Timestamp": photo.timestamp,
|
|
110
|
+
"Camera": _camera_setup(photo),
|
|
111
|
+
"Settings": _camera_settings(photo),
|
|
73
112
|
}
|
|
74
113
|
|
|
75
114
|
return render(request, "photo_objects/photo/show.html", {
|
photo_objects/img.py
CHANGED
|
@@ -17,13 +17,24 @@ def utcnow():
|
|
|
17
17
|
return datetime.now(UTC)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
class ExifReader:
|
|
21
|
+
def __init__(self, image: Image):
|
|
22
|
+
self.image = image
|
|
23
|
+
self._data = [
|
|
24
|
+
image.getexif(),
|
|
25
|
+
image.getexif().get_ifd(ExifTags.IFD.Exif),
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
def get(self, key):
|
|
29
|
+
for d in self._data:
|
|
30
|
+
value = d.get(key)
|
|
31
|
+
if value is not None:
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
|
|
20
35
|
def _read_original_datetime(image: Image) -> datetime:
|
|
21
36
|
try:
|
|
22
|
-
|
|
23
|
-
if value == "ExifOffset":
|
|
24
|
-
break
|
|
25
|
-
|
|
26
|
-
info = image.getexif().get_ifd(key)
|
|
37
|
+
info = ExifReader(image)
|
|
27
38
|
|
|
28
39
|
time = info.get(ExifTags.Base.DateTimeOriginal)
|
|
29
40
|
subsec = info.get(ExifTags.Base.SubsecTimeOriginal) or "0"
|
|
@@ -36,6 +47,25 @@ def _read_original_datetime(image: Image) -> datetime:
|
|
|
36
47
|
return None
|
|
37
48
|
|
|
38
49
|
|
|
50
|
+
def _read_camera_setup_and_settings(image: Image) -> dict:
|
|
51
|
+
try:
|
|
52
|
+
info = ExifReader(image)
|
|
53
|
+
|
|
54
|
+
return dict(
|
|
55
|
+
camera_make=info.get(ExifTags.Base.Make),
|
|
56
|
+
camera_model=info.get(ExifTags.Base.Model),
|
|
57
|
+
lens_make=info.get(ExifTags.Base.LensMake),
|
|
58
|
+
lens_model=info.get(ExifTags.Base.LensModel),
|
|
59
|
+
focal_length=info.get(ExifTags.Base.FocalLength),
|
|
60
|
+
f_number=info.get(ExifTags.Base.FNumber),
|
|
61
|
+
exposure_time=info.get(ExifTags.Base.ExposureTime),
|
|
62
|
+
iso_speed=info.get(ExifTags.Base.ISOSpeedRatings),
|
|
63
|
+
)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
raise e
|
|
66
|
+
return dict()
|
|
67
|
+
|
|
68
|
+
|
|
39
69
|
def _image_format(filename):
|
|
40
70
|
image_format = filename.split('.')[-1].upper()
|
|
41
71
|
|
|
@@ -50,6 +80,7 @@ def photo_details(photo_file):
|
|
|
50
80
|
|
|
51
81
|
width, height = image.size
|
|
52
82
|
timestamp = _read_original_datetime(image) or utcnow()
|
|
83
|
+
camera_setup_and_settings = _read_camera_setup_and_settings(image)
|
|
53
84
|
|
|
54
85
|
# TODO: remove all extra data from the image
|
|
55
86
|
resized = image.resize((3, 3))
|
|
@@ -61,7 +92,8 @@ def photo_details(photo_file):
|
|
|
61
92
|
timestamp=timestamp,
|
|
62
93
|
width=width,
|
|
63
94
|
height=height,
|
|
64
|
-
tiny_base64=b64encode(b.getvalue()).decode('ascii')
|
|
95
|
+
tiny_base64=b64encode(b.getvalue()).decode('ascii'),
|
|
96
|
+
**camera_setup_and_settings,
|
|
65
97
|
)
|
|
66
98
|
|
|
67
99
|
|
|
@@ -1,35 +1,37 @@
|
|
|
1
1
|
photo_objects/__init__.py,sha256=I1508w_ntomEqTFQgC74SurhxVXfCiDWZLRsny2f59g,60
|
|
2
2
|
photo_objects/config.py,sha256=e9uSuxytkEUR_s0z_WLy5yNWN6fSCdDLq8aw2k5250w,743
|
|
3
3
|
photo_objects/error.py,sha256=7afLYjxM0EaYioxVw_XUqHTvfSMSuQPUwwle0OVlaDY,45
|
|
4
|
-
photo_objects/img.py,sha256=
|
|
4
|
+
photo_objects/img.py,sha256=HzM3rCE494TCgaxUN9TwzwudJAFVu9TPj_ebEOyUXpk,3435
|
|
5
5
|
photo_objects/django/__init__.py,sha256=be66-iYaaJljPXmRhnYKtZe9AA71Aa-S-s0cgx3NwyQ,146
|
|
6
6
|
photo_objects/django/admin.py,sha256=sRnKTODk-8s6TAfwvsa2YAd7ECIfVmoOTR6PDASsvGg,122
|
|
7
7
|
photo_objects/django/apps.py,sha256=Z5-cqA0h-axZfj51kbz50YI-B9ubQsX_EjF4fW3n3FM,1072
|
|
8
8
|
photo_objects/django/context_processors.py,sha256=DLhLZHAvGlrpxYegaLiKEx9X7rpyZUFiAnAVF9jjkFA,165
|
|
9
|
-
photo_objects/django/forms.py,sha256=
|
|
10
|
-
photo_objects/django/models.py,sha256=
|
|
9
|
+
photo_objects/django/forms.py,sha256=VoDlyZAwiTLyNxW3rRk5bzjfPJvNuKV-wpuLGCe5TCY,6505
|
|
10
|
+
photo_objects/django/models.py,sha256=tRHBqswASWMnyz8rwiKsmHDqEvQX54ly9k8tvU7qNOM,4597
|
|
11
11
|
photo_objects/django/objsto.py,sha256=GZuoPZ1I5qX4rwEYZlOfZZiV4NQ19iknQBT6755421Y,2417
|
|
12
12
|
photo_objects/django/signals.py,sha256=Q_Swjl_9z6B6zP-97D_ep5zGSAEgmQfwUz0utMDY93A,1624
|
|
13
|
-
photo_objects/django/urls.py,sha256=
|
|
13
|
+
photo_objects/django/urls.py,sha256=safcovG-8G60F_1v31ZvCCtBE8d6sHUtma6d5ZQHymo,2356
|
|
14
14
|
photo_objects/django/api/__init__.py,sha256=BnEHlm3mwyBy-1xhk-NFasgZa4fjCDjtfkBUoH0puPY,62
|
|
15
|
-
photo_objects/django/api/album.py,sha256=
|
|
15
|
+
photo_objects/django/api/album.py,sha256=ZH8WfEoZ52L_ucMvCaccXf1lU7IrSgtub2sWEMrMpQs,1993
|
|
16
16
|
photo_objects/django/api/auth.py,sha256=LfMm02_TKwrP5XFie-KzBnW3FNFWQAafBlu0UE19Gd4,1370
|
|
17
17
|
photo_objects/django/api/photo.py,sha256=NGCg_Qd4X9NAd7t6lqByK9JGsoTq8HKkyEr-HwMCimI,3838
|
|
18
18
|
photo_objects/django/api/utils.py,sha256=DgAsbvLLZxck_D6AXLsvUXJ3OguLufZ8sadmsToenJ4,5245
|
|
19
19
|
photo_objects/django/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
20
|
photo_objects/django/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
-
photo_objects/django/management/commands/create-initial-admin-account.py,sha256=
|
|
21
|
+
photo_objects/django/management/commands/create-initial-admin-account.py,sha256=M4qv1d_qFSGy_lDcKbyKOmfGsQt8i16Zoit9cJ6PwAU,1099
|
|
22
|
+
photo_objects/django/management/commands/create-site-albums.py,sha256=t0g1hKVFHoZJoJayGOZUqJaR_z-Bdyt9Ut4ETpOaRjI,1036
|
|
22
23
|
photo_objects/django/migrations/0001_initial.py,sha256=BLW-EZ38sBgDhOYyprc-h_vuPpRxA11qxt4ZuYNO1Wo,2424
|
|
23
24
|
photo_objects/django/migrations/0002_created_at_updated_at.py,sha256=7OT2VvDffAkX9XKBHVY-jvzxeIl2yU0Jr1ByCNGcUfw,1039
|
|
24
25
|
photo_objects/django/migrations/0003_admin_visibility.py,sha256=PdxPOJzr-ViRBlOYUHEEGhe0hLtDysZJdMqvbjKVpEg,529
|
|
26
|
+
photo_objects/django/migrations/0004_camera_setup_and_settings.py,sha256=CS5xyIHgBE2Y7-PSJ52ffRQeCzs8p899px9upomk4O8,1844
|
|
25
27
|
photo_objects/django/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
28
|
photo_objects/django/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
|
-
photo_objects/django/templatetags/photo_objects_extras.py,sha256=
|
|
29
|
+
photo_objects/django/templatetags/photo_objects_extras.py,sha256=LjqvVZpcVqdEQQ-EgC7OkEv2DqIvmfmDC-EsQNmkY7M,1205
|
|
28
30
|
photo_objects/django/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
|
-
photo_objects/django/tests/test_album.py,sha256=
|
|
31
|
+
photo_objects/django/tests/test_album.py,sha256=eforhWjaL3ZD7aENrjzbVhXHLI9WpY8pGZRXkgF7yNw,13333
|
|
30
32
|
photo_objects/django/tests/test_auth.py,sha256=su00EypNzGQcvxqhmOtFevZriK31pY7yD1HxpQFcUT4,3928
|
|
31
|
-
photo_objects/django/tests/test_photo.py,sha256=
|
|
32
|
-
photo_objects/django/tests/test_utils.py,sha256=
|
|
33
|
+
photo_objects/django/tests/test_photo.py,sha256=SpQsVynzJLP2F9qYrvZMzCBSiHvsRDpOZ8-W_07K_q8,13386
|
|
34
|
+
photo_objects/django/tests/test_utils.py,sha256=DkhzDtZqu5OXg-IKzqHz2GY0sFS8RbwYC3w81RuPxS4,1259
|
|
33
35
|
photo_objects/django/tests/utils.py,sha256=9GSUw78XRfXshbQrWkxZzXUh9YjoU4clQ5Sp2keGJ1A,2467
|
|
34
36
|
photo_objects/django/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
37
|
photo_objects/django/views/utils.py,sha256=369CUBWwm6LGh7wX1QVqIPE_K_6nb7ePEgHWvV2sKOw,245
|
|
@@ -38,12 +40,13 @@ photo_objects/django/views/api/album.py,sha256=EZMOkxYzLSWr9wwXnd4yAO64JtXZq2k3F
|
|
|
38
40
|
photo_objects/django/views/api/auth.py,sha256=N53csbthH0DEMFjY3fqhtLmtdL7xqzkatktywjqZ8h0,819
|
|
39
41
|
photo_objects/django/views/api/photo.py,sha256=52UMP1r6TN_xfu8fHE25yOUiTa6W1F2XbgEichM8tTE,3526
|
|
40
42
|
photo_objects/django/views/api/utils.py,sha256=uQzKdSKHRAux5OZzqgWQr0gsK_FeweQP0cg_67OWA_Y,264
|
|
41
|
-
photo_objects/django/views/ui/__init__.py,sha256=
|
|
42
|
-
photo_objects/django/views/ui/album.py,sha256=
|
|
43
|
-
photo_objects/django/views/ui/
|
|
43
|
+
photo_objects/django/views/ui/__init__.py,sha256=vbFXPj5liue-M_CXQGP5PKhD98Z5ev53yKLPU8zWpUs,71
|
|
44
|
+
photo_objects/django/views/ui/album.py,sha256=bgFPg9ipkQFqZK7cQgYT9gqpUgdSJ_1NTHlszsfclKU,5010
|
|
45
|
+
photo_objects/django/views/ui/configuration.py,sha256=6u2qea2XhAE7eNRej9OA65UHZiRQN57G0re1T-BglAo,3375
|
|
46
|
+
photo_objects/django/views/ui/photo.py,sha256=Xct76YoO3fUmqsQ7sY43JO_gaw2NIld16QNvAOfYGJM,6018
|
|
44
47
|
photo_objects/django/views/ui/utils.py,sha256=YV_YcUbX-zUkdFnBlezPChR6aPDhZJ9loSOHBSzF6Cc,273
|
|
45
|
-
photo_objects-0.0.
|
|
46
|
-
photo_objects-0.0.
|
|
47
|
-
photo_objects-0.0.
|
|
48
|
-
photo_objects-0.0.
|
|
49
|
-
photo_objects-0.0.
|
|
48
|
+
photo_objects-0.0.3.dist-info/licenses/LICENSE,sha256=V3w6hTjXfP65F4r_mejveHcV5igHrblxao3-2RlfVlA,1068
|
|
49
|
+
photo_objects-0.0.3.dist-info/METADATA,sha256=nWl7HVknzOYcmEvyhwjkotBueiSZZoYS3hz5K1TxG7E,3671
|
|
50
|
+
photo_objects-0.0.3.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
51
|
+
photo_objects-0.0.3.dist-info/top_level.txt,sha256=SZeL8mhf-WMGdhRtTGFvZc3aIRBboQls9O0cFDMGdQ0,14
|
|
52
|
+
photo_objects-0.0.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|