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.
@@ -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)
@@ -1,3 +1,4 @@
1
1
  from .album import *
2
2
  from .auth import *
3
3
  from .photo import *
4
+ from .photo_change_request import *
@@ -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:
@@ -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
+ ]
@@ -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
- permissions = [
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
- for permission in permissions:
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
- permissions = [
25
+ add_permissions(
26
+ has_permission,
27
27
  'add_photo',
28
28
  'change_album',
29
29
  'change_photo',
30
- 'delete_photo']
31
- for permission in permissions:
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(
@@ -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,
@@ -1,3 +1,9 @@
1
1
  from .album import album, albums
2
2
  from .auth import has_permission
3
- from .photo import photo, photos, get_img
3
+ from .photo import (
4
+ photo,
5
+ photo_change_requests,
6
+ photos,
7
+ get_img,
8
+ expected_photo_change_requests,
9
+ )
@@ -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
+ )
@@ -1,4 +1,5 @@
1
1
  from .album import *
2
2
  from .configuration import *
3
3
  from .photo import *
4
+ from .photo_change_request import *
4
5
  from .users import *
@@ -10,8 +10,8 @@ from photo_objects.django.models import Album
10
10
  from photo_objects.django.views.utils import (
11
11
  BackLink,
12
12
  meta_description,
13
- render_markdown,
14
13
  )
14
+ from photo_objects.utils import render_markdown
15
15
 
16
16
  from .utils import json_problem_as_html
17
17
 
@@ -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, render_markdown
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
 
@@ -13,8 +13,8 @@ from photo_objects.django.models import Photo
13
13
  from photo_objects.django.views.utils import (
14
14
  BackLink,
15
15
  meta_description,
16
- render_markdown,
17
16
  )
17
+ from photo_objects.utils import render_markdown
18
18
 
19
19
  from .utils import json_problem_as_html
20
20
 
@@ -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
- _first_paragraph_textcontent(resource.description) or
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
- _first_paragraph_textcontent(resource.description) or
36
+ first_paragraph_textcontent(resource.description) or
55
37
  _default_photo_description(resource))
56
38
 
57
39
  if isinstance(resource, str):
58
- text = _first_paragraph_textcontent(resource)
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())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: photo-objects
3
- Version: 0.7.1
3
+ Version: 0.8.1
4
4
  Summary: Application for storing photos in S3 compatible object-storage.
5
5
  Author: Toni Kangas
6
6
  License: MIT License
@@ -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=sYliXid-bv2EgNA97woRaOnWU75yZFq7fuqzWaseiDg,139
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=ubYfhsWQ85_2moYGZCVneLQ2DVEetbVpvBNisIrtuhw,170
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=LcznhXrqPfqRY5b2SwHBbC9-uux1FiZFdQBlHsKX5NU,6649
12
- photo_objects/django/models.py,sha256=M40ZFSIX3WgyVXfHQY9SXQOY0s3fpFqfCxNV3CBD5M8,5753
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=pCUTxg4xSd3ZD8BZVjULb9QpJQ03Ek6-sKoVnYG3-OY,1975
16
- photo_objects/django/api/__init__.py,sha256=BnEHlm3mwyBy-1xhk-NFasgZa4fjCDjtfkBUoH0puPY,62
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/utils.py,sha256=xilnQndd1DXkNs6K6OqXpzUKkDSZFOG25xX3O2oJggU,5364
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=yjxP_M0bddS9Xpg1d1Wk5OyJsmxmkdYuljca4oFJSzc,13316
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=aZCZw7PIXIfIYX3q1lUxn8C53mLRBNK0qp18t7X4ZQ4,13696
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=s_3hzQEY4tdja_8406s_SQyRC6fCYI_MBPqvFjC8fB4,4345
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=oP9B6QtHPaukX97rqIlCHxanduktPGFVSCGkGxf4lzI,1691
44
- photo_objects/django/views/api/__init__.py,sha256=SxK-b7MgMtD9VRMz46rDV5qtm6AvkRcg3Moa1AWl5pY,108
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=iWDTBWrzztIKcfW7G6vh1eYTuH2JW129iOLSHjTfXCE,3743
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=N3ro5KggdV-JnfyHwoStX73b3SbVbpcsMuQNlxntVJs,92
50
- photo_objects/django/views/ui/album.py,sha256=PmVXAmqVjKLAie1NyB-qXO3eLqOmhIA8PTAGJewgxko,4738
51
- photo_objects/django/views/ui/configuration.py,sha256=oX6SV0TFBpbaxfp4cXIdSL41YJhy_aOy30TkBxOpq0M,5065
52
- photo_objects/django/views/ui/photo.py,sha256=fVpKI-XqSV8KfoTpDYxceCHR3rJzuVYfmRRQx1I0YIQ,6649
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.7.1.dist-info/licenses/LICENSE,sha256=V3w6hTjXfP65F4r_mejveHcV5igHrblxao3-2RlfVlA,1068
56
- photo_objects-0.7.1.dist-info/METADATA,sha256=JfylWVQAoc7GE1HIjjBt9CXuvrS9tn6qr6_2eRodPfs,3605
57
- photo_objects-0.7.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
58
- photo_objects-0.7.1.dist-info/top_level.txt,sha256=SZeL8mhf-WMGdhRtTGFvZc3aIRBboQls9O0cFDMGdQ0,14
59
- photo_objects-0.7.1.dist-info/RECORD,,
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,,