photo-objects 0.0.2__py3-none-any.whl → 0.0.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.
@@ -54,6 +54,13 @@ def delete_album(request: HttpRequest, album_key: str):
54
54
  check_permissions(request, 'photo_objects.delete_album')
55
55
  album = check_album_access(request, album_key)
56
56
 
57
+ if album.key.startswith('_'):
58
+ raise JsonProblem(
59
+ f"Album with {album_key} key is managed by the system and can not "
60
+ "be deleted.",
61
+ 409,
62
+ )
63
+
57
64
  try:
58
65
  album.delete()
59
66
  except ProtectedError:
@@ -23,14 +23,19 @@ KEY_POSTFIX_CHARS = 'bcdfghjklmnpqrstvwxz2456789'
23
23
  KEY_POSTFIX_LEN = 5
24
24
 
25
25
 
26
- def slugify(input: str, lower=False) -> str:
26
+ def slugify(input: str, lower=False, replace_leading_underscores=False) -> str:
27
27
  key = unicodedata.normalize(
28
28
  'NFKD', input).encode(
29
29
  'ascii', 'ignore').decode('ascii')
30
30
  if lower:
31
31
  key = key.lower()
32
+
32
33
  key = re.sub(r'[^a-zA-Z0-9._-]', '-', key)
33
34
  key = re.sub(r'[-_]{2,}', '-', key)
35
+
36
+ if replace_leading_underscores:
37
+ key = re.sub(r'^_+', '-', key)
38
+
34
39
  return key
35
40
 
36
41
 
@@ -87,6 +92,13 @@ class CreateAlbumForm(ModelForm):
87
92
 
88
93
  # If key is set to _new, generate a key from the title.
89
94
  if key != '_new':
95
+ if key.startswith('_'):
96
+ self.add_error(
97
+ 'key',
98
+ ValidationError(
99
+ _('Keys starting with underscore are reserved for '
100
+ 'system albums.'),
101
+ code='invalid'))
90
102
  return
91
103
 
92
104
  if title == '':
@@ -97,7 +109,7 @@ class CreateAlbumForm(ModelForm):
97
109
  code='required'))
98
110
  return
99
111
 
100
- key = slugify(title, True)
112
+ key = slugify(title, lower=True, replace_leading_underscores=True)
101
113
 
102
114
  postfix_iter = _postfix_generator()
103
115
  try:
@@ -170,7 +182,16 @@ class CreatePhotoForm(ModelForm):
170
182
  'timestamp',
171
183
  'height',
172
184
  'width',
