photo-objects 0.10.1__py3-none-any.whl → 0.10.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,6 +20,10 @@ from .models import Album, Photo, PhotoChangeRequest
20
20
  KEY_POSTFIX_CHARS = 'bcdfghjklmnpqrstvwxz2456789'
21
21
  KEY_POSTFIX_LEN = 5
22
22
 
23
+ ALBUM_TITLE_HELP = _(
24
+ 'When creating a new album, album key is generated based on the title. '
25
+ 'Modifying the title later does not change the album key.'
26
+ )
23
27
  ALT_TEXT_HELP = _('Alternative text content for the photo.')
24
28
 
25
29
 
@@ -37,6 +41,44 @@ def description_help(resource):
37
41
  }
38
42
 
39
43
 
44
+ def visibility_help(visibility: str):
45
+ visibility = Album.Visibility(visibility)
46
+ if visibility == Album.Visibility.PUBLIC:
47
+ return _(
48
+ 'The album is visible to anyone without authentication.')
49
+ if visibility == Album.Visibility.HIDDEN:
50
+ return _(
51
+ 'The album is visible to anyone with the link. Only '
52
+ 'authenticated users can see the album in albums list.')
53
+ if visibility == Album.Visibility.PRIVATE:
54
+ return _(
55
+ 'The album is only visible to authenticated users.')
56
+ if visibility == Album.Visibility.ADMIN:
57
+ return _(
58
+ 'The album is only visible to admin users.')
59
+ return None
60
+
61
+
62
+ class VisibilityRadioSelect(RadioSelect):
63
+ def create_option(
64
+ self, name, value, label, selected, index, subindex=None, attrs=None
65
+ ):
66
+ option = super().create_option(
67
+ name,
68
+ value,
69
+ label,
70
+ selected,
71
+ index,
72
+ subindex=subindex,
73
+ attrs=attrs)
74
+ option['label'] = mark_safe(f'''
75
+ <div>
76
+ <span class="label">{label}</span>
77
+ <p class="helptext">{visibility_help(option.get('value'))}</p>
78
+ </div>''')
79
+ return option
80
+
81
+
40
82
  def _check_admin_visibility(form):
41
83
  if form.user and form.user.is_staff:
42
84
  return
@@ -60,7 +102,10 @@ class CreateAlbumForm(ModelForm):
60
102
  fields = ['key', 'title', 'description', 'visibility']
61
103
  help_texts = {
62
104
  **description_help('album'),
105
+ 'title': ALBUM_TITLE_HELP,
63
106
  }
107
+ widgets = {'visibility': VisibilityRadioSelect(
108
+ attrs={'class': 'visibility-select'}), }
64
109
 
65
110
  def __init__(self, *args, user=None, **kwargs):
66
111
  super().__init__(*args, **kwargs)
@@ -136,9 +181,15 @@ class ModifyAlbumForm(ModelForm):
136
181
  'cover_photo': _(
137
182
  'Select a cover photo for the album. The cover photo is '
138
183
  'visible on the albums list page and in album preview image.'),
184
+ 'title': ALBUM_TITLE_HELP,
139
185
  }
140
186
  widgets = {
141
- 'cover_photo': RadioSelect(attrs={'class': 'photo-select'}),
187
+ 'cover_photo': RadioSelect(
188
+ attrs={
189
+ 'class': 'photo-select'}),
190
+ 'visibility': VisibilityRadioSelect(
191
+ attrs={
192
+ 'class': 'visibility-select'}),
142
193
  }
143
194
 
144
195
  def __init__(self, *args, user=None, **kwargs):
@@ -0,0 +1,29 @@
1
+ # Generated by Django 5.2.6 on 2025-12-20 18:09
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', '0007_backup'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddField(
15
+ model_name='sitesettings',
16
+ name='copyright_notice',
17
+ field=models.CharField(blank=True, help_text='Copyright notice to display in the site footer. Content will be HTML-escaped before rendering.'),
18
+ ),
19
+ migrations.AlterField(
20
+ model_name='sitesettings',
21
+ name='description',
22
+ field=models.TextField(blank=True, help_text='Description of the site, used in site level meta description tags and social media previews.'),
23
+ ),
24
+ migrations.AlterField(
25
+ model_name='sitesettings',
26
+ name='preview_image',
27
+ field=models.ForeignKey(blank=True, help_text='Photo to use as the site level social media preview image.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='photo_objects.photo'),
28
+ ),
29
+ ]
@@ -211,10 +211,26 @@ class SiteSettings(models.Model):
211
211
  site = models.OneToOneField(
212
212
  Site, on_delete=models.CASCADE, related_name="settings")
213
213
 
