diff --git a/app/app/settings.py b/app/app/settings.py index 70cf0b4b109..6061ad1e721 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -509,6 +509,7 @@ MAILCHIMP_LIST_ID = env.str('MAILCHIMP_LIST_ID', default='') MAILCHIMP_LIST_ID_HUNTERS = env.str('MAILCHIMP_LIST_ID_HUNTERS', default='') MAILCHIMP_LIST_ID_FUNDERS = env.str('MAILCHIMP_LIST_ID_FUNDERS', default='') +MAILCHIMP_LIST_ID_HACKERS = env.str('MAILCHIMP_LIST_ID_HACKERS', default='') # Github GITHUB_API_BASE_URL = env('GITHUB_API_BASE_URL', default='https://api.github.com') diff --git a/app/app/urls.py b/app/app/urls.py index 362a9039359..762b323ee44 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -173,6 +173,7 @@ path('hackathon/onboard//', dashboard.views.hackathon_onboard, name='hackathon_onboard'), path('hackathon/', dashboard.views.hackathon, name='hackathon_idx'), path('hackathon-list/', dashboard.views.get_hackathons, name='get_hackathons'), + url(r'^register_hackathon/', dashboard.views.hackathon_registration, name='hackathon_registration'), # action URLs url(r'^funder', retail.views.funder_bounties_redirect, name='funder_bounties_redirect'), diff --git a/app/dashboard/admin.py b/app/dashboard/admin.py index 67c9362950e..e7f9d000762 100644 --- a/app/dashboard/admin.py +++ b/app/dashboard/admin.py @@ -24,9 +24,9 @@ from .models import ( Activity, BlockedUser, Bounty, BountyFulfillment, BountyInvites, BountySyncRequest, CoinRedemption, - CoinRedemptionRequest, Coupon, Earning, FeedbackEntry, HackathonEvent, HackathonSponsor, Interest, LabsResearch, - PortfolioItem, Profile, ProfileView, RefundFeeRequest, SearchHistory, Sponsor, Tip, TokenApproval, Tool, ToolVote, - UserAction, UserVerificationModel, + CoinRedemptionRequest, Coupon, Earning, FeedbackEntry, HackathonEvent, HackathonRegistration, HackathonSponsor, + Interest, LabsResearch, PortfolioItem, Profile, ProfileView, RefundFeeRequest, SearchHistory, Sponsor, Tip, + TokenApproval, Tool, ToolVote, UserAction, UserVerificationModel, ) @@ -343,6 +343,10 @@ def link(self, instance): return mark_safe(f'http://gitcoin.co{url}') +class HackathonRegistrationAdmin(admin.ModelAdmin): + list_display = ['pk', 'name', 'referer'] + raw_id_fields = ['registrant'] + admin.site.register(SearchHistory, SearchHistoryAdmin) admin.site.register(Activity, ActivityAdmin) admin.site.register(Earning, EarningAdmin) @@ -365,6 +369,7 @@ def link(self, instance): admin.site.register(Sponsor, SponsorAdmin) admin.site.register(HackathonEvent, HackathonEventAdmin) admin.site.register(HackathonSponsor, HackathonSponsorAdmin) +admin.site.register(HackathonRegistration, HackathonRegistrationAdmin) admin.site.register(FeedbackEntry, FeedbackAdmin) admin.site.register(LabsResearch) admin.site.register(UserVerificationModel, VerificationAdmin) diff --git a/app/dashboard/migrations/0056_auto_20191007_2254.py b/app/dashboard/migrations/0056_auto_20191007_2254.py new file mode 100644 index 00000000000..c3fccf3ab41 --- /dev/null +++ b/app/dashboard/migrations/0056_auto_20191007_2254.py @@ -0,0 +1,35 @@ +# Generated by Django 2.2.4 on 2019-10-07 22:54 + +from django.db import migrations, models +import django.db.models.deletion +import economy.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0055_profile_referrer'), + ] + + operations = [ + migrations.CreateModel( + name='HackathonRegistration', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_on', models.DateTimeField(db_index=True, default=economy.models.get_time)), + ('modified_on', models.DateTimeField(default=economy.models.get_time)), + ('name', models.CharField(help_text='Hackathon slug', max_length=255)), + ('referer', models.URLField(blank=True, help_text='Url comes from', null=True)), + ('hackathon', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dashboard.HackathonEvent')), + ('registrant', models.ForeignKey(help_text='User profile', on_delete=django.db.models.deletion.CASCADE, related_name='hackathon_registration', to='dashboard.Profile')), + ], + options={ + 'ordering': ('name',), + }, + ), + migrations.AddField( + model_name='profile', + name='hackathons', + field=models.ManyToManyField(blank=True, to='dashboard.HackathonRegistration'), + ), + ] diff --git a/app/dashboard/models.py b/app/dashboard/models.py index 11902a2108a..625d1d024f7 100644 --- a/app/dashboard/models.py +++ b/app/dashboard/models.py @@ -2143,6 +2143,31 @@ def hidden(self): return self.filter(hide_profile=True) +class HackathonRegistration(SuperModel): + """Defines the Hackthon profiles registrations""" + name = models.CharField(max_length=255, help_text='Hackathon slug') + + hackathon = models.ForeignKey( + 'HackathonEvent', + on_delete=models.SET_NULL, + null=True, + blank=True + ) + referer = models.URLField(null=True, blank=True, help_text='Url comes from') + registrant = models.ForeignKey( + 'dashboard.Profile', + related_name='hackathon_registration', + on_delete=models.CASCADE, + help_text='User profile' + ) + + class Meta: + ordering = ('name',) + + def __str__(self): + return f"Name: {self.name}; Hackathon: {self.hackathon}; Referer: {self.referer}; Registrant: {self.registrant}" + + class Profile(SuperModel): """Define the structure of the user profile. @@ -2237,6 +2262,8 @@ class Profile(SuperModel): rank_org = models.IntegerField(default=0) rank_coder = models.IntegerField(default=0) referrer = models.ForeignKey('dashboard.Profile', related_name='referred', on_delete=models.CASCADE, null=True, db_index=True) + hackathons = models.ManyToManyField(HackathonRegistration, blank=True) + objects = ProfileQuerySet.as_manager() diff --git a/app/dashboard/templates/addinterest.html b/app/dashboard/templates/addinterest.html index d0c6cbe30b0..8bca8bba2ab 100644 --- a/app/dashboard/templates/addinterest.html +++ b/app/dashboard/templates/addinterest.html @@ -25,84 +25,91 @@
{% trans "Submit a Plan" %}
{% if user_logged_in %}
-
- {% if bounty.repo_type == 'private' %} -
-
-
-
{% trans "Non-Disclosure Agreement" %}
- {% blocktrans %} -

