From 19ffd57569ceeafcf1e87678b8df9aec88aa6691 Mon Sep 17 00:00:00 2001 From: umgelurgel Date: Thu, 8 Aug 2019 20:24:25 +0200 Subject: [PATCH 1/3] Add support for submitting slide urls. --- conference/forms/talks.py | 4 +- .../migrations/0015_add_talk_url_fields.py | 28 +++++++++ conference/models.py | 2 + conference/talks.py | 3 +- templates/ep19/bs/talks/talk.html | 7 ++- tests/test_talks.py | 57 +++++++++++++++++-- 6 files changed, 90 insertions(+), 11 deletions(-) create mode 100644 conference/migrations/0015_add_talk_url_fields.py diff --git a/conference/forms/talks.py b/conference/forms/talks.py index d616b42fa..c5ba2b4f9 100644 --- a/conference/forms/talks.py +++ b/conference/forms/talks.py @@ -58,12 +58,10 @@ def save(self, user): class TalkSlidesForm(forms.ModelForm): - slides = forms.FileField(required=True) - class Meta: model = Talk fields = [ - "slides" + "slides", "slides_url", "repository_url" ] diff --git a/conference/migrations/0015_add_talk_url_fields.py b/conference/migrations/0015_add_talk_url_fields.py new file mode 100644 index 000000000..f206687c9 --- /dev/null +++ b/conference/migrations/0015_add_talk_url_fields.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.21 on 2019-08-08 17:55 +from __future__ import unicode_literals + +import conference.models +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('conference', '0014_stripe_charge_default_uuid'), + ] + + operations = [ + migrations.AddField( + model_name='talk', + name='repository_url', + field=models.URLField(blank=True), + ), + migrations.AddField( + model_name='talk', + name='slides_url', + field=models.URLField(blank=True), + ), + ] diff --git a/conference/models.py b/conference/models.py index 291fe5a3d..fd65ded1a 100644 --- a/conference/models.py +++ b/conference/models.py @@ -702,6 +702,8 @@ class Talk(models.Model, UrlMixin): ) slides = models.FileField(upload_to=_fs_upload_to("slides"), blank=True) + slides_url = models.URLField(blank=True) + repository_url = models.URLField(blank=True) video_type = models.CharField( max_length=30, choices=VIDEO_TYPE, blank=True ) diff --git a/conference/talks.py b/conference/talks.py index a3cde7ced..c27b6a6cf 100644 --- a/conference/talks.py +++ b/conference/talks.py @@ -150,7 +150,8 @@ def dump_relevant_talk_information_to_dict(talk: Talk): "tags": [t.name for t in talk.tags.all()], "speakers": [], "schedule_url": talk.get_schedule_url(), - "slides_url": talk.slides, + "slides_file_url": talk.slides, + "slides_remote_url": talk.slides_url, } for speaker in talk.get_all_speakers(): diff --git a/templates/ep19/bs/talks/talk.html b/templates/ep19/bs/talks/talk.html index a58ae9a68..c239b2c21 100644 --- a/templates/ep19/bs/talks/talk.html +++ b/templates/ep19/bs/talks/talk.html @@ -43,8 +43,11 @@
{% for speaker in talk_as_dict.speakers %} {% if talk_as_dict.schedule_url %} See in schedule {% endif %} - {% if talk_as_dict.slides_url %} - Slides + {% if talk_as_dict.slides_file_url %} + Download Slides + {% endif %} + {% if talk_as_dict.slides_remote_url %} + View Slides {% endif %}

{{ talk_as_dict.abstract|linebreaks }}

