1313from typing import Any
1414
1515from django .db .models import Avg , Count
16+ from django .utils .text import slugify
1617
1718from marketplace .models import Review , ServiceCategory , ServiceProvider
1819
@@ -33,6 +34,14 @@ class CategoryNotFound(MarketplaceError):
3334 """Raised when a write references an unknown category slug."""
3435
3536
37+ class InvalidCategoryName (MarketplaceError ):
38+ """Raised when a suggested category name fails validation."""
39+
40+
41+ _CATEGORY_NAME_MIN = 2
42+ _CATEGORY_NAME_MAX = 80
43+
44+
3645# --------------------------------------------------------------------------- #
3746# Serialization
3847# --------------------------------------------------------------------------- #
@@ -43,6 +52,7 @@ def serialize_category(category: ServiceCategory) -> dict[str, Any]:
4352 'name' : category .name ,
4453 'slug' : category .slug ,
4554 'icon' : category .icon ,
55+ 'userSuggested' : category .user_suggested ,
4656 }
4757
4858
@@ -97,6 +107,48 @@ def _resolve_category(slug: str) -> ServiceCategory:
97107 raise CategoryNotFound (slug ) from exc
98108
99109
110+ def _normalize_category_name (name : str ) -> str :
111+ cleaned = ' ' .join (name .split ())
112+ if len (cleaned ) < _CATEGORY_NAME_MIN or len (cleaned ) > _CATEGORY_NAME_MAX :
113+ raise InvalidCategoryName ('length' )
114+ if not any (ch .isalpha () for ch in cleaned ):
115+ raise InvalidCategoryName ('letters' )
116+ slug = slugify (cleaned )[: _CATEGORY_NAME_MAX ]
117+ if not slug :
118+ raise InvalidCategoryName ('slug' )
119+ return cleaned
120+
121+
122+ def get_or_create_category_by_name (* , island , name : str ) -> ServiceCategory :
123+ """Resolve an existing category or create a user-suggested one."""
124+ cleaned = _normalize_category_name (name )
125+ slug = slugify (cleaned )[: _CATEGORY_NAME_MAX ]
126+ existing = ServiceCategory .objects .filter (island = island , slug = slug ).first ()
127+ if existing :
128+ return existing
129+ existing = ServiceCategory .objects .filter (island = island , name__iexact = cleaned ).first ()
130+ if existing :
131+ return existing
132+ return ServiceCategory .objects .create (
133+ island = island ,
134+ name = cleaned ,
135+ slug = slug ,
136+ user_suggested = True ,
137+ )
138+
139+
140+ def _resolve_category_from_write (* , island , data : dict [str , Any ]) -> ServiceCategory :
141+ slug = (data .get ('category_slug' ) or '' ).strip ()
142+ name = (data .get ('category_name' ) or '' ).strip ()
143+ if slug and name :
144+ raise InvalidCategoryName ('both' )
145+ if slug :
146+ return _resolve_category (slug )
147+ if name :
148+ return get_or_create_category_by_name (island = island , name = name )
149+ raise CategoryNotFound ('' )
150+
151+
100152# --------------------------------------------------------------------------- #
101153# Providers — reads
102154# --------------------------------------------------------------------------- #
@@ -180,15 +232,20 @@ def get_provider(
180232
181233
182234def _apply_provider_fields (provider : ServiceProvider , data : dict [str , Any ]) -> None :
183- if 'category_slug' in data and data ['category_slug' ]:
184- provider .category = _resolve_category (data ['category_slug' ])
235+ slug = (data .get ('category_slug' ) or '' ).strip () if 'category_slug' in data else ''
236+ name = (data .get ('category_name' ) or '' ).strip () if 'category_name' in data else ''
237+ if slug or name :
238+ provider .category = _resolve_category_from_write (
239+ island = provider .island ,
240+ data = {'category_slug' : slug , 'category_name' : name },
241+ )
185242 for field in _PROVIDER_WRITE_FIELDS :
186243 if field in data :
187244 setattr (provider , field , data [field ])
188245
189246
190247def create_provider (* , island , session_hash : str , data : dict [str , Any ]) -> dict [str , Any ]:
191- category = _resolve_category ( data . get ( 'category_slug' , '' ) )
248+ category = _resolve_category_from_write ( island = island , data = data )
192249 provider = ServiceProvider (
193250 island = island ,
194251 category = category ,
0 commit comments