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/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 @@
+
+
+
+ {{ item_id }}
+
+ {{ shortenedName }}
+ [
+
+
+
+ ID
+ Type
+ Name
+ Date created
+ Location
+ # of blocks
+
+
+
+
+
+
+ {{ itemTypes[equipment.type].display }}
+ {{ equipment.name }}
+ {{ $filters.IsoDatetimeToDate(equipment.date) }}
+ {{ equipment.location }}
+
+ {{ equipment.nblocks }}
+
+
+
+