diff --git a/app/app/templates/shared/messages.html b/app/app/templates/shared/messages.html index f1cc256796b..87b612ce51c 100644 --- a/app/app/templates/shared/messages.html +++ b/app/app/templates/shared/messages.html @@ -18,9 +18,6 @@ document.messages = [] {% for message in messages %} if (document.messages.indexOf('{{ message }}') == -1) { - setTimeout(function(){ - _alert({message: gettext('{{ message }}')}, '{{ message.tags }}'); - }, 1000); document.messages.push('{{ message }}'); } {% endfor %} diff --git a/app/app/urls.py b/app/app/urls.py index efe5acdc85e..f872d0d29cf 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -168,6 +168,7 @@ # quests re_path(r'^quests/?$', quests.views.index, name='quests_index'), re_path(r'^quests/next?$', quests.views.next_quest, name='next_quest'), + re_path(r'^quests/(?P\d+)/feedback', quests.views.feedback, name='quest_feedback'), re_path(r'^quests/(?P\d+)/(?P\w*)', quests.views.details, name='quest_details'), re_path(r'^quests/new/?', quests.views.editquest, name='newquest'), re_path(r'^quests/edit/(?P\d+)/?', quests.views.editquest, name='editquest'), @@ -187,7 +188,11 @@ path('hackathon/onboard//', dashboard.views.hackathon_onboard, name='hackathon_onboard'), path('hackathon/projects//', dashboard.views.hackathon_projects, name='hackathon_projects'), path('modal/new_project//', dashboard.views.hackathon_get_project, name='hackathon_get_project'), - path('modal/new_project///', dashboard.views.hackathon_get_project, name='hackathon_edit_project'), + path( + 'modal/new_project///', + dashboard.views.hackathon_get_project, + name='hackathon_edit_project' + ), path('modal/save_project/', dashboard.views.hackathon_save_project, name='hackathon_save_project'), re_path(r'^hackathon/?$/?', dashboard.views.hackathon, name='hackathon_idx'), re_path(r'^hackathon/(.*)?$', dashboard.views.hackathon, name='hackathon_idx2'), @@ -620,8 +625,11 @@ ] urlpatterns += [ - re_path(r'^([a-z|A-Z|0-9|\.](?:[a-z\d]|-(?=[a-z\d]))+)/([a-z|A-Z|0-9|\.]+)/?$', dashboard.views.profile, name='profile_min'), - + re_path( + r'^([a-z|A-Z|0-9|\.](?:[a-z\d]|-(?=[a-z\d]))+)/([a-z|A-Z|0-9|\.]+)/?$', + dashboard.views.profile, + name='profile_min' + ), re_path(r'^([a-z|A-Z|0-9|\.](?:[a-z\d]|-(?=[a-z\d]))+)/?$', dashboard.views.profile, name='profile_min'), ] diff --git a/app/assets/v2/css/quests.css b/app/assets/v2/css/quests.css index f1cffa5b504..0e9f7c7fa25 100644 --- a/app/assets/v2/css/quests.css +++ b/app/assets/v2/css/quests.css @@ -360,12 +360,24 @@ body.quest_battle .modal-backdrop { display: none !important; } +.br a { + display: inline-block; + padding: 5px 5px; + margin: 4px 4px; + background-color: rgba(256,256,256,0.3); +} + body.quest_battle a, body.quest_battle div, body.quest_battle p { color: white; } +.br a:hover { + background-color: rgba(256,256,256,0.7); + text-decoration: none; +} + #_hj_poll_container { display: none; } diff --git a/app/assets/v2/js/pages/bounty_details.js b/app/assets/v2/js/pages/bounty_details.js index 22907b37073..634a9ef44c2 100644 --- a/app/assets/v2/js/pages/bounty_details.js +++ b/app/assets/v2/js/pages/bounty_details.js @@ -239,7 +239,8 @@ var callbacks = { if (!result.keywords || result.keywords.length == 0) return [ 'issue_keywords', null ]; - var keywords = result.keywords.split(','); + + var keywords = result.keywords && result.keywords.split ? result.keywords.split(',') : result.keywords; var tags = []; keywords.forEach(function(keyword) { diff --git a/app/assets/v2/js/pages/quests.helpers.js b/app/assets/v2/js/pages/quests.helpers.js index 16ed78897e7..0fa7f39ea67 100644 --- a/app/assets/v2/js/pages/quests.helpers.js +++ b/app/assets/v2/js/pages/quests.helpers.js @@ -73,6 +73,7 @@ var toggle_character_class = async function(sel, classes) { function typeWriter() { if (document.typewriter_i == 0) { document.typewriter_offset = 0; + document.is_typewriter = true; } if (document.typewriter_offset + document.typewriter_i < document.typewriter_txt.length) { var char = document.typewriter_txt.charAt(document.typewriter_i); @@ -84,9 +85,17 @@ function typeWriter() { document.getElementById(document.typewriter_id).innerHTML += char; document.typewriter_i++; setTimeout(typeWriter, document.typewriter_speed); + } else { + document.is_typewriter = false; } } +var wait_for_typewriter = async function() { + while (document.is_typewriter) { + await sleep(100); + } +}; + var get_midi = function(name) { return '/static/v2/audio/' + name + '.mid'; }; @@ -194,6 +203,24 @@ $(document).ready(function() { }); + $('.give_feedback').on('click', async function(e) { + e.preventDefault(); + var feedback = prompt('Any comments for the quest author? (optional)', 'Is question #3 wrong? I tried everyhing!'); + var polarity = $(this).data('direction'); + + var params = { + 'polarity': polarity, + 'feedback': feedback + }; + var url = document.quest_feedback_url; + + $.post(url, params, function(response) { + _alert('Thank you for your feedback on this quest.', 'success'); + $('#vote_container').remove(); + }); + }); + + if (document.quest) { start_quest(); } diff --git a/app/assets/v2/js/pages/quests.quest.quiz_style.js b/app/assets/v2/js/pages/quests.quest.quiz_style.js index 93a394e5d5a..23cfe84c826 100644 --- a/app/assets/v2/js/pages/quests.quest.quiz_style.js +++ b/app/assets/v2/js/pages/quests.quest.quiz_style.js @@ -162,6 +162,8 @@ var advance_to_state = async function(new_state) { // -- individual transitions callbacks -- // 0 to 1 + var new_html; + if (old_state == 0 && new_state == 1) { await sleep(1000); await $('#header').html('Quest Intro'); @@ -173,17 +175,18 @@ var advance_to_state = async function(new_state) { document.typewriter_i = 0; document.typewriter_txt = document.quest.game_schema.intro; document.typewriter_speed = 30; + typeWriter(); + await wait_for_typewriter(); + var kudos_reward_html = "

