photo-objects 0.4.1__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. {photo_objects-0.4.1 → photo_objects-0.5.0}/PKG-INFO +1 -1
  2. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/admin.py +2 -1
  3. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/api/album.py +0 -21
  4. photo_objects-0.5.0/photo_objects/django/migrations/0005_sitesettings.py +27 -0
  5. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/models.py +42 -0
  6. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/signals.py +1 -30
  7. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/templatetags/photo_objects_extras.py +6 -5
  8. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/tests/test_album.py +0 -23
  9. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/views/ui/album.py +1 -10
  10. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/views/ui/configuration.py +28 -41
  11. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/views/ui/users.py +3 -3
  12. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects.egg-info/PKG-INFO +1 -1
  13. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects.egg-info/SOURCES.txt +1 -1
  14. {photo_objects-0.4.1 → photo_objects-0.5.0}/pyproject.toml +1 -1
  15. photo_objects-0.4.1/photo_objects/django/management/commands/create-site-albums.py +0 -28
  16. {photo_objects-0.4.1 → photo_objects-0.5.0}/LICENSE +0 -0
  17. {photo_objects-0.4.1 → photo_objects-0.5.0}/README.md +0 -0
  18. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/__init__.py +0 -0
  19. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/config.py +0 -0
  20. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/__init__.py +0 -0
  21. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/api/__init__.py +0 -0
  22. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/api/auth.py +0 -0
  23. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/api/photo.py +0 -0
  24. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/api/utils.py +0 -0
  25. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/apps.py +0 -0
  26. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/conf.py +0 -0
  27. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/context_processors.py +0 -0
  28. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/forms.py +0 -0
  29. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/management/__init__.py +0 -0
  30. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/management/commands/__init__.py +0 -0
  31. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/management/commands/clean-scaled-photos.py +0 -0
  32. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/management/commands/create-initial-admin-account.py +0 -0
  33. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/migrations/0001_initial.py +0 -0
  34. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/migrations/0002_created_at_updated_at.py +0 -0
  35. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/migrations/0003_admin_visibility.py +0 -0
  36. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/migrations/0004_camera_setup_and_settings.py +0 -0
  37. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/migrations/__init__.py +0 -0
  38. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/objsto.py +0 -0
  39. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/templatetags/__init__.py +0 -0
  40. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/tests/__init__.py +0 -0
  41. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/tests/test_auth.py +0 -0
  42. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/tests/test_commands.py +0 -0
  43. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/tests/test_img.py +0 -0
  44. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/tests/test_photo.py +0 -0
  45. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/tests/test_utils.py +0 -0
  46. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/tests/utils.py +0 -0
  47. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/urls.py +0 -0
  48. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/views/__init__.py +0 -0
  49. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/views/api/__init__.py +0 -0
  50. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/views/api/album.py +0 -0
  51. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/views/api/auth.py +0 -0
  52. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/views/api/photo.py +0 -0
  53. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/views/api/utils.py +0 -0
  54. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/views/ui/__init__.py +0 -0
  55. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/views/ui/photo.py +0 -0
  56. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/views/ui/utils.py +0 -0
  57. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/django/views/utils.py +0 -0
  58. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/error.py +0 -0
  59. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/img.py +0 -0
  60. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects/utils.py +0 -0
  61. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects.egg-info/dependency_links.txt +0 -0
  62. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects.egg-info/requires.txt +0 -0
  63. {photo_objects-0.4.1 → photo_objects-0.5.0}/photo_objects.egg-info/top_level.txt +0 -0
  64. {photo_objects-0.4.1 → photo_objects-0.5.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: photo-objects
3
- Version: 0.4.1
3
+ Version: 0.5.0
4
4
  Summary: Application for storing photos in S3 compatible object-storage.
5
5
  Author: Toni Kangas
6
6
  License: MIT License
@@ -1,6 +1,7 @@
1
1
  from django.contrib import admin
2
2
 
3
- from .models import Album, Photo
3
+ from .models import Album, Photo, SiteSettings
4
4
 
5
5
  admin.site.register(Album)
6
6
  admin.site.register(Photo)
7
+ admin.site.register(SiteSettings)
@@ -1,6 +1,3 @@
1
- import re
2
-
3
- from django.contrib.sites.models import Site
4
1
  from django.db.models.deletion import ProtectedError
5
2
  from django.http import HttpRequest
6
3
 
@@ -16,24 +13,6 @@ from .utils import (
16
13
  )
17
14
 
18
15
 
19
- def get_site_album(site: Site) -> tuple[Album, bool]:
20
- album_key = f'_site_{site.id}'
21
- return Album.objects.get_or_create(
22
- key=album_key,
23
- defaults={
24
- 'title': site.name,
25
- 'visibility': Album.Visibility.ADMIN,
26
- })
27
-
28
-
29
- def parse_site_id(album_key: str) -> int | None:
30
- key_match = re.match(r'_site_([0-9]+)', album_key)
31
- if not key_match:
32
- return None
33
-
34
- return int(key_match.group(1))
35
-
36
-
37
16
  def get_albums(request: HttpRequest):
38
17
  if not request.user.is_authenticated:
39
18
  return Album.objects.filter(visibility=Album.Visibility.PUBLIC)
@@ -0,0 +1,27 @@
1
+ # Generated by Django 5.2 on 2025-08-26 18:39
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('photo_objects', '0004_camera_setup_and_settings'),
11
+ ('sites', '0002_alter_domain_unique'),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.CreateModel(
16
+ name='SiteSettings',
17
+ fields=[
18
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19
+ ('description', models.TextField(blank=True)),
20
+ ('preview_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='photo_objects.photo')),
21
+ ('site', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='settings', to='sites.site')),
22
+ ],
23
+ options={
24
+ 'verbose_name_plural': 'site settings',
25
+ },
26
+ ),
27
+ ]
@@ -1,4 +1,6 @@
1
1
  from django.db import models
