photo-objects 0.7.0__py3-none-any.whl → 0.8.0__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,114 @@
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(
61
+ request,
62
+ 'photo_objects.change_photo',
63
+ 'photo_objects.delete_photochangerequest')
64
+
65
+ photos = Photo.objects.filter(
66
+ alt_text="",
67
+ ).annotate(
68
+ change_requests_count=Count('change_requests'),
69
+ ).filter(
70
+ change_requests_count=0,
71
+ )
72
+
73
+ if not request.user.is_staff:
74
+ photos = photos.exclude(album__visibility=Album.Visibility.ADMIN)
75
+
76
+ return [photo.key for photo in photos]
77
+
78
+
79
+ def get_photo_change_request_and_photo(
80
+ request: HttpRequest,
81
+ cr_id: int):
82
+ try:
83
+ change_request = PhotoChangeRequest.objects.get(id=cr_id)
84
+ except PhotoChangeRequest.DoesNotExist:
85
+ raise PhotoChangeRequestNotFound(cr_id) from None
86
+
87
+ album_key = change_request.photo.album.key
88
+ photo_key = change_request.photo.filename
89
+ photo = check_photo_access(request, album_key, photo_key, 'xs')
90
+
91
+ return change_request, photo
92
+
93
+
94
+ def review_photo_change_request(
95
+ request: HttpRequest,
96
+ cr_id: int):
97
+ check_permissions(
98
+ request,
99
+ 'photo_objects.change_photo',
100
+ 'photo_objects.delete_photochangerequest')
101
+
102
+ change_request, photo = get_photo_change_request_and_photo(request, cr_id)
103
+ data = parse_input_data(request)
104
+
105
+ f = ReviewPhotoChangeRequestForm(data, instance=change_request)
106
+
107
+ if not f.is_valid():
108
+ raise FormValidationFailed(f)
109
+
110
+ if f.cleaned_data['action'] == "approve":
111
+ photo.alt_text = f.cleaned_data['alt_text']
112
+ photo.save()
113
+
114
+ 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
 
