From 027311cdc58b251a3db1a06c971ebdfba488ae10 Mon Sep 17 00:00:00 2001 From: Owocki Date: Wed, 30 Jan 2019 11:48:59 -0700 Subject: [PATCH 01/24] make contribur landing page performant --- .../migrations/0010_auto_20190130_1822.py | 29 +++++ app/dashboard/models.py | 48 +++++--- app/economy/models.py | 19 +++- .../migrations/0008_auto_20190130_1822.py | 18 +++ .../management/commands/create_page_cache.py | 104 ++++++++++++++++++ .../management/commands/create_results.py | 66 ----------- app/retail/templates/shared/activity.html | 2 +- app/retail/views.py | 30 +++-- scripts/crontab | 2 +- 9 files changed, 223 insertions(+), 95 deletions(-) create mode 100644 app/dashboard/migrations/0010_auto_20190130_1822.py create mode 100644 app/grants/migrations/0008_auto_20190130_1822.py create mode 100644 app/perftools/management/commands/create_page_cache.py delete mode 100644 app/perftools/management/commands/create_results.py diff --git a/app/dashboard/migrations/0010_auto_20190130_1822.py b/app/dashboard/migrations/0010_auto_20190130_1822.py new file mode 100644 index 00000000000..c78e2eec476 --- /dev/null +++ b/app/dashboard/migrations/0010_auto_20190130_1822.py @@ -0,0 +1,29 @@ +# Generated by Django 2.1.2 on 2019-01-30 18:22 + +from django.db import migrations, models +import economy.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0009_merge_20190122_0857'), + ] + + operations = [ + migrations.AddField( + model_name='activity', + name='created_on', + field=models.DateTimeField(db_index=True, default=economy.models.get_time), + ), + migrations.AddField( + model_name='activity', + name='modified_on', + field=models.DateTimeField(default=economy.models.get_time), + ), + migrations.AlterField( + model_name='profile', + name='job_search_status', + field=models.CharField(blank=True, choices=[('AL', 'Actively looking for work'), ('PL', 'Passively looking and open to hearing new opportunities'), ('N', 'Not open to hearing new opportunities')], max_length=2), + ), + ] diff --git a/app/dashboard/models.py b/app/dashboard/models.py index 28a6c13fa4a..ce850c95df2 100644 --- a/app/dashboard/models.py +++ b/app/dashboard/models.py @@ -1512,7 +1512,7 @@ def escalated_for_removal(self): ) -class Activity(models.Model): +class Activity(SuperModel): """Represent Start work/Stop work event. Attributes: @@ -1603,27 +1603,39 @@ def view_props(self): 'new_kudos': 'fa-thumbs-up', } - activity = self - activity.icon = icons.get(activity.activity_type, 'fa-check-circle') - if activity.kudos: - activity.kudos_data = Token.objects.get(pk=activity.kudos.kudos_token_cloned_from_id) - obj = activity.metadata - if 'new_bounty' in activity.metadata: - obj = activity.metadata['new_bounty'] - activity.title = obj.get('title', '') + properties = [ + 'i18n_name' + 'title', + 'token_name', + 'created_human_time', + ] + activity = self.to_standard_dict(properties=properties) + for key, value in model_to_dict(self).items(): + activity[key] = value + for fk in ['bounty', 'tip', 'kudos', 'profile']: + if getattr(self, fk): + activity[fk] = getattr(self, fk).to_standard_dict(properties=properties) + + activity['icon'] = icons.get(self.activity_type, 'fa-check-circle') + if activity.get('kudos'): + activity['kudos_data'] = Token.objects.get(pk=self.kudos.kudos_token_cloned_from_id) + obj = self.metadata + if 'new_bounty' in self.metadata: + obj = self.metadata['new_bounty'] + activity['title'] = obj.get('title', '') if 'id' in obj: - activity.bounty_url = Bounty.objects.get(pk=obj['id']).get_relative_url() - if activity.title: - activity.urled_title = f'{activity.title}' + activity['bounty_url'] = Bounty.objects.get(pk=obj['id']).get_relative_url() + if activity.get('title'): + activity['urled_title'] = f'{activity["title"]}' else: - activity.urled_title = activity.title + activity['urled_title'] = activity.title if 'value_in_usdt_now' in obj: - activity.value_in_usdt_now = obj['value_in_usdt_now'] + activity['value_in_usdt_now'] = obj['value_in_usdt_now'] if 'token_name' in obj: - activity.token = token_by_name(obj['token_name']) - if 'value_in_token' in obj and activity.token: - activity.value_in_token_disp = round((float(obj['value_in_token']) / - 10 ** activity.token['decimals']) * 1000) / 1000 + activity['token'] = token_by_name(obj['token_name']) + if 'value_in_token' in obj and activity['token']: + activity['value_in_token_disp'] = round((float(obj['value_in_token']) / + 10 ** activity['token']['decimals']) * 1000) / 1000 return activity @property diff --git a/app/economy/models.py b/app/economy/models.py index 758b447b68d..88b74c85d4a 100644 --- a/app/economy/models.py +++ b/app/economy/models.py @@ -53,7 +53,7 @@ def save(self, *args, **kwargs): self.modified_on = get_time() return super(SuperModel, self).save(*args, **kwargs) - def to_standard_dict(self, fields=None, exclude=None): + def to_standard_dict(self, fields=None, exclude=None, properties=None): """Define the standard to dict representation of the object. Args: @@ -72,7 +72,16 @@ def to_standard_dict(self, fields=None, exclude=None): kwargs['fields'] = fields if exclude: kwargs['exclude'] = exclude - return model_to_dict(self, **kwargs) + return_me = model_to_dict(self, **kwargs) + if properties: + for key in dir(self): + if key in properties: + attr = getattr(self, key) + if callable(attr): + return_me[key] = attr() + else: + return_me[key] = attr + return return_me @property @@ -80,6 +89,12 @@ def admin_url(self): url = reverse('admin:{0}_{1}_change'.format(self._meta.app_label, self._meta.model_name), args=[self.id]) return '{0}'.format(url, escape(str(self))) + @property + def created_human_time(self): + from django.contrib.humanize.templatetags.humanize import naturaltime + return naturaltime(self.created_on) + + class ConversionRate(SuperModel): """Define the conversion rate model.""" diff --git a/app/grants/migrations/0008_auto_20190130_1822.py b/app/grants/migrations/0008_auto_20190130_1822.py new file mode 100644 index 00000000000..df1f1b452a8 --- /dev/null +++ b/app/grants/migrations/0008_auto_20190130_1822.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.2 on 2019-01-30 18:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('grants', '0007_grant_image_css'), + ] + + operations = [ + migrations.AlterField( + model_name='grant', + name='image_css', + field=models.CharField(blank=True, default='', help_text='additional CSS to attach to the grant-banner img.', max_length=255), + ), + ] diff --git a/app/perftools/management/commands/create_page_cache.py b/app/perftools/management/commands/create_page_cache.py new file mode 100644 index 00000000000..c6c6e6fa72a --- /dev/null +++ b/app/perftools/management/commands/create_page_cache.py @@ -0,0 +1,104 @@ +''' + Copyright (C) 2017 Gitcoin Core + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +''' + +import json + +from django.core.management.base import BaseCommand +from django.core.serializers.json import DjangoJSONEncoder +from django.db import transaction, models +from django.db.models.query import QuerySet +from django.forms.models import model_to_dict +from django.utils.encoding import force_text +from django.utils.functional import Promise +from economy.models import SuperModel +from perftools.models import JSONStore +from retail.utils import build_stat_results, programming_languages + + +class LazyEncoder(DjangoJSONEncoder): + def default(self, obj): + if isinstance(obj, Promise): + return force_text(obj) + if isinstance(obj, SuperModel): + return (obj.to_standard_dict()) + if isinstance(obj, models.Model): + return (model_to_dict(obj)) + if isinstance(obj, models.Model): + return (model_to_dict(obj)) + if isinstance(obj, QuerySet): + if obj.count() and type(obj.first()) == str: + return obj[::1] + return [LazyEncoder(instance) for instance in obj] + if isinstance(obj, list): + return [LazyEncoder(instance) for instance in obj] + if(callable(obj)): + return None + return super(LazyEncoder, self).default(obj) + +def create_results_cache(): + print('results') + keywords = [''] + programming_languages + view = 'results' + with transaction.atomic(): + items = [] + JSONStore.objects.filter(view=view).all().delete() + for keyword in keywords: + key = keyword + print(f"- executing {keyword}") + data = build_stat_results(keyword) + print("- creating") + items.append(JSONStore( + view=view, + key=key, + data=json.loads(json.dumps(data, cls=LazyEncoder)), + )) + JSONStore.objects.bulk_create(items) + + +def create_contributor_landing_page_context(): + print('create_contributor_landing_page_context') + keywords = [''] + programming_languages + # TODO + keywords = [''] + view = 'contributor_landing_page' + from retail.views import get_contributor_landing_page_context + with transaction.atomic(): + items = [] + JSONStore.objects.filter(view=view).all().delete() + for keyword in keywords: + key = keyword + print(f"- executing {keyword}") + data = get_contributor_landing_page_context(keyword) + print("- creating") + items.append(JSONStore( + view=view, + key=key, + data=json.loads(json.dumps(data, cls=LazyEncoder)), + )) + JSONStore.objects.bulk_create(items) + + + +class Command(BaseCommand): + + help = 'generates some /results data' + + def handle(self, *args, **options): + # TODO + # create_results_cache() + create_contributor_landing_page_context() diff --git a/app/perftools/management/commands/create_results.py b/app/perftools/management/commands/create_results.py deleted file mode 100644 index 930501a2f11..00000000000 --- a/app/perftools/management/commands/create_results.py +++ /dev/null @@ -1,66 +0,0 @@ -''' - Copyright (C) 2017 Gitcoin Core - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -''' - -import json - -from django.core.management.base import BaseCommand -from django.core.serializers.json import DjangoJSONEncoder -from django.db import transaction -from django.db.models.query import QuerySet -from django.forms.models import model_to_dict -from django.utils.encoding import force_text -from django.utils.functional import Promise - -from perftools.models import JSONStore -from retail.utils import build_stat_results, programming_languages - - -class LazyEncoder(DjangoJSONEncoder): - def default(self, obj): - if isinstance(obj, Promise): - return force_text(obj) - if isinstance(obj, QuerySet): - if obj.count() and type(obj.first()) == str: - return obj[::1] - return [model_to_dict(instance) for instance in obj] - return super(LazyEncoder, self).default(obj) - - -class Command(BaseCommand): - - help = 'generates some /results data' - - def handle(self, *args, **options): - keywords = [''] + programming_languages - # DEBUG OPTIONS - # keywords = [''] - view = 'results' - with transaction.atomic(): - items = [] - JSONStore.objects.filter(view=view).all().delete() - for keyword in keywords: - key = keyword - print(f"- executing {keyword}") - data = build_stat_results(keyword) - print("- creating") - items.append(JSONStore( - view=view, - key=key, - data=json.loads(json.dumps(data, cls=LazyEncoder)), - )) - JSONStore.objects.bulk_create(items) diff --git a/app/retail/templates/shared/activity.html b/app/retail/templates/shared/activity.html index 86388d74b98..4a4c20a7b5e 100644 --- a/app/retail/templates/shared/activity.html +++ b/app/retail/templates/shared/activity.html @@ -79,7 +79,7 @@

