diff --git a/app/assets/v2/images/emails/link-regular.png b/app/assets/v2/images/emails/link-regular.png new file mode 100644 index 00000000000..c19122fd19f Binary files /dev/null and b/app/assets/v2/images/emails/link-regular.png differ diff --git a/app/assets/v2/images/offer.gif b/app/assets/v2/images/offer.gif index 8d23ced6fc0..02018c43aec 100644 Binary files a/app/assets/v2/images/offer.gif and b/app/assets/v2/images/offer.gif differ diff --git a/app/marketing/admin.py b/app/marketing/admin.py index 68b7c2acc71..1e17bd304be 100644 --- a/app/marketing/admin.py +++ b/app/marketing/admin.py @@ -24,7 +24,7 @@ from .models import ( AccountDeletionRequest, Alumni, EmailEvent, EmailSubscriber, EmailSupressionList, GithubEvent, GithubOrgToTwitterHandleMapping, Job, Keyword, LeaderboardRank, ManualStat, MarketingCallback, Match, RoundupEmail, - SlackPresence, SlackUser, Stat, + SlackPresence, SlackUser, Stat, UpcomingDate, ) @@ -123,6 +123,7 @@ def membership_length_in_days(self, instance): admin.site.register(Match, MatchAdmin) admin.site.register(Job, GeneralAdmin) admin.site.register(ManualStat, GeneralAdmin) +admin.site.register(UpcomingDate, GeneralAdmin) admin.site.register(Stat, GeneralAdmin) admin.site.register(Keyword, GeneralAdmin) admin.site.register(EmailEvent, EmailEventAdmin) diff --git a/app/marketing/mails.py b/app/marketing/mails.py index 6bc5378b48f..79998e50a1b 100644 --- a/app/marketing/mails.py +++ b/app/marketing/mails.py @@ -32,19 +32,20 @@ from marketing.utils import func_name, get_or_save_email_subscriber, should_suppress_notification_email from python_http_client.exceptions import HTTPError, UnauthorizedError from retail.emails import ( - render_admin_contact_funder, render_bounty_changed, render_bounty_expire_warning, render_bounty_feedback, - render_bounty_request, render_bounty_startwork_expire_warning, render_bounty_unintersted, render_comment, - render_faucet_rejected, render_faucet_request, render_featured_funded_bounty, render_funder_payout_reminder, - render_funder_stale, render_gdpr_reconsent, render_gdpr_update, render_grant_cancellation_email, - render_grant_update, render_kudos_email, render_match_distribution, render_match_email, render_mention, - render_new_bounty, render_new_bounty_acceptance, render_new_bounty_rejection, render_new_bounty_roundup, - render_new_grant_email, render_new_supporter_email, render_new_work_submission, render_no_applicant_reminder, - render_nth_day_email_campaign, render_quarterly_stats, render_request_amount_email, render_reserved_issue, - render_share_bounty, render_start_work_applicant_about_to_expire, render_start_work_applicant_expired, - render_start_work_approved, render_start_work_new_applicant, render_start_work_rejected, - render_subscription_terminated_email, render_successful_contribution_email, render_support_cancellation_email, - render_tax_report, render_thank_you_for_supporting_email, render_tip_email, - render_unread_notification_email_weekly_roundup, render_wallpost, render_weekly_recap, + email_to_profile, get_notification_count, render_admin_contact_funder, render_bounty_changed, + render_bounty_expire_warning, render_bounty_feedback, render_bounty_request, render_bounty_startwork_expire_warning, + render_bounty_unintersted, render_comment, render_faucet_rejected, render_faucet_request, + render_featured_funded_bounty, render_funder_payout_reminder, render_funder_stale, render_gdpr_reconsent, + render_gdpr_update, render_grant_cancellation_email, render_grant_update, render_kudos_email, + render_match_distribution, render_match_email, render_mention, render_new_bounty, render_new_bounty_acceptance, + render_new_bounty_rejection, render_new_bounty_roundup, render_new_grant_email, render_new_supporter_email, + render_new_work_submission, render_no_applicant_reminder, render_nth_day_email_campaign, render_quarterly_stats, + render_request_amount_email, render_reserved_issue, render_share_bounty, + render_start_work_applicant_about_to_expire, render_start_work_applicant_expired, render_start_work_approved, + render_start_work_new_applicant, render_start_work_rejected, render_subscription_terminated_email, + render_successful_contribution_email, render_support_cancellation_email, render_tax_report, + render_thank_you_for_supporting_email, render_tip_email, render_unread_notification_email_weekly_roundup, + render_wallpost, render_weekly_recap, ) from sendgrid.helpers.mail import Attachment, Content, Email, Mail, Personalization from sendgrid.helpers.stats import Category @@ -1178,40 +1179,53 @@ def new_bounty_daily(bounties, old_bounties, to_emails=None): if to_emails is None: to_emails = [] - from marketing.views import quest_of_the_day, upcoming_grant, upcoming_hackathon, latest_activities + from marketing.views import quest_of_the_day, upcoming_grant, upcoming_hackathon, latest_activities, upcoming_dates, upcoming_dates, email_announcements quest = quest_of_the_day() grant = upcoming_grant() - hackathon = upcoming_hackathon() + dates = list(upcoming_hackathon()) + list(upcoming_dates()) + announcements = email_announcements() offers = f"" if to_emails: offers = "" + profile = email_to_profile(to_emails[0]) + notifications = get_notification_count(profile, 7, timezone.now()) + if notifications: + plural = 's' if notifications > 1 else '' + notifications = f"💬 {notifications} Notification{plural}" + else: + notifications = '' has_offer = is_email_townsquare_enabled(to_emails[0]) and is_there_an_action_available() if has_offer: - offers = f"💰1 New Action" + offers = f"⚡️ 1 New Action" new_bounties = "" if bounties: plural_bounties = "Bounties" if len(bounties)>1 else "Bounty" - new_bounties = f"⚡️{len(bounties)} {plural_bounties}" + new_bounties = f"💰{len(bounties)} {plural_bounties}" elif old_bounties: plural_old_bounties = "Bounties" if len(old_bounties)>1 else "Bounty" - new_bounties = f"⚡️{len(old_bounties)} {plural_old_bounties}" + new_bounties = f"💰{len(old_bounties)} {plural_old_bounties}" new_quests = "" if quest: new_quests = f"🎯1 Quest" - new_hackathons = "" - if hackathon: - plural_hackathon = "Hackathons" if len(hackathon)>1 else "Hackathon" - new_hackathons = f"🛠️{len(hackathon)} {plural_hackathon}" + new_dates = "" + if dates: + plural_dates = "Events" if len(dates)>1 else "Event" + new_dates = f"🛠📆{len(dates)} {plural_dates}" + + new_announcements = "" + if announcements: + plural = "Announcement" + new_announcements = f"📣 1 {plural}" def comma(a): - return ", " if a and (new_bounties or new_quests or new_hackathons) else "" + return ", " if a and (new_bounties or new_quests or new_dates or new_announcements or notifications) else "" - subject = f"Gitcoin Daily {offers}{comma(offers)}{new_bounties}{comma(new_bounties)}{new_quests}{comma(new_quests)}{new_hackathons}" + subject = f"{notifications}{comma(notifications)}{new_announcements}{comma(new_announcements)}{new_bounties}{comma(new_bounties)}{new_dates}{comma(new_dates)}{new_quests}{comma(new_quests)}{offers}" for to_email in to_emails: cur_language = translation.get_language() @@ -1220,10 +1234,10 @@ def comma(a): from_email = settings.CONTACT_EMAIL from django.contrib.auth.models import User - user = User.objects.get(email__iexact=to_email) + user = User.objects.filter(email__iexact=to_email).first() activities = latest_activities(user) - html, text = render_new_bounty(to_email, bounties, old_bounties='', quest_of_the_day=quest, upcoming_grant=grant, upcoming_hackathon=hackathon, latest_activities=activities) + html, text = render_new_bounty(to_email, bounties, old_bounties='', quest_of_the_day=quest, upcoming_grant=grant, upcoming_hackathon=upcoming_hackathon(), latest_activities=activities) if not should_suppress_notification_email(to_email, 'new_bounty_notifications'): send_mail(from_email, to_email, subject, text, html, categories=['marketing', func_name()]) diff --git a/app/marketing/management/commands/new_bounties_email.py b/app/marketing/management/commands/new_bounties_email.py index e7f26faf52a..e6d9f3ea563 100644 --- a/app/marketing/management/commands/new_bounties_email.py +++ b/app/marketing/management/commands/new_bounties_email.py @@ -16,6 +16,8 @@ ''' import logging +import time +import warnings from django.conf import settings from django.core.management.base import BaseCommand @@ -26,6 +28,9 @@ from marketing.models import EmailSubscriber from townsquare.utils import is_email_townsquare_enabled +warnings.filterwarnings("ignore") + +override_in_dev = True def get_bounties_for_keywords(keywords, hours_back): new_bounties_pks = [] @@ -57,7 +62,7 @@ class Command(BaseCommand): help = 'sends new_bounty_daily _emails' def handle(self, *args, **options): - if settings.DEBUG: + if settings.DEBUG and not override_in_dev: print("not active in non prod environments") return hours_back = 24 @@ -65,9 +70,12 @@ def handle(self, *args, **options): counter_grant_total = 0 counter_total = 0 counter_sent = 0 - print("got {} emails".format(eses.count())) + start_time = time.time() + total_count = eses.count() + print("got {} emails".format(total_count)) for es in eses: try: + # prep counter_grant_total += 1 to_email = es.email keywords = es.keywords @@ -77,13 +85,19 @@ def handle(self, *args, **options): continue counter_total += 1 new_bounties, all_bounties = get_bounties_for_keywords(keywords, hours_back) - print("{}/{}/{}) {}/{}: got {} new bounties & {} all bounties".format(counter_sent, counter_total, counter_grant_total, to_email, keywords, new_bounties.count(), all_bounties.count())) + + # stats + speed = round((time.time() - start_time) / counter_grant_total, 2) + ETA = round((total_count - counter_grant_total) / speed / 3600, 1) + print(f"{counter_sent} sent/{counter_total} enabled/{counter_grant_total} evaluated, {speed}/s, ETA:{ETA}h, working on {to_email} ") + + # send should_send = new_bounties.count() or town_square_enabled #should_send = new_bounties.count() if should_send: - print(f"sending to {to_email}") + #print(f"sending to {to_email}") new_bounty_daily(new_bounties, all_bounties, [to_email]) - print(f"/sent to {to_email}") + #print(f"/sent to {to_email}") counter_sent += 1 except Exception as e: logging.exception(e) diff --git a/app/marketing/migrations/0014_upcomingdate.py b/app/marketing/migrations/0014_upcomingdate.py new file mode 100644 index 00000000000..063b52e2a8b --- /dev/null +++ b/app/marketing/migrations/0014_upcomingdate.py @@ -0,0 +1,30 @@ +# Generated by Django 2.2.4 on 2020-05-27 18:20 + +from django.db import migrations, models +import economy.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('marketing', '0013_auto_20200413_1223'), + ] + + operations = [ + migrations.CreateModel( + name='UpcomingDate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_on', models.DateTimeField(db_index=True, default=economy.models.get_time)), + ('modified_on', models.DateTimeField(default=economy.models.get_time)), + ('title', models.CharField(max_length=255)), + ('date', models.DateTimeField(db_index=True)), + ('img_url', models.URLField(blank=True, db_index=True)), + ('url', models.URLField(db_index=True)), + ('comment', models.TextField(blank=True, default='', max_length=255)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/app/marketing/models.py b/app/marketing/models.py index 1d53354a942..8b480770c6d 100644 --- a/app/marketing/models.py +++ b/app/marketing/models.py @@ -407,3 +407,21 @@ def get_absolute_url(self): def __str__(self): return self.subject + +class UpcomingDate(SuperModel): + """Define the upcoming date model""" + + title = models.CharField(max_length=255) + date = models.DateTimeField(db_index=True) + img_url = models.URLField(db_index=True, blank=True) + url = models.URLField(db_index=True) + comment = models.TextField(max_length=255, default='', blank=True) + + @property + def naturaltime(self): + from django.contrib.humanize.templatetags.humanize import naturaltime + return naturaltime(self.date) + + + def __str__(self): + return f"{self.title}" diff --git a/app/marketing/utils.py b/app/marketing/utils.py index a933d530ecf..d417005bec0 100644 --- a/app/marketing/utils.py +++ b/app/marketing/utils.py @@ -229,7 +229,7 @@ def get_or_save_email_subscriber(email, source, send_slack_invite=True, profile= else: es = EmailSubscriber.objects.create(**defaults) created = True - print("EmailSubscriber:", es, "- created" if created else "- updated") + #print("EmailSubscriber:", es, "- created" if created else "- updated") except EmailSubscriber.MultipleObjectsReturned: email_subscriber_ids = EmailSubscriber.objects.filter(email__iexact=email) \ .values_list('id', flat=True) \ @@ -241,7 +241,7 @@ def get_or_save_email_subscriber(email, source, send_slack_invite=True, profile= es = EmailSubscriber.objects.create(**defaults) created = True except Exception as e: - print(f'Failed to update or create email subscriber: ({email}) - {e}') + #print(f'Failed to update or create email subscriber: ({email}) - {e}') return '' if created or not es.priv: diff --git a/app/marketing/views.py b/app/marketing/views.py index 3f809a9ceb0..23c4a435e7b 100644 --- a/app/marketing/views.py +++ b/app/marketing/views.py @@ -50,11 +50,12 @@ from marketing.country_codes import COUNTRY_CODES, COUNTRY_NAMES, FLAG_API_LINK, FLAG_ERR_MSG, FLAG_SIZE, FLAG_STYLE from marketing.mails import new_feedback from marketing.management.commands.new_bounties_email import get_bounties_for_keywords -from marketing.models import AccountDeletionRequest, EmailSubscriber, Keyword, LeaderboardRank +from marketing.models import AccountDeletionRequest, EmailSubscriber, Keyword, LeaderboardRank, UpcomingDate from marketing.utils import delete_user_from_mailchimp, get_or_save_email_subscriber, validate_slack_integration from quests.models import Quest from retail.emails import ALL_EMAILS, render_new_bounty, render_nth_day_email_campaign from retail.helpers import get_ip +from townsquare.models import Announcement logger = logging.getLogger(__name__) @@ -100,6 +101,11 @@ def get_settings_navs(request): return tabs +def upcoming_dates(): + return UpcomingDate.objects.filter(date__gt=timezone.now()).order_by('date') + +def email_announcements(): + return Announcement.objects.filter(key='founders_note_daily_email', valid_from__lt=timezone.now(), valid_to__gt=timezone.now()).order_by('valid_to').first() def settings_helper_get_auth(request, key=None): # setup @@ -994,17 +1000,20 @@ def upcoming_grant(): def upcoming_hackathon(): try: - return HackathonEvent.objects.filter(end_date__gt=timezone.now()).order_by('-start_date') + return HackathonEvent.objects.filter(end_date__gt=timezone.now(), visible=True).order_by('-start_date') except HackathonEvent.DoesNotExist: try: - return [HackathonEvent.objects.filter(start_date__gte=timezone.now()).order_by('start_date').first()] + return [HackathonEvent.objects.filter(start_date__gte=timezone.now(), visible=True).order_by('start_date').first()] except HackathonEvent.DoesNotExist: return None def latest_activities(user): from retail.views import get_specific_activities - cutoff_date = timezone.now() - timezone.timedelta(days=7) + from townsquare.tasks import increment_view_counts + cutoff_date = timezone.now() - timezone.timedelta(days=1) activities = get_specific_activities('connect', 0, user, 0)[:4] + activities_pks = list(activities.values_list('pk', flat=True)) + increment_view_counts.delay(activities_pks) return activities @staff_member_required diff --git a/app/retail/emails.py b/app/retail/emails.py index dc674376262..a1c2a16d2c5 100644 --- a/app/retail/emails.py +++ b/app/retail/emails.py @@ -36,6 +36,7 @@ from grants.models import Contribution, Grant, Subscription from marketing.models import LeaderboardRank from marketing.utils import get_or_save_email_subscriber +from premailer import Premailer from retail.utils import strip_double_chars, strip_html logger = logging.getLogger(__name__) @@ -44,9 +45,9 @@ # key, name, frequency MARKETING_EMAILS = [ + ('new_bounty_notifications', _('Daily Emails'), _('(up to) Daily')), ('welcome_mail', _('Welcome Emails'), _('First 3 days after you sign up')), ('roundup', _('Roundup Emails'), _('Weekly')), - ('new_bounty_notifications', _('Daily Bounty Action Emails'), _('(up to) Daily')), ('important_product_updates', _('Product Update Emails'), _('Quarterly')), ('general', _('General Email Updates'), _('as it comes')), ('quarterly', _('Quarterly Email Updates'), _('Quarterly')), @@ -76,11 +77,12 @@ ALL_EMAILS = MARKETING_EMAILS + TRANSACTIONAL_EMAILS + NOTIFICATION_EMAILS +# per speed notes at https://pypi.org/project/premailer/ +premailer = Premailer(base_url=settings.BASE_URL) def premailer_transform(html): cssutils.log.setLevel(logging.CRITICAL) - p = premailer.Premailer(html, base_url=settings.BASE_URL) - return p.transform() + return premailer.transform(html) def render_featured_funded_bounty(bounty): @@ -529,37 +531,41 @@ def render_funder_stale(github_username, days=60, time_as_str='a couple months') return response_html, response_txt -def render_new_bounty(to_email, bounties, old_bounties, offset=3, quest_of_the_day={}, upcoming_grant={}, upcoming_hackathon={}, latest_activities={}, from_date=date.today(), days_ago=7): - from townsquare.utils import is_email_townsquare_enabled, is_there_an_action_available - from dashboard.models import Profile +def get_notification_count(profile, days_ago, from_date): + from_date = from_date + timedelta(days=1) + to_date = from_date - timedelta(days=days_ago) + + notifications_count = 0 from inbox.models import Notification - sub = get_or_save_email_subscriber(to_email, 'internal') - - email_style = 26 + try: + notifications_count = Notification.objects.filter(to_user=profile.user.id, is_read=False, created_on__range=[to_date, from_date]).count() + except Notification.DoesNotExist: + pass + except AttributeError: + pass + return notifications_count - # Get notifications count from the Profile.User of to_email +def email_to_profile(to_email): + from dashboard.models import Profile try: profile = Profile.objects.filter(email__iexact=to_email).last() except Profile.DoesNotExist: pass + return profile - from_date = from_date + timedelta(days=1) - to_date = from_date - timedelta(days=days_ago) +def render_new_bounty(to_email, bounties, old_bounties, offset=3, quest_of_the_day={}, upcoming_grant={}, upcoming_hackathon={}, latest_activities={}, from_date=date.today(), days_ago=7): + from townsquare.utils import is_email_townsquare_enabled, is_there_an_action_available + from marketing.views import upcoming_dates, email_announcements + sub = get_or_save_email_subscriber(to_email, 'internal') + + email_style = 26 - try: - notifications_count = Notification.objects.filter(to_user=profile.user.id, is_read=False, created_on__range=[to_date, from_date]).count() - except Notification.DoesNotExist: - pass + # Get notifications count from the Profile.User of to_email + profile = email_to_profile(to_email) + + notifications_count = get_notification_count(profile, days_ago, from_date) upcoming_events = [] - if upcoming_grant: - upcoming_events.append({ - 'event': upcoming_grant, - 'title': upcoming_grant.title, - 'image_url': upcoming_grant.logo.url if upcoming_grant.logo else f'{settings.STATIC_URL}v2/images/emails/grants-neg.png', - 'url': upcoming_grant.url, - 'date': upcoming_grant.next_clr_calc_date.strftime("%Y-%d-%m") if upcoming_grant.next_clr_calc_date else upcoming_grant.created_on.strftime("%Y-%d-%m") - }) if upcoming_hackathon: for hackathon in upcoming_hackathon: upcoming_events.append({ @@ -573,6 +579,8 @@ def render_new_bounty(to_email, bounties, old_bounties, offset=3, quest_of_the_d params = { 'old_bounties': old_bounties, 'bounties': bounties, + 'upcoming_dates': upcoming_dates(), + 'email_announcements': email_announcements(), 'subscriber': sub, 'keywords': ",".join(sub.keywords) if sub and sub.keywords else '', 'email_style': email_style, diff --git a/app/retail/templates/emails/new_bounty.html b/app/retail/templates/emails/new_bounty.html index 718b490d9b6..37e6a0ca844 100644 --- a/app/retail/templates/emails/new_bounty.html +++ b/app/retail/templates/emails/new_bounty.html @@ -61,6 +61,23 @@ display: inline-block; margin-top: 3px; } + .arrow-up{ + width: 0; + height: 0; + border-left: 15px solid transparent; + border-right: 15px solid transparent; + border-bottom: 15px solid #ddd; + margin-left: auto; + margin-right: auto; +} + #founder_announce { + background-color: #fafafa; + margin-bottom: 15px; + + } + #founder_announce a{ + font-weight: bold; + } @@ -68,17 +85,18 @@
-
-+ {{email_announcements.desc|safe}} +
+{{event.date}}
+ Read More > + {% endfor %} +
+
+- {% trans "You are receiving this email because your email address is on the notification list" %}. {% trans "Manage Email Settings" %}. + {% trans "You are receiving this email because you are subscribed to daily emails." %} {% trans "Manage Email Settings" %} | {% trans "Unsubscribe" %}.
{% endblock %} diff --git a/app/retail/templates/shared/activity.html b/app/retail/templates/shared/activity.html index 745e63bd83c..964da39c8c0 100644 --- a/app/retail/templates/shared/activity.html +++ b/app/retail/templates/shared/activity.html @@ -287,7 +287,7 @@ >