photo-objects 0.0.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.0.1/LICENSE +21 -0
- photo_objects-0.0.1/PKG-INFO +79 -0
- photo_objects-0.0.1/README.md +33 -0
- photo_objects-0.0.1/photo_objects/__init__.py +0 -0
- photo_objects-0.0.1/photo_objects/django/__init__.py +10 -0
- photo_objects-0.0.1/photo_objects/django/admin.py +6 -0
- photo_objects-0.0.1/photo_objects/django/api/__init__.py +3 -0
- photo_objects-0.0.1/photo_objects/django/api/album.py +57 -0
- photo_objects-0.0.1/photo_objects/django/api/auth.py +49 -0
- photo_objects-0.0.1/photo_objects/django/api/photo.py +136 -0
- photo_objects-0.0.1/photo_objects/django/api/utils.py +191 -0
- photo_objects-0.0.1/photo_objects/django/apps.py +41 -0
- photo_objects-0.0.1/photo_objects/django/context_processors.py +9 -0
- photo_objects-0.0.1/photo_objects/django/forms.py +140 -0
- photo_objects-0.0.1/photo_objects/django/management/__init__.py +0 -0
- photo_objects-0.0.1/photo_objects/django/management/commands/__init__.py +0 -0
- photo_objects-0.0.1/photo_objects/django/management/commands/create-initial-admin-account.py +31 -0
- photo_objects-0.0.1/photo_objects/django/migrations/0001_initial.py +51 -0
- photo_objects-0.0.1/photo_objects/django/migrations/__init__.py +0 -0
- photo_objects-0.0.1/photo_objects/django/models.py +117 -0
- photo_objects-0.0.1/photo_objects/django/objsto.py +82 -0
- photo_objects-0.0.1/photo_objects/django/signals.py +26 -0
- photo_objects-0.0.1/photo_objects/django/tests/__init__.py +0 -0
- photo_objects-0.0.1/photo_objects/django/tests/test_album.py +278 -0
- photo_objects-0.0.1/photo_objects/django/tests/test_auth.py +91 -0
- photo_objects-0.0.1/photo_objects/django/tests/test_photo.py +289 -0
- photo_objects-0.0.1/photo_objects/django/tests/test_utils.py +18 -0
- photo_objects-0.0.1/photo_objects/django/tests/utils.py +55 -0
- photo_objects-0.0.1/photo_objects/django/urls.py +81 -0
- photo_objects-0.0.1/photo_objects/django/views/__init__.py +0 -0
- photo_objects-0.0.1/photo_objects/django/views/api/__init__.py +3 -0
- photo_objects-0.0.1/photo_objects/django/views/api/album.py +54 -0
- photo_objects-0.0.1/photo_objects/django/views/api/auth.py +26 -0
- photo_objects-0.0.1/photo_objects/django/views/api/photo.py +98 -0
- photo_objects-0.0.1/photo_objects/django/views/api/utils.py +10 -0
- photo_objects-0.0.1/photo_objects/django/views/ui/__init__.py +2 -0
- photo_objects-0.0.1/photo_objects/django/views/ui/album.py +110 -0
- photo_objects-0.0.1/photo_objects/django/views/ui/photo.py +120 -0
- photo_objects-0.0.1/photo_objects/django/views/ui/utils.py +10 -0
- photo_objects-0.0.1/photo_objects/django/views/utils.py +4 -0
- photo_objects-0.0.1/photo_objects/error.py +2 -0
- photo_objects-0.0.1/photo_objects/img.py +94 -0
- photo_objects-0.0.1/photo_objects.egg-info/PKG-INFO +79 -0
- photo_objects-0.0.1/photo_objects.egg-info/SOURCES.txt +47 -0
- photo_objects-0.0.1/photo_objects.egg-info/dependency_links.txt +1 -0
- photo_objects-0.0.1/photo_objects.egg-info/requires.txt +2 -0
- photo_objects-0.0.1/photo_objects.egg-info/top_level.txt +1 -0
- photo_objects-0.0.1/pyproject.toml +38 -0
- photo_objects-0.0.1/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Toni Kangas
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: photo-objects
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Application for storing photos in S3 compatible object-storage.
|
|
5
|
+
Author: Toni Kangas
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2024 Toni Kangas
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/kangasta/photo-objects
|
|
29
|
+
Project-URL: Repository, https://github.com/kangasta/photo-objects.git
|
|
30
|
+
Classifier: Environment :: Web Environment
|
|
31
|
+
Classifier: Framework :: Django
|
|
32
|
+
Classifier: Framework :: Django :: 5.0
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Operating System :: OS Independent
|
|
36
|
+
Classifier: Programming Language :: Python
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
39
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
40
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
41
|
+
Requires-Python: >=3.10
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
License-File: LICENSE
|
|
44
|
+
Requires-Dist: minio~=7.2
|
|
45
|
+
Requires-Dist: pillow~=10.4
|
|
46
|
+
|
|
47
|
+
# Photo Objects
|
|
48
|
+
|
|
49
|
+
[](https://github.com/kangasta/photo-objects/actions/workflows/ci.yml)
|
|
50
|
+
|
|
51
|
+
Application for storing photos in S3 compatible object-storage.
|
|
52
|
+
|
|
53
|
+
## Testing
|
|
54
|
+
|
|
55
|
+
Check and automatically fix formatting with:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pycodestyle --exclude back/api/settings.py,*/migrations/*.py back photo_objects
|
|
59
|
+
autopep8 -aaar --in-place --exclude back/api/settings.py,*/migrations/*.py back photo_objects
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Run static analysis with:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pylint -E --enable=invalid-name,unused-import,useless-object-inheritance back/api photo_objects
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Run integration tests (in the `api` directory) with:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
python3 runtests.py
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Get test coverage with:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
coverage run --branch --source photo_objects runtests.py
|
|
78
|
+
coverage report -m
|
|
79
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Photo Objects
|
|
2
|
+
|
|
3
|
+
[](https://github.com/kangasta/photo-objects/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
Application for storing photos in S3 compatible object-storage.
|
|
6
|
+
|
|
7
|
+
## Testing
|
|
8
|
+
|
|
9
|
+
Check and automatically fix formatting with:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pycodestyle --exclude back/api/settings.py,*/migrations/*.py back photo_objects
|
|
13
|
+
autopep8 -aaar --in-place --exclude back/api/settings.py,*/migrations/*.py back photo_objects
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Run static analysis with:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pylint -E --enable=invalid-name,unused-import,useless-object-inheritance back/api photo_objects
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Run integration tests (in the `api` directory) with:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
python3 runtests.py
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Get test coverage with:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
coverage run --branch --source photo_objects runtests.py
|
|
32
|
+
coverage report -m
|
|
33
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from django.db.models.deletion import ProtectedError
|
|
2
|
+
from django.http import HttpRequest
|
|
3
|
+
|
|
4
|
+
from photo_objects.django.forms import CreateAlbumForm, ModifyAlbumForm
|
|
5
|
+
from photo_objects.django.models import Album
|
|
6
|
+
|
|
7
|
+
from .auth import check_album_access
|
|
8
|
+
from .utils import (
|
|
9
|
+
FormValidationFailed,
|
|
10
|
+
JsonProblem,
|
|
11
|
+
check_permissions,
|
|
12
|
+
parse_input_data,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_albums(request: HttpRequest):
|
|
17
|
+
if not request.user.is_authenticated:
|
|
18
|
+
return Album.objects.filter(visibility=Album.Visibility.PUBLIC)
|
|
19
|
+
else:
|
|
20
|
+
return Album.objects.all()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_album(request: HttpRequest):
|
|
24
|
+
check_permissions(request, 'photo_objects.add_album')
|
|
25
|
+
data = parse_input_data(request)
|
|
26
|
+
|
|
27
|
+
f = CreateAlbumForm(data)
|
|
28
|
+
if not f.is_valid():
|
|
29
|
+
raise FormValidationFailed(f)
|
|
30
|
+
|
|
31
|
+
return f.save()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def modify_album(request: HttpRequest, album_key: str):
|
|
35
|
+
check_permissions(request, 'photo_objects.change_album')
|
|
36
|
+
album = check_album_access(request, album_key)
|
|
37
|
+
data = parse_input_data(request)
|
|
38
|
+
|
|
39
|
+
f = ModifyAlbumForm({**album.to_json(), **data}, instance=album)
|
|
40
|
+
if not f.is_valid():
|
|
41
|
+
raise FormValidationFailed(f)
|
|
42
|
+
|
|
43
|
+
return f.save()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def delete_album(request: HttpRequest, album_key: str):
|
|
47
|
+
check_permissions(request, 'photo_objects.delete_album')
|
|
48
|
+
album = check_album_access(request, album_key)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
album.delete()
|
|
52
|
+
except ProtectedError:
|
|
53
|
+
raise JsonProblem(
|
|
54
|
+
f"Album with {album_key} key can not be deleted because it "
|
|
55
|
+
"contains photos.",
|
|
56
|
+
409,
|
|
57
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from django.http import HttpRequest
|
|
2
|
+
|
|
3
|
+
from photo_objects.django import Size
|
|
4
|
+
from photo_objects.django.models import Album, Photo
|
|
5
|
+
|
|
6
|
+
from photo_objects.django.api.utils import (
|
|
7
|
+
AlbumNotFound,
|
|
8
|
+
InvalidSize,
|
|
9
|
+
PhotoNotFound,
|
|
10
|
+
Unauthorized,
|
|
11
|
+
join_key,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def check_album_access(request: HttpRequest, album_key: str):
|
|
16
|
+
try:
|
|
17
|
+
album = Album.objects.get(key=album_key)
|
|
18
|
+
except Album.DoesNotExist:
|
|
19
|
+
raise AlbumNotFound(album_key)
|
|
20
|
+
|
|
21
|
+
if not request.user.is_authenticated:
|
|
22
|
+
if album.visibility != Album.Visibility.PUBLIC:
|
|
23
|
+
raise AlbumNotFound(album_key)
|
|
24
|
+
|
|
25
|
+
return album
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def check_photo_access(
|
|
29
|
+
request: HttpRequest,
|
|
30
|
+
album_key: str,
|
|
31
|
+
photo_key: str,
|
|
32
|
+
size_key: str):
|
|
33
|
+
try:
|
|
34
|
+
size = Size(size_key)
|
|
35
|
+
except ValueError:
|
|
36
|
+
raise InvalidSize(size_key)
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
photo = Photo.objects.get(key=join_key(album_key, photo_key))
|
|
40
|
+
except Photo.DoesNotExist:
|
|
41
|
+
raise PhotoNotFound(album_key, photo_key)
|
|
42
|
+
|
|
43
|
+
if not request.user.is_authenticated:
|
|
44
|
+
if photo.album.visibility == Album.Visibility.PRIVATE:
|
|
45
|
+
raise AlbumNotFound(album_key)
|
|
46
|
+
if size == Size.ORIGINAL:
|
|
47
|
+
raise Unauthorized()
|
|
48
|
+
|
|
49
|
+
return photo
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from django.core.files.uploadedfile import UploadedFile
|
|
2
|
+
from django.http import HttpRequest
|
|
3
|
+
from minio.error import S3Error
|
|
4
|
+
from PIL import UnidentifiedImageError
|
|
5
|
+
|
|
6
|
+
from photo_objects.django import objsto
|
|
7
|
+
from photo_objects.django.forms import (
|
|
8
|
+
CreatePhotoForm,
|
|
9
|
+
ModifyPhotoForm,
|
|
10
|
+
UploadPhotosForm,
|
|
11
|
+
slugify,
|
|
12
|
+
)
|
|
13
|
+
from photo_objects.img import photo_details
|
|
14
|
+
|
|
15
|
+
from .auth import check_album_access, check_photo_access
|
|
16
|
+
from .utils import (
|
|
17
|
+
FormValidationFailed,
|
|
18
|
+
JsonProblem,
|
|
19
|
+
check_permissions,
|
|
20
|
+
parse_input_data,
|
|
21
|
+
parse_single_file,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_photos(request: HttpRequest, album_key: str):
|
|
26
|
+
album = check_album_access(request, album_key)
|
|
27
|
+
return album.photo_set.all()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _upload_photo(album_key: str, photo_file: UploadedFile):
|
|
31
|
+
try:
|
|
32
|
+
details = photo_details(photo_file)
|
|
33
|
+
except UnidentifiedImageError:
|
|
34
|
+
raise JsonProblem(
|
|
35
|
+
"Could not open photo file.",
|
|
36
|
+
400,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
f = CreatePhotoForm(dict(
|
|
40
|
+
key=f"{album_key}/{slugify(photo_file.name)}",
|
|
41
|
+
album=album_key,
|
|
42
|
+
title="",
|
|
43
|
+
description="",
|
|
44
|
+
**details,
|
|
45
|
+
))
|
|
46
|
+
|
|
47
|
+
if not f.is_valid():
|
|
48
|
+
raise FormValidationFailed(f)
|
|
49
|
+
photo = f.save()
|
|
50
|
+
|
|
51
|
+
photo_file.seek(0)
|
|
52
|
+
try:
|
|
53
|
+
objsto.put_photo(photo.album.key, photo.filename, "og", photo_file)
|
|
54
|
+
except S3Error:
|
|
55
|
+
# TODO: check that there is no photo entry in the database, if object
|
|
56
|
+
# storage upload fails.
|
|
57
|
+
photo.delete()
|
|
58
|
+
# TODO: logging
|
|
59
|
+
raise JsonProblem(
|
|
60
|
+
"Could not save photo to object storage.",
|
|
61
|
+
500,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return photo
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def upload_photo(request: HttpRequest, album_key: str):
|
|
68
|
+
check_permissions(
|
|
69
|
+
request,
|
|
70
|
+
'photo_objects.add_photo',
|
|
71
|
+
'photo_objects.change_album')
|
|
72
|
+
photo_file = parse_single_file(request)
|
|
73
|
+
return _upload_photo(album_key, photo_file)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def upload_photos(request: HttpRequest, album_key: str):
|
|
77
|
+
check_permissions(
|
|
78
|
+
request,
|
|
79
|
+
'photo_objects.add_photo',
|
|
80
|
+
'photo_objects.change_album')
|
|
81
|
+
|
|
82
|
+
f = UploadPhotosForm(request.POST, request.FILES)
|
|
83
|
+
if not f.is_valid():
|
|
84
|
+
raise FormValidationFailed(f)
|
|
85
|
+
|
|
86
|
+
photo_files = f.cleaned_data["photos"]
|
|
87
|
+
if len(photo_files) < 1:
|
|
88
|
+
f.add_error("photos", "Expected at least one file, got 0.")
|
|
89
|
+
raise FormValidationFailed(f)
|
|
90
|
+
|
|
91
|
+
photos = []
|
|
92
|
+
for photo_file in f.cleaned_data["photos"]:
|
|
93
|
+
try:
|
|
94
|
+
photos.append(_upload_photo(album_key, photo_file))
|
|
95
|
+
except JsonProblem:
|
|
96
|
+
# TODO: include error type in the message
|
|
97
|
+
f.add_error("photos", f"Failed to upload {photo_file.name}.")
|
|
98
|
+
|
|
99
|
+
if not f.is_valid():
|
|
100
|
+
raise FormValidationFailed(f)
|
|
101
|
+
|
|
102
|
+
return photos
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def modify_photo(request: HttpRequest, album_key: str, photo_key: str):
|
|
106
|
+
check_permissions(request, 'photo_objects.change_photo')
|
|
107
|
+
photo = check_photo_access(request, album_key, photo_key, 'xs')
|
|
108
|
+
data = parse_input_data(request)
|
|
109
|
+
|
|
110
|
+
f = ModifyPhotoForm({**photo.to_json(), **data}, instance=photo)
|
|
111
|
+
|
|
112
|
+
if not f.is_valid():
|
|
113
|
+
raise FormValidationFailed(f)
|
|
114
|
+
|
|
115
|
+
return f.save()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def delete_photo(request: HttpRequest, album_key: str, photo_key: str):
|
|
119
|
+
check_permissions(request, 'photo_objects.delete_photo')
|
|
120
|
+
photo = check_photo_access(request, album_key, photo_key, 'xs')
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
objsto.delete_photo(album_key, photo_key)
|
|
124
|
+
except S3Error:
|
|
125
|
+
raise JsonProblem(
|
|
126
|
+
"Could not delete photo from object storage.",
|
|
127
|
+
500,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
photo.delete()
|
|
132
|
+
except Exception:
|
|
133
|
+
raise JsonProblem(
|
|
134
|
+
"Could not delete photo from database.",
|
|
135
|
+
500,
|
|
136
|
+
)
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from django.forms import ModelForm
|
|
4
|
+
from django.http import HttpRequest, JsonResponse
|
|
5
|
+
from django.core.files.uploadedfile import UploadedFile
|
|
6
|
+
from django.shortcuts import render
|
|
7
|
+
from django.urls import reverse_lazy
|
|
8
|
+
|
|
9
|
+
from photo_objects.error import PhotoObjectsError
|
|
10
|
+
from photo_objects.django.views.utils import BackLink
|
|
11
|
+
from photo_objects.django import Size
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
APPLICATION_JSON = "application/json"
|
|
15
|
+
APPLICATION_X_WWW_FORM = "application/x-www-form-urlencoded"
|
|
16
|
+
MULTIPART_FORMDATA = "multipart/form-data"
|
|
17
|
+
APPLICATION_PROBLEM = "application/problem+json"
|
|
18
|
+
|
|
19
|
+
|
|
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
|
+
class JsonProblem(PhotoObjectsError):
|
|
26
|
+
def __init__(self, title, status, payload=None, headers=None, errors=None):
|
|
27
|
+
super().__init__(title)
|
|
28
|
+
|
|
29
|
+
self.title = title
|
|
30
|
+
self.status = status
|
|
31
|
+
self.payload = payload or {}
|
|
32
|
+
self.headers = headers
|
|
33
|
+
self.errors = errors
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def json_response(self):
|
|
37
|
+
payload = {
|
|
38
|
+
'title': self.title,
|
|
39
|
+
'status': self.status,
|
|
40
|
+
**self.payload
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if self.errors:
|
|
44
|
+
payload['errors'] = self.errors
|
|
45
|
+
|
|
46
|
+
return JsonResponse(
|
|
47
|
+
payload,
|
|
48
|
+
content_type=APPLICATION_PROBLEM,
|
|
49
|
+
status=self.status,
|
|
50
|
+
headers=self.headers
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def html_response(self, request: HttpRequest):
|
|
54
|
+
return render(request, "photo_objects/problem.html", {
|
|
55
|
+
"title": "Error",
|
|
56
|
+
"back": BackLink(
|
|
57
|
+
f'Back to albums',
|
|
58
|
+
reverse_lazy('photo_objects:list_albums')),
|
|
59
|
+
"problem_title": self.title,
|
|
60
|
+
"status": self.status
|
|
61
|
+
}, status=self.status)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class MethodNotAllowed(JsonProblem):
|
|
65
|
+
def __init__(self, expected: list[str], actual: str):
|
|
66
|
+
expected_human = _pretty_list(expected, "or")
|
|
67
|
+
|
|
68
|
+
super().__init__(
|
|
69
|
+
f"Expected {expected_human} method, got {actual}.",
|
|
70
|
+
405,
|
|
71
|
+
headers=dict(Allow=', '.join(expected))
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class UnsupportedMediaType(JsonProblem):
|
|
76
|
+
def __init__(self, expected: list[str], actual: str):
|
|
77
|
+
expected_human = _pretty_list(expected, "or")
|
|
78
|
+
|
|
79
|
+
super().__init__(
|
|
80
|
+
f"Expected {expected_human} content-type, got {actual}.",
|
|
81
|
+
415,
|
|
82
|
+
headers={'Accept-Post': ', '.join(expected)}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Unauthorized(JsonProblem):
|
|
87
|
+
def __init__(self):
|
|
88
|
+
super().__init__(
|
|
89
|
+
"Not authenticated.",
|
|
90
|
+
401,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class InvalidSize(JsonProblem):
|
|
95
|
+
def __init__(self, actual: str):
|
|
96
|
+
expected = _pretty_list([i.value for i in Size], "or")
|
|
97
|
+
|
|
98
|
+
super().__init__(
|
|
99
|
+
f"Expected {expected} size, got {actual or 'none'}.",
|
|
100
|
+
400,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class AlbumNotFound(JsonProblem):
|
|
105
|
+
def __init__(self, album_key: str):
|
|
106
|
+
super().__init__(
|
|
107
|
+
f"Album with {album_key} key does not exist.",
|
|
108
|
+
404,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class PhotoNotFound(JsonProblem):
|
|
113
|
+
def __init__(self, album_key: str, photo_key: str):
|
|
114
|
+
super().__init__(
|
|
115
|
+
f"Photo with {photo_key} key does not exist in {album_key} album.",
|
|
116
|
+
404,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class FormValidationFailed(JsonProblem):
|
|
121
|
+
def __init__(self, form: ModelForm):
|
|
122
|
+
try:
|
|
123
|
+
resource = form.instance.__class__.__name__
|
|
124
|
+
except AttributeError:
|
|
125
|
+
resource = "Form"
|
|
126
|
+
|
|
127
|
+
super().__init__(
|
|
128
|
+
f"{resource} validation failed.",
|
|
129
|
+
400,
|
|
130
|
+
errors=form.errors.get_json_data(),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
self.form = form
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def check_permissions(request: HttpRequest, *permissions: str):
|
|
137
|
+
if not request.user.is_authenticated:
|
|
138
|
+
raise Unauthorized()
|
|
139
|
+
if not request.user.has_perms(permissions):
|
|
140
|
+
raise JsonProblem(
|
|
141
|
+
f"Expected {_pretty_list(permissions, 'and')} permissions",
|
|
142
|
+
403,
|
|
143
|
+
headers=dict(Allow="GET, POST")
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def parse_json_body(request: HttpRequest):
|
|
148
|
+
if request.content_type != APPLICATION_JSON:
|
|
149
|
+
raise UnsupportedMediaType(
|
|
150
|
+
[APPLICATION_JSON],
|
|
151
|
+
request.content_type
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
return json.loads(request.body)
|
|
156
|
+
except BaseException:
|
|
157
|
+
raise JsonProblem(
|
|
158
|
+
"Could not parse JSON data from request body.",
|
|
159
|
+
400,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def parse_input_data(request: HttpRequest):
|
|
164
|
+
if request.content_type == APPLICATION_JSON:
|
|
165
|
+
return parse_json_body(request)
|
|
166
|
+
elif request.content_type == APPLICATION_X_WWW_FORM:
|
|
167
|
+
return request.POST.dict()
|
|
168
|
+
else:
|
|
169
|
+
raise UnsupportedMediaType(
|
|
170
|
+
[APPLICATION_JSON, APPLICATION_X_WWW_FORM], request.content_type)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def parse_single_file(request: HttpRequest) -> UploadedFile:
|
|
174
|
+
if request.content_type != MULTIPART_FORMDATA:
|
|
175
|
+
raise UnsupportedMediaType(
|
|
176
|
+
[MULTIPART_FORMDATA],
|
|
177
|
+
request.content_type
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if len(request.FILES) != 1:
|
|
181
|
+
raise JsonProblem(
|
|
182
|
+
f"Expected exactly one file, got {len(request.FILES)}.",
|
|
183
|
+
400,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
for _, f in request.FILES.items():
|
|
187
|
+
return f
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def join_key(*args):
|
|
191
|
+
return '/'.join(args)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
from django.core.checks import Error, register
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PhotoObjects(AppConfig):
|
|
7
|
+
default_auto_field = 'django.db.models.BigAutoField'
|
|
8
|
+
name = 'photo_objects.django'
|
|
9
|
+
label = 'photo_objects'
|
|
10
|
+
|
|
11
|
+
def ready(self):
|
|
12
|
+
from . import signals
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@register()
|
|
16
|
+
def photo_objects_check(app_configs, **kwargs):
|
|
17
|
+
errors = []
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
conf = settings.PHOTO_OBJECTS_OBJSTO
|
|
21
|
+
except AttributeError:
|
|
22
|
+
errors.append(
|
|
23
|
+
Error(
|
|
24
|
+
'The PHOTO_OBJECTS_OBJSTO setting must be defined.',
|
|
25
|
+
id='UNDEFINED_SETTING',
|
|
26
|
+
obj='photo_objects',
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
return errors
|
|
30
|
+
|
|
31
|
+
for key in ('URL', 'ACCESS_KEY', 'SECRET_KEY',):
|
|
32
|
+
if not conf.get(key):
|
|
33
|
+
errors.append(
|
|
34
|
+
Error(
|
|
35
|
+
f'The PHOTO_OBJECTS_OBJSTO setting must define {key} '
|
|
36
|
+
'field.',
|
|
37
|
+
id='UNDEFINED_FIELD',
|
|
38
|
+
obj='photo_objects',
|
|
39
|
+
))
|
|
40
|
+
|
|
41
|
+
return errors
|