""" ARCHES - a program developed to inventory and manage immovable cultural heritage. Copyright (C) 2013 J. Paul Getty Trust and World Monuments Fund This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . """ import json import time import uuid from unittest.mock import patch from django.contrib.auth.models import User, Group from django.db import connection from django.urls import reverse from django.test.client import Client from django.test.utils import CaptureQueriesContext from guardian.shortcuts import assign_perm, get_perms from arches.app.models import models from arches.app.models.graph import Graph from arches.app.models.resource import Resource from arches.app.models.tile import Tile from arches.app.utils.betterJSONSerializer import JSONSerializer from arches.app.utils.exceptions import ( InvalidNodeNameException, MultipleNodesFoundException, ) from arches.app.utils.index_database import ( index_resources_by_type, index_resources_using_singleprocessing, ) from arches.test.utils import sync_overridden_test_settings_to_arches from tests.base_test import ArchesTestCase from django.test import override_settings # these tests can be run from the command line via # python manage.py test tests.models.resource_test --settings="tests.test_settings" class ResourceTests(ArchesTestCase): graph_fixtures = ["Resource Test Model"] @classmethod def setUpTestData(cls): super().setUpTestData() cls.client = Client() cls.client.login(username="admin", password="admin") cls.search_model_graphid = uuid.UUID("c9b37a14-17b3-11eb-a708-acde48001122") cls.search_model_cultural_period_nodeid = "c9b3882e-17b3-11eb-a708-acde48001122" cls.search_model_creation_date_nodeid = "c9b38568-17b3-11eb-a708-acde48001122" cls.search_model_destruction_date_nodeid = ( "c9b3828e-17b3-11eb-a708-acde48001122" ) cls.search_model_name_nodeid = "c9b37b7c-17b3-11eb-a708-acde48001122" cls.search_model_sensitive_info_nodeid = "c9b38aea-17b3-11eb-a708-acde48001122" cls.search_model_geom_nodeid = "c9b37f96-17b3-11eb-a708-acde48001122" cls.user = User.objects.create_user( "test", "test@archesproject.org", "password" ) cls.user.groups.add(Group.objects.get(name="Guest")) graph = Graph.objects.get(pk=cls.search_model_graphid) graph.publish(user=cls.user) nodegroup = models.NodeGroup.objects.get( pk=cls.search_model_destruction_date_nodeid ) assign_perm("no_access_to_nodegroup", cls.user, nodegroup) # Add a concept that defines a min and max date concept = { "id": "00000000-0000-0000-0000-000000000001", "legacyoid": "ARCHES", "nodetype": "ConceptScheme", "values": [], "subconcepts": [ { "values": [ { "value": "Mock concept", "language": "en", "category": "label", "type": "prefLabel", "id": "", "conceptid": "", }, { "value": "1950", "language": "en", "category": "note", "type": "min_year", "id": "", "conceptid": "", }, { "value": "1980", "language": "en", "category": "note", "type": "max_year", "id": "", "conceptid": "", }, ], "relationshiptype": "hasTopConcept", "nodetype": "Concept", "id": "", "legacyoid": "", "subconcepts": [], "parentconcepts": [], "relatedconcepts": [], } ], } post_data = JSONSerializer().serialize(concept) content_type = "application/x-www-form-urlencoded" response = cls.client.post( reverse( "concept", kwargs={"conceptid": "00000000-0000-0000-0000-000000000001"} ), post_data, content_type, ) response_json = json.loads(response.content) valueid = response_json["subconcepts"][0]["values"][0]["id"] cls.conceptid = response_json["subconcepts"][0]["id"] # Add resource with Name, Cultural Period, Creation Date and Geometry cls.test_resource = Resource(graph_id=cls.search_model_graphid) # Add Name tile = Tile( data={ cls.search_model_name_nodeid: { "en": {"value": "Test Name 1"}, "es": {"value": "Prueba Nombre 1"}, } }, nodegroup_id=cls.search_model_name_nodeid, ) cls.test_resource.tiles.append(tile) # Add Cultural Period tile = Tile( data={cls.search_model_cultural_period_nodeid: [valueid]}, nodegroup_id=cls.search_model_cultural_period_nodeid, ) cls.test_resource.tiles.append(tile) # Add Creation Date tile = Tile( data={cls.search_model_creation_date_nodeid: "1941-01-01"}, nodegroup_id=cls.search_model_creation_date_nodeid, ) cls.test_resource.tiles.append(tile) # Add Geometry cls.geom = { "type": "FeatureCollection", "features": [ { "geometry": {"type": "Point", "coordinates": [0, 0]}, "type": "Feature", "properties": {}, } ], } tile = Tile( data={cls.search_model_geom_nodeid: cls.geom}, nodegroup_id=cls.search_model_geom_nodeid, ) cls.test_resource.tiles.append(tile) cls.test_resource.save() # add delay to allow for indexes to be updated time.sleep(1) def test_get_node_value_string(self): """ Query a string value """ node_name = "Name" result = self.test_resource.get_node_values(node_name) self.assertEqual("Test Name 1", result[0]["en"]["value"]) self.assertEqual("Prueba Nombre 1", result[0]["es"]["value"]) def test_get_node_value_date(self): """ Query a date value """ node_name = "Creation Date" result = self.test_resource.get_node_values(node_name) self.assertEqual("1941-01-01", result[0]) def test_get_node_value_concept(self): """ Query a concept value """ node_name = "Cultural Period Concept" result = self.test_resource.get_node_values(node_name) self.assertEqual("Mock concept", result[0]) def test_get_not_existing_value_from_concept(self): """ Query a concept node without a value """ test_resource_no_value = Resource(graph_id=self.search_model_graphid) tile = Tile( data={self.search_model_cultural_period_nodeid: ""}, nodegroup_id=self.search_model_cultural_period_nodeid, ) test_resource_no_value.tiles.append(tile) test_resource_no_value.save() node_name = "Cultural Period Concept" result = test_resource_no_value.get_node_values(node_name) self.assertEqual(None, result[0]) test_resource_no_value.delete() def test_get_value_from_not_existing_concept(self): """ Query a concept value that does not exist """ node_name = "Not Existing Concept" with self.assertRaises(InvalidNodeNameException): self.test_resource.get_node_values(node_name) def test_get_duplicate_node_value_concept(self): """ Query a concept value on a node that exists twice """ node_name = "Duplicate Node Concept" with self.assertRaises(MultipleNodesFoundException): self.test_resource.get_node_values(node_name) def test_get_node_value_geometry(self): """ Query a geometry value """ node_name = "Geometry" result = self.test_resource.get_node_values(node_name) self.assertEqual(self.geom, result[0]) def test_reindex_by_resource_type(self): """ Test re-index a resource by type """ time.sleep(1) result = index_resources_by_type( [self.search_model_graphid], clear_index=True, batch_size=4000 ) self.assertEqual(result, "Passed") @override_settings( ELASTICSEARCH_CUSTOM_INDEXES=[ { "module": "arches.app.search.base_index.BaseIndex", "name": "mock", "should_update_asynchronously": True, } ] ) @patch("arches.app.search.base_index.BaseIndex.delete_resources") def test_delete_acts_on_custom_indices(self, mock): other_resource = Resource(pk=uuid.uuid4()) with sync_overridden_test_settings_to_arches(): self.test_resource.delete_index(other_resource.pk) self.assertIn(str(other_resource.pk), str(mock._mock_call_args)) def test_publication_restored_on_save(self): """ If a resource lacks a graph publication, it is restored by a call to save(). """ # Hack out the graph publication (bypass the guard in save()) models.ResourceInstance.objects.filter(pk=self.test_resource.pk).update( graph_publication=None ) self.test_resource.refresh_from_db() # Ensure test setup is good self.assertIsNone(self.test_resource.graph_publication) # update_or_create() delegates to save() obj, created = models.ResourceInstance.objects.filter( pk=self.test_resource.pk ).update_or_create( pk=self.test_resource.pk, graph=self.test_resource.graph, ) obj.refresh_from_db() # give test opportunity to fail on Django 4.2+ self.assertIsNotNone(obj.graph_publication) def test_creator_has_permissions(self): """ Test user that created instance has full permissions """ user = User.objects.create_user( username="sam", email="sam@samsclub.com", password="Test12345!" ) user.save() group = Group.objects.get(name="Resource Editor") group.user_set.add(user) test_resource = Resource(graph_id=self.search_model_graphid) test_resource.save(user=user) perms = set(get_perms(user, test_resource)) self.assertNotEqual( perms, { "view_resourceinstance", "change_resourceinstance", "delete_resourceinstance", }, ) self.assertEqual(test_resource.principaluser, user) def test_provisional_user_can_delete_own_resource(self): """ Test provisional user can delete resource instance they created """ user = User.objects.create_user( username="sam", email="sam@samsclub.com", password="Test12345!" ) user.save() group = Group.objects.get(name="Resource Editor") group.user_set.add(user) test_resource = Resource(graph_id=self.search_model_graphid) test_resource.save(user=user) other_user = User.objects.create_user( username="fred", email="fred@samsclub.com", password="Test12345!" ) other_user.save() group = Group.objects.get(name="Resource Editor") group.user_set.add(other_user) with self.subTest(user="can't delete"): result = test_resource.delete(user=other_user) self.assertFalse(result) with self.subTest(user="can delete"): result = test_resource.delete(user=user) self.assertTrue(result) with self.subTest(user="can't delete"): test_resource = Resource(graph_id=self.search_model_graphid) test_resource.save(user=user) edit_log_entry = models.EditLog.objects.get( resourceinstanceid=test_resource.pk, edittype="create" ) edit_log_entry.userid = "" edit_log_entry.save() result = test_resource.delete(user=user) self.assertFalse(result) def test_recalculate_descriptors_prefetch_related_objects(self): r1 = Resource(graph_id=self.search_model_graphid) r2 = Resource(graph_id=self.search_model_graphid) r1_tile = Tile( data={self.search_model_creation_date_nodeid: "1941-01-01"}, nodegroup_id=self.search_model_creation_date_nodeid, ) r1.tiles.append(r1_tile) r2_tile = Tile( data={self.search_model_creation_date_nodeid: "1941-01-01"}, nodegroup_id=self.search_model_creation_date_nodeid, ) r2.tiles.append(r2_tile) r1.save(index=False) r2.save(index=False) # Ensure we start from scratch r1.descriptor_function = None r2.descriptor_function = None for test_name, resources in ( ("array", [r1, r2]), ("queryset", Resource.objects.filter(pk__in=[r1.pk, r2.pk])), ): with ( self.subTest(iterable=test_name), CaptureQueriesContext(connection) as queries, ): index_resources_using_singleprocessing( resources, recalculate_descriptors=True, quiet=True ) function_x_graph_selects = [ q for q in queries if q["sql"].startswith('SELECT "functions_x_graphs"."id"') ] self.assertEqual(len(function_x_graph_selects), 1) tile_selects = [ q for q in queries if q["sql"].startswith('SELECT "tiles"."tileid"') ] self.assertEqual(len(tile_selects), 1) def test_self_referring_resource_instance_descriptor(self): # Create a nodegroup with a string node and a resource-instance node. graph = Graph.new(name="Self-referring descriptor test", is_resource=True) node_group = models.NodeGroup.objects.create() string_node = models.Node.objects.create( graph=graph, nodegroup=node_group, name="String Node", datatype="string", istopnode=False, ) resource_instance_node = models.Node.objects.create( graph=graph, nodegroup=node_group, name="Resource Node", datatype="resource-instance", istopnode=False, ) # Configure the primary descriptor to use the string node models.FunctionXGraph.objects.create( graph=graph, function_id="60000000-0000-0000-0000-000000000001", config={ "descriptor_types": { "name": { "nodegroup_id": str(node_group.nodegroupid), # The bug report did not have in the descriptor # template, but including it here to allow the assertion to fail "string_template": " ", }, "map_popup": { "nodegroup_id": None, "string_template": "", }, "description": { "nodegroup_id": None, "string_template": "", }, }, }, ) # Create a tile that references itself resource = models.ResourceInstance.objects.create(graph=graph) tile = models.TileModel.objects.create( nodegroup=node_group, resourceinstance=resource, data={ str(string_node.pk): { "en": {"value": "test value", "direction": "ltr"}, }, str(resource_instance_node.pk): { "resourceId": str(resource.pk), "ontologyProperty": "", "inverseOntologyProperty": "", }, }, sortorder=0, ) models.ResourceXResource.objects.create( nodeid=resource_instance_node, resourceinstanceidfrom=resource, resourceinstanceidto=resource, tileid=tile, ) r = Resource.objects.get(pk=resource.pk) r.save_descriptors() # Until 7.4, a RecursionError was caught after this value was repeated many times. self.assertEqual(r.displayname(), "test value ") @patch("django.contrib.auth.models.User.has_perm") def test_user_can_see_edit_history_if_resource_editor(self, mock_has_perm): user = User.objects.create_user( username="john", email="john@archesproject.org", password="Test12345!" ) user.save() group = Group.objects.get(name="Resource Editor") group.user_set.add(user) self.client.login(username="john", password="Test12345!") self.client.get(reverse("resource_edit_log", args=[self.test_resource.pk])) mock_has_perm.assert_any_call( "read_nodegroup", self.test_resource.tiles[0].nodegroup )