Skip to content
Snippets Groups Projects
Commit 55260ba9 authored by Simon Künzel's avatar Simon Künzel
Browse files

Rework load options

parent ab6c82b4
No related branches found
No related tags found
No related merge requests found
from .fields import * from .fields import *
from .object import ( from .object import (
ApiObject, ApiObject,
RelationshipLoadMarker,
DeletableApiObject, DeletableApiObject,
VisibilityApiObject, VisibilityApiObject,
api_mapped, api_mapped,
......
...@@ -60,6 +60,10 @@ class ApiValueObject: ...@@ -60,6 +60,10 @@ class ApiValueObject:
pass pass
class RelationshipLoadMarker:
pass
class ApiObject: class ApiObject:
id: Mapped[int] = api_mapped( id: Mapped[int] = api_mapped(
mapped_column(nullable=False, primary_key=True, autoincrement=True, sort_order=-1), mapped_column(nullable=False, primary_key=True, autoincrement=True, sort_order=-1),
...@@ -109,8 +113,6 @@ class ApiObject: ...@@ -109,8 +113,6 @@ class ApiObject:
kwargs should all have default values and are only intended for optimization (e.g. prevent checking from both kwargs should all have default values and are only intended for optimization (e.g. prevent checking from both
directions of a relationship) directions of a relationship)
""" """
if len(kwargs) > 0:
raise Exception(f"Unknown kwargs: {kwargs}")
if isinstance(self, type): if isinstance(self, type):
return sql.True_() return sql.True_()
else: else:
...@@ -134,8 +136,15 @@ class ApiObject: ...@@ -134,8 +136,15 @@ class ApiObject:
return sql.select(cls).where(cls.is_active()) return sql.select(cls).where(cls.is_active())
@classmethod @classmethod
def select(cls, is_mod: bool, **kwargs): def select(cls, is_mod: bool, to_load: list[RelationshipLoadMarker], **kwargs):
return cls.basic_select().where(cls.has_access(is_mod, no_is_active_conditions=True, **kwargs)) return (cls.basic_select()
.where(cls.has_access(is_mod, no_is_active_conditions=True, **kwargs))
.options(*cls.load_options(is_mod, to_load, **kwargs))
)
@classmethod
def load_options(cls, is_mod: bool, to_load: list[RelationshipLoadMarker], **kwargs) -> list[ExecutableOption]:
return []
def serialize(self, **kwargs): def serialize(self, **kwargs):
return self.__api_class__.serialize(self, **kwargs) return self.__api_class__.serialize(self, **kwargs)
......
from .course import Course, Lecture, Chapter from .course import (
from .site import Announcement, AnnouncementType, AnnouncementPageVisibility, Featured Course,
Lecture,
Chapter,
LOAD_COURSE_LECTURES,
LOAD_LECTURE_COURSE,
LOAD_LECTURE_CHAPTERS,
LOAD_LECTURE_PUBLISH_MEDIA,
LOAD_LECTURE_TARGET_MEDIA,
)
from .site import Announcement, AnnouncementType, AnnouncementPageVisibility, Featured, LOAD_FEATURED_COURSE_OR_LECTURE
from .user import User from .user import User
from .medium import ( from .medium import (
PublishMedium, PublishMedium,
...@@ -8,7 +17,11 @@ from .medium import ( ...@@ -8,7 +17,11 @@ from .medium import (
PlainVideoTargetMedium, PlainVideoTargetMedium,
PlainAudioTargetMedium, PlainAudioTargetMedium,
ThumbnailTargetMedium, ThumbnailTargetMedium,
SourceMedium SourceMedium,
LOAD_PUBLISH_MEDIUM_LECTURE,
LOAD_PUBLISH_MEDIUM_TARGET_MEDIUM,
LOAD_TARGET_MEDIUM_LECTURE,
LOAD_TARGET_MEDIUM_PUBLISH_MEDIUM
) )
from .view_permissions import ViewPermissions, ViewPermissionsType, EffectiveViewPermissions from .view_permissions import ViewPermissions, ViewPermissionsType, EffectiveViewPermissions
from .changelog import ( from .changelog import (
...@@ -17,7 +30,8 @@ from .changelog import ( ...@@ -17,7 +30,8 @@ from .changelog import (
ChangelogCreationEntry, ChangelogCreationEntry,
ChangelogModificationEntry, ChangelogModificationEntry,
ChangelogDeletionChangeEntry, ChangelogDeletionChangeEntry,
ChangelogUnknownEntry ChangelogUnknownEntry,
LOAD_CHANGELOG_ENTRY_USER
) )
from .job import JobState, Job from .job import JobState, Job
......
...@@ -16,6 +16,9 @@ class ChangelogEntryType(Enum): ...@@ -16,6 +16,9 @@ class ChangelogEntryType(Enum):
_CHANGELOG_ENTRY_TYPE_ENUM = create_enum_type(ChangelogEntryType) _CHANGELOG_ENTRY_TYPE_ENUM = create_enum_type(ChangelogEntryType)
LOAD_CHANGELOG_ENTRY_USER = RelationshipLoadMarker()
class ChangelogEntry(ApiObject, Base): class ChangelogEntry(ApiObject, Base):
__mapper_args__ = { __mapper_args__ = {
"polymorphic_on": "type", "polymorphic_on": "type",
...@@ -61,22 +64,12 @@ class ChangelogEntry(ApiObject, Base): ...@@ -61,22 +64,12 @@ class ChangelogEntry(ApiObject, Base):
) )
@classmethod @classmethod
def select(cls, def load_options(cls, is_mod: bool, to_load: list[RelationshipLoadMarker], **kwargs) -> list[ExecutableOption]:
is_mod: bool, options = super().load_options(is_mod, to_load, **kwargs)
load_user: bool = False,
**kwargs): if LOAD_CHANGELOG_ENTRY_USER in to_load:
return super().select(is_mod, **kwargs).options(*cls.load_options(load_user=load_user)) options.append(orm.selectinload(ChangelogEntry.modifying_user))
@staticmethod
def load_options(
load_user=False,
) -> list[ExecutableOption]:
options = []
if load_user:
options.append(
orm.selectinload(ChangelogEntry.modifying_user).options()
)
return options return options
......
from datetime import datetime from datetime import datetime
from sqlalchemy import UniqueConstraint
from videoag_common.database import * from videoag_common.database import *
from videoag_common.api_object import * from videoag_common.api_object import *
from videoag_common.media_process import * from videoag_common.media_process import *
from videoag_common.miscellaneous import * from videoag_common.miscellaneous import *
from .view_permissions import EffectiveViewPermissions, ApiViewPermissionsObject, DEFAULT_VIEW_PERMISSIONS from .view_permissions import EffectiveViewPermissions, ApiViewPermissionsObject, DEFAULT_VIEW_PERMISSIONS
LOAD_COURSE_LECTURES = RelationshipLoadMarker()
LOAD_LECTURE_COURSE = RelationshipLoadMarker()
LOAD_LECTURE_CHAPTERS = RelationshipLoadMarker()
LOAD_LECTURE_PUBLISH_MEDIA = RelationshipLoadMarker()
LOAD_LECTURE_TARGET_MEDIA = RelationshipLoadMarker()
class Chapter(DeletableApiObject, VisibilityApiObject, Base): class Chapter(DeletableApiObject, VisibilityApiObject, Base):
__api_class__ = ApiObjectClass( __api_class__ = ApiObjectClass(
...@@ -151,7 +155,7 @@ class Lecture(DeletableApiObject, VisibilityApiObject, ApiViewPermissionsObject, ...@@ -151,7 +155,7 @@ class Lecture(DeletableApiObject, VisibilityApiObject, ApiViewPermissionsObject,
from .medium import PublishMedium from .medium import PublishMedium
return sql.and_( return sql.and_(
PublishMedium.lecture_id == Lecture.id, PublishMedium.lecture_id == Lecture.id,
PublishMedium.has_access(is_mod=True) PublishMedium.has_access(is_mod=True, from_lecture=True)
) )
@staticmethod @staticmethod
...@@ -159,7 +163,7 @@ class Lecture(DeletableApiObject, VisibilityApiObject, ApiViewPermissionsObject, ...@@ -159,7 +163,7 @@ class Lecture(DeletableApiObject, VisibilityApiObject, ApiViewPermissionsObject,
from .medium import PublishMedium from .medium import PublishMedium
return sql.and_( return sql.and_(
PublishMedium.lecture_id == Lecture.id, PublishMedium.lecture_id == Lecture.id,
PublishMedium.has_access(is_mod=False) PublishMedium.has_access(is_mod=False, from_lecture=True)
) )
@staticmethod @staticmethod
...@@ -167,7 +171,7 @@ class Lecture(DeletableApiObject, VisibilityApiObject, ApiViewPermissionsObject, ...@@ -167,7 +171,7 @@ class Lecture(DeletableApiObject, VisibilityApiObject, ApiViewPermissionsObject,
from .medium import TargetMedium from .medium import TargetMedium
return sql.and_( return sql.and_(
TargetMedium.lecture_id == Lecture.id, TargetMedium.lecture_id == Lecture.id,
TargetMedium.is_active() TargetMedium.is_active(from_lecture=True)
) )
@staticmethod @staticmethod
...@@ -279,64 +283,33 @@ class Lecture(DeletableApiObject, VisibilityApiObject, ApiViewPermissionsObject, ...@@ -279,64 +283,33 @@ class Lecture(DeletableApiObject, VisibilityApiObject, ApiViewPermissionsObject,
return cond return cond
@classmethod @classmethod
def select(cls, def load_options(cls, is_mod: bool, to_load: list[RelationshipLoadMarker], from_course=False, **kwargs) -> list[ExecutableOption]:
is_mod: bool, options = super().load_options(is_mod, to_load, **kwargs)
visibility_check: bool = True,
load_course=False,
load_chapters=False,
load_media=False,
**kwargs):
return (
super().select(is_mod, visibility_check=visibility_check, **kwargs)
.options(*Lecture.load_options(
is_mod,
False,
load_course=load_course,
load_chapters=load_chapters,
load_media=load_media
))
)
@staticmethod if LOAD_LECTURE_CHAPTERS in to_load:
def load_options(
is_mod: bool,
from_course: bool,
load_course=False,
load_chapters=False,
load_media=False
) -> list[ExecutableOption]:
"""
:param is_mod: If false, the public_publish_media, etc. are loaded
:param from_course: Set to True if these options are applied to an orm.selectinload(Course.lectures) (or similar)
:param load_course: Whether to load the course
:param load_chapters: Whether to load the chapters
:param load_media: Whether to load the publish_media
:return: The options
"""
options = []
if load_chapters:
options.append( options.append(
orm.selectinload(Lecture.chapters if is_mod else Lecture.public_chapters).options( orm.selectinload(Lecture.chapters if is_mod else Lecture.public_chapters).options(
# Causes no extra sql, just that the back reference is set (relationship has simple join)
orm.immediateload(Chapter.lecture) orm.immediateload(Chapter.lecture)
) )
) )
if load_media: if LOAD_LECTURE_PUBLISH_MEDIA in to_load:
from .medium import PublishMedium, TargetMedium, PlainVideoTargetMedium from .medium import PublishMedium, TargetMedium
options.append( options.append(
orm.selectinload(Lecture.publish_media if is_mod else Lecture.public_publish_media).options( orm.selectinload(Lecture.publish_media if is_mod else Lecture.public_publish_media).options(
orm.immediateload(PublishMedium.lecture), *PublishMedium.load_options(is_mod, to_load)
orm.joinedload(PublishMedium.target_medium)
) )
) )
if from_course: if from_course:
# Causes no extra sql, just that the back reference is set # Causes no extra sql, just that the back reference is set (relationship has simple join)
options.append(orm.immediateload(Lecture.course)) options.append(orm.immediateload(Lecture.course))
elif load_course: elif LOAD_LECTURE_COURSE in to_load:
options.append(orm.joinedload(Lecture.course)) options.append(orm.joinedload(Lecture.course).options(
*Course.load_options(is_mod, to_load)
))
return options return options
...@@ -518,36 +491,13 @@ class Course(DeletableApiObject, VisibilityApiObject, ApiViewPermissionsObject, ...@@ -518,36 +491,13 @@ class Course(DeletableApiObject, VisibilityApiObject, ApiViewPermissionsObject,
return cond return cond
@classmethod @classmethod
def select(cls, def load_options(cls, is_mod: bool, to_load: list[RelationshipLoadMarker], **kwargs) -> list[ExecutableOption]:
is_mod: bool, options = super().load_options(is_mod, to_load, **kwargs)
visibility_check: bool = True,
ignore_unlisted: bool = False,
load_lectures=False,
load_chapters=False,
load_media=False,
**kwargs):
return (
super().select(is_mod, visibility_check=visibility_check, ignore_unlisted=ignore_unlisted, **kwargs)
.options(*Course.load_options(
is_mod,
load_lectures=load_lectures,
load_chapters=load_chapters,
load_media=load_media
))
)
@staticmethod
def load_options(
is_mod: bool,
load_lectures=False,
load_chapters=False,
load_media=False) -> list[ExecutableOption]:
options = []
if load_lectures: if LOAD_COURSE_LECTURES in to_load:
options.append( options.append(
orm.selectinload(Course.lectures if is_mod else Course.public_lectures).options( orm.selectinload(Course.lectures if is_mod else Course.public_lectures).options(
*Lecture.load_options(is_mod, True, load_chapters=load_chapters, load_media=load_media) *Lecture.load_options(is_mod, to_load, from_course=True)
) )
) )
return options return options
...@@ -10,6 +10,12 @@ from .course import Lecture ...@@ -10,6 +10,12 @@ from .course import Lecture
from .job import Job from .job import Job
LOAD_PUBLISH_MEDIUM_LECTURE = RelationshipLoadMarker()
LOAD_PUBLISH_MEDIUM_TARGET_MEDIUM = RelationshipLoadMarker()
LOAD_TARGET_MEDIUM_PUBLISH_MEDIUM = RelationshipLoadMarker()
LOAD_TARGET_MEDIUM_LECTURE = RelationshipLoadMarker()
# A file which was uploaded and needs to be assigned to a lecture, to be converted to a SourceFileTargetMedium # A file which was uploaded and needs to be assigned to a lecture, to be converted to a SourceFileTargetMedium
class SourceMedium(DeletableApiObject, Base): class SourceMedium(DeletableApiObject, Base):
__table_args__ = ( __table_args__ = (
...@@ -134,6 +140,59 @@ class TargetMedium(DeletableApiObject, Base): ...@@ -134,6 +140,59 @@ class TargetMedium(DeletableApiObject, Base):
# TODO path # TODO path
return f"{self.process_target_id}.{self.id}" return f"{self.process_target_id}.{self.id}"
@hybrid_method
def is_active(self, usable_check: bool = True, from_lecture: bool = False, from_publish_medium: bool = False, **kwargs):
cond = super().is_active(**kwargs)
if usable_check:
cond &= self.is_produced
if not from_publish_medium and not from_lecture:
cond &= self.hybrid_rel(self.lecture, "is_active", **kwargs)
return cond
@hybrid_method
def has_access(self,
is_mod: bool,
usable_check: bool = True,
visibility_check: bool = True,
from_lecture: bool = False,
from_publish_medium: bool = False,
**kwargs):
cond = super().has_access(
is_mod,
usable_check=usable_check,
from_lecture=from_lecture,
from_publish_medium=from_publish_medium,
**kwargs
)
if not from_publish_medium:
cond &= self.hybrid_rel(self.publish_medium, "has_access", is_mod, visibility_check=visibility_check, **kwargs)
return cond
@classmethod
def load_options(cls, is_mod: bool, to_load: list[RelationshipLoadMarker], from_publish_medium=False, **kwargs) -> list[ExecutableOption]:
options = super().load_options(is_mod, to_load, **kwargs)
# There is no check for from_publish_medium because even though TargetMedium.publish_medium has a simple join,
# an immediateload would cause an extra sql because it is like the list part of a many-to-one relationship
# (although we use it as a one-to-one)
if LOAD_TARGET_MEDIUM_PUBLISH_MEDIUM in to_load:
options.append(orm.joinedload(TargetMedium.publish_medium).options(
*PublishMedium.load_options(is_mod, to_load)
))
if LOAD_TARGET_MEDIUM_LECTURE in to_load:
if (from_publish_medium or LOAD_TARGET_MEDIUM_PUBLISH_MEDIUM) and LOAD_PUBLISH_MEDIUM_LECTURE in to_load:
# Causes no extra sql, just that the back reference is set (relationship has simple join)
# because the lecture will have been loaded by the publish medium
options.append(orm.immediateload(TargetMedium.lecture))
else:
options.append(orm.joinedload(TargetMedium.lecture).options(
*Lecture.load_options(is_mod, to_load)
))
return options
# Note that this usually also includes audio # Note that this usually also includes audio
class PlainVideoTargetMedium(TargetMedium): class PlainVideoTargetMedium(TargetMedium):
...@@ -203,16 +262,24 @@ class PublishMedium(VisibilityApiObject, DeletableApiObject, Base): ...@@ -203,16 +262,24 @@ class PublishMedium(VisibilityApiObject, DeletableApiObject, Base):
back_populates="publish_media", back_populates="publish_media",
lazy="raise_on_sql" lazy="raise_on_sql"
) )
target_medium: Mapped["TargetMedium"] = api_mapped( target_medium: Mapped["TargetMedium"] = relationship(
relationship( primaryjoin=lambda: sql.and_(
primaryjoin=lambda: TargetMedium.id == PublishMedium.target_medium_id, orm.foreign(PublishMedium.target_medium_id) == orm.remote(TargetMedium.id), # foreign is the ForeignKey!
TargetMedium.has_access(is_mod=True, from_publish_medium=True)
),
back_populates="publish_medium", back_populates="publish_medium",
lazy="raise_on_sql" lazy="raise_on_sql"
),
ApiMany2OneRelationshipField(
include_in_data=True
) )
public_target_medium: Mapped["TargetMedium"] = relationship(
primaryjoin=lambda: sql.and_(
orm.foreign(PublishMedium.target_medium_id) == orm.remote(TargetMedium.id), # foreign is the ForeignKey!
TargetMedium.has_access(is_mod=False, from_publish_medium=True)
),
back_populates="publish_medium",
lazy="raise_on_sql",
viewonly=True
) )
__table_args__ = ( __table_args__ = (
sql.Index( sql.Index(
"check_target_medium_unique", "check_target_medium_unique",
...@@ -222,6 +289,51 @@ class PublishMedium(VisibilityApiObject, DeletableApiObject, Base): ...@@ -222,6 +289,51 @@ class PublishMedium(VisibilityApiObject, DeletableApiObject, Base):
), ),
) )
@api_include_in_data(
type_id="target_medium",
data_id="target_medium"
)
def _data_get_target_media(self, is_mod: bool):
return self.target_medium if is_mod else self.public_target_medium
@hybrid_method
def is_active(self, from_lecture: bool = False, **kwargs):
cond = super().is_active(**kwargs)
if not from_lecture:
cond &= self.hybrid_rel(self.lecture, "is_active", **kwargs)
return cond
@hybrid_method
def has_access(self,
is_mod: bool,
visibility_check: bool = True,
from_lecture: bool = False,
**kwargs):
cond = super().has_access(is_mod, visibility_check=visibility_check, from_lecture=from_lecture, **kwargs)
if not from_lecture:
cond &= self.hybrid_rel(self.lecture, "has_access", is_mod, visibility_check=visibility_check, **kwargs)
return cond
@classmethod
def load_options(cls, is_mod: bool, to_load: list[RelationshipLoadMarker], from_lecture=False, **kwargs) -> list[ExecutableOption]:
options = super().load_options(is_mod, to_load, **kwargs)
if from_lecture:
# Causes no extra sql, just that the back reference is set (relationship has simple join)
options.append(orm.immediateload(PublishMedium.lecture))
elif LOAD_PUBLISH_MEDIUM_LECTURE in to_load:
options.append(
orm.joinedload(PublishMedium.lecture).options(*Lecture.load_options(is_mod, to_load))
)
if LOAD_PUBLISH_MEDIUM_TARGET_MEDIUM in to_load:
options.append(
orm.joinedload(PublishMedium.target_medium if is_mod else PublishMedium.public_target_medium).options(
*TargetMedium.load_options(is_mod, to_load, from_publish_medium=True)
))
return options
class MediaProcessTemplate(ApiObject, Base): class MediaProcessTemplate(ApiObject, Base):
......
...@@ -70,6 +70,7 @@ class FeaturedType(Enum): ...@@ -70,6 +70,7 @@ class FeaturedType(Enum):
FEATURED_TYPE_ENUM = create_enum_type(FeaturedType) FEATURED_TYPE_ENUM = create_enum_type(FeaturedType)
LOAD_FEATURED_COURSE_OR_LECTURE = RelationshipLoadMarker()
class Featured(DeletableApiObject, VisibilityApiObject, Base): class Featured(DeletableApiObject, VisibilityApiObject, Base):
...@@ -112,42 +113,21 @@ class Featured(DeletableApiObject, VisibilityApiObject, Base): ...@@ -112,42 +113,21 @@ class Featured(DeletableApiObject, VisibilityApiObject, Base):
) )
@classmethod @classmethod
def select(cls, def load_options(cls, is_mod: bool, to_load: list[RelationshipLoadMarker], **kwargs) -> list[ExecutableOption]:
is_mod: bool, options = super().load_options(is_mod, to_load, **kwargs)
load_course_lecture=False,
**kwargs): if LOAD_FEATURED_COURSE_OR_LECTURE in to_load:
return (
super().select(is_mod, **kwargs)
.options(*cls.load_options(is_mod, load_course_lecture=load_course_lecture))
)
@staticmethod
def load_options(
is_mod: bool,
load_course_lecture=False,
) -> list[ExecutableOption]:
options = []
if load_course_lecture:
options.extend( options.extend(
[ [
orm.selectinload(LectureFeatured.lecture if is_mod else LectureFeatured.public_lecture).options( orm.selectinload(LectureFeatured.lecture if is_mod else LectureFeatured.public_lecture).options(
*Lecture.load_options( *Lecture.load_options(is_mod, to_load)
is_mod,
False,
load_course=True,
load_chapters=True,
load_media=True
)
), ),
orm.selectinload(CourseFeatured.course if is_mod else CourseFeatured.public_course).options( orm.selectinload(CourseFeatured.course if is_mod else CourseFeatured.public_course).options(
*Course.load_options( *Course.load_options(is_mod, to_load)
is_mod,
False
)
) )
] ]
) )
return options return options
......
...@@ -20,8 +20,8 @@ class ViewPermissions: ...@@ -20,8 +20,8 @@ class ViewPermissions:
def __init__(self, def __init__(self,
type: ViewPermissionsType, type: ViewPermissionsType,
rwth_authentication: bool = None, rwth_authentication: bool = False,
fsmpi_authentication: bool = None, fsmpi_authentication: bool = False,
moodle_course_ids: list[int] or None = None, moodle_course_ids: list[int] or None = None,
passwords: dict[str, str] or None = None): passwords: dict[str, str] or None = None):
super().__init__() super().__init__()
...@@ -35,7 +35,9 @@ class ViewPermissions: ...@@ -35,7 +35,9 @@ class ViewPermissions:
if (rwth_authentication and not is_auth) \ if (rwth_authentication and not is_auth) \
or (fsmpi_authentication and not is_auth) \ or (fsmpi_authentication and not is_auth) \
or ((moodle_course_ids is not None) != is_auth) \ or ((moodle_course_ids is not None) != is_auth) \
or ((passwords is not None) != is_auth): or ((passwords is not None) != is_auth) \
or rwth_authentication is None \
or fsmpi_authentication is None:
raise TypeError("Invalid view permissions") raise TypeError("Invalid view permissions")
if is_auth \ if is_auth \
and not rwth_authentication \ and not rwth_authentication \
...@@ -136,8 +138,8 @@ def view_permissions_from_json(json: CJsonValue or JsonTypes) -> "ViewPermission ...@@ -136,8 +138,8 @@ def view_permissions_from_json(json: CJsonValue or JsonTypes) -> "ViewPermission
type = ViewPermissionsType(type_str) type = ViewPermissionsType(type_str)
except ValueError: except ValueError:
raise json.raise_error(f"Unknown type '{truncate_string(type_str)}'") raise json.raise_error(f"Unknown type '{truncate_string(type_str)}'")
rwth_authentication = None rwth_authentication = False
fsmpi_authentication = None fsmpi_authentication = False
moodle_course_ids = None moodle_course_ids = None
passwords = None passwords = None
if type == ViewPermissionsType.AUTHENTICATION: if type == ViewPermissionsType.AUTHENTICATION:
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment