diff --git a/.prettierrc b/.prettierrc index 7aaa14162..151b6b9b4 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1 +1,2 @@ printWidth: 100 +trailingComma: all diff --git a/pydatalab/pydatalab/models/__init__.py b/pydatalab/pydatalab/models/__init__.py index f62483822..2d13e371f 100644 --- a/pydatalab/pydatalab/models/__init__.py +++ b/pydatalab/pydatalab/models/__init__.py @@ -4,6 +4,7 @@ from pydatalab.models.cells import Cell from pydatalab.models.collections import Collection +from pydatalab.models.equipment import Equipment from pydatalab.models.files import File from pydatalab.models.people import Person from pydatalab.models.samples import Sample @@ -13,6 +14,16 @@ "samples": Sample, "starting_materials": StartingMaterial, "cells": Cell, + "equipment": Equipment, } -__all__ = ("File", "Sample", "StartingMaterial", "Person", "Cell", "Collection", "ITEM_MODELS") +__all__ = ( + "File", + "Sample", + "StartingMaterial", + "Person", + "Cell", + "Collection", + "Equipment", + "ITEM_MODELS", +) diff --git a/pydatalab/pydatalab/models/equipment.py b/pydatalab/pydatalab/models/equipment.py new file mode 100644 index 000000000..d6c0587a4 --- /dev/null +++ b/pydatalab/pydatalab/models/equipment.py @@ -0,0 +1,20 @@ +from typing import Optional + +from pydantic import Field + +from pydatalab.models.items import Item + + +class Equipment(Item): + """A model for representing an experimental sample.""" + + type: str = Field("equipment", const="equipment", pattern="^equipment$") + + serial_numbers: Optional[str] + """A string describing one or more serial numbers for the instrument.""" + + manufacturer: Optional[str] + """The manufacturer of this piece of equipment""" + + location: Optional[str] + """Place where the equipment is located""" diff --git a/pydatalab/pydatalab/routes/v0_1/items.py b/pydatalab/pydatalab/routes/v0_1/items.py index 45fe281a1..01ced9e71 100644 --- a/pydatalab/pydatalab/routes/v0_1/items.py +++ b/pydatalab/pydatalab/routes/v0_1/items.py @@ -74,6 +74,56 @@ def dereference_files(file_ids: List[Union[str, ObjectId]]) -> Dict[str, Dict]: return results +def get_equipment_summary(): + if not current_user.is_authenticated and not CONFIG.TESTING: + return ( + jsonify( + status="error", + message="Authorization required to access chemical inventory.", + ), + 401, + ) + + _project = { + "_id": 0, + "creators": { + "display_name": 1, + "contact_email": 1, + }, + # "collections": { + # "collection_id": 1, + # "title": 1, + # }, + "item_id": 1, + "name": 1, + "nblocks": {"$size": "$display_order"}, + "characteristic_chemical_formula": 1, + "type": 1, + "date": 1, + "refcode": 1, + "location": 1, + } + + items = [ + doc + for doc in flask_mongo.db.items.aggregate( + [ + { + "$match": { + "type": "equipment", + **get_default_permissions(user_only=False), + } + }, + {"$project": _project}, + ] + ) + ] + return jsonify({"status": "success", "items": items}) + + +get_equipment_summary.methods = ("GET",) # type: ignore + + def get_starting_materials(): if not current_user.is_authenticated and not CONFIG.TESTING: return ( @@ -276,7 +326,10 @@ def search_items(): if isinstance(types, str): types = types.split(",") # should figure out how to parse as list automatically - match_obj = {"$text": {"$search": query}, **get_default_permissions(user_only=False)} + match_obj = { + "$text": {"$search": query}, + **get_default_permissions(user_only=False), + } if types is not None: match_obj["type"] = {"$in": types} @@ -360,7 +413,11 @@ def _create_sample(sample_dict: dict, copy_from_item_id: Optional[str] = None) - sample_dict = copied_doc elif copied_doc["type"] == "cells": - for component in ("positive_electrode", "negative_electrode", "electrolyte"): + for component in ( + "positive_electrode", + "negative_electrode", + "electrolyte", + ): if copied_doc.get(component): existing_consituent_ids = [ constituent["item"].get("item_id", None) @@ -402,7 +459,10 @@ def _create_sample(sample_dict: dict, copy_from_item_id: Optional[str] = None) - # locally for testing creator UI elements new_sample["creator_ids"] = [24 * "0"] new_sample["creators"] = [ - {"display_name": "Public testing user", "contact_email": "datalab@odbx.science"} + { + "display_name": "Public testing user", + "contact_email": "datalab@odbx.science", + } ] else: new_sample["creator_ids"] = [current_user.person.immutable_id] @@ -458,29 +518,36 @@ def _create_sample(sample_dict: dict, copy_from_item_id: Optional[str] = None) - 400, ) + sample_list_entry = { + "refcode": data_model.refcode, + "item_id": data_model.item_id, + "nblocks": 0, + "date": data_model.date, + "name": data_model.name, + "creator_ids": data_model.creator_ids, + # TODO: This workaround for creators & collections is still gross, need to figure this out properly + "creators": [json.loads(c.json(exclude_unset=True)) for c in data_model.creators] + if data_model.creators + else [], + "collections": [ + json.loads(c.json(exclude_unset=True, exclude_none=True)) + for c in data_model.collections + ] + if data_model.collections + else [], + "type": data_model.type, + } + + # hack to let us use _create_sample() for equipment too. We probably want to make + # a more general create_item() to more elegantly handle different returns. + if data_model.type == "equipment": + sample_list_entry["location"] = data_model.location + data = ( { "status": "success", "item_id": data_model.item_id, - "sample_list_entry": { - "refcode": data_model.refcode, - "item_id": data_model.item_id, - "nblocks": 0, - "date": data_model.date, - "name": data_model.name, - "creator_ids": data_model.creator_ids, - # TODO: This workaround for creators & collections is still gross, need to figure this out properly - "creators": [json.loads(c.json(exclude_unset=True)) for c in data_model.creators] - if data_model.creators - else [], - "collections": [ - json.loads(c.json(exclude_unset=True, exclude_none=True)) - for c in data_model.collections - ] - if data_model.collections - else [], - "type": data_model.type, - }, + "sample_list_entry": sample_list_entry, }, 201, # 201: Created ) @@ -585,7 +652,12 @@ def get_item_data(item_id, load_blocks: bool = False): # retrieve the entry from the database: cursor = flask_mongo.db.items.aggregate( [ - {"$match": {"item_id": item_id, **get_default_permissions(user_only=False)}}, + { + "$match": { + "item_id": item_id, + **get_default_permissions(user_only=False), + } + }, {"$lookup": creators_lookup()}, {"$lookup": collections_lookup()}, {"$lookup": files_lookup()}, @@ -708,7 +780,14 @@ def save_item(): updated_data = request_json["data"] # These keys should not be updated here and cannot be modified by the user through this endpoint - for k in ("_id", "file_ObjectIds", "creators", "creator_ids", "item_id", "relationships"): + for k in ( + "_id", + "file_ObjectIds", + "creators", + "creator_ids", + "item_id", + "relationships", + ): if k in updated_data: del updated_data[k] @@ -827,6 +906,7 @@ def search_users(): ENDPOINTS: Dict[str, Callable] = { "/samples/": get_samples, "/starting-materials/": get_starting_materials, + "/equipment/": get_equipment_summary, "/search-items/": search_items, "/search-users/": search_users, "/new-sample/": create_sample, diff --git a/webapp/package.json b/webapp/package.json index befffd914..7170e06f4 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -63,9 +63,9 @@ "cypress": "^13.6.1", "eslint": "^7.32.0", "eslint-config-prettier": "^8.3.0", - "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-vue": "^8.0.3", - "prettier": "^2.4.1", + "prettier": "3.0.3", "typescript": "~3.9.3", "web-worker": "^1.2.0" } diff --git a/webapp/src/components/CreateEquipmentModal.vue b/webapp/src/components/CreateEquipmentModal.vue new file mode 100644 index 000000000..8bcb28132 --- /dev/null +++ b/webapp/src/components/CreateEquipmentModal.vue @@ -0,0 +1,246 @@ + + + + + diff --git a/webapp/src/components/EquipmentInformation.vue b/webapp/src/components/EquipmentInformation.vue new file mode 100644 index 000000000..bcfd53f23 --- /dev/null +++ b/webapp/src/components/EquipmentInformation.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/webapp/src/components/EquipmentTable.vue b/webapp/src/components/EquipmentTable.vue new file mode 100644 index 000000000..76f47df3e --- /dev/null +++ b/webapp/src/components/EquipmentTable.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/webapp/src/components/FormattedItemName.ce.vue b/webapp/src/components/FormattedItemName.ce.vue new file mode 100644 index 000000000..0e58505e9 --- /dev/null +++ b/webapp/src/components/FormattedItemName.ce.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/webapp/src/components/Navbar.vue b/webapp/src/components/Navbar.vue index 32cc867b8..90371aa1b 100644 --- a/webapp/src/components/Navbar.vue +++ b/webapp/src/components/Navbar.vue @@ -13,6 +13,8 @@ Samples | Collections | Inventory | + Equipment | +  Graph View diff --git a/webapp/src/components/SampleTable.vue b/webapp/src/components/SampleTable.vue index 88b71eacf..80094db60 100644 --- a/webapp/src/components/SampleTable.vue +++ b/webapp/src/components/SampleTable.vue @@ -33,7 +33,7 @@ enableModifiedClick /> - {{ itemTypes[sample.type].display }} + {{ itemTypes[sample.type].display }} {{ sample.name }} {{ $filters.IsoDatetimeToDate(sample.date) }} diff --git a/webapp/src/components/TinyMceInline.vue b/webapp/src/components/TinyMceInline.vue index eeb5c7ab1..30c1113dd 100644 --- a/webapp/src/components/TinyMceInline.vue +++ b/webapp/src/components/TinyMceInline.vue @@ -8,13 +8,13 @@ menubar: false, placeholder: 'Add a description', toolbar_location: 'bottom', - plugins: 'hr image link lists charmap table', + plugins: 'hr image link lists charmap table emoticons code', table_default_styles: { width: '50%', 'margin-left': '1rem', }, toolbar: - 'bold italic underline strikethrough superscript subscript forecolor backcolor removeformat | alignleft aligncenter alignright | bullist numlist indent outdent | headergroup insertgroup | table', + 'bold italic underline strikethrough superscript subscript forecolor backcolor removeformat | alignleft aligncenter alignright | bullist numlist indent outdent | headergroup insertgroup | table | code', toolbar_groups: { formatgroup: { icon: 'format', @@ -40,16 +40,81 @@ }, skin: false, // important so that skin is loaded correctly, oddly content_css: false, // ^ same + setup: editorSetupFunction, + extended_valid_elements: 'item-reference[*]', + custom_elements: 'item-reference', }" /> + + diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 2578ee565..f93c1680e 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -1559,6 +1559,11 @@ resolved "https://registry.yarnpkg.com/@one-ini/wasm/-/wasm-0.1.1.tgz#6013659736c9dbfccc96e8a9c2b3de317df39323" integrity sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw== +"@pkgr/core@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.0.tgz#7d8dacb7fdef0e4387caf7396cbd77f179867d06" + integrity sha512-Zwq5OCzuwJC2jwqmpEQt7Ds1DTi6BWSwoGkbb1n9pO3hzb35BoJELx7c0T23iDkBGkh2e7tvOtjF3tr3OaQHDQ== + "@polka/url@^1.0.0-next.20": version "1.0.0-next.21" resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" @@ -5317,12 +5322,13 @@ eslint-plugin-cypress@^2.11.2: dependencies: globals "^13.20.0" -eslint-plugin-prettier@^4.0.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b" - integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ== +eslint-plugin-prettier@^5.0.0: + version "5.1.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz#17cfade9e732cef32b5f5be53bd4e07afd8e67e1" + integrity sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw== dependencies: prettier-linter-helpers "^1.0.0" + synckit "^0.8.6" eslint-plugin-vue@^8.0.3: version "8.7.1" @@ -9189,7 +9195,12 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -"prettier@^1.18.2 || ^2.0.0", prettier@^2.4.1: +prettier@3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643" + integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg== + +"prettier@^1.18.2 || ^2.0.0": version "2.8.8" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== @@ -10375,6 +10386,14 @@ symbol-observable@^1.0.4: resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== +synckit@^0.8.6: + version "0.8.8" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.8.tgz#fe7fe446518e3d3d49f5e429f443cf08b6edfcd7" + integrity sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ== + dependencies: + "@pkgr/core" "^0.1.0" + tslib "^2.6.2" + table@^6.0.9: version "6.8.1" resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf" @@ -10598,6 +10617,11 @@ tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410" integrity sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig== +tslib@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"