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 all 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
5 changes: 4 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 All @@ -45,6 +46,8 @@ before Arches 6.2.6 or 7.5.0 did not create ``RENDERERS``, so you may need to
[add it first](https://github.com/archesproject/arches/pull/10171/files)
before extending it as shown below):
```
FUNCTION_LOCATIONS.append("arches_for_science.pkg.extensions.functions")

TEMPLATES[0]["OPTIONS"]["context_processors"].append("arches_for_science.utils.context_processors.project_settings")

RENDERERS += [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
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())
),
};

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
const sortedCards = this.graph.cards.toSorted((a, b) => {
const nameA = a.name.toUpperCase();
const nameB = b.name.toUpperCase();
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}

return 0;
});

this.groupedNodesForSelect2 = [];
sortedCards.forEach(card => {
const stringNodes = this.graph.nodes.filter(
node => node.datatype === 'string' && node.nodegroup_id === card.nodegroup_id
);

if (stringNodes.length) {
this.groupedNodesForSelect2.push({
text: card.name,
children: stringNodes.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: 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
77 changes: 77 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,77 @@
# Generated by Django 4.2.4 on 2023-11-20 13:43

from django.db import migrations


STRING_TEMPLATE_REPLACEMENTS = {
"Statement_content": "statement_content",
"Name_content": "name_content",
"Name (top)_content": "name_top__content",
"<content >": "<statement_content>",
}
STRING_TEMPLATE_REPLACEMENTS_REVERSED = {v: k for k, v in STRING_TEMPLATE_REPLACEMENTS.items()}

ORIGINAL_DESCRIPTOR_FUNCTION_PK = "60000000-0000-0000-0000-000000000001"
MULTICARD_DESCRIPTOR_FUNCTION_PK = "00b2d15a-fda0-4578-b79a-784e4138664b"


def update_graphs(apps, from_function, to_function, string_replacement_map):
FunctionXGraph = apps.get_model("models", "FunctionXGraph")

for fn_x_graph in FunctionXGraph.objects.filter(function=from_function):
fn_x_graph.function = to_function
for descriptor_object in fn_x_graph.config["descriptor_types"].values():
for before, after in string_replacement_map.items():
descriptor_object["string_template"] = descriptor_object["string_template"].replace(before, after)
fn_x_graph.save()


def retire_core_primary_descriptor(apps, schema_editor):
Function = apps.get_model("models", "Function")
original_fn = Function.objects.get(pk=ORIGINAL_DESCRIPTOR_FUNCTION_PK)
original_fn.functiontype = "primarydescriptors_retired"
original_fn.name = "(retired) Define Resource Descriptors"
original_fn.save()

# create multi-card function
multi_card_fn, created = Function.objects.update_or_create(
pk=MULTICARD_DESCRIPTOR_FUNCTION_PK,
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=original_fn.defaultconfig,
component="views/components/functions/multicard-resource-descriptor",
)

update_graphs(apps, from_function=original_fn, to_function=multi_card_fn,
string_replacement_map=STRING_TEMPLATE_REPLACEMENTS)


def restore_core_primary_descriptor(apps, schema_editor):
Function = apps.get_model("models", "Function")
original_fn = Function.objects.get(pk=ORIGINAL_DESCRIPTOR_FUNCTION_PK)
original_fn.functiontype = "primarydescriptors"
original_fn.name = "Define Resource Descriptors"
original_fn.save()

# Update FunctionXGraph records before cascade deleted below
update_graphs(apps, from_function=multi_card_fn, to_function=original_fn,
string_replacement_map=STRING_TEMPLATE_REPLACEMENTS_REVERSED)

multi_card_fn = Function.objects.get(pk=MULTICARD_DESCRIPTOR_FUNCTION_PK)
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,
)
]

Large diffs are not rendered by default.

34 changes: 33 additions & 1 deletion arches_for_science/models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,41 @@
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,
),
]

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
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from arches.app.functions.primary_descriptors import AbstractPrimaryDescriptorsFunction
from arches.app.models.system_settings import settings

from django.utils.translation import get_language, gettext as _

# This duplicates the configuration declared in migration 0004,
# but on first package load, the function will be re-registered, because
# the .py file has not yet been placed in the destination folder.
# Re-registration will overwrite whatever the migration inserted.
details = {
"functionid": "00b2d15a-fda0-4578-b79a-784e4138664b",
"name": "Multi-card Resource Descriptor",
"type": "primarydescriptors",
"description": "Configure the name, description, and map popup of a resource",
"defaultconfig": {
"descriptor_types": {
"name": {
"nodegroup_id": "",
"string_template": "",
},
"map_popup": {
"nodegroup_id": "",
"string_template": "",
},
"description": {
"nodegroup_id": "",
"string_template": "",
},
}
},
"classname": "MulticardResourceDescriptor",
"component": "views/components/functions/multicard-resource-descriptor",
}


class MulticardResourceDescriptor(AbstractPrimaryDescriptorsFunction):
"""Implemented in the database via triggers on tiles table.

This implementation just fetches the calculated result from the db."""

def get_primary_descriptor_from_nodes(self, resource, config, context=None, descriptor=None):
result = ""
requested_language = context.get("language", None) if context else None

lookup_language = requested_language or get_language() or settings.LANGUAGE_CODE
try:
result = resource.descriptors[lookup_language][descriptor]
except KeyError:
pass

if result.strip() == "":
result = _("Undefined")

return result
Original file line number Diff line number Diff line change
Expand Up @@ -5629,20 +5629,20 @@
"descriptor_types": {
"description": {
"nodegroup_id": "56c7a5dc-c450-11e9-ad38-a4d18cec433a",
"string_template": "<Statement_content>"
"string_template": "<statement_content>"
},
"map_popup": {
"nodegroup_id": "",
"string_template": ""
},
"name": {
"nodegroup_id": "52aa1673-c450-11e9-8640-a4d18cec433a",
"string_template": "<Name_content>"
"string_template": "<name_content>"
}
},
"triggering_nodegroups": []
},
"function_id": "60000000-0000-0000-0000-000000000001",
"function_id": "00b2d15a-fda0-4578-b79a-784e4138664b",
"graph_id": "1b210ef3-b25c-11e9-a037-a4d18cec433a",
"id": "410bb31f-2c25-4801-a974-67fc1c9ad578"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6767,20 +6767,20 @@
"descriptor_types": {
"description": {
"nodegroup_id": "da1fac57-ca7a-11e9-86a3-a4d18cec433a",
"string_template": "<Statement_content>"
"string_template": "<statement_content>"
},
"map_popup": {
"nodegroup_id": "",
"string_template": ""
},
"name": {
"nodegroup_id": "d2fdae3d-ca7a-11e9-ad84-a4d18cec433a",
"string_template": "<Name_content>"
"string_template": "<name_content>"
}
},
"triggering_nodegroups": []
},
"function_id": "60000000-0000-0000-0000-000000000001",
"function_id": "00b2d15a-fda0-4578-b79a-784e4138664b",
"graph_id": "707cbd78-ca7a-11e9-990b-a4d18cec433a",
"id": "81e78264-86be-41f5-a22d-3e7c7e1e788d"
}
Expand Down
6 changes: 3 additions & 3 deletions arches_for_science/pkg/graphs/resource_models/Group.json
Original file line number Diff line number Diff line change
Expand Up @@ -7837,20 +7837,20 @@
"descriptor_types": {
"description": {
"nodegroup_id": "32db94f8-c05e-11e9-a2c1-a4d18cec433a",
"string_template": "<Statement_content>"
"string_template": "<statement_content>"
Copy link
Member Author

Choose a reason for hiding this comment

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

These now need to be node aliases rather than node names. I could make the lookup case-insensitive, I suppose?

Copy link
Member

Choose a reason for hiding this comment

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

That would work in many cases, but a node alias can differ from the node name in other ways, generally because an alias must be unique to a graph and a name only must be unique to its siblings. So, I think it's good to be explicit about changing from names to aliases as you've done here.

},
"map_popup": {
"nodegroup_id": "",
"string_template": ""
},
"name": {
"nodegroup_id": "12707705-c05e-11e9-8177-a4d18cec433a",
"string_template": "<Name_content>"
"string_template": "<name_content>"
}
},
"triggering_nodegroups": []
},
"function_id": "60000000-0000-0000-0000-000000000001",
"function_id": "00b2d15a-fda0-4578-b79a-784e4138664b",
"graph_id": "07883c9e-b25c-11e9-975a-a4d18cec433a",
"id": "cb049a30-7542-4efa-8b8d-9f465ab61fcb"
}
Expand Down
Loading
Loading