- {{ row.created | naturaltime }} + {{ row.created_human_time }} diff --git a/app/retail/views.py b/app/retail/views.py index d6de16b3155..c38c015f732 100644 --- a/app/retail/views.py +++ b/app/retail/views.py @@ -228,26 +228,42 @@ def contributor_bounties(request, tech_stack): } ] - available_bounties_count = open_bounties().count() - available_bounties_worth = amount_usdt_open_work() # tech_stack = '' #uncomment this if you wish to disable contributor specific LPs context = { - 'activities': get_activities(tech_stack), - 'title': tech_stack.title() + str(_(" Open Source Opportunities")) if tech_stack else "Open Source Opportunities", 'slides': slides, 'slideDurationInMs': 6000, 'active': 'home', 'newsletter_headline': _("Be the first to find out about newly posted bounties."), 'hide_newsletter_caption': True, 'hide_newsletter_consent': True, - 'projects': projects, 'gitcoin_description': gitcoin_description, + 'projects': projects, + } + + try: + new_context = JSONStore.objects.get(view='contributor_landing_page', key=tech_stack).data + + for key, value in new_context.items(): + context[key] = value + except Exception as e: + print(e) + raise Http404 + + return TemplateResponse(request, 'bounties/contributor.html', context) + + +def get_contributor_landing_page_context(tech_stack): + available_bounties_count = open_bounties().count() + available_bounties_worth = amount_usdt_open_work() + activities = get_activities(tech_stack) + return { + 'activities': activities, + 'title': tech_stack.title() + str(_(" Open Source Opportunities")) if tech_stack else "Open Source Opportunities", 'available_bounties_count': available_bounties_count, 'available_bounties_worth': available_bounties_worth, 'tech_stack': tech_stack, - } - return TemplateResponse(request, 'bounties/contributor.html', context) + } def how_it_works(request, work_type): diff --git a/scripts/crontab b/scripts/crontab index 800377e18ca..edfbc87b8f5 100644 --- a/scripts/crontab +++ b/scripts/crontab @@ -62,7 +62,7 @@ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/us 0 * * * * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash create_gas_history >> /var/log/gitcoin/create_gas_history.log 2>&1 -5 * * * * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash create_results >> /var/log/gitcoin/create_results.log 2>&1 +5 * * * * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash create_page_cache >> /var/log/gitcoin/create_page_cache.log 2>&1 10 * * * * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash create_d3_views >> /var/log/gitcoin/create_d3_views.log 2>&1 From 248ba884e4998089bc2dbbf671bc7602d68a5618 Mon Sep 17 00:00:00 2001 From: Owocki Date: Wed, 30 Jan 2019 11:52:27 -0700 Subject: [PATCH 02/24] make fix --- app/app/urls.py | 11 ++++++----- .../management/commands/create_page_cache.py | 3 ++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/app/urls.py b/app/app/urls.py index aef7c043984..f8a86f41ac3 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -114,13 +114,14 @@ path('revenue/attestations/new', revenue.views.new_attestation, name='revenue_new_attestation'), # action URLs - url(r'^$', retail.views.funder_bounties, name='funder_bounties'), # TODO : Update Path - re_path(r'^contributor/?(?P.*)/?', retail.views.contributor_bounties, name='contributor_bounties'), # TODO: Update Path + url(r'^$', retail.views.funder_bounties, name='funder_bounties'), # TODO : Update Path + re_path(r'^contributor/?(?P.*)/?', retail.views.contributor_bounties, + name='contributor_bounties'), # TODO: Update Path re_path(r'^bounty/quickstart/?', dashboard.views.quickstart, name='quickstart'), url(r'^bounty/new/?', dashboard.views.new_bounty, name='new_bounty'), re_path(r'^bounty/change/(?P.*)?', dashboard.views.change_bounty, name='change_bounty'), - url(r'^funding/new/?', dashboard.views.new_bounty, name='new_funding'), # TODO: Remove - url(r'^new/?', dashboard.views.new_bounty, name='new_funding_short'), # TODO: Remove + url(r'^funding/new/?', dashboard.views.new_bounty, name='new_funding'), # TODO: Remove + url(r'^new/?', dashboard.views.new_bounty, name='new_funding_short'), # TODO: Remove # TODO: Rename below to bounty/ path('issue/fulfill', dashboard.views.fulfill_bounty, name='fulfill_bounty'), path('issue/accept', dashboard.views.accept_bounty, name='process_funding'), @@ -223,7 +224,7 @@ re_path(r'^modal/extend_issue_deadline/?', dashboard.views.extend_issue_deadline, name='extend_issue_deadline'), # brochureware views - url(r'^homepage/$', retail.views.index, name='index'), # Update path to ^$ + url(r'^homepage/$', retail.views.index, name='index'), # Update path to ^$ re_path(r'^about/?', retail.views.about, name='about'), re_path(r'^mission/?', retail.views.mission, name='mission'), re_path(r'^vision/?', retail.views.vision, name='vision'), diff --git a/app/perftools/management/commands/create_page_cache.py b/app/perftools/management/commands/create_page_cache.py index c6c6e6fa72a..a0c0ea164e7 100644 --- a/app/perftools/management/commands/create_page_cache.py +++ b/app/perftools/management/commands/create_page_cache.py @@ -20,11 +20,12 @@ from django.core.management.base import BaseCommand from django.core.serializers.json import DjangoJSONEncoder -from django.db import transaction, models +from django.db import models, transaction from django.db.models.query import QuerySet from django.forms.models import model_to_dict from django.utils.encoding import force_text from django.utils.functional import Promise + from economy.models import SuperModel from perftools.models import JSONStore from retail.utils import build_stat_results, programming_languages From e5813e4f05d485c0d995dcb24dbdb7907530d119 Mon Sep 17 00:00:00 2001 From: Owocki Date: Wed, 30 Jan 2019 11:56:15 -0700 Subject: [PATCH 03/24] migrates everythign else to supermodel --- .../migrations/0011_auto_20190130_1852.py | 24 +++++++++++++ .../migrations/0012_auto_20190130_1853.py | 34 +++++++++++++++++++ app/dashboard/models.py | 6 ++-- .../migrations/0002_auto_20190130_1855.py | 24 +++++++++++++ app/gitcoinbot/models.py | 3 +- 5 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 app/dashboard/migrations/0011_auto_20190130_1852.py create mode 100644 app/dashboard/migrations/0012_auto_20190130_1853.py create mode 100644 app/gitcoinbot/migrations/0002_auto_20190130_1855.py diff --git a/app/dashboard/migrations/0011_auto_20190130_1852.py b/app/dashboard/migrations/0011_auto_20190130_1852.py new file mode 100644 index 00000000000..119178f8ffb --- /dev/null +++ b/app/dashboard/migrations/0011_auto_20190130_1852.py @@ -0,0 +1,24 @@ +# Generated by Django 2.1.2 on 2019-01-30 18:52 + +from django.db import migrations, models +import economy.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0010_auto_20190130_1822'), + ] + + operations = [ + migrations.AddField( + model_name='interest', + name='created_on', + field=models.DateTimeField(db_index=True, default=economy.models.get_time), + ), + migrations.AddField( + model_name='interest', + name='modified_on', + field=models.DateTimeField(default=economy.models.get_time), + ), + ] diff --git a/app/dashboard/migrations/0012_auto_20190130_1853.py b/app/dashboard/migrations/0012_auto_20190130_1853.py new file mode 100644 index 00000000000..5a86e47671e --- /dev/null +++ b/app/dashboard/migrations/0012_auto_20190130_1853.py @@ -0,0 +1,34 @@ +# Generated by Django 2.1.2 on 2019-01-30 18:53 + +from django.db import migrations, models +import economy.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0011_auto_20190130_1852'), + ] + + operations = [ + migrations.AddField( + model_name='labsresearch', + name='created_on', + field=models.DateTimeField(db_index=True, default=economy.models.get_time), + ), + migrations.AddField( + model_name='labsresearch', + name='modified_on', + field=models.DateTimeField(default=economy.models.get_time), + ), + migrations.AddField( + model_name='toolvote', + name='created_on', + field=models.DateTimeField(db_index=True, default=economy.models.get_time), + ), + migrations.AddField( + model_name='toolvote', + name='modified_on', + field=models.DateTimeField(default=economy.models.get_time), + ), + ] diff --git a/app/dashboard/models.py b/app/dashboard/models.py index ce850c95df2..00210903980 100644 --- a/app/dashboard/models.py +++ b/app/dashboard/models.py @@ -1418,7 +1418,7 @@ def warned(self): return self.filter(status=Interest.STATUS_WARNED) -class Interest(models.Model): +class Interest(SuperModel): """Define relationship for profiles expressing interest on a bounty.""" STATUS_REVIEW = 'review' @@ -1668,7 +1668,7 @@ def to_dict(self, fields=None, exclude=None): return model_to_dict(self, **kwargs) -class LabsResearch(models.Model): +class LabsResearch(SuperModel): """Define the structure of Labs Research object.""" title = models.CharField(max_length=255) @@ -2749,7 +2749,7 @@ def i18n_link_copy(self): return _(self.link_copy) -class ToolVote(models.Model): +class ToolVote(SuperModel): """Define the vote placed on a tool.""" profile = models.ForeignKey('dashboard.Profile', related_name='votes', on_delete=models.CASCADE) diff --git a/app/gitcoinbot/migrations/0002_auto_20190130_1855.py b/app/gitcoinbot/migrations/0002_auto_20190130_1855.py new file mode 100644 index 00000000000..c1a4b73ba8f --- /dev/null +++ b/app/gitcoinbot/migrations/0002_auto_20190130_1855.py @@ -0,0 +1,24 @@ +# Generated by Django 2.1.2 on 2019-01-30 18:55 + +from django.db import migrations, models +import economy.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('gitcoinbot', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='gitcoinbotresponses', + name='created_on', + field=models.DateTimeField(db_index=True, default=economy.models.get_time), + ), + migrations.AddField( + model_name='gitcoinbotresponses', + name='modified_on', + field=models.DateTimeField(default=economy.models.get_time), + ), + ] diff --git a/app/gitcoinbot/models.py b/app/gitcoinbot/models.py index 4fdaa144a51..e32f6ec57a2 100644 --- a/app/gitcoinbot/models.py +++ b/app/gitcoinbot/models.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- """Define Gitcoin Bot specific models.""" from django.db import models +from economy.models import SuperModel -class GitcoinBotResponses(models.Model): +class GitcoinBotResponses(SuperModel): """Define the Gitcoin Bot response model for recording bot request data.""" request = models.CharField(max_length=500, db_index=True, unique=True) response = models.CharField(max_length=500) From 0b94058e1b6dae859e2dc8fee326d7c1f2751557 Mon Sep 17 00:00:00 2001 From: Owocki Date: Wed, 30 Jan 2019 11:58:31 -0700 Subject: [PATCH 04/24] removes --- app/gitcoinbot/models.py | 1 + app/perftools/management/commands/create_page_cache.py | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/gitcoinbot/models.py b/app/gitcoinbot/models.py index e32f6ec57a2..7e0afa0f964 100644 --- a/app/gitcoinbot/models.py +++ b/app/gitcoinbot/models.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Define Gitcoin Bot specific models.""" from django.db import models + from economy.models import SuperModel diff --git a/app/perftools/management/commands/create_page_cache.py b/app/perftools/management/commands/create_page_cache.py index a0c0ea164e7..ad8ef48ed7a 100644 --- a/app/perftools/management/commands/create_page_cache.py +++ b/app/perftools/management/commands/create_page_cache.py @@ -18,6 +18,7 @@ import json +from django.conf import settings from django.core.management.base import BaseCommand from django.core.serializers.json import DjangoJSONEncoder from django.db import models, transaction @@ -54,6 +55,8 @@ def default(self, obj): def create_results_cache(): print('results') keywords = [''] + programming_languages + if settings.DEBUG: + keywords = [''] view = 'results' with transaction.atomic(): items = [] @@ -74,8 +77,8 @@ def create_results_cache(): def create_contributor_landing_page_context(): print('create_contributor_landing_page_context') keywords = [''] + programming_languages - # TODO - keywords = [''] + if settings.DEBUG: + keywords = [''] view = 'contributor_landing_page' from retail.views import get_contributor_landing_page_context with transaction.atomic(): @@ -101,5 +104,5 @@ class Command(BaseCommand): def handle(self, *args, **options): # TODO - # create_results_cache() + create_results_cache() create_contributor_landing_page_context() From 48f1c4da3700add501d39e45ba740b3903b74459 Mon Sep 17 00:00:00 2001 From: Owocki Date: Wed, 30 Jan 2019 12:01:55 -0700 Subject: [PATCH 05/24] encode anything --- app/dashboard/models.py | 7 +++++ app/economy/models.py | 21 +++++++++++++++ .../management/commands/create_page_cache.py | 26 +++---------------- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/app/dashboard/models.py b/app/dashboard/models.py index 00210903980..d21b0547711 100644 --- a/app/dashboard/models.py +++ b/app/dashboard/models.py @@ -1603,6 +1603,7 @@ def view_props(self): 'new_kudos': 'fa-thumbs-up', } + # load up this data package with all of the information in the already existing objects properties = [ 'i18n_name' 'title', @@ -1616,6 +1617,9 @@ def view_props(self): if getattr(self, fk): activity[fk] = getattr(self, fk).to_standard_dict(properties=properties) + # KO notes 2019/01/30 + # this is a bunch of bespoke information that is computed for the views + # in a later release, it couild be refactored such that its just contained in the above code block ^^. activity['icon'] = icons.get(self.activity_type, 'fa-check-circle') if activity.get('kudos'): activity['kudos_data'] = Token.objects.get(pk=self.kudos.kudos_token_cloned_from_id) @@ -1636,6 +1640,9 @@ def view_props(self): if 'value_in_token' in obj and activity['token']: activity['value_in_token_disp'] = round((float(obj['value_in_token']) / 10 ** activity['token']['decimals']) * 1000) / 1000 + + # finally done! + return activity @property diff --git a/app/economy/models.py b/app/economy/models.py index 88b74c85d4a..3996b2ba960 100644 --- a/app/economy/models.py +++ b/app/economy/models.py @@ -32,6 +32,27 @@ from django.utils.timezone import localtime +class EncodeAnything(DjangoJSONEncoder): + def default(self, obj): + if isinstance(obj, Promise): + return force_text(obj) + if isinstance(obj, SuperModel): + return (obj.to_standard_dict()) + if isinstance(obj, models.Model): + return (model_to_dict(obj)) + if isinstance(obj, models.Model): + return (model_to_dict(obj)) + if isinstance(obj, QuerySet): + if obj.count() and type(obj.first()) == str: + return obj[::1] + return [EncodeAnything(instance) for instance in obj] + if isinstance(obj, list): + return [EncodeAnything(instance) for instance in obj] + if(callable(obj)): + return None + return super(EncodeAnything, self).default(obj) + + def get_time(): """Get the local time.""" return localtime(timezone.now()) diff --git a/app/perftools/management/commands/create_page_cache.py b/app/perftools/management/commands/create_page_cache.py index ad8ef48ed7a..bc0c7baf117 100644 --- a/app/perftools/management/commands/create_page_cache.py +++ b/app/perftools/management/commands/create_page_cache.py @@ -27,31 +27,11 @@ from django.utils.encoding import force_text from django.utils.functional import Promise -from economy.models import SuperModel +from economy.models import EncodeAnything, SuperModel from perftools.models import JSONStore from retail.utils import build_stat_results, programming_languages -class LazyEncoder(DjangoJSONEncoder): - def default(self, obj): - if isinstance(obj, Promise): - return force_text(obj) - if isinstance(obj, SuperModel): - return (obj.to_standard_dict()) - if isinstance(obj, models.Model): - return (model_to_dict(obj)) - if isinstance(obj, models.Model): - return (model_to_dict(obj)) - if isinstance(obj, QuerySet): - if obj.count() and type(obj.first()) == str: - return obj[::1] - return [LazyEncoder(instance) for instance in obj] - if isinstance(obj, list): - return [LazyEncoder(instance) for instance in obj] - if(callable(obj)): - return None - return super(LazyEncoder, self).default(obj) - def create_results_cache(): print('results') keywords = [''] + programming_languages @@ -69,7 +49,7 @@ def create_results_cache(): items.append(JSONStore( view=view, key=key, - data=json.loads(json.dumps(data, cls=LazyEncoder)), + data=json.loads(json.dumps(data, cls=EncodeAnything)), )) JSONStore.objects.bulk_create(items) @@ -92,7 +72,7 @@ def create_contributor_landing_page_context(): items.append(JSONStore( view=view, key=key, - data=json.loads(json.dumps(data, cls=LazyEncoder)), + data=json.loads(json.dumps(data, cls=EncodeAnything)), )) JSONStore.objects.bulk_create(items) From 64fb6d5898bdd54c3506940dd756402997b2b527 Mon Sep 17 00:00:00 2001 From: Owocki Date: Wed, 30 Jan 2019 12:02:22 -0700 Subject: [PATCH 06/24] one last comment removal --- app/perftools/management/commands/create_page_cache.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/perftools/management/commands/create_page_cache.py b/app/perftools/management/commands/create_page_cache.py index bc0c7baf117..4ca5c296fe2 100644 --- a/app/perftools/management/commands/create_page_cache.py +++ b/app/perftools/management/commands/create_page_cache.py @@ -83,6 +83,5 @@ class Command(BaseCommand): help = 'generates some /results data' def handle(self, *args, **options): - # TODO create_results_cache() create_contributor_landing_page_context() From f8b0b1b0c7d70c6adb9e755867176c76f4f3c5d2 Mon Sep 17 00:00:00 2001 From: Onuwa Nnachi Isaac Date: Fri, 15 Feb 2019 13:14:23 +0100 Subject: [PATCH 07/24] Add campaign email feature --- app/app/urls.py | 2 + app/marketing/apps.py | 3 + app/marketing/mails.py | 21 ++++++- .../management/commands/campaign_email.py | 63 +++++++++++++++++++ app/marketing/signals.py | 33 ++++++++++ app/marketing/views.py | 11 +++- app/retail/emails.py | 19 ++++++ .../campaigns/email_campaign_day_1.html | 0 .../emails/campaigns/email_campaign_day_1.txt | 19 ++++++ .../campaigns/email_campaign_day_2.html | 0 .../emails/campaigns/email_campaign_day_2.txt | 10 +++ .../campaigns/email_campaign_day_3.html | 0 .../emails/campaigns/email_campaign_day_3.txt | 16 +++++ app/retail/views.py | 4 +- scripts/crontab | 1 + 15 files changed, 197 insertions(+), 5 deletions(-) create mode 100644 app/marketing/management/commands/campaign_email.py create mode 100644 app/marketing/signals.py create mode 100644 app/retail/templates/emails/campaigns/email_campaign_day_1.html create mode 100644 app/retail/templates/emails/campaigns/email_campaign_day_1.txt create mode 100644 app/retail/templates/emails/campaigns/email_campaign_day_2.html create mode 100644 app/retail/templates/emails/campaigns/email_campaign_day_2.txt create mode 100644 app/retail/templates/emails/campaigns/email_campaign_day_3.html create mode 100644 app/retail/templates/emails/campaigns/email_campaign_day_3.txt diff --git a/app/app/urls.py b/app/app/urls.py index a98e7daa888..716f7f5db9f 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -378,7 +378,9 @@ path('_administration/email/gdpr_reconsent', retail.emails.gdpr_reconsent, name='gdpr_reconsent'), path('_administration/email/share_bounty', retail.emails.share_bounty, name='share_bounty'), path('_administration/email/new_tip/resend', retail.emails.resend_new_tip, name='resend_new_tip'), + path('_administration/email/day_email_campaign/', marketing.views.day_email_campaign, name='day_email_campaign'), re_path( + r'^_administration/process_accesscode_request/(.*)$', tdi.views.process_accesscode_request, name='process_accesscode_request' diff --git a/app/marketing/apps.py b/app/marketing/apps.py index 4635c48ec67..a137811d977 100644 --- a/app/marketing/apps.py +++ b/app/marketing/apps.py @@ -24,3 +24,6 @@ class MarketingConfig(AppConfig): name = 'marketing' + + def ready(self): + from .signals import create_email_subscriber diff --git a/app/marketing/mails.py b/app/marketing/mails.py index 73a1322ba74..3fab8084aba 100644 --- a/app/marketing/mails.py +++ b/app/marketing/mails.py @@ -38,7 +38,7 @@ 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_thank_you_for_supporting_email, - render_tip_email, render_weekly_recap, + render_tip_email, render_weekly_recap, render_nth_day_email_campaign ) from sendgrid.helpers.mail import Content, Email, Mail, Personalization from sendgrid.helpers.stats import Category @@ -545,6 +545,25 @@ def reject_faucet_request(fr): finally: translation.activate(cur_language) +def nth_day_email_campaign(nth, subscriber): + + firstname = subscriber.email.split('@')[0] + + if subscriber.profile and subscriber.profile.user and subscriber.profile.user.first_name: + firstname = subscriber.profile.user.first_name + + if should_suppress_notification_email(subscriber.email, 'roundup'): + return False + + cur_language = translation.get_language() + try: + setup_lang(subscriber.email) + from_email = settings.CONTACT_EMAIL + if not should_suppress_notification_email(subscriber.email, 'welcome_mail'): + html, text, subject, = render_nth_day_email_campaign(subscriber.email, nth, firstname) + send_mail(from_email, subscriber.email, subject, text, html) + finally: + translation.activate(cur_language) def new_bounty_daily(bounties, old_bounties, to_emails=None): if not bounties: diff --git a/app/marketing/management/commands/campaign_email.py b/app/marketing/management/commands/campaign_email.py new file mode 100644 index 00000000000..254e5e9df13 --- /dev/null +++ b/app/marketing/management/commands/campaign_email.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +''' + Copyright (C) 2018 Gitcoin Core + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +''' +from datetime import datetime + +from django.core.management.base import BaseCommand +from django.utils import timezone + +from marketing.mails import nth_day_email_campaign +from marketing.models import EmailSubscriber + + +def n_days_ago(n): + before = timezone.now() - timezone.timedelta(days=n) + return datetime( + before.year, before.month, before.day, + 0, 0, 0, 0, + tzinfo=timezone.get_current_timezone()) + + +def send_nth_email_to_subscriber(nth, sub): + first_email = EmailSubscriber.objects.filter(email__iexact=sub.email).order_by('created_on').first() + if first_email.id == sub.id: + # it is the first time this subscriber is in our system + # send email to him/her + nth_day_email_campaign(nth, sub) + + +def send_nth_email(nth): + print('sending day {} email'.format(nth)) + # query all new subscribers created + # may contain duplicated email addresses with different source + subs = EmailSubscriber.objects.filter( + created_on__gt=n_days_ago(nth), + created_on__lte=n_days_ago(nth-1)) + for sub in subs: + send_nth_email_to_subscriber(nth, sub) + + +class Command(BaseCommand): + + help = 'Send marketing email to new subscribers' + + def handle(self, *args, **options): + # day 1 email has been sent + # start from day 2 + for i in range(2, 6): + send_nth_email(i) diff --git a/app/marketing/signals.py b/app/marketing/signals.py new file mode 100644 index 00000000000..9163f34ef8a --- /dev/null +++ b/app/marketing/signals.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +''' + Copyright (C) 2018 Gitcoin Core + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +''' + +from django.db.models.signals import post_save +from django.dispatch import receiver + +from marketing.mails import nth_day_email_campaign +from marketing.models import EmailSubscriber + + +@receiver(post_save, sender=EmailSubscriber) +def create_email_subscriber(sender, instance, created, **kwargs): + if created: + if not EmailSubscriber.objects.filter(email=instance.email).exclude(id=instance.id).exists(): + # this subscriber is the first time shown in our db + # send email + nth_day_email_campaign(1, instance) diff --git a/app/marketing/views.py b/app/marketing/views.py index 76f6facc503..c8a44f38c06 100644 --- a/app/marketing/views.py +++ b/app/marketing/views.py @@ -26,7 +26,7 @@ from django.contrib import messages from django.core.validators import validate_email from django.db.models import Max -from django.http import Http404 +from django.http import Http404, HttpResponse from django.shortcuts import redirect from django.template.response import TemplateResponse from django.urls import reverse @@ -44,7 +44,7 @@ from marketing.mails import new_feedback from marketing.models import AccountDeletionRequest, EmailSubscriber, Keyword, LeaderboardRank from marketing.utils import get_or_save_email_subscriber, validate_discord_integration, validate_slack_integration -from retail.emails import ALL_EMAILS +from retail.emails import ALL_EMAILS, render_nth_day_email_campaign from retail.helpers import get_ip logger = logging.getLogger(__name__) @@ -748,3 +748,10 @@ def leaderboard(request, key=''): } return TemplateResponse(request, 'leaderboard.html', context) + +@staff_member_required +def day_email_campaign(request, day): + if day not in list(range(1, 4)): + raise Http404 + response_html, _, _, = render_nth_day_email_campaign('foo@bar.com', day, 'staff_member') + return HttpResponse(response_html) \ No newline at end of file diff --git a/app/retail/emails.py b/app/retail/emails.py index d261be1363d..403c4c21653 100644 --- a/app/retail/emails.py +++ b/app/retail/emails.py @@ -43,6 +43,7 @@ # key, name, frequency MARKETING_EMAILS = [ + ('welcome_mail', _('Welcome Emails'), _('First 6 days after you sign up')), ('roundup', _('Roundup Emails'), _('Weekly')), ('new_bounty_notifications', _('New Bounty Notification Emails'), _('(up to) Daily')), ('important_product_updates', _('Product Update Emails'), _('Quarterly')), @@ -69,6 +70,24 @@ def premailer_transform(html): return p.transform() +def render_nth_day_email_campaign(to_email, nth, firstname): + subject_map = { + 1: "Day 1: Growing Open Source", + 2: "Day 2: Using Gitcoin's Issue Explorer", + 3: "Learning Blockchain" + } + + subject = subject_map[nth] + + params = { + "firstname": firstname, + "subscriber": get_or_save_email_subscriber(to_email, "internal"), + } + response_html = premailer_transform(render_to_string("emails/campaigns/email_campaign_day_{nth}.html", params)) + response_txt = render_to_string("emails/campaigns/email_campaign_day_{nth}.txt", params) + + return response_html, response_txt, subject + def render_new_grant_email(grant): params = {'grant': grant} response_html = premailer_transform(render_to_string("emails/grants/new_grant.html", params)) diff --git a/app/retail/templates/emails/campaigns/email_campaign_day_1.html b/app/retail/templates/emails/campaigns/email_campaign_day_1.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/retail/templates/emails/campaigns/email_campaign_day_1.txt b/app/retail/templates/emails/campaigns/email_campaign_day_1.txt new file mode 100644 index 00000000000..4f7d033b5b8 --- /dev/null +++ b/app/retail/templates/emails/campaigns/email_campaign_day_1.txt @@ -0,0 +1,19 @@ +Day 1: Growing Open Source + + +Welcome to Gitcoin! Our mission is to grow open source. Open source software is a force for good in the world, and we believe it’s the key to saving the internet. While a generation of software careers has been powered by open source software, it’s still hard to make a living working on open source. We plan to bring change in Web 3. + +The Issue Explorer is our first tool towards this end. Use it to find paid issues to work on in open source. Help build projects, ship code, and get paid in crypto tokens. Give it a try and let us know what you think! + +As time passes, we’ll send you more ways to get involved in Gitcoin. For now, we have a few areas you can nerd out with us, depending on where you’re most comfortable. + +Join the Slack channel and say hi on #community-intros! + +Follow us on Twitter for updates on open source software. + +Add the Gitcoin Livestream to your cal (iCal, Google) for Friday at 5PM EST. We’ll have two weekly product demo’s from the blockchain & open source ecosystems (like Decentraland and Meta Transactions with Austin Griffith. + +Oh, and here’s a kudos for getting to the bottom of this e-mail :) welcome to the Gitcoin Family! + +See you on the interwebs, + diff --git a/app/retail/templates/emails/campaigns/email_campaign_day_2.html b/app/retail/templates/emails/campaigns/email_campaign_day_2.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/retail/templates/emails/campaigns/email_campaign_day_2.txt b/app/retail/templates/emails/campaigns/email_campaign_day_2.txt new file mode 100644 index 00000000000..d04c31a710f --- /dev/null +++ b/app/retail/templates/emails/campaigns/email_campaign_day_2.txt @@ -0,0 +1,10 @@ +Day 2: Finding a Gitcoin issue for you + +By this point, you might have poked around the Gitcoin Issue Explorer to see the open source projects you could make money by contributing! If interested in doing work, here’s how. + +Start Work +Click ‘Express Interest’ to provide a brief message to the repo-maintainer that you’re interested in the issue. + +From here, you’ll receive an email and a notification when the issue is ready for you to work. You’ll be able to submit a PR directly onto the repo maintainer’s project from there. It’s that simple! + +CTA: Check out the Issue Explorer if you haven’t already! If you don’t find good fits, let us know an open source project that you’d like to see using Gitcoin. No promises, but we might have something up our sleeve. \ No newline at end of file diff --git a/app/retail/templates/emails/campaigns/email_campaign_day_3.html b/app/retail/templates/emails/campaigns/email_campaign_day_3.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/retail/templates/emails/campaigns/email_campaign_day_3.txt b/app/retail/templates/emails/campaigns/email_campaign_day_3.txt new file mode 100644 index 00000000000..21fe1ffb58c --- /dev/null +++ b/app/retail/templates/emails/campaigns/email_campaign_day_3.txt @@ -0,0 +1,16 @@ +Day 3: Leveraging the community + +Gitcoin Core is 11 team members, yet we’ve seen contributions from well over 100 developers using Gitcoin. We’d like to see you do the same! Here’s how to leverage a firehose of developer talent to do more, faster. + +Posting a bounty can be done in 90 seconds, once you know Gitcoin is an option to accelerate your development. Adding bounties to an existing project is like adding icing to a cake. We take what’s already great about OSS (mission, freedom, and great people) and add financial incentivization, a home base for projects seeking contributors, funding for developer, and community. + +Alongside posting bounties on the Issue Explorer, Gitcoin offers two complimentary products for open source software enthusiasts. + +Codefund: Sustain your OSS project with ethical advertising + +Gitcoin Kudos: Have a contributor that’s doing fantastic work? Send them a token of appreciation using Kudos! Here’s one for you :) + +Gitcoin Grants: Recurring funding for open source projects. + +Community is what makes Open Source work. A strong OSS community is inclusive, helpful, and mission-oriented. The Gitcoin community is 10,000 software developers who are focused on the tooling for the new world. Is there a chance that your next career connection could come from the Gitcoin community? Come find out! + diff --git a/app/retail/views.py b/app/retail/views.py index cb0e6bb29d6..8fbf1b3bfc4 100644 --- a/app/retail/views.py +++ b/app/retail/views.py @@ -38,7 +38,7 @@ from dashboard.models import Activity, Profile from dashboard.notifications import amount_usdt_open_work, open_bounties from economy.models import Token -from marketing.mails import new_funding_limit_increase_request, new_token_request +from marketing.mails import new_funding_limit_increase_request, new_token_request, render_nth_day_email_campaign from marketing.models import Alumni, LeaderboardRank from marketing.utils import get_or_save_email_subscriber, invite_to_slack from perftools.models import JSONStore @@ -1447,4 +1447,4 @@ def increase_funding_limit_request(request): 'card_desc': _('Do you hit the Funding Limit? Request a increasement!') } - return TemplateResponse(request, 'increase_funding_limit_request_form.html', params) + return TemplateResponse(request, 'increase_funding_limit_request_form.html', params) \ No newline at end of file diff --git a/scripts/crontab b/scripts/crontab index afdaa6fc831..cedf09b3277 100644 --- a/scripts/crontab +++ b/scripts/crontab @@ -54,6 +54,7 @@ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/us 15 10 * * * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash bounty_feedback_email >> /var/log/gitcoin/bounty_feedback_email.log 2>&1 15 10 * * * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash funder_stale_email 30 >> /var/log/gitcoin/funder_stale_email.log 2>&1 15 11 * * * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash new_bounties_email >> /var/log/gitcoin/new_bounties_email.log 2>&1 +15 10 * * * * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash campaign_email >> /var/log/gitcoin/campaign_email.log 2>&1 15 12 * * * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash bounty_requests_email >> /var/log/gitcoin/bounty_requests_email.log 2>&1 15 * * * * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash pending_start_work_actions >> /var/log/gitcoin/pending_start_work_actions.log 2>&1 0 0 1 */3 * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash send_quarterly_stats --live >> /var/log/gitcoin/send_quarterly_stats.log 2>&1 From 82f1ada458f154be0b15d229d3ee11d0d8fcdc42 Mon Sep 17 00:00:00 2001 From: Onuwa Nnachi Isaac Date: Sun, 17 Feb 2019 23:35:29 +0100 Subject: [PATCH 08/24] Update issue 794 --- app/app/urls.py | 7 +- app/marketing/mails.py | 30 +----- app/marketing/views.py | 9 +- .../campaigns/email_campaign_day_1.html | 99 +++++++++++++++++++ .../campaigns/email_campaign_day_2.html | 80 +++++++++++++++ .../campaigns/email_campaign_day_3.html | 97 ++++++++++++++++++ app/retail/views.py | 9 +- 7 files changed, 295 insertions(+), 36 deletions(-) diff --git a/app/app/urls.py b/app/app/urls.py index 716f7f5db9f..6f23060dd2f 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -378,9 +378,12 @@ path('_administration/email/gdpr_reconsent', retail.emails.gdpr_reconsent, name='gdpr_reconsent'), path('_administration/email/share_bounty', retail.emails.share_bounty, name='share_bounty'), path('_administration/email/new_tip/resend', retail.emails.resend_new_tip, name='resend_new_tip'), - path('_administration/email/day_email_campaign/', marketing.views.day_email_campaign, name='day_email_campaign'), + path( + '_administration/email/day_email_campaign/', + retail.views.day_email_campaign, + name='day_email_campaign' + ), re_path( - r'^_administration/process_accesscode_request/(.*)$', tdi.views.process_accesscode_request, name='process_accesscode_request' diff --git a/app/marketing/mails.py b/app/marketing/mails.py index 3fab8084aba..4f6b2419936 100644 --- a/app/marketing/mails.py +++ b/app/marketing/mails.py @@ -34,11 +34,11 @@ render_funder_stale, render_gdpr_reconsent, render_gdpr_update, render_grant_cancellation_email, render_kudos_email, render_match_email, 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_notify_ownership_change, render_quarterly_stats, 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_thank_you_for_supporting_email, - render_tip_email, render_weekly_recap, render_nth_day_email_campaign + render_notify_ownership_change, render_quarterly_stats, 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_thank_you_for_supporting_email, render_tip_email, render_weekly_recap, ) from sendgrid.helpers.mail import Content, Email, Mail, Personalization from sendgrid.helpers.stats import Category @@ -545,26 +545,6 @@ def reject_faucet_request(fr): finally: translation.activate(cur_language) -def nth_day_email_campaign(nth, subscriber): - - firstname = subscriber.email.split('@')[0] - - if subscriber.profile and subscriber.profile.user and subscriber.profile.user.first_name: - firstname = subscriber.profile.user.first_name - - if should_suppress_notification_email(subscriber.email, 'roundup'): - return False - - cur_language = translation.get_language() - try: - setup_lang(subscriber.email) - from_email = settings.CONTACT_EMAIL - if not should_suppress_notification_email(subscriber.email, 'welcome_mail'): - html, text, subject, = render_nth_day_email_campaign(subscriber.email, nth, firstname) - send_mail(from_email, subscriber.email, subject, text, html) - finally: - translation.activate(cur_language) - def new_bounty_daily(bounties, old_bounties, to_emails=None): if not bounties: return diff --git a/app/marketing/views.py b/app/marketing/views.py index c8a44f38c06..d28a1e493a9 100644 --- a/app/marketing/views.py +++ b/app/marketing/views.py @@ -26,7 +26,7 @@ from django.contrib import messages from django.core.validators import validate_email from django.db.models import Max -from django.http import Http404, HttpResponse +from django.http import Http404 from django.shortcuts import redirect from django.template.response import TemplateResponse from django.urls import reverse @@ -748,10 +748,3 @@ def leaderboard(request, key=''): } return TemplateResponse(request, 'leaderboard.html', context) - -@staff_member_required -def day_email_campaign(request, day): - if day not in list(range(1, 4)): - raise Http404 - response_html, _, _, = render_nth_day_email_campaign('foo@bar.com', day, 'staff_member') - return HttpResponse(response_html) \ No newline at end of file diff --git a/app/retail/templates/emails/campaigns/email_campaign_day_1.html b/app/retail/templates/emails/campaigns/email_campaign_day_1.html index e69de29bb2d..6a6399ab64e 100644 --- a/app/retail/templates/emails/campaigns/email_campaign_day_1.html +++ b/app/retail/templates/emails/campaigns/email_campaign_day_1.html @@ -0,0 +1,99 @@ +{% extends 'emails/template.html' %} + +{% comment %} + Copyright (C) 2018 Gitcoin Core + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +{% endcomment %} + +{% load i18n humanize %} + +{% block content %} + + + +
+ +

