Repository: RockefellerArchiveCenter/DACSspace

Issue:
Create reporting class
As part of #21, create a class which handles reporting logic. Currently this is written to a CSV but we could also print it to stdout, or possibly other formats.

Comments:

Patch:
diff --git a/dacsspace/reporter.py b/dacsspace/reporter.py
index 731b7a8..514a247 100644
--- a/dacsspace/reporter.py
+++ b/dacsspace/reporter.py
@@ -1,10 +1,13 @@
+import csv
+
 
 class CSVReporter:
     """Creates CSV reports."""
 
-    def __init__(self):
-        # TODO: set filepath for CSV
-        pass
+    def __init__(self, filename, filemode="w"):
+        """Sets the filename and filemode."""
+        self.filename = filename
+        self.filemode = filemode
 
     def write_report(self, results, invalid_only=True):
         """Writes results to a CSV file.
@@ -13,4 +16,15 @@ class CSVReporter:
             results (list): A list of dictionaries containing information about validation results.
             invalid_only (boolean): Only report on invalid results.
         """
-        pass
+
+        if self.filemode.startswith("r"):
+            raise ValueError("Filemode must allow write options.")
+        with open(self.filename, self.filemode) as f:
+            fieldnames = [
+                "valid",
+                "explanation"]
+            writer = csv.DictWriter(
+                f, fieldnames=fieldnames)
+            writer.writeheader()
+            filtered_results = [row for row in results if not row["valid"]] if invalid_only else results
+            writer.writerows(filtered_results)
diff --git a/dacsspace/validator.py b/dacsspace/validator.py
index bde8b8c..9f0fa71 100644
--- a/dacsspace/validator.py
+++ b/dacsspace/validator.py
@@ -1,25 +1,13 @@
-
+import json
 from jsonschema import ValidationError, validate
 
 
 class Validator:
     """Validates data from ArchivesSpace."""
 
-    def get_schema(self):
-        schema = {
-            "type": "object",
-            "title": "DACSspace schema",
-            "required": [
-                "title",
-                "id_0"
-            ],
-            "properties": {
-                "title": {
-                    "type": "string"
-                }
-            }
-        }
-        return schema
+    def __init__(self):
+        with open("single_level_required.json", "r") as json_file:
+            self.schema = json.load(json_file)
 
     def validate_data(self, data):
         """Validates data.
@@ -32,9 +20,8 @@ class Validator:
            indication of the validation result and, if necessary, an explanation
            of any validation errors. { "valid": False, "explanation": "You are missing the following fields..." }
         """
-        schema = self.get_schema()
         try:
-            validate(data, schema)
+            validate(data, self.schema)
             return {"valid": True}
         except ValidationError as error:
             return {"valid": False, "explanation": error.message}
