diff --git a/mpcontribs-api/mpcontribs/api/contributions/document.py b/mpcontribs-api/mpcontribs/api/contributions/document.py index db434cdf8..f069eb5a7 100644 --- a/mpcontribs-api/mpcontribs/api/contributions/document.py +++ b/mpcontribs-api/mpcontribs/api/contributions/document.py @@ -168,7 +168,26 @@ class Contributions(DynamicDocument): ReferenceField("Attachments", null=True), default=list, max_length=10 ) notebook = ReferenceField("Notebooks") - atlas = AtlasManager("formula_autocomplete") + atlas = AtlasManager( + "formula_autocomplete", + definition={ + "analyzer": "lucene.whitespace", + "searchAnalyzer": "lucene.whitespace", + "mappings": { + "dynamic": False, + "fields": { + "data": {"dynamic": True, "type": "document"}, + "formula": {"type": "string"}, + "identifier": {"type": "string"}, + "is_public": {"type": "boolean"}, + "last_modified": {"type": "date"}, + "needs_build": {"type": "boolean"}, + "project": [{"type": "stringFacet"}, {"type": "string"}], + }, + }, + "storedSource": True, + }, + ) meta = { "collection": "contributions", "indexes": [ @@ -197,31 +216,6 @@ def objects(doc_cls, queryset): "needs_build", ) - @classmethod - def atlas_filter(cls, term): - try: - comp = Composition(term) - except Exception: - raise ValueError(f"{term} is not a valid composition") - - try: - for element in comp.elements: - Element(element) - except Exception: - raise ValueError(f"{element} not a valid element") - - ind_str = [] - - if len(comp) == 1: - d = comp.get_integer_formula_and_factor() - ind_str.append(d[0] + str(int(d[1])) if d[1] != 1 else d[0]) - else: - for i, j in comp.reduced_composition.items(): - ind_str.append(i.name + str(int(j)) if j != 1 else i.name) - - final_terms = ["".join(entry) for entry in permutations(ind_str)] - return AtlasQ(formula=final_terms[0]) # TODO formula__in=final_terms - @classmethod def post_init(cls, sender, document, **kwargs): # replace existing components with according ObjectIds diff --git a/mpcontribs-api/mpcontribs/api/contributions/views.py b/mpcontribs-api/mpcontribs/api/contributions/views.py index 86bfb70b8..51154ab4f 100644 --- a/mpcontribs-api/mpcontribs/api/contributions/views.py +++ b/mpcontribs-api/mpcontribs/api/contributions/views.py @@ -169,59 +169,3 @@ def has_add_permission(self, req, obj): raise Unauthorized(f"{obj.identifier} already added for {obj.project.id}") return True - - -@contributions.route("/search") -def search(): - formula = request.args.get("formula") - if not formula: - abort(404, description="Missing formula param.") - - try: - comp = Composition(formula) - except (CompositionError, ValueError): - abort(400, description="Invalid formula provided.") - - ind_str = [] - - if len(comp) == 1: - d = comp.get_integer_formula_and_factor() - ind_str.append(d[0] + str(int(d[1])) if d[1] != 1 else d[0]) - else: - for i, j in comp.reduced_composition.items(): - ind_str.append(i.name + str(int(j)) if j != 1 else i.name) - - final_terms = ["".join(entry) for entry in permutations(ind_str)] - limit = request.args.get("limit", ContributionsResource.default_limit) - - pipeline = [ - { - "$search": { - "index": "formula_autocomplete", - "text": {"path": "formula", "query": final_terms}, - } - }, - {"$project": {"formula": 1, "length": {"$strLenCP": "$formula"}, "project": 1}}, - {"$match": {"length": {"$gte": len(final_terms[0])}}}, - {"$limit": limit}, - {"$sort": {"length": 1}}, - ] - - results = [] - - try: - for contrib in Contributions.objects().aggregate(pipeline, maxTimeMS=15000): - results.append( - { - "id": str(contrib["_id"]), - "formula": contrib["formula"], - "project": contrib["project"], - } - ) - except Exception: - abort( - 500, - description="Can't complete search. Please try a different formula or try again later.", - ) - - return jsonify(results) diff --git a/mpcontribs-api/mpcontribs/api/core.py b/mpcontribs-api/mpcontribs/api/core.py index 6524d099f..cf27b578f 100644 --- a/mpcontribs-api/mpcontribs/api/core.py +++ b/mpcontribs-api/mpcontribs/api/core.py @@ -2,7 +2,6 @@ """Custom meta-class and MethodView for Swagger""" import os -import logging import yaml from copy import deepcopy @@ -581,8 +580,18 @@ def has_read_permission(self, request, qs): return qs.none() else: names = None - if q and "project" in q and "$in" in q["project"]: - names = q.pop("project").pop("$in") + if hasattr(qs._query_obj, "children"): + children = deepcopy(qs._query_obj.children) + else: + children = [deepcopy(qs._query_obj)] + + qs._query_obj = Q() + for node in children: + for field, value in node.query.items(): + if field == "project__in": + names = value + else: + qs = qs.filter(**{field: value}) qfilter = self.get_projects_filter( username, groups, filter_names=names @@ -610,15 +619,20 @@ def has_read_permission(self, request, qs): qfilter = self.get_projects_filter(username, groups) component = component[:-1] if component == "notebooks" else component qfilter &= Q(**{f"{component}__in": ids}) - contribs = Contributions.objects(qfilter).only(component).limit(len(ids)) + contribs = ( + Contributions.objects(qfilter).only(component).limit(len(ids)) + ) # return new queryset using "ids__in" - readable_ids = [ - getattr(contrib, component).id for contrib in contribs - ] if component == "notebook" else [ - dbref.id for contrib in contribs - for dbref in getattr(contrib, component) - if dbref.id in ids - ] + readable_ids = ( + [getattr(contrib, component).id for contrib in contribs] + if component == "notebook" + else [ + dbref.id + for contrib in contribs + for dbref in getattr(contrib, component) + if dbref.id in ids + ] + ) if not readable_ids: return qs.none() diff --git a/mpcontribs-api/tests/test_contributions_anon.tavern.yaml b/mpcontribs-api/tests/test_contributions_anon.tavern.yaml new file mode 100644 index 000000000..a79561938 --- /dev/null +++ b/mpcontribs-api/tests/test_contributions_anon.tavern.yaml @@ -0,0 +1,71 @@ +test_name: anonymous retrieval of contributions + +stages: + - name: get a contribution from a private project with all fields + skip: True + request: + url: "{tavern.env_vars.MP_CONTRIBS_API_URL}/contributions/" + method: GET + headers: + accept: application/json + params: + project: periodic_band_structures + per_page: 1 + _limit: 1 + _fields: _all + response: + status_code: 200 + json: + data: [] + has_more: false + total_count: 0 + total_pages: 0 + + - name: get a contribution with all fields + skip: True + request: + url: "{tavern.env_vars.MP_CONTRIBS_API_URL}/contributions/" + method: GET + headers: + accept: application/json + params: + per_page: 1 + _limit: 1 + _fields: _all + response: + strict: false + status_code: 200 + save: + json: + contrib_anon: "data[0]" + + - name: retrieve single contribution with default fields + skip: True + request: + url: "{tavern.env_vars.MP_CONTRIBS_API_URL}/contributions/{contrib_anon.id:s}/" + method: GET + headers: + accept: application/json + response: + status_code: 200 + json: + id: "{contrib_anon.id}" + project: "{contrib_anon.project}" + identifier: "{contrib_anon.identifier}" + formula: "{contrib_anon.formula}" + is_public: !bool "{contrib_anon.is_public}" + last_modified: "{contrib_anon.last_modified}" + needs_build: !bool "{contrib_anon.needs_build}" + + - name: retrieve single contribution with all fields + skip: True + request: + url: "{tavern.env_vars.MP_CONTRIBS_API_URL}/contributions/{contrib_anon.id:s}/" + params: + _fields: _all + method: GET + headers: + accept: application/json + response: + status_code: 200 + json: !force_original_structure "{contrib_anon}" diff --git a/mpcontribs-api/tests/test_contributions_auth.tavern.yaml b/mpcontribs-api/tests/test_contributions_auth.tavern.yaml new file mode 100644 index 000000000..c33d7b2c5 --- /dev/null +++ b/mpcontribs-api/tests/test_contributions_auth.tavern.yaml @@ -0,0 +1,51 @@ +test_name: authenticated retrieval of contributions + +stages: + - name: get a contribution with all fields + request: + url: "{tavern.env_vars.MP_CONTRIBS_API_URL}/contributions/" + method: GET + headers: + accept: application/json + X-API-KEY: "{tavern.env_vars.MP_API_KEY}" + params: + per_page: 1 + _limit: 1 + _fields: _all + response: + strict: false + status_code: 200 + save: + json: + contrib_auth: "data[0]" + + - name: retrieve single contribution with default fields + request: + url: "{tavern.env_vars.MP_CONTRIBS_API_URL}/contributions/{contrib_auth.id:s}/" + method: GET + headers: + accept: application/json + X-API-KEY: "{tavern.env_vars.MP_API_KEY}" + response: + status_code: 200 + json: + id: "{contrib_auth.id}" + project: "{contrib_auth.project}" + identifier: "{contrib_auth.identifier}" + formula: "{contrib_auth.formula}" + is_public: !bool "{contrib_auth.is_public}" + last_modified: "{contrib_auth.last_modified}" + needs_build: !bool "{contrib_auth.needs_build}" + + - name: retrieve single contribution with all fields + request: + url: "{tavern.env_vars.MP_CONTRIBS_API_URL}/contributions/{contrib_auth.id:s}/" + params: + _fields: _all + method: GET + headers: + accept: application/json + X-API-KEY: "{tavern.env_vars.MP_API_KEY}" + response: + status_code: 200 + json: !force_original_structure "{contrib_auth}"