From 0091b052d18dd8cdd05e02adf4e7bd99966b4e13 Mon Sep 17 00:00:00 2001 From: vasileios Date: Thu, 16 Mar 2023 15:31:43 +0100 Subject: [PATCH 01/45] [#1249] Added headers to the logs, fixed requests hooks --- src/log_outgoing_requests/admin.py | 4 ++- src/log_outgoing_requests/formatters.py | 7 ++++ src/log_outgoing_requests/handlers.py | 2 ++ src/log_outgoing_requests/log_requests.py | 3 +- .../migrations/0002_auto_20230316_1459.py | 33 +++++++++++++++++++ src/log_outgoing_requests/models.py | 21 ++++++++++-- .../tests/test_logging.py | 11 +++++++ 7 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 src/log_outgoing_requests/migrations/0002_auto_20230316_1459.py diff --git a/src/log_outgoing_requests/admin.py b/src/log_outgoing_requests/admin.py index 8289caa55a..fdc9531548 100644 --- a/src/log_outgoing_requests/admin.py +++ b/src/log_outgoing_requests/admin.py @@ -17,6 +17,8 @@ class OutgoingRequestsLogAdmin(admin.ModelAdmin): "timestamp", "req_content_type", "res_content_type", + "req_headers", + "res_headers", "trace", ) readonly_fields = fields @@ -30,7 +32,7 @@ class OutgoingRequestsLogAdmin(admin.ModelAdmin): "timestamp", ) list_filter = ("method", "status_code", "hostname") - search_fields = ("params", "query_params", "hostname") + search_fields = ("url", "params", "hostname") date_hierarchy = "timestamp" show_full_result_count = False diff --git a/src/log_outgoing_requests/formatters.py b/src/log_outgoing_requests/formatters.py index de29e75486..b12eae1154 100644 --- a/src/log_outgoing_requests/formatters.py +++ b/src/log_outgoing_requests/formatters.py @@ -3,6 +3,9 @@ class HttpFormatter(logging.Formatter): + def _formatHeaders(self, d): + return "\n".join(f"{k}: {v}" for k, v in d.items()) + def formatMessage(self, record): result = super().formatMessage(record) if record.name == "requests": @@ -10,14 +13,18 @@ def formatMessage(self, record): """ ---------------- request ---------------- {req.method} {req.url} + {reqhdrs} ---------------- response ---------------- {res.status_code} {res.reason} {res.url} + {reshdrs} """ ).format( req=record.req, res=record.res, + reqhdrs=self._formatHeaders(record.req.headers), + reshdrs=self._formatHeaders(record.res.headers), ) return result diff --git a/src/log_outgoing_requests/handlers.py b/src/log_outgoing_requests/handlers.py index 9158307df7..0da03fff2e 100644 --- a/src/log_outgoing_requests/handlers.py +++ b/src/log_outgoing_requests/handlers.py @@ -29,6 +29,8 @@ def emit(self, record): "res_content_type": record.res.headers.get("Content-Type", ""), "timestamp": record.requested_at, "response_ms": int(record.res.elapsed.total_seconds() * 1000), + "req_headers": record.req.headers, + "res_headers": record.res.headers, "trace": trace, } diff --git a/src/log_outgoing_requests/log_requests.py b/src/log_outgoing_requests/log_requests.py index ea994ab9ae..84abd274db 100644 --- a/src/log_outgoing_requests/log_requests.py +++ b/src/log_outgoing_requests/log_requests.py @@ -12,6 +12,7 @@ def hook_requests_logging(response, *args, **kwargs): """ A hook for requests library in order to add extra data to the logs """ + response.request.headers.pop("Authorization", None) extra = {"requested_at": timezone.now(), "req": response.request, "res": response} logger.debug("Outgoing request", extra=extra) @@ -30,7 +31,7 @@ def install_outgoing_requests_logging(): Session._original_request = Session.request def new_request(self, *args, **kwargs): - kwargs.setdefault("hooks", {"response": hook_requests_logging}) + self.hooks["response"].append(hook_requests_logging) return self._original_request(*args, **kwargs) Session.request = new_request diff --git a/src/log_outgoing_requests/migrations/0002_auto_20230316_1459.py b/src/log_outgoing_requests/migrations/0002_auto_20230316_1459.py new file mode 100644 index 0000000000..b10b725e2f --- /dev/null +++ b/src/log_outgoing_requests/migrations/0002_auto_20230316_1459.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.15 on 2023-03-16 13:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("log_outgoing_requests", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="outgoingrequestslog", + name="req_headers", + field=models.TextField( + blank=True, + help_text="The request headers.", + null=True, + verbose_name="Request headers", + ), + ), + migrations.AddField( + model_name="outgoingrequestslog", + name="res_headers", + field=models.TextField( + blank=True, + help_text="The response headers.", + null=True, + verbose_name="Response headers", + ), + ), + ] diff --git a/src/log_outgoing_requests/models.py b/src/log_outgoing_requests/models.py index a9f1e658b5..215e481a5c 100644 --- a/src/log_outgoing_requests/models.py +++ b/src/log_outgoing_requests/models.py @@ -13,6 +13,8 @@ class OutgoingRequestsLog(models.Model): default="", help_text=_("The url of the outgoing request."), ) + + # hostname is added so we can filter on it in the admin page hostname = models.CharField( verbose_name=_("Hostname"), max_length=255, @@ -52,6 +54,18 @@ class OutgoingRequestsLog(models.Model): blank=True, help_text=_("The content type of the response."), ) + req_headers = models.TextField( + verbose_name=_("Request headers"), + blank=True, + null=True, + help_text=_("The request headers."), + ) + res_headers = models.TextField( + verbose_name=_("Response headers"), + blank=True, + null=True, + help_text=_("The response headers."), + ) response_ms = models.PositiveIntegerField( verbose_name=_("Response in ms"), default=0, @@ -79,6 +93,9 @@ def __str__(self): ) @cached_property + def url_parsed(self): + return urlparse(self.url) + + @property def query_params(self): - parsed_url = urlparse(self.url) - return parsed_url.query + return self.url_parsed.query diff --git a/src/log_outgoing_requests/tests/test_logging.py b/src/log_outgoing_requests/tests/test_logging.py index 368d6f0da2..c72a42aedf 100644 --- a/src/log_outgoing_requests/tests/test_logging.py +++ b/src/log_outgoing_requests/tests/test_logging.py @@ -69,6 +69,17 @@ def test_expected_data_is_saved_when_saving_enabled(self, m): ) self.assertIsNone(request_log.trace) + def test_d(self, m): + self._setUpMocks(m) + + with self.assertLogs("requests", level="DEBUG") as logs: + requests.get( + "http://example.com/some-path?version=2.0", + headers={"Authorization": "test"}, + ) + + self.assertNotIn("Authorization", logs.records[0].req.headers) + def test_data_is_not_saved_when_saving_disabled(self, m): self._setUpMocks(m) From d35063908c94131399f853f70ff7ef36973984f3 Mon Sep 17 00:00:00 2001 From: vasileios Date: Thu, 16 Mar 2023 16:16:37 +0100 Subject: [PATCH 02/45] [#1249] Fixed authorization header removal --- src/log_outgoing_requests/handlers.py | 2 ++ src/log_outgoing_requests/log_requests.py | 1 - src/log_outgoing_requests/tests/test_logging.py | 12 ++++++------ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/log_outgoing_requests/handlers.py b/src/log_outgoing_requests/handlers.py index 0da03fff2e..ce8f6d2ba7 100644 --- a/src/log_outgoing_requests/handlers.py +++ b/src/log_outgoing_requests/handlers.py @@ -14,6 +14,8 @@ def emit(self, record): # save only the requests coming from the library requests if record and record.getMessage() == "Outgoing request": + record.req.headers.pop("Authorization", None) + if record.exc_info: trace = traceback.format_exc() diff --git a/src/log_outgoing_requests/log_requests.py b/src/log_outgoing_requests/log_requests.py index 84abd274db..bac831275e 100644 --- a/src/log_outgoing_requests/log_requests.py +++ b/src/log_outgoing_requests/log_requests.py @@ -12,7 +12,6 @@ def hook_requests_logging(response, *args, **kwargs): """ A hook for requests library in order to add extra data to the logs """ - response.request.headers.pop("Authorization", None) extra = {"requested_at": timezone.now(), "req": response.request, "res": response} logger.debug("Outgoing request", extra=extra) diff --git a/src/log_outgoing_requests/tests/test_logging.py b/src/log_outgoing_requests/tests/test_logging.py index c72a42aedf..6d646a20b9 100644 --- a/src/log_outgoing_requests/tests/test_logging.py +++ b/src/log_outgoing_requests/tests/test_logging.py @@ -72,13 +72,13 @@ def test_expected_data_is_saved_when_saving_enabled(self, m): def test_d(self, m): self._setUpMocks(m) - with self.assertLogs("requests", level="DEBUG") as logs: - requests.get( - "http://example.com/some-path?version=2.0", - headers={"Authorization": "test"}, - ) + requests.get( + "http://example.com/some-path?version=2.0", + headers={"Authorization": "test"}, + ) + log = OutgoingRequestsLog.objects.get() - self.assertNotIn("Authorization", logs.records[0].req.headers) + self.assertNotIn("Authorization", eval(log.req_headers)) def test_data_is_not_saved_when_saving_disabled(self, m): self._setUpMocks(m) From 9640d6f1e8e0d7bf8eccd8f83a42e45e3a739ffd Mon Sep 17 00:00:00 2001 From: vasileios Date: Thu, 16 Mar 2023 16:47:29 +0100 Subject: [PATCH 03/45] [#1249] Fixed test --- src/log_outgoing_requests/tests/test_logging.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/log_outgoing_requests/tests/test_logging.py b/src/log_outgoing_requests/tests/test_logging.py index 6d646a20b9..2937d0513d 100644 --- a/src/log_outgoing_requests/tests/test_logging.py +++ b/src/log_outgoing_requests/tests/test_logging.py @@ -69,7 +69,8 @@ def test_expected_data_is_saved_when_saving_enabled(self, m): ) self.assertIsNone(request_log.trace) - def test_d(self, m): + @override_settings(LOG_OUTGOING_REQUESTS_DB_SAVE=True) + def test_authorization_header_is_not_saved(self, m): self._setUpMocks(m) requests.get( From 7d2861160f37bc1fe7d62fcf58448b66ca16ee0e Mon Sep 17 00:00:00 2001 From: vasileios Date: Wed, 15 Mar 2023 16:42:32 +0100 Subject: [PATCH 04/45] [#1238] Started with adding cta button inside product content --- src/open_inwoner/js/admin/ckeditor/editor.js | 43 ++++++++++++++++++++ src/open_inwoner/pdc/admin/product.py | 12 ++++++ src/open_inwoner/utils/ckeditor.py | 24 +++++++++++ 3 files changed, 79 insertions(+) diff --git a/src/open_inwoner/js/admin/ckeditor/editor.js b/src/open_inwoner/js/admin/ckeditor/editor.js index 393f406ba3..ffded06353 100644 --- a/src/open_inwoner/js/admin/ckeditor/editor.js +++ b/src/open_inwoner/js/admin/ckeditor/editor.js @@ -19,8 +19,48 @@ import Table from '@ckeditor/ckeditor5-table/src/table' import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar' import FilerImageAdapterPlugin from './plugins/filer/plugin' +import Plugin from '@ckeditor/ckeditor5-core/src/plugin' +import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview' + export default class ClassicEditor extends ClassicEditorBase {} +class CTARequestButtonPlugin extends Plugin { + init() { + const editor = this.editor + + editor.ui.componentFactory.add('addRequest', () => { + // The button will be an instance of ButtonView. + const button = new ButtonView() + + button.set({ + label: 'Add request button', + withText: true, + }) + + //Execute a callback function when the button is clicked + button.on('execute', () => { + const formId = document.getElementById('id_form') + const linkId = document.getElementById('id_link') + let productSlug = '' + + if (formId) { + productSlug = formId.dataset.productSlug + } else if (linkId) { + productSlug = linkId.dataset.productSlug + } + + editor.model.change((writer) => { + const link = writer.createText(`[CTA/${productSlug}]`) + + editor.model.insertContent(link, editor.model.document.selection) + }) + }) + + return button + }) + } +} + // Plugins to include in the build. ClassicEditor.builtinPlugins = [ Markdown, @@ -42,6 +82,7 @@ ClassicEditor.builtinPlugins = [ FilerImageAdapterPlugin, Table, TableToolbar, + CTARequestButtonPlugin, ] // Editor configuration. @@ -66,6 +107,8 @@ ClassicEditor.defaultConfig = { '|', 'undo', 'redo', + '|', + 'addRequest', ], }, image: { diff --git a/src/open_inwoner/pdc/admin/product.py b/src/open_inwoner/pdc/admin/product.py index 76571498a2..0a2e05d872 100644 --- a/src/open_inwoner/pdc/admin/product.py +++ b/src/open_inwoner/pdc/admin/product.py @@ -93,6 +93,18 @@ def display_categories(self, obj): display_categories.short_description = "categories" + def get_form(self, request, obj=None, **kwargs): + form = super().get_form(request, obj, **kwargs) + + if obj: + form.base_fields["link"].widget.attrs.update( + {"data-product-slug": obj.slug} + ) + form.base_fields["form"].widget.attrs.update( + {"data-product-slug": obj.slug} + ) + return form + @admin.register(ProductContact) class ProductContactAdmin(admin.ModelAdmin): diff --git a/src/open_inwoner/utils/ckeditor.py b/src/open_inwoner/utils/ckeditor.py index 6fca70d73e..13d5cf6c34 100644 --- a/src/open_inwoner/utils/ckeditor.py +++ b/src/open_inwoner/utils/ckeditor.py @@ -1,6 +1,8 @@ import markdown from bs4 import BeautifulSoup +from open_inwoner.pdc.models.product import Product + def get_rendered_content(content): """ @@ -38,5 +40,27 @@ def get_rendered_content(content): ) icon.append("open_in_new") element.append(icon) + if element.text.startswith("[CTA/"): + product_slug = element.text.split("/")[1][:-1] + try: + product = Product.objects.get(slug=product_slug) + except Product.DoesNotExist: + element.decompose() + continue + + icon = soup.new_tag("span") + icon.attrs.update( + {"aria-label": "Aanvraag starten", "class": "material-icons"} + ) + icon.append("arrow_forward") + element.string.replace_with("Aanvraag starten") + element.attrs.update( + { + "class": "button button--textless button--icon button--icon-before button--primary", + "href": f"{product.get_absolute_url()}/formulier", + } + ) + element.attrs["title"] = "Aanvraag starten" + element.append(icon) return soup From f43cbb2234a875f877d175781d8469768a4a9cdb Mon Sep 17 00:00:00 2001 From: vasileios Date: Wed, 15 Mar 2023 16:47:40 +0100 Subject: [PATCH 05/45] [#1238] Changed slug to id data attribute --- src/open_inwoner/js/admin/ckeditor/editor.js | 8 ++++---- src/open_inwoner/pdc/admin/product.py | 8 ++------ src/open_inwoner/utils/ckeditor.py | 1 + 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/open_inwoner/js/admin/ckeditor/editor.js b/src/open_inwoner/js/admin/ckeditor/editor.js index ffded06353..85759c6f1f 100644 --- a/src/open_inwoner/js/admin/ckeditor/editor.js +++ b/src/open_inwoner/js/admin/ckeditor/editor.js @@ -41,16 +41,16 @@ class CTARequestButtonPlugin extends Plugin { button.on('execute', () => { const formId = document.getElementById('id_form') const linkId = document.getElementById('id_link') - let productSlug = '' + let productId = '' if (formId) { - productSlug = formId.dataset.productSlug + productId = formId.dataset.productId } else if (linkId) { - productSlug = linkId.dataset.productSlug + productId = linkId.dataset.productId } editor.model.change((writer) => { - const link = writer.createText(`[CTA/${productSlug}]`) + const link = writer.createText(`[CTA/${productId}]`) editor.model.insertContent(link, editor.model.document.selection) }) diff --git a/src/open_inwoner/pdc/admin/product.py b/src/open_inwoner/pdc/admin/product.py index 0a2e05d872..9cc207118b 100644 --- a/src/open_inwoner/pdc/admin/product.py +++ b/src/open_inwoner/pdc/admin/product.py @@ -97,12 +97,8 @@ def get_form(self, request, obj=None, **kwargs): form = super().get_form(request, obj, **kwargs) if obj: - form.base_fields["link"].widget.attrs.update( - {"data-product-slug": obj.slug} - ) - form.base_fields["form"].widget.attrs.update( - {"data-product-slug": obj.slug} - ) + form.base_fields["link"].widget.attrs.update({"data-product-id": obj.id}) + form.base_fields["form"].widget.attrs.update({"data-product-id": obj.id}) return form diff --git a/src/open_inwoner/utils/ckeditor.py b/src/open_inwoner/utils/ckeditor.py index 13d5cf6c34..071bed850f 100644 --- a/src/open_inwoner/utils/ckeditor.py +++ b/src/open_inwoner/utils/ckeditor.py @@ -41,6 +41,7 @@ def get_rendered_content(content): icon.append("open_in_new") element.append(icon) if element.text.startswith("[CTA/"): + breakpoint() product_slug = element.text.split("/")[1][:-1] try: product = Product.objects.get(slug=product_slug) From 8c273cc1e92ca3a0eef11ff91aea6989bb21bba4 Mon Sep 17 00:00:00 2001 From: vasileios Date: Thu, 16 Mar 2023 12:47:36 +0100 Subject: [PATCH 06/45] [#1238] Fixed ckeditor for product detail content --- .../components/templatetags/product_tags.py | 18 +++ src/open_inwoner/js/admin/ckeditor/editor.js | 16 +-- .../templates/pages/product/detail.html | 4 +- src/open_inwoner/utils/ckeditor.py | 103 ++++++++++++------ 4 files changed, 96 insertions(+), 45 deletions(-) diff --git a/src/open_inwoner/components/templatetags/product_tags.py b/src/open_inwoner/components/templatetags/product_tags.py index 3885267787..a035693858 100644 --- a/src/open_inwoner/components/templatetags/product_tags.py +++ b/src/open_inwoner/components/templatetags/product_tags.py @@ -2,6 +2,8 @@ from django.urls import NoReverseMatch, reverse from django.utils.translation import gettext as _ +from open_inwoner.utils.ckeditor import get_product_rendered_content + register = template.Library() @@ -36,3 +38,19 @@ def product_finder( configurable_text=context["configurable_text"], ) return kwargs + + +@register.filter("product_ckeditor_content") +def product_ckeditor_content(product): + """ + Returns rendered content from ckeditor's textarea field specifically for a product. + + Usage: + {{ object|product_ckeditor_content }} + + Variables: + + product: Product | The product object + """ + rendered_content = get_product_rendered_content(product) + + return rendered_content diff --git a/src/open_inwoner/js/admin/ckeditor/editor.js b/src/open_inwoner/js/admin/ckeditor/editor.js index 85759c6f1f..3b109b6ffb 100644 --- a/src/open_inwoner/js/admin/ckeditor/editor.js +++ b/src/open_inwoner/js/admin/ckeditor/editor.js @@ -34,23 +34,17 @@ class CTARequestButtonPlugin extends Plugin { button.set({ label: 'Add request button', + class: 'cta_button_request', withText: true, }) //Execute a callback function when the button is clicked button.on('execute', () => { - const formId = document.getElementById('id_form') - const linkId = document.getElementById('id_link') - let productId = '' - - if (formId) { - productId = formId.dataset.productId - } else if (linkId) { - productId = linkId.dataset.productId - } - editor.model.change((writer) => { - const link = writer.createText(`[CTA/${productId}]`) + const link = writer.createText('[CTAREQUESTBUTTON]', { + //Temporary url for creating a link tag + linkHref: '/', + }) editor.model.insertContent(link, editor.model.document.selection) }) diff --git a/src/open_inwoner/templates/pages/product/detail.html b/src/open_inwoner/templates/pages/product/detail.html index 72196a4cf1..4d61227556 100644 --- a/src/open_inwoner/templates/pages/product/detail.html +++ b/src/open_inwoner/templates/pages/product/detail.html @@ -1,5 +1,5 @@ {% extends 'master.html' %} -{% load i18n l10n button_tags card_tags faq_tags file_tags grid_tags icon_tags link_tags map_tags notification_tags tag_tags utils condition_tags render_tags anchor_menu_tags %} +{% load i18n l10n button_tags card_tags faq_tags file_tags grid_tags icon_tags link_tags map_tags notification_tags tag_tags utils condition_tags render_tags anchor_menu_tags product_tags %} {% block header_image %} {% if object.image %} @@ -32,7 +32,7 @@

{% tag tags=object.tags.all %}

{{ object.summary }}

- {{ object.content|ckeditor_content|safe }} + {{ object|product_ckeditor_content|safe }} {% if product.form %} {% button_row mobile=True %} diff --git a/src/open_inwoner/utils/ckeditor.py b/src/open_inwoner/utils/ckeditor.py index 071bed850f..3992e231e4 100644 --- a/src/open_inwoner/utils/ckeditor.py +++ b/src/open_inwoner/utils/ckeditor.py @@ -30,38 +30,77 @@ def get_rendered_content(content): for element in soup.find_all(tag): element.attrs["class"] = class_name if element.name == "a" and element.attrs.get("href", "").startswith("http"): - icon = soup.new_tag("span") - icon.attrs.update( - { - "aria-hidden": "true", - "aria-label": "Opens in new window", - "class": "material-icons", - } - ) - icon.append("open_in_new") - element.append(icon) - if element.text.startswith("[CTA/"): - breakpoint() - product_slug = element.text.split("/")[1][:-1] - try: - product = Product.objects.get(slug=product_slug) - except Product.DoesNotExist: - element.decompose() - continue + element.attrs["target"] = "_blank" - icon = soup.new_tag("span") - icon.attrs.update( - {"aria-label": "Aanvraag starten", "class": "material-icons"} - ) - icon.append("arrow_forward") - element.string.replace_with("Aanvraag starten") - element.attrs.update( - { - "class": "button button--textless button--icon button--icon-before button--primary", - "href": f"{product.get_absolute_url()}/formulier", - } - ) - element.attrs["title"] = "Aanvraag starten" - element.append(icon) + return soup + + +def get_product_rendered_content(product): + """ + Takes product's content as an input and returns the rendered one. + """ + md = markdown.Markdown(extensions=["tables"]) + html = md.convert(product.content) + soup = BeautifulSoup(html, "html.parser") + class_adders = [ + ("h1", "h1"), + ("h2", "h2"), + ("h3", "h3"), + ("h4", "h4"), + ("h5", "h5"), + ("h6", "h6"), + ("img", "image"), + ("li", "li"), + ("p", "p"), + ("a", "link link--secondary"), + ("table", "table table--content"), + ("th", "table__header"), + ("td", "table__item"), + ] + for tag, class_name in class_adders: + for element in soup.find_all(tag): + element.attrs["class"] = class_name + if element.name == "a": + if "[CTAREQUESTBUTTON]" in element.text: + # decompose the element when product doesn't have either a link or a form + if not (product.link or product.form): + element.decompose() + continue + + # icon + icon = soup.new_tag("span") + icon.attrs.update( + {"aria-label": "Aanvraag starten", "class": "material-icons"} + ) + icon.append("arrow_forward") + + # button + element.string.replace_with("Aanvraag starten") + element.attrs.update( + { + "class": "start_request button button--textless button--icon button--icon-before button--primary", + "href": ( + product.link + if product.link + else f"{product.get_absolute_url()}formulier" + ), + "title": "Aanvraag starten", + } + ) + element.append(icon) + if product.link: + element.attrs.update({"target": "_blank"}) + + elif element.attrs.get("href", "").startswith("http"): + icon = soup.new_tag("span") + icon.attrs.update( + { + "aria-hidden": "true", + "aria-label": "Opens in new window", + "class": "material-icons", + } + ) + icon.append("open_in_new") + element.append(icon) return soup From 04816a96e26608ea8e4d8d3b3f078153c5748a98 Mon Sep 17 00:00:00 2001 From: vasileios Date: Fri, 17 Mar 2023 12:19:16 +0100 Subject: [PATCH 07/45] [#1238] Removed ckeditor button --- src/open_inwoner/js/admin/ckeditor/editor.js | 37 ----- src/open_inwoner/pdc/admin/product.py | 8 - .../migrations/0054_alter_product_content.py | 21 +++ src/open_inwoner/pdc/models/product.py | 4 +- src/open_inwoner/pdc/tests/test_product.py | 60 ++++++++ src/open_inwoner/utils/ckeditor.py | 142 +++++++++--------- 6 files changed, 151 insertions(+), 121 deletions(-) create mode 100644 src/open_inwoner/pdc/migrations/0054_alter_product_content.py diff --git a/src/open_inwoner/js/admin/ckeditor/editor.js b/src/open_inwoner/js/admin/ckeditor/editor.js index 3b109b6ffb..393f406ba3 100644 --- a/src/open_inwoner/js/admin/ckeditor/editor.js +++ b/src/open_inwoner/js/admin/ckeditor/editor.js @@ -19,42 +19,8 @@ import Table from '@ckeditor/ckeditor5-table/src/table' import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar' import FilerImageAdapterPlugin from './plugins/filer/plugin' -import Plugin from '@ckeditor/ckeditor5-core/src/plugin' -import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview' - export default class ClassicEditor extends ClassicEditorBase {} -class CTARequestButtonPlugin extends Plugin { - init() { - const editor = this.editor - - editor.ui.componentFactory.add('addRequest', () => { - // The button will be an instance of ButtonView. - const button = new ButtonView() - - button.set({ - label: 'Add request button', - class: 'cta_button_request', - withText: true, - }) - - //Execute a callback function when the button is clicked - button.on('execute', () => { - editor.model.change((writer) => { - const link = writer.createText('[CTAREQUESTBUTTON]', { - //Temporary url for creating a link tag - linkHref: '/', - }) - - editor.model.insertContent(link, editor.model.document.selection) - }) - }) - - return button - }) - } -} - // Plugins to include in the build. ClassicEditor.builtinPlugins = [ Markdown, @@ -76,7 +42,6 @@ ClassicEditor.builtinPlugins = [ FilerImageAdapterPlugin, Table, TableToolbar, - CTARequestButtonPlugin, ] // Editor configuration. @@ -101,8 +66,6 @@ ClassicEditor.defaultConfig = { '|', 'undo', 'redo', - '|', - 'addRequest', ], }, image: { diff --git a/src/open_inwoner/pdc/admin/product.py b/src/open_inwoner/pdc/admin/product.py index 9cc207118b..76571498a2 100644 --- a/src/open_inwoner/pdc/admin/product.py +++ b/src/open_inwoner/pdc/admin/product.py @@ -93,14 +93,6 @@ def display_categories(self, obj): display_categories.short_description = "categories" - def get_form(self, request, obj=None, **kwargs): - form = super().get_form(request, obj, **kwargs) - - if obj: - form.base_fields["link"].widget.attrs.update({"data-product-id": obj.id}) - form.base_fields["form"].widget.attrs.update({"data-product-id": obj.id}) - return form - @admin.register(ProductContact) class ProductContactAdmin(admin.ModelAdmin): diff --git a/src/open_inwoner/pdc/migrations/0054_alter_product_content.py b/src/open_inwoner/pdc/migrations/0054_alter_product_content.py new file mode 100644 index 0000000000..c379b70095 --- /dev/null +++ b/src/open_inwoner/pdc/migrations/0054_alter_product_content.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.15 on 2023-03-17 11:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("pdc", "0053_alter_product_summary"), + ] + + operations = [ + migrations.AlterField( + model_name="product", + name="content", + field=models.TextField( + help_text="Product content with build-in WYSIWYG editor. By adding '[CTABUTTON]' you can embed a cta-button for linking to the defined form or link", + verbose_name="Content", + ), + ), + ] diff --git a/src/open_inwoner/pdc/models/product.py b/src/open_inwoner/pdc/models/product.py index db031498bd..8d304ead94 100644 --- a/src/open_inwoner/pdc/models/product.py +++ b/src/open_inwoner/pdc/models/product.py @@ -88,7 +88,9 @@ class Product(models.Model): ) content = models.TextField( verbose_name=_("Content"), - help_text=_("Product content with build-in WYSIWYG editor"), + help_text=_( + "Product content with build-in WYSIWYG editor. By adding '[CTABUTTON]' you can embed a cta-button for linking to the defined form or link" + ), ) categories = models.ManyToManyField( "pdc.Category", diff --git a/src/open_inwoner/pdc/tests/test_product.py b/src/open_inwoner/pdc/tests/test_product.py index cc6f6c549d..2bf87361f8 100644 --- a/src/open_inwoner/pdc/tests/test_product.py +++ b/src/open_inwoner/pdc/tests/test_product.py @@ -147,3 +147,63 @@ def test_product_detail_shows_product_faq(self): self.assertTrue(response.pyquery('.anchor-menu a[href="#faq"]')) # check if the menu link target self.assertTrue(response.pyquery("#faq")) + + +class TestProductContent(WebTest): + def test_button_is_rendered_inside_content_when_link_and_cta_exist(self): + product = ProductFactory( + content="Some content [CTABUTTON]", link="http://www.example.com" + ) + + response = self.app.get( + reverse("pdc:product_detail", kwargs={"slug": product.slug}) + ) + cta_button = response.pyquery(".grid__main")[0].find_class("cta-button") + + self.assertTrue(cta_button) + self.assertIn("http://www.example.com", cta_button[0].values()) + + def test_button_is_rendered_inside_content_when_form_and_cta_exist(self): + product = ProductFactory(content="Some content [CTABUTTON]", form=2) + url = reverse("pdc:product_detail", kwargs={"slug": product.slug}) + + response = self.app.get( + reverse("pdc:product_detail", kwargs={"slug": product.slug}) + ) + cta_button = response.pyquery(".grid__main")[0].find_class("cta-button") + + self.assertTrue(cta_button) + self.assertIn(f"{url}formulier", cta_button[0].values()) + + def test_button_is_rendered_inside_content_when_form_and_link_and_cta_exist(self): + product = ProductFactory( + content="Some content [CTABUTTON]", link="http://www.example.com", form=2 + ) + + response = self.app.get( + reverse("pdc:product_detail", kwargs={"slug": product.slug}) + ) + cta_button = response.pyquery(".grid__main")[0].find_class("cta-button") + + self.assertTrue(cta_button) + self.assertIn("http://www.example.com", cta_button[0].values()) + + def test_button_is_not_rendered_inside_content_when_no_cta(self): + product = ProductFactory(content="Some content", link="http://www.example.com") + + response = self.app.get( + reverse("pdc:product_detail", kwargs={"slug": product.slug}) + ) + cta_button = response.pyquery(".grid__main")[0].find_class("cta-button") + + self.assertFalse(cta_button) + + def test_button_is_not_rendered_inside_content_when_no_form_or_link(self): + product = ProductFactory(content="Some content [CTABUTTON]") + + response = self.app.get( + reverse("pdc:product_detail", kwargs={"slug": product.slug}) + ) + cta_button = response.pyquery(".grid__main")[0].find_class("cta-button") + + self.assertFalse(cta_button) diff --git a/src/open_inwoner/utils/ckeditor.py b/src/open_inwoner/utils/ckeditor.py index 3992e231e4..a695ed1159 100644 --- a/src/open_inwoner/utils/ckeditor.py +++ b/src/open_inwoner/utils/ckeditor.py @@ -1,7 +1,21 @@ import markdown from bs4 import BeautifulSoup -from open_inwoner.pdc.models.product import Product +CLASS_ADDERS = [ + ("h1", "h1"), + ("h2", "h2"), + ("h3", "h3"), + ("h4", "h4"), + ("h5", "h5"), + ("h6", "h6"), + ("img", "image"), + ("li", "li"), + ("p", "p"), + ("a", "link link--secondary"), + ("table", "table table--content"), + ("th", "table__header"), + ("td", "table__item"), +] def get_rendered_content(content): @@ -11,22 +25,8 @@ def get_rendered_content(content): md = markdown.Markdown(extensions=["tables"]) html = md.convert(content) soup = BeautifulSoup(html, "html.parser") - class_adders = [ - ("h1", "h1"), - ("h2", "h2"), - ("h3", "h3"), - ("h4", "h4"), - ("h5", "h5"), - ("h6", "h6"), - ("img", "image"), - ("li", "li"), - ("p", "p"), - ("a", "link link--secondary"), - ("table", "table table--content"), - ("th", "table__header"), - ("td", "table__item"), - ] - for tag, class_name in class_adders: + + for tag, class_name in CLASS_ADDERS: for element in soup.find_all(tag): element.attrs["class"] = class_name if element.name == "a" and element.attrs.get("href", "").startswith("http"): @@ -42,65 +42,57 @@ def get_product_rendered_content(product): md = markdown.Markdown(extensions=["tables"]) html = md.convert(product.content) soup = BeautifulSoup(html, "html.parser") - class_adders = [ - ("h1", "h1"), - ("h2", "h2"), - ("h3", "h3"), - ("h4", "h4"), - ("h5", "h5"), - ("h6", "h6"), - ("img", "image"), - ("li", "li"), - ("p", "p"), - ("a", "link link--secondary"), - ("table", "table table--content"), - ("th", "table__header"), - ("td", "table__item"), - ] - for tag, class_name in class_adders: + + for tag, class_name in CLASS_ADDERS: for element in soup.find_all(tag): + if element.attrs.get("class") and "cta-button" in element.attrs["class"]: + continue + element.attrs["class"] = class_name - if element.name == "a": - if "[CTAREQUESTBUTTON]" in element.text: - # decompose the element when product doesn't have either a link or a form - if not (product.link or product.form): - element.decompose() - continue - - # icon - icon = soup.new_tag("span") - icon.attrs.update( - {"aria-label": "Aanvraag starten", "class": "material-icons"} - ) - icon.append("arrow_forward") - - # button - element.string.replace_with("Aanvraag starten") - element.attrs.update( - { - "class": "start_request button button--textless button--icon button--icon-before button--primary", - "href": ( - product.link - if product.link - else f"{product.get_absolute_url()}formulier" - ), - "title": "Aanvraag starten", - } - ) - element.append(icon) - if product.link: - element.attrs.update({"target": "_blank"}) - - elif element.attrs.get("href", "").startswith("http"): - icon = soup.new_tag("span") - icon.attrs.update( - { - "aria-hidden": "true", - "aria-label": "Opens in new window", - "class": "material-icons", - } - ) - icon.append("open_in_new") - element.append(icon) + + if "[CTABUTTON]" in element.text: + # decompose the element when product doesn't have either a link or a form + if not (product.link or product.form): + element.decompose() + continue + + # icon + icon = soup.new_tag("span") + icon.attrs.update( + {"aria-label": "Aanvraag starten", "class": "material-icons"} + ) + icon.append("arrow_forward") + + # button + element.name = "a" + + element.string.replace_with("Aanvraag starten") + element.attrs.update( + { + "class": "button button--textless button--icon button--icon-before button--primary cta-button", + "href": ( + product.link + if product.link + else f"{product.get_absolute_url()}formulier" + ), + "title": "Aanvraag starten", + } + ) + element.append(icon) + + if product.link: + element.attrs.update({"target": "_blank"}) + + if element.name == "a" and element.attrs.get("href", "").startswith("http"): + icon = soup.new_tag("span") + icon.attrs.update( + { + "aria-hidden": "true", + "aria-label": "Opens in new window", + "class": "material-icons", + } + ) + icon.append("open_in_new") + element.append(icon) return soup From 029e965eb907dffd449422b94c3d30563ff92d13 Mon Sep 17 00:00:00 2001 From: vasileios Date: Tue, 21 Mar 2023 08:42:40 +0100 Subject: [PATCH 08/45] [#1238] Fixed form links and button style --- src/open_inwoner/utils/ckeditor.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/open_inwoner/utils/ckeditor.py b/src/open_inwoner/utils/ckeditor.py index a695ed1159..5b6c7168cd 100644 --- a/src/open_inwoner/utils/ckeditor.py +++ b/src/open_inwoner/utils/ckeditor.py @@ -65,25 +65,23 @@ def get_product_rendered_content(product): # button element.name = "a" - - element.string.replace_with("Aanvraag starten") + element.string = "" element.attrs.update( { "class": "button button--textless button--icon button--icon-before button--primary cta-button", - "href": ( - product.link - if product.link - else f"{product.get_absolute_url()}formulier" - ), + "href": product.link if product.link else product.form_link, "title": "Aanvraag starten", } ) element.append(icon) + element.append("Aanvraag starten") if product.link: element.attrs.update({"target": "_blank"}) - if element.name == "a" and element.attrs.get("href", "").startswith("http"): + elif element.name == "a" and element.attrs.get("href", "").startswith( + "http" + ): icon = soup.new_tag("span") icon.attrs.update( { From ff5d1eb9253c063a96ab228604fbc243a0a4490c Mon Sep 17 00:00:00 2001 From: vasileios Date: Tue, 21 Mar 2023 08:50:26 +0100 Subject: [PATCH 09/45] [#1238] Fixed conflict in migrations --- ...alter_product_content.py => 0055_alter_product_content.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/open_inwoner/pdc/migrations/{0054_alter_product_content.py => 0055_alter_product_content.py} (83%) diff --git a/src/open_inwoner/pdc/migrations/0054_alter_product_content.py b/src/open_inwoner/pdc/migrations/0055_alter_product_content.py similarity index 83% rename from src/open_inwoner/pdc/migrations/0054_alter_product_content.py rename to src/open_inwoner/pdc/migrations/0055_alter_product_content.py index c379b70095..db32a93935 100644 --- a/src/open_inwoner/pdc/migrations/0054_alter_product_content.py +++ b/src/open_inwoner/pdc/migrations/0055_alter_product_content.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.15 on 2023-03-17 11:08 +# Generated by Django 3.2.15 on 2023-03-21 07:49 from django.db import migrations, models @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ - ("pdc", "0053_alter_product_summary"), + ("pdc", "0054_alter_organization_logo"), ] operations = [ From f0437e6089e4d7f9244b823052119b5e08b42f1f Mon Sep 17 00:00:00 2001 From: Alex de Landgraaf Date: Tue, 21 Mar 2023 07:53:44 +0000 Subject: [PATCH 10/45] Splitting up configuration --- .../conf/fixtures/django-admin-index.json | 118 ++++++++++-------- 1 file changed, 68 insertions(+), 50 deletions(-) diff --git a/src/open_inwoner/conf/fixtures/django-admin-index.json b/src/open_inwoner/conf/fixtures/django-admin-index.json index bdf7f3fe52..86cf91a760 100644 --- a/src/open_inwoner/conf/fixtures/django-admin-index.json +++ b/src/open_inwoner/conf/fixtures/django-admin-index.json @@ -120,7 +120,7 @@ { "model": "admin_index.appgroup", "fields": { - "order": 7, + "order": 9, "name": "Overige / Diverse", "slug": "overige-diverse", "models": [ @@ -162,7 +162,7 @@ { "model": "admin_index.appgroup", "fields": { - "order": 6, + "order": 7, "name": "Configuratie", "slug": "configuratie", "models": [ @@ -182,61 +182,13 @@ "flatpages", "flatpage" ], - [ - "haalcentraal", - "haalcentraalconfig" - ], [ "mail_editor", "mailtemplate" ], - [ - "mozilla_django_oidc_db", - "openidconnectconfig" - ], - [ - "notifications_api_common", - "notificationsconfig" - ], - [ - "notifications_api_common", - "subscription" - ], - [ - "openformsclient", - "configuration" - ], - [ - "openzaak", - "catalogusconfig" - ], - [ - "openzaak", - "openzaakconfig" - ], - [ - "openzaak", - "usercasestatusnotification" - ], - [ - "openzaak", - "zaaktypeconfig" - ], - [ - "openzaak", - "zaaktypeinformatieobjecttypeconfig" - ], [ "sites", "site" - ], - [ - "zgw_consumers", - "nlxconfig" - ], - [ - "zgw_consumers", - "service" ] ] } @@ -333,6 +285,72 @@ ] } }, +{ + "model": "admin_index.appgroup", + "fields": { + "order": 6, + "name": "Koppelingen", + "slug": "koppelingen", + "models": [ + [ + "haalcentraal", + "haalcentraalconfig" + ], + [ + "mozilla_django_oidc_db", + "openidconnectconfig" + ], + [ + "notifications_api_common", + "notificationsconfig" + ], + [ + "notifications_api_common", + "subscription" + ], + [ + "openformsclient", + "configuration" + ], + [ + "openzaak", + "catalogusconfig" + ], + [ + "openzaak", + "openzaakconfig" + ], + [ + "openzaak", + "usercaseinfoobjectnotification" + ], + [ + "openzaak", + "usercasestatusnotification" + ], + [ + "openzaak", + "zaaktypeconfig" + ], + [ + "openzaak", + "zaaktypeinformatieobjecttypeconfig" + ], + [ + "zgw_consumers", + "certificate" + ], + [ + "zgw_consumers", + "nlxconfig" + ], + [ + "zgw_consumers", + "service" + ] + ] + } +}, { "model": "admin_index.applink", "fields": { From c1df53b9d5418df8fa367afb179abbd6527d76c8 Mon Sep 17 00:00:00 2001 From: vasileios Date: Tue, 21 Mar 2023 09:26:48 +0100 Subject: [PATCH 11/45] [#1238] Fixed tests --- src/open_inwoner/pdc/tests/test_product.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/open_inwoner/pdc/tests/test_product.py b/src/open_inwoner/pdc/tests/test_product.py index 2bf87361f8..49b12de9a5 100644 --- a/src/open_inwoner/pdc/tests/test_product.py +++ b/src/open_inwoner/pdc/tests/test_product.py @@ -161,7 +161,7 @@ def test_button_is_rendered_inside_content_when_link_and_cta_exist(self): cta_button = response.pyquery(".grid__main")[0].find_class("cta-button") self.assertTrue(cta_button) - self.assertIn("http://www.example.com", cta_button[0].values()) + self.assertIn(product.link, cta_button[0].values()) def test_button_is_rendered_inside_content_when_form_and_cta_exist(self): product = ProductFactory(content="Some content [CTABUTTON]", form=2) @@ -173,7 +173,7 @@ def test_button_is_rendered_inside_content_when_form_and_cta_exist(self): cta_button = response.pyquery(".grid__main")[0].find_class("cta-button") self.assertTrue(cta_button) - self.assertIn(f"{url}formulier", cta_button[0].values()) + self.assertIn(product.form_link, cta_button[0].values()) def test_button_is_rendered_inside_content_when_form_and_link_and_cta_exist(self): product = ProductFactory( @@ -186,7 +186,7 @@ def test_button_is_rendered_inside_content_when_form_and_link_and_cta_exist(self cta_button = response.pyquery(".grid__main")[0].find_class("cta-button") self.assertTrue(cta_button) - self.assertIn("http://www.example.com", cta_button[0].values()) + self.assertIn(product.link, cta_button[0].values()) def test_button_is_not_rendered_inside_content_when_no_cta(self): product = ProductFactory(content="Some content", link="http://www.example.com") From 179464a53f0744f2028033dd1115f5963e7689c5 Mon Sep 17 00:00:00 2001 From: vasileios Date: Tue, 21 Mar 2023 09:40:08 +0100 Subject: [PATCH 12/45] [#1249] Fixed tests and migrations --- ..._20230316_1459.py => 0003_auto_20230321_0724.py} | 4 ++-- src/log_outgoing_requests/tests/test_logging.py | 13 ++++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) rename src/log_outgoing_requests/migrations/{0002_auto_20230316_1459.py => 0003_auto_20230321_0724.py} (86%) diff --git a/src/log_outgoing_requests/migrations/0002_auto_20230316_1459.py b/src/log_outgoing_requests/migrations/0003_auto_20230321_0724.py similarity index 86% rename from src/log_outgoing_requests/migrations/0002_auto_20230316_1459.py rename to src/log_outgoing_requests/migrations/0003_auto_20230321_0724.py index b10b725e2f..673313a1e8 100644 --- a/src/log_outgoing_requests/migrations/0002_auto_20230316_1459.py +++ b/src/log_outgoing_requests/migrations/0003_auto_20230321_0724.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.15 on 2023-03-16 13:59 +# Generated by Django 3.2.15 on 2023-03-21 06:24 from django.db import migrations, models @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ - ("log_outgoing_requests", "0001_initial"), + ("log_outgoing_requests", "0002_alter_outgoingrequestslog_url"), ] operations = [ diff --git a/src/log_outgoing_requests/tests/test_logging.py b/src/log_outgoing_requests/tests/test_logging.py index 2937d0513d..e5dd85b200 100644 --- a/src/log_outgoing_requests/tests/test_logging.py +++ b/src/log_outgoing_requests/tests/test_logging.py @@ -30,8 +30,6 @@ def test_outgoing_requests_are_logged(self, m): @override_settings(LOG_OUTGOING_REQUESTS_DB_SAVE=True) def test_expected_data_is_saved_when_saving_enabled(self, m): - self._setUpMocks(m) - methods = [ ("POST", requests.post, m.post), ("PUT", requests.put, m.put), @@ -63,6 +61,15 @@ def test_expected_data_is_saved_when_saving_enabled(self, m): self.assertEqual(request_log.req_content_type, "") self.assertEqual(request_log.res_content_type, "") self.assertEqual(request_log.response_ms, 0) + self.assertEqual( + request_log.req_headers, + ( + "{'User-Agent': 'python-requests/2.26.0', 'Accept-Encoding': 'gzip, deflate, br', 'Accept': '*/*', 'Connection': 'keep-alive'}" + if method == "HEAD" + else "{'User-Agent': 'python-requests/2.26.0', 'Accept-Encoding': 'gzip, deflate, br', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Length': '0'}" + ), + ) + self.assertEqual(request_log.res_headers, "{}") self.assertEqual( request_log.timestamp.strftime("%Y-%m-%d %H:%M:%S"), "2021-10-18 13:00:00", @@ -79,7 +86,7 @@ def test_authorization_header_is_not_saved(self, m): ) log = OutgoingRequestsLog.objects.get() - self.assertNotIn("Authorization", eval(log.req_headers)) + self.assertNotIn("Authorization", log.req_headers) def test_data_is_not_saved_when_saving_disabled(self, m): self._setUpMocks(m) From f62301b9c78bb95bccf38ea2e9e3dba2c536e5a8 Mon Sep 17 00:00:00 2001 From: vasileios Date: Tue, 21 Mar 2023 10:29:53 +0100 Subject: [PATCH 13/45] [#1249] Remove authorization without affecting the initial headers --- src/log_outgoing_requests/handlers.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/log_outgoing_requests/handlers.py b/src/log_outgoing_requests/handlers.py index ce8f6d2ba7..2418dc1608 100644 --- a/src/log_outgoing_requests/handlers.py +++ b/src/log_outgoing_requests/handlers.py @@ -14,13 +14,17 @@ def emit(self, record): # save only the requests coming from the library requests if record and record.getMessage() == "Outgoing request": - record.req.headers.pop("Authorization", None) + exclude_headers = ["Authorization"] + safe_req_headers = { + k: v + for k, v in record.req.headers.items() + if k not in exclude_headers + } if record.exc_info: trace = traceback.format_exc() parsed_url = urlparse(record.req.url) - kwargs = { "url": record.req.url, "hostname": parsed_url.hostname, @@ -31,7 +35,7 @@ def emit(self, record): "res_content_type": record.res.headers.get("Content-Type", ""), "timestamp": record.requested_at, "response_ms": int(record.res.elapsed.total_seconds() * 1000), - "req_headers": record.req.headers, + "req_headers": safe_req_headers, "res_headers": record.res.headers, "trace": trace, } From 0bb7974dc418203143a70662a706825c33cbef87 Mon Sep 17 00:00:00 2001 From: vasileios Date: Tue, 21 Mar 2023 12:27:39 +0100 Subject: [PATCH 14/45] [#1248] Use next url after login --- src/open_inwoner/accounts/templates/registration/login.html | 6 +++--- src/open_inwoner/accounts/tests/test_auth.py | 6 ++++++ .../components/templates/components/Typography/Link.html | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/open_inwoner/accounts/templates/registration/login.html b/src/open_inwoner/accounts/templates/registration/login.html index d319bb6f6c..89c47a6863 100644 --- a/src/open_inwoner/accounts/templates/registration/login.html +++ b/src/open_inwoner/accounts/templates/registration/login.html @@ -21,7 +21,7 @@

{% trans 'Welkom' %}

- {% link href='digid:login' text=_('Inloggen met DigiD') secondary=True icon='arrow_forward' extra_classes="link--digid" %} + {% link href='digid:login' next=next text=_('Inloggen met DigiD') secondary=True icon='arrow_forward' extra_classes="link--digid" %} {% endrender_card %} {% endif %} @@ -36,7 +36,7 @@

{% trans 'Welkom' %}

{% else %}
{% endif %} - {% link text=site_config.openid_connect_login_text href='oidc_authentication_init' secondary=True icon='arrow_forward' icon_position="after" %} + {% link text=site_config.openid_connect_login_text href='oidc_authentication_init' next=next secondary=True icon='arrow_forward' icon_position="after" %} {% endrender_card %} {% endif %} @@ -46,7 +46,7 @@

{% trans 'Welkom' %}

{% csrf_token %} {% input form.username %} {% input form.password %} - {% button text=_('Wachtwoord vergeten?') href='password_reset' secondary=True transparent=True align='right' %} + {% button text=_('Wachtwoord vergeten?') href='password_reset' next=next secondary=True transparent=True align='right' %} {% form_actions primary_text=_("Inloggen") primary_icon="arrow_forward" secondary_href='django_registration_register' secondary_text=_('Registreer') secondary_icon='arrow_forward' single=True %} {% endrender_form %} {% endrender_card %} diff --git a/src/open_inwoner/accounts/tests/test_auth.py b/src/open_inwoner/accounts/tests/test_auth.py index 71218d86f1..62e97e034c 100644 --- a/src/open_inwoner/accounts/tests/test_auth.py +++ b/src/open_inwoner/accounts/tests/test_auth.py @@ -819,6 +819,12 @@ def setUp(self): self.config.login_allow_registration = True self.config.save() + def test_login_page_has_next_url(self): + response = self.app.get( + f"{reverse('login')}?next={reverse('accounts:contact_list')}" + ) + self.assertIn(f"?next={reverse('accounts:contact_list')}", response) + def test_login(self): """Test that a user is successfully logged in.""" form = self.app.get(reverse("login")).forms["login-form"] diff --git a/src/open_inwoner/components/templates/components/Typography/Link.html b/src/open_inwoner/components/templates/components/Typography/Link.html index 2e3adc7213..3caa8931b3 100644 --- a/src/open_inwoner/components/templates/components/Typography/Link.html +++ b/src/open_inwoner/components/templates/components/Typography/Link.html @@ -1,7 +1,7 @@ {% load i18n helpers icon_tags %} Date: Tue, 21 Mar 2023 15:13:25 +0100 Subject: [PATCH 15/45] [#1238] Fixed translations and tests --- src/open_inwoner/pdc/tests/test_product.py | 7 ++++--- src/open_inwoner/utils/ckeditor.py | 12 +++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/open_inwoner/pdc/tests/test_product.py b/src/open_inwoner/pdc/tests/test_product.py index 49b12de9a5..4dae1e41a1 100644 --- a/src/open_inwoner/pdc/tests/test_product.py +++ b/src/open_inwoner/pdc/tests/test_product.py @@ -164,8 +164,7 @@ def test_button_is_rendered_inside_content_when_link_and_cta_exist(self): self.assertIn(product.link, cta_button[0].values()) def test_button_is_rendered_inside_content_when_form_and_cta_exist(self): - product = ProductFactory(content="Some content [CTABUTTON]", form=2) - url = reverse("pdc:product_detail", kwargs={"slug": product.slug}) + product = ProductFactory(content="Some content [CTABUTTON]", form="demo") response = self.app.get( reverse("pdc:product_detail", kwargs={"slug": product.slug}) @@ -177,7 +176,9 @@ def test_button_is_rendered_inside_content_when_form_and_cta_exist(self): def test_button_is_rendered_inside_content_when_form_and_link_and_cta_exist(self): product = ProductFactory( - content="Some content [CTABUTTON]", link="http://www.example.com", form=2 + content="Some content [CTABUTTON]", + link="http://www.example.com", + form="demo", ) response = self.app.get( diff --git a/src/open_inwoner/utils/ckeditor.py b/src/open_inwoner/utils/ckeditor.py index 5b6c7168cd..2b0406bb97 100644 --- a/src/open_inwoner/utils/ckeditor.py +++ b/src/open_inwoner/utils/ckeditor.py @@ -1,3 +1,5 @@ +from django.utils.translation import gettext as _ + import markdown from bs4 import BeautifulSoup @@ -59,7 +61,7 @@ def get_product_rendered_content(product): # icon icon = soup.new_tag("span") icon.attrs.update( - {"aria-label": "Aanvraag starten", "class": "material-icons"} + {"aria-label": _("Aanvraag starten"), "class": "material-icons"} ) icon.append("arrow_forward") @@ -69,12 +71,12 @@ def get_product_rendered_content(product): element.attrs.update( { "class": "button button--textless button--icon button--icon-before button--primary cta-button", - "href": product.link if product.link else product.form_link, - "title": "Aanvraag starten", + "href": (product.link if product.link else product.form_link), + "title": _("Aanvraag starten"), } ) element.append(icon) - element.append("Aanvraag starten") + element.append(_("Aanvraag starten")) if product.link: element.attrs.update({"target": "_blank"}) @@ -86,7 +88,7 @@ def get_product_rendered_content(product): icon.attrs.update( { "aria-hidden": "true", - "aria-label": "Opens in new window", + "aria-label": _("Opens in new window"), "class": "material-icons", } ) From 815c4517f30121541362a10276fbb41870060357 Mon Sep 17 00:00:00 2001 From: vasileios Date: Wed, 22 Mar 2023 11:04:03 +0100 Subject: [PATCH 16/45] [#1249] Fixed headers format and tests --- src/log_outgoing_requests/handlers.py | 19 ++++---- .../tests/test_logging.py | 48 +++++++++++++++---- 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/log_outgoing_requests/handlers.py b/src/log_outgoing_requests/handlers.py index 2418dc1608..21e1ec77d9 100644 --- a/src/log_outgoing_requests/handlers.py +++ b/src/log_outgoing_requests/handlers.py @@ -1,4 +1,5 @@ import logging +import textwrap import traceback from urllib.parse import urlparse @@ -14,12 +15,10 @@ def emit(self, record): # save only the requests coming from the library requests if record and record.getMessage() == "Outgoing request": - exclude_headers = ["Authorization"] - safe_req_headers = { - k: v - for k, v in record.req.headers.items() - if k not in exclude_headers - } + safe_req_headers = record.req.headers.copy() + + if "Authorization" in safe_req_headers: + safe_req_headers["Authorization"] = "***hidden***" if record.exc_info: trace = traceback.format_exc() @@ -35,9 +34,13 @@ def emit(self, record): "res_content_type": record.res.headers.get("Content-Type", ""), "timestamp": record.requested_at, "response_ms": int(record.res.elapsed.total_seconds() * 1000), - "req_headers": safe_req_headers, - "res_headers": record.res.headers, + "req_headers": self.format_headers(safe_req_headers), + "res_headers": self.format_headers(record.res.headers), "trace": trace, } OutgoingRequestsLog.objects.create(**kwargs) + + def format_headers(self, headers): + result = textwrap.dedent("\n".join(f"{k}: {v}" for k, v in headers.items())) + return result diff --git a/src/log_outgoing_requests/tests/test_logging.py b/src/log_outgoing_requests/tests/test_logging.py index e5dd85b200..7b3b35c622 100644 --- a/src/log_outgoing_requests/tests/test_logging.py +++ b/src/log_outgoing_requests/tests/test_logging.py @@ -17,6 +17,7 @@ def _setUpMocks(self, m): content=b"some content", ) + @override_settings(LOG_OUTGOING_REQUESTS_DB_SAVE=True) def test_outgoing_requests_are_logged(self, m): self._setUpMocks(m) @@ -31,6 +32,7 @@ def test_outgoing_requests_are_logged(self, m): @override_settings(LOG_OUTGOING_REQUESTS_DB_SAVE=True) def test_expected_data_is_saved_when_saving_enabled(self, m): methods = [ + ("GET", requests.get, m.get), ("POST", requests.post, m.post), ("PUT", requests.put, m.put), ("PATCH", requests.patch, m.patch), @@ -43,10 +45,21 @@ def test_expected_data_is_saved_when_saving_enabled(self, m): mocked( "http://example.com/some-path?version=2.0", status_code=200, - content=b"some content", + json={"test": "data"}, + request_headers={ + "Authorization": "test", + "Content-Type": "text/html", + }, + headers={ + "Date": "Tue, 21 Mar 2023 15:24:08 GMT", + "Content-Type": "application/json", + }, ) - response = func("http://example.com/some-path?version=2.0") + response = func( + "http://example.com/some-path?version=2.0", + headers={"Authorization": "test", "Content-Type": "text/html"}, + ) request_log = OutgoingRequestsLog.objects.last() @@ -58,18 +71,32 @@ def test_expected_data_is_saved_when_saving_enabled(self, m): self.assertEqual(request_log.query_params, "version=2.0") self.assertEqual(response.status_code, 200) self.assertEqual(request_log.method, method) - self.assertEqual(request_log.req_content_type, "") - self.assertEqual(request_log.res_content_type, "") + self.assertEqual(request_log.req_content_type, "text/html") + self.assertEqual(request_log.res_content_type, "application/json") self.assertEqual(request_log.response_ms, 0) self.assertEqual( request_log.req_headers, ( - "{'User-Agent': 'python-requests/2.26.0', 'Accept-Encoding': 'gzip, deflate, br', 'Accept': '*/*', 'Connection': 'keep-alive'}" - if method == "HEAD" - else "{'User-Agent': 'python-requests/2.26.0', 'Accept-Encoding': 'gzip, deflate, br', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Length': '0'}" + "User-Agent: python-requests/2.26.0\n" + "Accept-Encoding: gzip, deflate, br\n" + "Accept: */*" + "\nConnection: keep-alive\n" + "Authorization: ***hidden***\n" + "Content-Type: text/html" + if method in ["HEAD", "GET"] + else "User-Agent: python-requests/2.26.0\n" + "Accept-Encoding: gzip, deflate, br\n" + "Accept: */*\n" + "Connection: keep-alive\n" + "Authorization: ***hidden***\n" + "Content-Type: text/html\n" + "Content-Length: 0" ), ) - self.assertEqual(request_log.res_headers, "{}") + self.assertEqual( + request_log.res_headers, + "Date: Tue, 21 Mar 2023 15:24:08 GMT\nContent-Type: application/json", + ) self.assertEqual( request_log.timestamp.strftime("%Y-%m-%d %H:%M:%S"), "2021-10-18 13:00:00", @@ -77,7 +104,7 @@ def test_expected_data_is_saved_when_saving_enabled(self, m): self.assertIsNone(request_log.trace) @override_settings(LOG_OUTGOING_REQUESTS_DB_SAVE=True) - def test_authorization_header_is_not_saved(self, m): + def test_authorization_header_is_hidden(self, m): self._setUpMocks(m) requests.get( @@ -86,8 +113,9 @@ def test_authorization_header_is_not_saved(self, m): ) log = OutgoingRequestsLog.objects.get() - self.assertNotIn("Authorization", log.req_headers) + self.assertIn("Authorization: ***hidden***", log.req_headers) + @override_settings(LOG_OUTGOING_REQUESTS_DB_SAVE=False) def test_data_is_not_saved_when_saving_disabled(self, m): self._setUpMocks(m) From 1eab24ad4fef7d3ad2a4b00dd9ef5ea80b813a56 Mon Sep 17 00:00:00 2001 From: vasileios Date: Wed, 22 Mar 2023 14:09:38 +0100 Subject: [PATCH 17/45] [#1270] Fixed image in contact approval --- .../accounts/tests/test_contact_views.py | 45 ++++++++++++++++++- .../scss/components/Contacts/Contacts.scss | 3 ++ .../pages/profile/contacts/list.html | 14 +++--- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/open_inwoner/accounts/tests/test_contact_views.py b/src/open_inwoner/accounts/tests/test_contact_views.py index 84ff2db128..9edd9241d9 100644 --- a/src/open_inwoner/accounts/tests/test_contact_views.py +++ b/src/open_inwoner/accounts/tests/test_contact_views.py @@ -1,9 +1,12 @@ +import io + from django.core import mail +from django.core.files.images import ImageFile from django.urls import reverse from django.utils.translation import ugettext_lazy as _ from django_webtest import WebTest -from furl import furl +from PIL import Image from open_inwoner.accounts.models import User @@ -429,3 +432,43 @@ def test_notification_email_for_approval_is_not_sent_when_new_user(self): # Email should be the one for registration, not for approval self.assertEqual(email.subject, "Uitnodiging voor Open Inwoner Platform") + + def test_contacts_image_is_shown_in_contact_approval_section(self): + # prepare image + image = Image.new("RGB", (10, 10)) + byteIO = io.BytesIO() + image.save(byteIO, format="png") + img_bytes = byteIO.getvalue() + image = ImageFile(io.BytesIO(img_bytes), name="foo.jpg") + + # update user's type and image + existing_user = UserFactory( + email="ex@example.com", + contact_type=ContactTypeChoices.begeleider, + image=image, + ) + self.user.contact_type = ContactTypeChoices.begeleider + self.user.image = image + self.user.save() + self.user.contacts_for_approval.add(existing_user) + + # Receiver contact list page + response = self.app.get(self.list_url, user=existing_user) + avatar_class = response.pyquery(".avatar") + + self.assertIn(self.user.image.name, avatar_class[0].getchildren()[0].get("src")) + + def test_no_image_is_shown_in_contact_approval_section_when_no_image_set(self): + # update user's type + existing_user = UserFactory( + email="ex@example.com", contact_type=ContactTypeChoices.begeleider + ) + self.user.contact_type = ContactTypeChoices.begeleider + self.user.save() + self.user.contacts_for_approval.add(existing_user) + + # Receiver contact list page + response = self.app.get(self.list_url, user=existing_user) + avatar_class = response.pyquery(".avatar") + + self.assertFalse(avatar_class) diff --git a/src/open_inwoner/scss/components/Contacts/Contacts.scss b/src/open_inwoner/scss/components/Contacts/Contacts.scss index 7647e4fa59..727b4b855e 100644 --- a/src/open_inwoner/scss/components/Contacts/Contacts.scss +++ b/src/open_inwoner/scss/components/Contacts/Contacts.scss @@ -9,6 +9,9 @@ } .avatar { + display: flex; + justify-content: center; + align-items: center; padding: 15px 15px 10px; margin-top: var(--gutter-width); border: var(--border-width) solid var(--color-success-lighter); diff --git a/src/open_inwoner/templates/pages/profile/contacts/list.html b/src/open_inwoner/templates/pages/profile/contacts/list.html index 62182e1519..8892d61fc7 100644 --- a/src/open_inwoner/templates/pages/profile/contacts/list.html +++ b/src/open_inwoner/templates/pages/profile/contacts/list.html @@ -1,5 +1,5 @@ {% extends 'master.html' %} -{% load i18n button_tags link_tags icon_tags pagination_tags form_tags dropdown_tags messages_tags thumbnail %} +{% load i18n button_tags link_tags icon_tags pagination_tags form_tags dropdown_tags messages_tags thumbnail cropping %} {% block content %} @@ -51,10 +51,14 @@

{% trans "U bent toegevoegd als contactpersoon" %}

{% endif %} - {% if approvals_count == 1 and request.user.image %} -
- {{ image.default_alt_text }} -
+ {% if approvals_count == 1 %} + {% with pending_approvals.get as pending_approval %} + {% if pending_approval.image %} +
+ +
+ {% endif %} + {% endwith %} {% endif %} {% endwith %} From 9c64c4c798e2cd018c1201dfcc8f97f4ef939f95 Mon Sep 17 00:00:00 2001 From: vasileios Date: Thu, 23 Mar 2023 09:58:06 +0100 Subject: [PATCH 18/45] [#1231] Fixed breadcrumbs in plans --- .../accounts/tests/test_action_views.py | 6 ++++ .../templates/components/Action/Actions.html | 7 ++++- .../components/templatetags/action_tags.py | 18 +++++++++++ src/open_inwoner/plans/tests/test_views.py | 11 +++++++ src/open_inwoner/plans/urls.py | 6 ++++ src/open_inwoner/plans/views.py | 31 +++++++++++++++++-- 6 files changed, 75 insertions(+), 4 deletions(-) diff --git a/src/open_inwoner/accounts/tests/test_action_views.py b/src/open_inwoner/accounts/tests/test_action_views.py index 4b8b7c4f41..ad090df9b9 100644 --- a/src/open_inwoner/accounts/tests/test_action_views.py +++ b/src/open_inwoner/accounts/tests/test_action_views.py @@ -227,6 +227,12 @@ def test_action_history(self): self.assertEqual(response.status_code, 200) self.assertContains(response, self.action.name) + def test_action_history_breadcrumbs(self): + response = self.app.get(self.history_url, user=self.user) + crumbs = response.pyquery(".breadcrumbs__list-item") + self.assertIn(_("Mijn profiel"), crumbs[1].text_content()) + self.assertIn(_("Mijn acties"), crumbs[2].text_content()) + def test_action_history_not_your_action(self): other_user = UserFactory() self.app.get(self.history_url, user=other_user, status=404) diff --git a/src/open_inwoner/components/templates/components/Action/Actions.html b/src/open_inwoner/components/templates/components/Action/Actions.html index d3489fa4b7..0a18055937 100644 --- a/src/open_inwoner/components/templates/components/Action/Actions.html +++ b/src/open_inwoner/components/templates/components/Action/Actions.html @@ -26,7 +26,12 @@ {% button icon="edit" text=_("Bewerken") href=action_url icon_outlined=True transparent=True %}