Day 1: Growing Open Source

+ +
+

+ {% trans "Hi" %} {{ firstname }}, +

+ +

+ {% blocktrans %} + Welcome to Gitcoin! Our mission is to + grow open source. Open source software is a force for good in the world, and we believe it’s the key to saving the internet. While a generation of software careers has been powered by open source software, it’s still hard to make a living working on open source. We plan to bring change in Web 3. + {% endblocktrans %} +

+ +

+ {% blocktrans %} + The issue Explorer is our first tool towards this end. Use it to find paid issues to work on in open source. Help build projects, ship code, and get paid in crypto tokens. Give it a try and let us know what you think! + {% endblocktrans %} +

+ +

+ {% blocktrans %} + As time passes, we’ll send you more ways to get involved in Gitcoin. For now, we have a few areas you can nerd out with us, depending on where you’re most comfortable. +

+ {% endblocktrans %} + +

+ +

+ {% blocktrans %} + Oh, and here’s akudos for getting to the bottom of this e-mail :) welcome to the Gitcoin Family! + {% endblocktrans %} + +

+

+ {% blocktrans %} + See you on the interwebs, + {% endblocktrans %} +

+ +

+ {% blocktrans %} + Kevin & Vivek + {% endblocktrans %} +

+ +
+
+ +{% endblock %} diff --git a/app/retail/templates/emails/campaigns/email_campaign_day_2.html b/app/retail/templates/emails/campaigns/email_campaign_day_2.html index e69de29bb2d..f514fcb132b 100644 --- a/app/retail/templates/emails/campaigns/email_campaign_day_2.html +++ b/app/retail/templates/emails/campaigns/email_campaign_day_2.html @@ -0,0 +1,80 @@ +{% extends 'emails/template.html' %} + +{% comment %} + Copyright (C) 2018 Gitcoin Core + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +{% endcomment %} + +{% load i18n humanize %} + +{% block content %} + + + +
+ +

