Skip to content

Commit

Permalink
NEW MIGRATIONS, fiddle with guardian models, add system user, add fil…
Browse files Browse the repository at this point in the history
…e permissions view, rename upload api endpoint fields, create utility methods to assign permissions, add manage_basefile_permissions permission, use FileViewMixin in more views, add permissions app
  • Loading branch information
tykling committed Dec 23, 2024
1 parent a0f7722 commit 2717734
Show file tree
Hide file tree
Showing 48 changed files with 446 additions and 212 deletions.
3 changes: 3 additions & 0 deletions oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@
"allow": "Authorize",
}
auth = s.post(host + "/o/authorize/", allow_redirects=False, data=data, headers={"Referer": host + "/o/authorize/"})
if auth.status_code != 302: # noqa: PLR2004
print(f"/o/authorize/ returned status code {auth.status_code} - no token today") # noqa: T201
sys.exit(1)
url = auth.headers["Location"]
result = urlparse(url)
qs = parse_qs(result.query)
Expand Down
14 changes: 9 additions & 5 deletions src/albums/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.1.3 on 2024-12-03 13:49
# Generated by Django 5.1.4 on 2024-12-23 10:59

import albums.models
import django.contrib.postgres.fields.ranges
Expand Down Expand Up @@ -28,9 +28,11 @@ class Migration(migrations.Migration):
},
),
migrations.CreateModel(
name='AlbumGroupObjectPermission',
name='AlbumGroupPermission',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'abstract': False,
Expand All @@ -44,9 +46,11 @@ class Migration(migrations.Migration):
],
),
migrations.CreateModel(
name='AlbumUserObjectPermission',
name='AlbumUserPermission',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'abstract': False,
Expand Down
34 changes: 18 additions & 16 deletions src/albums/migrations/0002_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.1.3 on 2024-12-03 13:49
# Generated by Django 5.1.4 on 2024-12-23 10:59

import django.contrib.postgres.constraints
import django.db.models.deletion
Expand All @@ -25,20 +25,25 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='album',
name='owner',
field=models.ForeignKey(help_text='The creator of this album.', on_delete=models.SET(users.sentinel.get_sentinel_user), related_name='albums', to=settings.AUTH_USER_MODEL),
field=models.ForeignKey(help_text='The creator of this album.', on_delete=models.SET(users.sentinel.get_deleted_user), related_name='albums', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='albumgroupobjectpermission',
model_name='albumgrouppermission',
name='content_object',
field=models.ForeignKey(on_delete=utils.models.NP_CASCADE, related_name='group_permissions', to='albums.album'),
),
migrations.AddField(
model_name='albumgroupobjectpermission',
model_name='albumgrouppermission',
name='created_by',
field=models.ForeignKey(on_delete=models.SET(users.sentinel.get_deleted_user), related_name='%(app_label)s_%(class)s_permissions', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='albumgrouppermission',
name='group',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group'),
),
migrations.AddField(
model_name='albumgroupobjectpermission',
model_name='albumgrouppermission',
name='permission',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission'),
),
Expand All @@ -58,30 +63,27 @@ class Migration(migrations.Migration):
field=models.ManyToManyField(related_name='albums', through='albums.AlbumMember', to='files.basefile'),
),
migrations.AddField(
model_name='albumuserobjectpermission',
model_name='albumuserpermission',
name='content_object',
field=models.ForeignKey(on_delete=utils.models.NP_CASCADE, related_name='user_permissions', to='albums.album'),
),
migrations.AddField(
model_name='albumuserobjectpermission',
model_name='albumuserpermission',
name='created_by',
field=models.ForeignKey(on_delete=models.SET(users.sentinel.get_deleted_user), related_name='%(app_label)s_%(class)s_permissions', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='albumuserpermission',
name='permission',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission'),
),
migrations.AddField(
model_name='albumuserobjectpermission',
model_name='albumuserpermission',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AlterUniqueTogether(
name='albumgroupobjectpermission',
unique_together={('group', 'permission', 'content_object')},
),
migrations.AddConstraint(
model_name='albummember',
constraint=django.contrib.postgres.constraints.ExclusionConstraint(expressions=[(models.F('basefile'), '='), (models.F('album'), '='), ('period', '&&')], name='prevent_membership_overlaps'),
),
migrations.AlterUniqueTogether(
name='albumuserobjectpermission',
unique_together={('user', 'permission', 'content_object')},
),
]
29 changes: 17 additions & 12 deletions src/albums/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

import logging
import uuid
from typing import TypeAlias

from django.contrib.postgres.constraints import ExclusionConstraint
from django.contrib.postgres.fields import DateTimeRangeField
Expand All @@ -14,11 +13,13 @@
from django.utils import timezone
from guardian.models import GroupObjectPermissionBase
from guardian.models import UserObjectPermissionBase
from guardian.shortcuts import assign_perm
from psycopg2.extras import DateTimeTZRange

from files.models import BaseFile
from users.sentinel import get_sentinel_user
from permissions.models import PermissionModelBase
from permissions.utils import bma_assign_user_perm
from users.sentinel import get_deleted_user
from users.sentinel import get_system_user
from utils.models import NP_CASCADE

from .managers import AlbumManager
Expand All @@ -39,7 +40,7 @@ class Album(models.Model): # type: ignore[django-manager-missing]

owner = models.ForeignKey(
"users.User",
on_delete=models.SET(get_sentinel_user),
on_delete=models.SET(get_deleted_user),
related_name="albums",
help_text="The creator of this album.",
)
Expand Down Expand Up @@ -117,7 +118,8 @@ def remove_members(self, *file_uuids: str) -> int:

def add_initial_permissions(self) -> None:
"""Add initial permissions for newly created albums."""
assign_perm("change_album", self.owner, self)
sys = get_system_user()
bma_assign_user_perm("change_album", user=self.owner, obj=self, creator=sys)

def update_members(self, *file_uuids: str, replace: bool) -> None:
"""Update active album members to file_uuids, adding/removing or replacing as needed."""
Expand Down Expand Up @@ -185,16 +187,19 @@ def __str__(self) -> str:
return f"{self.basefile.uuid} is in album {self.album.uuid} from {self.period.lower}"


class AlbumUserObjectPermission(UserObjectPermissionBase):
"""Use a direct (non-generic) FK for user album permissions in guardian."""
class AlbumUserPermission(PermissionModelBase, UserObjectPermissionBase): # type: ignore[django-manager-missing]
"""The user object permissions class used by guardian for album permissions.
content_object = models.ForeignKey("albums.Album", related_name="user_permissions", on_delete=NP_CASCADE)
Uses a direct (non-generic) FK.
"""

content_object = models.ForeignKey("albums.Album", related_name="user_permissions", on_delete=NP_CASCADE)

class AlbumGroupObjectPermission(GroupObjectPermissionBase):
"""Use a direct (non-generic) FK for group album permissions in guardian."""

content_object = models.ForeignKey("albums.Album", related_name="group_permissions", on_delete=NP_CASCADE)
class AlbumGroupPermission(PermissionModelBase, GroupObjectPermissionBase): # type: ignore[django-manager-missing]
"""The group object permissions class used by guardian for album permissions.
Uses a direct (non-generic) FK.
"""

AlbumType: TypeAlias = Album
content_object = models.ForeignKey("albums.Album", related_name="group_permissions", on_delete=NP_CASCADE)
2 changes: 1 addition & 1 deletion src/audios/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.1.3 on 2024-12-03 13:49
# Generated by Django 5.1.4 on 2024-12-23 10:59

import django.db.models.deletion
import utils.upload
Expand Down
1 change: 1 addition & 0 deletions src/bma/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"tags",
"hitcounter",
"jobs",
"permissions",
]

MIDDLEWARE = [
Expand Down
2 changes: 1 addition & 1 deletion src/documents/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.1.3 on 2024-12-03 13:49
# Generated by Django 5.1.4 on 2024-12-23 10:59

import django.db.models.deletion
import utils.upload
Expand Down
10 changes: 5 additions & 5 deletions src/files/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,11 @@ def has_softdelete_basefile_permission(self, request: HttpRequest, obj: BaseFile
return True
return request.user.has_perm("softdelete_basefile", obj)

def has_undelete_basefile_permission(self, request: HttpRequest, obj: BaseFile | None = None) -> bool:
def has_unsoftdelete_basefile_permission(self, request: HttpRequest, obj: BaseFile | None = None) -> bool:
"""Called by the admin to check if the user has permission to undelete this type of/this specific object."""
if obj is None:
return True
return request.user.has_perm("undelete_basefile", obj)
return request.user.has_perm("unsoftdelete_basefile", obj)

def send_message(self, request: HttpRequest, selected: int, valid: int, updated: int, action: str) -> None:
"""Return a message to the user."""
Expand Down Expand Up @@ -187,13 +187,13 @@ def softdelete(self, request: HttpRequest, queryset: QuerySet[BaseFile]) -> None
self.send_message(request, selected=selected, valid=valids, updated=updated, action="deleted")

@admin.action(
description="Undelete selected %(verbose_name_plural)s",
permissions=["undelete_basefile"],
description="Unsoftdelete selected %(verbose_name_plural)s",
permissions=["unsoftdelete_basefile"],
)
def undelete(self, request: HttpRequest, queryset: QuerySet[BaseFile]) -> None:
"""Admin action to undelete files."""
selected = queryset.count()
valid = get_objects_for_user(request.user, "files.undelete_basefile", klass=queryset)
valid = get_objects_for_user(request.user, "files.unsoftdelete_basefile", klass=queryset)
valids = valid.count()
updated = valid.undelete()
self.send_message(request, selected=selected, valid=valids, updated=updated, action="undeleted")
Expand Down
29 changes: 16 additions & 13 deletions src/files/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from django.shortcuts import get_object_or_404
from django.utils import timezone
from guardian.shortcuts import get_objects_for_user
from ninja import Body
from ninja import File
from ninja import Query
from ninja import Router
from ninja.files import UploadedFile
Expand Down Expand Up @@ -67,19 +69,20 @@
)
def upload( # noqa: C901,PLR0913
request: HttpRequest,
f: UploadedFile,
f_metadata: UploadRequestSchema,
client: JobClientSchema,
t: UploadedFile | None = None,
t_metadata: ImageMetadataSchema | None = None,
file_data: File[UploadedFile],
file_metadata: Body[UploadRequestSchema],
client: Body[JobClientSchema],
# https://github.com/bornhack/bma/issues/291
thumbnail_data: File[UploadedFile] = None, # type: ignore[assignment]
thumbnail_metadata: Body[ImageMetadataSchema] = None, # type: ignore[assignment]
) -> FileApiResponseType:
"""API endpoint for file uploads."""
# make sure the uploading user is in the creators group
if not request.user.is_creator: # type: ignore[union-attr]
return 403, {"message": "Missing upload permissions"}

# get the file metadata
data = f_metadata.dict(exclude_unset=True)
data = file_metadata.dict(exclude_unset=True)

if data["mimetype"] in settings.ALLOWED_IMAGE_TYPES:
from images.models import Image as Model
Expand All @@ -100,9 +103,9 @@ def upload( # noqa: C901,PLR0913
# initiate the model instance
uploaded_file = Model(
uploader=request.user,
original=f,
original_filename=str(f.name),
file_size=f.size,
original=file_data,
original_filename=str(file_data.name),
file_size=file_data.size,
**data,
)

Expand Down Expand Up @@ -140,13 +143,13 @@ def upload( # noqa: C901,PLR0913
logger.debug(f"New {uploaded_file.filetype} file {uploaded_file.uuid} uploaded")

# was a thumbnailsource included?
if t is not None and t_metadata is not None:
tdata = t_metadata.dict()
if thumbnail_data is not None and thumbnail_metadata is not None:
tdata = thumbnail_metadata.dict()
ts = ThumbnailSource(
basefile=uploaded_file,
aspect_ratio=str(Fraction(tdata["width"] / tdata["height"])),
source=t,
file_size=t.size, # type: ignore[misc]
source=thumbnail_data,
file_size=thumbnail_data.size, # type: ignore[misc]
**tdata,
)
# validate everything and return 422 if something is fucky
Expand Down
2 changes: 2 additions & 0 deletions src/files/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ def get_queryset(self) -> models.QuerySet["BaseFile"]:
.annotate(hitcount=Count("hits", distinct=True))
.annotate(jobs_finished=Count("jobs", filter=models.Q(jobs__finished=True)))
.annotate(jobs_unfinished=Count("jobs", filter=models.Q(jobs__finished=False)))
.annotate(user_permission_count=Count("user_permissions"))
.annotate(group_permission_count=Count("group_permissions"))
.prefetch_active_albums_list(recursive=True)
.prefetch_related("thumbnails")
.prefetch_related(models.Prefetch("thumbnails", to_attr="thumbnail_list"))
Expand Down
16 changes: 10 additions & 6 deletions src/files/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.1.3 on 2024-12-03 13:49
# Generated by Django 5.1.4 on 2024-12-23 10:59

import django.db.models.expressions
import pictures.models
Expand Down Expand Up @@ -37,22 +37,26 @@ class Migration(migrations.Migration):
'verbose_name': 'file',
'verbose_name_plural': 'files',
'ordering': ('created_at',),
'permissions': (('unapprove_basefile', 'Unapprove file'), ('approve_basefile', 'Approve file'), ('unpublish_basefile', 'Unpublish file'), ('publish_basefile', 'Publish file'), ('undelete_basefile', 'Undelete file'), ('softdelete_basefile', 'Soft delete file')),
'permissions': (('approve_basefile', 'Can approve file'), ('unapprove_basefile', 'Can unapprove file'), ('publish_basefile', 'Can publish file'), ('unpublish_basefile', 'Can unpublish file'), ('softdelete_basefile', 'Can softdelete file'), ('unsoftdelete_basefile', 'Can unsoftdelete file'), ('manage_basefile_permissions', 'Can manage file permissions')),
},
),
migrations.CreateModel(
name='FileGroupObjectPermission',
name='FileGroupPermission',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='FileUserObjectPermission',
name='FileUserPermission',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'abstract': False,
Expand Down
2 changes: 1 addition & 1 deletion src/files/migrations/0002_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.1.3 on 2024-12-03 13:49
# Generated by Django 5.1.4 on 2024-12-23 10:59

import django.db.models.deletion
import utils.models
Expand Down
2 changes: 1 addition & 1 deletion src/files/migrations/0003_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.1.3 on 2024-12-03 13:49
# Generated by Django 5.1.4 on 2024-12-23 10:59

import taggit.managers
from django.db import migrations
Expand Down
Loading

0 comments on commit 2717734

Please sign in to comment.