diff --git a/tests/test_talks.py b/tests/test_talks.py index 5a64936ec..387ff478c 100644 --- a/tests/test_talks.py +++ b/tests/test_talks.py @@ -181,6 +181,31 @@ def test_author_can_post_submit_slides(user_client): talk.refresh_from_db() assert talk.slides +def test_author_can_post_submit_slides_url(user_client): + setup_conference_with_typical_fares() + talk = TalkFactory(created_by=user_client.user, status=TALK_STATUS.accepted) + + url = reverse("talks:submit_slides", args=[talk.slug]) + payload = {"slides_url": "https://ep2019.europython.eu"} + response = user_client.post(url, data=payload) + + assert redirects_to(response, talk.get_absolute_url()) + talk.refresh_from_db() + assert talk.slides_url + + +def test_author_can_post_submit_repository_url(user_client): + setup_conference_with_typical_fares() + talk = TalkFactory(created_by=user_client.user, status=TALK_STATUS.accepted) + + url = reverse("talks:submit_slides", args=[talk.slug]) + payload = {"repository_url": "https://ep2019.europython.eu"} + response = user_client.post(url, data=payload) + + assert redirects_to(response, talk.get_absolute_url()) + talk.refresh_from_db() + assert talk.repository_url + def test_submit_slides_url_on_talk_detail_page(client): """ @@ -212,9 +237,9 @@ def test_submit_slides_url_on_talk_detail_page(client): assert submit_slides_url in response.content.decode() -def test_view_slides_url_on_talk_detail_page(client): +def test_view_slides_file_url_on_talk_detail_page(client): """ - The view slides button only appears if the slides have been uploaded. + The download slides button only appears if the slides have been uploaded. """ setup_conference_with_typical_fares() talk = TalkFactory(status=TALK_STATUS.accepted) @@ -224,7 +249,7 @@ def test_view_slides_url_on_talk_detail_page(client): response = client.get(url) assert not talk.slides - assert 'slides' not in response.content.decode() + assert 'download slides' not in response.content.decode().lower() # Slides URL does appear when the slides have been uploaded talk.slides = SimpleUploadedFile('slides.pdf', 'pdf content'.encode()) @@ -232,5 +257,27 @@ def test_view_slides_url_on_talk_detail_page(client): response = client.get(url) - assert talk.slides - assert 'slides' in response.content.decode() + assert 'download slides' in response.content.decode().lower() + + +def test_view_slides_remote_url_on_talk_detail_page(client): + """ + The view slides button only appears if the slides url has been uploaded. + """ + setup_conference_with_typical_fares() + talk = TalkFactory(status=TALK_STATUS.accepted) + url = talk.get_absolute_url() + + # Slides URL does not appear when the slides haven't been uploaded + response = client.get(url) + + assert not talk.slides_url + assert 'view slides' not in response.content.decode().lower() + + # Slides URL does appear when the slides have been uploaded + talk.slides_url = "ep2019.europython.eu" + talk.save() + + response = client.get(url) + + assert 'view slides' in response.content.decode().lower() From 55bf8a84f2342750cba1ca1b296504e2618c59db Mon Sep 17 00:00:00 2001 From: umgelurgel Date: Fri, 9 Aug 2019 15:40:33 +0200 Subject: [PATCH 2/3] Add mailing tool to email spkeaers who haven't uploaded slides. --- conference/admin.py | 12 +++++++++++- conference/debug_panel.py | 6 +++--- conference/settings.py | 2 -- conference/tickets.py | 4 ++-- p3/stats.py | 41 +++++++++++++++++++++++++++++++++++---- pycon/settings.py | 14 +------------ 6 files changed, 54 insertions(+), 25 deletions(-) diff --git a/conference/admin.py b/conference/admin.py index 76c2db9cb..1eb3a1636 100644 --- a/conference/admin.py +++ b/conference/admin.py @@ -138,7 +138,17 @@ def wrapper(*args, **kwargs): def available_stats(self, conf): stats = [] - for path in settings.ADMIN_ATTENDEE_STATS: + stats_modules = ( + 'p3.stats.tickets_status', + 'p3.stats.conference_speakers', + 'p3.stats.conference_speakers_day', + 'p3.stats.speaker_status', + 'p3.stats.presence_days', + 'p3.stats.shirt_sizes', + 'p3.stats.diet_types', + 'p3.stats.pp_tickets', + ) + for path in stats_modules: func = utils.dotted_import(path) w = { 'get_data': self._stat_wrapper(func, conf), diff --git a/conference/debug_panel.py b/conference/debug_panel.py index 9c5850bd2..20bea09f0 100644 --- a/conference/debug_panel.py +++ b/conference/debug_panel.py @@ -39,7 +39,7 @@ set_early_bird_fare_dates, set_regular_fare_dates, ) -from conference.tickets import count_number_of_sold_training_tickets_including_combined_tickets +from conference.tickets import sold_training_tickets_including_combined_tickets def get_current_commit_hash(): @@ -90,9 +90,9 @@ def debug_panel_index(request): ('Python_Version', platform.python_version()), ('Conference_current', Conference.objects.current()), ('SOLD_TRAINING_TICKETS', - count_number_of_sold_training_tickets_including_combined_tickets( + sold_training_tickets_including_combined_tickets( conference_code=settings.CONFERENCE_CONFERENCE, - )), + ).count()), ] allowed_settings = [ diff --git a/conference/settings.py b/conference/settings.py index 8254d06e1..2132bd4a0 100644 --- a/conference/settings.py +++ b/conference/settings.py @@ -128,8 +128,6 @@ def _CONFERENCE_TICKETS(conf, ticket_type=None, fare_code=None): SCHEDULE_ATTENDEES = getattr(settings, 'CONFERENCE_SCHEDULE_ATTENDEES', lambda schedule, forecast=False: 0) -ADMIN_ATTENDEE_STATS = getattr(settings, 'CONFERENCE_ADMIN_ATTENDEE_STATS', ()) - X_SENDFILE = getattr(settings, 'CONFERENCE_X_SENDFILE', None) TALK_VIDEO_ACCESS = getattr(settings, 'CONFERENCE_TALK_VIDEO_ACCESS', lambda r, t: True) diff --git a/conference/tickets.py b/conference/tickets.py index 53839bf64..354a0d7b5 100644 --- a/conference/tickets.py +++ b/conference/tickets.py @@ -20,7 +20,7 @@ def reset_ticket_settings(ticket): return tc -def count_number_of_sold_training_tickets_including_combined_tickets(conference_code): +def sold_training_tickets_including_combined_tickets(conference_code): qs = Ticket.objects.filter( fare__conference=conference_code, frozen=False, @@ -37,4 +37,4 @@ def count_number_of_sold_training_tickets_including_combined_tickets(conference_ ] ) ) - return qs.count() + return qs diff --git a/p3/stats.py b/p3/stats.py index 33d54fefb..cdcce06c7 100644 --- a/p3/stats.py +++ b/p3/stats.py @@ -7,8 +7,8 @@ from p3 import models from conference import models as cmodels -from conference.models import Ticket, Speaker, Talk -from conference.tickets import count_number_of_sold_training_tickets_including_combined_tickets +from conference.models import Ticket, Speaker, Talk, TALK_STATUS +from conference.tickets import sold_training_tickets_including_combined_tickets def _create_option(id, title, total_qs, **kwargs): @@ -198,6 +198,30 @@ def tickets_status(conf, code=None): elif code == 'spam_recruiting': output = ticket_status_for_spam_recruiting(spam_recruiting) + elif code == 'training_tickets_sold': + output = ticket_status_for_training_tickets_including_combined(conference_code=conf) + + return output + + +def ticket_status_for_training_tickets_including_combined(conference_code): + output = { + 'columns': ( + ('ticket', 'Ticket'), + ('name', 'Attendee name'), + ('email', 'Email'), + ), + 'data': [], + } + tickets = sold_training_tickets_including_combined_tickets(conference_code=conference_code) + + for ticket in tickets: + output['data'].append({ + 'name': ticket.user.assopy_user.name(), + 'email': ticket.user.email, + 'ticket': ticket, + }) + return output @@ -374,7 +398,7 @@ def ticket_status_no_code(conf, multiple_assignments, orphan_tickets, spam_recru { 'id': 'training_tickets_sold', 'title': 'Sold training tickets (including combined)', - 'total': count_number_of_sold_training_tickets_including_combined_tickets(conference_code=conf), + 'total': sold_training_tickets_including_combined_tickets(conference_code=conf).count(), }, _create_option( 'tickets_with_unique_email', @@ -454,13 +478,20 @@ def conference_speakers(conf, code=None): accepted_spks = Speaker.objects.byConference(conf) not_scheduled = Speaker.objects\ .filter(talkspeaker__talk__in=Talk.objects\ - .filter(conference=conf, status='accepted', event=None))\ + .filter(conference=conf, status=TALK_STATUS.accepted, event=None))\ .distinct() + no_slides = Speaker.objects.filter( + Q(talkspeaker__talk__conference=conf) & + Q(talkspeaker__talk__status=TALK_STATUS.accepted) & + Q(talkspeaker__talk__slides='') & + Q(talkspeaker__talk__slides_url='') + ).distinct() if code is None: return [ _create_option('all_speakers', 'All speakers', all_spks), _create_option('accepted_speakers', 'Speakers with accepted talks', accepted_spks), _create_option('speakers_not_scheduled', 'Speakers with unscheduled accepted talks', not_scheduled), + _create_option('speakers_no_slides', 'Speakers who have not uploaded slides', no_slides), ] else: if code == 'all_speakers': @@ -469,6 +500,8 @@ def conference_speakers(conf, code=None): qs = accepted_spks elif code == 'speakers_not_scheduled': qs = not_scheduled + elif code == 'speakers_no_slides': + qs = no_slides output = { 'columns': ( ('name', 'Name'), diff --git a/pycon/settings.py b/pycon/settings.py index 333279831..2282fb9f3 100644 --- a/pycon/settings.py +++ b/pycon/settings.py @@ -65,7 +65,7 @@ def _(x): # -------- DATABASES = { 'default': dj_database_url.config( - default='sqlite:///{}/epcon.db'.format(SITE_DATA_ROOT) + default='sqlite:///{}/epcon_staging.db'.format(SITE_DATA_ROOT) ), } @@ -715,18 +715,6 @@ def CONFERENCE_SCHEDULE_ATTENDEES(schedule, forecast): return 0 -CONFERENCE_ADMIN_ATTENDEE_STATS = ( - 'p3.stats.tickets_status', - 'p3.stats.conference_speakers', - 'p3.stats.conference_speakers_day', - 'p3.stats.speaker_status', - 'p3.stats.presence_days', - 'p3.stats.shirt_sizes', - 'p3.stats.diet_types', - 'p3.stats.pp_tickets', -) - - CONFERENCE_TICKET_BADGE_ENABLED = True CONFERENCE_TICKET_BADGE_PROG_ARGS = ['-e', '0', '-p', 'A4', '-n', '1'] From d38aae9a57a0f2934b22bdcd888233e5ef3c3987 Mon Sep 17 00:00:00 2001 From: umgelurgel Date: Fri, 9 Aug 2019 15:50:07 +0200 Subject: [PATCH 3/3] Add link back to slides to the mailer. --- p3/stats.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/p3/stats.py b/p3/stats.py index cdcce06c7..46657b99c 100644 --- a/p3/stats.py +++ b/p3/stats.py @@ -506,6 +506,7 @@ def conference_speakers(conf, code=None): 'columns': ( ('name', 'Name'), ('email', 'Email'), + ('talks', 'Talks'), ), 'data': [], } @@ -514,12 +515,20 @@ def conference_speakers(conf, code=None): .select_related('user')\ .order_by('user__first_name', 'user__last_name') for x in qs: + talks = x.talks().filter(conference=conf, status=TALK_STATUS.accepted) + if code == 'speakers_no_slides': + talks = talks.filter(Q(talkspeaker__talk__slides='') & Q(talkspeaker__talk__slides_url='')) + data.append({ 'name': '%s %s' % ( reverse('admin:auth_user_change', args=(x.user_id,)), x.user.first_name, x.user.last_name), 'email': x.user.email, + 'talks': '
'.join([ + f"{talk.title}" + for talk in talks + ]), 'uid': x.user_id, }) return output