diff --git a/datameta/api/metadatasets.py b/datameta/api/metadatasets.py index 7e19ef16..f31349fa 100644 --- a/datameta/api/metadatasets.py +++ b/datameta/api/metadatasets.py @@ -18,7 +18,7 @@ from pyramid.request import Request from typing import Optional, Dict from ..linting import validate_metadataset_record -from .. import security, siteid, models +from .. import security, siteid, models, resource from ..security import authz import datetime from ..resource import resource_by_id, get_identifier @@ -29,25 +29,25 @@ class MetaDataSetResponse(DataHolderBase): """MetaDataSetResponse container for OpenApi communication""" id: dict - record: dict + record: Dict[str, Optional[str]] + file_ids: Dict[str, Optional[Dict[str, str]]] user_id: str submission_id: Optional[str] = None -def render_record_values(mdatum:Dict[str, models.MetaDatum], record:dict) -> dict: +def render_record_values(metadata:Dict[str, models.MetaDatum], record:dict) -> dict: """Renders values of a metadataset record. Please note: the record should already have passed validation.""" record_rendered = record.copy() - for field in mdatum: + for field in metadata: if not field in record_rendered.keys(): # if field is not contained in record, add it as None to the record: record_rendered[field] = None continue - elif record_rendered[field] and mdatum[field].datetimefmt: + elif record_rendered[field] and metadata[field].datetimefmt: # if MetaDatum is a datetime field, render the value in isoformat record_rendered[field] = datetime.datetime.strptime( record_rendered[field], - mdatum[field].datetimefmt + metadata[field].datetimefmt ).isoformat() - return record_rendered def formatted_mrec_value(mrec): @@ -59,9 +59,9 @@ def formatted_mrec_value(mrec): def get_record_from_metadataset(mdata_set:models.MetaDataSet) -> dict: """ Construct a dict containing all records of that MetaDataSet""" return { - rec.metadatum.name: formatted_mrec_value(rec) - for rec in mdata_set.metadatumrecords - } + rec.metadatum.name : formatted_mrec_value(rec) + for rec in mdata_set.metadatumrecords + } def delete_staged_metadataset_from_db(mdata_id, db, auth_user, request): # Find the requested metadataset @@ -124,8 +124,8 @@ def post(request:Request) -> MetaDataSetResponse: mdatum_query = db.query(models.MetaDatum).order_by( models.MetaDatum.order ).all() - mdatum = {mdat.name: mdat for mdat in mdatum_query } - record = render_record_values(mdatum, record) + metadata = {mdat.name: mdat for mdat in mdatum_query } + record = render_record_values(metadata, record) # construct new MetaDataSet: mdata_set = models.MetaDataSet( @@ -139,18 +139,19 @@ def post(request:Request) -> MetaDataSetResponse: # construct new MetaDatumRecords for name, value in record.items(): mdatum_rec = models.MetaDatumRecord( - metadatum_id = mdatum[name].id, - metadataset_id = mdata_set.id, - file_id = None, - value = value + metadatum_id = metadata[name].id, + metadataset_id = mdata_set.id, + file_id = None, + value = value ) db.add(mdatum_rec) return MetaDataSetResponse( - id = get_identifier(mdata_set), - record = record, - user_id = get_identifier(mdata_set.user), - submission_id = get_identifier(mdata_set.submission) if mdata_set.submission else None, + id = get_identifier(mdata_set), + record = record, + file_ids = { name : None for name, metadatum in metadata.items() if metadatum.isfile }, + user_id = get_identifier(mdata_set.user), + submission_id = get_identifier(mdata_set.submission) if mdata_set.submission else None, ) @view_config( @@ -160,7 +161,7 @@ def post(request:Request) -> MetaDataSetResponse: openapi=True ) def get_metadataset(request:Request) -> MetaDataSetResponse: - """Create new metadataset""" + """Get a metadataset by ID""" auth_user = security.revalidate_user(request) db = request.dbsession mdata_set = resource_by_id(db, models.MetaDataSet, request.matchdict['id']) @@ -172,10 +173,11 @@ def get_metadataset(request:Request) -> MetaDataSetResponse: raise HTTPForbidden() return MetaDataSetResponse( - id=get_identifier(mdata_set), - record=get_record_from_metadataset(mdata_set), - user_id=get_identifier(mdata_set.user), - submission_id=get_identifier(mdata_set.submission) if mdata_set.submission else None, + id = get_identifier(mdata_set), + record = get_record_from_metadataset(mdata_set), + file_ids = { mdrec.metadatum.name : resource.get_identifier_or_none(mdrec.file) for mdrec in mdata_set.metadatumrecords if mdrec.metadatum.isfile }, + user_id = get_identifier(mdata_set.user), + submission_id = get_identifier(mdata_set.submission) if mdata_set.submission else None, ) @view_config( diff --git a/datameta/api/openapi.yaml b/datameta/api/openapi.yaml index 27f7f4a1..8d4b731e 100644 --- a/datameta/api/openapi.yaml +++ b/datameta/api/openapi.yaml @@ -15,7 +15,7 @@ openapi: 3.0.0 info: description: DataMeta - version: 0.15.0 + version: 0.16.0 title: DataMeta servers: @@ -1096,8 +1096,11 @@ components: record: type: object additionalProperties: true - # a free-form object, - # any property is allowed + # a free-form object, any property is allowed + fileIds: + type: object + additionalProperties: true + # a free-form object mapping the field names to file IDs id: $ref: "#/components/schemas/Identifier" submissionId: diff --git a/datameta/api/ui/view.py b/datameta/api/ui/view.py index 42edce0b..6692e2fd 100644 --- a/datameta/api/ui/view.py +++ b/datameta/api/ui/view.py @@ -27,7 +27,8 @@ import pandas as pd -from ... import security, samplesheet, errors +from ... import security, samplesheet, errors, resource +from ...security import authz from ...resource import get_identifier from ...models import MetaDatum, MetaDataSet, MetaDatumRecord, User, Group, Submission @@ -40,6 +41,8 @@ class ViewTableResponse(MetaDataSetResponse): """Data class representing the JSON response returned by POST:/api/ui/view""" submission_label : Optional[str] = None group_id : Optional[dict] = None + group_name: Optional[str] = None + user_name: Optional[str] = None def metadata_index_to_name(db, idx): name = db.query(MetaDatum.name).order_by(MetaDatum.order).limit(1).offset(idx).scalar() @@ -116,7 +119,7 @@ def post(request: Request): # search term with the metadataset and submission site_ids, the # submission label and the MetaDatumRecord value, using the table alias # that was constructed for the search term before. - search_clauses = [ or_(*( field.ilike(f"%{search}%") for field in [ User.site_id, Group.site_id, MetaDataSetFilter.site_id, Submission.site_id, Submission.label, MetaDatumRecordFilter.value ])) for search, MetaDatumRecordFilter in searches ] + search_clauses = [ or_(*( field.ilike(f"%{search}%") for field in [ User.site_id, User.fullname, Group.site_id, Group.name, MetaDataSetFilter.site_id, Submission.site_id, Submission.label, MetaDatumRecordFilter.value ])) for search, MetaDatumRecordFilter in searches ] and_filters += search_clauses # Finally, the filter query, which will be added to the main query as a @@ -144,18 +147,16 @@ def post(request: Request): MetaDatumRecordOrder = aliased(MetaDatumRecord) mdata_name = None - if sort_idx == 0: # The submission site ID - mdatasets_base_query = mdatasets_base_query.join(Submission).order_by(direction(Submission.site_id)) - elif sort_idx == 1: # The submission label + if sort_idx == 0: # The submission label mdatasets_base_query = mdatasets_base_query.join(Submission).order_by(direction(Submission.label)) - elif sort_idx == 2: # The metadataset user site ID - mdatasets_base_query = mdatasets_base_query.join(User).order_by(direction(User.site_id)) - elif sort_idx == 3: # The submission group site ID - mdatasets_base_query = mdatasets_base_query.join(Submission).join(Group).order_by(direction(Group.site_id)) - elif sort_idx == 4: # The metadataset site ID + elif sort_idx == 1: # The user full name + mdatasets_base_query = mdatasets_base_query.join(User).order_by(direction(User.fullname)) # TODO FIX + elif sort_idx == 2: # The submission group name + mdatasets_base_query = mdatasets_base_query.join(Submission).join(Group).order_by(direction(Group.site_id)) # TODO FIX + elif sort_idx == 3: # The metadataset site ID mdatasets_base_query = mdatasets_base_query.order_by(direction(MetaDataSet.site_id)) else: # Sorting by a metadatum value - mdata_name = metadata_index_to_name(db, sort_idx - 5) + mdata_name = metadata_index_to_name(db, sort_idx - 4) # [WARNING] The following JOIN assumes that an inner join between MetaDatumRecord # and MetaDatum does not result in a loss of rows if the JOIN is restricted to one # particular MetaDatum.name. Put differently, this query requires that we always @@ -196,12 +197,15 @@ def post(request: Request): # Build the 'data' response data = [ ViewTableResponse( - id = get_identifier(mdata_set), - record = get_record_from_metadataset(mdata_set), - user_id = get_identifier(mdata_set.user), - group_id = get_identifier(mdata_set.submission.group), - submission_id = get_identifier(mdata_set.submission) if mdata_set.submission else None, - submission_label = mdata_set.submission.label + id = get_identifier(mdata_set), + record = get_record_from_metadataset(mdata_set), + file_ids = { mdrec.metadatum.name : resource.get_identifier_or_none(mdrec.file) for mdrec in mdata_set.metadatumrecords if mdrec.metadatum.isfile }, + user_id = get_identifier(mdata_set.user), + user_name = mdata_set.user.fullname, + group_id = get_identifier(mdata_set.submission.group), + group_name = mdata_set.submission.group.name, + submission_id = get_identifier(mdata_set.submission) if mdata_set.submission else None, + submission_label = mdata_set.submission.label ) for mdata_set, _ in mdata_sets ] diff --git a/datameta/resource.py b/datameta/resource.py index 72600850..07e15274 100644 --- a/datameta/resource.py +++ b/datameta/resource.py @@ -24,6 +24,12 @@ def get_identifier(db_obj): pass return ids +def get_identifier_or_none(db_obj): + """Given a database object, return the identifying IDs as a dictionary or None if the objecft is None""" + if db_obj is None: + return None + return get_identifier(db_obj) + def resource_query_by_id(db, model, idstring): """Returns a database query that returns an entity based on it's uuid or site_id as specified by idstring. diff --git a/datameta/security/authz.py b/datameta/security/authz.py index 7b00fc18..8c371916 100644 --- a/datameta/security/authz.py +++ b/datameta/security/authz.py @@ -30,6 +30,8 @@ def has_data_access(user, data_user_id, data_group_id=None, was_submitted=False) (not was_submitted and data_user_id and data_user_id == user.id) )) +def view_mset_any(user): + return user.site_read def view_apikey(user, target_user): return user_is_target(user, target_user) @@ -107,4 +109,4 @@ def update_user_name(user, target_user): return any(( has_group_rights(user, target_user.group), user_is_target(user, target_user) - )) \ No newline at end of file + )) diff --git a/datameta/static/css/datameta.css b/datameta/static/css/datameta.css index 5c197563..ff9f60ea 100644 --- a/datameta/static/css/datameta.css +++ b/datameta/static/css/datameta.css @@ -2,4 +2,13 @@ color: inherit; text-decoration: inherit; } +.text-accent { + color:#dc3545 !important; +} +.background-accent { + color:#ffc107 !important; +} +.border-accent { + border-color:#ffc107 !important; +} diff --git a/datameta/static/js/submit.js b/datameta/static/js/submit.js index 9f27fd27..e87c7bcd 100644 --- a/datameta/static/js/submit.js +++ b/datameta/static/js/submit.js @@ -226,7 +226,7 @@ DataMeta.submit.buildKeyColunns = function(fileKeys, keys) { if (fileKeys.includes(key)) { /* FILE VALUES */ columns.push({ title:key, data:null, render:function(metadataset) { - var str = metadataset.record[key] === null ? 'empty' : metadataset.record[key]; + var str = metadataset.record[key].value === null ? 'empty' : metadataset.record[key].value; return '' + '' + '' + @@ -237,7 +237,7 @@ DataMeta.submit.buildKeyColunns = function(fileKeys, keys) { } else { /* NON-FILE VALUES */ columns.push({ title:key, data:"record", render: function(record) { - var str = record[key] === null ? 'empty' : record[key]; + var str = record[key].value === null ? 'empty' : record[key].value; return str; }}); } diff --git a/datameta/static/js/view.js b/datameta/static/js/view.js index 5b6b9711..688ecb6c 100644 --- a/datameta/static/js/view.js +++ b/datameta/static/js/view.js @@ -19,13 +19,21 @@ DataMeta.view = {} DataMeta.view.buildColumns = function(mdata_names) { - return mdata_names.map(function(mdata_name) { - return { - title : mdata_name, - data : null, - render : mdataset => mdataset.record[mdata_name] ? mdataset.record[mdata_name] : "empty" - }; - }); + return mdata_names.map(function(mdata_name) { + return { + title : mdata_name, + data : null, + render : function(mdataset, type, row, meta) { + console.log(mdataset) + // Special case NULL + if (mdataset.record[mdata_name] === null) return 'empty'; + // Speical case file + if (mdataset.fileIds[mdata_name]) return ' '+mdataset.record[mdata_name]+''; + // All other cases + return mdataset.record[mdata_name]; + } + }; + }); } DataMeta.view.initTable = function() { @@ -44,11 +52,17 @@ DataMeta.view.initTable = function() { var mdata_names = json.map(record => record.name); var columns = [ - { title: "Submission", data: "submissionId.site", className: "id_col"}, - { title: "Label", data: "submissionLabel", className: "id_col"}, - { title: "User", data: "userId.site", className: "id_col"}, - { title: "Group", data: "groupId.site", className: "id_col"}, - { title: "Metadataset", data: "id.site", className: "id_col"} + { title: "Submission", data: null, className: "id_col", render: function(data) { + var label = data.submissionLabel ? data.submissionLabel : 'empty'; + return '
' + label + '
' + data.submissionId.site + '
' + }}, + { title: "User", data: null, className: "id_col", render: data => + '
'+data.userName+'
'+data.userId.site+'
' + }, + { title: "Group", data: null, className: "id_col", render: data => + '
'+data.groupName+'
'+data.groupId.site+'
' + }, + { title: "Metadataset", data: "id.site", className: "id_col", render: data => '' + data + ''} ].concat(DataMeta.view.buildColumns(mdata_names)) // Build table based on field names diff --git a/datameta/templates/layout.pt b/datameta/templates/layout.pt index 37d079fd..bfd5c06b 100644 --- a/datameta/templates/layout.pt +++ b/datameta/templates/layout.pt @@ -97,7 +97,7 @@ -
+