diff --git a/src/videoag_common/database/drift_detector.py b/src/videoag_common/database/drift_detector.py index 059af8e341391bcac8266be7b9663e517707e007..4025e774e65045241cf9781e4fefd2c6559a9461 100644 --- a/src/videoag_common/database/drift_detector.py +++ b/src/videoag_common/database/drift_detector.py @@ -96,6 +96,10 @@ def _check_constraint_equal(actual_constraint: Constraint, schema_constraint: Co and actual_constraint.name != schema_constraint.name): return False + if actual_constraint.deferrable != schema_constraint.deferrable: + # TODO add to to_string + return False + if not isinstance(schema_constraint, ColumnCollectionConstraint): print(f"Unknown type of constraint {schema_constraint}. Can't compare") return False @@ -113,13 +117,19 @@ def _check_constraint_equal(actual_constraint: Constraint, schema_constraint: Co if not isinstance(actual_constraint, CheckConstraint): return False - # We try our best to check if they are equal + # We try our best to check if they are equal, but with things like 'x IN ()' it breaks (because the db + # transforms the statements) if schema_constraint.sqltext == actual_constraint.sqltext: return True # Sometimes the db returns it without correct brackets. Just strip them if str(schema_constraint.sqltext).strip("()") == str(actual_constraint.sqltext).strip("()"): return True - return False + + print(f"Warning: Can't compare schema clause of constraint '{schema_constraint.name}'\n" + f" {schema_constraint.sqltext}\n" + f" with clause in database\n" + f" {actual_constraint.sqltext}") + return True case ForeignKeyConstraint(): assert isinstance(schema_constraint, ForeignKeyConstraint) # For pycharm diff --git a/src/videoag_common/media_process/__init__.py b/src/videoag_common/media_process/__init__.py index d45dacf16076a7a593e208846b819ce9caaef93d..1d4cff64f5fe3e0fbdecac0c41f1d516e74a0526 100644 --- a/src/videoag_common/media_process/__init__.py +++ b/src/videoag_common/media_process/__init__.py @@ -5,4 +5,4 @@ from .basic_targets import ( SourceFileTargetProducer, DownscaleVideoTarget, ) -from .media_process import MediaProcess, ApiMediaProcessField +from .media_process import MediaProcess, ApiMediaProcessField, get_permanent_lecture_dir diff --git a/src/videoag_common/media_process/media_process.py b/src/videoag_common/media_process/media_process.py index 88ed5aa590696baaacf15bb0824a0c33c2ffb104..1909220352796ffa4c11504a620998a9a9e354a7 100644 --- a/src/videoag_common/media_process/media_process.py +++ b/src/videoag_common/media_process/media_process.py @@ -3,12 +3,29 @@ from collections.abc import Iterable from videoag_common.miscellaneous import * from videoag_common.database import * from videoag_common.api_object import ApiSimpleColumnField, FieldContext +import videoag_common from .target import TargetProducer, TARGET_ID_PATTERN, TARGET_ID_MAX_LENGTH # # Important: Json default values should be avoided in case the default value is changed and an old process relied on the default # +_PERMANENT_MEDIA_DIR_NAME = videoag_common.config["PERMANENT_MEDIA_DIR"] +if _PERMANENT_MEDIA_DIR_NAME.endswith("/"): + raise ValueError("PERMANENT_MEDIA_DIR must NOT end with /") + + +# noinspection PyUnresolvedReferences +def get_permanent_lecture_dir(lecture: "Lecture") -> str: + """ + Returns the db path for the directory of the given lecture. Does NOT end with a / + """ + return ( + f"{_PERMANENT_MEDIA_DIR_NAME}/" + f"course-{lecture.course.id}.{lecture.course.handle}/" + f"lecture-{lecture.id}.{lecture.time.strftime("%y%m%d")}" + ) + class MediaProcess: diff --git a/src/videoag_common/miscellaneous/json.py b/src/videoag_common/miscellaneous/json.py index 8895662ae8a52f46ea284f37327a5be97b565a8a..23f66fcce585f7ed4b01c460331bfe688ce0c9ea 100644 --- a/src/videoag_common/miscellaneous/json.py +++ b/src/videoag_common/miscellaneous/json.py @@ -186,7 +186,7 @@ def replace_json_arguments(data: JsonTypes, argument_data: JsonTypes) -> JsonTyp if not isinstance(data, str): raise TypeError(f"Unknown json type: {type(data)}") - def _sub_match(match): + def _get_match_value(match, force_string: bool = True): path = match.group(1) val = argument_data for path_ele in path.split("."): @@ -194,7 +194,16 @@ def replace_json_arguments(data: JsonTypes, argument_data: JsonTypes) -> JsonTyp raise ValueError(f"Can't replace with path '{path}' because element '{path_ele}' is not present " f"(or parent has bad type) in argument data") val = val[path_ele] + if force_string: + if isinstance(val, str) or isinstance(val, int): + val = str(val) + else: + raise ValueError(f"Can't replace with path '{path}' because result is not a str or int") return val - return _JSON_ARGUMENT_PATTERN.sub(_sub_match, data) + full_match = _JSON_ARGUMENT_PATTERN.fullmatch(data) + if full_match is not None: + return _get_match_value(full_match, force_string=False) + + return _JSON_ARGUMENT_PATTERN.sub(_get_match_value, data) diff --git a/src/videoag_common/objects/course.py b/src/videoag_common/objects/course.py index 0117ae3d798936ff9ae036cd0de1bebe74c52b95..09b7fb05d9c35a41ec7b4dd96475bf455f33dc96 100644 --- a/src/videoag_common/objects/course.py +++ b/src/videoag_common/objects/course.py @@ -172,7 +172,7 @@ class Lecture(DeletableApiObject, VisibilityApiObject, ApiViewPermissionsObject, from .medium import TargetMedium return sql.and_( TargetMedium.lecture_id == Lecture.id, - TargetMedium.is_active(from_lecture=True) + TargetMedium.is_active(usable_check=False, from_lecture=True) ) @staticmethod diff --git a/src/videoag_common/objects/medium.py b/src/videoag_common/objects/medium.py index 497bdd0db03697828ac3f12ea0bf8a331e56afdc..9e1160fed89917b5af63600f834af8f74af03149 100644 --- a/src/videoag_common/objects/medium.py +++ b/src/videoag_common/objects/medium.py @@ -1,6 +1,8 @@ from datetime import datetime from enum import Enum +from pathlib import Path +import videoag_common from videoag_common.database import * from videoag_common.miscellaneous import * from videoag_common.api_object import * @@ -16,6 +18,11 @@ LOAD_TARGET_MEDIUM_PUBLISH_MEDIUM = RelationshipLoadMarker() LOAD_TARGET_MEDIUM_LECTURE = RelationshipLoadMarker() +_API_BASE_URL = videoag_common.config["API_BASE_URL"] +if _API_BASE_URL.endswith("/"): + raise ValueError("API_BASE_URL must NOT end with /") + + # A file which was uploaded and needs to be assigned to a lecture, to be converted to a SourceFileTargetMedium class SourceMedium(DeletableApiObject, Base): __table_args__ = ( @@ -116,6 +123,42 @@ class TargetMedium(DeletableApiObject, Base): "polymorphic_on": "type", "with_polymorphic": "*" # Always load all attributes for all types } + # This isn't pretty. With a joined table inheritance it might be a bit nicer, but we couldn't check is_produced + # Also with a joined table inheritance we would have much more columns (since here we can reuse columns for different types) + __table_args__ = ( + CheckConstraint( + f"type NOT IN ('plain_video', 'plain_audio') OR NOT is_produced OR duration_sec IS NOT NULL", + name="check_duration_sec_not_null" + ), + CheckConstraint( + f"type NOT IN ('plain_video', 'plain_audio', 'thumbnail') OR NOT is_produced OR file_path IS NOT NULL", + name="check_file_path_not_null" + ), + CheckConstraint( + f"type NOT IN ('plain_video', 'plain_audio') OR NOT is_produced OR audio_sample_rate IS NOT NULL", + name="check_audio_sample_rate_not_null" + ), + CheckConstraint( + f"type NOT IN ('plain_video', 'plain_audio') OR NOT is_produced OR audio_channel_count IS NOT NULL", + name="check_audio_channel_count_not_null" + ), + CheckConstraint( + f"type NOT IN ('plain_video') OR NOT is_produced OR video_vertical_resolution IS NOT NULL", + name="check_video_vertical_resolution_not_null" + ), + CheckConstraint( + f"type NOT IN ('plain_video') OR NOT is_produced OR video_horizontal_resolution IS NOT NULL", + name="check_video_horizontal_resolution_not_null" + ), + CheckConstraint( + f"type NOT IN ('plain_video') OR NOT is_produced OR video_frame_rate_numerator IS NOT NULL", + name="check_video_frame_rate_numerator_not_null" + ), + CheckConstraint( + f"type NOT IN ('plain_video') OR NOT is_produced OR video_frame_rate_denominator IS NOT NULL", + name="check_video_frame_rate_denominator_not_null" + ), + ) # Note that these four may NOT be unique. E.g. if one output is generated twice (because another output of the same # producer was deleted, etc.) @@ -210,13 +253,11 @@ class FileMedium(ApiObject): data_notes="URL to the mediums's file (Maybe with a redirect)" ) def url(self) -> str: - # This is not too nice and only works from the API, but we need to access the API config - import api - return f"{api.config['API_BASE_URL']}/resources/target_medium/{self.id}" + return f"{_API_BASE_URL}/resources/target_medium/{self.id}" def get_default_file_path_no_ending(self): - # TODO path - return f"{self.process_target_id}.{self.id}" + assert isinstance(self, TargetMedium) + return f"{get_permanent_lecture_dir(self.lecture)}/target-{self.id}.{self.process_target_id}" class SingleAudioContainingMedium(ApiObject):