Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redesign Daily Email #6471

Merged
merged 46 commits into from
May 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
ee2f26e
add new header26 image
sebastiantf Apr 19, 2020
b009e5b
add Bounties in new_bounty
sebastiantf Apr 19, 2020
3615b3f
add Quest of The Day
sebastiantf Apr 19, 2020
8140ead
add Upcoming Grant
sebastiantf Apr 19, 2020
a6aba03
add Upcoming Hackathon
sebastiantf Apr 19, 2020
c1fbab2
add Latest Activities/Townsquare Posts
sebastiantf Apr 19, 2020
4796f68
add number of unread notifs
sebastiantf Apr 19, 2020
c626e45
fix bug in unread_notification_email_weekly
sebastiantf Apr 19, 2020
9d90bfc
change new_bounty footer
sebastiantf Apr 19, 2020
a99e615
add Gitcoin Daily Digest heading
sebastiantf Apr 19, 2020
a1fd51b
some margin for #grow-oss
sebastiantf Apr 19, 2020
2204c3b
revert email_style
sebastiantf Apr 19, 2020
04cad32
import required data for new_bounties_email
sebastiantf May 2, 2020
a91a9df
use .full-width for Save The Date
sebastiantf May 2, 2020
6908487
copy bootstrap styles used for activities
sebastiantf May 2, 2020
9160ee3
copy and fix several styles for activities
sebastiantf May 2, 2020
c88381e
show unread notifications only if there are any
sebastiantf May 2, 2020
30d89a1
change kudos stack using div to table
sebastiantf May 2, 2020
275b3cf
fix font-awesome images not loading
sebastiantf May 2, 2020
f764e91
misc style changes
sebastiantf May 2, 2020
23dbd6d
send emails even if there are no new bounties
sebastiantf May 4, 2020
f82e503
show bounties section only if there are any
sebastiantf May 4, 2020
20fbe3b
update new_bounty_daily_preview() to use the new render_new_bounty() …
sebastiantf May 4, 2020
f2fbe3e
get_specific_activities() in latest_activities()
sebastiantf May 5, 2020
99744a2
remove bootstrap import
sebastiantf May 5, 2020
b309e75
some misc styles
sebastiantf May 5, 2020
39ddda1
hide additional data in activitys if not for_email
sebastiantf May 5, 2020
0a15906
order grants by weighted_shuffle
sebastiantf May 7, 2020
86b7d08
exception handling for Profile, Notification
sebastiantf May 7, 2020
7cc39b1
fix quest-img margin on large displays
sebastiantf May 8, 2020
0cb046a
conditional upcoming_events
sebastiantf May 8, 2020
02ae986
all ongoing/first upcoming hackathon
sebastiantf May 12, 2020
dd1bb60
always navy footer
sebastiantf May 12, 2020
ab81775
upcoming_grant.next_clr_calc_date or .created_on
sebastiantf May 12, 2020
8f4e52c
padding and hr if no upcoming_events
sebastiantf May 12, 2020
d86ce29
limit max no. of bounties to 5
sebastiantf May 13, 2020
2751f2c
update subject line
sebastiantf May 14, 2020
fd83ff1
return single HackathonEvent as List
sebastiantf May 14, 2020
165d0a4
remove commented code
sebastiantf May 14, 2020
b22a7e7
remove temp variables
sebastiantf May 14, 2020
12d1050
boilerplate images for missing logos
sebastiantf May 14, 2020
2f1ce82
boilerplate image for kudos
sebastiantf May 20, 2020
c014741
replace refs to external stylesheets with includes
sebastiantf May 21, 2020
d0bdd37
set email_style = 26 for new header
sebastiantf May 26, 2020
6fcb88a
remove failing test
danlipert May 27, 2020
feae48b
Merge branch 'master' into redesign-daily-email
danlipert May 27, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added app/assets/v2/images/emails/comment-regular.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/v2/images/emails/ethereum-brands-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/v2/images/emails/eye-regular.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/v2/images/emails/grants-neg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/v2/images/emails/hackathons-neg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/v2/images/emails/heart-regular.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/v2/images/emails/kudos_template.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 32 additions & 16 deletions app/marketing/mails.py
Original file line number Diff line number Diff line change
Expand Up @@ -1155,42 +1155,58 @@ def reject_faucet_request(fr):


def new_bounty_daily(bounties, old_bounties, to_emails=None):
from marketing.views import trending_quests

if not bounties:
return
max_bounties = 10
max_bounties = 5
if len(bounties) > max_bounties:
bounties = bounties[0:max_bounties]
if to_emails is None:
to_emails = []
plural = "s" if len(bounties) != 1 else ""
worth = round(sum([bounty.value_in_usdt for bounty in bounties if bounty.value_in_usdt]), 2)
worth = f"${worth}" if worth else ""