2
+ from django.db.models.signals import pre_delete, pre_save
3
+ from django.contrib.sites.models import Site
2
4
  from django.core.validators import RegexValidator
3
5
  from django.utils.translation import gettext_lazy as _
4
6
 
@@ -146,3 +148,43 @@ class Photo(BaseModel):
146
148
  exposure_time=self.exposure_time,
147
149
  iso_speed=self.iso_speed,
148
150
  )
151
+
152
+
153
+ SETTINGS_CACHE = {}
154
+
155
+
156
+ class SiteSettingsManager(models.Manager):
157
+ def get(self, site: Site):
158
+ cached = SETTINGS_CACHE.get(site.id)
159
+ if cached:
160
+ return cached
161
+ settings, _ = self.get_or_create(site=site)
162
+ SETTINGS_CACHE[site.id] = settings
163
+ return settings
164
+
165
+
166
+ class SiteSettings(models.Model):
167
+ class Meta:
168
+ verbose_name_plural = "site settings"
169
+
170
+ site = models.OneToOneField(
171
+ Site, on_delete=models.CASCADE, related_name="settings")
172
+
173
+ description = models.TextField(blank=True)
174
+ preview_image = models.ForeignKey(
175
+ Photo, blank=True, null=True, on_delete=models.SET_NULL)
176
+
177
+ objects = SiteSettingsManager()
178
+
179
+ def __str__(self):
180
+ return f"Settings for {self.site.name}"
181
+
182
+
183
+ def clear_cached_settings(sender, **kwargs):
184
+ site_id = kwargs.get("instance").site.id
185
+ if site_id in SETTINGS_CACHE:
186
+ del SETTINGS_CACHE[site_id]
187
+
188
+
189
+ pre_save.connect(clear_cached_settings, sender=SiteSettings)
190
+ pre_delete.connect(clear_cached_settings, sender=SiteSettings)
@@ -1,9 +1,7 @@
1
- from django.contrib.sites.models import Site
2
1
  from django.db.models.signals import post_save, post_delete
