diff --git a/events/management/commands/send_email_updates.py b/events/management/commands/send_email_updates.py
index f04178a3..88b2a0cb 100644
--- a/events/management/commands/send_email_updates.py
+++ b/events/management/commands/send_email_updates.py
@@ -38,7 +38,7 @@
utm = "utm_campaign=govtrack_email_update&utm_source=govtrack/email_update&utm_medium=email"
template_body_text = None
template_body_html = None
-announce = None
+latest_blog_post = None
class Command(BaseCommand):
help = 'Sends out email updates of events to subscribing users.'
@@ -49,7 +49,7 @@ def add_arguments(self, parser):
def handle(self, *args, **options):
global template_body_text
global template_body_html
- global announce
+ global latest_blog_post
if options["mode"][0] not in ('daily', 'weekly', 'testadmin', 'testcount'):
print("Specify daily or weekly or testadmin or testcount.")
@@ -109,7 +109,7 @@ def handle(self, *args, **options):
# load globals
template_body_text = get_template("events/emailupdate_body.txt")
template_body_html = get_template("events/emailupdate_body.html")
- announce = load_announcement("website/email/email_update_announcement.md", options["mode"][0] == "testadmin")
+ latest_blog_post = load_latest_blog_post()
# counters for analytics on what we sent
counts = {
@@ -235,10 +235,12 @@ def pool_worker(conn):
def send_email_update(user_id, list_email_freq, send_mail, mark_lists, send_old_events, mail_connection):
global launch_time
+ global latest_blog_post
user_start_time = datetime.now()
user = User.objects.get(id=user_id)
+ profile = UserProfile.objects.get(user=user)
# get the email's From: header and return path
emailfromaddr = getattr(settings, 'EMAIL_UPDATES_FROMADDR',
@@ -272,10 +274,15 @@ def send_email_update(user_id, list_email_freq, send_mail, mark_lists, send_old_
if most_recent_event is None: most_recent_event = max_id
most_recent_event = max(most_recent_event, max_id)
+ # Suppress the latest blog post if the user was already sent it.
+ if latest_blog_post and profile.last_blog_post_emailed >= latest_blog_post.id:
+ latest_blog_post = None
+
user_querying_end_time = datetime.now()
- # Don't send an empty email.... unless we're testing and we want to send some old events.
- if len(eventslists) == 0 and not send_old_events and announce is None:
+ # Don't send an empty email (no events and no latest blog post)
+ # .... unless we're testing and we want to send some old events.
+ if len(eventslists) == 0 and not send_old_events and latest_blog_post is None:
return {
"total_time_querying": user_querying_end_time-user_start_time,
}
@@ -321,7 +328,7 @@ def send_email_update(user_id, list_email_freq, send_mail, mark_lists, send_old_
"emailpingurl": emailpingurl,
"body_text": body_text,
"body_html": body_html,
- "announcement": announce,
+ "latest_blog_post": latest_blog_post,
"SITE_ROOT_URL": settings.SITE_ROOT_URL,
"utm": utm,
},
@@ -329,7 +336,7 @@ def send_email_update(user_id, list_email_freq, send_mail, mark_lists, send_old_
'Reply-To': emailfromaddr,
'Auto-Submitted': 'auto-generated',
'X-Auto-Response-Suppress': 'OOF',
- 'X-Unsubscribe-Link': UserProfile.objects.get(user=user).get_one_click_unsub_url(),
+ 'X-Unsubscribe-Link': profile.get_one_click_unsub_url(),
},
fail_silently=False,
connection=mail_connection,
@@ -358,6 +365,9 @@ def send_email_update(user_id, list_email_freq, send_mail, mark_lists, send_old_
sublist.last_event_mailed = max(sublist.last_event_mailed, most_recent_event) if sublist.last_event_mailed is not None else most_recent_event
sublist.last_email_sent = launch_time
sublist.save()
+ if latest_blog_post:
+ profile.last_blog_post_emailed = latest_blog_post.id
+ profile.save()
user_sending_end_time = datetime.now()
@@ -369,46 +379,19 @@ def send_email_update(user_id, list_email_freq, send_mail, mark_lists, send_old_
"total_time_sending": user_sending_end_time-user_rendering_end_time,
}
-def load_announcement(template_path, testing):
- # Load the Markdown template for the current blast.
- templ = get_template(template_path)
-
- # Get the text-only body content, which also includes some email metadata.
- # Replace Markdown-style [text][href] links with the text plus bracketed href.
- ctx = { "format": "text", "utm": "" }
- body_text = templ.render(ctx).strip()
- body_text = re.sub(r"\[(.*?)\]\((.*?)\)", r"\1 at \2", body_text)
-
- # The top of the text content contains metadata in YAML format,
- # with "id" and "subject" required and active: true or rundate set to today's date in ISO format.
- meta_info, body_text = body_text.split("----------", 1)
- body_text = body_text.strip()
- meta_info = yaml.load(meta_info)
-
- # Under what cases do we use this announcement?
- if meta_info.get("active"):
- pass # active is set to something truthy
- elif meta_info.get("rundate") == launch_time.date().isoformat():
- pass # rundate matches date this job was started
- elif "rundate" in meta_info and testing:
- pass # when testing ignore the actual date set
- else:
- # the announcement file is inactive/stale
+def load_latest_blog_post():
+ from website.models import BlogPost
+ latest_blog_post = BlogPost.objects\
+ .filter(published=True)\
+ .order_by('-created')\
+ .first()
+ if not latest_blog_post:
return None
- # Get the HTML body content.
- ctx = {
- "format": "html",
- "utm": "",
- }
- body_html = templ.render(ctx).strip()
- body_html = markdown(body_html)
+ # Pre-render the HTML and plain text versions.
+ latest_blog_post.body_html = latest_blog_post.body_html()
+ latest_blog_post.body_text = latest_blog_post.body_text()
- # Store everything in meta_info.
-
- meta_info["body_text"] = body_text
- meta_info["body_html"] = body_html
-
- return meta_info
+ return latest_blog_post
diff --git a/requirements.txt b/requirements.txt
index f179872b..047f5112 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -44,3 +44,4 @@ cmarkgfm<0.5.0
django_otp
Mastodon.py
requests
+django-markdownx
diff --git a/settings.py b/settings.py
index a541d451..108ec328 100644
--- a/settings.py
+++ b/settings.py
@@ -99,6 +99,8 @@
'crispy_forms',
'django_otp',
'django_otp.plugins.otp_totp',
+ #'django_otp.plugins.otp_static', # necessary for bootstrapping access to the admin
+ 'markdownx',
'haystack',
'htmlemailer',
@@ -208,3 +210,5 @@
SHOW_TOOLBAR_CALLBACK = lambda : True
OTP_TOTP_ISSUER = "GovTrack.us"
+
+MARKDOWNX_MARKDOWNIFY_FUNCTION = 'website.templatetags.govtrack_utils.markdown'
diff --git a/static/markdownx b/static/markdownx
new file mode 120000
index 00000000..5ef984e1
--- /dev/null
+++ b/static/markdownx
@@ -0,0 +1 @@
+../.venv/lib/python3.6/site-packages/markdownx/static/markdownx/
\ No newline at end of file
diff --git a/templates/events/emailupdate.html b/templates/events/emailupdate.html
index be1aecb8..d7b658d7 100644
--- a/templates/events/emailupdate.html
+++ b/templates/events/emailupdate.html
@@ -92,16 +92,6 @@
-
- {% if announcement %}
-
-
- {% if announcement.subject %} {{announcement.subject}}{% endif %}
- {{announcement.body_html|safe}}
-
-
- {% endif %}
-
Like these updates?
A recurring tip or one-time tip will help us keep this service free for everyone.
@@ -109,6 +99,14 @@
for more updates.
+ {% if latest_blog_post %}
+
+
+ {{latest_blog_post.title}}
+ {{latest_blog_post.body_html|safe}}
+
+
+ {% endif %}
{{body_html|safe}}
diff --git a/templates/events/emailupdate.txt b/templates/events/emailupdate.txt
index e7317e24..5ba71308 100644
--- a/templates/events/emailupdate.txt
+++ b/templates/events/emailupdate.txt
@@ -1,9 +1,16 @@
{% autoescape off %}GovTrack Email Update
=====================
-{% if announcement %}----- {{announcement.body_text}} -----
+This is your email update from www.GovTrack.us. To change your email update settings, including to unsubscribe, go to {{SITE_ROOT_URL}}/accounts/profile.
-{% endif %}This is your email update from www.GovTrack.us. To change your email update settings, including to unsubscribe, go to {{SITE_ROOT_URL}}/accounts/profile.
+{% if latest_blog_post %}=====================================================================
+
+{{latest_blog_post.title}}
+
+{{last_blog_post_emailed.body_text}}
+
+{% endif %}
+=====================================================================
{{body_text}}
diff --git a/templates/events/emailupdate_subject.txt b/templates/events/emailupdate_subject.txt
index b71538fb..de51b321 100644
--- a/templates/events/emailupdate_subject.txt
+++ b/templates/events/emailupdate_subject.txt
@@ -1 +1 @@
-GovTrack Update for {{date}}{% if announcement.subject %} | {{announcement.subject}}{% endif %}
+Activity in Congress{% if latest_blog_post %}: {{latest_blog_post.title}}{% endif %} ({{date}})
diff --git a/templates/website/index.html b/templates/website/index.html
index a18bd782..dcb7b256 100644
--- a/templates/website/index.html
+++ b/templates/website/index.html
@@ -210,19 +210,20 @@ Find legislation that affects you:
-
+
+ {% if latest_blog_post %}
-
+
- Middle school social studies textbooks and Schoolhouse Rock songs paint a stereotypical portrait of how congressional legislation gets passed. But in real life, the story of how federal laws actually get enacted usually proves far more complex. To examine how, GovTrack Insider browsed through all the laws enacted by Congress in 2019–20, looking for one that seemed both particularly interesting and undercovered by national media.
- Read
- the story of The Emancipation National Historic Trail Study Act »
+ {{latest_blog_post.body_html|safe}}
+ — {{latest_blog_post.created|date:"SHORT_DATETIME_FORMAT"}}
+ {% endif %}
-
+
{% for post_group in post_groups %}
diff --git a/urls.py b/urls.py
index 5069c81f..4cc9fab7 100644
--- a/urls.py
+++ b/urls.py
@@ -22,8 +22,9 @@
urlpatterns += [
url(r'^admin/', admin.site.urls),
+ url('markdownx/', include('markdownx.urls')),
- # main URLs
+ # main URLs
url(r'', include('redirect.urls')),
url(r'', include('website.urls')),
url(r'^congress/members(?:$|/)', include('person.urls')),
@@ -44,7 +45,7 @@
url(r'^accounts/logout$', auth_views.LogoutView.as_view(), { "redirect_field_name": "next" }),
url(r'^accounts/profile$', registration.views.profile, name='registration.views.profile'),
- url(r'^dump_request', website.views.dumprequest),
+ url(r'^dump_request', website.views.dumprequest),
]
# sitemaps
diff --git a/website/admin.py b/website/admin.py
index 56873fe3..100564c7 100644
--- a/website/admin.py
+++ b/website/admin.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8
from django.contrib import admin
+from markdownx.admin import MarkdownxModelAdmin
from website.models import *
class UserProfileAdmin(admin.ModelAdmin):
@@ -11,9 +12,12 @@ class UserProfileAdmin(admin.ModelAdmin):
class MediumPostAdmin(admin.ModelAdmin):
list_display = ['published', 'title', 'url']
+class BlogPostAdmin(MarkdownxModelAdmin):
+ list_display = ['title', 'published', 'created', 'updated']
+
admin.site.register(UserProfile, UserProfileAdmin)
admin.site.register(MediumPost, MediumPostAdmin)
admin.site.register(Community)
admin.site.register(CommunityMessageBoard)
admin.site.register(CommunityMessage)
-
+admin.site.register(BlogPost, BlogPostAdmin)
\ No newline at end of file
diff --git a/website/migrations/0008_blogpost.py b/website/migrations/0008_blogpost.py
new file mode 100644
index 00000000..d6fcb4a8
--- /dev/null
+++ b/website/migrations/0008_blogpost.py
@@ -0,0 +1,34 @@
+# Generated by Django 2.2.28 on 2024-01-12 16:11
+
+from django.db import migrations, models
+import markdownx.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('website', '0007_ipaddrinfo'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='BlogPost',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=128)),
+ ('body', markdownx.models.MarkdownxField()),
+ ('created', models.DateTimeField(auto_now_add=True)),
+ ('updated', models.DateTimeField(auto_now=True)),
+ ('published', models.BooleanField(db_index=True, default=False)),
+ ],
+ options={
+ 'index_together': {('published', 'created')},
+ },
+ ),
+
+ migrations.AddField(
+ model_name='userprofile',
+ name='last_blog_post_emailed',
+ field=models.IntegerField(default=0),
+ ),
+ ]
diff --git a/website/models.py b/website/models.py
index cb23cdf0..e9c01c66 100644
--- a/website/models.py
+++ b/website/models.py
@@ -3,6 +3,7 @@
from django.conf import settings
from jsonfield import JSONField
+from markdownx.models import MarkdownxField
from events.models import Feed, SubscriptionList
@@ -11,6 +12,7 @@ class UserProfile(models.Model):
massemail = models.BooleanField(default=True) # may we send you mail?
old_id = models.IntegerField(blank=True, null=True) # from the pre-2012 GovTrack database
last_mass_email = models.IntegerField(default=0)
+ last_blog_post_emailed = models.IntegerField(default=0)
congressionaldistrict = models.CharField(max_length=4, blank=True, null=True, db_index=True) # or 'XX00' if the user doesn't want to provide it
# monetization
@@ -436,3 +438,27 @@ class IpAddrInfo(models.Model):
last_hit = models.DateTimeField(auto_now=True, db_index=True)
hits = models.IntegerField(default=1, db_index=True)
leadfeeder = JSONField(default={}, blank=True, null=True)
+
+class BlogPost(models.Model):
+ title = models.CharField(max_length=128)
+ body = MarkdownxField()
+ created = models.DateTimeField(auto_now_add=True)
+ updated = models.DateTimeField(auto_now=True)
+ published = models.BooleanField(default=False, db_index=True)
+
+ class Meta:
+ index_together = [("published", "created")]
+
+ def __str__(self):
+ return self.title
+
+ def body_html(self):
+ from website.templatetags.govtrack_utils import markdown
+ return markdown(self.body)
+
+ def body_text(self):
+ # Replace Markdown-style [text][href] links with the text plus bracketed href.
+ import re
+ body_text = self.body
+ body_text = re.sub(r"\[(.*?)\]\((.*?)\)", r"\1 at \2", body_text)
+ return body_text
diff --git a/website/views.py b/website/views.py
index a89b0990..c74fae79 100644
--- a/website/views.py
+++ b/website/views.py
@@ -27,10 +27,16 @@ def index(request):
from bill.views import subject_choices
bill_subject_areas = subject_choices()
-
post_groups = []
MAX_PER_GROUP = 3
+ # Get our latest blog post.
+ from .models import BlogPost
+ latest_blog_post = BlogPost.objects\
+ .filter(published=True)\
+ .order_by('-created')\
+ .first()
+
# Trending feeds. These are short (no image, no snippet) so they go first.
trending_feeds = [Feed.objects.get(id=f) for f in Feed.get_trending_feeds()[0:6]]
if len(trending_feeds) > 0:
@@ -64,13 +70,9 @@ def index(request):
})
- from person.models import Person
- from vote.models import Vote
return {
- # for the action area below the splash
- 'bill_subject_areas': bill_subject_areas,
-
- # for the highlights blocks
+ 'bill_subject_areas': bill_subject_areas, # for the action area below the splash
+ 'latest_blog_post': latest_blog_post,
'post_groups': post_groups,
}
|