Skip to content

Commit

Permalink
tie dap review into DBIO project classes and web interface
Browse files Browse the repository at this point in the history
  • Loading branch information
RayPlante committed Aug 19, 2024
1 parent 81bd9fb commit 11b4cff
Show file tree
Hide file tree
Showing 10 changed files with 269 additions and 17 deletions.
56 changes: 56 additions & 0 deletions python/nistoar/midas/dap/review/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,59 @@
* the first value in the ``comments`` array is a user-oriented suggestion about what the user should
do. (In contrast, the ``specification`` value is expected to be a more pendantic statement.)
"""
from typing import List

from nistoar.pdr.utils.validate import Validator, ValidationResults, ALL
from .nerdm import DAPNERDmValidator
from ..nerdstore import NERDResourceStorage

class DAPReviewer(Validator):
"""
A Validator that conducts a review of a draft DAP for completeness and correctness.
"""

def __init__(self, dapvals: List[Validator]=[], nrdstor: NERDResourceStorage=None,
nerdmvals: List[Validator]=[]):
"""
:param list(Validator) dapvals: a list of validators that take a DAP Project record as its
target
:param NERDResourceStore nrdstor: The NERDResourceStorage where the NERDm record corresponding
to a requested DAP record can be retrieved
:param list(Validator) nerdmvals: a list of validators that take the DAP's NERDm document
as its target
"""
self.store = nrdstor
self.nrdvals = list(nerdmvals)
self.dapvals = list(dapvals)

def _target_name(self, prec):
return prec.id

def validate(self, prec, want=ALL, results: ValidationResults=None, targetname: str=None, **kw):
if not targetname:
targetname = self._target_name(prec)

out = results
if not out:
out = ValidationResults(targetname, want, **kw)

if self.store:
nerd = self.store.open(prec.id).get_data()
for val in self.nrdvals:
val.validate(nerd, want, out)

for val in self.dapvals:
val.validate(prec, want, out)

return out

@classmethod
def create_reviewer(cls, nrdstore: NERDResourceStorage=None):
nrdvals = []
if nrdstore:
nrdvals = [ DAPNERDmValidator() ]

dapvals = []

return cls(dapvals, nrdstore, nrdvals)

26 changes: 16 additions & 10 deletions python/nistoar/midas/dap/service/mds3.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
``_moderate_*`` functions in the :py:class:`DAPService` class.)
Support for the web service frontend is provided via :py:class:`DAPApp` class, an implementation
of the WSGI-based :ref:class:`~nistoar.pdr.publish.service.wsgi.ServiceApp`.
of the WSGI-based :ref:class:`~nistoar.web.rest.ServiceApp`.
"""
import os, re, pkg_resources, random, string, time, math
from datetime import datetime
Expand All @@ -37,6 +37,7 @@
from nistoar.pdr import def_schema_dir, def_etc_dir, constants as const
from nistoar.pdr.utils import build_mime_type_map, read_json
from nistoar.pdr.utils.prov import Agent, Action
from nistoar.pdr.utils.validate import ValidationResults, ALL