3
2
  from django.dispatch import receiver
4
3
 
5
- from .api.album import parse_site_id, get_site_album
6
- from .models import Album, Photo
4
+ from .models import Photo
7
5
 
8
6
 
9
7
  @receiver(post_save, sender=Photo)
@@ -58,30 +56,3 @@ def update_album_on_photo_delete(sender, **kwargs):
58
56
 
59
57
  if needs_save:
60
58
  album.save()
61
-
62
-
63
- @receiver(post_save, sender=Site)
64
- def update_album_on_site_save(sender, **kwargs):
65
- site = kwargs.get('instance', None)
66
- album, created = get_site_album(site)
67
-
68
- if not created and album.title != site.name:
69
- album.title = site.name
70
- album.save()
71
-
72
-
73
- @receiver(post_save, sender=Album)
74
- def update_site_on_album_save(sender, **kwargs):
75
- album = kwargs.get('instance', None)
76
- site_id = parse_site_id(album.key)
77
- if site_id is None:
78
- return
79
-
80
- try:
81
- site = Site.objects.get(id=site_id)
82
- except Site.DoesNotExist:
83
- return
84
-
85
- if site.name != album.title:
86
- site.name = album.title
87
- site.save()
@@ -1,7 +1,7 @@
1
1
  from datetime import datetime
2
2
  from django import template
3
3
 
4
- from photo_objects.django.api.album import get_site_album
4
+ from photo_objects.django.models import SiteSettings
5
5
  from photo_objects.django.views.utils import meta_description
6
6
 
7
7
 
@@ -48,13 +48,14 @@ def meta_og(context):
48
48
  try:
49
49
  request = context.get("request")
50
50
  site = request.site
51
- album, _ = get_site_album(site)
51
+
52
+ settings = SiteSettings.objects.get(site)
52
53
 
53
54
  return {
54
55
  'request': request,
55
- "title": album.title or site.name,
56
- "description": meta_description(request, album.description),
57
- "photo": album.cover_photo,
56
+ "title": site.name,
57
+ "description": meta_description(request, settings.description),
58
+ "photo": settings.preview_image,
58
59
  }
59
60
  except Exception:
60
61
  return context
@@ -3,7 +3,6 @@ from time import sleep
3
3
 
4
4
  from django.contrib.auth import get_user_model
5
5
  from django.contrib.auth.models import Permission
6
- from django.contrib.sites.models import Site
7
6
 
8
7
  from photo_objects.django.models import Album
9
8
  from photo_objects.img import utcnow
@@ -376,25 +375,3 @@ class AlbumViewTests(TestCase):
376
375
 
377
376
  response = self.client.delete("/api/albums/copenhagen")
378
377
  self.assertStatus(response, 204)
379
-
380
- def test_site_config_album_title_change(self):
381
- site = Site.objects.get(id=1)
382
- album, _ = Album.objects.get_or_create(
383
- key="_site_1",
384
- defaults={
385
- "title": "Site 1",
386
- "visibility": Album.Visibility.ADMIN,
387
- }
388
- )
389
-
390
- site.name = "Changed in site"
391
- site.save()
392
-
393
- album.refresh_from_db()
394
- self.assertEqual(album.title, "Changed in site")
395
-
396
- album.title = "Changed in album"
397
- album.save()
398
-
399
- site.refresh_from_db()
400
- self.assertEqual(site.name, "Changed in album")
@@ -4,7 +4,6 @@ from django.urls import reverse
4
4
  from django.utils.translation import gettext_lazy as _
5
5
 
6
6
  from photo_objects.django import api
7
- from photo_objects.django.api.album import parse_site_id
8
7
  from photo_objects.django.api.utils import FormValidationFailed
9
8
  from photo_objects.django.forms import CreateAlbumForm, ModifyAlbumForm