214
- description = models.TextField(blank=True)
214
+ description = models.TextField(
215
+ blank=True,
216
+ help_text=_(
217
+ "Description of the site, used in site level meta description "
218
+ "tags and social media previews."),
219
+ )
215
220
  preview_image = models.ForeignKey(
216
- Photo, blank=True, null=True, on_delete=models.SET_NULL)
217
-
221
+ Photo,
222
+ blank=True,
223
+ null=True,
224
+ on_delete=models.SET_NULL,
225
+ help_text=_(
226
+ "Photo to use as the site level social media preview image."),
227
+ )
228
+ copyright_notice = models.CharField(
229
+ blank=True,
230
+ help_text=_(
231
+ "Copyright notice to display in the site footer. Content will be "
232
+ "HTML-escaped before rendering."),
233
+ )
218
234
  objects = SiteSettingsManager()
219
235
 
220
236
  def __str__(self):
@@ -1,5 +1,7 @@
1
1
  from datetime import datetime
2
2
  from django import template
3
+ from django.utils.safestring import mark_safe
4
+ from django.utils.html import escape
3
5
 
4
6
  from photo_objects.django.models import SiteSettings
5
7
  from photo_objects.django.views.utils import meta_description
@@ -59,3 +61,19 @@ def meta_og(context):
59
61
  }
60
62
  except Exception:
61
63
  return context
64
+
65
+
66
+ @register.simple_tag(takes_context=True)
67
+ def copyright_notice(context):
68
+ try:
69
+ request = context.get("request")
70
+ site = request.site
71
+
72
+ settings = SiteSettings.objects.get(site)
73
+
74
+ if settings.copyright_notice:
75
+ notice = escape(settings.copyright_notice)
76
+ return mark_safe(f"<div>{notice}</div>")
77
+ return ""
78
+ except Exception:
79
+ return ""
@@ -56,6 +56,18 @@ def get_info(request: HttpRequest, album_key: str):
56
56
  return None
57
57
 
58
58
 
59
+ def _timeline(album: Album):
60
+ if not album.first_timestamp or not album.last_timestamp:
61
+ return None
62
+
63
+ start = album.first_timestamp.strftime("%Y %B")
64
+ end = album.last_timestamp.strftime("%Y %B")
65
+
66
+ if start == end:
67
+ return start
68
+ return f"{start} – {end}"
69
+
70
+
59
71
  @json_problem_as_html
60
72
  def show_album(request: HttpRequest, album_key: str):
61
73
  album = api.check_album_access(request, album_key)
@@ -64,6 +76,7 @@ def show_album(request: HttpRequest, album_key: str):
64
76
  back = BackLink("Albums", reverse('photo_objects:list_albums'))
65
77
  details = {
66
78
  "Description": render_markdown(album.description),
79
+ "Timeline": _timeline(album),
67
80
  "Visibility": Album.Visibility(album.visibility).label,
68
81
  }
69
82
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: photo-objects
3
- Version: 0.10.1
3
+ Version: 0.10.3
4
4
  Summary: Application for storing photos in S3 compatible object-storage.
5
5
  Author: Toni Kangas
6
6
  License: MIT License
@@ -8,8 +8,8 @@ photo_objects/django/admin.py,sha256=pvyQKs2FMtKtSS2PE_NKR_jsSi7-GKz3bbsqDNgjt6w
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=HIkDRQhSMzvj0D4BdKEuEndB3bV-3NTH_h0gMRQ9UlQ,6123
12
- photo_objects/django/models.py,sha256=OxkkczIBg7TaFWWm4VdtbRMJOK_OQKrV29g-X2cm5BQ,7247
11
+ photo_objects/django/forms.py,sha256=DWCnVEznev3h5ztYXz0gjMGZ8QUwlqML4KUKHcUiiZ4,7848
12
+ photo_objects/django/models.py,sha256=DsbaFo3xhzy5MHgtZXYmZuaHYqJSPPCTaZWAy_lHndU,7746
13
13
  photo_objects/django/objsto.py,sha256=kf-Tv-kDt47Mnx0xMolAct_lP4M0H9xg03M3BnfAnJM,6909
14
14
  photo_objects/django/signals.py,sha256=_gb4vlZkeFNYWXxwhNreaUJoOsbIWvP8OovVLtzepaE,2161
15
15
  photo_objects/django/urls.py,sha256=QHGjDj3XCArLtmMcP3DKp8H9Xfd2X4SEvEKmMnx2KDk,2551
@@ -32,9 +32,10 @@ photo_objects/django/migrations/0004_camera_setup_and_settings.py,sha256=CS5xyIH
32
32
  photo_objects/django/migrations/0005_sitesettings.py,sha256=Ilf5vUwTFQfXVP37zz0NWo5dQdeHDh5e-MV3enm0ZKI,994
