photo-objects 0.3.1__py3-none-any.whl → 0.4.1__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.
Files changed (38) hide show
  1. photo_objects/config.py +6 -0
  2. photo_objects/django/__init__.py +0 -10
  3. photo_objects/django/api/album.py +2 -2
  4. photo_objects/django/api/auth.py +6 -6
  5. photo_objects/django/api/photo.py +4 -4
  6. photo_objects/django/api/utils.py +8 -12
  7. photo_objects/django/apps.py +14 -2
  8. photo_objects/django/conf.py +105 -0
  9. photo_objects/django/forms.py +2 -2
  10. photo_objects/django/management/commands/clean-scaled-photos.py +64 -0
  11. photo_objects/django/management/commands/create-initial-admin-account.py +6 -4
  12. photo_objects/django/management/commands/create-site-albums.py +1 -1
  13. photo_objects/django/objsto.py +62 -13
  14. photo_objects/django/signals.py +0 -2
  15. photo_objects/django/templatetags/photo_objects_extras.py +3 -2
  16. photo_objects/django/tests/test_album.py +7 -7
  17. photo_objects/django/tests/test_auth.py +3 -3
  18. photo_objects/django/tests/test_commands.py +80 -0
  19. photo_objects/django/tests/test_img.py +29 -0
  20. photo_objects/django/tests/test_photo.py +4 -4
  21. photo_objects/django/tests/test_utils.py +24 -3
  22. photo_objects/django/tests/utils.py +35 -1
  23. photo_objects/django/urls.py +1 -2
  24. photo_objects/django/views/api/auth.py +1 -1
  25. photo_objects/django/views/api/photo.py +7 -7
  26. photo_objects/django/views/ui/album.py +8 -5
  27. photo_objects/django/views/ui/configuration.py +1 -1
  28. photo_objects/django/views/ui/photo.py +8 -4
  29. photo_objects/django/views/ui/users.py +1 -1
  30. photo_objects/django/views/utils.py +48 -0
  31. photo_objects/img.py +44 -2
  32. photo_objects/utils.py +3 -0
  33. {photo_objects-0.3.1.dist-info → photo_objects-0.4.1.dist-info}/METADATA +2 -2
  34. photo_objects-0.4.1.dist-info/RECORD +58 -0
  35. photo_objects-0.3.1.dist-info/RECORD +0 -53
  36. {photo_objects-0.3.1.dist-info → photo_objects-0.4.1.dist-info}/WHEEL +0 -0
  37. {photo_objects-0.3.1.dist-info → photo_objects-0.4.1.dist-info}/licenses/LICENSE +0 -0
  38. {photo_objects-0.3.1.dist-info → photo_objects-0.4.1.dist-info}/top_level.txt +0 -0
photo_objects/config.py CHANGED
@@ -29,3 +29,9 @@ def get_secret_key() -> str:
29
29
  write_to_home_directory("secret_key", key)
30
30
 
31
31
  return key
32
+
33
+
34
+ def add_port_to_host(url: str, port: str | None) -> str:
35
+ if port:
36
+ return f"{url}:{port}"
37
+ return url
@@ -1,10 +0,0 @@
1
- from enum import Enum
2
-
3
-
4
- class Size(Enum):
5
- TINY = "xs"
6
- SMALL = "sm"
7
- MEDIUM = "md"
8
- LARGE = "lg"
9
- HUGE = "xl"
10
- ORIGINAL = "og"
@@ -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 Size
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 = Size(size_key)
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 == Size.ORIGINAL:
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 Size
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
- f'Back to albums',
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 = _pretty_list(expected, "or")
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 = _pretty_list(expected, "or")
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 = _pretty_list([i.value for i in Size], "or")
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 {_pretty_list(permissions, 'and')} permissions",
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
- conf = settings.PHOTO_OBJECTS_OBJSTO
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 conf.get(key):
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(input: str, lower=False, replace_leading_underscores=False) -> str:
26
+ def slugify(title: str, lower=False, replace_leading_underscores=False) -> str:
27
27
  key = unicodedata.normalize(
28
- 'NFKD', input).encode(
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
- User = get_user_model()
13
- superuser_count = User.objects.filter(is_superuser=True).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
- User.objects.create_superuser(username, password=password)
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 django.conf import settings
7
+ from minio import Minio, S3Error
6
8
 
7
- from minio import Minio
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 = settings.PHOTO_OBJECTS_OBJSTO
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"{album_key}/{photo_key}/{size_key}"
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 client.list_objects(
83
- bucket,
84
- prefix=photo_path(
85
- album_key,
86
- photo_key,
87
- ""),
88
- recursive=True):
89
- client.remove_object(bucket, i.object_name)
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
@@ -1,5 +1,3 @@
1
- import re
2
-
3
1
  from django.contrib.sites.models import Site
4
2
  from django.db.models.signals import post_save, post_delete
5
3
  from django.dispatch import receiver
@@ -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.id)
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
- User = get_user_model()
26
- User.objects.create_user(username='test-visibility', password='test')
27
- User.objects.create_user(
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
- User = get_user_model()
95
- User.objects.create_user(username='no_permission', password='test')
94
+ user = get_user_model()
95
+ user.objects.create_user(username='no_permission', password='test')
96
96
 
97
- has_permission = User.objects.create_user(
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 = User.objects.create_user(
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"{album}/{photo}/{size}"
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
- User = get_user_model()
16
- User.objects.create_user(username='test-auth', password='test')
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)