diff --git a/fixtures/invalid_resource.json b/fixtures/multiple_invalid.json
similarity index 100%
rename from fixtures/invalid_resource.json
rename to fixtures/multiple_invalid.json
diff --git a/fixtures/no_metadata_rights.json b/fixtures/no_metadata_rights.json
new file mode 100644
index 0000000..18ec544
--- /dev/null
+++ b/fixtures/no_metadata_rights.json
@@ -0,0 +1,160 @@
+{
+    "lock_version": 17,
+    "title": "Henry Luce Foundation records, Clare Boothe Luce Program",
+    "publish": true,
+    "restrictions": false,
+    "ead_id": "FA1725.xml",
+    "finding_aid_title": "A Guide to the Henry Luce Foundation records, Clare Boothe Luce Program",
+    "finding_aid_filing_title": "Henry Luce Foundation records, Clare Boothe Luce Program",
+    "created_by": "aberish",
+    "last_modified_by": "battalb",
+    "create_time": "2020-02-14T16:21:48Z",
+    "system_mtime": "2021-12-22T15:31:27Z",
+    "user_mtime": "2021-11-05T15:52:48Z",
+    "suppressed": false,
+    "is_slug_auto": false,
+    "id_0": "FA1725",
+    "level": "recordgrp",
+    "resource_type": "records",
+    "finding_aid_description_rules": "Describing Archives: A Content Standard",
+    "finding_aid_language": "eng",
+    "finding_aid_script": "Latn",
+    "jsonmodel_type": "resource",
+    "external_ids": [],
+    "subjects": [
+        {
+            "ref": "/subjects/1725"
+        },
+        {
+            "ref": "/subjects/20081"
+        }
+    ],
+    "linked_events": [],
+    "extents": [
+        {
+            "lock_version": 0,
+            "number": "11.44",
+            "container_summary": "16 letter-sized document boxes",
+            "created_by": "battalb",
+            "last_modified_by": "battalb",
+            "create_time": "2021-11-05T15:53:05Z",
+            "system_mtime": "2021-11-05T15:53:05Z",
+            "user_mtime": "2021-11-05T15:53:05Z",
+            "portion": "whole",
+            "extent_type": "Cubic Feet",
+            "jsonmodel_type": "extent"
+        }
+    ],
+    "lang_materials": [
+        {
+            "lock_version": 0,
+            "created_by": "battalb",
+            "last_modified_by": "battalb",
+            "create_time": "2021-11-05T15:53:05Z",
+            "system_mtime": "2021-11-05T15:53:05Z",
+            "user_mtime": "2021-11-05T15:53:05Z",
+            "jsonmodel_type": "lang_material",
+            "notes": [],
+            "language_and_script": {
+                "lock_version": 0,
+                "created_by": "battalb",
+                "last_modified_by": "battalb",
+                "create_time": "2021-11-05T15:53:05Z",
+                "system_mtime": "2021-11-05T15:53:05Z",
+                "user_mtime": "2021-11-05T15:53:05Z",
+                "language": "eng",
+                "jsonmodel_type": "language_and_script"
+            }
+        }
+    ],
+    "dates": [
+        {
+            "lock_version": 0,
+            "expression": "1986-2014",
+            "begin": "1986",
+            "end": "2014",
+            "created_by": "battalb",
+            "last_modified_by": "battalb",
+            "create_time": "2021-11-05T15:53:05Z",
+            "system_mtime": "2021-11-05T15:53:05Z",
+            "user_mtime": "2021-11-05T15:53:05Z",
+            "date_type": "inclusive",
+            "label": "creation",
+            "jsonmodel_type": "date"
+        }
+    ],
+    "external_documents": [],
+    "rights_statements": [],
+    "linked_agents": [
+        {
+            "role": "creator",
+            "relator": "aut",
+            "terms": [],
+            "ref": "/agents/corporate_entities/4246"
+        }
+    ],
+    "revision_statements": [],
+    "instances": [],
+    "deaccessions": [],
+    "related_accessions": [
+        {
+            "ref": "/repositories/2/accessions/3971"
+        }
+    ],
+    "classifications": [],
+    "notes": [
+        {
+            "jsonmodel_type": "note_multipart",
+            "persistent_id": "bf2011f3353f37f389577d36cd942721",
+            "type": "accessrestrict",
+            "rights_restriction": {
+                "local_access_restriction_type": []
+            },
+            "subnotes": [
+                {
+                    "jsonmodel_type": "note_text",
+                    "content": "Open for research with select materials restricted as noted. Brittle or damaged items are available at the discretion of RAC.",
+                    "publish": true
+                }
+            ],
+            "publish": true
+        },
+        {
+            "jsonmodel_type": "note_multipart",
+            "persistent_id": "b4ad37dbd0891b871c8fe5d3e969f487",
+            "type": "userestrict",
+            "rights_restriction": {
+                "local_access_restriction_type": []
+            },
+            "subnotes": [
+                {
+                    "jsonmodel_type": "note_text",
+                    "content": "RAC holds legal title. The Henry Luce Foundation retains all applicable intellectual property rights in so far as it holds them.",
+                    "publish": true
+                }
+            ],
+            "publish": true
+        },
+        {
+            "jsonmodel_type": "note_multipart",
+            "persistent_id": "a0fb232b40ebc33ff21f51fc5fa71017",
+            "type": "scopecontent",
+            "subnotes": [
+                {
+                    "jsonmodel_type": "note_text",
+                    "content": "The Henry Luce Foundation was established by Henry R. Luce in 1936. As a New York-based nonprofit organization, the Luce Foundation awards grants in American Art, Asian studies, Higher Education, Theology, and supports the Clare Boothe Luce Program for women in science, mathematics, and engineering.",
+                    "publish": true
+                }
+            ],
+            "publish": true
+        }
+    ],
+    "metadata_rights_declarations": [],
+    "uri": "/repositories/2/resources/13192",
+    "repository": {
+        "ref": "/repositories/2"
+    },
+    "tree": {
+        "ref": "/repositories/2/resources/13192/tree"
+    }
+}
diff --git a/fixtures/valid_resource.json b/fixtures/valid_resource.json
index 18ec544..62c4457 100644
--- a/fixtures/valid_resource.json
+++ b/fixtures/valid_resource.json
@@ -149,7 +149,7 @@
             "publish": true
         }
     ],
