From a163d7f6a91c7a2833a509fafa4f4108cf8f5bb5 Mon Sep 17 00:00:00 2001 From: Patryk Perduta Date: Mon, 19 Feb 2018 10:17:47 +0100 Subject: [PATCH 1/3] Issue #97 - WIP: Prepare weekly newsletter sent by email. --- newsletter/__init__.py | 0 newsletter/admin.py | 10 ++++ newsletter/apps.py | 5 ++ newsletter/management/__init__.py | 0 newsletter/management/commands/__init__.py | 0 .../management/commands/send_newsletter.py | 10 ++++ newsletter/migrations/0001_initial.py | 27 +++++++++++ .../migrations/0002_auto_20180219_0326.py | 20 ++++++++ newsletter/migrations/__init__.py | 0 newsletter/models.py | 7 +++ newsletter/services.py | 46 +++++++++++++++++++ newsletter/templates/newsletter.html | 5 ++ newsletter/tests.py | 6 +++ newsletter/views.py | 3 ++ settings/base.py | 1 + 15 files changed, 140 insertions(+) create mode 100644 newsletter/__init__.py create mode 100644 newsletter/admin.py create mode 100644 newsletter/apps.py create mode 100644 newsletter/management/__init__.py create mode 100644 newsletter/management/commands/__init__.py create mode 100644 newsletter/management/commands/send_newsletter.py create mode 100644 newsletter/migrations/0001_initial.py create mode 100644 newsletter/migrations/0002_auto_20180219_0326.py create mode 100644 newsletter/migrations/__init__.py create mode 100644 newsletter/models.py create mode 100644 newsletter/services.py create mode 100644 newsletter/templates/newsletter.html create mode 100644 newsletter/tests.py create mode 100644 newsletter/views.py diff --git a/newsletter/__init__.py b/newsletter/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/newsletter/admin.py b/newsletter/admin.py new file mode 100644 index 00000000..a81345d0 --- /dev/null +++ b/newsletter/admin.py @@ -0,0 +1,10 @@ +from django.contrib import admin + +from newsletter.models import NewsletterCache + + +class NewsletterCacheAdmin(admin.ModelAdmin): + pass + + +admin.site.register(NewsletterCache, NewsletterCacheAdmin) diff --git a/newsletter/apps.py b/newsletter/apps.py new file mode 100644 index 00000000..e4a2ad5b --- /dev/null +++ b/newsletter/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class WeeklyNewsletterConfig(AppConfig): + name = 'newsletter' diff --git a/newsletter/management/__init__.py b/newsletter/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/newsletter/management/commands/__init__.py b/newsletter/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/newsletter/management/commands/send_newsletter.py b/newsletter/management/commands/send_newsletter.py new file mode 100644 index 00000000..019d5dbe --- /dev/null +++ b/newsletter/management/commands/send_newsletter.py @@ -0,0 +1,10 @@ +from django.core.management.base import BaseCommand + +from newsletter.services import Newsletter + + +class Command(BaseCommand): + help = 'tbd' + + def handle(self, *args, **kwargs): + Newsletter() diff --git a/newsletter/migrations/0001_initial.py b/newsletter/migrations/0001_initial.py new file mode 100644 index 00000000..2b5389d3 --- /dev/null +++ b/newsletter/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-02-19 03:26 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='NewsletterCache', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('last_time_sent', models.DateTimeField()), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/newsletter/migrations/0002_auto_20180219_0326.py b/newsletter/migrations/0002_auto_20180219_0326.py new file mode 100644 index 00000000..bd9dda20 --- /dev/null +++ b/newsletter/migrations/0002_auto_20180219_0326.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-02-19 03:26 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('newsletter', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='newslettercache', + name='last_time_sent', + field=models.DateTimeField(null=True), + ), + ] diff --git a/newsletter/migrations/__init__.py b/newsletter/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/newsletter/models.py b/newsletter/models.py new file mode 100644 index 00000000..925ddfa4 --- /dev/null +++ b/newsletter/models.py @@ -0,0 +1,7 @@ +from django.contrib.auth.models import User +from django.db import models + + +class NewsletterCache(models.Model): + user = models.ForeignKey(User) + last_time_sent = models.DateTimeField(null=True) diff --git a/newsletter/services.py b/newsletter/services.py new file mode 100644 index 00000000..8497110b --- /dev/null +++ b/newsletter/services.py @@ -0,0 +1,46 @@ +from datetime import datetime, timedelta + +from django.conf import settings +from django.contrib.auth.models import User +from django.core.mail import EmailMultiAlternatives +from django.template.loader import get_template + +from newsletter.models import NewsletterCache +from package.models import Project + + +class Newsletter: + @staticmethod + def get_user_favorite_projects(user): + return Project.objects.all() # TODO: make it work + + def __init__(self): + for user in User.objects.all(): + newsletter_cache, _ = NewsletterCache.objects.get_or_create(user=user) + seven_days_ago = datetime.now() - timedelta(days=7) + if not newsletter_cache.last_time_sent or newsletter_cache.last_time_sent <= seven_days_ago: + # newsletter_cache.last_time_sent = datetime.now() + newsletter_cache.save() + self.send_newsletter(user) + + @staticmethod + def send_newsletter(user): + user_favorite_projects = Newsletter.get_user_favorite_projects(user) + + # plain_template = get_template('newsletter.txt') + html_template = get_template('newsletter.html') + + d = {'username': 'Patryk', 'favorite': ['abc', 123, 4]} + + html_content = html_template.render(d) + print(html_content) + import pdb; pdb.set_trace() + + # msg = EmailMultiAlternatives( + # subject='{0} Newsletter'.format(settings.EMAIL_SUBJECT_PREFIX), + # body='Newsletter body', + # from_email=settings.VALIDATION_EMAIL_SENDER, + # to=[user.email], + # ) + # msg.esp_extra = {"sender_domain": settings.EMAIL_SENDER_DOMAIN} + # msg.send() diff --git a/newsletter/templates/newsletter.html b/newsletter/templates/newsletter.html new file mode 100644 index 00000000..e6630464 --- /dev/null +++ b/newsletter/templates/newsletter.html @@ -0,0 +1,5 @@ +Hello {{username}}! + +{% for fav in favorite %} + fav {{fav}} +{% endfor %} diff --git a/newsletter/tests.py b/newsletter/tests.py new file mode 100644 index 00000000..ae6200ee --- /dev/null +++ b/newsletter/tests.py @@ -0,0 +1,6 @@ +from django.test import TestCase + + +class NewsletterServiceTestCase(TestCase): + def test_get_user_favorite_projects(self): + self.fail() diff --git a/newsletter/views.py b/newsletter/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/newsletter/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/settings/base.py b/settings/base.py index af79d567..f56d67b8 100644 --- a/settings/base.py +++ b/settings/base.py @@ -137,6 +137,7 @@ "apiv3", "social_auth_local", "im", + 'newsletter' ] PREREQ_APPS = [ From c12596476536d4d794c01240892a7f4df9cbcb66 Mon Sep 17 00:00:00 2001 From: Patryk Perduta Date: Fri, 23 Feb 2018 15:59:34 +0100 Subject: [PATCH 2/3] WIP --- .../0003_newslettercache_subscribes.py | 20 +++ newsletter/models.py | 1 + newsletter/services.py | 62 +++++++-- newsletter/templates/newsletter.html | 122 +++++++++++++++++- newsletter/templates/newsletter.txt | 0 newsletter/urls.py | 8 ++ newsletter/views.py | 43 +++++- settings/base.py | 5 +- social_auth_local/pipeline.py | 9 ++ social_auth_local/views.py | 1 - urls.py | 1 + 11 files changed, 250 insertions(+), 22 deletions(-) create mode 100644 newsletter/migrations/0003_newslettercache_subscribes.py create mode 100644 newsletter/templates/newsletter.txt create mode 100644 newsletter/urls.py diff --git a/newsletter/migrations/0003_newslettercache_subscribes.py b/newsletter/migrations/0003_newslettercache_subscribes.py new file mode 100644 index 00000000..7e4df564 --- /dev/null +++ b/newsletter/migrations/0003_newslettercache_subscribes.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-02-22 05:43 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('newsletter', '0002_auto_20180219_0326'), + ] + + operations = [ + migrations.AddField( + model_name='newslettercache', + name='subscribes', + field=models.BooleanField(default=False), + ), + ] diff --git a/newsletter/models.py b/newsletter/models.py index 925ddfa4..fa5d9fdb 100644 --- a/newsletter/models.py +++ b/newsletter/models.py @@ -5,3 +5,4 @@ class NewsletterCache(models.Model): user = models.ForeignKey(User) last_time_sent = models.DateTimeField(null=True) + subscribes = models.BooleanField(default=False) diff --git a/newsletter/services.py b/newsletter/services.py index 8497110b..446d86e4 100644 --- a/newsletter/services.py +++ b/newsletter/services.py @@ -3,44 +3,78 @@ from django.conf import settings from django.contrib.auth.models import User from django.core.mail import EmailMultiAlternatives +from django.core.signing import Signer from django.template.loader import get_template +from django.urls import reverse from newsletter.models import NewsletterCache -from package.models import Project +from package.models import TimelineEvent, Project class Newsletter: + NEWSLETTER_FREQUENCY_IN_DAYS = 7 + AMOUNT_OF_LATEST_PROJECTS_IN_NEWSLETTER = 3 + + @staticmethod + def get_unsubscribe_link(user): + token = Signer().sign(user.username).split(':')[1] + return reverse('unsubscribe', kwargs={'username': user.username, 'token': token}) + @staticmethod def get_user_favorite_projects(user): - return Project.objects.all() # TODO: make it work + return user.project_set.all() def __init__(self): for user in User.objects.all(): newsletter_cache, _ = NewsletterCache.objects.get_or_create(user=user) - seven_days_ago = datetime.now() - timedelta(days=7) - if not newsletter_cache.last_time_sent or newsletter_cache.last_time_sent <= seven_days_ago: - # newsletter_cache.last_time_sent = datetime.now() - newsletter_cache.save() - self.send_newsletter(user) + newsletter_cache.subscribes = True + if not newsletter_cache.subscribes: + continue + # newsletter_cache.last_time_sent = datetime.now() + newsletter_cache.save() + self.send_newsletter(user) + + @staticmethod + def get_favorite_project_events(user): + newsletter_cache, _ = NewsletterCache.objects.get_or_create(user=user) + favorite_projects = Newsletter.get_user_favorite_projects(user) + timeline_events = TimelineEvent.objects.filter( + project__in=favorite_projects, + date__gte=datetime.now() - timedelta(days=Newsletter.NEWSLETTER_FREQUENCY_IN_DAYS)) + return timeline_events + + @staticmethod + def get_latest_projects(): + return Project.objects.all().order_by('-id')[:Newsletter.AMOUNT_OF_LATEST_PROJECTS_IN_NEWSLETTER] @staticmethod def send_newsletter(user): - user_favorite_projects = Newsletter.get_user_favorite_projects(user) + favorite_project_events = Newsletter.get_favorite_project_events(user) + if not favorite_project_events: + return False + + latest_projects = Newsletter.get_latest_projects() + + unsubscribe_link = Newsletter.get_unsubscribe_link(user) - # plain_template = get_template('newsletter.txt') + plain_template = get_template('newsletter.txt') html_template = get_template('newsletter.html') - d = {'username': 'Patryk', 'favorite': ['abc', 123, 4]} + d = {'username': user.username, + 'favorite_project_events': favorite_project_events, + 'latest_projects': latest_projects, + 'unsubscribe_link': unsubscribe_link, + } + plain_content = plain_template.render(d) html_content = html_template.render(d) - print(html_content) - import pdb; pdb.set_trace() # msg = EmailMultiAlternatives( # subject='{0} Newsletter'.format(settings.EMAIL_SUBJECT_PREFIX), - # body='Newsletter body', + # body=plain_content, # from_email=settings.VALIDATION_EMAIL_SENDER, - # to=[user.email], + # to=['patryk@perduta.net'], # ) + # msg.attach_alternative(html_content, 'text/html') # msg.esp_extra = {"sender_domain": settings.EMAIL_SENDER_DOMAIN} # msg.send() diff --git a/newsletter/templates/newsletter.html b/newsletter/templates/newsletter.html index e6630464..42930b96 100644 --- a/newsletter/templates/newsletter.html +++ b/newsletter/templates/newsletter.html @@ -1,5 +1,119 @@ -Hello {{username}}! + + + + Happy email + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+   +
+