Day 2: Finding A Gitcoin Issue For You

+ +
+

+ {% trans "Hi" %} {{ firstname }}, +

+ +

+ {% blocktrans %} + By this point, you might have poked around the Gitcoin Issue Explorer to see the open source projects you could make money by contributing! If interested in doing work, here’s how. + {% endblocktrans %} +

+ +

Start Work

+

+ {% blocktrans %} + Click ‘Express Interest’ to provide a brief message to the repo-maintainer that you’re interested in the issue. + {% endblocktrans %} +

+ +

+ {% blocktrans %} + From here, you’ll receive an email and a notification when the issue is ready for you to work. You’ll be able to submit a PR directly onto the repo maintainer’s project from there. It’s that simple! + {% endblocktrans %} +

+ +

+ {% blocktrans %} + CTA: Check out the Issue Explorer if you haven’t already! If you don’t find good fits, let us know an open source project that you’d like to see using Gitcoin. No promises, but we might have something up our sleeve. + {% endblocktrans %} +

+ +
+
+ +{% endblock %} diff --git a/app/retail/templates/emails/campaigns/email_campaign_day_3.html b/app/retail/templates/emails/campaigns/email_campaign_day_3.html index e69de29bb2d..33238554668 100644 --- a/app/retail/templates/emails/campaigns/email_campaign_day_3.html +++ b/app/retail/templates/emails/campaigns/email_campaign_day_3.html @@ -0,0 +1,97 @@ +{% extends 'emails/template.html' %} + +{% comment %} + Copyright (C) 2018 Gitcoin Core + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +{% endcomment %} + +{% load i18n humanize %} + +{% block content %} + + + +
+ +

