From 6994db438bd0977f1f9051627b8b0255af29629e Mon Sep 17 00:00:00 2001 From: Pavel 'LEdoian' Turinsky Date: Thu, 5 Jan 2023 04:40:16 +0100 Subject: [PATCH 1/7] =?UTF-8?q?P=C5=99id=C3=A1n=C3=AD=20tagu=20{%=20mailli?= =?UTF-8?q?nk=20%}?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vyrábí odkazy, které vedou na poslání mailu. Psal jsem to spíš po paměti, nejsem si jistý, že to takhle je přesně podle příslušného RFC, ale jako PoC dobrý a když to nebude fungovat, tak se implementace opraví. Všimněte si, že to je otestované, takže když někdo opraví testy (=předpis chování), tak je pak snadné z diffu a všeho odvodit úpravu. V Django dokumentaci se píše něco o tom, že by se měl použít spíš `format_html` a `conditional_escape`, ale zatím jsem to víc nezkoumal. Je žádoucí z tagu {% maillink %} odddělit i tag {% mailurl %}, který by vracel samotnou URL. Obojí dává smysl umět (speciálně bastlení odkazů z URL je stejně strašně nepřehledné, takže je lepší to zavřít do {% maillink %} a nikdy nevidět), ale zatím to oddělené není… (Ale jsou na to testy, takže by se mělo aspoň dát poznat, že rozdělení nerozbije chování.) --- various/templatetags/__init__.py | 0 various/templatetags/mail.py | 30 ++++++++++++++++++++++ various/tests.py | 43 +++++++++++++++++++++++++++++++- 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 various/templatetags/__init__.py create mode 100644 various/templatetags/mail.py diff --git a/various/templatetags/__init__.py b/various/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/various/templatetags/mail.py b/various/templatetags/mail.py new file mode 100644 index 00000000..fe11d218 --- /dev/null +++ b/various/templatetags/mail.py @@ -0,0 +1,30 @@ +from django import template +from django.utils.safestring import mark_safe +from urllib.request import quote as urlencode +register = template.Library() + +@register.simple_tag +def maillink(text: str, subject=None, body=None, to=[], attrs=None): + """TODO: Dokumentace""" + if isinstance(to, str): + to = [to] + assert isinstance(to, list) + parts = [ + f'mailto:{str.join(",", to)}', + ] + if len(to) < 1: + raise ValueError('Cannot mail to empty set of people') + + if subject: + parts.append(f'subject={urlencode(subject)}') + if body: + parts.append(f'body={urlencode(body)}') + + if len(parts) > 1: + url = parts[0] + '?' + str.join('&', parts[1:]) + else: + url = parts[0] + if not attrs: attrs = '' + mezera = ' '*bool(attrs) + full_link = f'{text}' + return mark_safe(full_link) diff --git a/various/tests.py b/various/tests.py index 7ce503c2..7884e618 100644 --- a/various/tests.py +++ b/various/tests.py @@ -1,3 +1,44 @@ from django.test import TestCase +# TODO: Možná vyrobit separátní soubory v tests/… než mít všechny testy v jednom souboru? +from various.templatetags.mail import maillink -# Create your tests here. +class MailTagsTest(TestCase): + """Testuje template tagy ohledně mailů.""" + def test_maillink(self): + # Tohle nedává smysl dělit do víc funkcí, bylo by v nich víc boilerplatu než užitečného kódu. + self.assertEquals(maillink('Hello', to='some@body.test'), r'Hello') + self.assertEquals(maillink('Hello', to=['some@body.test']), r'Hello') + self.assertEquals( + maillink('Hello', to=['alice@test.test', 'bob@jinde.test']), + r'Hello', + ) + self.assertEquals( + maillink('Hello', to='some@body.test', attrs='class="trida" id="id"'), + r'Hello', + ) + # Následující test toho testuje moc zároveň, měly by předcházet dedikované testy… (kašlu na ně :-P) + self.assertEquals( + maillink('Text odkazu', to='prijemce@wtf.test', subject="Předmět", body="Čau"), + r'Text odkazu', + ) + self.assertRaises(ValueError, lambda: maillink('Nemám příjemce')) + self.assertRaises(TypeError, lambda: maillink()) # Nemá text, takže to shodí python + + def test_render_in_template(self): + # Pomocná funkce: vykreslí template do stringu + # Ref: https://stackoverflow.com/a/1690879 + def render_template(template, context=None): + from django.template import Template, Context + context = context or {} + context = Context(context) + return Template(template).render(context) + + template=( + r'{% load mail %}' + # TODO: Vyzkoušet i víc adresátů. (Nepamatuji si z hlavy syntaxi…) + r'{% maillink "Text" to="alice@test.test" subject="Oprava řešení" %}' + ) + self.assertEquals( + render_template(template), + r'Text', + ) -- 2.39.5 From ff996c2924ee8d0612a5326f4c0b03763763cf97 Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Sun, 8 Jan 2023 08:51:01 +0100 Subject: [PATCH 2/7] =?UTF-8?q?P=C5=99ejmenov=C3=A1n=C3=AD=20hodnocen?= =?UTF-8?q?=C3=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Označení bylo zavádějící, protože se vůbec nejedná o objekt Hodnocení. Neříkám, že nové jméno je nějak úchvatné, ale aspoň mě nemate a na proměnnou s životností dva řádky je to stejně jedno… --- odevzdavatko/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/odevzdavatko/views.py b/odevzdavatko/views.py index 0100ef24..e983860a 100644 --- a/odevzdavatko/views.py +++ b/odevzdavatko/views.py @@ -235,8 +235,8 @@ class DetailReseniView(DetailView): def get_context_data(self, **kw): self.check_access() ctx = super().get_context_data(**kw) - hodnoceni = self.aktualni_hodnoceni() - ctx["hodnoceni"] = hodnoceni + detaily_hodnoceni = self.aktualni_hodnoceni() + ctx["hodnoceni"] = detaily_hodnoceni return ctx def get(self, request, *args, **kwargs): -- 2.39.5 From efe1b4bb5a47de3d749f1bdfc7cedac9f2150a66 Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Sun, 8 Jan 2023 08:52:01 +0100 Subject: [PATCH 3/7] =?UTF-8?q?Pou=C5=BEit=C3=AD=20{%maillink%}=20v=20deta?= =?UTF-8?q?ilu=20=C5=99e=C5=A1en=C3=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ten řádek má sice pořád 117 znaků, ale IMHO je to o dost lepší. Mně to i správně vyplňuje subjecty v Thunderbirdu, takže můj kód asi není úplně mimo :-) --- odevzdavatko/templates/odevzdavatko/detail.html | 6 +++++- odevzdavatko/views.py | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/odevzdavatko/templates/odevzdavatko/detail.html b/odevzdavatko/templates/odevzdavatko/detail.html index 06f69609..379bdc68 100644 --- a/odevzdavatko/templates/odevzdavatko/detail.html +++ b/odevzdavatko/templates/odevzdavatko/detail.html @@ -1,6 +1,7 @@ {% extends "base.html" %} {% load static %} {% load deadliny %} +{% load mail %} {% block content %} @@ -14,7 +15,10 @@ {% if edit %}