from marketing.views import quest_of_the_day, upcoming_grant, upcoming_hackathon, latest_activities
quest = quest_of_the_day()
grant = upcoming_grant()
hackathon = upcoming_hackathon()

offers = f""
if to_emails:
offers = ""

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.count():
new_bounties = f"⚡️ {worth} In New Bounties Available"
elif old_bounties.count():
new_bounties = f"😁 {len(old_bounties)} Bounties Available"
if bounties:
plural_bounties = "Bounties" if len(bounties)>1 else "Bounty"
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_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}"

_and = "&& " if has_offer and new_bounties else ""
def comma(a):
return ", " if a and (new_bounties or new_quests or new_hackathons) else ""

subject = f"{offers} {_and}{new_bounties} "
subject = f"Gitcoin Daily {offers}{comma(offers)}{new_bounties}{comma(new_bounties)}{new_quests}{comma(new_quests)}{new_hackathons}"

for to_email in to_emails:
cur_language = translation.get_language()
try:
setup_lang(to_email)
from_email = settings.CONTACT_EMAIL
html, text = render_new_bounty(to_email, bounties, old_bounties, trending_quests=trending_quests())

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you planning on updating the subject line of this email? right now it advertises new bounties. it should probalby not anymore.

@frankchen07 any thoughts on a great subject line?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: (The?) Gitcoin Daily Digest

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PixelantDesign @frankchen07 any thoughts here? Gitcoin Daily Digest is good but rather generic. i wonder if we should test adding dynamic data to it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets go with "Gitcoin Daily (x Bounties, y quests/ z hacakthons)" where each variable is the number of each in the email

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 03137b3

from django.contrib.auth.models import User
user = User.objects.get(email__iexact=to_email)
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)

if not should_suppress_notification_email(to_email, 'new_bounty_notifications'):
send_mail(from_email, to_email, subject, text, html, categories=['marketing', func_name()])
Expand Down
33 changes: 30 additions & 3 deletions app/marketing/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
from cacheops import cached_view
from chartit import PivotChart, PivotDataPool
from chat.tasks import update_chat_notifications
from dashboard.models import Profile, TokenApproval
from dashboard.models import Profile, TokenApproval, HackathonEvent, Activity
from dashboard.utils import create_user_action, get_orgs_perms, is_valid_eth_address
from enssubdomain.models import ENSSubdomainRegistration
from gas.utils import recommend_min_gas_price_to_confirm_in_time
Expand All @@ -52,6 +52,7 @@
from marketing.models import AccountDeletionRequest, EmailSubscriber, Keyword, LeaderboardRank
from marketing.utils import delete_user_from_mailchimp, get_or_save_email_subscriber, validate_slack_integration
from quests.models import Quest
from grants.models import Grant
from retail.emails import ALL_EMAILS, render_new_bounty, render_nth_day_email_campaign
from retail.helpers import get_ip

Expand Down Expand Up @@ -983,12 +984,38 @@ def trending_quests():
).order_by('-recent_attempts').all()[0:10]
return quests

def quest_of_the_day():
quest = trending_quests()[0]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this guaranteed to be different day over day? id be annoyed if i got the same quest every day

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, trending_quests() was actually written and approved in #6080. quest_of_the_day() only pulls the first of its results

Copy link
Contributor Author

@sebastiantf sebastiantf May 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And trending_quests() actually filters the quests by number of recent attempts and creation date.

Wouldn't number of recent attempts make the trending quests different each day?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think using the upstream changes is fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will rebase on upstream master and fix merge conflicts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

return quest

def upcoming_grant():
grant = Grant.objects.order_by('-weighted_shuffle').first()
return grant

def upcoming_hackathon():
try:
return HackathonEvent.objects.filter(end_date__gt=timezone.now()).order_by('-start_date')
except HackathonEvent.DoesNotExist:
try:
return [HackathonEvent.objects.filter(start_date__gte=timezone.now()).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)
activities = get_specific_activities('connect', 0, user, 0)[:4]
return activities

@staff_member_required
def new_bounty_daily_preview(request):
profile = request.user.profile
keywords = profile.keywords
hours_back = 2000

