photo-objects 0.3.1__tar.gz → 0.4.1__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.
- {photo_objects-0.3.1 → photo_objects-0.4.1}/PKG-INFO +2 -2
- {photo_objects-0.3.1 → photo_objects-0.4.1}/README.md +1 -1
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/config.py +6 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/api/album.py +2 -2
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/api/auth.py +6 -6
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/api/photo.py +4 -4
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/api/utils.py +8 -12
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/apps.py +14 -2
- photo_objects-0.4.1/photo_objects/django/conf.py +105 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/forms.py +2 -2
- photo_objects-0.4.1/photo_objects/django/management/commands/clean-scaled-photos.py +64 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/management/commands/create-initial-admin-account.py +6 -4
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/management/commands/create-site-albums.py +1 -1
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/objsto.py +62 -13
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/signals.py +0 -2
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/templatetags/photo_objects_extras.py +3 -2
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/tests/test_album.py +7 -7
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/tests/test_auth.py +3 -3
- photo_objects-0.4.1/photo_objects/django/tests/test_commands.py +80 -0
- photo_objects-0.4.1/photo_objects/django/tests/test_img.py +29 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/tests/test_photo.py +4 -4
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/tests/test_utils.py +24 -3
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/tests/utils.py +35 -1
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/urls.py +1 -2
- photo_objects-0.4.1/photo_objects/django/views/__init__.py +0 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/views/api/auth.py +1 -1
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/views/api/photo.py +7 -7
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/views/ui/album.py +8 -5
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/views/ui/configuration.py +1 -1
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/views/ui/photo.py +8 -4
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/views/ui/users.py +1 -1
- photo_objects-0.4.1/photo_objects/django/views/utils.py +60 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/img.py +44 -2
- photo_objects-0.4.1/photo_objects/utils.py +3 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects.egg-info/PKG-INFO +2 -2
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects.egg-info/SOURCES.txt +5 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/pyproject.toml +1 -1
- photo_objects-0.3.1/photo_objects/django/__init__.py +0 -10
- photo_objects-0.3.1/photo_objects/django/views/utils.py +0 -12
- {photo_objects-0.3.1 → photo_objects-0.4.1}/LICENSE +0 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/__init__.py +0 -0
- {photo_objects-0.3.1/photo_objects/django/management → photo_objects-0.4.1/photo_objects/django}/__init__.py +0 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/admin.py +0 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/api/__init__.py +0 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/context_processors.py +0 -0
- {photo_objects-0.3.1/photo_objects/django/management/commands → photo_objects-0.4.1/photo_objects/django/management}/__init__.py +0 -0
- {photo_objects-0.3.1/photo_objects/django/migrations → photo_objects-0.4.1/photo_objects/django/management/commands}/__init__.py +0 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/migrations/0001_initial.py +0 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/migrations/0002_created_at_updated_at.py +0 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/migrations/0003_admin_visibility.py +0 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/migrations/0004_camera_setup_and_settings.py +0 -0
- {photo_objects-0.3.1/photo_objects/django/templatetags → photo_objects-0.4.1/photo_objects/django/migrations}/__init__.py +0 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/models.py +0 -0
- {photo_objects-0.3.1/photo_objects/django/tests → photo_objects-0.4.1/photo_objects/django/templatetags}/__init__.py +0 -0
- {photo_objects-0.3.1/photo_objects/django/views → photo_objects-0.4.1/photo_objects/django/tests}/__init__.py +0 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/views/api/__init__.py +0 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/views/api/album.py +0 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/views/api/utils.py +0 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/views/ui/__init__.py +0 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/django/views/ui/utils.py +0 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects/error.py +0 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects.egg-info/dependency_links.txt +0 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects.egg-info/requires.txt +0 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/photo_objects.egg-info/top_level.txt +0 -0
- {photo_objects-0.3.1 → photo_objects-0.4.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: photo-objects
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Application for storing photos in S3 compatible object-storage.
|
|
5
5
|
Author: Toni Kangas
|
|
6
6
|
License: MIT License
|
|
@@ -74,7 +74,7 @@ autopep8 -aaar --in-place --exclude back/api/settings.py,*/migrations/*.py back
|
|
|
74
74
|
Run static analysis with:
|
|
75
75
|
|
|
76
76
|
```sh
|
|
77
|
-
pylint
|
|
77
|
+
pylint back/api photo_objects
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
### Integration tests
|
|
@@ -26,7 +26,7 @@ autopep8 -aaar --in-place --exclude back/api/settings.py,*/migrations/*.py back
|
|
|
26
26
|
Run static analysis with:
|
|
27
27
|
|
|
28
28
|
```sh
|
|
29
|
-
pylint
|
|
29
|
+
pylint back/api photo_objects
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
### Integration tests
|
|
@@ -16,7 +16,7 @@ from .utils import (
|
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def get_site_album(site: Site) -> Album:
|
|
19
|
+
def get_site_album(site: Site) -> tuple[Album, bool]:
|
|
20
20
|
album_key = f'_site_{site.id}'
|
|
21
21
|
return Album.objects.get_or_create(
|
|
22
22
|
key=album_key,
|
|
@@ -89,4 +89,4 @@ def delete_album(request: HttpRequest, album_key: str):
|
|
|
89
89
|
f"Album with {album_key} key can not be deleted because it "
|
|
90
90
|
"contains photos.",
|
|
91
91
|
409,
|
|
92
|
-
)
|
|
92
|
+
) from None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from django.http import HttpRequest
|
|
2
2
|
|
|
3
|
-
from photo_objects.django import
|
|
3
|
+
from photo_objects.django.conf import PhotoSize
|
|
4
4
|
from photo_objects.django.models import Album, Photo
|
|
5
5
|
|
|
6
6
|
from photo_objects.django.api.utils import (
|
|
@@ -16,7 +16,7 @@ def check_album_access(request: HttpRequest, album_key: str):
|
|
|
16
16
|
try:
|
|
17
17
|
album = Album.objects.get(key=album_key)
|
|
18
18
|
except Album.DoesNotExist:
|
|
19
|
-
raise AlbumNotFound(album_key)
|
|
19
|
+
raise AlbumNotFound(album_key) from None
|
|
20
20
|
|
|
21
21
|
if not request.user.is_authenticated:
|
|
22
22
|
if album.visibility == Album.Visibility.PRIVATE:
|
|
@@ -35,19 +35,19 @@ def check_photo_access(
|
|
|
35
35
|
photo_key: str,
|
|
36
36
|
size_key: str):
|
|
37
37
|
try:
|
|
38
|
-
size =
|
|
38
|
+
size = PhotoSize(size_key)
|
|
39
39
|
except ValueError:
|
|
40
|
-
raise InvalidSize(size_key)
|
|
40
|
+
raise InvalidSize(size_key) from None
|
|
41
41
|
|
|
42
42
|
try:
|
|
43
43
|
photo = Photo.objects.get(key=join_key(album_key, photo_key))
|
|
44
44
|
except Photo.DoesNotExist:
|
|
45
|
-
raise PhotoNotFound(album_key, photo_key)
|
|
45
|
+
raise PhotoNotFound(album_key, photo_key) from None
|
|
46
46
|
|
|
47
47
|
if not request.user.is_authenticated:
|
|
48
48
|
if photo.album.visibility == Album.Visibility.PRIVATE:
|
|
49
49
|
raise AlbumNotFound(album_key)
|
|
50
|
-
if size ==
|
|
50
|
+
if size == PhotoSize.ORIGINAL:
|
|
51
51
|
raise Unauthorized()
|
|
52
52
|
|
|
53
53
|
return photo
|
|
@@ -36,7 +36,7 @@ def _upload_photo(album_key: str, photo_file: UploadedFile):
|
|
|
36
36
|
raise JsonProblem(
|
|
37
37
|
"Could not open photo file.",
|
|
38
38
|
400,
|
|
39
|
-
)
|
|
39
|
+
) from None
|
|
40
40
|
|
|
41
41
|
f = CreatePhotoForm(dict(
|
|
42
42
|
key=f"{album_key}/{slugify(photo_file.name)}",
|
|
@@ -59,7 +59,7 @@ def _upload_photo(album_key: str, photo_file: UploadedFile):
|
|
|
59
59
|
msg = objsto.with_error_code(
|
|
60
60
|
"Could not save photo to object storage", e)
|
|
61
61
|
logger.error(f"{msg}: {str(e)}")
|
|
62
|
-
raise JsonProblem(f"{msg}.", 500)
|
|
62
|
+
raise JsonProblem(f"{msg}.", 500) from e
|
|
63
63
|
|
|
64
64
|
return photo
|
|
65
65
|
|
|
@@ -125,11 +125,11 @@ def delete_photo(request: HttpRequest, album_key: str, photo_key: str):
|
|
|
125
125
|
msg = objsto.with_error_code(
|
|
126
126
|
"Could not delete photo from object storage", e)
|
|
127
127
|
logger.error(f"{msg}: {str(e)}")
|
|
128
|
-
raise JsonProblem("{msg}.", 500)
|
|
128
|
+
raise JsonProblem(f"{msg}.", 500) from e
|
|
129
129
|
|
|
130
130
|
try:
|
|
131
131
|
photo.delete()
|
|
132
132
|
except Exception as e:
|
|
133
133
|
msg = "Could not delete photo from database"
|
|
134
134
|
logger.error(f"{msg}: {str(e)}")
|
|
135
|
-
raise JsonProblem(f"{msg}.", 500)
|
|
135
|
+
raise JsonProblem(f"{msg}.", 500) from e
|
|
@@ -7,8 +7,9 @@ from django.shortcuts import render
|
|
|
7
7
|
from django.urls import reverse_lazy
|
|
8
8
|
|
|
9
9
|
from photo_objects.error import PhotoObjectsError
|
|
10
|
+
from photo_objects.utils import pretty_list
|
|
10
11
|
from photo_objects.django.views.utils import BackLink
|
|
11
|
-
from photo_objects.django import
|
|
12
|
+
from photo_objects.django.conf import PhotoSize
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
APPLICATION_JSON = "application/json"
|
|
@@ -17,11 +18,6 @@ MULTIPART_FORMDATA = "multipart/form-data"
|
|
|
17
18
|
APPLICATION_PROBLEM = "application/problem+json"
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
def _pretty_list(in_: list, conjunction: str):
|
|
21
|
-
return f' {conjunction} '.join(
|
|
22
|
-
i for i in (', '.join(in_[:-1]), in_[-1],) if i)
|
|
23
|
-
|
|
24
|
-
|
|
25
21
|
class JsonProblem(PhotoObjectsError):
|
|
26
22
|
def __init__(self, title, status, payload=None, headers=None, errors=None):
|
|
27
23
|
super().__init__(title)
|
|
@@ -54,7 +50,7 @@ class JsonProblem(PhotoObjectsError):
|
|
|
54
50
|
return render(request, "photo_objects/problem.html", {
|
|
55
51
|
"title": "Error",
|
|
56
52
|
"back": BackLink(
|
|
57
|
-
|
|
53
|
+
'Back to albums',
|
|
58
54
|
reverse_lazy('photo_objects:list_albums')),
|
|
59
55
|
"problem_title": self.title,
|
|
60
56
|
"status": self.status
|
|
@@ -63,7 +59,7 @@ class JsonProblem(PhotoObjectsError):
|
|
|
63
59
|
|
|
64
60
|
class MethodNotAllowed(JsonProblem):
|
|
65
61
|
def __init__(self, expected: list[str], actual: str):
|
|
66
|
-
expected_human =
|
|
62
|
+
expected_human = pretty_list(expected, "or")
|
|
67
63
|
|
|
68
64
|
super().__init__(
|
|
69
65
|
f"Expected {expected_human} method, got {actual}.",
|
|
@@ -74,7 +70,7 @@ class MethodNotAllowed(JsonProblem):
|
|
|
74
70
|
|
|
75
71
|
class UnsupportedMediaType(JsonProblem):
|
|
76
72
|
def __init__(self, expected: list[str], actual: str):
|
|
77
|
-
expected_human =
|
|
73
|
+
expected_human = pretty_list(expected, "or")
|
|
78
74
|
|
|
79
75
|
super().__init__(
|
|
80
76
|
f"Expected {expected_human} content-type, got {actual}.",
|
|
@@ -93,7 +89,7 @@ class Unauthorized(JsonProblem):
|
|
|
93
89
|
|
|
94
90
|
class InvalidSize(JsonProblem):
|
|
95
91
|
def __init__(self, actual: str):
|
|
96
|
-
expected =
|
|
92
|
+
expected = pretty_list([i.value for i in PhotoSize], "or")
|
|
97
93
|
|
|
98
94
|
super().__init__(
|
|
99
95
|
f"Expected {expected} size, got {actual or 'none'}.",
|
|
@@ -138,7 +134,7 @@ def check_permissions(request: HttpRequest, *permissions: str):
|
|
|
138
134
|
raise Unauthorized()
|
|
139
135
|
if not request.user.has_perms(permissions):
|
|
140
136
|
raise JsonProblem(
|
|
141
|
-
f"Expected {
|
|
137
|
+
f"Expected {pretty_list(permissions, 'and')} permissions",
|
|
142
138
|
403,
|
|
143
139
|
headers=dict(Allow="GET, POST")
|
|
144
140
|
)
|
|
@@ -157,7 +153,7 @@ def parse_json_body(request: HttpRequest):
|
|
|
157
153
|
raise JsonProblem(
|
|
158
154
|
"Could not parse JSON data from request body.",
|
|
159
155
|
400,
|
|
160
|
-
)
|
|
156
|
+
) from None
|
|
161
157
|
|
|
162
158
|
|
|
163
159
|
def parse_input_data(request: HttpRequest):
|
|
@@ -2,6 +2,8 @@ from django.apps import AppConfig
|
|
|
2
2
|
from django.core.checks import Error, register
|
|
3
3
|
from django.conf import settings
|
|
4
4
|
|
|
5
|
+
from photo_objects.django.conf import validate_photo_sizes
|
|
6
|
+
|
|
5
7
|
|
|
6
8
|
class PhotoObjects(AppConfig):
|
|
7
9
|
default_auto_field = 'django.db.models.BigAutoField'
|
|
@@ -17,7 +19,7 @@ def photo_objects_check(app_configs, **kwargs):
|
|
|
17
19
|
errors = []
|
|
18
20
|
|
|
19
21
|
try:
|
|
20
|
-
|
|
22
|
+
objsto_conf = settings.PHOTO_OBJECTS_OBJSTO
|
|
21
23
|
except AttributeError:
|
|
22
24
|
errors.append(
|
|
23
25
|
Error(
|
|
@@ -29,7 +31,7 @@ def photo_objects_check(app_configs, **kwargs):
|
|
|
29
31
|
return errors
|
|
30
32
|
|
|
31
33
|
for key in ('URL', 'ACCESS_KEY', 'SECRET_KEY',):
|
|
32
|
-
if not
|
|
34
|
+
if not objsto_conf.get(key):
|
|
33
35
|
errors.append(
|
|
34
36
|
Error(
|
|
35
37
|
f'The PHOTO_OBJECTS_OBJSTO setting must define {key} '
|
|
@@ -38,4 +40,14 @@ def photo_objects_check(app_configs, **kwargs):
|
|
|
38
40
|
obj='photo_objects',
|
|
39
41
|
))
|
|
40
42
|
|
|
43
|
+
try:
|
|
44
|
+
sizes_conf = settings.PHOTO_OBJECTS_PHOTO_SIZES
|
|
45
|
+
errors.extend(
|
|
46
|
+
validate_photo_sizes(
|
|
47
|
+
sizes_conf,
|
|
48
|
+
'The PHOTO_OBJECTS_PHOTO_SIZES'))
|
|
49
|
+
except AttributeError:
|
|
50
|
+
# Use default values if sizes are not configured
|
|
51
|
+
pass
|
|
52
|
+
|
|
41
53
|
return errors
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
|
|
6
|
+
from photo_objects.utils import pretty_list
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def objsto_settings() -> dict:
|
|
10
|
+
return settings.PHOTO_OBJECTS_OBJSTO
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PhotoSize(Enum):
|
|
14
|
+
TINY = "xs"
|
|
15
|
+
SMALL = "sm"
|
|
16
|
+
MEDIUM = "md"
|
|
17
|
+
LARGE = "lg"
|
|
18
|
+
ORIGINAL = "og"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
CONFIGURABLE_PHOTO_SIZES = [
|
|
22
|
+
PhotoSize.SMALL.value,
|
|
23
|
+
PhotoSize.MEDIUM.value,
|
|
24
|
+
PhotoSize.LARGE.value
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass()
|
|
29
|
+
class PhotoSizeDimensions:
|
|
30
|
+
max_width: int = None
|
|
31
|
+
max_height: int = None
|
|
32
|
+
max_aspect_ratio: float = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
DEFAULT_SM = dict(
|
|
36
|
+
max_width=512,
|
|
37
|
+
max_height=512,
|
|
38
|
+
max_aspect_ratio=1.5
|
|
39
|
+
)
|
|
40
|
+
DEFAULT_MD = dict(
|
|
41
|
+
max_width=2048,
|
|
42
|
+
max_height=2048
|
|
43
|
+
)
|
|
44
|
+
DEFAULT_LG = dict(
|
|
45
|
+
max_width=4096,
|
|
46
|
+
max_height=4096
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class PhotoSizes:
|
|
52
|
+
sm: PhotoSizeDimensions = None
|
|
53
|
+
md: PhotoSizeDimensions = None
|
|
54
|
+
lg: PhotoSizeDimensions = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def validate_photo_sizes(data: dict, prefix=None) -> list[str]:
|
|
58
|
+
prefix = prefix or "Photo size"
|
|
59
|
+
errors = []
|
|
60
|
+
|
|
61
|
+
for key, value in data.items():
|
|
62
|
+
if key not in CONFIGURABLE_PHOTO_SIZES:
|
|
63
|
+
expected = pretty_list(CONFIGURABLE_PHOTO_SIZES, 'or')
|
|
64
|
+
errors.append(
|
|
65
|
+
f"{prefix} key '{key}' is invalid, expected one of {expected}")
|
|
66
|
+
|
|
67
|
+
if not isinstance(value, dict):
|
|
68
|
+
errors.append(f"{prefix} '{key}' must be a dict.")
|
|
69
|
+
|
|
70
|
+
if 'max_width' not in value and 'max_height' not in value:
|
|
71
|
+
errors.append(
|
|
72
|
+
f"{prefix} '{key}' must define at least one dimension.")
|
|
73
|
+
|
|
74
|
+
if (
|
|
75
|
+
'max_aspect_ratio' in value and
|
|
76
|
+
value['max_aspect_ratio'] is not None and
|
|
77
|
+
not isinstance(value['max_aspect_ratio'], (float, int))
|
|
78
|
+
):
|
|
79
|
+
errors.append(
|
|
80
|
+
f"{prefix} '{key}' max_aspect_ratio must be a number.")
|
|
81
|
+
|
|
82
|
+
return errors
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def parse_photo_sizes(data: dict) -> PhotoSizes:
|
|
86
|
+
errors = validate_photo_sizes(data)
|
|
87
|
+
|
|
88
|
+
if errors:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"Invalid photo sizes configuration: {' '.join(errors)}")
|
|
91
|
+
|
|
92
|
+
return PhotoSizes(
|
|
93
|
+
sm=PhotoSizeDimensions(**data.get('sm', DEFAULT_SM)),
|
|
94
|
+
md=PhotoSizeDimensions(**data.get('md', DEFAULT_MD)),
|
|
95
|
+
lg=PhotoSizeDimensions(**data.get('lg', DEFAULT_LG)),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def photo_sizes() -> PhotoSizes:
|
|
100
|
+
try:
|
|
101
|
+
data = settings.PHOTO_OBJECTS_PHOTO_SIZES
|
|
102
|
+
except AttributeError:
|
|
103
|
+
data = {}
|
|
104
|
+
|
|
105
|
+
return parse_photo_sizes(data)
|
|
@@ -23,9 +23,9 @@ KEY_POSTFIX_CHARS = 'bcdfghjklmnpqrstvwxz2456789'
|
|
|
23
23
|
KEY_POSTFIX_LEN = 5
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def slugify(
|
|
26
|
+
def slugify(title: str, lower=False, replace_leading_underscores=False) -> str:
|
|
27
27
|
key = unicodedata.normalize(
|
|
28
|
-
'NFKD',
|
|
28
|
+
'NFKD', title).encode(
|
|
29
29
|
'ascii', 'ignore').decode('ascii')
|
|
30
30
|
if lower:
|
|
31
31
|
key = key.lower()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# pylint: disable=invalid-name
|
|
2
|
+
from django.core.management.base import BaseCommand
|
|
3
|
+
|
|
4
|
+
from photo_objects.utils import pretty_list
|
|
5
|
+
from photo_objects.django.conf import photo_sizes, CONFIGURABLE_PHOTO_SIZES
|
|
6
|
+
from photo_objects.django.objsto import (
|
|
7
|
+
delete_scaled_photos,
|
|
8
|
+
get_photo_sizes,
|
|
9
|
+
put_photo_sizes,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Command(BaseCommand):
|
|
14
|
+
help = "Remove scaled photos when scaling settings have changed."
|
|
15
|
+
|
|
16
|
+
def handle(self, *args, **options):
|
|
17
|
+
current = photo_sizes()
|
|
18
|
+
previous = get_photo_sizes()
|
|
19
|
+
|
|
20
|
+
if current == previous:
|
|
21
|
+
self.stdout.write(
|
|
22
|
+
self.style.SUCCESS(
|
|
23
|
+
"No changes in photo sizes configuration."
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
if previous is None:
|
|
29
|
+
self.stdout.write(
|
|
30
|
+
self.style.WARNING(
|
|
31
|
+
"No previous photo sizes configuration found. "
|
|
32
|
+
"Removing all scaled photos:"
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
to_delete = CONFIGURABLE_PHOTO_SIZES
|
|
36
|
+
else:
|
|
37
|
+
to_delete = []
|
|
38
|
+
for size in CONFIGURABLE_PHOTO_SIZES:
|
|
39
|
+
if getattr(previous, size) != getattr(current, size):
|
|
40
|
+
to_delete.append(size)
|
|
41
|
+
|
|
42
|
+
changed = pretty_list(to_delete, 'and')
|
|
43
|
+
self.stdout.write(
|
|
44
|
+
self.style.NOTICE(
|
|
45
|
+
"Found changes in photo sizes configuration for "
|
|
46
|
+
f"{changed} sizes. Deleting scaled photos:"
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
total = 0
|
|
52
|
+
for key in delete_scaled_photos(to_delete):
|
|
53
|
+
self.stdout.write(f" {key}")
|
|
54
|
+
total += 1
|
|
55
|
+
self.stdout.write(f"Total deleted photos: {total}")
|
|
56
|
+
except Exception as e:
|
|
57
|
+
self.stdout.write(
|
|
58
|
+
self.style.ERROR(
|
|
59
|
+
f"Error occurred while deleting scaled photos: {e}"
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
exit(1)
|
|
63
|
+
|
|
64
|
+
put_photo_sizes(current)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
# pylint: disable=invalid-name
|
|
2
|
+
from secrets import token_urlsafe
|
|
3
|
+
|
|
1
4
|
from django.core.management.base import BaseCommand
|
|
2
5
|
from django.contrib.auth import get_user_model
|
|
3
|
-
from secrets import token_urlsafe
|
|
4
6
|
|
|
5
7
|
from photo_objects.config import write_to_home_directory
|
|
6
8
|
|
|
@@ -9,13 +11,13 @@ class Command(BaseCommand):
|
|
|
9
11
|
help = "Create initial admin user account."
|
|
10
12
|
|
|
11
13
|
def handle(self, *args, **options):
|
|
12
|
-
|
|
13
|
-
superuser_count =
|
|
14
|
+
user = get_user_model()
|
|
15
|
+
superuser_count = user.objects.filter(is_superuser=True).count()
|
|
14
16
|
|
|
15
17
|
if superuser_count == 0:
|
|
16
18
|
username = 'admin'
|
|
17
19
|
password = token_urlsafe(32)
|
|
18
|
-
|
|
20
|
+
user.objects.create_superuser(username, password=password)
|
|
19
21
|
|
|
20
22
|
write_to_home_directory("initial_admin_password", password)
|
|
21
23
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
# pylint: disable=invalid-name
|
|
1
2
|
from django.core.management.base import BaseCommand
|
|
2
3
|
from django.contrib.sites.models import Site
|
|
3
4
|
|
|
4
5
|
from photo_objects.django.api.album import get_site_album
|
|
5
|
-
from photo_objects.django.models import Album
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class Command(BaseCommand):
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
+
from dataclasses import asdict
|
|
2
|
+
from io import BytesIO
|
|
1
3
|
import json
|
|
2
4
|
import mimetypes
|
|
3
5
|
import urllib3
|
|
4
6
|
|
|
5
|
-
from
|
|
7
|
+
from minio import Minio, S3Error
|
|
6
8
|
|
|
7
|
-
from
|
|
9
|
+
from photo_objects.django.conf import (
|
|
10
|
+
PhotoSize,
|
|
11
|
+
PhotoSizes,
|
|
12
|
+
objsto_settings,
|
|
13
|
+
parse_photo_sizes,
|
|
14
|
+
)
|
|
8
15
|
|
|
9
16
|
|
|
10
17
|
MEGABYTE = 1 << 20
|
|
@@ -26,8 +33,7 @@ def _anonymous_readonly_policy(bucket: str):
|
|
|
26
33
|
|
|
27
34
|
|
|
28
35
|
def _objsto_access() -> tuple[Minio, str]:
|
|
29
|
-
conf =
|
|
30
|
-
|
|
36
|
+
conf = objsto_settings()
|
|
31
37
|
http = urllib3.PoolManager(
|
|
32
38
|
retries=urllib3.util.Retry(connect=1),
|
|
33
39
|
timeout=urllib3.util.Timeout(connect=2.5, read=20),
|
|
@@ -51,7 +57,7 @@ def _objsto_access() -> tuple[Minio, str]:
|
|
|
51
57
|
|
|
52
58
|
|
|
53
59
|
def photo_path(album_key, photo_key, size_key):
|
|
54
|
-
return f"{
|
|
60
|
+
return f"{size_key}/{album_key}/{photo_key}"
|
|
55
61
|
|
|
56
62
|
|
|
57
63
|
def put_photo(album_key, photo_key, size_key, photo_file):
|
|
@@ -79,14 +85,31 @@ def get_photo(album_key, photo_key, size_key):
|
|
|
79
85
|
def delete_photo(album_key, photo_key):
|
|
80
86
|
client, bucket = _objsto_access()
|
|
81
87
|
|
|
82
|
-
for i in
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
for i in PhotoSize:
|
|
89
|
+
client.remove_object(bucket, photo_path(album_key, photo_key, i.value))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def delete_scaled_photos(sizes):
|
|
93
|
+
client, bucket = _objsto_access()
|
|
94
|
+
|
|
95
|
+
for size in sizes:
|
|
96
|
+
while True:
|
|
97
|
+
objects = client.list_objects(
|
|
98
|
+
bucket,
|
|
99
|
+
prefix=f"{size}/",
|
|
100
|
+
recursive=True)
|
|
101
|
+
|
|
102
|
+
if not objects:
|
|
103
|
+
break
|
|
104
|
+
|
|
105
|
+
empty = True
|
|
106
|
+
for i in objects:
|
|
107
|
+
empty = False
|
|
108
|
+
client.remove_object(bucket, i.object_name)
|
|
109
|
+
yield i.object_name
|
|
110
|
+
|
|
111
|
+
if empty:
|
|
112
|
+
break
|
|
90
113
|
|
|
91
114
|
|
|
92
115
|
def get_error_code(e: Exception) -> str:
|
|
@@ -101,3 +124,29 @@ def with_error_code(msg: str, e: Exception) -> str:
|
|
|
101
124
|
if code:
|
|
102
125
|
return f'{msg} ({code})'
|
|
103
126
|
return msg
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def put_photo_sizes(sizes: PhotoSizes):
|
|
130
|
+
data = json.dumps(asdict(sizes))
|
|
131
|
+
stream = BytesIO(data.encode('utf-8'))
|
|
132
|
+
|
|
133
|
+
client, bucket = _objsto_access()
|
|
134
|
+
client.put_object(
|
|
135
|
+
bucket,
|
|
136
|
+
"photo_sizes.json",
|
|
137
|
+
stream,
|
|
138
|
+
length=-1,
|
|
139
|
+
part_size=10 * MEGABYTE,
|
|
140
|
+
content_type="application/json",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def get_photo_sizes() -> PhotoSizes:
|
|
145
|
+
client, bucket = _objsto_access()
|
|
146
|
+
try:
|
|
147
|
+
data = client.get_object(bucket, "photo_sizes.json")
|
|
148
|
+
return parse_photo_sizes(json.loads(data.read()))
|
|
149
|
+
except S3Error as e:
|
|
150
|
+
if e.code == "NoSuchKey":
|
|
151
|
+
return None
|
|
152
|
+
raise
|
|
@@ -2,6 +2,7 @@ from datetime import datetime
|
|
|
2
2
|
from django import template
|
|
3
3
|
|
|
4
4
|
from photo_objects.django.api.album import get_site_album
|
|
5
|
+
from photo_objects.django.views.utils import meta_description
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
register = template.Library()
|
|
@@ -47,12 +48,12 @@ def meta_og(context):
|
|
|
47
48
|
try:
|
|
48
49
|
request = context.get("request")
|
|
49
50
|
site = request.site
|
|
50
|
-
album = get_site_album(site
|
|
51
|
+
album, _ = get_site_album(site)
|
|
51
52
|
|
|
52
53
|
return {
|
|
53
54
|
'request': request,
|
|
54
55
|
"title": album.title or site.name,
|
|
55
|
-
"description": album.description,
|
|
56
|
+
"description": meta_description(request, album.description),
|
|
56
57
|
"photo": album.cover_photo,
|
|
57
58
|
}
|
|
58
59
|
except Exception:
|
|
@@ -22,9 +22,9 @@ PHOTOS_DIRECTORY = "photos"
|
|
|
22
22
|
class ViewVisibilityTests(TestCase):
|
|
23
23
|
@classmethod
|
|
24
24
|
def setUpTestData(cls):
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
user = get_user_model()
|
|
26
|
+
user.objects.create_user(username='test-visibility', password='test')
|
|
27
|
+
user.objects.create_user(
|
|
28
28
|
username='test-staff-visibility',
|
|
29
29
|
password='test',
|
|
30
30
|
is_staff=True)
|
|
@@ -91,16 +91,16 @@ class ViewVisibilityTests(TestCase):
|
|
|
91
91
|
|
|
92
92
|
class AlbumViewTests(TestCase):
|
|
93
93
|
def setUp(self):
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
user = get_user_model()
|
|
95
|
+
user.objects.create_user(username='no_permission', password='test')
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
user.objects.create_user(
|
|
98
98
|
username='superuser',
|
|
99
99
|
password='test',
|
|
100
100
|
is_staff=True,
|
|
101
101
|
is_superuser=True)
|
|
102
102
|
|
|
103
|
-
has_permission =
|
|
103
|
+
has_permission = user.objects.create_user(
|
|
104
104
|
username='has_permission', password='test')
|
|
105
105
|
permissions = [
|
|
106
106
|
'add_album',
|
|
@@ -6,14 +6,14 @@ from .utils import TestCase, create_dummy_photo
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def _path_fn(album, photo):
|
|
9
|
-
return lambda size: f"{
|
|
9
|
+
return lambda size: f"{size}/{album}/{photo}"
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class AuthViewTests(TestCase):
|
|
13
13
|
@classmethod
|
|
14
14
|
def setUpTestData(cls):
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
user = get_user_model()
|
|
16
|
+
user.objects.create_user(username='test-auth', password='test')
|
|
17
17
|
|
|
18
18
|
public_album = Album.objects.create(
|
|
19
19
|
key="test-auth-public", visibility=Album.Visibility.PUBLIC)
|