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 multi-card primary descriptor #1368 #1452

Merged
merged 27 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6ceef83
Fix typo in TimeSpan of Part Removal Event card
jacobtylerwalls Nov 20, 2023
11ec6a0
Retire the default primary descriptor #1368
jacobtylerwalls Nov 20, 2023
366ebe3
Implement frontend component
jacobtylerwalls Nov 21, 2023
de7ac54
Node names -> node aliases in string templates
jacobtylerwalls Nov 21, 2023
3124e29
Python backend for resource descriptor just fetches db value
jacobtylerwalls Nov 21, 2023
e08b19d
Add FUNCTION_LOCATIONS to test settings
jacobtylerwalls Nov 28, 2023
885e7df
Populate select with nodes from template value
jacobtylerwalls Nov 21, 2023
144ca60
Add django-pgtrigger
jacobtylerwalls Nov 21, 2023
1a88efb
Implement trigger
jacobtylerwalls Nov 28, 2023
87484d3
Add trigger for FunctionXGraph
jacobtylerwalls Nov 28, 2023
7ec9d60
Add migration
jacobtylerwalls Nov 28, 2023
366f5a0
Remove contenttypes dependency from second migration
jacobtylerwalls Nov 28, 2023
ee07138
Fix trimming issue and save to name field
jacobtylerwalls Nov 29, 2023
033511a
[cosmetic] Avoid string replacement, break function into chunks
jacobtylerwalls Nov 29, 2023
13f025d
Handle missing string values with ' -- '
jacobtylerwalls Dec 1, 2023
e5a0576
Handle a completely static descriptor template
jacobtylerwalls Dec 1, 2023
1c91bf6
Removes deferral so trigger is fired before instance is indexed
chiatt Dec 1, 2023
25a36e5
Use `<select>` tag
jacobtylerwalls Dec 4, 2023
583ef44
Instruct to extend `FUNCTION_LOCATIONS`
jacobtylerwalls Dec 4, 2023
e4a4caf
Add missing migration re #1368
jacobtylerwalls Dec 4, 2023
36e5911
Consolidate to one migration for triggers
jacobtylerwalls Dec 4, 2023
a56915f
Two more `<input>` -> `<select>`
jacobtylerwalls Dec 4, 2023
6745159
Flatten data array re #1368
jacobtylerwalls Dec 4, 2023
bb5661f
Sort cards by name re #1368
jacobtylerwalls Dec 4, 2023
55b766b
create -> update_or_create
jacobtylerwalls Dec 12, 2023
9fd24fa
Add FunctionXGraph related changes to migration re #1368
jacobtylerwalls Dec 12, 2023
2175749
nit re #1368
jacobtylerwalls Dec 12, 2023
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ If you don't already have an Arches project, you'll need to create one by follow
Since Arches for Science uses `Cantaloupe` as its IIIF server, take notice of the
Cantaloupe [installation instructions](https://arches.readthedocs.io/en/stable/developing/advanced/managing-and-hosting-iiif/), too.

When your project is ready add "arches_templating" and "arches_for_science" to INSTALLED_APPS and "arches_for_science" to ARCHES_APPLICATIONS in your project's settings.py file:
When your project is ready add "arches_templating", "arches_for_science", and "pgtrigger" to INSTALLED_APPS and "arches_for_science" to ARCHES_APPLICATIONS in your project's settings.py file:
```
INSTALLED_APPS = (
...
"arches_templating",
"arches_for_science",
"pgtrigger",
"myappname",
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
define([
'jquery',
'underscore',
'arches',
'knockout',
'knockout-mapping',
'views/list',
'views/components/functions/primary-descriptors',
'templates/views/components/functions/multicard-resource-descriptor.htm',
'bindings/select2-query',
],
function($, _, arches, ko, koMapping, ListView, PrimaryDescriptorsView, multicardResourceDescriptor) {
// Get the parent component we want to inherit from from the ko registry.
let parentComponent;
const setParentComponent = (found) => {
parentComponent = found;
}
ko.components.defaultLoader.getConfig('views/components/functions/primary-descriptors', setParentComponent);

return ko.components.register('views/components/functions/multicard-resource-descriptor', {
viewModel: function(params) {
var self = this;
parentComponent.viewModel.apply(this, arguments);

this.parseNodeIdsFromStringTemplate = (initialValue) => {
const regex = /<(.*?)>/g;
const aliases = [...initialValue.matchAll(regex)].map(matchObj => matchObj[1]);
return self.graph.nodes.filter(n => aliases.includes(n.alias)).map(n => n.nodeid);
}

this.selectedNodes = {
name: ko.observableArray(
self.parseNodeIdsFromStringTemplate(self.name.string_template())
),
description: ko.observableArray(
self.parseNodeIdsFromStringTemplate(self.description.string_template())
),
map_popup: ko.observableArray(
self.parseNodeIdsFromStringTemplate(self.map_popup.string_template())
),
};

this.groupedNodesForSelect2 = self.graph.cards.map(card => {
return {
text: card.name,
children: self.graph.nodes.filter(
node => node.datatype === 'string' && node.nodegroup_id === card.nodegroup_id
).map(node => {
return {
id: node.nodeid,
text: node.alias,
}
}),
}
});

Object.entries(this.selectedNodes).forEach(
([observableName, observable]) => {
observable.subscribe(actions => {
actions.forEach(action => {
self.updateTemplate(action.value, action.status, observableName)
})
}, this, 'arrayChange')
}
);

this.baseSelect2Config = {
multiple: true,
placeholder: arches.translations.selectPrimaryDescriptionIdentifierCard,
data: {results: self.groupedNodesForSelect2},
};

this.updateTemplate = (nodeid, actionType, descriptorName) => {
const templateObservable = params.config.descriptor_types[descriptorName].string_template;
const priorValue = templateObservable();
const nodeAlias = self.graph.nodes.find(n => n.nodeid === nodeid).alias;

if (actionType === 'deleted') {
if (priorValue.startsWith(`<${nodeAlias}>`)) {
templateObservable(priorValue.replace(`<${nodeAlias}>`, ''));
} else {
templateObservable(priorValue.replace(` <${nodeAlias}>`, ''));
}
} else {
if (priorValue === '') {
templateObservable(`<${nodeAlias}>`);
} else {
templateObservable(`${priorValue} <${nodeAlias}>`);
}
}
};
},
template: multicardResourceDescriptor,
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ def assign_view_plugin(apps, schema_editor):

class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("arches_for_science", "0001_initial"),
("guardian", "0002_generic_permissions_index"),
]
Expand Down
47 changes: 47 additions & 0 deletions arches_for_science/migrations/0004_improve_primary_descriptor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Generated by Django 4.2.4 on 2023-11-20 13:43

from django.db import migrations


def retire_core_primary_descriptor(apps, schema_editor):
Function = apps.get_model("models", "Function")
fn = Function.objects.get(pk="60000000-0000-0000-0000-000000000001")
fn.functiontype = "primarydescriptors_retired"
fn.name = "(retired) Define Resource Descriptors"
fn.save()

# create multi-card function
Function.objects.create(
pk="00b2d15a-fda0-4578-b79a-784e4138664b",
modulename="multicard_resource_descriptor.py",
classname="MulticardResourceDescriptor",
functiontype="primarydescriptors",
name="Multi-card Resource Descriptor",
description="Configure the name, description, and map popup of a resource",
defaultconfig=fn.defaultconfig,
component="views/components/functions/multicard-resource-descriptor",
)


def restore_core_primary_descriptor(apps, schema_editor):
Function = apps.get_model("models", "Function")
fn = Function.objects.get(pk="60000000-0000-0000-0000-000000000001")
fn.functiontype = "primarydescriptors"
fn.name = "Define Resource Descriptors"
fn.save()

multi_card_fn = Function.objects.get(pk="00b2d15a-fda0-4578-b79a-784e4138664b")
multi_card_fn.delete()


class Migration(migrations.Migration):
dependencies = [
("arches_for_science", "0003_manifest_x_canvas_x_digitalresource"),
]

operations = [
migrations.RunPython(
retire_core_primary_descriptor,
restore_core_primary_descriptor,
)
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Generated by Django 4.2.4 on 2023-11-29 13:48

from django.db import migrations
import pgtrigger.compiler
import pgtrigger.migrations


class Migration(migrations.Migration):
dependencies = [
("models", "10260_add_iiifmanifest_globalid"),
("arches_for_science", "0004_improve_primary_descriptor"),
]

operations = [
migrations.CreateModel(
name="FunctionXGraphProxy",
fields=[],
options={
"proxy": True,
"indexes": [],
"constraints": [],
},
bases=("models.functionxgraph",),
),
migrations.CreateModel(
name="TileModelProxy",
fields=[],
options={
"proxy": True,
"indexes": [],
"constraints": [],
},
bases=("models.tilemodel",),
),
pgtrigger.migrations.AddTrigger(
model_name="functionxgraphproxy",
trigger=pgtrigger.compiler.Trigger(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect the trigger to be removed on the reverse migration, but it persists. When you reverse migrate, are the pgtriggers dropped?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me:

Reversing from 0008 to 0004:

root@d6b16aa0f3ba:/web_root/disco# python3 manage.py migrate arches_for_science 0004
System check identified some issues:

WARNINGS:
?: (urls.W005) URL namespace 'admin' isn't unique. You may not be able to reverse all URLs in this namespace
?: (urls.W005) URL namespace 'oauth2' isn't unique. You may not be able to reverse all URLs in this namespace
Operations to perform:
  Target specific migration: 0004_improve_primary_descriptor, from arches_for_science
Running migrations:
  Rendering model states... DONE
  Unapplying arches_for_science.0008_remove_tilemodelproxy_calculate_multicard_primary_descriptor_single_and_more... OK
  Unapplying arches_for_science.0007_update_primary_description_trigger_handle_static_value... OK
  Unapplying arches_for_science.0006_update_primary_description_trigger_null_handling... OK
  Unapplying arches_for_science.0005_deploy_triggers_for_primary_descriptor... OK
root@d6b16aa0f3ba:/web_root/disco# python3 manage.py dbshell
psql (14.9 (Ubuntu 14.9-1.pgdg22.04+1), server 14.5 (Debian 14.5-1.pgdg110+1))
Type "help" for help.

disco=# \dS tiles
...
Triggers:
    __arches_check_excess_tiles_trigger BEFORE INSERT ON tiles FOR EACH ROW EXECUTE FUNCTION __arches_check_excess_tiles_trigger_function()
    __arches_trg_update_spatial_attributes AFTER INSERT OR DELETE OR UPDATE ON tiles DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION __arches_trg_fnc_update_spatial_attributes()

I noticed that there was a missing migration on this branch from Friday, that might have been the issue.

name="calculate_multicard_primary_descriptor_all",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition='WHEN (NEW."config" IS NOT NULL AND NEW."functionid" = \'00b2d15a-fda0-4578-b79a-784e4138664b\'::uuid)',
func="\nDECLARE\n fn_config jsonb;\n name_template text;\n map_popup_template text;\n description_template text;\n all_templates text[];\n this_template text;\n loop_index int;\n descriptor_key text;\n\n graph UUID;\n resourceid UUID;\n node_alias text;\n alias_with_separators text;\n node_for_alias UUID;\n nodegroup_for_alias UUID;\n localized_string_node_value jsonb;\n lang text;\n inner18n_obj_for_lang jsonb;\n resolved_node_value_for_lang text;\n working_string text;\n localized_result_all jsonb;\n localized_result_name_only jsonb;\n\nBEGIN\n\nSELECT NEW.config, NEW.graphid\nINTO fn_config, graph;\n\nFOR resourceid IN (SELECT resourceinstanceid FROM resource_instances WHERE graphid = graph)\nLOOP\n SELECT JSONB_OBJECT('{}') INTO localized_result_all;\n SELECT JSONB_OBJECT('{}') INTO localized_result_name_only;\n SELECT fn_config -> 'descriptor_types' -> 'name' -> 'string_template' INTO name_template;\n SELECT fn_config -> 'descriptor_types' -> 'map_popup' -> 'string_template' INTO map_popup_template;\n SELECT fn_config -> 'descriptor_types' -> 'description' -> 'string_template' INTO description_template;\n\n SELECT ARRAY[name_template, map_popup_template, description_template] INTO all_templates;\n SELECT 0 INTO loop_index;\n FOR this_template IN SELECT UNNEST(all_templates)\n LOOP\n SELECT loop_index + 1 INTO loop_index;\n SELECT\n CASE\n WHEN loop_index = 1 THEN 'name'\n WHEN loop_index = 2 THEN 'map_popup'\n ELSE 'description'\n END\n INTO descriptor_key;\n\n -- Resolve node values to localized strings\n FOR alias_with_separators IN SELECT UNNEST(REGEXP_MATCHES(this_template, '<\\w+>', 'g'))\n LOOP\n SELECT TRIM(BOTH '<>' FROM alias_with_separators) INTO node_alias;\n\n SELECT nodeid, nodegroupid INTO node_for_alias, nodegroup_for_alias\n FROM nodes\n WHERE alias = node_alias\n AND graphid = graph;\n\n SELECT tiledata ->> node_for_alias::text INTO localized_string_node_value\n FROM tiles\n WHERE resourceinstanceid = resourceid\n AND nodegroupid = nodegroup_for_alias\n AND sortorder = 0\n LIMIT 1;\n\n -- Replace template with localized string\n FOR lang, inner18n_obj_for_lang IN SELECT * FROM jsonb_each(localized_string_node_value) \n LOOP\n -- Initialize language key if missing\n IF localized_result_all -> lang IS NULL THEN\n SELECT jsonb_set(\n localized_result_all,\n ARRAY[lang],\n JSONB_OBJECT('{}')\n ) INTO localized_result_all;\n END IF;\n\n -- Retrieve the current working value, or start from the template\n SELECT\n CASE\n WHEN localized_result_all -> lang -> descriptor_key IS NULL\n THEN this_template\n ELSE TRIM((localized_result_all -> lang -> descriptor_key)::text, '\\\"')\n END\n INTO working_string;\n\n SELECT REPLACE(\n working_string,\n alias_with_separators,\n TRIM((inner18n_obj_for_lang -> 'value')::text, '\"')\n )\n INTO resolved_node_value_for_lang;\n\n -- Update the working value\n SELECT jsonb_set(\n localized_result_all,\n ARRAY[lang, descriptor_key],\n TO_JSONB(resolved_node_value_for_lang)\n ) INTO localized_result_all;\n\n IF descriptor_key = 'name' THEN\n SELECT jsonb_set(\n localized_result_name_only,\n ARRAY[lang],\n TO_JSONB(resolved_node_value_for_lang)\n ) INTO localized_result_name_only;\n END IF;\n\n END LOOP;\n\n END LOOP;\n\n END LOOP;\n\n UPDATE resource_instances\n SET descriptors = localized_result_all, name = localized_result_name_only\n WHERE resourceinstanceid = resourceid;\n\nEND LOOP;\nEND;\nRETURN NULL;\n",
hash="4fa923b9bb279654c25cdf95a42b3620d974385e",
operation="INSERT OR UPDATE",
pgid="pgtrigger_calculate_multicard_primary_descriptor_all_3029e",
table="functions_x_graphs",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="tilemodelproxy",
trigger=pgtrigger.compiler.Trigger(
name="calculate_multicard_primary_descriptor_single",
sql=pgtrigger.compiler.UpsertTriggerSql(
constraint="CONSTRAINT",
func="\nDECLARE\n fn_config jsonb;\n name_template text;\n map_popup_template text;\n description_template text;\n all_templates text[];\n this_template text;\n loop_index int;\n descriptor_key text;\n\n graph UUID;\n resourceid UUID;\n node_alias text;\n alias_with_separators text;\n node_for_alias UUID;\n nodegroup_for_alias UUID;\n localized_string_node_value jsonb;\n lang text;\n inner18n_obj_for_lang jsonb;\n resolved_node_value_for_lang text;\n working_string text;\n localized_result_all jsonb;\n localized_result_name_only jsonb;\n\nBEGIN\n\nSELECT NEW.resourceinstanceid INTO resourceid;\nSELECT graphid INTO graph FROM resource_instances WHERE resourceinstanceid = resourceid;\n\nSELECT config\nINTO fn_config\nFROM functions_x_graphs\nWHERE\n functionid = '00b2d15a-fda0-4578-b79a-784e4138664b'\n AND functions_x_graphs.config IS NOT NULL\n AND graph = graphid;\n\nIF FOUND THEN\n SELECT JSONB_OBJECT('{}') INTO localized_result_all;\n SELECT JSONB_OBJECT('{}') INTO localized_result_name_only;\n SELECT fn_config -> 'descriptor_types' -> 'name' -> 'string_template' INTO name_template;\n SELECT fn_config -> 'descriptor_types' -> 'map_popup' -> 'string_template' INTO map_popup_template;\n SELECT fn_config -> 'descriptor_types' -> 'description' -> 'string_template' INTO description_template;\n\n SELECT ARRAY[name_template, map_popup_template, description_template] INTO all_templates;\n SELECT 0 INTO loop_index;\n FOR this_template IN SELECT UNNEST(all_templates)\n LOOP\n SELECT loop_index + 1 INTO loop_index;\n SELECT\n CASE\n WHEN loop_index = 1 THEN 'name'\n WHEN loop_index = 2 THEN 'map_popup'\n ELSE 'description'\n END\n INTO descriptor_key;\n\n -- Resolve node values to localized strings\n FOR alias_with_separators IN SELECT UNNEST(REGEXP_MATCHES(this_template, '<\\w+>', 'g'))\n LOOP\n SELECT TRIM(BOTH '<>' FROM alias_with_separators) INTO node_alias;\n\n SELECT nodeid, nodegroupid INTO node_for_alias, nodegroup_for_alias\n FROM nodes\n WHERE alias = node_alias\n AND graphid = graph;\n\n SELECT tiledata ->> node_for_alias::text INTO localized_string_node_value\n FROM tiles\n WHERE resourceinstanceid = resourceid\n AND nodegroupid = nodegroup_for_alias\n AND sortorder = 0\n LIMIT 1;\n\n -- Replace template with localized string\n FOR lang, inner18n_obj_for_lang IN SELECT * FROM jsonb_each(localized_string_node_value) \n LOOP\n -- Initialize language key if missing\n IF localized_result_all -> lang IS NULL THEN\n SELECT jsonb_set(\n localized_result_all,\n ARRAY[lang],\n JSONB_OBJECT('{}')\n ) INTO localized_result_all;\n END IF;\n\n -- Retrieve the current working value, or start from the template\n SELECT\n CASE\n WHEN localized_result_all -> lang -> descriptor_key IS NULL\n THEN this_template\n ELSE TRIM((localized_result_all -> lang -> descriptor_key)::text, '\\\"')\n END\n INTO working_string;\n\n SELECT REPLACE(\n working_string,\n alias_with_separators,\n TRIM((inner18n_obj_for_lang -> 'value')::text, '\"')\n )\n INTO resolved_node_value_for_lang;\n\n -- Update the working value\n SELECT jsonb_set(\n localized_result_all,\n ARRAY[lang, descriptor_key],\n TO_JSONB(resolved_node_value_for_lang)\n ) INTO localized_result_all;\n\n IF descriptor_key = 'name' THEN\n SELECT jsonb_set(\n localized_result_name_only,\n ARRAY[lang],\n TO_JSONB(resolved_node_value_for_lang)\n ) INTO localized_result_name_only;\n END IF;\n\n END LOOP;\n\n END LOOP;\n\n END LOOP;\n\n UPDATE resource_instances\n SET descriptors = localized_result_all, name = localized_result_name_only\n WHERE resourceinstanceid = resourceid;\n\nEND IF;\nEND;\nRETURN NULL;\n",
hash="b29436e21dec77b4e176cc0f31227029a15d9b5b",
operation="INSERT OR UPDATE OR DELETE",
pgid="pgtrigger_calculate_multicard_primary_descriptor_single_26494",
table="tiles",
timing="DEFERRABLE INITIALLY DEFERRED",
when="AFTER",
),
),
),
]
35 changes: 34 additions & 1 deletion arches_for_science/models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,42 @@
import uuid

from arches.app.models.models import IIIFManifest, TileModel, FunctionXGraph
from django.db import models
from django.db.models import JSONField
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from arches.app.models.models import IIIFManifest
import pgtrigger

from .trigger_functions import CALCULATE_MULTICARD_PRIMARY_DESCRIPTOR_SINGLE, CALCULATE_MULTICARD_PRIMARY_DESCRIPTOR_ALL

class TileModelProxy(TileModel):
class Meta:
proxy = True
triggers = [
pgtrigger.Trigger(
name='calculate_multicard_primary_descriptor_single',
when=pgtrigger.After,
operation=pgtrigger.Insert | pgtrigger.Update | pgtrigger.Delete,
func=CALCULATE_MULTICARD_PRIMARY_DESCRIPTOR_SINGLE,
timing=pgtrigger.Deferred,
),
]

class FunctionXGraphProxy(FunctionXGraph):
class Meta:
proxy = True
triggers = [
pgtrigger.Trigger(
name='calculate_multicard_primary_descriptor_all',
when=pgtrigger.After,
condition=pgtrigger.Q(
new__function_id="00b2d15a-fda0-4578-b79a-784e4138664b",
new__config__isnull=False,
),
operation=pgtrigger.Insert | pgtrigger.Update,
func=CALCULATE_MULTICARD_PRIMARY_DESCRIPTOR_ALL,
),
]


class RendererConfig(models.Model):
Expand Down
Loading