If you're successful in this quest, you'll earn this limited edition " + document.kudos_reward['name'] + " Kudos:

high_threshold: diff --git a/app/dashboard/templates/dashboard/hackathon/projects.html b/app/dashboard/templates/dashboard/hackathon/projects.html index 433ffd39110..297e5eb5ec5 100644 --- a/app/dashboard/templates/dashboard/hackathon/projects.html +++ b/app/dashboard/templates/dashboard/hackathon/projects.html @@ -152,7 +152,7 @@

{{hackathon.name}} Projects

{% if project.logo %} {% else %} - + {% endif %}
@@ -179,7 +179,7 @@
{{ project.name }}
diff --git a/app/dashboard/templates/shared/add_kudos.html b/app/dashboard/templates/shared/add_kudos.html index 961a25b1adc..4d543ca3693 100644 --- a/app/dashboard/templates/shared/add_kudos.html +++ b/app/dashboard/templates/shared/add_kudos.html @@ -22,7 +22,7 @@
-
diff --git a/app/kudos/templates/kudos_details.html b/app/kudos/templates/kudos_details.html index 875c989b67a..df62278eaa4 100644 --- a/app/kudos/templates/kudos_details.html +++ b/app/kudos/templates/kudos_details.html @@ -47,7 +47,7 @@ {% endif %}

