From cd86d6228c700a096c5d193c68dff445eb6b3ef5 Mon Sep 17 00:00:00 2001
From: Sorin Marti <32014438+sorinmarti@users.noreply.github.com>
Date: Wed, 13 Dec 2023 19:17:49 +0100
Subject: [PATCH] Implemented result tag formatting, worked on translation,
implemented tests
---
ndr_core/admin_forms/result_card_forms.py | 2 +-
ndr_core/admin_forms/result_field_forms.py | 8 -
ndr_core/admin_forms/search_config_forms.py | 10 +-
ndr_core/admin_forms/translation_forms.py | 29 ++-
ndr_core/admin_views/color_views.py | 3 +-
ndr_core/admin_views/result_views.py | 14 +-
ndr_core/admin_views/search_field_views.py | 4 +-
ndr_core/admin_views/search_views.py | 3 +-
ndr_core/admin_views/settings_views.py | 1 -
ndr_core/api/mongodb/mongodb_query.py | 1 -
ndr_core/api/mongodb/mongodb_result.py | 2 -
ndr_core/form_preview.py | 11 +-
ndr_core/forms/forms_contact.py | 1 -
ndr_core/forms/forms_search.py | 6 +-
.../commands/ndr_import_manifests.py | 2 +-
...searchconfiguration_citation_expression.py | 24 +++
...configuration_compact_result_is_default.py | 20 ++
ndr_core/models.py | 25 ++-
ndr_core/ndr_template_tags.py | 2 -
ndr_core/ndr_templatetags/abstract_filter.py | 77 ++++++++
ndr_core/ndr_templatetags/filter_mixins.py | 66 -------
ndr_core/ndr_templatetags/filters.py | 181 +++++++++---------
ndr_core/ndr_templatetags/html_element.py | 126 ++++++++++++
ndr_core/ndr_templatetags/template_string.py | 18 +-
ndr_core/prog_stats.py | 26 ++-
ndr_core/static/ndr_core/js/admin/preview.js | 29 ++-
.../admin_views/edit/result_card_edit.html | 9 +-
.../overview/configure_search.html | 2 +-
.../configured_fields_template.html | 3 +-
ndr_core/templatetags/ndr_utils.py | 20 +-
ndr_core/tests/test_html_element.py | 30 +++
ndr_core/tests/test_strings.py | 76 +++++---
ndr_core/views.py | 9 +-
33 files changed, 566 insertions(+), 274 deletions(-)
create mode 100644 ndr_core/migrations/0009_ndrcoresearchconfiguration_citation_expression.py
create mode 100644 ndr_core/migrations/0010_ndrcoresearchconfiguration_compact_result_is_default.py
create mode 100644 ndr_core/ndr_templatetags/abstract_filter.py
delete mode 100644 ndr_core/ndr_templatetags/filter_mixins.py
create mode 100644 ndr_core/ndr_templatetags/html_element.py
create mode 100644 ndr_core/tests/test_html_element.py
diff --git a/ndr_core/admin_forms/result_card_forms.py b/ndr_core/admin_forms/result_card_forms.py
index d07092e..a39e081 100644
--- a/ndr_core/admin_forms/result_card_forms.py
+++ b/ndr_core/admin_forms/result_card_forms.py
@@ -19,7 +19,7 @@ def __init__(self, *args, **kwargs):
if result_field_conf_row == 0:
required = True
- result_field = forms.ModelChoiceField(queryset=NdrCoreResultField.objects.all(),
+ result_field = forms.ModelChoiceField(queryset=NdrCoreResultField.objects.all().order_by('label'),
required=required, help_text="")
row_field = forms.IntegerField(required=required,
help_text="")
diff --git a/ndr_core/admin_forms/result_field_forms.py b/ndr_core/admin_forms/result_field_forms.py
index 2643deb..3f1a27d 100644
--- a/ndr_core/admin_forms/result_field_forms.py
+++ b/ndr_core/admin_forms/result_field_forms.py
@@ -40,14 +40,6 @@ def helper(self):
)
layout.append(form_row)
- form_row = Row(
- Column('display_border', css_class='form-group col-4'),
- Column('html_display', css_class='form-group col-4'),
- Column('md_display', css_class='form-group col-4'),
- css_class='form-row'
- )
- layout.append(form_row)
-
form_row = Row(
Column(
get_info_box('Access your variables in the following form', 'xxx_info'),
diff --git a/ndr_core/admin_forms/search_config_forms.py b/ndr_core/admin_forms/search_config_forms.py
index 3372d04..99f15b2 100644
--- a/ndr_core/admin_forms/search_config_forms.py
+++ b/ndr_core/admin_forms/search_config_forms.py
@@ -18,7 +18,8 @@ class Meta:
'api_type', 'api_connection_url',
'api_user_name', 'api_password', 'api_auth_key',
'search_id_field', 'sort_field', 'sort_order',
- 'search_has_compact_result', 'page_size', 'compact_page_size', 'repository_url',
+ 'search_has_compact_result', 'compact_result_is_default', 'page_size',
+ 'compact_page_size', 'citation_expression', 'repository_url',
'has_simple_search', 'simple_search_first', 'simple_query_main_field',
'simple_query_label', 'simple_query_help_text',]
@@ -107,6 +108,13 @@ def helper(self):
)
layout.append(form_row)
+ form_row = Row(
+ Column('compact_result_is_default', css_class='col-3'),
+ Column('citation_expression', css_class='col-9'),
+ css_class='form-row'
+ )
+ layout.append(form_row)
+
form_row = Row(
Column(Div(HTML('''
diff --git a/ndr_core/admin_forms/translation_forms.py b/ndr_core/admin_forms/translation_forms.py
index 80ff7d1..6291231 100644
--- a/ndr_core/admin_forms/translation_forms.py
+++ b/ndr_core/admin_forms/translation_forms.py
@@ -32,11 +32,20 @@ def init_form(self):
initial_values = {}
for item in self.items:
for field in self.translatable_fields:
+ values = {}
+ if isinstance(field, dict):
+ values = field
+ field = values['field']
+
+ if 'condition' in values:
+ if item.__getattribute__(values['condition']['field']) not in values['condition']['values']:
+ continue
+
self.fields[f"{field}_{item.pk}"] = forms.CharField(label=f"Translate: '{field}' for '{item.__getattribute__(field)}'",
required=False,
max_length=1000,
help_text='')
- if field.startswith('rich_'):
+ if 'widget' in values and values['widget'] == 'textarea':
self.fields[f"{field}_{item.pk}"].widget = forms.Textarea(attrs={'rows': 3})
initial_values[f"{field}_{item.pk}"] = self.get_initial_value(field, str(item.pk))
@@ -54,6 +63,15 @@ def do_helper(self):
cols = []
for field in self.translatable_fields:
+ values = {}
+ if isinstance(field, dict):
+ values = field
+ field = values['field']
+
+ if 'condition' in values:
+ if item.__getattribute__(values['condition']['field']) not in values['condition']['values']:
+ continue
+
cols.append(Column(f"{field}_{item.pk}", css_class=f'form-group col-{int(12/len(self.translatable_fields))}'),)
form_row = Row(
@@ -81,7 +99,6 @@ def save_translations(self):
"""Saves the translations to the database. """
self.is_valid()
- print(self.cleaned_data)
for item in self.items:
for field in self.translatable_fields:
self.save_translation(str(item.pk), field, self.cleaned_data[f"{field}_{item.pk}"])
@@ -117,7 +134,11 @@ class TranslateFieldForm(TranslateForm):
"""Form to translate form field values """
items = NdrCoreSearchField.objects.all()
- translatable_fields = ['field_label', 'help_text']
+ translatable_fields = ['field_label', 'help_text',
+ {'field': 'list_choices',
+ 'widget': 'textarea',
+ 'condition': {'field': 'field_type',
+ 'values': [NdrCoreSearchField.FieldType.INFO_TEXT]}}]
table_name = 'NdrCoreSearchField'
def __init__(self, *args, **kwargs):
@@ -170,7 +191,7 @@ class TranslateResultForm(TranslateForm):
"""Form to translate settings values. """
items = NdrCoreResultField.objects.all()
- translatable_fields = ['rich_expression']
+ translatable_fields = [{'field': 'rich_expression', 'widget': 'textarea'}]
table_name = 'NdrCoreResultField'
def __init__(self, *args, **kwargs):
diff --git a/ndr_core/admin_views/color_views.py b/ndr_core/admin_views/color_views.py
index 1e7f3de..4b2223b 100644
--- a/ndr_core/admin_views/color_views.py
+++ b/ndr_core/admin_views/color_views.py
@@ -97,9 +97,8 @@ def form_valid(self, form):
my_string = f.read().decode('utf-8')
deserialized_object = serializers.deserialize("json", "["+my_string+"]")
for obj in deserialized_object:
- if NdrCoreColorScheme.objects.filter(scheme_name=obj.object.scheme_name).count()>0:
+ if NdrCoreColorScheme.objects.filter(scheme_name=obj.object.scheme_name).count() > 0:
messages.info(self.request, f'The scheme "{obj.object.scheme_name}" was updated')
- print(obj.save())
except DeserializationError:
messages.error(self.request, 'Could not deserialize object.')
diff --git a/ndr_core/admin_views/result_views.py b/ndr_core/admin_views/result_views.py
index d09f004..b5ff26c 100644
--- a/ndr_core/admin_views/result_views.py
+++ b/ndr_core/admin_views/result_views.py
@@ -6,7 +6,7 @@
from ndr_core.admin_forms.result_card_forms import SearchConfigurationResultEditForm
from ndr_core.admin_views.admin_views import AdminViewMixin
-from ndr_core.form_preview import get_search_form_image_from_raw_data
+from ndr_core.form_preview import PreviewImage
from ndr_core.admin_forms.result_field_forms import ResultFieldCreateForm, ResultFieldEditForm
from ndr_core.models import (
NdrCoreResultField,
@@ -73,8 +73,8 @@ def get_form(self, form_class=None):
"""Returns the form for this view. """
form = super().get_form(form_class=form_class)
all_fields = NdrCoreSearchConfiguration.objects.get(pk=self.kwargs['pk']).result_card_fields.all()
- normal_fields = all_fields.filter(result_card_group='normal')
- compact_fields = all_fields.filter(result_card_group='compact')
+ normal_fields = all_fields.filter(result_card_group='normal').order_by('field_column').order_by('field_row')
+ compact_fields = all_fields.filter(result_card_group='compact').order_by('field_column').order_by('field_row')
form_row = 0
for field in normal_fields:
@@ -96,6 +96,8 @@ def form_valid(self, form):
"""Creates or updates the result card configuration for a search configuration. """
response = super().form_valid(form)
conf_object = NdrCoreSearchConfiguration.objects.get(pk=self.kwargs['pk'])
+ all_fields = conf_object.result_card_fields.all()
+ all_fields.delete()
for row in range(20):
fields = self.get_row_fields(row)
@@ -142,6 +144,7 @@ def form_valid(self, form):
def preview_result_card_image(request, img_config):
"""Creates a result card preview image of a result form configuration. """
+
data = []
config_rows = img_config.split(",")
for row in config_rows:
@@ -152,7 +155,6 @@ def preview_result_card_image(request, img_config):
'row': int(config_row[0]),
'col': int(config_row[1]),
'size': int(config_row[2]),
- 'text': '',
- 'type': field.field_type})
- image_data = get_search_form_image_from_raw_data(data)
+ 'text': field.label})
+ image_data = PreviewImage().create_result_card_image_from_raw_data(data)
return HttpResponse(image_data, content_type="image/png")
diff --git a/ndr_core/admin_views/search_field_views.py b/ndr_core/admin_views/search_field_views.py
index 7ee21d5..17bb633 100644
--- a/ndr_core/admin_views/search_field_views.py
+++ b/ndr_core/admin_views/search_field_views.py
@@ -5,7 +5,7 @@
from django.views.generic import CreateView, UpdateView, DeleteView
from ndr_core.admin_views.admin_views import AdminViewMixin
-from ndr_core.form_preview import get_search_form_image_from_raw_data
+from ndr_core.form_preview import PreviewImage
from ndr_core.admin_forms.search_field_forms import SearchFieldCreateForm, SearchFieldEditForm
from ndr_core.models import NdrCoreSearchField
@@ -52,5 +52,5 @@ def preview_search_form_image(request, img_config):
'size': int(config_row[2]),
'text': field.field_label,
'type': field.field_type})
- image_data = get_search_form_image_from_raw_data(data)
+ image_data = PreviewImage().create_search_form_image_from_raw_data(data)
return HttpResponse(image_data, content_type="image/png")
diff --git a/ndr_core/admin_views/search_views.py b/ndr_core/admin_views/search_views.py
index a771f3c..b27a03d 100644
--- a/ndr_core/admin_views/search_views.py
+++ b/ndr_core/admin_views/search_views.py
@@ -86,7 +86,8 @@ class SearchConfigurationFormEditView(AdminViewMixin, LoginRequiredMixin, FormVi
def get_form(self, form_class=None):
"""Returns the form for this view. """
form = super().get_form(form_class=form_class)
- fields = NdrCoreSearchConfiguration.objects.get(pk=self.kwargs['pk']).search_form_fields.all()
+ fields = (NdrCoreSearchConfiguration.objects.get(pk=self.kwargs['pk']).search_form_fields.all().
+ order_by('field_column').order_by('field_row'))
form_row = 0
for field in fields:
diff --git a/ndr_core/admin_views/settings_views.py b/ndr_core/admin_views/settings_views.py
index 1888f9c..5af7c77 100644
--- a/ndr_core/admin_views/settings_views.py
+++ b/ndr_core/admin_views/settings_views.py
@@ -194,7 +194,6 @@ def form_valid(self, form):
for obj in deserialized_object:
if NdrCoreValue.objects.filter(value_name=obj.object.value_name).count() > 0:
messages.info(self.request, f'The setting "{obj.object.value_name}" was updated')
- print(obj.save())
except DeserializationError:
messages.error(self.request, 'Could not deserialize object.')
diff --git a/ndr_core/api/mongodb/mongodb_query.py b/ndr_core/api/mongodb/mongodb_query.py
index f9b0a5e..caee40a 100644
--- a/ndr_core/api/mongodb/mongodb_query.py
+++ b/ndr_core/api/mongodb/mongodb_query.py
@@ -79,7 +79,6 @@ def get_advanced_query(self, *kwargs):
except NdrCoreSearchField.DoesNotExist:
pass
- print(query)
return query
def get_list_query(self, list_name, add_page_and_size=True, search_term=None, tags=None):
diff --git a/ndr_core/api/mongodb/mongodb_result.py b/ndr_core/api/mongodb/mongodb_result.py
index e96b5e1..630d27d 100644
--- a/ndr_core/api/mongodb/mongodb_result.py
+++ b/ndr_core/api/mongodb/mongodb_result.py
@@ -19,9 +19,7 @@ def download_result(self):
try:
# Get the connection string and collection from the configuration
connection_string_arr = self.search_configuration.api_connection_url.split('/')
- print(connection_string_arr)
connection_string = '/'.join(connection_string_arr[:-1])
- print(connection_string)
db_client = pymongo.MongoClient(connection_string, serverSelectionTimeoutMS=2000)
collection = db_client[connection_string_arr[-2]][connection_string_arr[-1]]
diff --git a/ndr_core/form_preview.py b/ndr_core/form_preview.py
index 16e8e3b..ccf1107 100644
--- a/ndr_core/form_preview.py
+++ b/ndr_core/form_preview.py
@@ -182,17 +182,8 @@ def create_result_card_image_from_raw_data(self, data):
coords_offset = self.get_coordinates(data_point['row'], data_point['col'], data_point['size'], offset=3)
draw.rounded_rectangle(coords_offset, 5, fill=self.shadow_color, outline="#333333")
draw.rounded_rectangle(coords, 5, fill=self.field_color, outline="#36454F")
+ draw.text((coords[0][0] + 10, coords[0][1] + 5), data_point['text'], (0, 0, 0))
output = io.BytesIO()
img.save(output, "PNG")
return output.getvalue()
-
-
-def get_search_form_image_from_raw_data(data):
- """This function gets called as view. It returns a preview image of a form."""
- return PreviewImage().create_search_form_image_from_raw_data(data)
-
-
-def get_result_card_image_from_raw_data(data):
- """This function gets called as view. It returns a preview image of a result card."""
- return PreviewImage().create_result_card_image_from_raw_data(data)
diff --git a/ndr_core/forms/forms_contact.py b/ndr_core/forms/forms_contact.py
index 7194625..7a11307 100644
--- a/ndr_core/forms/forms_contact.py
+++ b/ndr_core/forms/forms_contact.py
@@ -19,7 +19,6 @@ class Meta:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- # print(NdrCoreValue.get_or_initialize(value_name='contact_form_default_subject').translated_value())
self.fields['message_subject'].initial = NdrCoreValue.get_or_initialize(
value_name='contact_form_default_subject').translated_value()
self.fields['message_subject'].label = _('Message Subject')
diff --git a/ndr_core/forms/forms_search.py b/ndr_core/forms/forms_search.py
index e24ba80..d641da1 100644
--- a/ndr_core/forms/forms_search.py
+++ b/ndr_core/forms/forms_search.py
@@ -24,10 +24,10 @@ def __init__(self, *args, **kwargs):
"""Initializes all needed form fields for the configured search based on
the page's search configuration. """
- if self.ndr_page is not None:
- self.search_configs = self.ndr_page.search_configs.all()
- elif 'ndr_page' in kwargs:
+ if 'ndr_page' in kwargs:
self.ndr_page = kwargs.pop('ndr_page')
+
+ if self.ndr_page is not None:
self.search_configs = self.ndr_page.search_configs.all()
elif 'search_config' in kwargs:
self.search_configs = [kwargs.pop('search_config')]
diff --git a/ndr_core/management/commands/ndr_import_manifests.py b/ndr_core/management/commands/ndr_import_manifests.py
index aed1b10..e715373 100644
--- a/ndr_core/management/commands/ndr_import_manifests.py
+++ b/ndr_core/management/commands/ndr_import_manifests.py
@@ -35,4 +35,4 @@ def handle(self, *args, **options):
order_value_1=year,
order_value_2=issue,
order_value_3=issue_id)
- print(f"Created: {year}/{issue}: {title}")
+ self.stdout.write(self.style.SUCCESS(f"Created: {year}/{issue}: {title}"))
diff --git a/ndr_core/migrations/0009_ndrcoresearchconfiguration_citation_expression.py b/ndr_core/migrations/0009_ndrcoresearchconfiguration_citation_expression.py
new file mode 100644
index 0000000..3ae819a
--- /dev/null
+++ b/ndr_core/migrations/0009_ndrcoresearchconfiguration_citation_expression.py
@@ -0,0 +1,24 @@
+# Generated by Django 4.2.7 on 2023-12-07 21:53
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("ndr_core", "0008_ndrcoreresultfield_label"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="ndrcoresearchconfiguration",
+ name="citation_expression",
+ field=models.CharField(
+ blank=True,
+ default=None,
+ help_text="Expression to generate a citation for a result.",
+ max_length=512,
+ null=True,
+ verbose_name="Citation Expression",
+ ),
+ ),
+ ]
diff --git a/ndr_core/migrations/0010_ndrcoresearchconfiguration_compact_result_is_default.py b/ndr_core/migrations/0010_ndrcoresearchconfiguration_compact_result_is_default.py
new file mode 100644
index 0000000..474a0eb
--- /dev/null
+++ b/ndr_core/migrations/0010_ndrcoresearchconfiguration_compact_result_is_default.py
@@ -0,0 +1,20 @@
+# Generated by Django 4.2.7 on 2023-12-07 22:03
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("ndr_core", "0009_ndrcoresearchconfiguration_citation_expression"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="ndrcoresearchconfiguration",
+ name="compact_result_is_default",
+ field=models.BooleanField(
+ default=False,
+ help_text="If the compact result view is the default, check this box.",
+ ),
+ ),
+ ]
diff --git a/ndr_core/models.py b/ndr_core/models.py
index a9c50f9..94fe210 100644
--- a/ndr_core/models.py
+++ b/ndr_core/models.py
@@ -245,7 +245,7 @@ def get_list_choices(self):
"""Returns the list choices as a list of tuples. This is used to render the dropdowns
in the search form and result template lists."""
if not self.field_type == self.FieldType.LIST and not self.field_type == self.FieldType.MULTI_LIST:
- return {}
+ return []
file_handle = StringIO(self.list_choices)
reader = csv.reader(file_handle, delimiter=',')
@@ -256,12 +256,25 @@ def get_list_choices(self):
for row in reader:
if row_number == 0:
header = row
+ print(row)
else:
+
try:
val = row[header.index(f'value_{get_language()}')]
except ValueError:
val = row[header.index('value')]
- result_list.append((row[header.index('key')], val))
+
+ try:
+ searchable = header.index('is_searchable')
+ searchable = row[searchable]
+ except ValueError:
+ searchable = 'true'
+ except IndexError:
+ searchable = 'true'
+
+ if searchable.lower() == 'true':
+ result_list.append((row[0], val))
+
row_number += 1
@@ -468,6 +481,10 @@ class NdrCoreSearchConfiguration(TranslatableMixin, models.Model):
"check this box.")
"""If the result has a normal and a compact view, check this box."""
+ compact_result_is_default = models.BooleanField(default=False,
+ help_text="If the compact result view is the default, "
+ "check this box.")
+
page_size = models.IntegerField(default=10,
verbose_name="Page Size",
help_text="Size of the result page (e.g. 'How many results at once')")
@@ -483,6 +500,10 @@ class NdrCoreSearchConfiguration(TranslatableMixin, models.Model):
help_text="URL to the data repository where this data is stored.")
"""URL to the repository's website."""
+ citation_expression = models.CharField(max_length=512, default=None, null=True, blank=True,
+ verbose_name="Citation Expression",
+ help_text="Expression to generate a citation for a result.")
+
def __str__(self):
return self.conf_name
diff --git a/ndr_core/ndr_template_tags.py b/ndr_core/ndr_template_tags.py
index 80422bb..685a48e 100644
--- a/ndr_core/ndr_template_tags.py
+++ b/ndr_core/ndr_template_tags.py
@@ -126,10 +126,8 @@ def get_element(self, template, element_id):
else:
kw = {'pk': element_id}
element = element_class.objects.get(**kw)
- # print(f"Element found {element_class} / {element_id}: {element}")
return element
except element_class.DoesNotExist:
- # print(f"Element not found {element_class} / {element_id}")
return None
def get_pre_rendered_text(self):
diff --git a/ndr_core/ndr_templatetags/abstract_filter.py b/ndr_core/ndr_templatetags/abstract_filter.py
new file mode 100644
index 0000000..dfe7f09
--- /dev/null
+++ b/ndr_core/ndr_templatetags/abstract_filter.py
@@ -0,0 +1,77 @@
+from abc import ABC, abstractmethod
+
+from django.utils.translation import get_language
+
+
+class AbstractFilter(ABC):
+ """ A class to represent a filter. """
+
+ filter_name = ""
+ value = ""
+ filter_configurations = {}
+
+ def __init__(self, filter_name, value, filter_configurations):
+ self.filter_name = filter_name
+ self.value = value
+ self.filter_configurations = filter_configurations
+
+ self.check_configuration()
+
+ @abstractmethod
+ def get_rendered_value(self):
+ pass
+
+ @abstractmethod
+ def needed_attributes(self):
+ return []
+
+ @abstractmethod
+ def allowed_attributes(self):
+ return []
+
+ @abstractmethod
+ def needed_options(self):
+ return []
+
+ def check_configuration(self):
+ for needed_option in self.needed_options():
+ if not self.get_configuration(needed_option):
+ raise ValueError(f"Filter {self.filter_name} requires option {needed_option}.")
+ for needed_attribute in self.needed_attributes():
+ if not self.get_configuration(needed_attribute):
+ raise ValueError(f"Filter {self.filter_name} requires attribute {needed_attribute}.")
+ for attribute in self.filter_configurations:
+ if (attribute not in self.allowed_attributes()
+ and attribute not in self.needed_attributes()
+ and attribute not in self.needed_options()):
+ raise ValueError(f"Filter {self.filter_name} does not allow attribute {attribute}.")
+
+ def get_value(self):
+ """Returns the formatted string."""
+ return self.value
+
+ def get_configuration(self, name):
+ """Returns the configuration value."""
+ try:
+ value = self.filter_configurations[name]
+ except KeyError:
+ return None
+ except AttributeError:
+ return None
+ return value
+
+ @staticmethod
+ def replace_key_values(value):
+ """ Replaces a value if it is a key value"""
+ if value == '__none__':
+ return ''
+ return value
+
+ @staticmethod
+ def get_language_value_field_name():
+ """Returns the language value field name."""
+ value_field_name = 'value'
+ language = get_language()
+ if language != 'en':
+ value_field_name = f'value_{language}'
+ return value_field_name
diff --git a/ndr_core/ndr_templatetags/filter_mixins.py b/ndr_core/ndr_templatetags/filter_mixins.py
deleted file mode 100644
index bce167e..0000000
--- a/ndr_core/ndr_templatetags/filter_mixins.py
+++ /dev/null
@@ -1,66 +0,0 @@
-class ColorOptionMixin:
- """ A mixin for color options.
- Example: {test_value|pill:color=primary}"""
-
- color_options = ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark']
- color = None
-
- @staticmethod
- def get_color_from_value(value, lightness=80):
- """Translates a value to a color."""
- if value is None:
- return ''
-
- hash_value = 0
- for char in value:
- hash_value = ord(char) + ((hash_value << 5) - hash_value)
- hash_value = hash_value & hash_value
-
- return f'hsl({hash_value % 360}, {100}%, {lightness}%)'
-
- def get_color_string(self, color_option, data=None, color_string='color'):
- """Returns the color."""
- if color_option:
-
- if color_option in self.color_options:
- return f'{color_string}: {color_option};'
- if color_option.startswith('#') or color_option.startswith('rgb') or color_option.startswith('hsl'):
- return f'{color_string}: {color_option};'
- if color_option.startswith('val__'):
- if data:
- try:
- return f'{color_string}: {data[color_option[5:]]};'
- except KeyError:
- return ''
- except TypeError:
- return ''
- else:
- return ''
- if color_option.startswith('byval__'):
- if data:
- try:
- value = data[color_option[7:]]
- return f'{color_string}: {self.get_color_from_value(value)};'
- except KeyError:
- return ''
- except TypeError:
- return ''
- else:
- return ''
- if color_option == 'byval':
- if data:
- return f'{color_string}: {self.get_color_from_value(data)};'
- else:
- return ''
- color = f'color: {color_option};'
- else:
- color = ''
-
- return color
-
- def set_color(self, color):
- """Sets the color."""
- if color in self.color_options:
- self.color = color
- else:
- raise ValueError(f"Color {color} not found.")
diff --git a/ndr_core/ndr_templatetags/filters.py b/ndr_core/ndr_templatetags/filters.py
index 062d093..a393efc 100644
--- a/ndr_core/ndr_templatetags/filters.py
+++ b/ndr_core/ndr_templatetags/filters.py
@@ -4,7 +4,8 @@
from django.utils.translation import get_language
from ndr_core.models import NdrCoreSearchField
-from ndr_core.ndr_templatetags.filter_mixins import ColorOptionMixin
+from ndr_core.ndr_templatetags.abstract_filter import AbstractFilter
+from ndr_core.ndr_templatetags.html_element import HTMLElement
def get_get_filter_class(filter_name):
@@ -15,60 +16,35 @@ def get_get_filter_class(filter_name):
return BoolFilter
if filter_name == 'fieldify':
return FieldTemplateFilter
- elif filter_name in ['badge', 'pill']:
- return PillTemplateFilter
- elif filter_name == 'img':
+ if filter_name in ['badge', 'pill']:
+ return BadgeTemplateFilter
+ if filter_name == 'img':
return ImageTemplateFilter
- else:
- return None
-
-class AbstractFilter(ABC):
- """ A class to represent a filter. """
-
- filter_name = ""
- value = ""
- filter_configurations = {}
-
- def __init__(self, filter_name, value, filter_configurations):
- self.filter_name = filter_name
- self.value = value
- self.filter_configurations = filter_configurations
-
- @abstractmethod
- def get_rendered_value(self):
- pass
-
- def get_value(self):
- """Returns the formatted string."""
- print(self.filter_configurations)
- return self.value
-
- def get_configuration(self, name):
- try:
- value = self.filter_configurations[name]
- except KeyError:
- return None
- except AttributeError:
- return None
- return value
+ raise ValueError(f"Filter {filter_name} not found.")
class StringFilter(AbstractFilter):
""" A class to represent a template filter. """
- def __init__(self, filter_name, value, filter_configurations):
- super().__init__(filter_name, value, filter_configurations)
+ def needed_attributes(self):
+ return []
+
+ def allowed_attributes(self):
+ return []
+
+ def needed_options(self):
+ return []
def get_rendered_value(self):
"""Returns the formatted string."""
if self.filter_name == 'upper':
return self.get_value().upper()
- elif self.filter_name == 'lower':
+ if self.filter_name == 'lower':
return self.get_value().lower()
- elif self.filter_name == 'title':
+ if self.filter_name == 'title':
return self.get_value().title()
- elif self.filter_name == 'capitalize':
+ if self.filter_name == 'capitalize':
return self.get_value().capitalize()
return self.get_value()
@@ -76,8 +52,16 @@ def get_rendered_value(self):
class BoolFilter(AbstractFilter):
+ def needed_attributes(self):
+ return []
+
+ def allowed_attributes(self):
+ return []
+
+ def needed_options(self):
+ return ['o0', 'o1']
+
def get_rendered_value(self):
- print(self.filter_configurations, self.value, type(self.value))
true_value = "True"
if self.get_configuration('o0'):
true_value = self.get_configuration('o0')
@@ -88,24 +72,15 @@ def get_rendered_value(self):
if isinstance(self.value, bool):
if self.value:
return self.replace_key_values(true_value)
- else:
- return self.replace_key_values(false_value)
+ return self.replace_key_values(false_value)
if isinstance(self.value, str):
if self.value.lower() == 'true':
return self.replace_key_values(true_value)
- else:
- return self.replace_key_values(false_value)
+ return self.replace_key_values(false_value)
return self.get_value()
- @staticmethod
- def replace_key_values(value):
- """ Replaces a value if it is a key value"""
- if value == '__none__':
- return ''
- return value
-
class FieldTemplateFilter(AbstractFilter):
""" A class to represent a template filter. """
@@ -116,27 +91,24 @@ class FieldTemplateFilter(AbstractFilter):
def __init__(self, filter_name, value, filter_configurations):
super().__init__(filter_name, value, filter_configurations)
try:
- self.search_field = NdrCoreSearchField.objects.get(field_name=self.get_configuration('field'))
+ self.search_field = NdrCoreSearchField.objects.get(field_name=self.get_configuration('o0'))
try:
- self.field_value = self.search_field.get_list_choices_as_dict()[self.value][self.get_language_value_field_name()]
+ self.field_value =\
+ self.search_field.get_list_choices_as_dict()[self.value][self.get_language_value_field_name()]
except KeyError:
self.field_value = self.value
except NdrCoreSearchField.DoesNotExist:
self.search_field = None
- @staticmethod
- def get_language_value_field_name():
- """Returns the language value field name."""
- value_field_name = 'value'
- language = get_language()
- if language != 'en':
- value_field_name = f'value_{language}'
- return value_field_name
+ def needed_attributes(self):
+ return []
- def get_value(self):
- """Returns the formatted string."""
- return self.value
+ def allowed_attributes(self):
+ return []
+
+ def needed_options(self):
+ return ['o0']
def get_rendered_value(self):
"""Returns the formatted string."""
@@ -146,45 +118,68 @@ def get_rendered_value(self):
return self.field_value
-class PillTemplateFilter(AbstractFilter, ColorOptionMixin):
+class BadgeTemplateFilter(AbstractFilter):
""" A class to represent a template filter. """
- template = ""
-
- def __init__(self, filter_name, value, filter_configurations):
- super().__init__(filter_name, value, filter_configurations)
-
- color = self.get_color_string(self.get_configuration('color'), self.value, color_string='background-color')
+ def needed_attributes(self):
+ return []
- display_string = self.value
- if isinstance(self.value, dict):
- value_option = self.get_configuration('value')
- if value_option:
- try:
- display_string = self.value[value_option]
- except KeyError:
- display_string = "KEY_ERROR"
- else:
- display_string = json.dumps(self.value)
+ def allowed_attributes(self):
+ return ['field', 'color', 'bg']
- self.template = f'''{display_string}'''
- if ' style=""' in self.template:
- self.template = self.template.replace(' style=""', '')
-
- def get_value(self):
- """Returns the formatted string."""
- return self.value
+ def needed_options(self):
+ return []
def get_rendered_value(self):
"""Returns the formatted string."""
- return self.template
+
+ badge_element = HTMLElement('span')
+ badge_element.add_attribute('class', 'badge')
+ badge_element.add_attribute('class', 'text-dark')
+ badge_element.add_attribute('class', 'font-weight-normal')
+
+ field_options = None
+ if self.get_configuration('field'):
+ field = NdrCoreSearchField.objects.get(field_name=self.get_configuration('field'))
+ all_field_options = field.get_list_choices_as_dict()
+ field_options = all_field_options[self.value]
+ if "is_printable" in field_options:
+ if not field_options["is_printable"].lower() == "false":
+ return None
+ badge_element.add_content(field_options[self.get_language_value_field_name()])
+ else:
+ badge_element.add_content(self.value)
+
+ if self.get_configuration('color'):
+ badge_element.manage_color_attribute('color', self.get_configuration('color'),
+ self.value, field_options)
+ if self.get_configuration('bg'):
+ badge_element.manage_color_attribute('bg', self.get_configuration('bg'),
+ self.value, field_options)
+
+ return str(badge_element)
class ImageTemplateFilter(AbstractFilter):
+ def needed_attributes(self):
+ return []
+
+ def allowed_attributes(self):
+ return ['iiif_resize']
+
+ def needed_options(self):
+ return []
+
def get_rendered_value(self):
url = self.value
- if 'iiif_resize' in self.filter_configurations:
+
+ if self.get_configuration('iiif_resize'):
url = url.replace('/full/0/default.',
f'/pct:{self.filter_configurations["iiif_resize"]}/0/default.')
- return f''
+ element = HTMLElement('img')
+ element.add_attribute('src', url)
+ element.add_attribute('class', 'img-fluid')
+ element.add_attribute('alt', 'Responsive image')
+
+ return str(element)
\ No newline at end of file
diff --git a/ndr_core/ndr_templatetags/html_element.py b/ndr_core/ndr_templatetags/html_element.py
new file mode 100644
index 0000000..5aba9f3
--- /dev/null
+++ b/ndr_core/ndr_templatetags/html_element.py
@@ -0,0 +1,126 @@
+
+class HTMLElement:
+ """A class to represent an HTML element."""
+
+ COLOR_CLASSES = ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark']
+ HTML_COLOR_NAMES = [
+ "AliceBlue", "AntiqueWhite", "Aqua", "Aquamarine", "Azure",
+ "Beige", "Bisque", "Black", "BlanchedAlmond", "Blue",
+ "BlueViolet", "Brown", "BurlyWood", "CadetBlue", "Chartreuse",
+ "Chocolate", "Coral", "CornflowerBlue", "Cornsilk", "Crimson",
+ "Cyan", "DarkBlue", "DarkCyan", "DarkGoldenRod", "DarkGray",
+ "DarkGrey", "DarkGreen", "DarkKhaki", "DarkMagenta", "DarkOliveGreen",
+ "DarkOrange", "DarkOrchid", "DarkRed", "DarkSalmon", "DarkSeaGreen",
+ "DarkSlateBlue", "DarkSlateGray", "DarkSlateGrey", "DarkTurquoise", "DarkViolet",
+ "DeepPink", "DeepSkyBlue", "DimGray", "DimGrey", "DodgerBlue",
+ "FireBrick", "FloralWhite", "ForestGreen", "Fuchsia", "Gainsboro",
+ "GhostWhite", "Gold", "GoldenRod", "Gray", "Grey",
+ "Green", "GreenYellow", "HoneyDew", "HotPink", "IndianRed",
+ "Indigo", "Ivory", "Khaki", "Lavender", "LavenderBlush",
+ "LawnGreen", "LemonChiffon", "LightBlue", "LightCoral", "LightCyan",
+ "LightGoldenRodYellow", "LightGray", "LightGrey", "LightGreen", "LightPink",
+ "LightSalmon", "LightSeaGreen", "LightSkyBlue", "LightSlateGray", "LightSlateGrey",
+ "LightSteelBlue", "LightYellow", "Lime", "LimeGreen", "Linen",
+ "Magenta", "Maroon", "MediumAquaMarine", "MediumBlue", "MediumOrchid",
+ "MediumPurple", "MediumSeaGreen", "MediumSlateBlue", "MediumSpringGreen", "MediumTurquoise",
+ "MediumVioletRed", "MidnightBlue", "MintCream", "MistyRose", "Moccasin",
+ "NavajoWhite", "Navy", "OldLace", "Olive", "OliveDrab",
+ "Orange", "OrangeRed", "Orchid", "PaleGoldenRod", "PaleGreen",
+ "PaleTurquoise", "PaleVioletRed", "PapayaWhip", "PeachPuff", "Peru",
+ "Pink", "Plum", "PowderBlue", "Purple", "Red",
+ "RosyBrown", "RoyalBlue", "SaddleBrown", "Salmon", "SandyBrown",
+ "SeaGreen", "SeaShell", "Sienna", "Silver", "SkyBlue",
+ "SlateBlue", "SlateGray", "SlateGrey", "Snow", "SpringGreen",
+ "SteelBlue", "Tan", "Teal", "Thistle", "Tomato",
+ "Turquoise", "Violet", "Wheat", "White", "WhiteSmoke",
+ "Yellow", "YellowGreen"
+ ]
+
+ def __init__(self, tag, attrs=None, content=None):
+ self.tag = tag
+ self.attrs = attrs or {}
+ self.content = content or []
+
+ def __str__(self):
+ return self.render()
+
+ def render(self):
+ """Renders the element."""
+ attrs = self.render_attrs()
+ content = self.render_content()
+ return f"<{self.tag}{attrs}>{content}{self.tag}>"
+
+ def render_attrs(self):
+ """Renders the attributes of the element."""
+ # Remove duplicate attributes
+ for key, value in self.attrs.items():
+ self.attrs[key] = list(set(value))
+
+ # Sort attribute items
+ for key, value in self.attrs.items():
+ self.attrs[key] = sorted(value)
+
+ # Sort attributes
+ self.attrs = dict(sorted(self.attrs.items()))
+
+ # Render attributes
+ attrs = ""
+ for key, value in self.attrs.items():
+ attrs += f' {key}="{" ".join(value)}"'
+ return attrs
+
+ def render_content(self):
+ """Renders the content of the element."""
+ content = ""
+ for item in self.content:
+ content += str(item)
+ return content
+
+ def add_attribute(self, key, value):
+ """Adds an attribute."""
+ if key not in self.attrs:
+ self.attrs[key] = []
+ self.attrs[key].append(value)
+
+ def add_content(self, content):
+ """Adds content."""
+ self.content.append(content)
+
+ def manage_color_attribute(self, option_name, option_value, value, data):
+ """Manages the color attribute. option_name can be 'color' or 'bg'.
+ The value can be a bootstrap class name, a color name, a hex value, a rgb value, a hsl value,
+ a value from the data dictionary."""
+
+ print(option_name, option_value, value, data)
+
+ if option_value in self.COLOR_CLASSES:
+ self.add_attribute('class', f"badge-{value}")
+
+ color_style_string = 'color'
+ if option_name == 'bg':
+ color_style_string = 'background-color'
+
+ if option_value in self.HTML_COLOR_NAMES:
+ self.add_attribute('style', f'{color_style_string}: {option_value};')
+ if option_value.startswith('#') or option_value.startswith('rgb') or option_value.startswith('hsl'):
+ self.add_attribute('style', f'{color_style_string}: {option_value};')
+ if option_value.startswith('val__'):
+ self.add_attribute('style', f'{color_style_string}: niy;')
+ if option_value.startswith('byval__'):
+ self.add_attribute('style', f'{color_style_string}: niy;')
+ if option_value == 'byval':
+ self.add_attribute('style', f'{color_style_string}: {self.get_color_from_value(value)};')
+
+ @staticmethod
+ def get_color_from_value(value, lightness=80):
+ """Translates a value to a color."""
+ if value is None:
+ return ''
+
+ value = str(value)
+ hash_value = 0
+ for char in value:
+ hash_value = ord(char) + ((hash_value << 5) - hash_value)
+ hash_value = hash_value & hash_value
+
+ return f'hsl({hash_value % 360}, {100}%, {lightness}%)'
diff --git a/ndr_core/ndr_templatetags/template_string.py b/ndr_core/ndr_templatetags/template_string.py
index e9c65e4..cbbcb77 100644
--- a/ndr_core/ndr_templatetags/template_string.py
+++ b/ndr_core/ndr_templatetags/template_string.py
@@ -89,7 +89,7 @@ def get_raw_value(self, data):
try:
return data[self.variable]
except KeyError as e:
- raise KeyError(f"Key not found in data: {e}")
+ raise KeyError(f"Key not found in data: {e}") from e
def get_value(self, data):
"""Returns the value of the variable with the filter applied."""
@@ -99,14 +99,16 @@ def get_value(self, data):
if isinstance(raw_value, list):
filtered_values = []
for value in raw_value:
- filtered_values.append(self.apply_filters(value))
+ applied = self.apply_filters(value)
+ if applied is not None:
+ filtered_values.append(applied)
return filtered_values
return self.apply_filters(self.get_raw_value(data))
return raw_value
except KeyError as e:
- raise e
+ raise KeyError(f"Key not found in data: {e}") from e
except ValueError as e:
- raise e
+ raise ValueError(f"Could not parse variable: {e}") from e
def _get_nested_value(self, data):
"""Returns the value of the variable."""
@@ -120,9 +122,9 @@ def _get_nested_value(self, data):
try:
value = value[int(key)]
except IndexError:
- raise KeyError(f"Nested key not found {e}")
+ raise IndexError(f"Nested key not found: {e}") from e
except KeyError as e:
- raise KeyError(f"Nested key not found {e}")
+ raise KeyError(f"Nested key not found: {e}") from e
return value
def apply_filters(self, value):
@@ -203,8 +205,7 @@ def get_variables(self, flatten=False):
return flat_variables
return variables
except ValueError as e:
- self.errors.append(e)
- return []
+ raise ValueError(f"Could not parse string: {e}") from e
def get_string(self):
"""Returns the string.
@@ -249,6 +250,7 @@ def join_list(variable, data):
return ''
def get_error(self, e):
+ """Returns an error message to display within the result HTML."""
if self.show_errors:
alert = f'''
- {{ field }}) + {{ field }}
diff --git a/ndr_core/templates/ndr_core/result_renderers/configured_fields_template.html b/ndr_core/templates/ndr_core/result_renderers/configured_fields_template.html index a047fdb..4cc4c7a 100644 --- a/ndr_core/templates/ndr_core/result_renderers/configured_fields_template.html +++ b/ndr_core/templates/ndr_core/result_renderers/configured_fields_template.html @@ -3,11 +3,10 @@ {% load i18n %} {% block search_result_content %} - {{ result.results }} {{ card_content }} {% endblock %} {% block citation_info %} {% translate 'Cite:' %} - <no citation> + {{ citation }} {% endblock %} diff --git a/ndr_core/templatetags/ndr_utils.py b/ndr_core/templatetags/ndr_utils.py index 2df4b5b..53125e0 100644 --- a/ndr_core/templatetags/ndr_utils.py +++ b/ndr_core/templatetags/ndr_utils.py @@ -30,12 +30,21 @@ def __init__(self, result, search_config, result_card_group='normal'): def create_card(self, context, result): """Creates a result card.""" card_context = {"result": result, - "card_content": self.create_grid(context, result['data'])} + "card_content": self.create_grid(context, result['data']), + "citation": self.create_citation(context, result)} card_template = 'ndr_core/result_renderers/configured_fields_template.html' card_template_str = get_template(card_template).render(card_context) return mark_safe(card_template_str) + def create_citation(self, context, result): + """Creates a citation.""" + exp = self.search_config.resolve(context).citation_expression + template_string = TemplateString(exp, result['data'], show_errors=False) + citation = template_string.get_formatted_string() + citation = template_string.sanitize_html(citation) + return mark_safe(citation) + def create_grid(self, context, data): """Creates a grid of result fields.""" row_template = 'ndr_core/result_renderers/elements/result_row.html' @@ -48,7 +57,7 @@ def create_grid(self, context, data): row += 1 fields = [] for column in result_card_fields.filter(field_row=row).order_by('field_column'): - field_html = self.create_field(context, column, data) + field_html = self.create_field(column, data) fields.append(field_html) row_context = {"fields": fields} row_template_str = get_template(row_template).render(row_context) @@ -56,11 +65,12 @@ def create_grid(self, context, data): return mark_safe(card_grid_str) - def create_field(self, context, field, data): + @staticmethod + def create_field(field, data): """Creates a result field.""" field_template = 'ndr_core/result_renderers/elements/result_field.html' result_field = field.result_field - template_string = TemplateString(result_field.rich_expression, data, show_errors=False) + template_string = TemplateString(result_field.rich_expression, data, show_errors=True) field_content = template_string.get_formatted_string() field_content = template_string.sanitize_html(field_content) @@ -81,7 +91,6 @@ def render(self, context): html_string += self.create_card(context, result) else: # No result card fields configured, so we render the result as pretty json - print("INFO: No result card fields configured, so we render the result as pretty json") card_context = {"result": result} card_template = 'ndr_core/result_renderers/default_template.html' html_string += get_template(card_template).render(card_context) @@ -126,4 +135,3 @@ def url_deparse(value): # return urllib.parse.unquote(value) return value.replace('_sl_', '/') - diff --git a/ndr_core/tests/test_html_element.py b/ndr_core/tests/test_html_element.py new file mode 100644 index 0000000..0139bd1 --- /dev/null +++ b/ndr_core/tests/test_html_element.py @@ -0,0 +1,30 @@ +from django.test import TestCase + +from ndr_core.ndr_templatetags.html_element import HTMLElement + + +class TemplateStringTestCase(TestCase): + + def test_html_element(self): + """ Tests the HTMLElement class.""" + element = HTMLElement("div", attrs={"class": ["test"]}, content=["Hello World"]) + self.assertEqual(str(element), '