new_bounties, all_bounties = get_bounties_for_keywords(keywords, hours_back)
quests = trending_quests()
response_html, _ = render_new_bounty('[email protected]', new_bounties, all_bounties, offset=3, trending_quests=quests)
max_bounties = 5
if len(new_bounties) > max_bounties:
new_bounties = new_bounties[0:max_bounties]
response_html, _ = render_new_bounty(settings.CONTACT_EMAIL, new_bounties, old_bounties='', offset=3, quest_of_the_day=quest_of_the_day(), upcoming_grant=upcoming_grant(), upcoming_hackathon=upcoming_hackathon(), latest_activities=latest_activities(request.user))
return HttpResponse(response_html)
53 changes: 47 additions & 6 deletions app/retail/emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,10 +529,47 @@ 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, trending_quests=[]):
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
email_style = (int(timezone.now().strftime("%-j")) + offset) % 24
from dashboard.models import Profile
from inbox.models import Notification
sub = get_or_save_email_subscriber(to_email, 'internal')

email_style = 26

# Get notifications count from the Profile.User of to_email
try:
profile = Profile.objects.filter(email__iexact=to_email).last()
except Profile.DoesNotExist:
pass
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we want to pass here instead of failing?

Copy link
Contributor Author

@sebastiantf sebastiantf May 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems all emails that come at to_email need not have a profile: #6471 (comment) @owocki. So if we fail here the email won't be send. Correct?
Reference: #6471 (comment)


from_date = from_date + timedelta(days=1)
to_date = from_date - timedelta(days=days_ago)

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

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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the CLR calc date being used here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the Save The Date section is supposed to display upcoming dates, I thought next_clr_calc_date would be later than the time of creation of the email and created_on of the grant. So display next_clr_calc_date if it exists or creation date if it doesn't

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should use grants.views.after_that_next_round_begin instead probably this is just the clr calc date

})
if upcoming_hackathon:
for hackathon in upcoming_hackathon:
upcoming_events.append({
'event': hackathon,
'title': hackathon.name,
'image_url': hackathon.logo.url if hackathon.logo else f'{settings.STATIC_URL}v2/images/emails/hackathons-neg.png',
'url': hackathon.url,
'date': hackathon.start_date.strftime("%Y-%d-%m")
})

params = {
'old_bounties': old_bounties,
'bounties': bounties,
Expand All @@ -542,7 +579,10 @@ def render_new_bounty(to_email, bounties, old_bounties, offset=3, trending_quest
'email_type': 'new_bounty_notifications',
'base_url': settings.BASE_URL,
'show_action': True,
'trending_quests': trending_quests,
'quest_of_the_day': quest_of_the_day,
'upcoming_events': upcoming_events,
'activities': latest_activities,
'notifications_count': notifications_count,
'show_action': is_email_townsquare_enabled(to_email) and is_there_an_action_available()
}

Expand All @@ -560,7 +600,7 @@ def render_unread_notification_email_weekly_roundup(to_email, from_date=date.tod
from_date = from_date + timedelta(days=1)
to_date = from_date - timedelta(days=days_ago)

notifications = Notification.objects.filter(to_user=profile.id, is_read=False, created_on__range=[to_date, from_date]).count()
notifications = Notification.objects.filter(to_user=profile.user.id, is_read=False, created_on__range=[to_date, from_date]).count()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch here! 👍


params = {
'subscriber': subscriber,
Expand Down Expand Up @@ -1146,7 +1186,7 @@ def weekly_recap(request):

@staff_member_required
def unread_notification_email_weekly_roundup(request):
response_html, _ = render_unread_notification_email_weekly_roundup('[email protected]')
response_html, _, _ = render_unread_notification_email_weekly_roundup('[email protected]')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we render this email such that we use the requester's email rather than hardcode it?

Copy link
Contributor Author

@sebastiantf sebastiantf May 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded email was actually already there:

response_html, _ = render_unread_notification_email_weekly_roundup('[email protected]')

response_html, _, _ is what I actually wanted to add because it's part of the bugfix above. See commit 18d165d

Could change it to:

response_html, _, _ = render_unread_notification_email_weekly_roundup(request.user.email)

Should I?

return HttpResponse(response_html)

@staff_member_required
Expand Down Expand Up @@ -1224,9 +1264,10 @@ def resend_new_tip(request):
@staff_member_required
def new_bounty(request):
from dashboard.models import Bounty
from marketing.views import quest_of_the_day, upcoming_grant, upcoming_hackathon, latest_activities
bounties = Bounty.objects.current().order_by('-web3_created')[0:3]
old_bounties = Bounty.objects.current().order_by('-web3_created')[0:3]
response_html, _ = render_new_bounty(settings.CONTACT_EMAIL, bounties, old_bounties, int(request.GET.get('offset', 2)))
response_html, _ = render_new_bounty(settings.CONTACT_EMAIL, bounties, old_bounties='', offset=int(request.GET.get('offset', 2)), quest_of_the_day=quest_of_the_day(), upcoming_grant=upcoming_grant(), upcoming_hackathon=upcoming_hackathon(), latest_activities=latest_activities(request.user))
return HttpResponse(response_html)


Expand Down
Loading