173
- 'tiny_base64']
185
+ 'tiny_base64',
186
+ 'camera_make',
187
+ 'camera_model',
188
+ 'lens_make',
189
+ 'lens_model',
190
+ 'focal_length',
191
+ 'f_number',
192
+ 'exposure_time',
193
+ 'iso_speed',
194
+ ]
174
195
  error_messages = {
175
196
  'album': {
176
197
  'invalid_choice': _('Album with %(value)s key does not exist.')
@@ -19,13 +19,11 @@ class Command(BaseCommand):
19
19
 
20
20
  write_to_home_directory("initial_admin_password", password)
21
21
 
22
- msg = (
22
+ self.stdout.write(
23
23
  self.style.SUCCESS('Initial admin account created:') +
24
24
  f'\n Username: {username}'
25
25
  f'\n Password: {password}'
26
26
  )
27
-
28
- self.stdout.write(msg)
29
27
  else:
30
28
  self.stdout.write(
31
29
  self.style.NOTICE(
@@ -0,0 +1,32 @@
1
+ from django.core.management.base import BaseCommand
2
+ from django.contrib.sites.models import Site
3
+
4
+ from photo_objects.django.models import Album
5
+
6
+
7
+ class Command(BaseCommand):
8
+ help = "Create albums for configuring site metadata."
9
+
10
+ def handle(self, *args, **options):
11
+ sites = Site.objects.all()
12
+
13
+ for site in sites:
14
+ album_key = f'_site_{site.id}'
15
+ _, created = Album.objects.get_or_create(
16
+ key=album_key,
17
+ defaults={
18
+ 'visibility': Album.Visibility.ADMIN,
19
+ })
20
+
21
+ if created:
22
+ self.stdout.write(
23
+ self.style.SUCCESS(
24
+ f'Album for site {site.domain} created:') +
25
+ f'\n Key: {album_key}')
26
+ else:
27
+ self.stdout.write(
28
+ self.style.NOTICE(
29
+ f'Album creation for site {site.domain} skipped: '
30
+ 'Album already exists.'
31
+ )
32
+ )
@@ -0,0 +1,63 @@
1
+ # Generated by Django 5.0.7 on 2025-04-05 21:45
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('photo_objects', '0003_admin_visibility'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='photo',
15
+ name='camera_make',
16
+ field=models.CharField(blank=True),
17
+ ),
18
+ migrations.AddField(
19
+ model_name='photo',
20
+ name='camera_model',
21
+ field=models.CharField(blank=True),
22
+ ),
23
+ migrations.AddField(
24
+ model_name='photo',
25
+ name='exposure_time',
26
+ field=models.FloatField(blank=True, null=True),
27
+ ),
28
+ migrations.AddField(
29
+ model_name='photo',
30
+ name='f_number',
31
+ field=models.FloatField(blank=True, null=True),
32
+ ),
33
+ migrations.AddField(
34
+ model_name='photo',
35
+ name='focal_length',
36
+ field=models.FloatField(blank=True, null=True),
37
+ ),
38
+ migrations.AddField(
39
+ model_name='photo',
40
+ name='iso_speed',
41
+ field=models.IntegerField(blank=True, null=True),
42
+ ),
43
+ migrations.AddField(
44
+ model_name='photo',
45
+ name='lens_make',
46
+ field=models.CharField(blank=True),
47
+ ),
48
+ migrations.AddField(
49
+ model_name='photo',
50
+ name='lens_model',
51
+ field=models.CharField(blank=True),
52
+ ),
53
+ migrations.AlterField(
54
+ model_name='album',
55
+ name='first_timestamp',
56
+ field=models.DateTimeField(blank=True, null=True),
57
+ ),
58
+ migrations.AlterField(
59
+ model_name='album',
60
+ name='last_timestamp',
61
+ field=models.DateTimeField(blank=True, null=True),
62
+ ),
63
+ ]
@@ -65,8 +65,8 @@ class Album(BaseModel):
65
65
  null=True,
66
66
  on_delete=models.SET_NULL,
67
67
  related_name="+")
68
- first_timestamp = models.DateTimeField(null=True)
69
- last_timestamp = models.DateTimeField(null=True)
68
+ first_timestamp = models.DateTimeField(blank=True, null=True)
69
+ last_timestamp = models.DateTimeField(blank=True, null=True)
70
70
 
71
71
  def __str__(self):
72
72
  return _str(self.key, title=self.title, visibility=self.visibility)
@@ -96,6 +96,16 @@ class Photo(BaseModel):
96
96
  width = models.PositiveIntegerField()
97
97
  tiny_base64 = models.TextField(blank=True)
98
98
 
99
+ camera_make = models.CharField(blank=True)
100
+ camera_model = models.CharField(blank=True)
101
+ lens_make = models.CharField(blank=True)
102
+ lens_model = models.CharField(blank=True)
103
+
104
+ focal_length = models.FloatField(blank=True, null=True)
105
+ f_number = models.FloatField(blank=True, null=True)
106
+ exposure_time = models.FloatField(blank=True, null=True)
107
+ iso_speed = models.IntegerField(blank=True, null=True)
108
+
99
109
  def __str__(self):
100
110
  return _str(
101
111
  self.key,
@@ -127,4 +137,12 @@ class Photo(BaseModel):
127
137
  height=self.height,
128
138
  width=self.width,
129
139
  tiny_base64=self.tiny_base64,
140
+ camera_make=self.camera_make,
141
+ camera_model=self.camera_model,
142
+ lens_make=self.lens_make,
143
+ lens_model=self.lens_model,
144
+ focal_length=self.focal_length,
145
+ f_number=self.f_number,
146
+ exposure_time=self.exposure_time,
147
+ iso_speed=self.iso_speed,
130
148
  )
@@ -1,5 +1,7 @@
1
1
  from django import template
2
2
 
3
+ from photo_objects.django.models import Album
4
+
3
5
 
4
6
  register = template.Library()
5
7
 
@@ -21,3 +23,31 @@ def display_name(user):
21
23
  if user.first_name or user.last_name:
22
24
  return f'{user.first_name} {user.last_name}'.strip()
23
25
  return user.username
26
+
27
+
28
+ @register.filter
29
+ def is_list(value):
30
+ return isinstance(value, list)
31
+
32
+
33
+ @register.inclusion_tag("photo_objects/meta-og.html", takes_context=True)
34
+ def meta_og(context):
35
+ photo = context.get("photo")
36
+ title = context.get("title")
37
+
38
+ if photo and title:
39
+ return context
40
+
41
+ try:
42
+ request = context.get("request")
43
+ site = request.site
44
+ album = Album.objects.get(key=f'_site_{site.id}')
45
+
46
+ return {
47
+ 'request': request,
48
+ "title": album.title or site.name,
49
+ "description": album.description,
50
+ "photo": album.cover_photo,
51
+ }
52
+ except Exception:
53
+ return context
@@ -249,7 +249,7 @@ class AlbumViewTests(TestCase):
249
249
  username='has_permission', password='test')
250
250
  self.assertTrue(login_success)
251
251
 
252
- for key in ["", "#invalid", "()"]:
252
+ for key in ["", "#invalid", "()", "_reserved"]:
253
253
  response = self.client.post(
254
254
  "/api/albums",
255
255
  content_type="application/json",
@@ -245,6 +245,14 @@ class PhotoViewTests(TestCase):
245
245
  self.assertEqual(data.get("timestamp"), "2024-03-20T14:28:04+00:00")
246
246
  self.assertEqual(data.get("height"), 512)
247
247
  self.assertEqual(data.get("width"), 341)
248
+ self.assertEqual(data.get("camera_make"), "FUJIFILM")
249
+ self.assertEqual(data.get("camera_model"), "X-E3")
250
+ self.assertEqual(data.get("lens_make"), "FUJIFILM")
251
+ self.assertEqual(data.get("lens_model"), "XF23mmF2 R WR")
252
+ self.assertEqual(data.get("focal_length"), 23.0)
253
+ self.assertEqual(data.get("f_number"), 8.0)
254
+ self.assertEqual(data.get("exposure_time"), 0.00025)
255
+ self.assertEqual(data.get("iso_speed"), 800)
248
256
 
249
257
  tic = utcnow()
250
258
  sleep(0.1)
@@ -23,6 +23,13 @@ class TestUtils(TestCase):
23
23
  def test_slugify_lower(self):
24
24
  self.assertEqual(slugify("QwErTy!", True), "qwerty-")
25
25
 
26
+ def test_slugify_replace_leading_underscores(self):
27
+ self.assertEqual(
28
+ slugify(
29
+ "__SecretAlbum",
30
+ replace_leading_underscores=True),
31
+ "-SecretAlbum")
32
+
26
33
  def test_with_error_code(self):
27
34
  self.assertEqual(
28
35
  objsto.with_error_code("Failed", Exception('TEST')),
@@ -60,6 +60,11 @@ urlpatterns = [
60
60
  ui.delete_photo,
61
61
  name="delete_photo",
62
62
  ),
63
+ path(
64
+ "configuration",
65
+ ui.configuration,
66
+ name="configuration",
67
+ ),
63
68
  # TODO: img/<str:album_key>/<str:photo_key>/<str:size_key> path
64
69
  path(
65
70
  "users/login",
@@ -1,2 +1,3 @@
1
1
  from .album import *
2
+ from .configuration import *
2
3
  from .photo import *
@@ -1,3 +1,5 @@
1
+ import re
2
+
1
3
  from django.http import HttpRequest, HttpResponseRedirect
2
4
  from django.shortcuts import render
3
5
  from django.urls import reverse
@@ -45,6 +47,15 @@ def new_album(request: HttpRequest):
45
47
  })
46
48
 
47
49
 
50
+ def get_info(request: HttpRequest, album_key: str):
51
+ if re.match(r'_site_[0-9]+', album_key) and request.site:
52
+ return (
53
+ "This is a special album for configuring site metadata for "
54
+ f"{request.site.name}. Use album title to override the site name, "
55
+ "albums cover photo to configure the preview image, and album "
56
+ "description to configure the site description.")
57
+
58
+
48
59
  @json_problem_as_html
49
60
  def show_album(request: HttpRequest, album_key: str):
50
61
  album = api.check_album_access(request, album_key)
@@ -64,6 +75,7 @@ def show_album(request: HttpRequest, album_key: str):
64
75
  "back": back,
65
76
  "details": details,
66
77
  "photo": album.cover_photo,
78
+ "info": get_info(request, album_key),
67
79
  })
68
80
 
69
81
 
@@ -102,6 +114,7 @@ def edit_album(request: HttpRequest, album_key: str):
102
114
  "title": "Edit album",
103
115
  "back": back,
104
116
  "photo": album.cover_photo,
117
+ "info": get_info(request, album_key),
105
118
  })
