photo-objects 0.8.5__py3-none-any.whl → 0.9.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,8 +1,14 @@
1
1
  from django.contrib import admin
2
2
 
3
- from .models import Album, Photo, PhotoChangeRequest, SiteSettings
3
+ from .models import Album, Backup, Photo, PhotoChangeRequest, SiteSettings
4
+
5
+
6
+ class BackupAdmin(admin.ModelAdmin):
7
+ readonly_fields = ["status"]
8
+
4
9
 
5
10
  admin.site.register(Album)
6
11
  admin.site.register(Photo)
7
12
  admin.site.register(SiteSettings)
8
13
  admin.site.register(PhotoChangeRequest)
14
+ admin.site.register(Backup, BackupAdmin)
@@ -1,4 +1,5 @@
1
1
  from .album import *
2
2
  from .auth import *
3
+ from .backup import *
3
4
  from .photo import *
4
5
  from .photo_change_request import *
@@ -0,0 +1,162 @@
1
+ from django.contrib.auth import get_user_model
2
+ from django.contrib.auth.models import Group, Permission
3
+ from django.db import transaction
4
+
5
+ from photo_objects.django.models import Album, Backup, Photo
6
+ from photo_objects.django.objsto import (
7
+ backup_data_key,
8
+ backup_info_key,
9
+ delete_backup_objects,
10
+ get_backup_data,
11
+ get_backup_objects,
12
+ put_backup_json,
13
+ )
14
+ from photo_objects.utils import timestamp_str
15
+
16
+
17
+ def _permission_dict(permission: Permission):
18
+ return {
19
+ "app_label": permission.content_type.app_label,
20
+ "codename": permission.codename,
21
+ }
22
+
23
+
24
+ def _get_permissions(permissions=None):
25
+ for permission in (permissions or []):
26
+ yield Permission.objects.get(
27
+ content_type__app_label=permission.get("app_label"),
28
+ codename=permission.get("codename"))
29
+
30
+
31
+ def _user_dict(user):
32
+ return {
33
+ "username": user.username,
34
+ "first_name": user.first_name,
35
+ "last_name": user.last_name,
36
+ "email": user.email,
37
+ "groups": [
38
+ i.name for i in user.groups.all()],
39
+ "user_permissions": [
40
+ _permission_dict(i) for i in user.user_permissions.all()],
41
+ "is_staff": user.is_staff,
42
+ "is_active": user.is_active,
43
+ "is_superuser": user.is_superuser,
44
+ "last_login": timestamp_str(
45
+ user.last_login),
46
+ "date_joined": timestamp_str(
47
+ user.date_joined)}
48
+
49
+
50
+ def _group_dict(group: Group):
51
+ return {
52
+ "name": group.name,
53
+ "permissions": [_permission_dict(i) for i in group.permissions.all()],
54
+ }
55
+
56
+
57
+ def create_backup(backup: Backup):
58
+ user_model = get_user_model()
59
+
60
+ backup.status = "pending"
61
+ backup.save()
62
+
63
+ try:
64
+ albums = Album.objects.all()
65
+ for album in albums:
66
+ album_dict = album.to_json()
67
+ photos = []
68
+ for photo in album.photo_set.all():
69
+ photos.append(photo.to_json())
70
+ album_dict["photos"] = photos
71
+
72
+ put_backup_json(
73
+ backup_data_key(
74
+ backup.id,
75
+ 'album',
76
+ album.key),
77
+ album_dict,
78
+ )
79
+
80
+ groups = Group.objects.all()
81
+ for group in groups:
82
+ put_backup_json(
83
+ backup_data_key(
84
+ backup.id,
85
+ 'group',
86
+ group.name),
87
+ _group_dict(group),
88
+ )
89
+
90
+ users = user_model.objects.all()
91
+ for user in users:
92
+ if user.username == "admin":
93
+ continue
94
+
95
+ put_backup_json(
96
+ backup_data_key(
97
+ backup.id,
98
+ 'user',
99
+ user.username),
100
+ _user_dict(user))
101
+
102
+ put_backup_json(backup_info_key(backup.id), backup.to_json())
103
+ except Exception:
104
+ backup.status = "create_failed"
105
+ backup.save()
106
+
107
+ backup.status = "ready"
108
+ backup.save()
109
+
110
+
111
+ def delete_backup(backup: Backup):
112
+ return delete_backup_objects(backup.id)
113
+
114
+
115
+ def get_backups() -> list[dict]:
116
+ return get_backup_objects()
117
+
118
+
119
+ @transaction.atomic
120
+ def restore_backup(backup_id: int):
121
+ user_model = get_user_model()
122
+
123
+ for i in get_backups():
124
+ Backup.objects.create(**i)
125
+
126
+ for i in get_backup_data(backup_id, "group"):
127
+ group = Group.objects.create(name=i.get('name'))
128
+
129
+ for permission in _get_permissions(i.get('permissions')):
130
+ group.permissions.add(permission)
131
+
132
+ group.save()
133
+
134
+ for i in get_backup_data(backup_id, "user"):
135
+ permissions = i.pop("user_permissions")
136
+ groups = i.pop("groups")
137
+
138
+ user = user_model.objects.create(**i)
139
+
140
+ for group in groups:
141
+ user.groups.add(Group.objects.get(name=group))
142
+
143
+ for permission in _get_permissions(permissions):
144
+ user.user_permissions.add(permission)
145
+
146
+ user.save()
147
+
148
+ for i in get_backup_data(backup_id, "album"):
149
+ photos = i.pop("photos")
150
+ cover_photo = i.pop("cover_photo")
151
+
152
+ album = Album.objects.create(**i)
153
+
154
+ for photo in photos:
155
+ photo.pop("album", None)
156
+ filename = photo.pop("filename")
157
+
158
+ photo = Photo.objects.create(**photo, album=album)
159
+
160
+ if cover_photo == filename:
161
+ album.cover_photo = photo
162
+ album.save()
@@ -1,6 +1,4 @@
1
1
  import random