Day 3: Leveraging The Gitcoin Community

+ +
+

+ {% trans "Hi" %} {{ firstname }}, +

+ +

+ {% blocktrans %} + Gitcoin Core is 11 team members, yet we’ve seen contributions from well over 100 developers using Gitcoin. We’d like to see you do the same! Here’s how to leverage a firehose of developer talent to do more, faster. + {% endblocktrans %} +

+ +

+ {% blocktrans %} + Posting a bounty can be done in 90 seconds, once you know Gitcoin is an option to accelerate your development. Adding bounties to an existing project is like adding icing to a cake. We take what’s already great about OSS (mission, freedom, and great people) and add financial incentivization, a home base for projects seeking contributors, funding for developer, and community. + {% endblocktrans %} +

+ +

+ {% blocktrans %} + Alongside posting bounties on the Issue Explorer, Gitcoin offers two complimentary products for open source software enthusiasts. +

+ {% endblocktrans %} +

+ +

+ {% blocktrans %} + Over the first week of you joining, we're going to send over some information on key products and education + tools available in the Gitcoin ecosystem. For now, we have a few areas you can nerd out with us, depending on where + you're most comfortable. + {% endblocktrans %} + +

+ +

+ {% blocktrans %} + Community is what makes Open Source work. A strong OSS community is inclusive, helpful, and mission-oriented. The Gitcoin community is 10,000 software developers who are focused on the tooling for the new world. Is there a chance that your next career connection could come from the Gitcoin community? Come find out! + {% endblocktrans %} + +

