1- from typing import AbstractSet , Any , Dict , List , Optional , Sequence , cast
1+ from typing import AbstractSet , Any , Dict , List , Mapping , Optional , Sequence , cast
22
33from flask import current_app
44from marshmallow import Schema , ValidationError , fields , utils , validate , validates_schema
55from marshmallow .schema import SchemaMeta , SchemaOpts
66from marshmallow_sqlalchemy import SQLAlchemyAutoSchema , auto_field
77from sqlalchemy .orm import Session
8+
89from api .access_config import get_access_config
910from api .extensions import db
1011from api .models import (
2526access_config = get_access_config ()
2627
2728
29+ def context_aware_description_field () -> fields .Field :
30+ """
31+ Returns a context-aware description field that reads REQUIRE_DESCRIPTIONS
32+ from Flask app config at validation time instead of module import time.
33+
34+ This allows tests to parametrize the REQUIRE_DESCRIPTIONS setting without
35+ needing separate test environments or module reloading.
36+ """
37+
38+ class ContextAwareDescriptionField (fields .String ):
39+ def deserialize (
40+ self , value : Any , attr : Optional [str ] = None , data : Optional [Mapping [str , Any ]] = None , ** kwargs : Any
41+ ) -> Any :
42+ # Read config at deserialization time (when processing request data)
43+ require_descriptions = current_app .config .get ("REQUIRE_DESCRIPTIONS" , False )
44+
45+ # Check if field was provided in the input data
46+ field_was_provided = data is not None and attr is not None and attr in data
47+
48+ # If field wasn't provided and descriptions are required, raise error
49+ if not field_was_provided and require_descriptions :
50+ raise ValidationError ("Description is required." )
51+
52+ # If field wasn't provided and descriptions are not required, return empty string
53+ if not field_was_provided :
54+ return ""
55+
56+ # Field was provided, validate it
57+ if value == "" and require_descriptions :
58+ raise ValidationError ("Description must be between 1 and 1024 characters" )
59+
60+ # Use parent deserialization for type conversion
61+ if value is None or value == "" :
62+ return "" if not require_descriptions else self .fail ("required" )
63+
64+ result = super ().deserialize (value , attr , data , ** kwargs )
65+
66+ # Validate length
67+ if result and len (result ) > 1024 :
68+ raise ValidationError ("Description must be 1024 characters or less" )
69+
70+ return result
71+
72+ return ContextAwareDescriptionField (allow_none = True , load_default = "" , dump_default = "" )
73+
74+
2875# See https://stackoverflow.com/a/58646612
2976class OktaUserGroupMemberSchema (SQLAlchemyAutoSchema ):
3077 group = fields .Nested (lambda : PolymorphicGroupSchema )
@@ -255,7 +302,7 @@ class OktaGroupSchema(SQLAlchemyAutoSchema):
255302 ),
256303 ),
257304 )
258- description = auto_field ( load_default = "" , validate = validate . Length ( max = 1024 ) )
305+ description = context_aware_description_field ( )
259306
260307 externally_managed_data = fields .Dict ()
261308
@@ -628,7 +675,7 @@ class RoleGroupSchema(SQLAlchemyAutoSchema):
628675 ),
629676 ),
630677 )
631- description = auto_field ( load_default = "" , validate = validate . Length ( max = 1024 ) )
678+ description = context_aware_description_field ( )
632679
633680 externally_managed_data = fields .Dict ()
634681
@@ -847,7 +894,7 @@ class AppGroupSchema(SQLAlchemyAutoSchema):
847894 ),
848895 ),
849896 )
850- description = auto_field ( load_default = "" , validate = validate . Length ( max = 1024 ) )
897+ description = context_aware_description_field ( )
851898
852899 externally_managed_data = fields .Dict ()
853900
@@ -1138,7 +1185,7 @@ class InitialAppGroupSchema(Schema):
11381185 ),
11391186 ),
11401187 )
1141- description = fields . String ( load_default = "" , validate = validate . Length ( max = 1024 ) )
1188+ description = context_aware_description_field ( )
11421189
11431190
11441191class AppSchema (SQLAlchemyAutoSchema ):
@@ -1152,7 +1199,7 @@ class AppSchema(SQLAlchemyAutoSchema):
11521199 ),
11531200 ),
11541201 )
1155- description = auto_field ( validate = validate . Length ( max = 1024 ) )
1202+ description = context_aware_description_field ( )
11561203
11571204 initial_owner_id = fields .String (validate = validate .Length (min = 1 , max = 255 ), load_only = True )
11581205 initial_owner_role_ids = fields .List (fields .String (validate = validate .Length (equal = 20 )), load_only = True )
@@ -1282,7 +1329,7 @@ def load(
12821329 exclude = self ._polymorphic_fields_intersection (group_class , self .exclude ),
12831330 load_only = self ._polymorphic_fields_intersection (group_class , self .load_only ),
12841331 dump_only = self ._polymorphic_fields_intersection (group_class , self .dump_only ),
1285- ).load (data , session = session , instance = instance , transient = transient )
1332+ ).load (data , session = session , instance = instance , transient = transient , ** kwargs )
12861333 else :
12871334 raise ValidationError (f"Unexpected group type, expecting one of { self .TYPE_TO_GROUP_SCHEMA_MAP .keys ()} " )
12881335 raise ValidationError (f"Unable to validate with: { self .TYPE_TO_GROUP_SCHEMA_MAP } " )
@@ -1466,7 +1513,7 @@ class TagSchema(SQLAlchemyAutoSchema):
14661513 ),
14671514 ),
14681515 )
1469- description = auto_field ( load_default = "" , validate = validate . Length ( max = 1024 ) )
1516+ description = context_aware_description_field ( )
14701517
14711518 def validate_constraints (value ) -> bool :
14721519 if not isinstance (value , dict ):
0 commit comments