Download the repo’s Non-Disclosure Agreement and upload the signed document.

- {% endblocktrans %} - - + {% if is_registered or not bounty.event %} + + {% if bounty.repo_type == 'private' %} +
+
+
+
{% trans "Non-Disclosure Agreement" %}
+ {% blocktrans %} +

Download the repo’s Non-Disclosure Agreement and upload the signed document.

+ {% endblocktrans %} + + +
+
-
+ {% endif %} +
+ {% if bounty.event %} +

+ This bounty is part of {{bounty.event.name}}, please read the rules to participate before you continue. +

+ {% endif %} + +
- {% endif %} -
+
{% if bounty.event %} -

- This bounty is part of hackathon, please read the rules to participate before you continue. -

- {% endif %} - - -
-
- {% if bounty.event %} -
-
- - - Join Discord to get updates from our sponsors prizes and find help - +
+
+ + + Join Discord to get updates from our sponsors prizes and find help +
-
- {% endif %} -
- - -
- + {% endif %} +
+ + +
+ +
-
-
-
- - -
- +
+
+ + +
+ +
+ + + {% else %} +
+

This bounty is part of {{bounty.event.name}}.
+ Please register for the hackathon and read the rules of participation before you continue.

+ Register for Hackathon
- - + {% endif %}
{% else %}
diff --git a/app/dashboard/templates/dashboard/hackathon_onboard.html b/app/dashboard/templates/dashboard/hackathon_onboard.html index af7b162ae8e..6439668b714 100644 --- a/app/dashboard/templates/dashboard/hackathon_onboard.html +++ b/app/dashboard/templates/dashboard/hackathon_onboard.html @@ -50,7 +50,6 @@