Řešitelé: - {% for r in object.resitele.all %}{{ r }} ({{ r.osoba.email }}){% if forloop.revcounter0 != 0 %}, {% endif %}{% endfor %} + {% for r in object.resitele.all %} + {{ r }} + ({% maillink r.osoba.email to=r.osoba.email subject=mailsubject %}){% if forloop.revcounter0 != 0 %}, {% endif %} + {% endfor %}

{% else %}

Řešitelé: {{ object.resitele.all | join:", " }}

diff --git a/odevzdavatko/views.py b/odevzdavatko/views.py index e983860a..3100eb9c 100644 --- a/odevzdavatko/views.py +++ b/odevzdavatko/views.py @@ -237,6 +237,9 @@ class DetailReseniView(DetailView): ctx = super().get_context_data(**kw) detaily_hodnoceni = self.aktualni_hodnoceni() ctx["hodnoceni"] = detaily_hodnoceni + + # Subject případného mailu (template neumí použitelně spojovat řetězce: https://stackoverflow.com/q/4386168) + ctx["mailsubject"] = "Oprava řešení M&M "+self.reseni.problem.first().hlavni_problem.nazev return ctx def get(self, request, *args, **kwargs): -- 2.39.5 From 0956b0780aa56a028c8bc6a3b295436aab28ddda Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Mon, 6 Feb 2023 20:13:29 +0100 Subject: [PATCH 4/7] =?UTF-8?q?Odd=C4=9Blen=C3=AD=20tagu=20{%mailurl%}?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- various/templatetags/mail.py | 7 ++++++- various/tests.py | 20 ++++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/various/templatetags/mail.py b/various/templatetags/mail.py index fe11d218..972040f6 100644 --- a/various/templatetags/mail.py +++ b/various/templatetags/mail.py @@ -4,7 +4,7 @@ from urllib.request import quote as urlencode register = template.Library() @register.simple_tag -def maillink(text: str, subject=None, body=None, to=[], attrs=None): +def mailurl(*, subject=None, body=None, to=[]): """TODO: Dokumentace""" if isinstance(to, str): to = [to] @@ -24,6 +24,11 @@ def maillink(text: str, subject=None, body=None, to=[], attrs=None): url = parts[0] + '?' + str.join('&', parts[1:]) else: url = parts[0] + return url + +@register.simple_tag +def maillink(text, subject=None, body=None, to=[], attrs=None): + url = mailurl(subject=subject, body=body, to=to) if not attrs: attrs = '' mezera = ' '*bool(attrs) full_link = f'{text}' diff --git a/various/tests.py b/various/tests.py index 7884e618..0abf4e26 100644 --- a/various/tests.py +++ b/various/tests.py @@ -1,6 +1,6 @@ from django.test import TestCase # TODO: Možná vyrobit separátní soubory v tests/… než mít všechny testy v jednom souboru? -from various.templatetags.mail import maillink +from various.templatetags.mail import maillink, mailurl class MailTagsTest(TestCase): """Testuje template tagy ohledně mailů.""" @@ -24,6 +24,16 @@ class MailTagsTest(TestCase): self.assertRaises(ValueError, lambda: maillink('Nemám příjemce')) self.assertRaises(TypeError, lambda: maillink()) # Nemá text, takže to shodí python + def test_mailurl(self): + self.assertEquals(mailurl(to='some@body.test'), r'mailto:some@body.test') + self.assertEquals(mailurl(to=['some@body.test']), r'mailto:some@body.test') + self.assertEquals(mailurl(to=['alice@test.test', 'bob@jinde.test']), r'mailto:alice@test.test,bob@jinde.test') + self.assertEquals( + mailurl(to='some@body.test', body='Tělo', subject='Předmět'), + r'mailto:some@body.test?subject=P%C5%99edm%C4%9Bt&body=T%C4%9Blo', + ) + self.assertRaises(ValueError, lambda: mailurl()) + def test_render_in_template(self): # Pomocná funkce: vykreslí template do stringu # Ref: https://stackoverflow.com/a/1690879 @@ -33,7 +43,7 @@ class MailTagsTest(TestCase): context = Context(context) return Template(template).render(context) - template=( + template = ( r'{% load mail %}' # TODO: Vyzkoušet i víc adresátů. (Nepamatuji si z hlavy syntaxi…) r'{% maillink "Text" to="alice@test.test" subject="Oprava řešení" %}' @@ -42,3 +52,9 @@ class MailTagsTest(TestCase): render_template(template), r'Text', ) + + mailurltemplate = ( + r'{% load mail %}' + r'{% mailurl to="alice@test.test" subject="Čau Alice" %}' + ) + self.assertEquals(render_template(mailurltemplate), r'mailto:alice@test.test?subject=%C4%8Cau%20Alice') -- 2.39.5 From 65cd15ecbbc972e3b6d5642c1c3404396fe6617c Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Mon, 6 Feb 2023 20:27:19 +0100 Subject: [PATCH 5/7] =?UTF-8?q?Koment=C3=A1=C5=99=20k=20tomu,=20kde=20se?= =?UTF-8?q?=20vyr=C3=A1b=C3=AD=20mailsubject?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- odevzdavatko/templates/odevzdavatko/detail.html | 1 + 1 file changed, 1 insertion(+) diff --git a/odevzdavatko/templates/odevzdavatko/detail.html b/odevzdavatko/templates/odevzdavatko/detail.html index 379bdc68..74352509 100644 --- a/odevzdavatko/templates/odevzdavatko/detail.html +++ b/odevzdavatko/templates/odevzdavatko/detail.html @@ -17,6 +17,7 @@

