Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add status field for items and equipment #866

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pydatalab/pydatalab/models/equipment.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from pydantic import Field

from pydatalab.models.items import Item
from pydatalab.models.utils import (
EquipmentStatus,
)


class Equipment(Item):
Expand All @@ -21,3 +24,6 @@ class Equipment(Item):

contact: Optional[str]
"""Contact information for equipment (e.g., email address or phone number)."""

status: EquipmentStatus = Field(default=EquipmentStatus.WORKING)
"""The status of the equipment, indicating its current state."""
7 changes: 6 additions & 1 deletion pydatalab/pydatalab/models/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
IsoformatDateTime,
PyObjectId,
Refcode,
ItemStatus,
)


Expand Down Expand Up @@ -45,6 +46,9 @@ class Item(Entry, HasOwner, HasRevisionControl, IsCollectable, HasBlocks, abc.AB
file_ObjectIds: List[PyObjectId] = Field([])
"""Links to object IDs of files stored within the database."""

status: ItemStatus = Field(default=ItemStatus.PLANNED)
"""The status of the item, indicating its current state."""

@validator("refcode", pre=True, always=True)
def refcode_validator(cls, v):
"""Generate a refcode if not provided."""
Expand All @@ -54,6 +58,7 @@ def refcode_validator(cls, v):
id = None
prefix, id = v.split(":")
if prefix is None or id is None:
raise ValueError(f"refcode missing prefix or ID {id=}, {prefix=} from {v=}")
raise ValueError(
f"refcode missing prefix or ID {id=}, {prefix=} from {v=}")

return v
22 changes: 21 additions & 1 deletion pydatalab/pydatalab/models/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@ class ItemType(str, Enum):
STARTING_MATERIALS = "starting_materials"


class ItemStatus(str, Enum):
"""An enumeration of the status of items"""

PLANNED = "planned"
ACTIVE = "active"
COMPLETED = "completed"
FAILED = "failed"


class EquipmentStatus(str, Enum):
"""An enumeration of the status of equipments"""

WORKING = "working"
BROKEN = "broken"
BEING_FIXED = "being_fixed"
DEFUNCT = "defunct"
NOT_BEING_FIXED = "not_being_fixed"


class KnownType(str, Enum):
"""An enumeration of the types of entry known by this implementation, should be made dynamic in the future."""

Expand Down Expand Up @@ -106,7 +125,8 @@ def __get_validators__(self):
def validate(self, v):
q = self.Q(v)
if not q.check(self._dimensions):
raise ValueError("Value {v} must have dimensions of mass, not {v.dimensions}")
raise ValueError(
"Value {v} must have dimensions of mass, not {v.dimensions}")
return q

@classmethod
Expand Down
52 changes: 37 additions & 15 deletions pydatalab/pydatalab/routes/v0_1/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from pydatalab.models import ITEM_MODELS
from pydatalab.models.items import Item
from pydatalab.models.relationships import RelationshipType
from pydatalab.models.utils import generate_unique_refcode
from pydatalab.models.utils import generate_unique_refcode, ItemStatus, EquipmentStatus
from pydatalab.mongo import flask_mongo
from pydatalab.permissions import PUBLIC_USER_ID, active_users_or_get_only, get_default_permissions

Expand All @@ -41,11 +41,13 @@ def reserialize_blocks(display_order: List[str], blocks_obj: Dict[str, Dict]) ->
try:
block_data = blocks_obj[block_id]
except KeyError:
LOGGER.warning(f"block_id {block_id} found in display order but not in blocks_obj")
LOGGER.warning(
f"block_id {block_id} found in display order but not in blocks_obj")
continue
blocktype = block_data["blocktype"]
blocks_obj[block_id] = (
BLOCK_TYPES.get(blocktype, BLOCK_TYPES["notsupported"]).from_db(block_data).to_web()
BLOCK_TYPES.get(blocktype, BLOCK_TYPES["notsupported"]).from_db(
block_data).to_web()
)

return blocks_obj
Expand Down Expand Up @@ -269,7 +271,8 @@ def _check_collections(sample_dict: dict) -> list[dict[str, str]]:
query.update(c)
if "immutable_id" in c:
query["_id"] = ObjectId(query.pop("immutable_id"))
result = flask_mongo.db.collections.find_one({**query, **get_default_permissions()})
result = flask_mongo.db.collections.find_one(
{**query, **get_default_permissions()})
if not result:
raise ValueError(f"No collection found matching request: {c}")
sample_dict["collections"][ind] = {"immutable_id": result["_id"]}
Expand Down Expand Up @@ -300,7 +303,8 @@ def search_items():
nresults = request.args.get("nresults", default=100, type=int)
types = request.args.get("types", default=None)
if isinstance(types, str):
types = types.split(",") # should figure out how to parse as list automatically
# should figure out how to parse as list automatically
types = types.split(",")

match_obj = {
"$text": {"$search": query},
Expand Down Expand Up @@ -347,9 +351,11 @@ def _create_sample(
)

if copy_from_item_id:
copied_doc = flask_mongo.db.items.find_one({"item_id": copy_from_item_id})
copied_doc = flask_mongo.db.items.find_one(
{"item_id": copy_from_item_id})

LOGGER.debug(f"Copying from pre-existing item {copy_from_item_id} with data:\n{copied_doc}")
LOGGER.debug(
f"Copying from pre-existing item {copy_from_item_id} with data:\n{copied_doc}")
if not copied_doc:
return (
dict(
Expand Down Expand Up @@ -428,10 +434,10 @@ def _create_sample(
raise RuntimeError("Invalid type")
model = ITEM_MODELS[type_]

## the following code was used previously to explicitely check schema properties.
## it doesn't seem to be necessary now, with extra = "ignore" turned on in the pydantic models,
## and it breaks in instances where the models use aliases (e.g., in the starting_material model)
## so we are taking it out now, but leaving this comment in case it needs to be reverted.
# the following code was used previously to explicitely check schema properties.
# it doesn't seem to be necessary now, with extra = "ignore" turned on in the pydantic models,
# and it breaks in instances where the models use aliases (e.g., in the starting_material model)
# so we are taking it out now, but leaving this comment in case it needs to be reverted.
# schema = model.schema()
# new_sample = {k: sample_dict[k] for k in schema["properties"] if k in sample_dict}
new_sample = sample_dict
Expand Down Expand Up @@ -497,7 +503,8 @@ def _create_sample(
# via joins for a specific query.
# TODO: encode this at the model level, via custom schema properties or hard-coded `.store()` methods
# the `Entry` model.
result = flask_mongo.db.items.insert_one(data_model.dict(exclude={"creators", "collections"}))
result = flask_mongo.db.items.insert_one(
data_model.dict(exclude={"creators", "collections"}))
if not result.acknowledged:
return (
dict(
Expand Down Expand Up @@ -553,7 +560,8 @@ def create_sample():
response, http_code = _create_sample(
sample_dict=request_json["new_sample_data"],
copy_from_item_id=request_json.get("copy_from_item_id"),
generate_id_automatically=request_json.get("generate_id_automatically", False),
generate_id_automatically=request_json.get(
"generate_id_automatically", False),
)
else:
response, http_code = _create_sample(request_json)
Expand Down Expand Up @@ -644,7 +652,8 @@ def get_item_data(
call its render function).

"""
redirect_to_ui = bool(request.args.get("redirect-to-ui", default=False, type=json.loads))
redirect_to_ui = bool(request.args.get(
"redirect-to-ui", default=False, type=json.loads))
if refcode and redirect_to_ui and CONFIG.APP_URL:
return redirect(f"{CONFIG.APP_URL}/items/{refcode}", code=307)

Expand Down Expand Up @@ -814,7 +823,8 @@ def save_item():
for block_id, block_data in updated_data.get("blocks_obj", {}).items():
blocktype = block_data["blocktype"]

block = BLOCK_TYPES.get(blocktype, BLOCK_TYPES["notsupported"]).from_web(block_data)
block = BLOCK_TYPES.get(
blocktype, BLOCK_TYPES["notsupported"]).from_web(block_data)

updated_data["blocks_obj"][block_id] = block.to_db()

Expand Down Expand Up @@ -922,3 +932,15 @@ def search_users():
)

return jsonify({"status": "success", "users": list(cursor)}), 200


@ITEMS.route('/item_status_options', methods=['GET'])
def get_item_status_options():
status_options = [status.value for status in ItemStatus]
return jsonify(status_options)


@ITEMS.route('/equipment_status_options', methods=['GET'])
def get_equipment_status_options():
status_options = [status.value for status in EquipmentStatus]
return jsonify(status_options)
29 changes: 29 additions & 0 deletions webapp/src/components/EquipmentInformation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@
<label for="equip-contact" class="mr-2">Contact information</label>
<input id="equip-contact" v-model="Contact" class="form-control" />
</div>
<div class="col-md-4 pb-3 d-flex justify-content-center align-items-center">
<span :class="getStatusBadgeClass(Status)" class="badge text-uppercase">
{{ formatStatus(Status) }}
</span>
</div>
</div>
<label id="equip-description-label" class="mr-2">Description</label>
<TinyMceInline v-model="ItemDescription" aria-labelledby="equip-description-label" />
Expand Down Expand Up @@ -107,6 +112,27 @@ export default {
SerialNos: createComputedSetterForItemField("serial_numbers"),
Maintainers: createComputedSetterForItemField("creators"),
Contact: createComputedSetterForItemField("contact"),
Status: createComputedSetterForItemField("status"),
},

methods: {
getStatusBadgeClass(status) {
switch (status) {
case "working":
return "badge-success";
case "broken":
return "badge-warning";
case "being_fixed":
return "badge-info";
case "defunct":
return "badge-dark";
case "not_being_fixed":
return "badge-danger";
}
},
formatStatus(status) {
return status.split("_").join(" ");
},
},
};
</script>
Expand All @@ -116,4 +142,7 @@ label {
font-weight: 500;
color: #298651;
}
.badge {
font-size: 1em;
}
</style>
30 changes: 29 additions & 1 deletion webapp/src/components/SampleInformation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,16 @@
<Creators :creators="ItemCreators" :size="36" />
</div>
</div>
<div class="col-md-6 col-sm-7 pr-2">
<div class="form-group col-md-3 col-sm-3 col-6 pb-3">
<ToggleableCollectionFormGroup v-model="Collections" />
</div>
<div
class="form-group col-md-3 col-sm-3 col-6 pb-3 d-flex justify-content-center align-items-center"
>
<span :class="getStatusBadgeClass(Status)" class="badge text-uppercase">
{{ Status }}
</span>
</div>
</div>
<div class="form-row">
<div class="col">
Expand Down Expand Up @@ -104,6 +111,27 @@ export default {
DateCreated: createComputedSetterForItemField("date"),
ItemCreators: createComputedSetterForItemField("creators"),
Collections: createComputedSetterForItemField("collections"),
Status: createComputedSetterForItemField("status"),
},
methods: {
getStatusBadgeClass(status) {
switch (status) {
case "planned":
return "badge-secondary";
case "active":
return "badge-primary";
case "completed":
return "badge-success";
case "failed":
return "badge-danger";
}
},
},
};
</script>

<style scoped>
.badge {
font-size: 1em;
}
</style>
24 changes: 24 additions & 0 deletions webapp/src/server_fetch_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -704,3 +704,27 @@ export function getBlocksInfos() {
throw error;
});
}

export function getItemStatusOptions() {
return fetch_get(`${API_URL}/item_status_options`)
.then(function (response_json) {
return response_json;
})
.catch((error) => {
console.error("Error when fetching item status options");
console.error(error);
throw error;
});
}

export function getEquipmentStatusOptions() {
return fetch_get(`${API_URL}/equipment_status_options`)
.then(function (response_json) {
return response_json;
})
.catch((error) => {
console.error("Error when fetching equipment status options");
console.error(error);
throw error;
});
}
Loading