diff --git a/app/app/urls.py b/app/app/urls.py index a1018c157e7..d602a5246bc 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -216,6 +216,7 @@ path('hackathon//new/', dashboard.views.new_hackathon_bounty, name='new_hackathon_bounty'), path('hackathon//new', dashboard.views.new_hackathon_bounty, name='new_hackathon_bounty2'), path('hackathon//', dashboard.views.hackathon, name='hackathon'), + path('hackathon/dashboard/', dashboard.views.dashboard_sponsors, name='sponsors-dashboard'), path('hackathon/', dashboard.views.hackathon, name='hackathon2'), path('hackathon//onboard/', dashboard.views.hackathon_onboard, name='hackathon_onboard2'), path('hackathon///', dashboard.views.hackathon, name='hackathon'), @@ -251,6 +252,7 @@ re_path(r'^hackathons/?$', dashboard.views.get_hackathons, name='get_hackathons4'), url(r'^register_hackathon/', dashboard.views.hackathon_registration, name='hackathon_registration'), path('api/v0.1/hackathon//save/', dashboard.views.save_hackathon, name='save_hackathon'), + path('api/v1/hackathon//prizes', dashboard.views.hackathon_prizes, name='hackathon_prizes'), path('api/v0.1/hackathon//showcase/', dashboard.views.showcase, name='hackathon_showcase'), # action URLs @@ -688,7 +690,7 @@ url(settings.GITHUB_EVENT_HOOK_URL, gitcoinbot.views.payload, name='payload'), url(r'^impersonate/', include('impersonate.urls')), url(r'^api/v0.1/hackathon_project/set_winner/', dashboard.views.set_project_winner, name='project_winner'), - url(r'^api/v0.1/hackathon_project/set_winner/', dashboard.views.set_project_winner, name='project_winner'), + url(r'^api/v0.1/hackathon_project/set_notes/', dashboard.views.set_project_notes, name='project_notes'), # users url(r'^api/v0.1/user_bounties/', dashboard.views.get_user_bounties, name='get_user_bounties'), diff --git a/app/assets/v2/js/pages/dashboard-hackathon.js b/app/assets/v2/js/pages/dashboard-hackathon.js index 82a2f11cfba..3eb7579b37c 100644 --- a/app/assets/v2/js/pages/dashboard-hackathon.js +++ b/app/assets/v2/js/pages/dashboard-hackathon.js @@ -928,11 +928,34 @@ } }, + computed: { + isSponsor: () => { + let vm = this; + + if (document.contxt.is_staff) { + return true; + } + + for (let i = 0; i < vm.hackathonSponsors.length; i++) { + if (vm.hackathonSponsors[i].org_name === document.contxt.github_handle) { + return true; + } + } + + for (let i = 0; i < vm.prizeFounders.length; i++) { + if (vm.prizeFounders[i] === document.contxt.github_handle) { + return true; + } + } + return false; + } + }, data: () => ({ is_registered: document.is_registered, activePanel: document.activePanel, hackathonObj: document.hackathonObj, hackathonSponsors: document.hackathonSponsors, + prizeFounders: document.prizeFounders, hackathonProjects: [], chatURL: document.chatURL, hackHasEnded: document.displayShowcase diff --git a/app/assets/v2/js/pages/dashboard-sponsors.js b/app/assets/v2/js/pages/dashboard-sponsors.js new file mode 100644 index 00000000000..2e2188e614f --- /dev/null +++ b/app/assets/v2/js/pages/dashboard-sponsors.js @@ -0,0 +1,136 @@ +(function($) { + $(function() { + window.hackathonApp = new Vue({ + delimiters: [ '[[', ']]' ], + el: '#sponsors-app', + mounted() { + this.retrieveSponsorPrizes(); + }, + methods: { + markWinner: function($event, project, prizeIndex) { + let vm = this; + const url = '/api/v0.1/hackathon_project/set_winner/'; + const markWinner = fetchData(url, 'POST', { + project_id: project.pk, + winner: $event ? 1 : 0 + }, {'X-CSRFToken': vm.csrf}); + + vm.prizes[prizeIndex].submissions.forEach((submission, submissionIndex) => { + if (submission.pk !== project.pk && submission.winner) { + console.log(submission.pk); + console.log(project.pk); + const unmarkPreviousWinner = fetchData(url, 'POST', { + project_id: submission.pk, + winner: 0 + }, {'X-CSRFToken': vm.csrf}); + + $.when(unmarkPreviousWinner).then(() => { + vm.$set(vm.prizes[prizeIndex].submissions[submissionIndex], 'winner', false); + }); + } + }); + + $.when(markWinner).then(response => { + if (response.message) { + alert(response.message); + } + }).catch(err => { + console.log(err); + }); + }, + setNote: function($event, project) { + let vm = this; + + const url = '/api/v0.1/hackathon_project/set_notes/'; + const setNotes = fetchData(url, 'POST', { + project_id: project.pk, + notes: vm.comments[project.pk] + }, {'X-CSRFToken': vm.csrf}); + + $.when(setNotes).then(response => { + if (response.message) { + alert(response.message); + } + }).catch(err => { + console.log(err); + }); + }, + addNote: function(project) { + let vm = this; + + vm.$set(vm.comments, project.pk, ''); + }, + getComment: function(project) { + let vm = this; + + return vm.comments[project]; + }, + retrieveSponsorPrizes: function() { + const vm = this; + const hackathon = fetchData(`/api/v1/hackathon/${vm.hackathonObj['slug']}/prizes`); + + $.when(hackathon).then((response) => { + for (let i = 0; i < response.prizes.length; i++) { + if (response.prizes[i].submissions.length) { + response.prizes[i].submissions.forEach((submission) => { + vm.$set(vm.comments, submission.pk, submission.extra.notes); + }); + } + } + vm.prizes = response.prizes; + }); + }, + tabChange: function(input) { + let vm = this; + + switch (input) { + default: + case 0: + newPathName = 'prizes'; + break; + case 1: + newPathName = 'submissions'; + break; + } + let newUrl = `/hackathon/dashboard/${vm.hackathonObj['slug']}/${newPathName}`; + + history.pushState({}, `${vm.hackathonObj['slug']} - ${newPathName}`, newUrl); + + }, + start_and_end: function(str) { + if (str.length > 25) { + return str.substr(0, 8) + '...' + str.substr(str.length - 5, str.length); + } + return str; + }, + getSummary: function(project) { + if (project.showDescription || project.summary.length < 177) { + return project.summary; + } + return `${project.summary.slice(0, 177)}...`; + }, + toggleSummary: function(prizeIndex, submissionIndex) { + let vm = this; + const showDescription = !vm.prizes[prizeIndex].submissions[submissionIndex].showDescription; + + vm.$set(vm.prizes[prizeIndex].submissions[submissionIndex], 'showDescription', showDescription); + } + }, + computed: { + isMobileDevice() { + return this.windowWidth < 576; + } + }, + data: () => ({ + activePanel: document.activePanel, + hackathonObj: document.hackathonObj, + hackathonSponsors: document.hackathonSponsors, + hackathonProjects: [], + chatURL: document.chatURL, + prizes: [], + comments: [], + csrf: $("input[name='csrfmiddlewaretoken']").val() || '' + }) + }); + }); +})(jQuery); diff --git a/app/assets/v2/js/vue-components.js b/app/assets/v2/js/vue-components.js index 946d4c80caa..ef8f9cb24dd 100644 --- a/app/assets/v2/js/vue-components.js +++ b/app/assets/v2/js/vue-components.js @@ -9,9 +9,11 @@ Vue.mixin({ }, methods: { chatWindow: function(channel, dm) { + let vm = this; + dm = dm || channel ? channel.indexOf('@') >= 0 : false; channel = channel || 'town-square'; - let vm = this; + const hackathonTeamSlug = 'hackathons'; const gitcoinTeamSlug = 'gitcoin'; const isHackathon = (document.hackathon_id !== null); diff --git a/app/dashboard/models.py b/app/dashboard/models.py index e861d614136..aa519390aa8 100644 --- a/app/dashboard/models.py +++ b/app/dashboard/models.py @@ -4893,6 +4893,33 @@ def url(self): def get_absolute_url(self): return self.url() + def to_json(self): + profiles = [ + { + 'handle': profile.handle, + 'name': profile.name, + 'email': profile.email, + 'payout_address': profile.preferred_payout_address, + 'url': profile.url, + 'avatar': profile.active_avatar.avatar_url if profile.active_avatar else '' + } for profile in self.profiles.all() + ] + + return { + 'pk': self.pk, + 'name': self.name, + 'logo': self.logo.url, + 'badge': self.badge, + 'profiles': profiles, + 'work_url': self.work_url, + 'summary': self.summary, + 'status': self.status, + 'message': self.message, + 'chat_channel_id': self.chat_channel_id, + 'winner': self.winner, + 'extra': self.extra + } + class FeedbackEntry(SuperModel): bounty = models.ForeignKey( diff --git a/app/dashboard/templates/dashboard/index-vue.html b/app/dashboard/templates/dashboard/index-vue.html index c1b21c72d95..a243cb53c75 100644 --- a/app/dashboard/templates/dashboard/index-vue.html +++ b/app/dashboard/templates/dashboard/index-vue.html @@ -124,6 +124,7 @@ Hackathon Over {% endif %}