106
119
 
107
120
 
@@ -125,6 +138,9 @@ def delete_album(request: HttpRequest, album_key: str):
125
138
  error = {'error': _(
126
139
  'Album can not be deleted because it contains photos. Delete '
127
140
  'all photos from the album to be able to delete the album.')}
141
+ if album.key.startswith('_'):
142
+ error = {'error': _(
143
+ 'This album is managed by the system and can not be deleted.')}
128
144
 
129
145
  return render(request, 'photo_objects/delete.html', {
130
146
  "title": f"Delete album",
@@ -0,0 +1,117 @@
1
+ from dataclasses import dataclass
2
+
3
+ from django.http import HttpRequest
4
+ from django.shortcuts import render
5
+ from django.urls import reverse
6
+ from django.utils.translation import gettext_lazy as _
7
+
8
+ from photo_objects.django.models import Album
9
+ from photo_objects.django.views.utils import BackLink
10
+
11
+ from .utils import json_problem_as_html
12
+
13
+
14
+ @dataclass
15
+ class Validation:
16
+ check: str
17
+ status: str
18
+ detail: str = None
19
+
20
+
21
+ def status(b: bool) -> str:
22
+ return _("OK") if b else _("Error")
23
+
24
+
25
+ def uses_https(request: HttpRequest) -> Validation:
26
+ return Validation(
27
+ check=_("Site is served over HTTPS"),
28
+ status=status(request.is_secure()),
29
+ )
30
+
31
+
32
+ def site_is_configured(request: HttpRequest) -> Validation:
33
+ detail = None
34
+ try:
35
+ ok = request.site.domain != "example.com"
36
+ if not ok:
37
+ detail = (
38
+ f'Site domain is set to "example.com". This is a placeholder '
39
+ 'domain and should be changed to the actual domain of the '
40
+ 'site.')
41
+ except Exception as e:
42
+ ok = False
43
+ detail = (
44
+ f"Failed to resolve site domain: {str(e)}. Check that sites "
45
+ "framework is installed, site middleware is configured, and that "
46
+ "the site exists in the database.")
47
+
48
+ return Validation(
49
+ check=_("Site is configured"),
50
+ status=status(ok),
51
+ detail=detail,
52
+ )
53
+
54
+
55
+ def domain_matches_request(request: HttpRequest) -> Validation:
56
+ detail = None
57
+ try:
58
+ host = request.get_host().lower()
59
+ domain = request.site.domain.lower()
60
+ ok = request.get_host() == request.site.domain
61
+ if not ok:
62
+ detail = (
63
+ f'Host in the request does not match domain configured for '
64
+ 'the site: expected "{domain}", got "{host}".')
65
+ except Exception as e:
66
+ ok = False
67
+ detail = (
68
+ f"Failed to resolve host or domain: {str(e)}. Check that "
69
+ "sites framework is installed, site middleware is configured, "
70
+ "and that the site exists in the database.")
71
+
72
+ return Validation(
73
+ check=_("Configured domain matches host in request"),
74
+ status=status(ok),
75
+ detail=detail,
76
+ )
77
+
78
+
79
+ def site_preview_configured(request: HttpRequest) -> Validation:
80
+ detail = None
81
+
82
+ try:
83
+ site_id = request.site.id
84
+ album_key = f"_site_{site_id}"
85
+ album = Album.objects.get(key=album_key)
86
+ ok = album.cover_photo is not None
87
+ if not ok:
88
+ detail = (
89
+ f'Set cover photo for "{album_key}" album to configure '
90
+ 'the preview image.')
91
+ except Exception as e:
92
+ ok = False
93
+ detail = f'Failed to resolve site or album: {str(e)}'
94
+
95
+ return Validation(
96
+ check=_("Site has a default preview image"),
97
+ status=status(ok),
98
+ detail=detail,
99
+ )
100
+
101
+
102
+ @json_problem_as_html
103
+ def configuration(request: HttpRequest):
104
+ validations = [
105
+ uses_https(request),
106
+ site_is_configured(request),
107
+ domain_matches_request(request),
108
+ site_preview_configured(request),
109
+ ]
110
+
111
+ back = BackLink("Back to albums", reverse('photo_objects:list_albums'))
112
+
113
+ return render(request, "photo_objects/configuration.html", {
114
+ "title": "Configuration",
115
+ "validations": validations,
116
+ "back": back,
117
+ })
@@ -1,10 +1,12 @@
1
1
  from django.http import HttpRequest, HttpResponseRedirect
2
2
  from django.shortcuts import render
3
3
  from django.urls import reverse
4
+ from django.utils.safestring import mark_safe
4
5
 
5
6
  from photo_objects.django import api
6
7
  from photo_objects.django.api.utils import AlbumNotFound, FormValidationFailed
7
8
  from photo_objects.django.forms import ModifyPhotoForm, UploadPhotosForm
9
+ from photo_objects.django.models import Photo
8
10
  from photo_objects.django.views.utils import BackLink, render_markdown
9
11
 
10
12
  from .utils import json_problem_as_html
@@ -39,6 +41,41 @@ def upload_photos(request: HttpRequest, album_key: str):
39
41
  })
