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'Responsive image' + 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}" + + 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'''