+ Sponsor dashboard
@@ -817,6 +818,7 @@

[[hackathon.name]] Wall of Fame< {{orgs|json_script:"sponsor-list"}} {{hackathon_obj|json_script:"hackathon-object"}} + {{prize_founders|json_script:"prize-founders"}} + {{orgs|json_script:"sponsor-list"}} + {{hackathon_obj|json_script:"hackathon-object"}} + + + {% include 'shared/activity_scripts.html' %} + + + + + + {% include 'shared/current_profile.html' %} + + + diff --git a/app/dashboard/views.py b/app/dashboard/views.py index 4368f1a80a5..2cec25ead33 100644 --- a/app/dashboard/views.py +++ b/app/dashboard/views.py @@ -936,6 +936,27 @@ def set_project_winner(request): return JsonResponse({}) +@require_POST +def set_project_notes(request): + + notes = request.POST.get('notes', False) + project_id = request.POST.get('project_id', None) + if not project_id: + return JsonResponse({ + 'message': 'Invalid Project' + }) + project = HackathonProject.objects.get(pk=project_id) + + if not request.user.is_authenticated and (request.user.is_staff or request.user.profile.handle == project.bounty.bounty_owner_github_username): + return JsonResponse({ + 'message': 'UNAUTHORIZED' + }) + + project.extra['notes'] = notes + project.save() + + return JsonResponse({}) + @require_GET def users_fetch(request): @@ -3589,6 +3610,107 @@ def get_kudos(request): return HttpResponse(data, mimetype) +def hackathon_prizes(request, hackathon=''): + hackathon_event = get_object_or_404(HackathonEvent, + slug__iexact=hackathon) + + is_founder = Bounty.objects.filter(event=hackathon_event, bounty_owner_profile=request.user.profile) + + if not is_founder: + raise Http404("You need fund one prize in this hackathon to access the dashboard.") + + query_prizes = Bounty.objects.filter(event=hackathon_event, bounty_owner_profile=request.user.profile) + prizes = [] + for prize in query_prizes: + total_submitted = BountyEvent.objects.filter(bounty=prize, event_type='submit_work').count() + prize_in_json = { + 'pk': prize.pk, + 'title': prize.title_or_desc, + 'url': prize.get_absolute_url(), + 'value_eth': prize.get_value_in_eth, + 'values_usdt': prize.get_value_in_usdt_now, + 'paid': bool(prize.paid), + 'submissions': [project.to_json() for project in prize.project_bounty.all()], + 'total_projects': prize.project_bounty.count(), + 'total_submitted': total_submitted + } + + prizes.append(prize_in_json) + + return JsonResponse({ + 'prizes': prizes + }) + + +def dashboard_sponsors(request, hackathon='', panel='prizes'): + """Handle rendering of HackathonEvents. Reuses the dashboard template.""" + + hackathon_event = get_object_or_404(HackathonEvent, slug__iexact=hackathon) + + is_founder = Bounty.objects.filter(event=hackathon_event, bounty_owner_profile=request.user.profile) + + if not is_founder: + raise Http404("You need fund one prize in this hackathon to access the dashboard.") + + + + title = hackathon_event.name.title() + description = f"{title} | Gitcoin Virtual Hackathon" + avatar_url = hackathon_event.logo.url if hackathon_event.logo else request.build_absolute_uri( + static('v2/images/twitter_cards/tw_cards-02.png')) + network = get_default_network() + hackathon_not_started = timezone.now() < hackathon_event.start_date and not request.user.is_staff + + sponsor_profile = request.user.profile + org = { + 'display_name': sponsor_profile.name, + 'avatar_url': sponsor_profile.avatar_url, + 'org_name': sponsor_profile.handle, + 'follower_count': sponsor_profile.tribe_members.all().count(), + 'bounty_count': sponsor_profile.bounties.count() + } + + view_tags = get_tags(request) + active_tab = 0 + if panel == "prizes": + active_tab = 0 + elif panel == "submissions": + active_tab = 1 + + filter = '' + if request.GET.get('filter'): + filter = f':{request.GET.get("filter")}' + + what = f'hackathon:{hackathon_event.id}{filter}' + + num_participants = HackathonRegistration.objects.filter(hackathon=hackathon_event).count() + num_submissions = BountyEvent.objects.filter(bounty__event_id=hackathon_event.id, event_type='submit_work').count() + params = { + 'active': 'dashboard', + 'prize_count': hackathon_event.get_current_bounties.count(), + 'type': 'hackathon', + 'title': title, + 'card_desc': description, + 'what': what, + 'org': org, + 'keywords': json.dumps([str(key) for key in Keyword.objects.all().values_list('keyword', flat=True)]), + 'hackathon': hackathon_event, + 'hackathon_obj': HackathonEventSerializer(hackathon_event).data, + 'hackathon_not_started': hackathon_not_started, + 'user': request.user, + 'avatar_url': avatar_url, + 'tags': view_tags, + 'activities': [], + 'use_pic_card': True, + 'projects': [], + 'panel': active_tab, + 'num_participants': num_participants, + 'num_submissions': num_submissions + } + + return TemplateResponse(request, 'dashboard/sponsors.html', params) + + def hackathon(request, hackathon='', panel='prizes'): """Handle rendering of HackathonEvents. Reuses the dashboard template.""" @@ -3675,6 +3797,8 @@ def hackathon(request, hackathon='', panel='prizes'): 'hacker_count': hacker_count, 'projects_count': projects_count, 'hackathon_obj': HackathonEventSerializer(hackathon_event).data, + 'prize_founders': list( + Bounty.objects.filter(event=hackathon_event).values_list('bounty_owner_profile__handle', flat=True).distinct()), 'is_registered': json.dumps(True if is_registered else False), 'hackathon_not_started': hackathon_not_started, 'user': request.user,