+ +
+
+ +{% endblock %} diff --git a/app/retail/views.py b/app/retail/views.py index 8fbf1b3bfc4..9011636f824 100644 --- a/app/retail/views.py +++ b/app/retail/views.py @@ -1447,4 +1447,11 @@ def increase_funding_limit_request(request): 'card_desc': _('Do you hit the Funding Limit? Request a increasement!') } - return TemplateResponse(request, 'increase_funding_limit_request_form.html', params) \ No newline at end of file + return TemplateResponse(request, 'increase_funding_limit_request_form.html', params) + +@staff_member_required +def day_email_campaign(request, day): + if day not in list(range(1, 4)): + raise Http404 + response_html, _, _, = render_nth_day_email_campaign('foo@bar.com', day, 'staff_member') + return HttpResponse(response_html) From a3b589f1ef6fc88ea87058c43d82f5f7292a0d58 Mon Sep 17 00:00:00 2001 From: Onuwa Nnachi Isaac Date: Sun, 17 Feb 2019 23:46:06 +0100 Subject: [PATCH 09/24] Update import --- app/retail/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/retail/views.py b/app/retail/views.py index 9011636f824..652f55e9768 100644 --- a/app/retail/views.py +++ b/app/retail/views.py @@ -38,7 +38,8 @@ from dashboard.models import Activity, Profile from dashboard.notifications import amount_usdt_open_work, open_bounties from economy.models import Token -from marketing.mails import new_funding_limit_increase_request, new_token_request, render_nth_day_email_campaign +from marketing.mails import new_funding_limit_increase_request, new_token_request +from retail.emails import render_nth_day_email_campaign from marketing.models import Alumni, LeaderboardRank from marketing.utils import get_or_save_email_subscriber, invite_to_slack from perftools.models import JSONStore From fab7674a281be3269e2375e9332eaa2ab5adb418 Mon Sep 17 00:00:00 2001 From: Onuwa Nnachi Isaac Date: Mon, 18 Feb 2019 03:14:58 +0100 Subject: [PATCH 10/24] Fix test failures --- app/marketing/mails.py | 10 +++++----- app/retail/views.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/marketing/mails.py b/app/marketing/mails.py index 4f6b2419936..acdc8f1c3fb 100644 --- a/app/marketing/mails.py +++ b/app/marketing/mails.py @@ -34,11 +34,11 @@ render_funder_stale, render_gdpr_reconsent, render_gdpr_update, render_grant_cancellation_email, render_kudos_email, render_match_email, 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_notify_ownership_change, render_quarterly_stats, 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_thank_you_for_supporting_email, render_tip_email, render_weekly_recap, + render_notify_ownership_change, render_quarterly_stats, 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_thank_you_for_supporting_email, + render_tip_email, render_weekly_recap, ) from sendgrid.helpers.mail import Content, Email, Mail, Personalization from sendgrid.helpers.stats import Category diff --git a/app/retail/views.py b/app/retail/views.py index 652f55e9768..68f21e6677b 100644 --- a/app/retail/views.py +++ b/app/retail/views.py @@ -39,11 +39,11 @@ from dashboard.notifications import amount_usdt_open_work, open_bounties from economy.models import Token from marketing.mails import new_funding_limit_increase_request, new_token_request -from retail.emails import render_nth_day_email_campaign from marketing.models import Alumni, LeaderboardRank from marketing.utils import get_or_save_email_subscriber, invite_to_slack from perftools.models import JSONStore from ratelimit.decorators import ratelimit +from retail.emails import render_nth_day_email_campaign from retail.helpers import get_ip from .forms import FundingLimitIncreaseRequestForm From 381aa79dbfd3bbfb8add3cec0ded1dcafe3a561d Mon Sep 17 00:00:00 2001 From: Onuwa Nnachi Isaac Date: Wed, 20 Feb 2019 14:55:28 +0100 Subject: [PATCH 11/24] Refactor campaign email --- app/app/urls.py | 2 +- app/marketing/mails.py | 30 +++++++++++++++---- .../management/commands/campaign_email.py | 2 +- app/marketing/views.py | 10 ++++++- app/retail/emails.py | 6 ++-- app/retail/views.py | 9 +----- 6 files changed, 40 insertions(+), 19 deletions(-) diff --git a/app/app/urls.py b/app/app/urls.py index 6f23060dd2f..30abe1ef5a4 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -380,7 +380,7 @@ path('_administration/email/new_tip/resend', retail.emails.resend_new_tip, name='resend_new_tip'), path( '_administration/email/day_email_campaign/', - retail.views.day_email_campaign, + marketing.views.day_email_campaign, name='day_email_campaign' ), re_path( diff --git a/app/marketing/mails.py b/app/marketing/mails.py index acdc8f1c3fb..fd619b50293 100644 --- a/app/marketing/mails.py +++ b/app/marketing/mails.py @@ -20,6 +20,7 @@ import logging from django.conf import settings +from django.http import Http404, HttpResponse from django.utils import timezone, translation from django.utils.translation import gettext from django.utils.translation import gettext_lazy as _ @@ -34,11 +35,11 @@ render_funder_stale, render_gdpr_reconsent, render_gdpr_update, render_grant_cancellation_email, render_kudos_email, render_match_email, 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_notify_ownership_change, render_quarterly_stats, 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_thank_you_for_supporting_email, - render_tip_email, render_weekly_recap, + render_notify_ownership_change, render_nth_day_email_campaign, render_quarterly_stats, 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_thank_you_for_supporting_email, render_tip_email, render_weekly_recap, ) from sendgrid.helpers.mail import Content, Email, Mail, Personalization from sendgrid.helpers.stats import Category @@ -109,6 +110,25 @@ def send_mail(from_email, _to_email, subject, body, html=False, return response +def nth_day_email_campaign(nth, subscriber): + firstname = subscriber.email.split('@')[0] + + if subscriber.profile and subscriber.profile.user and subscriber.profile.user.first_name: + firstname = subscriber.profile.user.first_name + + if should_suppress_notification_email(subscriber.email, 'roundup'): + return False + cur_language = translation.get_language() + + try: + setup_lang(subscriber.email) + from_email = settings.CONTACT_EMAIL + if not should_suppress_notification_email(subscriber.email, 'welcome_mail'): + html, text, subject = render_nth_day_email_campaign(subscriber.email, nth, firstname) + send_mail(from_email, subscriber.email, subject, text, html) + finally: + translation.activate(cur_language) + def new_grant(grant, profile): from_email = settings.CONTACT_EMAIL to_email = profile.email diff --git a/app/marketing/management/commands/campaign_email.py b/app/marketing/management/commands/campaign_email.py index 254e5e9df13..7e7e5065e35 100644 --- a/app/marketing/management/commands/campaign_email.py +++ b/app/marketing/management/commands/campaign_email.py @@ -59,5 +59,5 @@ class Command(BaseCommand): def handle(self, *args, **options): # day 1 email has been sent # start from day 2 - for i in range(2, 6): + for i in range(2, 3): send_nth_email(i) diff --git a/app/marketing/views.py b/app/marketing/views.py index d28a1e493a9..5b42ac377cc 100644 --- a/app/marketing/views.py +++ b/app/marketing/views.py @@ -24,9 +24,10 @@ from django.conf import settings from django.contrib import messages +from django.contrib.admin.views.decorators import staff_member_required from django.core.validators import validate_email from django.db.models import Max -from django.http import Http404 +from django.http import Http404, HttpResponse from django.shortcuts import redirect from django.template.response import TemplateResponse from django.urls import reverse @@ -748,3 +749,10 @@ def leaderboard(request, key=''): } return TemplateResponse(request, 'leaderboard.html', context) + +@staff_member_required +def day_email_campaign(request, day): + if day not in list(range(1, 3)): + raise Http404 + response_html, _, _, = render_nth_day_email_campaign('foo@bar.com', day, 'staff member') + return HttpResponse(response_html) diff --git a/app/retail/emails.py b/app/retail/emails.py index 403c4c21653..c85cbc6613d 100644 --- a/app/retail/emails.py +++ b/app/retail/emails.py @@ -43,7 +43,7 @@ # key, name, frequency MARKETING_EMAILS = [ - ('welcome_mail', _('Welcome Emails'), _('First 6 days after you sign up')), + ('welcome_mail', _('Welcome Emails'), _('First 3 days after you sign up')), ('roundup', _('Roundup Emails'), _('Weekly')), ('new_bounty_notifications', _('New Bounty Notification Emails'), _('(up to) Daily')), ('important_product_updates', _('Product Update Emails'), _('Quarterly')), @@ -83,8 +83,8 @@ def render_nth_day_email_campaign(to_email, nth, firstname): "firstname": firstname, "subscriber": get_or_save_email_subscriber(to_email, "internal"), } - response_html = premailer_transform(render_to_string("emails/campaigns/email_campaign_day_{nth}.html", params)) - response_txt = render_to_string("emails/campaigns/email_campaign_day_{nth}.txt", params) + response_html = premailer_transform(render_to_string(f"emails/campaigns/email_campaign_day_{nth}.html", params)) + response_txt = render_to_string(f"emails/campaigns/email_campaign_day_{nth}.txt", params) return response_html, response_txt, subject diff --git a/app/retail/views.py b/app/retail/views.py index 68f21e6677b..ea4f9abecfa 100644 --- a/app/retail/views.py +++ b/app/retail/views.py @@ -24,7 +24,7 @@ from django.core.exceptions import ValidationError from django.core.paginator import Paginator from django.core.validators import validate_email -from django.http import Http404, HttpResponse, JsonResponse +from django.http import Http404, JsonResponse from django.shortcuts import redirect from django.template.response import TemplateResponse from django.templatetags.static import static @@ -1449,10 +1449,3 @@ def increase_funding_limit_request(request): } return TemplateResponse(request, 'increase_funding_limit_request_form.html', params) - -@staff_member_required -def day_email_campaign(request, day): - if day not in list(range(1, 4)): - raise Http404 - response_html, _, _, = render_nth_day_email_campaign('foo@bar.com', day, 'staff_member') - return HttpResponse(response_html) From b8d786e4a747e3829a42f70b713c754655d09661 Mon Sep 17 00:00:00 2001 From: Onuwa Nnachi Isaac Date: Wed, 20 Feb 2019 14:55:40 +0100 Subject: [PATCH 12/24] Add tests for campaign email --- app/marketing/tests/test_mails.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/app/marketing/tests/test_mails.py b/app/marketing/tests/test_mails.py index bd5fe9ab2ce..b439c883acc 100644 --- a/app/marketing/tests/test_mails.py +++ b/app/marketing/tests/test_mails.py @@ -22,7 +22,8 @@ from django.utils import timezone from dashboard.models import Profile -from marketing.mails import setup_lang +from marketing.mails import nth_day_email_campaign, setup_lang +from retail.emails import render_nth_day_email_campaign from test_plus.test import TestCase @@ -41,6 +42,7 @@ def setUp(self): data={}, ) self.user.save() + self.days = [1, 2, 3] @patch('django.utils.translation.activate') def test_setup_lang(self, mock_translation_activate): @@ -54,3 +56,24 @@ def test_setup_lang_no_user(self, mock_translation_activate): """Test the marketing mails setup_lang method.""" setup_lang('bademail@gitcoin.co') assert mock_translation_activate.call_count == 0 + + @patch('marketing.mails.send_mail') + def test_day_1_campaign_email(self, mock_send_mail): + """Test the campaign email for day 1 is sent.""" + + nth_day_email_campaign(self.days[0], self.user) + assert mock_send_mail.call_count == 1 + + @patch('marketing.mails.send_mail') + def test_day_2_campaign_email(self, mock_send_mail): + """Test the campaign email for day 2 is sent.""" + + nth_day_email_campaign(self.days[1], self.user) + assert mock_send_mail.call_count == 1 + + @patch('marketing.mails.send_mail') + def test_day_3_campaign_email(self, mock_send_mail): + """Test the campaign email for day 3 is sent.""" + + nth_day_email_campaign(self.days[2], self.user) + assert mock_send_mail.call_count == 1 From 21c8e0aad9e1e58821feae442dd22296317b1021 Mon Sep 17 00:00:00 2001 From: Owocki Date: Thu, 21 Feb 2019 15:40:59 -0700 Subject: [PATCH 13/24] fixes for code review --- app/economy/models.py | 16 ++++++++-------- .../management/commands/create_page_cache.py | 8 +++----- app/retail/views.py | 6 ++++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/economy/models.py b/app/economy/models.py index 3996b2ba960..edee15790ad 100644 --- a/app/economy/models.py +++ b/app/economy/models.py @@ -30,25 +30,24 @@ from django.utils import timezone from django.utils.html import escape from django.utils.timezone import localtime +from django.core.serializers.json import DjangoJSONEncoder class EncodeAnything(DjangoJSONEncoder): def default(self, obj): if isinstance(obj, Promise): return force_text(obj) - if isinstance(obj, SuperModel): + elif isinstance(obj, SuperModel): return (obj.to_standard_dict()) - if isinstance(obj, models.Model): + elif isinstance(obj, models.Model): return (model_to_dict(obj)) - if isinstance(obj, models.Model): - return (model_to_dict(obj)) - if isinstance(obj, QuerySet): + elif isinstance(obj, QuerySet): if obj.count() and type(obj.first()) == str: return obj[::1] return [EncodeAnything(instance) for instance in obj] - if isinstance(obj, list): + elif isinstance(obj, list): return [EncodeAnything(instance) for instance in obj] - if(callable(obj)): + elif(callable(obj)): return None return super(EncodeAnything, self).default(obj) @@ -95,7 +94,8 @@ def to_standard_dict(self, fields=None, exclude=None, properties=None): kwargs['exclude'] = exclude return_me = model_to_dict(self, **kwargs) if properties: - for key in dir(self): + keys = [k for k in dir(self) if not k.startswith('_')] + for key in keys: if key in properties: attr = getattr(self, key) if callable(attr): diff --git a/app/perftools/management/commands/create_page_cache.py b/app/perftools/management/commands/create_page_cache.py index 4ca5c296fe2..838fd68c484 100644 --- a/app/perftools/management/commands/create_page_cache.py +++ b/app/perftools/management/commands/create_page_cache.py @@ -1,5 +1,5 @@ ''' - Copyright (C) 2017 Gitcoin Core + Copyright (C) 2019 Gitcoin Core This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published @@ -42,13 +42,12 @@ def create_results_cache(): items = [] JSONStore.objects.filter(view=view).all().delete() for keyword in keywords: - key = keyword print(f"- executing {keyword}") data = build_stat_results(keyword) print("- creating") items.append(JSONStore( view=view, - key=key, + key=keyword, data=json.loads(json.dumps(data, cls=EncodeAnything)), )) JSONStore.objects.bulk_create(items) @@ -65,13 +64,12 @@ def create_contributor_landing_page_context(): items = [] JSONStore.objects.filter(view=view).all().delete() for keyword in keywords: - key = keyword print(f"- executing {keyword}") data = get_contributor_landing_page_context(keyword) print("- creating") items.append(JSONStore( view=view, - key=key, + key=keyword, data=json.loads(json.dumps(data, cls=EncodeAnything)), )) JSONStore.objects.bulk_create(items) diff --git a/app/retail/views.py b/app/retail/views.py index c38c015f732..9920d86ef8b 100644 --- a/app/retail/views.py +++ b/app/retail/views.py @@ -18,6 +18,7 @@ ''' from json import loads as json_parse from os import walk as walkdir +import logging from django.conf import settings from django.contrib.admin.views.decorators import staff_member_required @@ -48,6 +49,7 @@ from .forms import FundingLimitIncreaseRequestForm from .utils import programming_languages +logger = logging.getLogger(__name__) @cached_as( Activity.objects.select_related('bounty').filter(bounty__network='mainnet').order_by('-created'), @@ -246,7 +248,7 @@ def contributor_bounties(request, tech_stack): for key, value in new_context.items(): context[key] = value except Exception as e: - print(e) + logger.exception(e) raise Http404 return TemplateResponse(request, 'bounties/contributor.html', context) @@ -258,7 +260,7 @@ def get_contributor_landing_page_context(tech_stack): activities = get_activities(tech_stack) return { 'activities': activities, - 'title': tech_stack.title() + str(_(" Open Source Opportunities")) if tech_stack else "Open Source Opportunities", + 'title': tech_stack.title() + str(_(" Open Source Opportunities")) if tech_stack else str(_("Open Source Opportunities")), 'available_bounties_count': available_bounties_count, 'available_bounties_worth': available_bounties_worth, 'tech_stack': tech_stack, From 83e2d3e5fe8781436379dc264f0d0c29fe07e358 Mon Sep 17 00:00:00 2001 From: Owocki Date: Thu, 21 Feb 2019 15:45:10 -0700 Subject: [PATCH 14/24] make fix --- app/assets/v2/js/grants/index.js | 9 +++++---- app/economy/models.py | 2 +- app/retail/views.py | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/assets/v2/js/grants/index.js b/app/assets/v2/js/grants/index.js index e4c3faaf755..abde1df5a62 100644 --- a/app/assets/v2/js/grants/index.js +++ b/app/assets/v2/js/grants/index.js @@ -7,12 +7,13 @@ $(document).ready(() => { minimumResultsForSearch: Infinity }); - $(".grant-item").click(function(){ - $(this).find('img').each(function(){ + $('.grant-item').click(function() { + $(this).find('img').each(function() { var src_url = $(this).data('src'); + $(this).attr('src', src_url); - }) - }) + }); + }); searchGrant(); populateFilters(); diff --git a/app/economy/models.py b/app/economy/models.py index edee15790ad..60c54f500fa 100644 --- a/app/economy/models.py +++ b/app/economy/models.py @@ -22,6 +22,7 @@ from django.contrib.humanize.templatetags.humanize import naturaltime from django.contrib.postgres.fields import JSONField +from django.core.serializers.json import DjangoJSONEncoder from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver @@ -30,7 +31,6 @@ from django.utils import timezone from django.utils.html import escape from django.utils.timezone import localtime -from django.core.serializers.json import DjangoJSONEncoder class EncodeAnything(DjangoJSONEncoder): diff --git a/app/retail/views.py b/app/retail/views.py index d43855a3b88..30c5e325e3f 100644 --- a/app/retail/views.py +++ b/app/retail/views.py @@ -16,9 +16,9 @@ along with this program. If not, see . ''' +import logging from json import loads as json_parse from os import walk as walkdir -import logging from django.conf import settings from django.contrib.admin.views.decorators import staff_member_required From 4a59cd69c42833a8af546fa6b038d862df5b206d Mon Sep 17 00:00:00 2001 From: Owocki Date: Thu, 21 Feb 2019 15:47:24 -0700 Subject: [PATCH 15/24] makemigrations --- .../migrations/0014_merge_20190221_2246.py | 14 ++++++++++++++ app/grants/migrations/0012_merge_20190221_2246.py | 14 ++++++++++++++ .../management/commands/create_page_cache.py | 1 - 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 app/dashboard/migrations/0014_merge_20190221_2246.py create mode 100644 app/grants/migrations/0012_merge_20190221_2246.py diff --git a/app/dashboard/migrations/0014_merge_20190221_2246.py b/app/dashboard/migrations/0014_merge_20190221_2246.py new file mode 100644 index 00000000000..ecef3fa662f --- /dev/null +++ b/app/dashboard/migrations/0014_merge_20190221_2246.py @@ -0,0 +1,14 @@ +# Generated by Django 2.1.2 on 2019-02-21 22:46 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0012_auto_20190130_1853'), + ('dashboard', '0013_bounty_featuring_date'), + ] + + operations = [ + ] diff --git a/app/grants/migrations/0012_merge_20190221_2246.py b/app/grants/migrations/0012_merge_20190221_2246.py new file mode 100644 index 00000000000..bb3cf75c6fc --- /dev/null +++ b/app/grants/migrations/0012_merge_20190221_2246.py @@ -0,0 +1,14 @@ +# Generated by Django 2.1.2 on 2019-02-21 22:46 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('grants', '0008_auto_20190130_1822'), + ('grants', '0011_auto_20190221_2050'), + ] + + operations = [ + ] diff --git a/app/perftools/management/commands/create_page_cache.py b/app/perftools/management/commands/create_page_cache.py index 838fd68c484..c5fde56e1ef 100644 --- a/app/perftools/management/commands/create_page_cache.py +++ b/app/perftools/management/commands/create_page_cache.py @@ -81,5 +81,4 @@ class Command(BaseCommand): help = 'generates some /results data' def handle(self, *args, **options): - create_results_cache() create_contributor_landing_page_context() From 4ea7f9acbca2e8d1236e4d51a1c49ac778e415d9 Mon Sep 17 00:00:00 2001 From: Owocki Date: Thu, 21 Feb 2019 15:53:41 -0700 Subject: [PATCH 16/24] serializer --- app/economy/models.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/economy/models.py b/app/economy/models.py index 60c54f500fa..adec9349f7b 100644 --- a/app/economy/models.py +++ b/app/economy/models.py @@ -24,11 +24,14 @@ from django.contrib.postgres.fields import JSONField from django.core.serializers.json import DjangoJSONEncoder from django.db import models +from django.db.models.query import QuerySet from django.db.models.signals import post_save from django.dispatch import receiver from django.forms.models import model_to_dict from django.urls import reverse from django.utils import timezone +from django.utils.functional import Promise +from django.db.models.fields.files import FieldFile from django.utils.html import escape from django.utils.timezone import localtime @@ -37,6 +40,8 @@ class EncodeAnything(DjangoJSONEncoder): def default(self, obj): if isinstance(obj, Promise): return force_text(obj) + if isinstance(obj, FieldFile): + return bool(obj) elif isinstance(obj, SuperModel): return (obj.to_standard_dict()) elif isinstance(obj, models.Model): From 1d43534f64ae75bb235d057c98a1e8efd01dac0f Mon Sep 17 00:00:00 2001 From: Owocki Date: Thu, 21 Feb 2019 15:54:43 -0700 Subject: [PATCH 17/24] fixes --- app/perftools/management/commands/create_page_cache.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/perftools/management/commands/create_page_cache.py b/app/perftools/management/commands/create_page_cache.py index c5fde56e1ef..838fd68c484 100644 --- a/app/perftools/management/commands/create_page_cache.py +++ b/app/perftools/management/commands/create_page_cache.py @@ -81,4 +81,5 @@ class Command(BaseCommand): help = 'generates some /results data' def handle(self, *args, **options): + create_results_cache() create_contributor_landing_page_context() From f37f11480460d04d802aa378be51932aa1547b61 Mon Sep 17 00:00:00 2001 From: octavioamu Date: Fri, 22 Feb 2019 16:26:56 -0300 Subject: [PATCH 18/24] fix stylesheet used in email --- app/retail/templates/emails/template.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/retail/templates/emails/template.html b/app/retail/templates/emails/template.html index 7eb714fa29a..fc7f7bd33db 100644 --- a/app/retail/templates/emails/template.html +++ b/app/retail/templates/emails/template.html @@ -20,7 +20,7 @@ - +