photo-objects 0.7.1__py3-none-any.whl → 0.8.1__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/admin.py +2 -1
- photo_objects/django/api/__init__.py +1 -0
- photo_objects/django/api/photo_change_request.py +111 -0
- photo_objects/django/api/utils.py +8 -0
- photo_objects/django/forms.py +24 -2
- photo_objects/django/migrations/0006_photo_alt_text.py +31 -0
- photo_objects/django/models.py +45 -0
- photo_objects/django/tests/test_album.py +5 -8
- photo_objects/django/tests/test_photo.py +5 -9
- photo_objects/django/tests/test_photo_change_requests.py +82 -0
- photo_objects/django/tests/utils.py +9 -0
- photo_objects/django/urls.py +18 -0
- photo_objects/django/views/api/__init__.py +7 -1
- photo_objects/django/views/api/photo.py +31 -0
- photo_objects/django/views/ui/__init__.py +1 -0
- photo_objects/django/views/ui/album.py +1 -1
- photo_objects/django/views/ui/configuration.py +2 -1
- photo_objects/django/views/ui/photo.py +1 -1
- photo_objects/django/views/ui/photo_change_request.py +61 -0
- photo_objects/django/views/utils.py +4 -22
- photo_objects/utils.py +21 -0
- {photo_objects-0.7.1.dist-info → photo_objects-0.8.1.dist-info}/METADATA +1 -1
- {photo_objects-0.7.1.dist-info → photo_objects-0.8.1.dist-info}/RECORD +26 -22
- {photo_objects-0.7.1.dist-info → photo_objects-0.8.1.dist-info}/WHEEL +0 -0
- {photo_objects-0.7.1.dist-info → photo_objects-0.8.1.dist-info}/licenses/LICENSE +0 -0
- {photo_objects-0.7.1.dist-info → photo_objects-0.8.1.dist-info}/top_level.txt +0 -0
photo_objects/django/admin.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from django.contrib import admin
|
|
2
2
|
|
|
3
|
-
from .models import Album, Photo, SiteSettings
|
|
3
|
+
from .models import Album, Photo, PhotoChangeRequest, SiteSettings
|
|
4
4
|
|
|
5
5
|
admin.site.register(Album)
|
|
6
6
|
admin.site.register(Photo)
|
|
7
7
|
admin.site.register(SiteSettings)
|
|
8
|
+
admin.site.register(PhotoChangeRequest)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from django.db.models import Count
|
|
2
|
+
from django.http import HttpRequest
|
|
3
|
+
|
|
4
|
+
from photo_objects.django.forms import (
|
|
5
|
+
CreatePhotoChangeRequestForm,
|
|
6
|
+
ReviewPhotoChangeRequestForm,
|
|
7
|
+
)
|
|
8
|
+
from photo_objects.django.models import Album, Photo, PhotoChangeRequest
|
|
9
|
+
|
|
10
|
+
from .auth import check_photo_access
|
|
11
|
+
from .utils import (
|
|
12
|
+
FormValidationFailed,
|
|
13
|
+
PhotoChangeRequestNotFound,
|
|
14
|
+
JsonProblem,
|
|
15
|
+
check_permissions,
|
|
16
|
+
parse_input_data,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_photo_change_request(
|
|
21
|
+
request: HttpRequest,
|
|
22
|
+
album_key: str,
|
|
23
|
+
photo_key: str):
|
|
24
|
+
check_permissions(request, 'photo_objects.add_photochangerequest')
|
|
25
|
+
photo = check_photo_access(request, album_key, photo_key, 'xs')
|
|
26
|
+
data = parse_input_data(request)
|
|
27
|
+
|
|
28
|
+
f = CreatePhotoChangeRequestForm({**data, "photo": photo.key})
|
|
29
|
+
|
|
30
|
+
if not f.is_valid():
|
|
31
|
+
raise FormValidationFailed(f)
|
|
32
|
+
|
|
33
|
+
change_request = f.save()
|
|
34
|
+
return change_request
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_photo_change_request_count(request: HttpRequest):
|
|
38
|
+
check_permissions(
|
|
39
|
+
request,
|
|
40
|
+
'photo_objects.change_photo',
|
|
41
|
+
'photo_objects.delete_photochangerequest')
|
|
42
|
+
|
|
43
|
+
return PhotoChangeRequest.objects.count()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_next_photo_change_request(request: HttpRequest):
|
|
47
|
+
check_permissions(
|
|
48
|
+
request,
|
|
49
|
+
'photo_objects.change_photo',
|
|
50
|
+
'photo_objects.delete_photochangerequest')
|
|
51
|
+
|
|
52
|
+
change_request = PhotoChangeRequest.objects.first()
|
|
53
|
+
if not change_request:
|
|
54
|
+
raise JsonProblem("No pending photo change requests.", 404) from None
|
|
55
|
+
|
|
56
|
+
return change_request
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_expected_photo_change_requests(request: HttpRequest):
|
|
60
|
+
check_permissions(request, 'photo_objects.add_photochangerequest')
|
|
61
|
+
|
|
62
|
+
photos = Photo.objects.filter(
|
|
63
|
+
alt_text="",
|
|
64
|
+
).annotate(
|
|
65
|
+
change_requests_count=Count('change_requests'),
|
|
66
|
+
).filter(
|
|
67
|
+
change_requests_count=0,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if not request.user.is_staff:
|
|
71
|
+
photos = photos.exclude(album__visibility=Album.Visibility.ADMIN)
|
|
72
|
+
|
|
73
|
+
return [photo.key for photo in photos]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_photo_change_request_and_photo(
|
|
77
|
+
request: HttpRequest,
|
|
78
|
+
cr_id: int):
|
|
79
|
+
try:
|
|
80
|
+
change_request = PhotoChangeRequest.objects.get(id=cr_id)
|
|
81
|
+
except PhotoChangeRequest.DoesNotExist:
|
|
82
|
+
raise PhotoChangeRequestNotFound(cr_id) from None
|
|
83
|
+
|
|
84
|
+
album_key = change_request.photo.album.key
|
|
85
|
+
photo_key = change_request.photo.filename
|
|
86
|
+
photo = check_photo_access(request, album_key, photo_key, 'xs')
|
|
87
|
+
|
|
88
|
+
return change_request, photo
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def review_photo_change_request(
|
|
92
|
+
request: HttpRequest,
|
|
93
|
+
cr_id: int):
|
|
94
|
+
check_permissions(
|
|
95
|
+
request,
|
|
96
|
+
'photo_objects.change_photo',
|
|
97
|
+
'photo_objects.delete_photochangerequest')
|
|
98
|
+
|
|
99
|
+
change_request, photo = get_photo_change_request_and_photo(request, cr_id)
|
|
100
|
+
data = parse_input_data(request)
|
|
101
|
+
|
|
102
|
+
f = ReviewPhotoChangeRequestForm(data, instance=change_request)
|
|
103
|
+
|
|
104
|
+
if not f.is_valid():
|
|
105
|
+
raise FormValidationFailed(f)
|
|
106
|
+
|
|
107
|
+
if f.cleaned_data['action'] == "approve":
|
|
108
|
+
photo.alt_text = f.cleaned_data['alt_text']
|
|
109
|
+
photo.save()
|
|
110
|
+
|
|
111
|
+
f.instance.delete()
|
|
@@ -113,6 +113,14 @@ class PhotoNotFound(JsonProblem):
|
|
|
113
113
|
)
|
|
114
114
|
|
|
115
115
|
|
|
116
|
+
class PhotoChangeRequestNotFound(JsonProblem):
|
|
117
|
+
def __init__(self, id_: int):
|
|
118
|
+
super().__init__(
|
|
119
|
+
f"Photo change request with id {id_} does not exist.",
|
|
120
|
+
404,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
116
124
|
class FormValidationFailed(JsonProblem):
|
|
117
125
|
def __init__(self, form: ModelForm):
|
|
118
126
|
try:
|
photo_objects/django/forms.py
CHANGED
|
@@ -2,6 +2,7 @@ import random
|
|
|
2
2
|
import re
|
|
3
3
|
import unicodedata
|
|
4
4
|
|
|
5
|
+
from django import forms
|
|
5
6
|
from django.forms import (
|
|
6
7
|
CharField,
|
|
7
8
|
ClearableFileInput,
|
|
@@ -15,13 +16,15 @@ from django.forms import (
|
|
|
15
16
|
from django.utils.safestring import mark_safe
|
|
16
17
|
from django.utils.translation import gettext_lazy as _
|
|
17
18
|
|
|
18
|
-
from .models import Album, Photo
|
|
19
|
+
from .models import Album, Photo, PhotoChangeRequest
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
# From Kubernetes random postfix.
|
|
22
23
|
KEY_POSTFIX_CHARS = 'bcdfghjklmnpqrstvwxz2456789'
|
|
23
24
|
KEY_POSTFIX_LEN = 5
|
|
24
25
|
|
|
26
|
+
ALT_TEXT_HELP = _('Alternative text content for the photo.')
|
|
27
|
+
|
|
25
28
|
|
|
26
29
|
def slugify(title: str, lower=False, replace_leading_underscores=False) -> str:
|
|
27
30
|
key = unicodedata.normalize(
|
|
@@ -206,13 +209,32 @@ class CreatePhotoForm(ModelForm):
|
|
|
206
209
|
class ModifyPhotoForm(ModelForm):
|
|
207
210
|
class Meta:
|
|
208
211
|
model = Photo
|
|
209
|
-
fields = ['title', 'description']
|
|
212
|
+
fields = ['title', 'description', 'alt_text']
|
|
210
213
|
help_texts = {
|
|
211
214
|
**description_help('photo'),
|
|
212
215
|
'title': _(
|
|
213
216
|
'Title for the photo. If not defined, the filename of the '
|
|
214
217
|
'photo is used as the title.'
|
|
215
218
|
),
|
|
219
|
+
'alt_text': ALT_TEXT_HELP,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class CreatePhotoChangeRequestForm(ModelForm):
|
|
224
|
+
class Meta:
|
|
225
|
+
model = PhotoChangeRequest
|
|
226
|
+
fields = ['photo', 'alt_text']
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class ReviewPhotoChangeRequestForm(ModelForm):
|
|
230
|
+
action = forms.ChoiceField(
|
|
231
|
+
choices=[('approve', 'Approve'), ('reject', 'Reject')], widget=None)
|
|
232
|
+
|
|
233
|
+
class Meta:
|
|
234
|
+
model = PhotoChangeRequest
|
|
235
|
+
fields = ['alt_text']
|
|
236
|
+
help_texts = {
|
|
237
|
+
'alt_text': ALT_TEXT_HELP,
|
|
216
238
|
}
|
|
217
239
|
|
|
218
240
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Generated by Django 5.2 on 2025-09-06 19:33
|
|
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', '0005_sitesettings'),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.AddField(
|
|
15
|
+
model_name='photo',
|
|
16
|
+
name='alt_text',
|
|
17
|
+
field=models.TextField(blank=True),
|
|
18
|
+
),
|
|
19
|
+
migrations.CreateModel(
|
|
20
|
+
name='PhotoChangeRequest',
|
|
21
|
+
fields=[
|
|
22
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
23
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
24
|
+
('alt_text', models.TextField()),
|
|
25
|
+
('photo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='change_requests', to='photo_objects.photo')),
|
|
26
|
+
],
|
|
27
|
+
options={
|
|
28
|
+
'ordering': ['-created_at'],
|
|
29
|
+
},
|
|
30
|
+
),
|
|
31
|
+
]
|
photo_objects/django/models.py
CHANGED
|
@@ -4,6 +4,8 @@ from django.contrib.sites.models import Site
|
|
|
4
4
|
from django.core.validators import RegexValidator
|
|
5
5
|
from django.utils.translation import gettext_lazy as _
|
|
6
6
|
|
|
7
|
+
from photo_objects.utils import first_paragraph_textcontent
|
|
8
|
+
|
|
7
9
|
|
|
8
10
|
album_key_validator = RegexValidator(
|
|
9
11
|
r"^[a-zA-Z0-9._-]+$",
|
|
@@ -108,6 +110,8 @@ class Photo(BaseModel):
|
|
|
108
110
|
exposure_time = models.FloatField(blank=True, null=True)
|
|
109
111
|
iso_speed = models.IntegerField(blank=True, null=True)
|
|
110
112
|
|
|
113
|
+
alt_text = models.TextField(blank=True)
|
|
114
|
+
|
|
111
115
|
def __str__(self):
|
|
112
116
|
return _str(
|
|
113
117
|
self.key,
|
|
@@ -115,6 +119,18 @@ class Photo(BaseModel):
|
|
|
115
119
|
timestamp=self.timestamp.isoformat()
|
|
116
120
|
)
|
|
117
121
|
|
|
122
|
+
@property
|
|
123
|
+
def alt(self):
|
|
124
|
+
if self.alt_text:
|
|
125
|
+
return self.alt_text
|
|
126
|
+
|
|
127
|
+
if self.description:
|
|
128
|
+
text = first_paragraph_textcontent(self.description)
|
|
129
|
+
if text:
|
|
130
|
+
return text
|
|
131
|
+
|
|
132
|
+
return self.title or self.filename
|
|
133
|
+
|
|
118
134
|
@property
|
|
119
135
|
def filename(self):
|
|
120
136
|
return self.key.split('/')[-1]
|
|
@@ -147,6 +163,35 @@ class Photo(BaseModel):
|
|
|
147
163
|
f_number=self.f_number,
|
|
148
164
|
exposure_time=self.exposure_time,
|
|
149
165
|
iso_speed=self.iso_speed,
|
|
166
|
+
alt_text=self.alt_text,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class PhotoChangeRequest(models.Model):
|
|
171
|
+
class Meta:
|
|
172
|
+
ordering = ["-created_at"]
|
|
173
|
+
|
|
174
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
175
|
+
|
|
176
|
+
photo = models.ForeignKey(
|
|
177
|
+
"Photo",
|
|
178
|
+
on_delete=models.CASCADE,
|
|
179
|
+
related_name="change_requests")
|
|
180
|
+
|
|
181
|
+
alt_text = models.TextField()
|
|
182
|
+
|
|
183
|
+
def __str__(self):
|
|
184
|
+
return _str(
|
|
185
|
+
self.photo.key,
|
|
186
|
+
created_at=self.created_at.isoformat()
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def to_json(self):
|
|
190
|
+
return dict(
|
|
191
|
+
id=self.id,
|
|
192
|
+
photo=self.photo.key,
|
|
193
|
+
created_at=_timestamp_str(self.created_at),
|
|
194
|
+
alt_text=self.alt_text,
|
|
150
195
|
)
|
|
151
196
|
|
|
152
197
|
|
|
@@ -2,13 +2,13 @@ import json
|
|
|
2
2
|
from time import sleep
|
|
3
3
|
|
|
4
4
|
from django.contrib.auth import get_user_model
|
|
5
|
-
from django.contrib.auth.models import Permission
|
|
6
5
|
|
|
7
6
|
from photo_objects.django.models import Album
|
|
8
7
|
from photo_objects.img import utcnow
|
|
9
8
|
|
|
10
9
|
from .utils import (
|
|
11
10
|
TestCase,
|
|
11
|
+
add_permissions,
|
|
12
12
|
parse_timestamps,
|
|
13
13
|
create_dummy_photo,
|
|
14
14
|
open_test_photo
|
|
@@ -101,17 +101,14 @@ class AlbumViewTests(TestCase):
|
|
|
101
101
|
|
|
102
102
|
has_permission = user.objects.create_user(
|
|
103
103
|
username='has_permission', password='test')
|
|
104
|
-
|
|
104
|
+
add_permissions(
|
|
105
|
+
has_permission,
|
|
105
106
|
'add_album',
|
|
106
107
|
'add_photo',
|
|
107
108
|
'change_album',
|
|
108
109
|
'delete_album',
|
|
109
|
-
'delete_photo'
|
|
110
|
-
|
|
111
|
-
has_permission.user_permissions.add(
|
|
112
|
-
Permission.objects.get(
|
|
113
|
-
content_type__app_label='photo_objects',
|
|
114
|
-
codename=permission))
|
|
110
|
+
'delete_photo',
|
|
111
|
+
)
|
|
115
112
|
|
|
116
113
|
def test_post_album_with_non_json_data_fails(self):
|
|
117
114
|
login_success = self.client.login(
|
|
@@ -5,7 +5,6 @@ from time import sleep
|
|
|
5
5
|
from unittest import mock
|
|
6
6
|
|
|
7
7
|
from django.contrib.auth import get_user_model
|
|
8
|
-
from django.contrib.auth.models import Permission
|
|
9
8
|
from PIL import Image
|
|
10
9
|
from urllib3.exceptions import HTTPError
|
|
11
10
|
|
|
@@ -13,7 +12,7 @@ from photo_objects.django.models import Album
|
|
|
13
12
|
from photo_objects.img import utcnow
|
|
14
13
|
from photo_objects.django.objsto import get_photo
|
|
15
14
|
|
|
16
|
-
from .utils import TestCase, open_test_photo, parse_timestamps
|
|
15
|
+
from .utils import TestCase, add_permissions, open_test_photo, parse_timestamps
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
class PhotoViewTests(TestCase):
|
|
@@ -23,16 +22,13 @@ class PhotoViewTests(TestCase):
|
|
|
23
22
|
|
|
24
23
|
has_permission = user.objects.create_user(
|
|
25
24
|
username='has_permission', password='test')
|
|
26
|
-
|
|
25
|
+
add_permissions(
|
|
26
|
+
has_permission,
|
|
27
27
|
'add_photo',
|
|
28
28
|
'change_album',
|
|
29
29
|
'change_photo',
|
|
30
|
-
'delete_photo'
|
|
31
|
-
|
|
32
|
-
has_permission.user_permissions.add(
|
|
33
|
-
Permission.objects.get(
|
|
34
|
-
content_type__app_label='photo_objects',
|
|
35
|
-
codename=permission))
|
|
30
|
+
'delete_photo',
|
|
31
|
+
)
|
|
36
32
|
|
|
37
33
|
Album.objects.create(
|
|
38
34
|
key="test-photo-a",
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from django.contrib.auth import get_user_model
|
|
2
|
+
|
|
3
|
+
from photo_objects.django.models import Album
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from .utils import TestCase, add_permissions, create_dummy_photo
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
TEST_PREFIX = "test-photo-change-request"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _filter_by_test_prefix(items):
|
|
13
|
+
return [item for item in items if item.startswith(TEST_PREFIX)]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PhotoChangeRequestTests(TestCase):
|
|
17
|
+
def setUp(self):
|
|
18
|
+
user = get_user_model()
|
|
19
|
+
user.objects.create_user(username='no_permission', password='test')
|
|
20
|
+
|
|
21
|
+
user.objects.create_user(
|
|
22
|
+
username='superuser',
|
|
23
|
+
password='test',
|
|
24
|
+
is_staff=True,
|
|
25
|
+
is_superuser=True)
|
|
26
|
+
|
|
27
|
+
has_permission = user.objects.create_user(
|
|
28
|
+
username='has_permission', password='test')
|
|
29
|
+
add_permissions(has_permission, 'add_photochangerequest')
|
|
30
|
+
|
|
31
|
+
self.private_album = Album.objects.create(
|
|
32
|
+
key=f"{TEST_PREFIX}-private", visibility=Album.Visibility.PRIVATE)
|
|
33
|
+
self.admin_album = Album.objects.create(
|
|
34
|
+
key=f"{TEST_PREFIX}-admin", visibility=Album.Visibility.ADMIN)
|
|
35
|
+
|
|
36
|
+
create_dummy_photo(self.private_album, "001.jpg")
|
|
37
|
+
create_dummy_photo(self.private_album, "002.jpg")
|
|
38
|
+
create_dummy_photo(self.admin_album, "003.jpg")
|
|
39
|
+
|
|
40
|
+
def test_expected_photo_change_requests(self):
|
|
41
|
+
tests = [
|
|
42
|
+
("no_permission", 403, 0),
|
|
43
|
+
("has_permission", 200, 2),
|
|
44
|
+
("superuser", 200, 3),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
for username, expected_status, expected_count in tests:
|
|
48
|
+
with self.subTest(username=username):
|
|
49
|
+
self.client.login(username=username, password='test')
|
|
50
|
+
response = self.client.get(
|
|
51
|
+
'/api/photo-change-requests/expected')
|
|
52
|
+
self.assertStatus(response, expected_status)
|
|
53
|
+
if expected_status == 200:
|
|
54
|
+
photos = _filter_by_test_prefix(response.json())
|
|
55
|
+
self.assertEqual(len(photos), expected_count)
|
|
56
|
+
|
|
57
|
+
def test_create_photo_change_requests(self):
|
|
58
|
+
self.client.login(username="has_permission", password='test')
|
|
59
|
+
|
|
60
|
+
response = self.client.get(
|
|
61
|
+
'/api/photo-change-requests/expected')
|
|
62
|
+
self.assertEqual(response.status_code, 200)
|
|
63
|
+
photos = _filter_by_test_prefix(response.json())
|
|
64
|
+
self.assertEqual(len(photos), 2)
|
|
65
|
+
|
|
66
|
+
response = self.client.post(
|
|
67
|
+
f'/api/albums/{TEST_PREFIX}-private/photos/001.jpg/change-requests', # noqa: E501
|
|
68
|
+
data={'alt_text': ''},
|
|
69
|
+
content_type="application/json")
|
|
70
|
+
self.assertStatus(response, 400)
|
|
71
|
+
|
|
72
|
+
response = self.client.post(
|
|
73
|
+
f'/api/albums/{TEST_PREFIX}-private/photos/001.jpg/change-requests', # noqa: E501
|
|
74
|
+
data={'alt_text': 'Test alt text'},
|
|
75
|
+
content_type="application/json")
|
|
76
|
+
self.assertStatus(response, 201)
|
|
77
|
+
|
|
78
|
+
response = self.client.get(
|
|
79
|
+
'/api/photo-change-requests/expected')
|
|
80
|
+
self.assertEqual(response.status_code, 200)
|
|
81
|
+
photos = _filter_by_test_prefix(response.json())
|
|
82
|
+
self.assertEqual(len(photos), 1)
|
|
@@ -5,6 +5,7 @@ import shutil
|
|
|
5
5
|
import tempfile
|
|
6
6
|
|
|
7
7
|
from django.conf import settings
|
|
8
|
+
from django.contrib.auth.models import Permission
|
|
8
9
|
from django.core.management import call_command
|
|
9
10
|
from django.test import TestCase as DjangoTestCase, override_settings
|
|
10
11
|
from django.utils import timezone
|
|
@@ -16,6 +17,14 @@ from photo_objects.django.models import Album, Photo
|
|
|
16
17
|
from photo_objects.django.objsto import _objsto_access
|
|
17
18
|
|
|
18
19
|
|
|
20
|
+
def add_permissions(user, *permissions):
|
|
21
|
+
for permission in permissions:
|
|
22
|
+
user.user_permissions.add(
|
|
23
|
+
Permission.objects.get(
|
|
24
|
+
content_type__app_label='photo_objects',
|
|
25
|
+
codename=permission))
|
|
26
|
+
|
|
27
|
+
|
|
19
28
|
def open_test_photo(filename):
|
|
20
29
|
path = os.path.join(
|
|
21
30
|
os.path.dirname(
|
photo_objects/django/urls.py
CHANGED
|
@@ -11,6 +11,14 @@ urlpatterns = [
|
|
|
11
11
|
path("api/albums/<str:album_key>", api.album),
|
|
12
12
|
path("api/albums/<str:album_key>/photos", api.photos),
|
|
13
13
|
path("api/albums/<str:album_key>/photos/<str:photo_key>", api.photo),
|
|
14
|
+
path(
|
|
15
|
+
"api/albums/<str:album_key>/photos/<str:photo_key>/change-requests",
|
|
16
|
+
api.photo_change_requests,
|
|
17
|
+
),
|
|
18
|
+
path(
|
|
19
|
+
"api/photo-change-requests/expected",
|
|
20
|
+
api.expected_photo_change_requests,
|
|
21
|
+
),
|
|
14
22
|
path("api/albums/<str:album_key>/photos/<str:photo_key>/img", api.get_img),
|
|
15
23
|
# TODO: ui views
|
|
16
24
|
path('', lambda _: HttpResponseRedirect('albums')),
|
|
@@ -59,6 +67,16 @@ urlpatterns = [
|
|
|
59
67
|
ui.delete_photo,
|
|
60
68
|
name="delete_photo",
|
|
61
69
|
),
|
|
70
|
+
path(
|
|
71
|
+
"photo-change-requests/_next",
|
|
72
|
+
ui.next_photo_change_request,
|
|
73
|
+
name="next_photo_change_request",
|
|
74
|
+
),
|
|
75
|
+
path(
|
|
76
|
+
"photo-change-requests/<int:cr_id>/_review",
|
|
77
|
+
ui.review_photo_change_request,
|
|
78
|
+
name="review_photo_change_request",
|
|
79
|
+
),
|
|
62
80
|
path(
|
|
63
81
|
"configuration",
|
|
64
82
|
ui.configuration,
|
|
@@ -111,3 +111,34 @@ def get_img(request: HttpRequest, album_key: str, photo_key: str):
|
|
|
111
111
|
scaled_photo.seek(0)
|
|
112
112
|
return HttpResponse(
|
|
113
113
|
scaled_photo.read(), content_type=content_type, headers=headers)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@json_problem_as_json
|
|
117
|
+
def photo_change_requests(
|
|
118
|
+
request: HttpRequest,
|
|
119
|
+
album_key: str,
|
|
120
|
+
photo_key: str):
|
|
121
|
+
if request.method == "POST":
|
|
122
|
+
return create_change_request(request, album_key, photo_key)
|
|
123
|
+
else:
|
|
124
|
+
return MethodNotAllowed(["POST"], request.method).json_response
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def create_change_request(
|
|
128
|
+
request: HttpRequest,
|
|
129
|
+
album_key: str,
|
|
130
|
+
photo_key: str):
|
|
131
|
+
change_request = api.create_photo_change_request(
|
|
132
|
+
request, album_key, photo_key)
|
|
133
|
+
return JsonResponse(change_request.to_json(), status=201)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@json_problem_as_json
|
|
137
|
+
def expected_photo_change_requests(request: HttpRequest):
|
|
138
|
+
if request.method != "GET":
|
|
139
|
+
return MethodNotAllowed(["GET"], request.method).json_response
|
|
140
|
+
|
|
141
|
+
return JsonResponse(
|
|
142
|
+
api.get_expected_photo_change_requests(request),
|
|
143
|
+
safe=False,
|
|
144
|
+
)
|
|
@@ -6,7 +6,8 @@ from django.urls import reverse
|
|
|
6
6
|
from django.utils.translation import gettext_lazy as _
|
|
7
7
|
|
|
8
8
|
from photo_objects.django.api.utils import JsonProblem
|
|
9
|
-
from photo_objects.django.views.utils import BackLink
|
|
9
|
+
from photo_objects.django.views.utils import BackLink
|
|
10
|
+
from photo_objects.utils import render_markdown
|
|
10
11
|
|
|
11
12
|
from .utils import json_problem_as_html
|
|
12
13
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from django.http import HttpRequest, HttpResponseRedirect
|
|
2
|
+
from django.shortcuts import render
|
|
3
|
+
from django.urls import reverse
|
|
4
|
+
|
|
5
|
+
from photo_objects.django import api
|
|
6
|
+
from photo_objects.django.api.utils import (
|
|
7
|
+
FormValidationFailed,
|
|
8
|
+
)
|
|
9
|
+
from photo_objects.django.forms import ReviewPhotoChangeRequestForm
|
|
10
|
+
from photo_objects.django.views.utils import BackLink
|
|
11
|
+
from photo_objects.utils import render_markdown
|
|
12
|
+
|
|
13
|
+
from .utils import json_problem_as_html
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@json_problem_as_html
|
|
17
|
+
def next_photo_change_request(request: HttpRequest):
|
|
18
|
+
change_request = api.get_next_photo_change_request(request)
|
|
19
|
+
|
|
20
|
+
return HttpResponseRedirect(
|
|
21
|
+
reverse(
|
|
22
|
+
'photo_objects:review_photo_change_request',
|
|
23
|
+
kwargs={"cr_id": change_request.id},
|
|
24
|
+
))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@json_problem_as_html
|
|
28
|
+
def review_photo_change_request(request: HttpRequest, cr_id: str):
|
|
29
|
+
if request.method == "POST":
|
|
30
|
+
try:
|
|
31
|
+
api.review_photo_change_request(request, cr_id)
|
|
32
|
+
return HttpResponseRedirect(
|
|
33
|
+
reverse('photo_objects:next_photo_change_request'))
|
|
34
|
+
except FormValidationFailed as e:
|
|
35
|
+
_, photo = api.get_photo_change_request_and_photo(request, cr_id)
|
|
36
|
+
form = e.form
|
|
37
|
+
else:
|
|
38
|
+
change_request, photo = api.get_photo_change_request_and_photo(
|
|
39
|
+
request, cr_id)
|
|
40
|
+
form = ReviewPhotoChangeRequestForm(
|
|
41
|
+
initial={**change_request.to_json()},
|
|
42
|
+
instance=change_request)
|
|
43
|
+
|
|
44
|
+
count = api.get_photo_change_request_count(request)
|
|
45
|
+
if count == 1:
|
|
46
|
+
info = "This is the last change request in the review queue."
|
|
47
|
+
else:
|
|
48
|
+
info = f"There are {count} change requests in the review queue."
|
|
49
|
+
|
|
50
|
+
back = BackLink("Back to albums", reverse('photo_objects:list_albums'))
|
|
51
|
+
|
|
52
|
+
return render(request, 'photo_objects/form.html', {
|
|
53
|
+
"form": form,
|
|
54
|
+
"title": "Review photo change request",
|
|
55
|
+
"back": back,
|
|
56
|
+
"photo": photo,
|
|
57
|
+
"info": info,
|
|
58
|
+
"instructions": render_markdown(
|
|
59
|
+
f'The current alt text for `{photo.key}` is: '
|
|
60
|
+
f'_"{photo.alt_text}"_.'),
|
|
61
|
+
})
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
from xml.etree import ElementTree as ET
|
|
2
|
-
|
|
3
1
|
from django.http import HttpRequest
|
|
4
2
|
from django.utils.dateformat import format as format_date
|
|
5
|
-
from django.utils.safestring import mark_safe
|
|
6
|
-
from markdown import markdown
|
|
7
3
|
|
|
8
4
|
from photo_objects.django.models import Album, Photo
|
|
5
|
+
from photo_objects.utils import first_paragraph_textcontent
|
|
9
6
|
|
|
10
7
|
|
|
11
8
|
class BackLink:
|
|
@@ -14,21 +11,6 @@ class BackLink:
|
|
|
14
11
|
self.url = url
|
|
15
12
|
|
|
16
13
|
|
|
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
14
|
def _default_album_description(request: HttpRequest, album: Album) -> str:
|
|
33
15
|
count = album.photo_set.count()
|
|
34
16
|
plural = 's' if count != 1 else ''
|
|
@@ -46,15 +28,15 @@ def meta_description(
|
|
|
46
28
|
text = None
|
|
47
29
|
if isinstance(resource, Album):
|
|
48
30
|
text = (
|
|
49
|
-
|
|
31
|
+
first_paragraph_textcontent(resource.description) or
|
|
50
32
|
_default_album_description(request, resource))
|
|
51
33
|
|
|
52
34
|
if isinstance(resource, Photo):
|
|
53
35
|
text = (
|
|
54
|
-
|
|
36
|
+
first_paragraph_textcontent(resource.description) or
|
|
55
37
|
_default_photo_description(resource))
|
|
56
38
|
|
|
57
39
|
if isinstance(resource, str):
|
|
58
|
-
text =
|
|
40
|
+
text = first_paragraph_textcontent(resource)
|
|
59
41
|
|
|
60
42
|
return text or "A simple self-hosted photo server."
|
photo_objects/utils.py
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
from xml.etree import ElementTree as ET
|
|
2
|
+
|
|
3
|
+
from django.utils.safestring import mark_safe
|
|
4
|
+
from markdown import markdown
|
|
5
|
+
|
|
6
|
+
|
|
1
7
|
def pretty_list(in_: list, conjunction: str):
|
|
2
8
|
return f' {conjunction} '.join(
|
|
3
9
|
i for i in (', '.join(in_[:-1]), in_[-1],) if i)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def render_markdown(value: str):
|
|
13
|
+
return mark_safe(markdown(value))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def first_paragraph_textcontent(raw: str) -> str | None:
|
|
17
|
+
html = render_markdown(raw)
|
|
18
|
+
root = ET.fromstring(f"<root>{html}</root>")
|
|
19
|
+
|
|
20
|
+
first = root.find("p")
|
|
21
|
+
if first is None:
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
return ''.join(first.itertext())
|
|
@@ -2,22 +2,23 @@ photo_objects/__init__.py,sha256=I1508w_ntomEqTFQgC74SurhxVXfCiDWZLRsny2f59g,60
|
|
|
2
2
|
photo_objects/config.py,sha256=0-Aeo-z-d_fxx-cjAjxSwPJZUgYaAi7NTodiErlxIXo,861
|
|
3
3
|
photo_objects/error.py,sha256=7afLYjxM0EaYioxVw_XUqHTvfSMSuQPUwwle0OVlaDY,45
|
|
4
4
|
photo_objects/img.py,sha256=2HVGS2g7rS2hnomozYL92oxrcN6zjDTHvWNr-UAqtGQ,4620
|
|
5
|
-
photo_objects/utils.py,sha256=
|
|
5
|
+
photo_objects/utils.py,sha256=_93yzQ18UXa_QIC4pLCPvgcwi8uCGn9-pDT_7e4p_uw,579
|
|
6
6
|
photo_objects/django/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
photo_objects/django/admin.py,sha256=
|
|
7
|
+
photo_objects/django/admin.py,sha256=ZrNhr-OatgIyqepaU-LDZbf4djCRQteg4yx-TS3OYy0,230
|
|
8
8
|
photo_objects/django/apps.py,sha256=Apqu6o6fpoxda18NQgKupvQRvTAZxVviIK_-dUR3rck,1444
|
|
9
9
|
photo_objects/django/conf.py,sha256=ZpeIulEc1tpr8AO52meNKOF30Xf5osbDtDyHvQRtkx4,2593
|
|
10
10
|
photo_objects/django/context_processors.py,sha256=XacUmcYV-4NMMMNXPWrHKdvNd6lfyamisngaVerREiU,306
|
|
11
|
-
photo_objects/django/forms.py,sha256=
|
|
12
|
-
photo_objects/django/models.py,sha256=
|
|
11
|
+
photo_objects/django/forms.py,sha256=95tdNFrvxSVWUYzy4-zrLmCaQVdEpA02umqQyfCllws,7258
|
|
12
|
+
photo_objects/django/models.py,sha256=2qYQeyfwf32m9CNb-W1xnhU5IrT8QD5bI11HS-z7fNs,6824
|
|
13
13
|
photo_objects/django/objsto.py,sha256=B7DxPWuqFaPFXPLhsHCFlqIzYl7EXLxcHde6zJDe89A,4238
|
|
14
14
|
photo_objects/django/signals.py,sha256=Q_Swjl_9z6B6zP-97D_ep5zGSAEgmQfwUz0utMDY93A,1624
|
|
15
|
-
photo_objects/django/urls.py,sha256=
|
|
16
|
-
photo_objects/django/api/__init__.py,sha256=
|
|
15
|
+
photo_objects/django/urls.py,sha256=XnOSEB8YtAJamlEsjKYz6U1DfDu7HHZpAinpqdulR8k,2501
|
|
16
|
+
photo_objects/django/api/__init__.py,sha256=inUPmjlTAawHwco4NzjwNEVNnf9c_IIwLH5Ze1ujpuI,98
|
|
17
17
|
photo_objects/django/api/album.py,sha256=CJDeGLCuYoxGUDcjssZRpFnToxG_KVE9Ii7NduFW2ks,2003
|
|
18
18
|
photo_objects/django/api/auth.py,sha256=lS0S1tMVH2uN30g4jlixklv3eMnQ2FbQVQvuRXeMGYo,1420
|
|
19
19
|
photo_objects/django/api/photo.py,sha256=-lo1g6jfBr884wy-WV2DAEPxzH9V-tFUTRtitmA6i28,4471
|
|
20
|
-
photo_objects/django/api/
|
|
20
|
+
photo_objects/django/api/photo_change_request.py,sha256=v94RA7SUM60tC9mIZdz8HppbNKfHWeTFNPr_BPw3pys,3075
|
|
21
|
+
photo_objects/django/api/utils.py,sha256=8r51YgFgKPD05Zjzhstl4jlQ4JM8DtsxUyAzhjXi5Pk,5567
|
|
21
22
|
photo_objects/django/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
23
|
photo_objects/django/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
24
|
photo_objects/django/management/commands/clean-scaled-photos.py,sha256=KJY6phgTCxcmbMUsUfCRQjatvCmKyFninM8zT-tB3Kc,2008
|
|
@@ -27,33 +28,36 @@ photo_objects/django/migrations/0002_created_at_updated_at.py,sha256=7OT2VvDffAk
|
|
|
27
28
|
photo_objects/django/migrations/0003_admin_visibility.py,sha256=PdxPOJzr-ViRBlOYUHEEGhe0hLtDysZJdMqvbjKVpEg,529
|
|
28
29
|
photo_objects/django/migrations/0004_camera_setup_and_settings.py,sha256=CS5xyIHgBE2Y7-PSJ52ffRQeCzs8p899px9upomk4O8,1844
|
|
29
30
|
photo_objects/django/migrations/0005_sitesettings.py,sha256=Ilf5vUwTFQfXVP37zz0NWo5dQdeHDh5e-MV3enm0ZKI,994
|
|
31
|
+
photo_objects/django/migrations/0006_photo_alt_text.py,sha256=5ZWR-bfS_72Mpb0SrvtnWzVME-zlcRPzDCofmFWkUeU,1003
|
|
30
32
|
photo_objects/django/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
33
|
photo_objects/django/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
34
|
photo_objects/django/templatetags/photo_objects_extras.py,sha256=1L6wweVA96rTWVXqgyf8gSey1wQKi01h6sqv9kZeTIY,1399
|
|
33
35
|
photo_objects/django/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
|
-
photo_objects/django/tests/test_album.py,sha256=
|
|
36
|
+
photo_objects/django/tests/test_album.py,sha256=Uik36QbhaEojed6g862xqj8YI0d2963StU-lOpqMqAc,13095
|
|
35
37
|
photo_objects/django/tests/test_auth.py,sha256=hgr1UMVLvSI1x5zY7wTEXSBKfM5E_sNMIFlx8mVWYPY,3928
|
|
36
38
|
photo_objects/django/tests/test_commands.py,sha256=e3lE1ZhFR39WIq2VSKDNcQHUkSJqSWDYuAcAfu29svs,2955
|
|
37
39
|
photo_objects/django/tests/test_img.py,sha256=HEAWcr5fpTkzePkhoQ4YrWsDO9TvFOr7my_0LqVbaO4,829
|
|
38
40
|
photo_objects/django/tests/test_og_meta.py,sha256=Kk5a9KvE88KZ60gLqXSe6rTz5YU-gdjteksYolHd-nw,1804
|
|
39
|
-
photo_objects/django/tests/test_photo.py,sha256=
|
|
41
|
+
photo_objects/django/tests/test_photo.py,sha256=JWXN3fF2VtuByMKm2o5b19HnxwDr6ecRwuGzgc5RsBw,13471
|
|
42
|
+
photo_objects/django/tests/test_photo_change_requests.py,sha256=Ld5ytqxxZiEWrqfX8htJ6-5ARU7tqTYD-iUhb7EMcnU,3078
|
|
40
43
|
photo_objects/django/tests/test_utils.py,sha256=zBLv8lkvcLMCaH4D6GR1KZqUe-rPowhcBkQX19-Kshs,2007
|
|
41
|
-
photo_objects/django/tests/utils.py,sha256=
|
|
44
|
+
photo_objects/django/tests/utils.py,sha256=7oAwFotAFzxIE2YFGysj_a_y-u7E7Z7_K7vl7ycUAzM,4639
|
|
42
45
|
photo_objects/django/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
43
|
-
photo_objects/django/views/utils.py,sha256=
|
|
44
|
-
photo_objects/django/views/api/__init__.py,sha256=
|
|
46
|
+
photo_objects/django/views/utils.py,sha256=AjJK5r5HmTF63E9Q4W3pKggDESuhNXUvbROpS8m70KM,1319
|
|
47
|
+
photo_objects/django/views/api/__init__.py,sha256=GgywMJMSFmP5aoMEaYut5V666zachd5YFQIDBMr5znU,188
|
|
45
48
|
photo_objects/django/views/api/album.py,sha256=EZMOkxYzLSWr9wwXnd4yAO64JtXZq2k3FYohiNMFbGQ,1602
|
|
46
49
|
photo_objects/django/views/api/auth.py,sha256=EN_ExegzmLN-bhSzu3L9-6UE9qodPd7_ZRLilzrvc8Y,819
|
|
47
|
-
photo_objects/django/views/api/photo.py,sha256=
|
|
50
|
+
photo_objects/django/views/api/photo.py,sha256=WHSayWTi_wG6otq6Rz1IKqJ5ik5riclR3AWB15ge5RU,4613
|
|
48
51
|
photo_objects/django/views/api/utils.py,sha256=uQzKdSKHRAux5OZzqgWQr0gsK_FeweQP0cg_67OWA_Y,264
|
|
49
|
-
photo_objects/django/views/ui/__init__.py,sha256=
|
|
50
|
-
photo_objects/django/views/ui/album.py,sha256=
|
|
51
|
-
photo_objects/django/views/ui/configuration.py,sha256=
|
|
52
|
-
photo_objects/django/views/ui/photo.py,sha256=
|
|
52
|
+
photo_objects/django/views/ui/__init__.py,sha256=Y3XrckZExbHpWVNwDUGLfb99_midb8-5j6Ouf_Yu_G4,128
|
|
53
|
+
photo_objects/django/views/ui/album.py,sha256=WmWrbY3nPCK7NIZYE9rJPzC39HjwN-TW8fN4gxIC3Yk,4765
|
|
54
|
+
photo_objects/django/views/ui/configuration.py,sha256=jyZT3ZAzKa7RnzhED5anDShRCBiCHxOU81Cddt10-4Q,5096
|
|
55
|
+
photo_objects/django/views/ui/photo.py,sha256=flwcET5nSChzfyAEWRTUlklTW2o64przNXSdWn-jxLw,6676
|
|
56
|
+
photo_objects/django/views/ui/photo_change_request.py,sha256=eaYGXFqtHj8qonDAmPyn4nrEHkL13EBD-1s8Phs0XP4,2098
|
|
53
57
|
photo_objects/django/views/ui/users.py,sha256=nb73cnzuV98QkJb0j8F2hqPgOGFIWpUFTFu6dXMeVwM,722
|
|
54
58
|
photo_objects/django/views/ui/utils.py,sha256=YV_YcUbX-zUkdFnBlezPChR6aPDhZJ9loSOHBSzF6Cc,273
|
|
55
|
-
photo_objects-0.
|
|
56
|
-
photo_objects-0.
|
|
57
|
-
photo_objects-0.
|
|
58
|
-
photo_objects-0.
|
|
59
|
-
photo_objects-0.
|
|
59
|
+
photo_objects-0.8.1.dist-info/licenses/LICENSE,sha256=V3w6hTjXfP65F4r_mejveHcV5igHrblxao3-2RlfVlA,1068
|
|
60
|
+
photo_objects-0.8.1.dist-info/METADATA,sha256=ieM9ijgsXYCJSUp3MCGrcHgcq9lz9AAv6MwlaNlqWDY,3605
|
|
61
|
+
photo_objects-0.8.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
62
|
+
photo_objects-0.8.1.dist-info/top_level.txt,sha256=SZeL8mhf-WMGdhRtTGFvZc3aIRBboQls9O0cFDMGdQ0,14
|
|
63
|
+
photo_objects-0.8.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|