From 594794e01ddfe4eaec7edde46955d30e67e32e1a Mon Sep 17 00:00:00 2001 From: Paul Balluff Date: Sat, 15 Jun 2024 22:22:10 +0200 Subject: [PATCH] #62 true support for Facets in List predicates --- meteor/api/sanitizer.py | 7 +-- meteor/flaskdgraph/dgraph_types.py | 69 ++++++++++++++++++++++++++++-- meteor/main/model.py | 3 +- tests/test_api_sanitizer.py | 20 ++++++--- 4 files changed, 82 insertions(+), 17 deletions(-) diff --git a/meteor/api/sanitizer.py b/meteor/api/sanitizer.py index 39c2eb6..98da8ac 100644 --- a/meteor/api/sanitizer.py +++ b/meteor/api/sanitizer.py @@ -229,12 +229,7 @@ def _preprocess_facets(self): self.facets[predicate].update({facet: self.data[key]}) else: self.facets[predicate] = {facet: self.data[key]} - - # for list predicates, we track facets via the value - if '@' in key: - val, facet = key.split('@') - self.facets[val] = {facet: self.data[key]} - + def _postprocess_list_facets(self): for ll in self.entry.values(): if isinstance(ll, list): diff --git a/meteor/flaskdgraph/dgraph_types.py b/meteor/flaskdgraph/dgraph_types.py index 301032b..d6e85da 100644 --- a/meteor/flaskdgraph/dgraph_types.py +++ b/meteor/flaskdgraph/dgraph_types.py @@ -455,12 +455,28 @@ def validate(self, data, facets: dict=None, **kwargs): # preferably this method should return a Scalar object if not self.overwrite and data is None: - raise InventoryValidationError(f"Tried to delete predicate <{self.label}> by supplying null value.") + raise InventoryValidationError(f"Tried to delete predicate <{self.predicate}> by supplying null value.") data = self.validation_hook(data) if isinstance(data, (list, set, tuple)): - return [Scalar(item, facets=facets) if isinstance(item, (str, int, datetime.datetime, datetime.date)) else item for item in data] + _data = [] + for item in data: + if isinstance(item, (str, int, datetime.datetime, datetime.date)): + _data.append(Scalar(item)) + else: + _data.append(item) + + if facets: + assert isinstance(facets, dict), InventoryValidationError(f"Error in <{self.predicate}>: Facets provided in wrong format!") + for key, val in facets.items(): + assert isinstance(val, dict), InventoryValidationError(f"Error in <{self.predicate}>: Facets provided in wrong format!") + for counter, subval in val.items(): + _data[int(counter)].update_facets({key: subval}) + + return _data + + # return [Scalar(item, facets=facets) if isinstance(item, (str, int, datetime.datetime, datetime.date)) else item for item in data] elif isinstance(data, (str, int, datetime.datetime, datetime.date)): return Scalar(data, facets=facets) else: @@ -1609,6 +1625,51 @@ def openapi_component(self) -> dict: o['description'] = self.description return o + +class ListDatetime(DateTime): + + dgraph_predicate_type = "[datetime]" + _type = list + + def validation_hook(self, data): + if data is None: + return None + if isinstance(data, (list, tuple, set)): + _data = [] + for val in data: + try: + _data.append(dateparser.parse(val)) + except: + raise InventoryValidationError( + f'Error in <{self.predicate}> Cannot parse provided value to date: {data}') + return _data + if isinstance(data, (datetime.date, datetime.datetime)): + return data + elif isinstance(data, int): + try: + return datetime.date(year=data, month=1, day=1) + except: + pass + try: + return dateparser.parse(data) + except: + raise InventoryValidationError( + f'Error in <{self.predicate}> Cannot parse provided value to date: {data}') + + + @property + def openapi_component(self) -> dict: + o = {'type': "array", + 'items': { + 'type': 'string', + 'format': "date-time" + } + } + if self.description: + o['description'] = self.description + return o + + class Year(DateTime): dgraph_predicate_type = 'datetime' @@ -1622,7 +1683,7 @@ def validation_hook(self, data): return datetime.datetime(year=int(data), month=1, day=1) except: raise InventoryValidationError( - f'Cannot parse provided value to year: {data}') + f'Error in <{self.predicate}>: Cannot parse provided value to year: {data}') @property def wtf_field(self) -> IntegerField: @@ -1694,7 +1755,7 @@ def validation_hook(self, data): return data > 0 else: raise InventoryValidationError( - f'Cannot evaluate provided value as bool: {data}!') + f'Error in <{self.predicate}>: Cannot evaluate provided value as bool: {data}!') def query_filter(self, vals, **kwargs) -> str: if isinstance(vals, list): diff --git a/meteor/main/model.py b/meteor/main/model.py index dfb480f..0d81366 100644 --- a/meteor/main/model.py +++ b/meteor/main/model.py @@ -553,8 +553,7 @@ class NewsSource(Entry): queryable=True, ) - audience_size = ListYear( - edit=False, + audience_size = ListDatetime( facets=[ Facet( "unit", diff --git a/tests/test_api_sanitizer.py b/tests/test_api_sanitizer.py index 9a94643..e482103 100644 --- a/tests/test_api_sanitizer.py +++ b/tests/test_api_sanitizer.py @@ -99,10 +99,11 @@ def test_new_entry(self): def test_list_facets(self): mock_data = { 'name': 'Test', - 'alternate_names': 'Jay Jay,Jules,JB', - 'Jay Jay@kind': 'first', - 'Jules@kind': 'official', - 'JB@kind': 'CS-GO' + 'alternate_names': ['Jay Jay' , 'Jules', 'JB'], + 'alternate_names|kind': {"0": 'first', + "1": 'official', + "2": "CS-GO" + } } with self.app.app_context(): @@ -298,7 +299,16 @@ def test_draft_source(self): 'contains_ads': 'non subscribers', 'publishes_org': self.derstandard_mbh_uid, 'related_news_sources': [self.falter_print_uid], - 'entry_review_status': 'pending'} + 'entry_review_status': 'pending', + 'audience_size': ["2024-06-15T21:20:13", + "2023-05-14", + "2022-04"], + 'audience_size|unit': {"0": "subscribers", + "1": "subscribers", + "2": "subscribers"}, + 'audience_size|count': {"0": 100, + "1": 200, + "2": 300}} with self.app.app_context(): # test if random user can edit