10
9
  from photo_objects.django.models import Album
@@ -51,15 +50,7 @@ def new_album(request: HttpRequest):
51
50
 
52
51
 
53
52
  def get_info(request: HttpRequest, album_key: str):
54
- site_id = parse_site_id(album_key)
55
- if site_id is not None and request.site:
56
- return render_markdown(
57
- "This is a special album for configuring site metadata for "
58
- f"**{request.site.name}**. Use album title to change the site "
59
- "name, albums cover photo to configure the preview image, and "
60
- "album description to configure the site description. The album "
61
- "title is automatically updated when the related sites name is "
62
- "changed and vice versa.")
53
+ # TODO: Remove this later if not needed
63
54
  return None
64
55
 
65
56
 
@@ -5,7 +5,7 @@ from django.shortcuts import render
5
5
  from django.urls import reverse
6
6
  from django.utils.translation import gettext_lazy as _
7
7
 
8
- from photo_objects.django.models import Album
8
+ from photo_objects.django.api.utils import JsonProblem
9
9
  from photo_objects.django.views.utils import BackLink, render_markdown
10
10
 
11
11
  from .utils import json_problem_as_html
@@ -108,25 +108,20 @@ def domain_matches_request(request: HttpRequest) -> Validation:
108
108
  )
109
109
 
110
110
 
111
- def site_preview_configured(
112
- request: HttpRequest,
113
- album: Album | Exception) -> Validation:
111
+ def site_preview_configured(request: HttpRequest) -> Validation:
114
112
  detail = None
115
113
 
116
- if isinstance(album, Exception):
117
- ok = False
118
- detail = f'Failed to resolve site or album: `{str(album)}`'
114
+ ok = request.site.settings.preview_image is not None
115
+ if ok:
116
+ detail = (
117
+ f'The site settings for `{request.site.domain}` configure a '
118
+ 'preview image.'
119
+ )
119
120
  else:
120
- ok = album.cover_photo is not None
121
- if ok:
122
- detail = (
123
- f'The `{album.key}` album has a cover photo configured. This '
124
- 'photo will be used as the site preview image.'
125
- )
126
- else:
127
- detail = (
128
- f'Set cover photo for `{album.key}` album to configure '
129
- 'the preview image.')
121
+ detail = (
122
+ 'Configure a preview image in site settings for '
123
+ f'`{request.site.domain}`.'
124
+ )
130
125
 
