diff --git a/src/api/course.py b/src/api/course.py index 56473d4c3e95ab609acb6f89440dd40cbbb81656..56b0a223973ecce4e8313a9e3ec40f31200e92a9 100644 --- a/src/api/course.py +++ b/src/api/course.py @@ -24,9 +24,9 @@ COURSE_SECONDARY_DB_SELECTION = f""" def _semester_db_to_json(semester_db: str): - if semester_db is None or semester_db.isspace(): + if semester_db is None or len(semester_db) == 0 or semester_db.isspace(): return "none" # pragma: no cover - from api.objects.types import SEMESTER_STRING_PATTERN + from api.objects.type import SEMESTER_STRING_PATTERN if SEMESTER_STRING_PATTERN.fullmatch(semester_db) is None: # pragma: no cover print(f"Warning: Invalid semester string in database: {truncate_string(semester_db)}") return "none" diff --git a/src/api/objects/changelog_v1.py b/src/api/objects/changelog_v1.py index 5f24db19b0aa6865915f9f7c134507f22a7206ca..6e5a11320b60ea3e4e045f7a58a7e4a392d006b3 100644 --- a/src/api/objects/changelog_v1.py +++ b/src/api/objects/changelog_v1.py @@ -6,6 +6,7 @@ from api.objects.changelog import ChangelogVersionBuilder, ChangelogEntryError, from api.objects.changelog_entries import ModificationEntry, DeletionEntry from api.objects.field import ObjectField from api.objects.objects import * +from api.objects.type import SEMESTER_STRING_PATTERN def _encode_identity(value_json: str) -> str: @@ -86,11 +87,21 @@ def _decode_legacy_string_to_id_string(legacy_value: str) -> str: def _decode_legacy_string_to_semester_string(legacy_value: str) -> str: if legacy_value is None: raise ChangelogEntryError("Cannot encode null values") + if len(legacy_value) == 0 or legacy_value.isspace(): + return "none" if len(legacy_value) > 100 or SEMESTER_STRING_PATTERN.fullmatch(legacy_value) is None: raise ChangelogEntryError(f"Legacy string is not a semester string: '{truncate_string(legacy_value)}'") return legacy_value +def _encode_legacy_semester_string(value_json: str) -> str: + if value_json is None: + raise ChangelogEntryError("Cannot encode null values") + if value_json == "none": + return "" + return value_json + + def _decode_legacy_announcement_level(legacy_value: str) -> str: level = _decode_legacy_integer_non_negative(legacy_value) match level: @@ -164,7 +175,7 @@ class _DeletionEntryCodec(ChangelogEntryCodec[SimpleChangelogEntrySubject]): _CODEC_BOOLEAN = _LegacyModificationEntryCodec(_decode_legacy_boolean, _encode_legacy_boolean) _CODEC_STRING_SHORT = _LegacyModificationEntryCodec(_decode_legacy_shortstring_to_string_short, _encode_identity) _CODEC_STRING_TO_ID_STRING = _LegacyModificationEntryCodec(_decode_legacy_string_to_id_string, _encode_identity) -_CODEC_STRING_TO_SEMESTER_STRING = _LegacyModificationEntryCodec(_decode_legacy_string_to_semester_string, _encode_identity) +_CODEC_STRING_TO_SEMESTER_STRING = _LegacyModificationEntryCodec(_decode_legacy_string_to_semester_string, _encode_legacy_semester_string) _CODEC_TEXT_TO_STRING_LONG = _LegacyModificationEntryCodec(_decode_legacy_text_to_string_long, _encode_identity) _CODEC_DATETIME = _LegacyModificationEntryCodec(_decode_legacy_datetime, _encode_legacy_datetime) _CODEC_INTEGER_NON_NEGATIVE = _LegacyModificationEntryCodec(_decode_legacy_integer_non_negative, _encode_legacy_integer) diff --git a/src/api/objects/type.py b/src/api/objects/type.py index ccea4051a3aa9f41716e95cacccb7cd6965a1524..0d0fd9cdd4cc0c6944ec04d8fa2ead4f53dd8b17 100644 --- a/src/api/objects/type.py +++ b/src/api/objects/type.py @@ -308,6 +308,39 @@ class MappedStringType(AbstractStringType): return self.__json_to_db[string] +SEMESTER_STRING_REGEX = "([0-9]{4}(ws|ss)|none)" +SEMESTER_STRING_PATTERN = re.compile(SEMESTER_STRING_REGEX) + + +class SemesterStringType(AbstractStringType): + + def __init__(self): + super().__init__("semester_string", False) + + def get_max_string_length(self) -> int: + """Returns the exact maximum length of this string""" + return 6 + + def db_value_to_json(self, db_value: str) -> str: + if len(db_value) == 0 or db_value.isspace(): + return "none" + if SEMESTER_STRING_PATTERN.fullmatch(db_value) is None: + print(f"Warning: Invalid semester string in database: {truncate_string(db_value)}") + return "none" + return db_value + + def validate_and_convert_client_value(self, + transaction: AbstractTransaction, + object_id: int or None, + client_value: CJsonValue) -> str or None: + value = client_value.as_string(50, 0) + if SEMESTER_STRING_PATTERN.fullmatch(value) is None: + client_value.raise_error("String does not match pattern") + if value == "none": + value = "" + return value + + class DatetimeType(AbstractStringType): def __init__(self, may_be_none: bool = False): diff --git a/src/api/objects/types.py b/src/api/objects/types.py index 9dbe2d43e16701af763180c36f27d3995b211652..62faf7954713543ab4463110aacaf14661ab0c1f 100644 --- a/src/api/objects/types.py +++ b/src/api/objects/types.py @@ -1,14 +1,10 @@ -import re from api.miscellaneous import ID_STRING_REGEX_NO_LENGTH -from api.objects.type import (NumberType, IntType, LongType, +from api.objects.type import (IntType, LongType, SimpleType, ObjectIdType, - StringType, SimpleStringType, UniqueStringType, MappedStringType, + StringType, SimpleStringType, UniqueStringType, MappedStringType, SemesterStringType, DatetimeType) -SEMESTER_STRING_REGEX = "([0-9]{4}(ws|ss)|none)" -SEMESTER_STRING_PATTERN = re.compile(SEMESTER_STRING_REGEX) - TYPE_INT = IntType() TYPE_LONG = LongType() TYPE_BOOLEAN = SimpleType("boolean", lambda db: bool(db), lambda json_value: json_value.as_bool()) @@ -24,7 +20,7 @@ TYPE_COURSE_ID_STRING = UniqueStringType( "id", "handle" ) -TYPE_SEMESTER_STRING = SimpleStringType("semester_string", 6, 0, SEMESTER_STRING_REGEX) # Max length for safety +TYPE_SEMESTER_STRING = SemesterStringType() TYPE_DURATION = IntType(0, 2147483647) TYPE_DATETIME = DatetimeType() TYPE_TIMESTAMP = IntType(0, 2147483647)