@@ -0,0 +1,91 @@
1
+ from django.contrib.auth import get_user_model
2
+ from django.contrib.auth.models import Permission
3
+
4
+ from photo_objects.django.models import Album
5
+
6
+
7
+ from .utils import TestCase, create_dummy_photo
8
+
9
+
10
+ TEST_PREFIX = "test-photo-change-request"
11
+
12
+
13
+ def _filter_by_test_prefix(items):
14
+ return [item for item in items if item.startswith(TEST_PREFIX)]
15
+
16
+
17
+ class PhotoChangeRequestTests(TestCase):
18
+ def setUp(self):
19
+ user = get_user_model()
20
+ user.objects.create_user(username='no_permission', password='test')
21
+
22
+ user.objects.create_user(
23
+ username='superuser',
24
+ password='test',
25
+ is_staff=True,
26
+ is_superuser=True)
27
+
28
+ has_permission = user.objects.create_user(
29
+ username='has_permission', password='test')
30
+ permissions = [
31
+ 'change_photo',
32
+ 'delete_photochangerequest',
33
+ ]
34
+ for permission in permissions:
35
+ has_permission.user_permissions.add(
36
+ Permission.objects.get(
37
+ content_type__app_label='photo_objects',
38
+ codename=permission))
39
+
40
+ self.private_album = Album.objects.create(
41
+ key=f"{TEST_PREFIX}-private", visibility=Album.Visibility.PRIVATE)
42
+ self.admin_album = Album.objects.create(
43
+ key=f"{TEST_PREFIX}-admin", visibility=Album.Visibility.ADMIN)
44
+
45
+ create_dummy_photo(self.private_album, "001.jpg")
46
+ create_dummy_photo(self.private_album, "002.jpg")
47
+ create_dummy_photo(self.admin_album, "003.jpg")
48
+
49
+ def test_expected_photo_change_requests(self):
50
+ tests = [
51
+ ("no_permission", 403, 0),
52
+ ("has_permission", 200, 2),
53
+ ("superuser", 200, 3),
54
+ ]
55
+
56
+ for username, expected_status, expected_count in tests:
57
+ with self.subTest(username=username):
58
+ self.client.login(username=username, password='test')
59
+ response = self.client.get(
60
+ '/api/photo-change-requests/expected')
61
+ self.assertStatus(response, expected_status)
62
+ if expected_status == 200:
63
+ photos = _filter_by_test_prefix(response.json())
64
+ self.assertEqual(len(photos), expected_count)
65
+
66
+ def test_create_photo_change_requests(self):
67
+ self.client.login(username="superuser", password='test')
68
+
69
+ response = self.client.get(
70
+ '/api/photo-change-requests/expected')
71
+ self.assertEqual(response.status_code, 200)
72
+ photos = _filter_by_test_prefix(response.json())
73
+ self.assertEqual(len(photos), 3)
74
+
75
+ response = self.client.post(
76
+ f'/api/albums/{TEST_PREFIX}-private/photos/001.jpg/change-requests', # noqa: E501
77
+ data={'alt_text': ''},
78
+ content_type="application/json")
79
+ self.assertStatus(response, 400)
80
+
81
+ response = self.client.post(
82
+ f'/api/albums/{TEST_PREFIX}-private/photos/001.jpg/change-requests', # noqa: E501
83
+ data={'alt_text': 'Test alt text'},
84
+ content_type="application/json")
85
+ self.assertStatus(response, 201)
86
+
87
+ response = self.client.get(
88
+ '/api/photo-change-requests/expected')
89
+ self.assertEqual(response.status_code, 200)
90
+ photos = _filter_by_test_prefix(response.json())
91
+ self.assertEqual(len(photos), 2)
@@ -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.0
3
+ Version: 0.8.0
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=GOaUttPCdm0EJO-8608p1odoliWha5qVgtiha51_vhs,3133
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,6 +28,7 @@ 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
@@ -37,23 +39,25 @@ photo_objects/django/tests/test_commands.py,sha256=e3lE1ZhFR39WIq2VSKDNcQHUkSJqS
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
41
  photo_objects/django/tests/test_photo.py,sha256=aZCZw7PIXIfIYX3q1lUxn8C53mLRBNK0qp18t7X4ZQ4,13696
42
+ photo_objects/django/tests/test_photo_change_requests.py,sha256=zYsiRfWuUncrd3QdcrsD9B3XoYTdmAJ0DVh7a_vHbUs,3374
40
43
  photo_objects/django/tests/test_utils.py,sha256=zBLv8lkvcLMCaH4D6GR1KZqUe-rPowhcBkQX19-Kshs,2007
41
44
  photo_objects/django/tests/utils.py,sha256=s_3hzQEY4tdja_8406s_SQyRC6fCYI_MBPqvFjC8fB4,4345
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.0.dist-info/licenses/LICENSE,sha256=V3w6hTjXfP65F4r_mejveHcV5igHrblxao3-2RlfVlA,1068
56
- photo_objects-0.7.0.dist-info/METADATA,sha256=wHfCOuVG8iXwYP6V0arHPNUURsF2HV0PvBEBEcmnB0Q,3605
57
- photo_objects-0.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
58
- photo_objects-0.7.0.dist-info/top_level.txt,sha256=SZeL8mhf-WMGdhRtTGFvZc3aIRBboQls9O0cFDMGdQ0,14
59
- photo_objects-0.7.0.dist-info/RECORD,,
59
+ photo_objects-0.8.0.dist-info/licenses/LICENSE,sha256=V3w6hTjXfP65F4r_mejveHcV5igHrblxao3-2RlfVlA,1068
60
+ photo_objects-0.8.0.dist-info/METADATA,sha256=BnYleSRms4H1v4kO5BAb4Vt5zaMitpBTSyGr3xZi5wg,3605
61
+ photo_objects-0.8.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
62
+ photo_objects-0.8.0.dist-info/top_level.txt,sha256=SZeL8mhf-WMGdhRtTGFvZc3aIRBboQls9O0cFDMGdQ0,14
63
+ photo_objects-0.8.0.dist-info/RECORD,,