Skip to content

Commit d0a4b2b

Browse files
committed
Management command for generating LGA conference meeting invites
1 parent a916ce9 commit d0a4b2b

File tree

1 file changed

+120
-0
lines changed

1 file changed

+120
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
from html import escape
2+
from os.path import join
3+
4+
from django.conf import settings
5+
from django.core.management.base import BaseCommand
6+
from django.template import Template, Context
7+
8+
from tqdm import tqdm
9+
10+
from caps.models import Council
11+
12+
file_header = """
13+
<!doctype html>
14+
<html>
15+
<head>
16+
<meta charset="utf-8">
17+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet">
18+
</head>
19+
<body>
20+
<div class="p-3 p-lg-4 bg-light border-bottom position-sticky" style="top: 0;">
21+
<label for="q" class="form-label">Filter councils by name</label>
22+
<input type="search" class="form-control" id="q">
23+
</div>
24+
<div class="p-3 p-lg-4">
25+
"""
26+
27+
file_footer = """
28+
</div>
29+
<script>
30+
document.querySelector('#q').addEventListener('keyup', function(e){
31+
var q = this.value.toLowerCase();
32+
if ( q == '' ) {
33+
document.querySelectorAll('section.d-none').forEach(function(el){
34+
el.classList.remove('d-none');
35+
});
36+
} else {
37+
document.querySelectorAll('section').forEach(function(el){
38+
var councilName = el.querySelector('h1').textContent.toLowerCase();
39+
var shouldBeHidden = (councilName.indexOf(q) == -1);
40+
el.classList.toggle('d-none', shouldBeHidden);
41+
});
42+
}
43+
});
44+
document.querySelectorAll('section .form-control').forEach(function(el){
45+
el.addEventListener('click', function(e){
46+
this.select();
47+
});
48+
});
49+
</script>
50+
</body>
51+
</html>
52+
"""
53+
54+
email_template = Template(
55+
"""
56+
<p>Hi NAME,</p>
57+
<p>You probably already know that the council most similar to yours in terms of emissions, deprivation and rural/urban factor is {{ twin.name }}.</p>
58+
{% with n=twin.plan_overlap.just_in_b %}<p>But do you know which {% if n|length > 1 %}{{ n|length }} {% endif %}{{ n|pluralize:"topic appears,topics appear" }} in their climate plans and {{ n|pluralize:"doesn’t appear,don’t appear" }} in yours?</p>{% endwith %}
59+
<p>…We do! I’m the climate lead at <a href="https://www.mysociety.org"><u>mySociety</u></a>, a charity building free digital tools that enable local authorities to reach their net zero goals. Our topic-based council comparisons are just the latest feature we’ve added, and I’d love to get your feedback on it.</p>
60+
<p><a href="https://calendly.com/zarino-mysociety/lga-conference"><u>Book a 15 minute chat with me at the conference next week</u></a>, or stop by stand T10A, and I’ll give you a demo. Hopefully you’ll also come away with some useful insights to share with your own teams at {{ council.name }}.</p>
61+
<p>Thank you so much, I really appreciate your time.</p>
62+
<p><strong>Zarino Zappia</strong><br>Climate Programme Lead, mySociety<br>mysociety.org</p>
63+
<p>PS. This winter we’ll also be working with a few pilot councils on a <a href="https://www.mysociety.org/tag/neighbourhood-warmth/"><u>community-led approach to domestic retrofit</u></a>. I know incentivising domestic decarbonisation is a big challenge for local authorities – happy to share more when we meet!</p>"""
64+
)
65+
66+
67+
class Command(BaseCommand):
68+
help = "generates HTML for one email per local authority, ready to copy-paste into the LGA conference message editor"
69+
70+
def handle(self, *args, **options):
71+
html_filepath = join(settings.MEDIA_ROOT, "data", "lga_conf_emails.html")
72+
with open(html_filepath, "w") as f:
73+
f.writelines(file_header)
74+
75+
for council in tqdm(Council.current_councils()):
76+
tqdm.write(f"Generating email for {council.name}")
77+
78+
context = {"council": council}
79+
80+
related_councils = council.get_related_councils()
81+
related_councils_intersection = (
82+
council.related_council_keyphrase_intersection()
83+
)
84+
for group in related_councils:
85+
for c in group["councils"]:
86+
c.plan_overlap = related_councils_intersection[c]
87+
if group["type"].slug == "composite":
88+
context["twin"] = group["councils"][0]
89+
90+
email_html = email_template.render(Context(context))
91+
escaped_email_text = escape(email_html)
92+
93+
if (
94+
"twin" in context
95+
and len(context["twin"].plan_overlap.just_in_b) > 0
96+
):
97+
f.writelines("<section class='mb-5'>")
98+
f.writelines(f"<h1>{council.name}</h1>\n")
99+
else:
100+
f.writelines("<section class='mb-5 text-danger'>")
101+
f.writelines(f"<h1>{council.name} (no twins)</h1>\n")
102+
103+
f.writelines(
104+
"<input class='form-control mt-3' value='What inspiration could you take from your council’s climate twin?'>\n"
105+
)
106+
107+
if "twin" in context:
108+
f.writelines(
109+
f"<textarea class='form-control mt-3 font-monospace' rows='10'>{escaped_email_text}</textarea>\n"
110+
)
111+
else:
112+
f.writelines(
113+
"<div class='alert alert-danger'>No twin for this council</div>"
114+
)
115+
116+
f.writelines("</section>\n")
117+
118+
f.writelines(file_footer)
119+
120+
print(f"output written to {html_filepath}")

0 commit comments

Comments
 (0)