40
42
 
41
43
 
44
+ def _camera_setup(photo: Photo):
45
+ r = []
46
+ if photo.camera_make or photo.camera_model:
47
+ r.append(
48
+ " ".join(i for i in [
49
+ photo.camera_make,
50
+ photo.camera_model,
51
+ ] if i))
52
+ if photo.lens_make or photo.lens_model:
53
+ r.append(" ".join(i for i in [photo.lens_make, photo.lens_model] if i))
54
+ return r
55
+
56
+
57
+ def _exposure_time_to_string(exposure_time: float | None):
58
+ if exposure_time is None:
59
+ return None
60
+ if exposure_time < 1:
61
+ return f"1/{int(1 / exposure_time)}\u202Fs"
62
+ else:
63
+ return f"{int(exposure_time)}\u202Fs"
64
+
65
+
66
+ def _camera_settings(photo: Photo):
67
+ r = []
68
+ if photo.focal_length:
69
+ r.append(f"{round(photo.focal_length)}\u202Fmm")
70
+ if photo.f_number:
71
+ r.append(f"f/{photo.f_number}")
72
+ if photo.exposure_time:
73
+ r.append(_exposure_time_to_string(photo.exposure_time))
74
+ if photo.iso_speed:
75
+ r.append(f"ISO\u202F{photo.iso_speed}")
76
+ return r
77
+
78
+
42
79
  @json_problem_as_html