Řešitelé: {% for r in object.resitele.all %} {{ r }} + {# DjangoTemplates neumí spojovat řetězce (https://stackoverflow.com/q/4386168), tak si necháváme vyrobit subject mailu ve view. #} ({% maillink r.osoba.email to=r.osoba.email subject=mailsubject %}){% if forloop.revcounter0 != 0 %}, {% endif %} {% endfor %}

-- 2.39.5 From f6cb669277d2143338b67f64475e804a85f9378b Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Mon, 6 Feb 2023 20:27:35 +0100 Subject: [PATCH 6/7] =?UTF-8?q?P=C5=99ejmenov=C3=A1n=C3=AD=20mailsubjectu?= =?UTF-8?q?=20do=20=C4=8De=C5=A1tiny?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- odevzdavatko/templates/odevzdavatko/detail.html | 2 +- odevzdavatko/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/odevzdavatko/templates/odevzdavatko/detail.html b/odevzdavatko/templates/odevzdavatko/detail.html index 74352509..468b0322 100644 --- a/odevzdavatko/templates/odevzdavatko/detail.html +++ b/odevzdavatko/templates/odevzdavatko/detail.html @@ -18,7 +18,7 @@ {% for r in object.resitele.all %} {{ r }} {# DjangoTemplates neumí spojovat řetězce (https://stackoverflow.com/q/4386168), tak si necháváme vyrobit subject mailu ve view. #} - ({% maillink r.osoba.email to=r.osoba.email subject=mailsubject %}){% if forloop.revcounter0 != 0 %}, {% endif %} + ({% maillink r.osoba.email to=r.osoba.email subject=predmetmailu %}){% if forloop.revcounter0 != 0 %}, {% endif %} {% endfor %}

{% else %} diff --git a/odevzdavatko/views.py b/odevzdavatko/views.py index 3100eb9c..57822bf4 100644 --- a/odevzdavatko/views.py +++ b/odevzdavatko/views.py @@ -239,7 +239,7 @@ class DetailReseniView(DetailView): ctx["hodnoceni"] = detaily_hodnoceni # Subject případného mailu (template neumí použitelně spojovat řetězce: https://stackoverflow.com/q/4386168) - ctx["mailsubject"] = "Oprava řešení M&M "+self.reseni.problem.first().hlavni_problem.nazev + ctx["predmetmailu"] = "Oprava řešení M&M "+self.reseni.problem.first().hlavni_problem.nazev return ctx def get(self, request, *args, **kwargs): -- 2.39.5 From 04c3c6257cfee4dd9723a47db425d4a3ef237369 Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Mon, 6 Feb 2023 21:56:06 +0100 Subject: [PATCH 7/7] =?UTF-8?q?Podpora=20cc=20a=20bcc=20v=20{%maillink%}?= =?UTF-8?q?=20[neotestov=C3=A1no]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../templates/odevzdavatko/detail.html | 3 +++ odevzdavatko/views.py | 1 + various/templatetags/mail.py | 23 +++++++++++++++---- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/odevzdavatko/templates/odevzdavatko/detail.html b/odevzdavatko/templates/odevzdavatko/detail.html index 468b0322..73265563 100644 --- a/odevzdavatko/templates/odevzdavatko/detail.html +++ b/odevzdavatko/templates/odevzdavatko/detail.html @@ -21,6 +21,9 @@ ({% maillink r.osoba.email to=r.osoba.email subject=predmetmailu %}){% if forloop.revcounter0 != 0 %}, {% endif %} {% endfor %}

+

+ {% maillink "Poslat mail všem řešitelům" bcc=maily_vsech_resitelu subject=predmetmailu %} +

{% else %}

Řešitelé: {{ object.resitele.all | join:", " }}

{% endif %} diff --git a/odevzdavatko/views.py b/odevzdavatko/views.py index 57822bf4..9ac1ac29 100644 --- a/odevzdavatko/views.py +++ b/odevzdavatko/views.py @@ -240,6 +240,7 @@ class DetailReseniView(DetailView): # Subject případného mailu (template neumí použitelně spojovat řetězce: https://stackoverflow.com/q/4386168) ctx["predmetmailu"] = "Oprava řešení M&M "+self.reseni.problem.first().hlavni_problem.nazev + ctx["maily_vsech_resitelu"] = [y for x in self.reseni.resitele.all().values_list('osoba__email') for y in x] return ctx def get(self, request, *args, **kwargs): diff --git a/various/templatetags/mail.py b/various/templatetags/mail.py index 972040f6..ecbb2a39 100644 --- a/various/templatetags/mail.py +++ b/various/templatetags/mail.py @@ -4,21 +4,34 @@ from urllib.request import quote as urlencode register = template.Library() @register.simple_tag -def mailurl(*, subject=None, body=None, to=[]): - """TODO: Dokumentace""" +def mailurl(*, subject=None, body=None, to=[], cc=[], bcc=[]): + """Tag na vytváření správně zakódované mailto: adresy + + Ref: RFC 6068, """ if isinstance(to, str): to = [to] + if isinstance(cc, str): + cc = [cc] + if isinstance(bcc, str): + bcc = [bcc] assert isinstance(to, list) + assert isinstance(cc, list) + assert isinstance(bcc, list) + # FIXME: adresa není správně zakódovaná, rozbije se to na adresách s divnými znaky parts = [ f'mailto:{str.join(",", to)}', ] - if len(to) < 1: + if len(to) + len(cc) + len(bcc) < 1: raise ValueError('Cannot mail to empty set of people') if subject: parts.append(f'subject={urlencode(subject)}') if body: parts.append(f'body={urlencode(body)}') + if len(cc) > 0: + parts.append(f'cc={str.join(",", cc)}') + if len(bcc) > 0: + parts.append(f'bcc={str.join(",", bcc)}') if len(parts) > 1: url = parts[0] + '?' + str.join('&', parts[1:]) @@ -27,8 +40,8 @@ def mailurl(*, subject=None, body=None, to=[]): return url @register.simple_tag -def maillink(text, subject=None, body=None, to=[], attrs=None): - url = mailurl(subject=subject, body=body, to=to) +def maillink(text, subject=None, body=None, to=[], cc=[], bcc=[], attrs=None): + url = mailurl(subject=subject, body=body, to=to, cc=cc, bcc=bcc) if not attrs: attrs = '' mezera = ' '*bool(attrs) full_link = f'{text}' -- 2.39.5