+

Logo steemprojects

+

+ Latest events from your favorite projects +

+


+
+ + + + + +
+ + + SteemConnect 2.0: + Easy, Fast, Efficient Access to the Steem Blockchain +
+
+
+
+ + + + + +
+ + + + Introducing SteemConnect by Busy : Identity, authentication, authorization for + Steem blockchain’s ap + +
+
+
+
+ + + + + +
+ + + + The REALLY gentle guide to becoming a witness + +
+
-{% for fav in favorite %} - fav {{fav}} -{% endfor %} +

+ Your welcome, @steemprojects + Follow us on + steem +

+
+
+
+
+

+ To unsubscribe click this link: unsubscribe. +

+
+
+ + diff --git a/newsletter/templates/newsletter.txt b/newsletter/templates/newsletter.txt new file mode 100644 index 00000000..e69de29b diff --git a/newsletter/urls.py b/newsletter/urls.py new file mode 100644 index 00000000..278c3519 --- /dev/null +++ b/newsletter/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls import url + +from newsletter.views import unsubscribe, ask_for_newsletter + +urlpatterns = [ + url(regex=r'^unsubscribe/(?P[\w.@+-]+)/(?P[\w.:\-_=]+)/$', view=unsubscribe, name='unsubscribe'), + url(regex=r'^ask/$', view=ask_for_newsletter, name='ask_for_newsletter'), +] diff --git a/newsletter/views.py b/newsletter/views.py index 91ea44a2..7e56e047 100644 --- a/newsletter/views.py +++ b/newsletter/views.py @@ -1,3 +1,42 @@ -from django.shortcuts import render +from django.contrib import messages +from django.contrib.auth.models import User +from django.core.signing import Signer, BadSignature +from django.shortcuts import render, redirect, get_object_or_404 +from social_django.utils import load_strategy -# Create your views here. +from newsletter.models import NewsletterCache +from social_auth_local.decorators import render_to + + +def unsubscribe(request, username, token): + user = get_object_or_404(User, username=username) + newsletter_cache = NewsletterCache.objects.get(user=user) + + try: + key = '{}:{}'.format(username, token) + Signer().unsign(key) + except BadSignature: + messages.add_message(request, messages.ERROR, 'Your subscribtion cancellation link is invalid.') + return redirect('/') + + if newsletter_cache.subscribes: + newsletter_cache.subscribes = False + newsletter_cache.save() + messages.add_message(request, messages.INFO, 'You\'ve been succesfully unsubscribed from our newsletter. ;-(') + return redirect('/') + + messages.add_message(request, messages.INFO, 'You are already have been unsubscribed from our newsletter!') + return redirect('/') + + +@render_to('social_auth_local/ask_for_newsletter.html') +def ask_for_newsletter(request): + strategy = load_strategy() + partial_token = request.GET.get('partial_token') + partial = strategy.partial_load(partial_token) + + return { + 'ask_for_newsletter': True, + 'partial_backend_name': partial.backend if partial else None, + 'partial_token': partial_token, + } diff --git a/settings/base.py b/settings/base.py index f56d67b8..9bd46ee9 100644 --- a/settings/base.py +++ b/settings/base.py @@ -312,7 +312,10 @@ 'social_auth_local.pipeline.social_user', # CUSTOM PIPELINE - 'social_auth_local.pipeline.require_email', + # 'social_auth_local.pipeline.require_email', + + # CUSTOM PIPELINE + 'social_auth_local.pipeline.ask_for_newsletter', # Make up a username for this person, appends a random string at the end if # there's any collision. diff --git a/social_auth_local/pipeline.py b/social_auth_local/pipeline.py index 22b978c2..c7cce3f4 100644 --- a/social_auth_local/pipeline.py +++ b/social_auth_local/pipeline.py @@ -61,6 +61,15 @@ def require_email(strategy, details, user=None, is_new=False, *args, **kwargs): ) +@partial +def ask_for_newsletter(strategy, details, *args, **kwargs): + subscription = strategy.request_data().get('subscription') + if subscription: + details['subscription'] = subscription + current_partial = kwargs.get('current_partial') + return strategy.redirect('/newsletter/ask/?partial_token={}'.format(current_partial.token)) + + def save_profile_pipeline(backend, user, response, details, social, *args, **kwargs): try: # profile could be created for a user which previously logged in diff --git a/social_auth_local/views.py b/social_auth_local/views.py index 30676e14..bcb4376c 100644 --- a/social_auth_local/views.py +++ b/social_auth_local/views.py @@ -29,7 +29,6 @@ def merging_accounts(request): } - @render_to('social_auth_local/email_required.html') def require_email(request): """Email required page""" diff --git a/urls.py b/urls.py index bbeee78a..9d2be922 100644 --- a/urls.py +++ b/urls.py @@ -36,6 +36,7 @@ url(r"^projects/", include("package.urls")), url(r"^grids/", include("grid.urls")), url(r"^feeds/", include("feeds.urls")), + url(r'^newsletter/', include('newsletter.urls')), url(r"^categories/(?P[-\w]+)/$", category, name="category"), url(r"^categories/$", homepage, name="categories"), From ef0e7b669535ba74b3e82f76bf69fc565de682ba Mon Sep 17 00:00:00 2001 From: Patryk Perduta Date: Fri, 23 Feb 2018 16:00:03 +0100 Subject: [PATCH 3/3] WIP --- .../social_auth_local/ask_for_newsletter.html | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 templates/social_auth_local/ask_for_newsletter.html diff --git a/templates/social_auth_local/ask_for_newsletter.html b/templates/social_auth_local/ask_for_newsletter.html new file mode 100644 index 00000000..c7815320 --- /dev/null +++ b/templates/social_auth_local/ask_for_newsletter.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% load i18n static %} + +{% block head_title %}{% trans "Email Required" %}{% endblock %} + +{% block body_class %}email_required{% endblock %} + +{% block extra_head %} + +{% endblock %} + +{% block body %} +
+

Newsletter

+
+ + + Do you really desire to get our newsletter? + +
+
+{% endblock %} +