43
80
  def show_photo(request: HttpRequest, album_key: str, photo_key: str):
44
81
  photo = api.check_photo_access(request, album_key, photo_key, "lg")
@@ -70,6 +107,8 @@ def show_photo(request: HttpRequest, album_key: str, photo_key: str):
70
107
  details = {
71
108
  "Description": render_markdown(photo.description),
72
109
  "Timestamp": photo.timestamp,
110
+ "Camera": _camera_setup(photo),
111
+ "Settings": _camera_settings(photo),
73
112
  }
74
113
 
75
114
  return render(request, "photo_objects/photo/show.html", {
photo_objects/img.py CHANGED
@@ -17,13 +17,24 @@ def utcnow():
17
17
  return datetime.now(UTC)
18
18
 
19
19
 
20
+ class ExifReader:
21
+ def __init__(self, image: Image):
22
+ self.image = image
23
+ self._data = [
24
+ image.getexif(),
25
+ image.getexif().get_ifd(ExifTags.IFD.Exif),
26
+ ]
27
+
28
+ def get(self, key):
29
+ for d in self._data:
30
+ value = d.get(key)
31
+ if value is not None:
32
+ return value
33
+
34
+
20
35
  def _read_original_datetime(image: Image) -> datetime:
21
36
  try:
22
- for key, value in ExifTags.TAGS.items():
23
- if value == "ExifOffset":
24
- break
25
-
26
- info = image.getexif().get_ifd(key)
37
+ info = ExifReader(image)
27
38
 
28
39
  time = info.get(ExifTags.Base.DateTimeOriginal)
29
40
  subsec = info.get(ExifTags.Base.SubsecTimeOriginal) or "0"
@@ -36,6 +47,25 @@ def _read_original_datetime(image: Image) -> datetime:
36
47
  return None
37
48
 
38
49
 
50
+ def _read_camera_setup_and_settings(image: Image) -> dict:
51
+ try:
52
+ info = ExifReader(image)
53
+
54
+ return dict(
55
+ camera_make=info.get(ExifTags.Base.Make),
56
+ camera_model=info.get(ExifTags.Base.Model),
57
+ lens_make=info.get(ExifTags.Base.LensMake),
58
+ lens_model=info.get(ExifTags.Base.LensModel),
59
+ focal_length=info.get(ExifTags.Base.FocalLength),
60
+ f_number=info.get(ExifTags.Base.FNumber),
61
+ exposure_time=info.get(ExifTags.Base.ExposureTime),
62
+ iso_speed=info.get(ExifTags.Base.ISOSpeedRatings),
63
+ )
64
+ except Exception as e:
65
+ raise e
66
+ return dict()
67
+
68
+
39
69
  def _image_format(filename):
40
70
  image_format = filename.split('.')[-1].upper()
41
71
 
@@ -50,6 +80,7 @@ def photo_details(photo_file):
50
80
 
51
81
  width, height = image.size
52
82
  timestamp = _read_original_datetime(image) or utcnow()
83
+ camera_setup_and_settings = _read_camera_setup_and_settings(image)
53
84
 
54
85
  # TODO: remove all extra data from the image
55
86
  resized = image.resize((3, 3))
@@ -61,7 +92,8 @@ def photo_details(photo_file):
61
92
  timestamp=timestamp,
62
93
  width=width,
63
94
  height=height,
64
- tiny_base64=b64encode(b.getvalue()).decode('ascii')
95
+ tiny_base64=b64encode(b.getvalue()).decode('ascii'),
96
+ **camera_setup_and_settings,
65
97
  )
66
98
 
67
99
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: photo-objects
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: Application for storing photos in S3 compatible object-storage.
5
5
  Author: Toni Kangas
6
6
  License: MIT License
@@ -1,35 +1,37 @@
1
1
  photo_objects/__init__.py,sha256=I1508w_ntomEqTFQgC74SurhxVXfCiDWZLRsny2f59g,60
2
2
  photo_objects/config.py,sha256=e9uSuxytkEUR_s0z_WLy5yNWN6fSCdDLq8aw2k5250w,743
3
3
  photo_objects/error.py,sha256=7afLYjxM0EaYioxVw_XUqHTvfSMSuQPUwwle0OVlaDY,45
4
- photo_objects/img.py,sha256=1-z2l4pmv4nw9E87FQPL51IXQYb3MY4mdyxVOW3C9XI,2429
4
+ photo_objects/img.py,sha256=HzM3rCE494TCgaxUN9TwzwudJAFVu9TPj_ebEOyUXpk,3435
5
5
  photo_objects/django/__init__.py,sha256=be66-iYaaJljPXmRhnYKtZe9AA71Aa-S-s0cgx3NwyQ,146
6
6
  photo_objects/django/admin.py,sha256=sRnKTODk-8s6TAfwvsa2YAd7ECIfVmoOTR6PDASsvGg,122
7
7
  photo_objects/django/apps.py,sha256=Z5-cqA0h-axZfj51kbz50YI-B9ubQsX_EjF4fW3n3FM,1072
8
8
  photo_objects/django/context_processors.py,sha256=DLhLZHAvGlrpxYegaLiKEx9X7rpyZUFiAnAVF9jjkFA,165
9
- photo_objects/django/forms.py,sha256=Qsye0zAum-Tk5sFccw0WEIkm_MAGE52-ztc75Jtfs0k,5836
10
- photo_objects/django/models.py,sha256=zFb1N1m1bczrcsuj0JugeVpb8O5m9anTbCauOVFCRhY,3821
9
+ photo_objects/django/forms.py,sha256=VoDlyZAwiTLyNxW3rRk5bzjfPJvNuKV-wpuLGCe5TCY,6505
10
+ photo_objects/django/models.py,sha256=tRHBqswASWMnyz8rwiKsmHDqEvQX54ly9k8tvU7qNOM,4597
11
11
  photo_objects/django/objsto.py,sha256=GZuoPZ1I5qX4rwEYZlOfZZiV4NQ19iknQBT6755421Y,2417
12
12
  photo_objects/django/signals.py,sha256=Q_Swjl_9z6B6zP-97D_ep5zGSAEgmQfwUz0utMDY93A,1624
13
- photo_objects/django/urls.py,sha256=CIOui0dMOOA9mxc3zrQVzADSJgS8MIN8iJ3dcFN9gmo,2258
13
+ photo_objects/django/urls.py,sha256=safcovG-8G60F_1v31ZvCCtBE8d6sHUtma6d5ZQHymo,2356
14
14
  photo_objects/django/api/__init__.py,sha256=BnEHlm3mwyBy-1xhk-NFasgZa4fjCDjtfkBUoH0puPY,62
15
- photo_objects/django/api/album.py,sha256=kpmE5bazup5mwYMZaTphZXH4Iovh6LOoCsR-TjWWKUg,1797
15
+ photo_objects/django/api/album.py,sha256=ZH8WfEoZ52L_ucMvCaccXf1lU7IrSgtub2sWEMrMpQs,1993
16
16
  photo_objects/django/api/auth.py,sha256=LfMm02_TKwrP5XFie-KzBnW3FNFWQAafBlu0UE19Gd4,1370
17
17
  photo_objects/django/api/photo.py,sha256=NGCg_Qd4X9NAd7t6lqByK9JGsoTq8HKkyEr-HwMCimI,3838
18
18
  photo_objects/django/api/utils.py,sha256=DgAsbvLLZxck_D6AXLsvUXJ3OguLufZ8sadmsToenJ4,5245
19
19
  photo_objects/django/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  photo_objects/django/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- photo_objects/django/management/commands/create-initial-admin-account.py,sha256=lq2xPAiK_RBzUbQk9YTNv6jOVsZIYHpksrBkuJiragA,1124
21
+ photo_objects/django/management/commands/create-initial-admin-account.py,sha256=M4qv1d_qFSGy_lDcKbyKOmfGsQt8i16Zoit9cJ6PwAU,1099
22
+ photo_objects/django/management/commands/create-site-albums.py,sha256=t0g1hKVFHoZJoJayGOZUqJaR_z-Bdyt9Ut4ETpOaRjI,1036
22
23
  photo_objects/django/migrations/0001_initial.py,sha256=BLW-EZ38sBgDhOYyprc-h_vuPpRxA11qxt4ZuYNO1Wo,2424
23
24
  photo_objects/django/migrations/0002_created_at_updated_at.py,sha256=7OT2VvDffAkX9XKBHVY-jvzxeIl2yU0Jr1ByCNGcUfw,1039
24
25
  photo_objects/django/migrations/0003_admin_visibility.py,sha256=PdxPOJzr-ViRBlOYUHEEGhe0hLtDysZJdMqvbjKVpEg,529
26
+ photo_objects/django/migrations/0004_camera_setup_and_settings.py,sha256=CS5xyIHgBE2Y7-PSJ52ffRQeCzs8p899px9upomk4O8,1844
25
27
  photo_objects/django/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
28
  photo_objects/django/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
- photo_objects/django/templatetags/photo_objects_extras.py,sha256=Yy6q_jMMXL0eCw28TrtBkj22A2jTRhbqqL84wv17s_E,497
29
+ photo_objects/django/templatetags/photo_objects_extras.py,sha256=LjqvVZpcVqdEQQ-EgC7OkEv2DqIvmfmDC-EsQNmkY7M,1205
28
30
  photo_objects/django/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
- photo_objects/django/tests/test_album.py,sha256=2tlINXfF9Kn7sd0s8R5MvNb0BeXxr4QPa5u9H6B8hPo,13320
31
+ photo_objects/django/tests/test_album.py,sha256=eforhWjaL3ZD7aENrjzbVhXHLI9WpY8pGZRXkgF7yNw,13333
30
32
  photo_objects/django/tests/test_auth.py,sha256=su00EypNzGQcvxqhmOtFevZriK31pY7yD1HxpQFcUT4,3928
31
- photo_objects/django/tests/test_photo.py,sha256=44ZYYcDLXwZvRI0APdFJH2DmHK255qgIabxN9XCIqN8,12916
32
- photo_objects/django/tests/test_utils.py,sha256=PGQKryi9K2zSwwG_0cNznpWzdbpt7JZKdeG1gkAziyE,1043
33
+ photo_objects/django/tests/test_photo.py,sha256=SpQsVynzJLP2F9qYrvZMzCBSiHvsRDpOZ8-W_07K_q8,13386
34
+ photo_objects/django/tests/test_utils.py,sha256=DkhzDtZqu5OXg-IKzqHz2GY0sFS8RbwYC3w81RuPxS4,1259
33
35
  photo_objects/django/tests/utils.py,sha256=9GSUw78XRfXshbQrWkxZzXUh9YjoU4clQ5Sp2keGJ1A,2467
34
36
  photo_objects/django/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
37
  photo_objects/django/views/utils.py,sha256=369CUBWwm6LGh7wX1QVqIPE_K_6nb7ePEgHWvV2sKOw,245
@@ -38,12 +40,13 @@ photo_objects/django/views/api/album.py,sha256=EZMOkxYzLSWr9wwXnd4yAO64JtXZq2k3F
38
40
  photo_objects/django/views/api/auth.py,sha256=N53csbthH0DEMFjY3fqhtLmtdL7xqzkatktywjqZ8h0,819
39
41
  photo_objects/django/views/api/photo.py,sha256=52UMP1r6TN_xfu8fHE25yOUiTa6W1F2XbgEichM8tTE,3526
40
42
  photo_objects/django/views/api/utils.py,sha256=uQzKdSKHRAux5OZzqgWQr0gsK_FeweQP0cg_67OWA_Y,264
41
- photo_objects/django/views/ui/__init__.py,sha256=83-fJvydo46-XgzF3_FMWfXtQSBsbU37C7o_Jx3JTgo,42
42
- photo_objects/django/views/ui/album.py,sha256=duLAPiLDhIkyYem12vpV9G-ORGldg3suvzgtL-KzFXA,4332
43
- photo_objects/django/views/ui/photo.py,sha256=tgg0hczISk777bksMTqi5SUhWvC3WS2DO-UmvQj2AXc,4848
43
+ photo_objects/django/views/ui/__init__.py,sha256=vbFXPj5liue-M_CXQGP5PKhD98Z5ev53yKLPU8zWpUs,71
44
+ photo_objects/django/views/ui/album.py,sha256=bgFPg9ipkQFqZK7cQgYT9gqpUgdSJ_1NTHlszsfclKU,5010
45
+ photo_objects/django/views/ui/configuration.py,sha256=6u2qea2XhAE7eNRej9OA65UHZiRQN57G0re1T-BglAo,3375
46
+ photo_objects/django/views/ui/photo.py,sha256=Xct76YoO3fUmqsQ7sY43JO_gaw2NIld16QNvAOfYGJM,6018
44
47
  photo_objects/django/views/ui/utils.py,sha256=YV_YcUbX-zUkdFnBlezPChR6aPDhZJ9loSOHBSzF6Cc,273
45
- photo_objects-0.0.2.dist-info/licenses/LICENSE,sha256=V3w6hTjXfP65F4r_mejveHcV5igHrblxao3-2RlfVlA,1068
46
- photo_objects-0.0.2.dist-info/METADATA,sha256=jWH7P8FK2WclIRK3C4NdoAFwXXq9KXt_OdKgL5dRDoQ,3671
47
- photo_objects-0.0.2.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
48
- photo_objects-0.0.2.dist-info/top_level.txt,sha256=SZeL8mhf-WMGdhRtTGFvZc3aIRBboQls9O0cFDMGdQ0,14
49
- photo_objects-0.0.2.dist-info/RECORD,,
48
+ photo_objects-0.0.3.dist-info/licenses/LICENSE,sha256=V3w6hTjXfP65F4r_mejveHcV5igHrblxao3-2RlfVlA,1068
49
+ photo_objects-0.0.3.dist-info/METADATA,sha256=nWl7HVknzOYcmEvyhwjkotBueiSZZoYS3hz5K1TxG7E,3671
50
+ photo_objects-0.0.3.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
51
+ photo_objects-0.0.3.dist-info/top_level.txt,sha256=SZeL8mhf-WMGdhRtTGFvZc3aIRBboQls9O0cFDMGdQ0,14
52
+ photo_objects-0.0.3.dist-info/RECORD,,