{{hackathon.name}} Guide

- {% if hackathon.end_date|timesince >= "1 min" %}

This hackathon event had ended at {{hackathon.end_date}}, please check the ongoing hackathons.

{% endif %} @@ -162,12 +161,63 @@

How does the Hackathon work?

- + {% if not github_handle %} + +
+ + + {% trans "Register with GitHub for Hackathon" %} + +

By registering you agree to receive hackathon emails announcements

+
+ + {% else %} +
+ {% if is_registered %} + Start Hacking + {% else %} + Register for Hackathon +

By registering you agree to receive hackathon emails announcements

+ {% endif %} +
+ {% endif %}
+ {% csrf_token %} {% include 'shared/analytics.html' %} {% include 'shared/footer_scripts.html' %} {% include 'shared/footer.html' %} + + diff --git a/app/dashboard/templates/profiles/profile.html b/app/dashboard/templates/profiles/profile.html index 08166092157..c89044fc063 100644 --- a/app/dashboard/templates/profiles/profile.html +++ b/app/dashboard/templates/profiles/profile.html @@ -47,7 +47,7 @@
{% if total_kudos_count %} -
+
{% for kudos_group in my_kudos %} {% endfor %} @@ -118,4 +118,4 @@ {% endif %} - \ No newline at end of file + diff --git a/app/dashboard/views.py b/app/dashboard/views.py index e8b7a5479be..88f85f1ffb3 100644 --- a/app/dashboard/views.py +++ b/app/dashboard/views.py @@ -18,6 +18,7 @@ ''' from __future__ import print_function, unicode_literals +import hashlib import json import logging import os @@ -43,6 +44,7 @@ from django.urls import reverse from django.utils import timezone from django.utils.html import escape, strip_tags +from django.utils.http import is_safe_url from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ from django.views.decorators.clickjacking import xframe_options_exempt @@ -62,6 +64,7 @@ from git.utils import get_auth_url, get_github_user_data, is_github_token_valid, search_users from kudos.models import KudosTransfer, Token, Wallet from kudos.utils import humanize_name +from mailchimp3 import MailChimp from marketing.mails import admin_contact_funder, bounty_uninterested from marketing.mails import funder_payout_reminder as funder_payout_reminder_mail from marketing.mails import ( @@ -76,9 +79,9 @@ from .helpers import get_bounty_data_for_activity, handle_bounty_views, load_files_in_directory from .models import ( Activity, Bounty, BountyDocuments, BountyFulfillment, BountyInvites, CoinRedemption, CoinRedemptionRequest, Coupon, - Earning, FeedbackEntry, HackathonEvent, HackathonSponsor, Interest, LabsResearch, PortfolioItem, Profile, - ProfileSerializer, ProfileView, RefundFeeRequest, SearchHistory, Sponsor, Subscription, Tool, ToolVote, UserAction, - UserVerificationModel, + Earning, FeedbackEntry, HackathonEvent, HackathonRegistration, HackathonSponsor, Interest, LabsResearch, + PortfolioItem, Profile, ProfileSerializer, ProfileView, RefundFeeRequest, SearchHistory, Sponsor, Subscription, + Tool, ToolVote, UserAction, UserVerificationModel, ) from .notifications import ( maybe_market_tip_to_email, maybe_market_tip_to_github, maybe_market_tip_to_slack, maybe_market_to_email, @@ -213,12 +216,18 @@ def get_interest_modal(request): except Bounty.DoesNotExist: raise Http404 + if bounty.event and request.user.is_authenticated: + is_registered = request.user.profile.hackathons.filter(hackathon_id=bounty.event.id).first() or None + else: + is_registered = None + context = { 'bounty': bounty, 'gitcoin_discord_username': request.user.profile.gitcoin_discord_username if request.user.is_authenticated else None, 'active': 'get_interest_modal', 'title': _('Add Interest'), 'user_logged_in': request.user.is_authenticated, + 'is_registered': is_registered, 'login_link': '/login/github?next=' + request.GET.get('redirect', '/') } return TemplateResponse(request, 'addinterest.html', context) @@ -3278,19 +3287,98 @@ def hackathon(request, hackathon=''): def hackathon_onboard(request, hackathon=''): + referer = request.META.get('HTTP_REFERER', '') try: hackathon_event = HackathonEvent.objects.filter(slug__iexact=hackathon).latest('id') + is_registered = request.user.profile.hackathons.filter(hackathon_id=hackathon_event.pk).first() if request.user.is_authenticated else None except HackathonEvent.DoesNotExist: hackathon_event = HackathonEvent.objects.last() + params = { 'active': 'hackathon_onboard', 'title': 'Hackathon Onboard', 'hackathon': hackathon_event, + 'referer': referer, + 'is_registered': is_registered, } return TemplateResponse(request, 'dashboard/hackathon_onboard.html', params) +@csrf_exempt +@require_POST +def hackathon_registration(request): + """Claim Work for a Bounty. + + :request method: POST + + Args: + bounty_id (int): ID of the Bounty. + + Returns: + dict: The success key with a boolean value and accompanying error. + + """ + profile = request.user.profile if request.user.is_authenticated and hasattr(request.user, 'profile') else None + hackathon = request.POST.get('name') + referer = request.POST.get('referer') + + if not profile: + return JsonResponse( + {'error': _('You must be authenticated via github to use this feature!')}, + status=401) + try: + hackathon_event = HackathonEvent.objects.filter(slug__iexact=hackathon).latest('id') + registration_data = HackathonRegistration.objects.create( + name=hackathon, + hackathon= hackathon_event, + referer=referer, + registrant=profile + ) + + profile.hackathons.add(registration_data.id) + except Exception as e: + logger.error('Error while saving registration', e) + + client = MailChimp(mc_api=settings.MAILCHIMP_API_KEY, mc_user=settings.MAILCHIMP_USER) + mailchimp_data = { + 'email_address': profile.email, + 'status_if_new': 'subscribed', + 'status': 'subscribed', + + 'merge_fields': { + 'HANDLE': profile.handle, + 'HACKATHON': hackathon, + }, + } + + user_email_hash = hashlib.md5(profile.email.encode('utf')).hexdigest() + + try: + client.lists.members.create_or_update(settings.MAILCHIMP_LIST_ID_HACKERS, user_email_hash, mailchimp_data) + client.lists.members.tags.update( + settings.MAILCHIMP_LIST_ID_HACKERS, + user_email_hash, + { + 'tags': [ + {'name': hackathon, 'status': 'active'}, + ], + } + ) + print('pushed_to_list') + except Exception as e: + logger.error(f"error in record_action: {e} - {instance}") + pass + + if referer and is_safe_url(referer, request.get_host()): + messages.success(request, _(f'You have successfully registered to {hackathon_event.name}. Happy hacking!')) + redirect = referer + else: + messages.success(request, _(f'You have successfully registered to {hackathon_event.name}. Happy hacking!')) + redirect = f'/hackathon/{hackathon}' + + return JsonResponse({'redirect': redirect}) + def get_hackathons(request): """Handle rendering all Hackathons.""" diff --git a/app/grants/templates/grants/card/front.html b/app/grants/templates/grants/card/front.html index 7e867ea7cfc..23fa27862f3 100644 --- a/app/grants/templates/grants/card/front.html +++ b/app/grants/templates/grants/card/front.html @@ -41,7 +41,7 @@

- {% if clr_active or show_past_clr %} + {% if clr_active %} {% include 'grants/card/clr_match.html' %} {% else %}

MONTHLY RECURRING

@@ -70,7 +70,7 @@

-
+
{% if grant.active %}
@@ -84,7 +84,7 @@

+
{{ grant.contribution_count}} contribution{% if grant.contribution_count > 1 %}s{% endif %} by {{ grant.contributor_count}} contributor{% if grant.contributor_count > 1 %}s{% endif %} diff --git a/app/marketing/admin.py b/app/marketing/admin.py index 96fbd9da92f..b66a4d7aac1 100644 --- a/app/marketing/admin.py +++ b/app/marketing/admin.py @@ -23,7 +23,8 @@ from .models import ( AccountDeletionRequest, Alumni, EmailEvent, EmailSubscriber, EmailSupressionList, GithubEvent, - GithubOrgToTwitterHandleMapping, Keyword, LeaderboardRank, MarketingCallback, Match, SlackPresence, SlackUser, Stat, + GithubOrgToTwitterHandleMapping, Keyword, LeaderboardRank, ManualStat, MarketingCallback, Match, SlackPresence, + SlackUser, Stat, ) @@ -120,6 +121,7 @@ def membership_length_in_days(self, instance): admin.site.register(Alumni, AlumniAdmin) admin.site.register(GithubEvent, GithubEventAdmin) admin.site.register(Match, MatchAdmin) +admin.site.register(ManualStat, GeneralAdmin) admin.site.register(Stat, GeneralAdmin) admin.site.register(Keyword, GeneralAdmin) admin.site.register(EmailEvent, EmailEventAdmin) diff --git a/app/marketing/migrations/0006_manualstat.py b/app/marketing/migrations/0006_manualstat.py new file mode 100644 index 00000000000..df6f05fad4e --- /dev/null +++ b/app/marketing/migrations/0006_manualstat.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.3 on 2019-09-26 17:34 + +from django.db import migrations, models +import economy.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('marketing', '0005_marketingcallback'), + ] + + operations = [ + migrations.CreateModel( + name='ManualStat', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_on', models.DateTimeField(db_index=True, default=economy.models.get_time)), + ('modified_on', models.DateTimeField(default=economy.models.get_time)), + ('key', models.CharField(db_index=True, max_length=50)), + ('date', models.DateTimeField(db_index=True)), + ('val', models.FloatField()), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/app/marketing/models.py b/app/marketing/models.py index 5d54bbceb54..76b89ab8b7a 100644 --- a/app/marketing/models.py +++ b/app/marketing/models.py @@ -137,6 +137,17 @@ def psave_es(sender, instance, **kwargs): instance.build_email_preferences() +class ManualStat(SuperModel): + """Define the manual stat model; which records stats that are not available on the platform + """ + + key = models.CharField(max_length=50, db_index=True) + date = models.DateTimeField(db_index=True) + val = models.FloatField() + + def __str__(self): + return f"{self.key}: {self.date}: {self.val}" + class Stat(SuperModel): key = models.CharField(max_length=50, db_index=True) diff --git a/app/retail/templates/emails/template.html b/app/retail/templates/emails/template.html index f524204d6ef..47045aabcdf 100644 --- a/app/retail/templates/emails/template.html +++ b/app/retail/templates/emails/template.html @@ -126,11 +126,10 @@ display: inline-block; {% if email_style %} max-height: 300px; - max-width: 100%; + width: 80%; {% else %} max-height: 80px; {% endif %} - max-width: 100%; } #grow-oss { diff --git a/app/retail/utils.py b/app/retail/utils.py index 7d0b91089f9..940a790dc39 100644 --- a/app/retail/utils.py +++ b/app/retail/utils.py @@ -144,6 +144,7 @@ def get_ecosystem_history_at_date(date, keyword): def get_codefund_history_at_date(date, keyword): + from marketing.models import ManualStat date = date.replace(tzinfo=None) amount = 0 # July => Feb 2019 @@ -179,7 +180,7 @@ def get_codefund_history_at_date(date, keyword): if date > timezone.datetime(2019, 9, 9): amount += 55000 if date > timezone.datetime(2019, 10, 9): - amount += 0 # october month to date + amount += sum(ManualStat.objects.filter(key='codefund_gmv', date__gt=date).values_list('val', flat=True)) return amount diff --git a/docs/README.md b/docs/README.md index a708639c3db..93f0c17bb0e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -191,7 +191,7 @@ We may introduce Arbitration [via Delphi](http://delphi.network/) at some point #### Showing Support to Individuals - via [Tips](https://gitcoin.co/tips) - - A free, fast way to show immediate gratitude towards an individual via github username or email adress + - A free, fast way to show immediate gratitude towards an individual via github username or email address - via [Kudos](https://gitcoin.co/kudos) - Showcases special skills and appreciation towards other Gitcoin members.