33
33
  photo_objects/django/migrations/0006_photo_alt_text.py,sha256=5ZWR-bfS_72Mpb0SrvtnWzVME-zlcRPzDCofmFWkUeU,1003
34
34
  photo_objects/django/migrations/0007_backup.py,sha256=tLHLAnojppT6mhFF3FokGdwDbIj40_yqO9qMFm0H3UI,649
35
+ photo_objects/django/migrations/0008_sitesettings_copyright_notice.py,sha256=PmSAR85zZ6YEqfjZJCBnTONroIf4GSGC2m9BUkb2xf4,1118
35
36
  photo_objects/django/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
37
  photo_objects/django/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
- photo_objects/django/templatetags/photo_objects_extras.py,sha256=1L6wweVA96rTWVXqgyf8gSey1wQKi01h6sqv9kZeTIY,1399
38
+ photo_objects/django/templatetags/photo_objects_extras.py,sha256=FH5JlNigVf1StIphRrd87nRzEhbrB9FYDWSiS7M1cAY,1890
38
39
  photo_objects/django/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
40
  photo_objects/django/tests/test_album.py,sha256=Uik36QbhaEojed6g862xqj8YI0d2963StU-lOpqMqAc,13095
40
41
  photo_objects/django/tests/test_auth.py,sha256=hgr1UMVLvSI1x5zY7wTEXSBKfM5E_sNMIFlx8mVWYPY,3928
@@ -53,14 +54,14 @@ photo_objects/django/views/api/auth.py,sha256=EN_ExegzmLN-bhSzu3L9-6UE9qodPd7_ZR
53
54
  photo_objects/django/views/api/photo.py,sha256=WHSayWTi_wG6otq6Rz1IKqJ5ik5riclR3AWB15ge5RU,4613
54
55
  photo_objects/django/views/api/utils.py,sha256=uQzKdSKHRAux5OZzqgWQr0gsK_FeweQP0cg_67OWA_Y,264
55
56
  photo_objects/django/views/ui/__init__.py,sha256=Y3XrckZExbHpWVNwDUGLfb99_midb8-5j6Ouf_Yu_G4,128
56
- photo_objects/django/views/ui/album.py,sha256=IYjhjBj9MOffUDUSTm3IMjJoz-xGfvA_LVa0Vc_aXV8,5167
57
+ photo_objects/django/views/ui/album.py,sha256=0n_c-5MYJouzg2plK6_ZBtE6iqOdaqttY1RdsFO2V9s,5495
57
58
  photo_objects/django/views/ui/configuration.py,sha256=miEJTm_cRANu9Wt3SCcU-tYUwM7WLKgQm8zgApmKMxE,5464
58
59
  photo_objects/django/views/ui/photo.py,sha256=EOu43klthhmG6ysCrw2NWb7Vcz8I3FBR6GZUFhLBLG4,6281
59
60
  photo_objects/django/views/ui/photo_change_request.py,sha256=PGP98APOVTkTw3FTvzYn92BBshensFwA8U0w8l1vIjg,2127
60
61
  photo_objects/django/views/ui/users.py,sha256=ABAtKwNoViYy2ht6X313BSm6wgvL302LVUNimp43gxc,649
61
62
  photo_objects/django/views/ui/utils.py,sha256=fAEgG6cdL_u9TBLWgqQGG8zC0RX1a-9vC6C06ST7InM,823
62
- photo_objects-0.10.1.dist-info/licenses/LICENSE,sha256=V3w6hTjXfP65F4r_mejveHcV5igHrblxao3-2RlfVlA,1068
63
- photo_objects-0.10.1.dist-info/METADATA,sha256=-epYO3uZXgbPW4MgGPKBKAb6JBG-ByGD0ID_3ts2gtA,3616
64
- photo_objects-0.10.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
65
- photo_objects-0.10.1.dist-info/top_level.txt,sha256=SZeL8mhf-WMGdhRtTGFvZc3aIRBboQls9O0cFDMGdQ0,14
66
- photo_objects-0.10.1.dist-info/RECORD,,
63
+ photo_objects-0.10.3.dist-info/licenses/LICENSE,sha256=V3w6hTjXfP65F4r_mejveHcV5igHrblxao3-2RlfVlA,1068
64
+ photo_objects-0.10.3.dist-info/METADATA,sha256=9I5HA3kH3eW3Sg5bY3GC1Yd24wv_0haEP2Jta8IEyOw,3616
65
+ photo_objects-0.10.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
66
+ photo_objects-0.10.3.dist-info/top_level.txt,sha256=SZeL8mhf-WMGdhRtTGFvZc3aIRBboQls9O0cFDMGdQ0,14
67
+ photo_objects-0.10.3.dist-info/RECORD,,