131
126
  return Validation(
132
127
  check=_("Site has a default preview image"),
@@ -135,25 +130,21 @@ def site_preview_configured(
135
130
  )
136
131
 
137
132
 
138
- def site_description_configured(
139
- request: HttpRequest,
140
- album: Album | Exception) -> Validation:
133
+ def site_description_configured(request: HttpRequest) -> Validation:
141
134
  detail = None
142
135
 
143
- if isinstance(album, Exception):
144
- ok = False
145
- detail = f'Failed to resolve site or album: `{str(album)}`'
136
+ settings = request.site.settings
137
+ ok = settings.description is not None and len(settings.description) > 0
138
+ if ok:
139
+ detail = (
140
+ f'The site settings for `{request.site.domain}` configure a '
141
+ 'description.'
142
+ )
146
143
  else:
147
- ok = album.description is not None and len(album.description) > 0
148
- if ok:
149
- detail = (
150
- f'The `{album.key}` album has a description. This description '
151
- 'will be used as the site description.'
152
- )
153
- else:
154
- detail = (
155
- f'Set description for `{album.key}` album to configure '
156
- 'the site description.')
144
+ detail = (
145
+ 'Configure a description in site settings for '
146
+ f'`{request.site.domain}`.'
147
+ )
157
148
 
158
149
  return Validation(
159
150
  check=_("Site has a default description"),
@@ -164,19 +155,15 @@ def site_description_configured(
164
155
 
165
156
  @json_problem_as_html
166
157
  def configuration(request: HttpRequest):
167
- try:
168
- site_id = request.site.id
169
- album_key = f"_site_{site_id}"
170
- album = Album.objects.get(key=album_key)
171
- except Exception as e:
172
- album = e
158
+ if not request.user.is_staff:
159
+ raise JsonProblem("Page not found", status=404)
173
160
 
174
161
  validations = [
175
162
  uses_https(request),
176
163
  site_is_configured(request),
177
164
  domain_matches_request(request),
178
- site_preview_configured(request, album),
179
- site_description_configured(request, album),
165
+ site_preview_configured(request),
166
+ site_description_configured(request),
180
167
  ]
181
168
 
182
169
  back = BackLink("Back to albums", reverse('photo_objects:list_albums'))
@@ -2,18 +2,18 @@ from django.contrib.auth import views as auth_views
2
2
  from django.http import HttpRequest
3
3
  from django.urls import reverse_lazy
4
4
 
5
- from photo_objects.django.api.album import get_site_album
5
+ from photo_objects.django.models import SiteSettings
6
6
  from photo_objects.django.views.utils import BackLink
7
7
 
8
8
 
9
9
  def login(request: HttpRequest):
10
- album, _ = get_site_album(request.site)
10
+ settings = SiteSettings.objects.get(request.site)
11
11
 
12
12
  return auth_views.LoginView.as_view(
13
13
  template_name="photo_objects/form.html",
14
14
  extra_context={
15
15
  "title": "Login",
16
- "photo": album.cover_photo,
16
+ "photo": settings.preview_image,
17
17
  "action": "Login",
18
18
  "back": BackLink(
19
19
  'Back to albums',
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: photo-objects
3
- Version: 0.4.1
3
+ Version: 0.5.0
4
4
  Summary: Application for storing photos in S3 compatible object-storage.
5
5
  Author: Toni Kangas
6
6
  License: MIT License
@@ -30,11 +30,11 @@ photo_objects/django/management/__init__.py
30
30
  photo_objects/django/management/commands/__init__.py
31
31
  photo_objects/django/management/commands/clean-scaled-photos.py
32
32
  photo_objects/django/management/commands/create-initial-admin-account.py
33
- photo_objects/django/management/commands/create-site-albums.py
34
33
  photo_objects/django/migrations/0001_initial.py
35
34
  photo_objects/django/migrations/0002_created_at_updated_at.py
36
35
  photo_objects/django/migrations/0003_admin_visibility.py
37
36
  photo_objects/django/migrations/0004_camera_setup_and_settings.py
37
+ photo_objects/django/migrations/0005_sitesettings.py
38
38
  photo_objects/django/migrations/__init__.py
39
39
  photo_objects/django/templatetags/__init__.py
40
40
  photo_objects/django/templatetags/photo_objects_extras.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "photo-objects"
7
- version = "0.4.1"
7
+ version = "0.5.0"
8
8
  dependencies = [
9
9
  "markdown~=3.7",
10
10
  "minio~=7.2",
@@ -1,28 +0,0 @@
1
- # pylint: disable=invalid-name
2
- from django.core.management.base import BaseCommand
3
- from django.contrib.sites.models import Site
4
-
5
- from photo_objects.django.api.album import get_site_album
6
-
7
-
8
- class Command(BaseCommand):
9
- help = "Create albums for configuring site metadata."
10
-
11
- def handle(self, *args, **options):
12
- sites = Site.objects.all()
13
-
14
- for site in sites:
15
- album, created = get_site_album(site)
16
- if created:
17
- self.stdout.write(
18
- self.style.SUCCESS(
19
- f'Album for site {site.domain} created:') +
20
- f'\n Key: {album.key}'
21
- f'\n Title: {album.title}')
22
- else:
23
- self.stdout.write(
24
- self.style.NOTICE(
25
- f'Album creation for site {site.domain} skipped: '
26
- 'Album already exists.'
27
- )
28
- )
File without changes
File without changes
File without changes