from . import validate
from .. import nerdstore
Expand Down Expand Up @@ -1645,16 +1646,23 @@ def _update_objlist(self, objlist, moderate_func, data: List[Mapping], doval: bo
# objlist.set(i, item)
# else:
# objlist.append(item)







#################


def review(self, id, want=ALL) -> ValidationResults:
"""
Review the record with the given identifier for completeness and correctness, and return lists of
suggestions for completing the record. If None is returned, review is not supported for this type
of project.
:raises ObjectNotFound: if a record with that ID does not exist
:raises NotAuthorized: if the record exists but the current user is not authorized to read it.
:return: the review results
:rtype: ValidationResults
"""
prec = self.get_record(id) # may raise exceptions
reviewer = DAPReviewer.create_reviewer(self._store)
return reviewer.validate(prec, want)

def validate_json(self, json, schemauri=None):
"""
validate the given JSON data record against the give schema, raising an exception if it
Expand Down Expand Up @@ -2395,5 +2403,3 @@ def _adv_selected_records(self, filter, perms) -> Iterator[ProjectRecord]:
for rec in self._dbcli.select_constraint_records(filter, perms):
yield to_DAPRec(rec, self._fmcli)



1 change: 1 addition & 0 deletions python/nistoar/midas/dbio/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from .. import MIDASException
from .status import RecordStatus
from nistoar.pdr.utils.prov import ANONYMOUS_USER
from nistoar.pdr.utils.validate import ValidationResults, ALL

DAP_PROJECTS = "dap"
DMP_PROJECTS = "dmp"
Expand Down
14 changes: 14 additions & 0 deletions python/nistoar/midas/dbio/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from . import status
from .. import MIDASException, MIDASSystem
from nistoar.pdr.utils.prov import Agent, Action
from nistoar.pdr.utils.validate import ValidationResults, ALL
from nistoar.id.versions import OARVersion
from nistoar.pdr import ARK_NAAN

Expand Down Expand Up @@ -191,6 +192,19 @@ def get_status(self, id) -> RecordStatus:
"""
return self.get_record(id).status

def review(self, id, want=ALL) -> ValidationResults:
"""
Review the record with the given identifier for completeness and correctness, and return lists of
suggestions for completing the record. If None is returned, review is not supported for this type
of project.
:raises ObjectNotFound: if a record with that ID does not exist
:raises NotAuthorized: if the record exists but the current user is not authorized to read it.
:return: the review results
:rtype: ValidationResults
"""
prec = self.get_record(id) # may raise exceptions
return None

def get_data(self, id, part=None):
"""
return a data content from the record with the given ID
Expand Down
67 changes: 63 additions & 4 deletions python/nistoar/midas/dbio/wsgi/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@
from collections.abc import Mapping, Sequence, Callable
from typing import Iterator
from urllib.parse import parse_qs
import json
import json, re

from nistoar.web.rest import ServiceApp, Handler, Agent
from nistoar.pdr.utils.validate import ValidationResults
from ... import dbio
from ...dbio import ProjectRecord, ProjectService, ProjectServiceFactory
from .base import DBIOHandler
Expand Down Expand Up @@ -839,12 +840,20 @@ def do_GET(self, path, ashead=False):
elif path == "action":
out = out.action
elif path == "message":
out = out.action
out = out.message
elif path == "todo":
out = self.review()
if out is None:
return self.send_error_resp(404, "todo property not accessible",
"Status property, todo, is not accessible",
self._id, ashead=ashead)
elif path:
return self.send_error_resp(404, "Status property not accessible",
"Requested status property is not accessible", self._id, ashead=ashead)
else:
out = out.to_dict()

return self.send_json(out.to_dict(), ashead=ashead)
return self.send_json(out, ashead=ashead)

def do_PUT(self, path):
"""
Expand Down Expand Up @@ -905,7 +914,57 @@ def _apply_action(self, action, message=None):

return self.send_json(stat.to_dict())

def review(self, ashead=False):
"""
run the review operation on the project record and send the results back to the client
"""
try:

res = self.svc.review(self._id)

except dbio.NotAuthorized as ex:
return self.send_unauthorized()
except dbio.ObjectNotFound as ex:
return self.send_error_resp(404, "ID not found",
"Record with requested identifier not found", self._id)

if res is None:
return self.send_error_resp(404, "todo property not accessible",
"Status property, todo, is not accessible", self._id, ashead=ashead)

return self.send_json(self.__class__.export_review(res))

@classmethod
def export_review(cls, res: ValidationResults) -> Mapping:
"""
return JSON-encodable version of review results appropriate for sending back to web clients
"""
subjre = re.compile("#(\w\S*)$")

# convert ValidationResults to todo export JSON
todo = { "req": [], "warn": [], "rec": [] }
for issue in res.failed():
jissue = { "id": issue.label, "subject": "" }
m = subjre.search(issue.label)
if m:
jissue["subject"] = m.group(1)
if len(issue.comments) > 0:
jissue["summary"] = issue.comments[0]
jissue["details"] = [ issue.specification ] + list(issue.comments[1:])
else:
jissue["summary"] = issue.specification
jissue["details"] = []

if issue._type & issue.REQ:
todo["req"].append(jissue)
elif issue._type & issue.WARN:
todo["warn"].append(jissue)
elif issue._type & issue.REC:
todo["rec"].append(jissue)

return todo



class MIDASProjectApp(ServiceApp):
"""
Expand Down Expand Up @@ -1003,4 +1062,4 @@ def factory_for(cls, project_coll):





4 changes: 2 additions & 2 deletions python/nistoar/pdr/utils/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def specification(self):
def specification(self, text):
self._spec = text

type_labels = { REQ: "error", WARN: "warning", REC: "recommendation" }
type_labels = { REQ: "requirement", WARN: "warning", REC: "recommendation" }
REQ_LAB = type_labels[REQ]
ERROR_LAB = type_labels[REQ]
WARN_LAB = type_labels[WARN]
Expand Down Expand Up @@ -409,7 +409,7 @@ def validate(self, target, want=ALL, results=None, targetname: str=None, **kw):

out = results
if not out:
out = ValidationResults(bag.name, want, **kw)
out = ValidationResults(self._target_name(target), want, **kw)

for v in self._vals:
v.validate(bag, want, out)
Expand Down
5 changes: 5 additions & 0 deletions python/tests/nistoar/midas/dap/review/test_nerdm.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ def test_test_simple_props(self):
self.assertEqual(res.failed()[0].comments[0], "Add a description")
self.assertEqual(res.failed()[1].comments[0], "Add some keywords")


def test_validate(self):
res = self.val.validate(self.nerd)
self.assertEqual(res.count_applied(), 3)
self.assertEqual(res.count_passed(), 3)



Expand Down
63 changes: 63 additions & 0 deletions python/tests/nistoar/midas/dap/review/test_review.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import os, sys, pdb, json, tempfile, logging
import unittest as test
from pathlib import Path

import nistoar.midas.dap.review as rev
from nistoar.midas.dap.review.nerdm import DAPNERDmValidator
from nistoar.midas.dbio import ProjectRecord, DAP_PROJECTS
from nistoar.midas.dbio.wsgi.project import ProjectStatusHandler
import nistoar.pdr.utils.validate as base
from nistoar.pdr.utils.io import read_nerd
from nistoar.midas.dap.nerdstore.inmem import InMemoryResourceStorage

testdir = Path(__file__).parent
datadir = testdir.parent / "data"
sipdir = datadir / "mdssip"/"mdst:1491"

class TestDAPReviewer(test.TestCase):

def setUp(self):
nerd = read_nerd(sipdir/"nerdm.json")
self.store = InMemoryResourceStorage()
self.store.load_from(nerd, "mdst:1491")
self.prec = ProjectRecord(DAP_PROJECTS, { "id": "mdst:1491", "data": {}, "meta": {} })
self.reviewer = rev.DAPReviewer.create_reviewer(self.store)


def test_ctor(self):
self.assertIs(self.reviewer.store, self.store)
self.assertEqual(len(self.reviewer.nrdvals), 1)
self.assertTrue(isinstance(self.reviewer.nrdvals[0], DAPNERDmValidator))
self.assertEqual(len(self.reviewer.dapvals), 0)

def test_validate(self):
res = self.reviewer.validate(self.prec)
self.assertEqual(res.target, "mdst:1491")
self.assertEqual(res.count_applied(), 3)
self.assertEqual(res.count_passed(), 3)

def test_export(self):
self.store.load_from({"@id": "mdst-1492"}, "mdst:1492")
self.prec = ProjectRecord(DAP_PROJECTS, { "id": "mdst:1492", "data": {}, "meta": {} })
self.reviewer = rev.DAPReviewer.create_reviewer(self.store)

res = self.reviewer.validate(self.prec)
self.assertEqual(res.target, "mdst:1492")
self.assertEqual(res.count_applied(), 3)
self.assertEqual(res.count_failed(), 3)

todo = ProjectStatusHandler.export_review(res)
self.assertEqual(len(todo.get("req", [])), 3)
self.assertEqual(len(todo.get("warn", [1])), 0)
self.assertEqual(len(todo.get("rec", [1])), 0)

self.assertEqual(todo["req"][0].get("subject"), "title")
self.assertEqual(todo["req"][0].get("summary"), "Add a title")
self.assertIn("title", todo["req"][0].get("details", [""])[0])


if __name__ == '__main__':
test.main()



6 changes: 6 additions & 0 deletions python/tests/nistoar/midas/dbio/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,13 @@ def test_finalize(self):
with self.assertRaises(project.NotEditable):
self.project.finalize(prec.id)

def test_default_review(self):
self.create_service()
prec = self.project.create_record("goob")
self.assertIsNone(self.project.review(prec.id))

with self.assertRaises(project.ObjectNotFound):
self.project.review("goober")


class TestProjectServiceFactory(test.TestCase):
Expand Down
Loading

0 comments on commit 11b4cff

Please sign in to comment.