class VocabulariesService(BaseService): system_keys = set(DEFAULT_SCHEMA.keys()).union(set(DEFAULT_EDITOR.keys())) def on_create(self, docs): for doc in docs: if doc.get('field_type') and doc['_id'] in self.system_keys: raise SuperdeskApiError(message='{} is in use'.format( doc['_id']), payload={'_id': { 'conflict': 1 }}) def on_replace(self, document, original): document[app.config['LAST_UPDATED']] = utcnow() document[app.config['DATE_CREATED']] = original[ app.config['DATE_CREATED']] if original else utcnow() logger.info("updating vocabulary item: %s", document["_id"]) def on_fetched(self, doc): """Overriding to filter out inactive vocabularies and pops out 'is_active' property from the response. It keeps it when requested for manageable vocabularies. """ if request and hasattr(request, 'args') and request.args.get('where'): where_clause = json.loads(request.args.get('where')) if where_clause.get('type') == 'manageable': return doc for item in doc[config.ITEMS]: self._filter_inactive_vocabularies(item) self._cast_items(item) def on_fetched_item(self, doc): """ Overriding to filter out inactive vocabularies and pops out 'is_active' property from the response. """ self._filter_inactive_vocabularies(doc) self._cast_items(doc) def on_update(self, updates, original): """Checks the duplicates if a unique field is defined""" unique_field = original.get('unique_field') if unique_field: self._check_uniqueness(updates.get('items', []), unique_field) def on_updated(self, updates, original): """ Overriding this to send notification about the replacement """ self._send_notification(original) def on_replaced(self, document, original): """ Overriding this to send notification about the replacement """ self._send_notification(document) def _check_uniqueness(self, items, unique_field): """Checks the uniqueness if a unique field is defined :param items: list of items to check for uniqueness :param unique_field: name of the unique field """ unique_values = [] for item in items: # compare only the active items if not item.get('is_active'): continue if not item.get(unique_field): raise SuperdeskApiError.badRequestError( "{} cannot be empty".format(unique_field)) unique_value = str(item.get(unique_field)).upper() if unique_value in unique_values: raise SuperdeskApiError.badRequestError( "Value {} for field {} is not unique".format( item.get(unique_field), unique_field)) unique_values.append(unique_value) def _filter_inactive_vocabularies(self, item): vocs = item['items'] active_vocs = ({k: voc[k] for k in voc.keys() if k != 'is_active'} for voc in vocs if voc.get('is_active', True)) item['items'] = list(active_vocs) def _cast_items(self, vocab): """Cast values in vocabulary items using predefined schema. :param vocab """ schema = vocab_schema.get(vocab.get('_id'), {}) for item in vocab.get('items', []): for field, field_schema in schema.items(): if field in item: item[field] = serialize_value(field_schema['type'], item[field]) def _send_notification(self, updated_vocabulary): """ Sends notification about the updated vocabulary to all the connected clients. """ user = get_user_from_request() push_notification('vocabularies:updated', vocabulary=updated_vocabulary.get('display_name'), user=str(user[config.ID_FIELD]) if user else None) def get_rightsinfo(self, item): rights_key = item.get('source', item.get('original_source', 'default')) all_rights = self.find_one(req=None, _id='rightsinfo') if not all_rights or not all_rights.get('items'): return {} try: default_rights = next(info for info in all_rights['items'] if info['name'] == 'default') except StopIteration: default_rights = None try: rights = next(info for info in all_rights['items'] if info['name'] == rights_key) except StopIteration: rights = default_rights if rights: return { 'copyrightholder': rights.get('copyrightHolder'), 'copyrightnotice': rights.get('copyrightNotice'), 'usageterms': rights.get('usageTerms'), } else: return {}
class VocabulariesService(BaseService): system_keys = set(DEFAULT_SCHEMA.keys()).union(set(DEFAULT_EDITOR.keys())) def _validate_items(self, update): # if we have qcode and not unique_field set, we want it to be qcode try: update['schema']['qcode'] except KeyError: pass else: update.setdefault('unique_field', 'qcode') unique_field = update.get('unique_field') if 'schema' in update and 'items' in update: for index, item in enumerate(update['items']): for field, desc in update.get('schema', {}).items(): if ((desc.get('required', False) or unique_field == field) and (field not in item or not item[field])): raise SuperdeskApiError.badRequestError('Required ' + field + ' in item ' + str(index)) def on_create(self, docs): for doc in docs: self._validate_items(doc) if doc.get('field_type') and doc['_id'] in self.system_keys: raise SuperdeskApiError(message='{} is in use'.format( doc['_id']), payload={'_id': { 'conflict': 1 }}) def on_created(self, docs): for doc in docs: self._send_notification(doc, event='vocabularies:created') def on_replace(self, document, original): self._validate_items(document) document[app.config['LAST_UPDATED']] = utcnow() document[app.config['DATE_CREATED']] = original.get( app.config['DATE_CREATED'], utcnow()) if original else utcnow() logger.info("updating vocabulary item: %s", document["_id"]) def on_fetched(self, doc): """Overriding to filter out inactive vocabularies and pops out 'is_active' property from the response. It keeps it when requested for manageable vocabularies. """ if request and hasattr(request, 'args') and request.args.get('where'): where_clause = json.loads(request.args.get('where')) if where_clause.get('type') == 'manageable': return doc for item in doc[config.ITEMS]: self._filter_inactive_vocabularies(item) self._cast_items(item) def on_fetched_item(self, doc): """ Overriding to filter out inactive vocabularies and pops out 'is_active' property from the response. """ self._filter_inactive_vocabularies(doc) self._cast_items(doc) def on_update(self, updates, original): """Checks the duplicates if a unique field is defined""" if 'items' in updates: updated = deepcopy(original) updated.update(updates) self._validate_items(updated) unique_field = original.get('unique_field') if unique_field: self._check_uniqueness(updates.get('items', []), unique_field) def on_updated(self, updates, original): """ Overriding this to send notification about the replacement """ self._send_notification(original) def on_replaced(self, document, original): """ Overriding this to send notification about the replacement """ self._send_notification(document) def on_delete(self, doc): """ Overriding to validate vocabulary deletion """ if 'field_type' not in doc: raise SuperdeskApiError.badRequestError( 'Default vocabularies cannot be deleted') def _check_uniqueness(self, items, unique_field): """Checks the uniqueness if a unique field is defined :param items: list of items to check for uniqueness :param unique_field: name of the unique field """ unique_values = [] for item in items: # compare only the active items if not item.get('is_active'): continue if not item.get(unique_field): raise SuperdeskApiError.badRequestError( "{} cannot be empty".format(unique_field)) unique_value = str(item.get(unique_field)).upper() if unique_value in unique_values: raise SuperdeskApiError.badRequestError( "Value {} for field {} is not unique".format( item.get(unique_field), unique_field)) unique_values.append(unique_value) def _filter_inactive_vocabularies(self, item): vocs = item['items'] active_vocs = ({k: voc[k] for k in voc.keys() if k != 'is_active'} for voc in vocs if voc.get('is_active', True)) item['items'] = list(active_vocs) def _cast_items(self, vocab): """Cast values in vocabulary items using predefined schema. :param vocab """ schema = vocab_schema.get(vocab.get('_id'), {}) for item in vocab.get('items', []): for field, field_schema in schema.items(): if field in item: item[field] = serialize_value(field_schema['type'], item[field]) def _send_notification(self, updated_vocabulary, event='vocabularies:updated'): """ Sends notification about the updated vocabulary to all the connected clients. """ user = get_user_from_request() push_notification(event, vocabulary=updated_vocabulary.get('display_name'), user=str(user[config.ID_FIELD]) if user else None, vocabulary_id=updated_vocabulary['_id']) def get_rightsinfo(self, item): rights_key = item.get('source', item.get('original_source', 'default')) all_rights = self.find_one(req=None, _id='rightsinfo') if not all_rights or not all_rights.get('items'): return {} try: all_rights['items'] = self.get_locale_vocabulary( all_rights.get('items'), item.get('language')) default_rights = next(info for info in all_rights['items'] if info['name'] == 'default') except StopIteration: default_rights = None try: rights = next(info for info in all_rights['items'] if info['name'] == rights_key) except StopIteration: rights = default_rights if rights: return { 'copyrightholder': rights.get('copyrightHolder'), 'copyrightnotice': rights.get('copyrightNotice'), 'usageterms': rights.get('usageTerms'), } else: return {} def get_extra_fields(self): return list( self.get(req=None, lookup={'field_type': { '$exists': True, '$ne': None }})) def get_custom_vocabularies(self): return list( self.get(req=None, lookup={ 'field_type': None, 'service': { '$exists': True }, })) def get_locale_vocabulary(self, vocabulary, language): if not vocabulary or not language: return vocabulary locale_vocabulary = [] for item in vocabulary: if 'translations' not in item: locale_vocabulary.append(item) continue new_item = item.copy() locale_vocabulary.append(new_item) for field, values in new_item.get('translations', {}).items(): if field in new_item and language in values: new_item[field] = values[language] return locale_vocabulary
class VocabulariesService(BaseService): system_keys = set(DEFAULT_SCHEMA.keys()).union(set(DEFAULT_EDITOR.keys())) def _validate_items(self, update): # if we have qcode and not unique_field set, we want it to be qcode try: update["schema"]["qcode"] except KeyError: pass else: update.setdefault("unique_field", "qcode") unique_field = update.get("unique_field") vocabs = {} if "schema" in update and "items" in update: for index, item in enumerate(update["items"]): for field, desc in update.get("schema", {}).items(): if (desc.get("required", False) or unique_field == field) and (field not in item or not item[field]): msg = "Required " + field + " in item " + str(index) payload = { "error": { "required_field": 1 }, "params": { "field": field, "item": index } } raise SuperdeskApiError.badRequestError( message=msg, payload=payload) elif desc.get("link_vocab") and desc.get("link_field"): if not vocabs.get(desc["link_vocab"]): linked_vocab = self.find_one( req=None, _id=desc["link_vocab"]) or {} vocabs[desc["link_vocab"]] = [ vocab.get(desc["link_field"]) for vocab in linked_vocab.get("items") or [] ] if item.get(field) and item[field] not in vocabs[ desc["link_vocab"]]: msg = '{} "{}={}" not found'.format( desc["link_vocab"], desc["link_field"], item[field]) payload = { "error": { "required_field": 1, "params": { "field": field, "item": index } } } raise SuperdeskApiError.badRequestError( message=msg, payload=payload) def on_create(self, docs): for doc in docs: self._validate_items(doc) if doc.get("field_type") and doc["_id"] in self.system_keys: raise SuperdeskApiError(message="{} is in use".format( doc["_id"]), payload={"_id": { "conflict": 1 }}) if self.find_one(req=None, **{ "_id": doc["_id"], "_deleted": True }): raise SuperdeskApiError( message="{} is used by deleted vocabulary".format( doc["_id"]), payload={"_id": { "deleted": 1 }}) def on_created(self, docs): for doc in docs: self._send_notification(doc, event="vocabularies:created") def on_replace(self, document, original): self._validate_items(document) document[app.config["LAST_UPDATED"]] = utcnow() document[app.config["DATE_CREATED"]] = (original.get( app.config["DATE_CREATED"], utcnow()) if original else utcnow()) logger.info("updating vocabulary item: %s", document["_id"]) def on_fetched(self, doc): """Overriding to filter out inactive vocabularies and pops out 'is_active' property from the response. It keeps it when requested for manageable vocabularies. """ if request and hasattr(request, "args") and request.args.get("where"): where_clause = json.loads(request.args.get("where")) if where_clause.get("type") == "manageable": return doc for item in doc[config.ITEMS]: self._filter_inactive_vocabularies(item) self._cast_items(item) def on_fetched_item(self, doc): """ Overriding to filter out inactive vocabularies and pops out 'is_active' property from the response. """ self._filter_inactive_vocabularies(doc) self._cast_items(doc) def on_update(self, updates, original): """Checks the duplicates if a unique field is defined""" if "items" in updates: updated = deepcopy(original) updated.update(updates) self._validate_items(updated) unique_field = original.get("unique_field") if unique_field: self._check_uniqueness(updates.get("items", []), unique_field) def on_updated(self, updates, original): """ Overriding this to send notification about the replacement """ self._send_notification(original) def on_replaced(self, document, original): """ Overriding this to send notification about the replacement """ self._send_notification(document) def on_delete(self, doc): """ Overriding to validate vocabulary deletion """ if "field_type" not in doc: raise SuperdeskApiError.badRequestError( "Default vocabularies cannot be deleted") def _check_uniqueness(self, items, unique_field): """Checks the uniqueness if a unique field is defined :param items: list of items to check for uniqueness :param unique_field: name of the unique field """ unique_values = [] for item in items: # compare only the active items if not item.get("is_active"): continue if not item.get(unique_field): raise SuperdeskApiError.badRequestError( "{} cannot be empty".format(unique_field)) unique_value = str(item.get(unique_field)).upper() if unique_value in unique_values: raise SuperdeskApiError.badRequestError( "Value {} for field {} is not unique".format( item.get(unique_field), unique_field)) unique_values.append(unique_value) def _filter_inactive_vocabularies(self, item): vocs = item["items"] active_vocs = ({k: voc[k] for k in voc.keys() if k != "is_active"} for voc in vocs if voc.get("is_active", True)) item["items"] = list(active_vocs) def _cast_items(self, vocab): """Cast values in vocabulary items using predefined schema. :param vocab """ schema = vocab_schema.get(vocab.get("_id"), {}) for item in vocab.get("items", []): for field, field_schema in schema.items(): if field in item: item[field] = serialize_value(field_schema["type"], item[field]) def _send_notification(self, updated_vocabulary, event="vocabularies:updated"): """ Sends notification about the updated vocabulary to all the connected clients. """ user = get_user_from_request() push_notification( event, vocabulary=updated_vocabulary.get("display_name"), user=str(user[config.ID_FIELD]) if user else None, vocabulary_id=updated_vocabulary["_id"], ) def get_rightsinfo(self, item): rights_key = item.get("source", item.get("original_source", "default")) all_rights = self.find_one(req=None, _id="rightsinfo") if not all_rights or not all_rights.get("items"): return {} try: all_rights["items"] = self.get_locale_vocabulary( all_rights.get("items"), item.get("language")) default_rights = next(info for info in all_rights["items"] if info["name"] == "default") except StopIteration: default_rights = None try: rights = next(info for info in all_rights["items"] if info["name"] == rights_key) except StopIteration: rights = default_rights if rights: return { "copyrightholder": rights.get("copyrightHolder"), "copyrightnotice": rights.get("copyrightNotice"), "usageterms": rights.get("usageTerms"), } else: return {} def get_extra_fields(self): return list( self.get(req=None, lookup={"field_type": { "$exists": True, "$ne": None }})) def get_custom_vocabularies(self): return list( self.get( req=None, lookup={ "field_type": None, "service": { "$exists": True }, }, )) def get_forbiden_custom_vocabularies(self): return list( self.get( req=None, lookup={ "field_type": None, "selection_type": "do not show", "service": { "$exists": True }, }, )) def get_locale_vocabulary(self, vocabulary, language): if not vocabulary or not language: return vocabulary locale_vocabulary = [] for item in vocabulary: if "translations" not in item: locale_vocabulary.append(item) continue new_item = item.copy() locale_vocabulary.append(new_item) for field, values in new_item.get("translations", {}).items(): if field in new_item and language in values: new_item[field] = values[language] return locale_vocabulary def add_missing_keywords(self, keywords, language=None): if not keywords: return cv = self.find_one(req=None, _id=KEYWORDS_CV) if cv: existing = {item["name"].lower() for item in cv.get("items", [])} missing = [ keyword for keyword in keywords if keyword.lower() not in existing ] if missing: updates = {"items": cv.get("items", [])} for keyword in missing: updates["items"].append({ "name": keyword, "qcode": keyword, "is_active": True, }) self.on_update(updates, cv) self.system_update(cv["_id"], updates, cv) self.on_updated(updates, cv) else: items = [{ "name": keyword, "qcode": keyword, "is_active": True, } for keyword in keywords] cv = { "_id": KEYWORDS_CV, "items": items, "type": "manageable", "display_name": _("Keywords"), "unique_field": "name", "schema": { "name": {}, "qcode": {}, }, } self.post([cv]) def get_article_cv_item(self, item, scheme): article_item = { k: v for k, v in item.items() if k not in ("is_active", ) } article_item.update({"scheme": scheme}) return article_item def get_items(self, _id: str, qcode: str = None, is_active: bool = True, name: str = None, lang: str = None) -> List: """ Return `items` with specified filters from the CV with specified `_id`. If `lang` is provided then `name` is looked in `items.translations.name.{lang}`, otherwise `name` is looked in `items.name`. :param _id: custom vocabulary _id :param qcode: items.qcode filter :param is_active: items.is_active filter :param name: items.name filter :param lang: items.lang filter :return: items list """ projection: Dict[str, Any] = {} lookup = {"_id": _id} if qcode: elem_match = projection.setdefault("items", {}).setdefault( "$elemMatch", {}) elem_match["qcode"] = qcode # if `lang` is provided `name` is looked in `translations.name.{lang}` if name and lang: elem_match = projection.setdefault("items", {}).setdefault( "$elemMatch", {}) elem_match[f"translations.name.{lang}"] = { "$regex": r"^{}$".format(name), # case-insensitive "$options": "i", } elif name: elem_match = projection.setdefault("items", {}).setdefault( "$elemMatch", {}) elem_match["name"] = { "$regex": r"^{}$".format(name), # case-insensitive "$options": "i", } cursor = self.get_from_mongo(req=None, lookup=lookup, projection=projection) try: items = cursor.next()["items"] except (StopIteration, KeyError): return [] # $elemMatch projection contains only the first element matching the condition, # that"s why `is_active` filter is filtered via python if is_active is not None: items = [i for i in items if i.get("is_active", True) == is_active] def format_item(item): try: del item["is_active"] except KeyError: pass item["scheme"] = _id return item items = list(map(format_item, items)) return items def get_languages(self): return self.get_items(_id="languages")