{{ kudos.ui_name }} {% if kudos.generation %}Gen {{ kudos.generation }}{% endif %}

- {% if kudos.quests_reward %} + {% if kudos.quests_reward.count %} Reward for beating {% for quest in kudos.quests_reward.all %} {{quest.title}} Quest diff --git a/app/kudos/views.py b/app/kudos/views.py index 9da6129da57..9ef0dc185df 100644 --- a/app/kudos/views.py +++ b/app/kudos/views.py @@ -707,8 +707,9 @@ def redeem_bulk_coupon(coupon, profile, address, ip_address, save_addr=False): # send email maybe_market_kudos_to_email(kudos_transfer) except Exception as e: - logger.exception(e) error = "Could not redeem your kudos. Please try again soon." + if "replacement transaction underpriced" in str(e): + error = "There is already an airdrop transfer in progress. Please try again in a minute or two.. (note: in the future we will add 'queue'-ing so you dont have to resubmit, as soon as this ticket (https://github.com/gitcoinco/web/issues/4976) is deployed)" return None, error, None return True, None, kudos_transfer diff --git a/app/marketing/mails.py b/app/marketing/mails.py index 518c6949c03..baea276edf5 100644 --- a/app/marketing/mails.py +++ b/app/marketing/mails.py @@ -429,6 +429,28 @@ def new_grant_admin(grant): translation.activate(cur_language) +def send_user_feedback(quest, feedback, user): + to_email = quest.creator.email + from_email = user.email + cur_language = translation.get_language() + try: + setup_lang(to_email) + subject = f"New Gitcoin Quest Feedback: {quest.title}" + body_str = f"quest: {quest.title}\nurl: {quest.url}\nedit: {quest.edit_url}\n\n> {feedback}\n\nfrom: {user.email} ( {user.profile.url} )" + body = f"{body_str}" + if not should_suppress_notification_email(to_email, 'quest'): + send_mail( + from_email, + to_email, + subject, + body, + from_name=f"@{user.profile.handle} on gitcoin.co", + categories=['admin', func_name()], + ) + finally: + translation.activate(cur_language) + + def new_quest_request(quest, is_edit): to_email = settings.PERSONAL_CONTACT_EMAIL from_email = settings.SERVER_EMAIL diff --git a/app/marketing/views.py b/app/marketing/views.py index 0b9b73d613d..8f5d908879d 100644 --- a/app/marketing/views.py +++ b/app/marketing/views.py @@ -308,7 +308,6 @@ def email_settings(request, key): if request.POST and request.POST.get('submit'): email = request.POST.get('email') level = request.POST.get('level') - preferred_language = request.POST.get('preferred_language') validation_passed = True try: email_in_use = User.objects.filter(email=email) | User.objects.filter(profile__email=email) @@ -326,16 +325,7 @@ def email_settings(request, key): print(e) validation_passed = False msg = str(e) - if preferred_language: - if preferred_language not in [i[0] for i in settings.LANGUAGES]: - msg = _('Unknown language') - validation_passed = False if validation_passed: - if profile: - profile.pref_lang_code = preferred_language - profile.save() - request.session[LANGUAGE_SESSION_KEY] = preferred_language - translation.activate(preferred_language) if es: key = get_or_save_email_subscriber(email, 'settings') es.preferences['level'] = level diff --git a/app/quests/admin.py b/app/quests/admin.py index 93dbcc4a875..98eb006388e 100644 --- a/app/quests/admin.py +++ b/app/quests/admin.py @@ -2,14 +2,14 @@ from django.utils.safestring import mark_safe # Register your models here. -from .models import Quest, QuestAttempt, QuestPointAward +from .models import Quest, QuestAttempt, QuestFeedback, QuestPointAward class QuestAdmin(admin.ModelAdmin): raw_id_fields = ['kudos_reward', 'unlocked_by', 'creator'] ordering = ['-id'] list_display = ['created_on', '__str__'] - readonly_fields = ['background_preview'] + readonly_fields = ['feedback','background_preview'] def response_change(self, request, obj): if "_approve_quest" in request.POST: @@ -41,6 +41,20 @@ def response_change(self, request, obj): self.message_user(request, f"Quest Approved + Points awarded + Made Live.") return super().response_change(request, obj) + def feedback(self, instance): + fb = instance.feedbacks + html = f""" +
+ratio: {fb['ratio']}
+
+stats: {fb['stats']}
+
+feedback: {fb['feedback']}
+
+
+ """ + return mark_safe(html) + def background_preview(self, instance): html = '' for ext in ['png', 'jpg']: @@ -54,11 +68,19 @@ class QuestAttemptAdmin(admin.ModelAdmin): ordering = ['-id'] list_display = ['created_on', '__str__'] + +class QuestFeedbackAdmin(admin.ModelAdmin): + raw_id_fields = ['quest', 'profile'] + ordering = ['-id'] + list_display = ['created_on', '__str__'] + + class QuestPointAwardAdmin(admin.ModelAdmin): raw_id_fields = ['questattempt', 'profile'] ordering = ['-id'] list_display = ['created_on', '__str__'] +admin.site.register(QuestFeedback, QuestFeedbackAdmin) admin.site.register(QuestPointAward, QuestPointAwardAdmin) admin.site.register(Quest, QuestAdmin) admin.site.register(QuestAttempt, QuestAttemptAdmin) diff --git a/app/quests/helpers.py b/app/quests/helpers.py index e025128d080..bb76dd0826a 100644 --- a/app/quests/helpers.py +++ b/app/quests/helpers.py @@ -95,12 +95,14 @@ def get_base_quest_view_params(user, quest): """ profile = user.profile if user.is_authenticated else None attempts = quest.attempts.filter(profile=profile) if profile else QuestAttempt.objects.none() - + is_owner = quest.creator.pk == user.profile.pk if user.is_authenticated else False params = { 'quest': quest, 'hide_col': True, 'attempt_count': attempts.count() + 1, 'success_count': attempts.filter(success=True).count(), + 'is_owner': is_owner, + 'is_owner_or_staff': is_owner or user.is_staff, 'body_class': 'quest_battle', 'title': "Play the *" + quest.title + (f"* Gitcoin Quest and win a *{quest.kudos_reward.humanized_name}* Kudos" if quest.kudos_reward else ""), 'avatar_url': quest.avatar_url_png, diff --git a/app/quests/migrations/0021_questfeedback.py b/app/quests/migrations/0021_questfeedback.py new file mode 100644 index 00000000000..ba0f746700d --- /dev/null +++ b/app/quests/migrations/0021_questfeedback.py @@ -0,0 +1,31 @@ +# Generated by Django 2.2.3 on 2019-11-07 22:40 + +from django.db import migrations, models +import django.db.models.deletion +import economy.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0062_hackathonevent_show_results'), + ('quests', '0020_auto_20191106_1509'), + ] + + operations = [ + migrations.CreateModel( + name='QuestFeedback', + 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)), + ('vote', models.IntegerField(default=1)), + ('comment', models.TextField(blank=True, default='')), + ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quest_feedback', to='dashboard.Profile')), + ('quest', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='feedback', to='quests.Quest')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/app/quests/models.py b/app/quests/models.py index c51cdb4bdaf..574f15aa4b5 100644 --- a/app/quests/models.py +++ b/app/quests/models.py @@ -59,7 +59,6 @@ def __str__(self): """Return the string representation of this obj.""" return f'{self.pk}, {self.title} (visible: {self.visible})' - @property def url(self): from django.conf import settings @@ -70,6 +69,28 @@ def edit_url(self): from django.conf import settings return settings.BASE_URL + f"quests/edit/{self.pk}" + @property + def feedback_url(self): + from django.conf import settings + return settings.BASE_URL + f"quests/{self.pk}/feedback" + + @property + def feedbacks(self): + stats = {1 : 0, -1 : 0, 0 : 0} + for fb in self.feedback.all(): + stats[fb.vote] += 1 + ratio_upvotes = 0 + if self.feedback.count(): + ratio_upvotes = stats[1]/self.feedback.count() + return_me = { + 'ratio': ratio_upvotes, + 'stats': stats, + 'feedback': [] + } + for fb in self.feedback.all(): + return_me['feedback'].append(fb.comment) + return return_me + @property def est_read_time_mins(self): return self.game_schema.get('est_read_time_mins', 10) @@ -225,6 +246,7 @@ def psave_quest(sender, instance, **kwargs): instance.ui_data['attempts_count'] = instance.attempts.count() instance.ui_data['tags'] = instance.tags instance.ui_data['success_pct'] = instance.success_pct + instance.ui_data['feedbacks'] = instance.feedbacks instance.ui_data['creator'] = { 'url': instance.creator.url, 'handle': instance.creator.handle, @@ -247,6 +269,22 @@ def __str__(self): return f'{self.pk}, {self.profile.handle} => {self.quest.title} state: {self.state} success: {self.success}' +class QuestFeedback(SuperModel): + + quest = models.ForeignKey('quests.Quest', blank=True, null=True, related_name='feedback', on_delete=models.SET_NULL) + profile = models.ForeignKey( + 'dashboard.Profile', + on_delete=models.CASCADE, + related_name='quest_feedback', + ) + vote = models.IntegerField(default=1) + comment = models.TextField(default='', blank=True) + + def __str__(self): + """Return the string representation of this obj.""" + return f'{self.pk}, {self.profile.handle} => {self.quest.title} ({self.comment})' + + class QuestPointAward(SuperModel): questattempt = models.ForeignKey('quests.QuestAttempt', related_name='pointawards', on_delete=models.CASCADE) diff --git a/app/quests/templates/quests/types/quiz_style.html b/app/quests/templates/quests/types/quiz_style.html index 85ae68b9d83..54aa1acab45 100644 --- a/app/quests/templates/quests/types/quiz_style.html +++ b/app/quests/templates/quests/types/quiz_style.html @@ -22,10 +22,16 @@ -{% if not hidden and is_staff %} +{% if not hidden and is_owner_or_staff %}
+ {% if is_staff %} Staff only | {% trans "Quest Admin" %} + {% endif %} + {% if is_staff or is_owner %} + {% if not is_staff %} Owner Only {% endif %} | + {% trans "Edit Quest" %} + {% endif %}
{% endif %} diff --git a/app/retail/templates/emails/share_bounty_email.html b/app/retail/templates/emails/share_bounty_email.html index 4ac71cc8bd6..9a91cc86121 100644 --- a/app/retail/templates/emails/share_bounty_email.html +++ b/app/retail/templates/emails/share_bounty_email.html @@ -32,7 +32,7 @@

@{{from_profile.handle}} {% trans "invited you to work on a bounty" %}

< {% if kudos_invite %}
-

{% trans "... you can get a special Kudos too" %}

+

If you accept the invite... you can get a special Kudos from @{{from_profile.handle}} for working on this bounty!



diff --git a/app/retail/templates/settings/email.html b/app/retail/templates/settings/email.html index 3838cd122c8..bb6675584cf 100644 --- a/app/retail/templates/settings/email.html +++ b/app/retail/templates/settings/email.html @@ -33,21 +33,6 @@
{% trans "Email Preferences" %}
{% endfor %} -
- -
- -
- {% include 'svgs/arrow-down.svg' %} -
-
-
{% csrf_token %}