2
- import re
3
- import unicodedata
4
2
 
5
3
  from django import forms
6
4
  from django.forms import (
@@ -16,6 +14,8 @@ from django.forms import (
16
14
  from django.utils.safestring import mark_safe
17
15
  from django.utils.translation import gettext_lazy as _
18
16
 
17
+ from photo_objects.utils import slugify
18
+
19
19
  from .models import Album, Photo, PhotoChangeRequest
20
20
 
21
21
 
@@ -26,22 +26,6 @@ KEY_POSTFIX_LEN = 5
26
26
  ALT_TEXT_HELP = _('Alternative text content for the photo.')
27
27
 
28
28
 
29
- def slugify(title: str, lower=False, replace_leading_underscores=False) -> str:
30
- key = unicodedata.normalize(
31
- 'NFKD', title).encode(
32
- 'ascii', 'ignore').decode('ascii')
33
- if lower:
34
- key = key.lower()
35
-
36
- key = re.sub(r'[^a-zA-Z0-9._-]', '-', key)
37
- key = re.sub(r'[-_]{2,}', '-', key)
38
-
39
- if replace_leading_underscores:
40
- key = re.sub(r'^_+', '-', key)
41
-
42
- return key
43
-
44
-
45
29
  def _postfix_generator():
46
30
  for _ in range(13):
47
31
  yield '-' + ''.join(
@@ -12,7 +12,9 @@ class Command(BaseCommand):
12
12
 
13
13
  def handle(self, *args, **options):
14
14
  user = get_user_model()
15
- superuser_count = user.objects.filter(is_superuser=True).count()
15
+ superuser_count = user.objects.filter(
16
+ is_superuser=True).exclude(
17
+ password="").count()
16
18
 
17
19
  if superuser_count == 0:
18
20
  username = 'admin'
@@ -0,0 +1,74 @@
1
+ # pylint: disable=invalid-name
2
+ from django.core.management.base import BaseCommand
3
+ from django.contrib.auth import get_user_model
4
+ from django.contrib.auth.models import Group
5
+
6
+ from photo_objects.django.api.backup import get_backups, restore_backup
7
+ from photo_objects.django.models import Album, Photo
8
+
9
+
10
+ class DatabaseStatus:
11
+ def __init__(self):
12
+ user = get_user_model()
13
+
14
+ self._users = user.objects.count()
15
+ self._groups = Group.objects.count()
16
+ self._albums = Album.objects.count()
17
+ self._photos = Photo.objects.count()
18
+
19
+ def should_restore(self):
20
+ count = self._users + self._groups + self._albums + self._photos
21
+ return count == 0
22
+
23
+ def __str__(self):
24
+ return (
25
+ f"Database contains {self._users} users, "
26
+ f"{self._groups} groups, {self._albums} albums, and "
27
+ f"{self._photos} photos"
28
+ )
29
+
30
+
31
+ class Command(BaseCommand):
32
+ help = "Restore latest backup if database is empty."
33
+
34
+ def handle(self, *args, **options):
35
+ status = DatabaseStatus()
36
+ if not status.should_restore():
37
+ self.stdout.write(
38
+ self.style.NOTICE(
39
+ f'Restoring backup skipped: {status}'
40
+ )
41
+ )
42
+ return
43
+
44
+ backups = get_backups()
45
+
46
+ if not backups:
47
+ self.stdout.write(
48
+ self.style.NOTICE(
49
+ 'Restoring backup skipped: No backups found.'
50
+ )
51
+ )
52
+ return
53
+
54
+ try:
55
+ id_ = backups[-1].get("id")
56
+ self.stdout.write(
57
+ self.style.NOTICE(
58
+ f'Restoring backup with ID {id_}.'
59
+ )
60
+ )
61
+ restore_backup(id_)
62
+ status = DatabaseStatus()
63
+ self.stdout.write(
64
+ self.style.SUCCESS(
65
+ f'Restored backup with ID {id_}: {status}'
66
+ )
67
+ )
68
+ except Exception as e:
69
+ self.stdout.write(
70
+ self.style.ERROR(
71
+ f'Failed to restore backup: {e}'
72
+ )
73
+ )
74
+ exit(1)
@@ -0,0 +1,22 @@
1
+ # Generated by Django 5.2.6 on 2025-10-26 23:26
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('photo_objects', '0006_photo_alt_text'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.CreateModel(
14
+ name='Backup',
15
+ fields=[
16
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17
+ ('created_at', models.DateTimeField(auto_now_add=True)),
18
+ ('comment', models.TextField(blank=True)),
19
+ ('status', models.TextField(blank=True)),
20
+ ],
21
+ ),
22
+ ]
@@ -4,7 +4,7 @@ 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
7
+ from photo_objects.utils import first_paragraph_textcontent, timestamp_str
8
8
 
9
9
 
10
10
  album_key_validator = RegexValidator(
@@ -23,10 +23,6 @@ def _str(key, **kwargs):
23
23
  return f'{key} ({details})' if details else key
24
24
 
25
25
 
26
- def _timestamp_str(timestamp):
27
- return timestamp.isoformat() if timestamp else None
28
-
29
-
30
26
  class BaseModel(models.Model):
31
27
  title = models.CharField(blank=True)
32
28
  description = models.TextField(blank=True)
@@ -41,8 +37,8 @@ class BaseModel(models.Model):
41
37
  return dict(
42
38
  title=self.title,
43
39
  description=self.description,
44
- created_at=_timestamp_str(self.created_at),
45
- updated_at=_timestamp_str(self.updated_at),
40
+ created_at=timestamp_str(self.created_at),
41
+ updated_at=timestamp_str(self.updated_at),
46
42
  )
47
43
 
48
44
 
@@ -82,8 +78,8 @@ class Album(BaseModel):
82
78
  visibility=self.visibility,
83
79
  cover_photo=(
84
80
  self.cover_photo.filename if self.cover_photo else None),
85
- first_timestamp=_timestamp_str(self.first_timestamp),
86
- last_timestamp=_timestamp_str(self.last_timestamp),
81
+ first_timestamp=timestamp_str(self.first_timestamp),
82
+ last_timestamp=timestamp_str(self.last_timestamp),
87
83
  )
88
84
 
89
85
 
@@ -190,7 +186,7 @@ class PhotoChangeRequest(models.Model):
190
186
  return dict(
191
187
  id=self.id,
192
188
  photo=self.photo.key,
193
- created_at=_timestamp_str(self.created_at),
189
+ created_at=timestamp_str(self.created_at),
194
190
  alt_text=self.alt_text,
195
191
  )
196
192
 
@@ -233,3 +229,23 @@ def clear_cached_settings(sender, **kwargs):
233
229
 
234
230
  pre_save.connect(clear_cached_settings, sender=SiteSettings)
235
231
  pre_delete.connect(clear_cached_settings, sender=SiteSettings)
232
+
233
+
234
+ class Backup(models.Model):
235
+ created_at = models.DateTimeField(auto_now_add=True)
236
+ comment = models.TextField(blank=True)
237
+ status = models.TextField(blank=True)
238
+
239
+ def __str__(self):
240
+ return _str(
241
+ f'Backup {self.id}',
242
+ created_at=self.created_at,
243
+ comment=self.comment,
244
+ )
245
+
246
+ def to_json(self):
247
+ return dict(
248
+ id=self.id,
249
+ created_at=timestamp_str(self.created_at),
250
+ comment=self.comment,
251
+ )
@@ -13,6 +13,7 @@ from photo_objects.django.conf import (
13
13
  objsto_settings,
14
14
  parse_photo_sizes,
15
15
  )
16
+ from photo_objects.utils import slugify
16
17
 
17
18
 
18
19
  MEGABYTE = 1 << 20
@@ -33,20 +34,35 @@ def _anonymous_readonly_policy(bucket: str):
33
34
  return json.dumps(policy)
34
35
 
35
36
 
36
- def _objsto_access() -> tuple[Minio, str]:
37
+ def _objsto_access() -> tuple[dict, Minio]:
37
38
  conf = objsto_settings()
38
39
  http = urllib3.PoolManager(
39
40
  retries=urllib3.util.Retry(connect=1),
40
41
  timeout=urllib3.util.Timeout(connect=2.5, read=20),
41
42
  )
42
43
 
43
- client = Minio(
44
+ return (conf, Minio(
44
45
  conf.get('URL'),
45
46
  conf.get('ACCESS_KEY'),
46
47
  conf.get('SECRET_KEY'),
47
48
  http_client=http,
48
49
  secure=conf.get('SECURE', True),
49
- )
50
+ ))
51
+
52
+
53
+ def _backup_access() -> tuple[Minio, str]:
54
+ conf, client = _objsto_access()
55
+ bucket = conf.get('BACKUP_BUCKET', 'backups')
56
+
57
+ # TODO: move this to management command
58
+ if not client.bucket_exists(bucket):
59
+ client.make_bucket(bucket)
60
+
61
+ return client, bucket
62
+
63
+
64
+ def _photos_access() -> tuple[Minio, str]:
65
+ conf, client = _objsto_access()
50
66
  bucket = conf.get('BUCKET', 'photos')
51
67
 
52
68
  # TODO: move this to management command
@@ -57,6 +73,88 @@ def _objsto_access() -> tuple[Minio, str]:
57
73
  return client, bucket
58
74
 
59
75
 
76
+ def _put_json(key, data, access_fn):
77
+ data_str = json.dumps(data)
78
+ stream = BytesIO(data_str.encode('utf-8'))
79
+
80
+ client, bucket = access_fn()
81
+ client.put_object(
82
+ bucket,
83
+ key,
84
+ stream,
85
+ length=-1,
86
+ part_size=10 * MEGABYTE,
87
+ content_type="application/json",
88
+ )
89
+
90
+
91
+ def _list_all(client: Minio, bucket: str, prefix: str):
92
+ start_after = None
93
+ while True:
94
+ objects = client.list_objects(
95
+ bucket,
96
+ prefix=prefix,
97
+ recursive=True,
98
+ start_after=start_after)
99
+
100
+ if not objects:
101
+ break
102
+
103
+ empty = True
104
+ for i in objects:
105
+ empty = False
106
+ yield i
107
+ start_after = i.object_name
108
+
109
+ if empty:
110
+ break
111
+
112
+
113
+ def _get_all(client: Minio, bucket: str, prefix: str):
114
+ for i in _list_all(client, bucket, prefix):
115
+ yield client.get_object(bucket, i.object_name)
116
+
117
+
118
+ def _delete_all(client: Minio, bucket: str, prefix: str):
119
+ for i in _list_all(client, bucket, prefix):
120
+ client.remove_object(bucket, i.object_name)
121
+ yield i.object_name
122
+
123
+
124
+ def backup_info_key(id_):
125
+ return f'info_{id_}.json'
126
+
127
+
128
+ def backup_data_key(id_, type_, key):
129
+ return f'data_{id_}/{type_}_{slugify(key)}.json'
130
+
131
+
132
+ def backup_data_prefix(id_, type_=None):
133
+ return f'data_{id_}/{type_ or ""}'
134
+
135
+
136
+ def put_backup_json(key: str, data: dict):
137
+ return _put_json(key, data, _backup_access)
138
+
139
+
140
+ def get_backup_objects():
141
+ client, bucket = _backup_access()
142
+ return [json.loads(i.read()) for i in _get_all(client, bucket, 'info_')]
143
+
144
+
145
+ def get_backup_data(id_: int, type_=None):
146
+ client, bucket = _backup_access()
147
+ for i in _get_all(client, bucket, backup_data_prefix(id_, type_)):
148
+ yield json.loads(i.read())
149
+
150
+
151
+ def delete_backup_objects(id_: int):
152
+ client, bucket = _backup_access()
153
+ client.remove_object(bucket, backup_info_key(id_))
154
+ for _ in _delete_all(client, bucket, backup_data_prefix(id_)):
155
+ continue
156
+
157
+
60
158
  def photo_path(album_key, photo_key, size_key):
61
159
  return f"{size_key}/{album_key}/{photo_key}"
62
160
 
@@ -86,7 +184,7 @@ def photo_content_headers(
86
184
  def put_photo(album_key, photo_key, size_key, photo_file, image_format=None):
87
185
  content_type, headers = photo_content_headers(photo_key, image_format)
88
186
 
89
- client, bucket = _objsto_access()
187
+ client, bucket = _photos_access()
90
188
  return client.put_object(
91
189
  bucket,
92
190
  photo_path(album_key, photo_key, size_key),
@@ -99,7 +197,7 @@ def put_photo(album_key, photo_key, size_key, photo_file, image_format=None):
99
197
 
100
198
 
101
199
  def get_photo(album_key, photo_key, size_key):
102
- client, bucket = _objsto_access()
200
+ client, bucket = _photos_access()
103
201
  return client.get_object(
104
202
  bucket,
105
203
  photo_path(album_key, photo_key, size_key)
@@ -107,33 +205,17 @@ def get_photo(album_key, photo_key, size_key):
107
205
 
108
206
 
109
207
  def delete_photo(album_key, photo_key):
110
- client, bucket = _objsto_access()
208
+ client, bucket = _photos_access()
111
209
 
112
210
  for i in PhotoSize:
113
211
  client.remove_object(bucket, photo_path(album_key, photo_key, i.value))
114
212
 
115
213
 
116
214
  def delete_scaled_photos(sizes):
117
- client, bucket = _objsto_access()
215
+ client, bucket = _photos_access()
118
216
 
119
217
  for size in sizes:
120
- while True:
121
- objects = client.list_objects(
122
- bucket,
123
- prefix=f"{size}/",
124
- recursive=True)
125
-
126
- if not objects:
127
- break
128
-
129
- empty = True
130
- for i in objects:
131
- empty = False
132
- client.remove_object(bucket, i.object_name)
133
- yield i.object_name
134
-
135
- if empty:
136
- break
218
+ yield from _delete_all(client, bucket, f'{size}/')
137
219
 
138
220
 
139
221
  def get_error_code(e: Exception) -> str:
@@ -151,22 +233,11 @@ def with_error_code(msg: str, e: Exception) -> str:
151
233
 
152
234
 
153
235
  def put_photo_sizes(sizes: PhotoSizes):
154
- data = json.dumps(asdict(sizes))
155
- stream = BytesIO(data.encode('utf-8'))
156
-
157
- client, bucket = _objsto_access()
158
- client.put_object(
159
- bucket,
160
- "photo_sizes.json",
161
- stream,
162
- length=-1,
163
- part_size=10 * MEGABYTE,
164
- content_type="application/json",
165
- )
236
+ return _put_json("photo_sizes.json", asdict(sizes), _photos_access)
166
237
 
167
238
 
168
239
  def get_photo_sizes() -> PhotoSizes:
169
- client, bucket = _objsto_access()
240
+ client, bucket = _photos_access()
170
241
  try:
171
242
  data = client.get_object(bucket, "photo_sizes.json")
172
243
  return parse_photo_sizes(json.loads(data.read()))
@@ -1,7 +1,10 @@
1
- from django.db.models.signals import post_save, post_delete
1
+ from django.db.models.signals import post_save, post_delete, pre_delete
2
2
  from django.dispatch import receiver
3
3
 
4
- from .models import Photo
4
+ from photo_objects.django.api import create_backup
5
+ from photo_objects.django.api.backup import delete_backup
6
+
7
+ from .models import Backup, Photo
5
8
 
6
9
 
7
10
  @receiver(post_save, sender=Photo)
@@ -56,3 +59,19 @@ def update_album_on_photo_delete(sender, **kwargs):
56
59
 
57
60
  if needs_save:
58
61
  album.save()
62
+
63
+
64
+ @receiver(post_save, sender=Backup)
65
+ def create_backup_to_objsto(sender, **kwargs):
66
+ created = kwargs.get('created', False)
67
+ if not created:
68
+ return None
69
+
70
+ backup = kwargs.get('instance', None)
71
+ return create_backup(backup)
72
+
73
+
74
+ @receiver(pre_delete, sender=Backup)
75
+ def delete_backup_from_objsto(sender, **kwargs):
76
+ backup = kwargs.get('instance', None)
77
+ return delete_backup(backup)
@@ -14,7 +14,7 @@ from minio import S3Error
14
14
 
15
15
  from photo_objects.django import objsto
16
16
  from photo_objects.django.models import Album, Photo
17
- from photo_objects.django.objsto import _objsto_access
17
+ from photo_objects.django.objsto import _photos_access
18
18
 
19
19
 
20
20
  def add_permissions(user, *permissions):
@@ -56,7 +56,7 @@ class TestCase(DjangoTestCase):
56
56
  # pylint: disable=invalid-name
57
57
  @classmethod
58
58
  def tearDownClass(cls):
59
- client, bucket = _objsto_access()
59
+ client, bucket = _photos_access()
60
60
 
61
61
  for i in client.list_objects(bucket, recursive=True):
62
62
  client.remove_object(bucket, i.object_name)
photo_objects/utils.py CHANGED
@@ -1,3 +1,6 @@
1
+ from datetime import datetime
2
+ import re
3
+ import unicodedata
1
4
  from xml.etree import ElementTree as ET
2
5
 
3
6
  from django.utils.safestring import mark_safe
@@ -22,3 +25,23 @@ def first_paragraph_textcontent(raw: str) -> str | None:
22
25
  return None
23
26
 
24
27
  return ''.join(first.itertext())
28
+
29
+
30
+ def timestamp_str(timestamp: datetime):
31
+ return timestamp.isoformat() if timestamp else None
32
+
33
+
34
+ def slugify(title: str, lower=False, replace_leading_underscores=False) -> str:
35
+ key = unicodedata.normalize(
36
+ 'NFKD', title).encode(
37
+ 'ascii', 'ignore').decode('ascii')
38
+ if lower:
39
+ key = key.lower()
40
+
41
+ key = re.sub(r'[^a-zA-Z0-9._-]', '-', key)
42
+ key = re.sub(r'[-_]{2,}', '-', key)
43
+
44
+ if replace_leading_underscores:
45
+ key = re.sub(r'^_+', '-', key)
46
+
47
+ return key
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: photo-objects
3
- Version: 0.8.5
3
+ Version: 0.9.0
4
4
  Summary: Application for storing photos in S3 compatible object-storage.
5
5
  Author: Toni Kangas
6
6
  License: MIT License
@@ -2,33 +2,36 @@ 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=_93yzQ18UXa_QIC4pLCPvgcwi8uCGn9-pDT_7e4p_uw,579
5
+ photo_objects/utils.py,sha256=V0THzeLSGkgLTWdVhP-ee1nb-vl051mtF4AQqdgMo88,1145
6
6
  photo_objects/django/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- photo_objects/django/admin.py,sha256=ZrNhr-OatgIyqepaU-LDZbf4djCRQteg4yx-TS3OYy0,230
7
+ photo_objects/django/admin.py,sha256=pvyQKs2FMtKtSS2PE_NKR_jsSi7-GKz3bbsqDNgjt6w,352
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=95tdNFrvxSVWUYzy4-zrLmCaQVdEpA02umqQyfCllws,7258
12
- photo_objects/django/models.py,sha256=2qYQeyfwf32m9CNb-W1xnhU5IrT8QD5bI11HS-z7fNs,6824
13
- photo_objects/django/objsto.py,sha256=B7DxPWuqFaPFXPLhsHCFlqIzYl7EXLxcHde6zJDe89A,4238
14
- photo_objects/django/signals.py,sha256=Q_Swjl_9z6B6zP-97D_ep5zGSAEgmQfwUz0utMDY93A,1624
11
+ photo_objects/django/forms.py,sha256=zSSmdIYn4PEx1Nnw-tuo4h4otKj3C099lB4-vp2BRUc,6861
12
+ photo_objects/django/models.py,sha256=OxkkczIBg7TaFWWm4VdtbRMJOK_OQKrV29g-X2cm5BQ,7247
13
+ photo_objects/django/objsto.py,sha256=lYOd1PJzrcmqtqw-Xci85MX_9yWqzeisYx4uJjXhHdQ,6046
14
+ photo_objects/django/signals.py,sha256=_gb4vlZkeFNYWXxwhNreaUJoOsbIWvP8OovVLtzepaE,2161
15
15
  photo_objects/django/urls.py,sha256=XnOSEB8YtAJamlEsjKYz6U1DfDu7HHZpAinpqdulR8k,2501
16
- photo_objects/django/api/__init__.py,sha256=inUPmjlTAawHwco4NzjwNEVNnf9c_IIwLH5Ze1ujpuI,98
16
+ photo_objects/django/api/__init__.py,sha256=51CRTiE975ufVhvI5x-M_2D28JP8FZWyLFiuV5EaQSg,120
17
17
  photo_objects/django/api/album.py,sha256=CJDeGLCuYoxGUDcjssZRpFnToxG_KVE9Ii7NduFW2ks,2003
18
18
  photo_objects/django/api/auth.py,sha256=lS0S1tMVH2uN30g4jlixklv3eMnQ2FbQVQvuRXeMGYo,1420
19
+ photo_objects/django/api/backup.py,sha256=Lq9fTjuoZssQG86QJFIxlLpQi2FU0AS8yCZklvu_ues,4379
19
20
  photo_objects/django/api/photo.py,sha256=-lo1g6jfBr884wy-WV2DAEPxzH9V-tFUTRtitmA6i28,4471
20
21
  photo_objects/django/api/photo_change_request.py,sha256=v94RA7SUM60tC9mIZdz8HppbNKfHWeTFNPr_BPw3pys,3075
21
22
  photo_objects/django/api/utils.py,sha256=8r51YgFgKPD05Zjzhstl4jlQ4JM8DtsxUyAzhjXi5Pk,5567
22
23
  photo_objects/django/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
24
  photo_objects/django/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
25
  photo_objects/django/management/commands/clean-scaled-photos.py,sha256=KJY6phgTCxcmbMUsUfCRQjatvCmKyFninM8zT-tB3Kc,2008
25
- photo_objects/django/management/commands/create-initial-admin-account.py,sha256=SuDSEP7S7OR99NtXvjXHnIyuawNOBgMiuwoHs7aCj6g,1131
26
+ photo_objects/django/management/commands/create-initial-admin-account.py,sha256=PhC7jnRxsph8rML384vKfXzYfJqMQdd8oUHxUY7JEW8,1178
27
+ photo_objects/django/management/commands/restore-backup.py,sha256=pnlHWcEgFDlgKaslC5PdhVcm491uRylCD3ZdRwGS0B0,2157
26
28
  photo_objects/django/migrations/0001_initial.py,sha256=BLW-EZ38sBgDhOYyprc-h_vuPpRxA11qxt4ZuYNO1Wo,2424
27
29
  photo_objects/django/migrations/0002_created_at_updated_at.py,sha256=7OT2VvDffAkX9XKBHVY-jvzxeIl2yU0Jr1ByCNGcUfw,1039
28
30
  photo_objects/django/migrations/0003_admin_visibility.py,sha256=PdxPOJzr-ViRBlOYUHEEGhe0hLtDysZJdMqvbjKVpEg,529
29
31
  photo_objects/django/migrations/0004_camera_setup_and_settings.py,sha256=CS5xyIHgBE2Y7-PSJ52ffRQeCzs8p899px9upomk4O8,1844
30
32
  photo_objects/django/migrations/0005_sitesettings.py,sha256=Ilf5vUwTFQfXVP37zz0NWo5dQdeHDh5e-MV3enm0ZKI,994
31
33
  photo_objects/django/migrations/0006_photo_alt_text.py,sha256=5ZWR-bfS_72Mpb0SrvtnWzVME-zlcRPzDCofmFWkUeU,1003
34
+ photo_objects/django/migrations/0007_backup.py,sha256=tLHLAnojppT6mhFF3FokGdwDbIj40_yqO9qMFm0H3UI,649
32
35
  photo_objects/django/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
36
  photo_objects/django/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
37
  photo_objects/django/templatetags/photo_objects_extras.py,sha256=1L6wweVA96rTWVXqgyf8gSey1wQKi01h6sqv9kZeTIY,1399
@@ -41,7 +44,7 @@ photo_objects/django/tests/test_og_meta.py,sha256=Kk5a9KvE88KZ60gLqXSe6rTz5YU-gd
41
44
  photo_objects/django/tests/test_photo.py,sha256=JWXN3fF2VtuByMKm2o5b19HnxwDr6ecRwuGzgc5RsBw,13471
42
45
  photo_objects/django/tests/test_photo_change_requests.py,sha256=Ld5ytqxxZiEWrqfX8htJ6-5ARU7tqTYD-iUhb7EMcnU,3078
43
46
  photo_objects/django/tests/test_utils.py,sha256=zBLv8lkvcLMCaH4D6GR1KZqUe-rPowhcBkQX19-Kshs,2007
44
- photo_objects/django/tests/utils.py,sha256=7oAwFotAFzxIE2YFGysj_a_y-u7E7Z7_K7vl7ycUAzM,4639
47
+ photo_objects/django/tests/utils.py,sha256=LiObyRARkmO4arnY2gXNi_T8XxT9eSKKszENMo2UIh8,4639
45
48
  photo_objects/django/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
49
  photo_objects/django/views/utils.py,sha256=AjJK5r5HmTF63E9Q4W3pKggDESuhNXUvbROpS8m70KM,1319
47
50
  photo_objects/django/views/api/__init__.py,sha256=GgywMJMSFmP5aoMEaYut5V666zachd5YFQIDBMr5znU,188
@@ -56,8 +59,8 @@ photo_objects/django/views/ui/photo.py,sha256=flwcET5nSChzfyAEWRTUlklTW2o64przNX
56
59
  photo_objects/django/views/ui/photo_change_request.py,sha256=eaYGXFqtHj8qonDAmPyn4nrEHkL13EBD-1s8Phs0XP4,2098
57
60
  photo_objects/django/views/ui/users.py,sha256=nb73cnzuV98QkJb0j8F2hqPgOGFIWpUFTFu6dXMeVwM,722
58
61
  photo_objects/django/views/ui/utils.py,sha256=YV_YcUbX-zUkdFnBlezPChR6aPDhZJ9loSOHBSzF6Cc,273
59
- photo_objects-0.8.5.dist-info/licenses/LICENSE,sha256=V3w6hTjXfP65F4r_mejveHcV5igHrblxao3-2RlfVlA,1068
60
- photo_objects-0.8.5.dist-info/METADATA,sha256=UnVMH30qioih4ZjW7gQxNt98bqDeaFUS22mG0LJSk6A,3605
61
- photo_objects-0.8.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
62
- photo_objects-0.8.5.dist-info/top_level.txt,sha256=SZeL8mhf-WMGdhRtTGFvZc3aIRBboQls9O0cFDMGdQ0,14
63
- photo_objects-0.8.5.dist-info/RECORD,,
62
+ photo_objects-0.9.0.dist-info/licenses/LICENSE,sha256=V3w6hTjXfP65F4r_mejveHcV5igHrblxao3-2RlfVlA,1068
63
+ photo_objects-0.9.0.dist-info/METADATA,sha256=54uuCEzSyejSGgQ2AlWFlCjuwawJFHfLBxRE_EQ47D4,3605
64
+ photo_objects-0.9.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
65
+ photo_objects-0.9.0.dist-info/top_level.txt,sha256=SZeL8mhf-WMGdhRtTGFvZc3aIRBboQls9O0cFDMGdQ0,14
66
+ photo_objects-0.9.0.dist-info/RECORD,,