-    "metadata_rights_declarations": [],
+    "metadata_rights_declarations": ["ADD SOMETHING LATER"],
     "uri": "/repositories/2/resources/13192",
     "repository": {
         "ref": "/repositories/2"
diff --git a/single_level_required.json b/single_level_required.json
new file mode 100644
index 0000000..848a030
--- /dev/null
+++ b/single_level_required.json
@@ -0,0 +1,47 @@
+{
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "$id": "single_level_required.json",
+    "type": "object",
+    "title": "DACSspace Single Level Required Schema",
+    "required": [
+        "id_0",
+        "title",
+        "dates",
+        "extents",
+        "linked_agents",
+        "lang_materials",
+        "metadata_rights_declarations"
+    ],
+    "properties": {
+        "id_0": {
+            "type": "string"
+        },
+        "title": {
+            "type": "string"
+        },
+        "dates": {
+            "type": "array",
+            "minItems": 1
+        },
+        "extents": {
+            "type": "array",
+            "minItems": 1
+        },
+        "linked_agents": {
+            "type": "array",
+            "minItems": 1
+        },
+        "notes": {
+            "type": "array",
+            "minItems": 1
+        },
+        "lang_materials": {
+            "type": "array",
+            "minItems": 1
+        },
+        "metadata_rights_declarations": {
+            "type": "array",
+            "minItems": 1
+        }
+    }
+}
\ No newline at end of file
diff --git a/tox.ini b/tox.ini
index c3ec76b..32e8736 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py39
+envlist = py310
 skipsdist = True
 
 [testenv]


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_reporter.py b/tests/test_reporter.py
index f2d4612..92b27c9 100644
--- a/tests/test_reporter.py
+++ b/tests/test_reporter.py
@@ -1,7 +1,44 @@
-# from unittest.mock import patch
-#
-# from dacsspace.reporter import CSVReporter
+import os
+from unittest import TestCase
+from unittest.mock import patch
 
+from dacsspace.reporter import CSVReporter
 
-def test_reporter():
-    pass
+
+class CSVReporterTest(TestCase):
+
+    def setUp(self):
+        """Sets filename and data attributes for test file.
+
+        Checks if test file exists, then deletes it.
+        """
+        self.filename = "DACSSpace_results"
+        self.results = [{"valid": True, "explanation": None}, {"valid": False, "explanation": "No title"}]
+        if os.path.isfile(self.filename):
+            os.remove(self.filename)
+
+    def test_CSV(self):
+        """Asserts that the results are correctly written to the file.
+
+        Raises an error if the file has an incorrect filemode and asserts that the filemode must allow write options.
+        """
+        CSVReporter(self.filename).write_report(self.results)
+        self.assertTrue(self.filename)
+        with self.assertRaises(ValueError) as err:
+            CSVReporter(self.filename, "r").write_report(self.results)
+        self.assertEqual(str(err.exception), "Filemode must allow write options.")
+
+    @patch("csv.DictWriter.writerows")
+    def test_invalid(self, mock_writerows):
+        """Mocks writing only invalid results and valid results to file."""
+        CSVReporter(self.filename).write_report(self.results)
+        mock_writerows.assert_called_with([{"valid": False, "explanation": "No title"}])
+        CSVReporter(self.filename).write_report(self.results, invalid_only=False)
+        mock_writerows.assert_called_with(self.results)
+
+    def tearDown(self):
+        """Tears down test file.
+
+        Checks if test file exists, then deletes it."""
+        if os.path.isfile(self.filename):
+            os.remove(self.filename)
diff --git a/tests/test_validator.py b/tests/test_validator.py
index 3796a93..ca424a5 100644
--- a/tests/test_validator.py
+++ b/tests/test_validator.py
@@ -10,14 +10,15 @@ from dacsspace.validator import Validator
 class TestValidator(unittest.TestCase):
     def test_validator(self):
         valid_json = "fixtures/valid_resource.json"
-        multiple_invalid = "fixtures/invalid_resource.json"
+        invalid_fixtures = ["fixtures/multiple_invalid.json", "fixtures/no_metadata_rights.json"]
         with open(valid_json, 'r') as v:
             valid_json = json.load(v)
             result = Validator().validate_data(valid_json)
         self.assertTrue(isinstance(result, dict))
         self.assertEqual(result["valid"], True)
-        with open(multiple_invalid, 'r') as i:
-            multiple_invalid = json.load(i)
-            result = Validator().validate_data(multiple_invalid)
-        self.assertTrue(isinstance(result, dict))
-        self.assertEqual(result["valid"], False)
+        for f in invalid_fixtures:
+            with open(f, 'r') as i:
+                invalid_json = json.load(i)
+                result = Validator().validate_data(invalid_json)
+            self.assertTrue(isinstance(result, dict))
+            self.assertEqual(result["valid"], False)