Skip to content
This repository was archived by the owner on Apr 29, 2022. It is now read-only.

Commit 06a7a91

Browse files
authored
Ask potential speakers about their time slot availability (#1379)
* initial * renamed to something more sensible * Add a required checkbox to the CfP proposal submission page. The checkbox content is not stored in the database, but submission will fail unless checked. * added test to verify that speakers do have to agree to the speaker release agreement * initial * renamed to something more sensible * Added a template simple_tag to list a user tickets for the current conference. Uses functionality already implemented. * Added documentation * Added a template simple_tag to list a user tickets for the current conference. Uses functionality already implemented. * Upgraded jquery-flot to v0.8.3 for compatibility with django admin jquery version - v3.5.1 * Added timeslot availability selector in talk proposal form and admin. The admin might need some more work to make it nice * typo * implemented tests and updated talk factory for timezone availability
1 parent e9e40bc commit 06a7a91

File tree

9 files changed

+157
-33
lines changed

9 files changed

+157
-33
lines changed

conference/forms/forms.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,25 @@ class TalkBaseForm(forms.Form):
157157
help_text=_('<p>Please add anything you may find useful for the review of your session proposal, e.g. references of where you have held talks, blogs, YouTube channels, books you have written, etc. This information will only be shown for talk review purposes.</p>'),
158158
widget=forms.Textarea,
159159
required=False)
160+
161+
# For online conference we want to know availability in the defined
162+
# timeslots. Of course no timeslot defined, no need to ask :-)
163+
# Timeslots are defined in settings.py
164+
timeslots = getattr(settings, 'CONFERENCE_TIMESLOTS', None)
165+
if timeslots:
166+
availability = forms.MultipleChoiceField(
167+
label='Timezone Availability',
168+
choices=timeslots,
169+
help_text=_(
170+
'<p>Please select yout timezone availability. You can '
171+
'select multiple time-slots using SHIFT. Select '
172+
'non-contigous slots using CMD (macOS) or CTRL. You can '
173+
'deselect a selcted entry using CMD (macOS) or CTRL.</p>'
174+
),
175+
required=True,
176+
)
177+
178+
# Make sure that they agree to the Speaker Release Terms.
160179
i_accept_speaker_release = forms.BooleanField(
161180
label=RELEASE_AGREEMENT_CHECKBOX,
162181
required=True

conference/forms/talks.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ class TalkUpdateForm(forms.ModelForm):
2020
prerequisites = TalkBaseForm.base_fields["prerequisites"]
2121
level = TalkBaseForm.base_fields["level"]
2222
domain_level = TalkBaseForm.base_fields["domain_level"]
23+
if 'availability' in TalkBaseForm.base_fields:
24+
availability = TalkBaseForm.base_fields["availability"]
2325
i_accept_speaker_release = TalkBaseForm.base_fields[
2426
'i_accept_speaker_release'
2527
]
@@ -42,6 +44,7 @@ def __init__(self, *args, **kwargs):
4244

4345
if kwargs.get("instance"):
4446
self.fields["abstract"].initial = kwargs["instance"].getAbstract().body
47+
self.fields['availability'].initial = kwargs['instance'].get_availability()
4548

4649
def save(self, user):
4750
"""
@@ -52,6 +55,7 @@ def save(self, user):
5255
talk.created_by = user
5356
talk.slug = f"{talk.uuid}-{slugify(talk.title)}"
5457
talk.conference = Conference.objects.current().code
58+
talk.set_availability(self.cleaned_data['availability'])
5559
talk.save()
5660
talk.setAbstract(self.cleaned_data["abstract"])
5761

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 2.2.19 on 2021-04-24 14:38
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('conference', '0028_changes_to_conferencetag_and_conferencetaggeditem'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='talk',
15+
name='availability',
16+
field=models.TextField(blank=True, default='', help_text='<p>Please enter your time availability.</p>', verbose_name='Timezone availability'),
17+
),
18+
]

conference/models.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,13 @@ class Talk(models.Model):
647647
default="",
648648
)
649649

650+
availability = models.TextField(
651+
verbose_name=_('Timezone availability'),
652+
help_text=_('<p>Please enter your time availability.</p>'),
653+
blank=True,
654+
default='',
655+
)
656+
650657
slides = models.FileField(upload_to=_fs_upload_to("slides"), blank=True)
651658
slides_url = models.URLField(blank=True)
652659
repository_url = models.URLField(blank=True)
@@ -803,6 +810,13 @@ def get_abstract(self, language=None):
803810
else:
804811
return self.abstract_short
805812

813+
def set_availability(self, values, language=None):
814+
encoded_value = '|'.join(values)
815+
self.availability = encoded_value
816+
817+
def get_availability(self):
818+
return self.availability.split('|')
819+
806820

807821
class TalkSpeaker(models.Model):
808822
talk = models.ForeignKey(Talk, on_delete=models.CASCADE)

conference/talks.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,11 +134,15 @@ def dump_relevant_talk_information_to_dict(talk: Talk, speaker_tickets=None):
134134

135135
""" Dumps information about talk to a dictionary suitable for sending
136136
back as JSON.
137-
137+
138138
speaker_tickets may be given as dictionary mapping assigned to email
139139
to Ticket object and is used for defining has_ticket.
140-
140+
141141
"""
142+
if not talk.availability:
143+
availability = None
144+
else:
145+
availability = talk.availability.split('|')
142146
event = talk.get_event()
143147
if event is not None:
144148
event = event.json_dump()
@@ -163,6 +167,7 @@ def dump_relevant_talk_information_to_dict(talk: Talk, speaker_tickets=None):
163167
"event": event,
164168
"schedule_url": talk.get_schedule_url(),
165169
"slides_url": talk.get_slides_url(),
170+
"availability": availability,
166171
}
167172

168173
for speaker in talk.get_all_speakers():

pycon/settings.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,14 @@ def _(x):
454454

455455
CONFERENCE_CONFERENCE = 'ep2021'
456456
CONFERENCE_NAME = "EuroPython 2021"
457+
# IF The conference has timeslots and IF we want to ask speakers for their
458+
# availability in these timeslots, add/edit below. Otherwise just set to None
459+
# CONFERENCE_TIMESLOTS = None
460+
CONFERENCE_TIMESLOTS = (
461+
('morning', _('08-12 CEST / 06-10 UTC')),
462+
('afternoon', _('12-16 CEST / 10-14 UTC')),
463+
('evening', _('16-20 CEST / 14-18 UTC')),
464+
)
457465
CONFERENCE_SEND_EMAIL_TO = ["[email protected]"]
458466
CONFERENCE_TALK_SUBMISSION_NOTIFICATION_EMAIL = []
459467
CONFERENCE_VOTING_DISALLOWED = 'https://ep2021.europython.eu/talk-voting/'

tests/factories.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
)
2929
from conference.fares import ALL_POSSIBLE_FARE_CODES, TALK_VOTING_FARE_CODES
3030
from p3.models import TICKET_CONFERENCE_SHIRT_SIZES, TICKET_CONFERENCE_DIETS
31+
try:
32+
from pycon.settings import CONFERENCE_TIMESLOTS
33+
except ImportError:
34+
CONFERENCE_TIMESLOTS = None
3135

3236

3337
fake = Faker()
@@ -340,6 +344,11 @@ class Meta:
340344
Conference.objects.all().values_list("code", flat=True)
341345
)
342346
language = factory.Iterator(TALK_LANGUAGES, getter=lambda x: x[0])
347+
_slots = [ts[0] for ts in CONFERENCE_TIMESLOTS]
348+
if CONFERENCE_TIMESLOTS:
349+
availability = '|'.join(
350+
random.choices(_slots, k=random.randint(1, len(_slots)))
351+
)
343352

344353
@factory.post_generation
345354
def abstract(self, create, extracted, **kwargs):

tests/test_cfp.py

Lines changed: 71 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
dump_relevant_talk_information_to_dict,
1111
AddSpeakerToTalkForm,
1212
)
13+
try:
14+
from pycon.settings import CONFERENCE_TIMESLOTS
15+
except ImportError:
16+
CONFERENCE_TIMESLOTS = None
1317

1418
from tests.common_tools import redirects_to, template_used
1519
from tests.factories import TalkFactory
@@ -140,22 +144,23 @@ def test_if_user_can_submit_talk_details_and_is_redirect_to_step2(user_client):
140144
)
141145
step1_url = reverse("cfp:step1_submit_proposal")
142146

143-
response = user_client.post(
144-
step1_url,
145-
{
146-
"type": TALK_TYPE_CHOICES.t_30,
147-
"abstract": "Abstract goes here",
148-
"title": "A title",
149-
"sub_title": "A sub title",
150-
"abstract_short": "Short abstract",
151-
"abstract_extra": "Abstract _extra",
152-
"tags": "abc, defg",
153-
"level": TALK_LEVEL.beginner,
154-
"domain_level": TALK_LEVEL.advanced,
155-
"i_accept_speaker_release": True,
156-
},
157-
)
158-
147+
data = {
148+
"type": TALK_TYPE_CHOICES.t_30,
149+
"abstract": "Abstract goes here",
150+
"title": "A title",
151+
"sub_title": "A sub title",
152+
"abstract_short": "Short abstract",
153+
"abstract_extra": "Abstract _extra",
154+
"tags": "abc, defg",
155+
"level": TALK_LEVEL.beginner,
156+
"domain_level": TALK_LEVEL.advanced,
157+
"i_accept_speaker_release": True,
158+
}
159+
if CONFERENCE_TIMESLOTS and \
160+
isinstance(CONFERENCE_TIMESLOTS, (list, tuple)):
161+
data['availability'] = [CONFERENCE_TIMESLOTS[0][0], ]
162+
163+
response = user_client.post(step1_url, data)
159164
assert response.status_code == STEP1_CORRECT_REDIRECT_302
160165

161166
talk = Talk.objects.get()
@@ -169,12 +174,45 @@ def test_if_user_can_submit_talk_details_and_is_redirect_to_step2(user_client):
169174
assert talk_dict["python_level"] == "Beginner"
170175
assert talk_dict["domain_level"] == "Advanced"
171176
assert talk_dict["speakers"] == []
177+
if 'availability' in data:
178+
assert talk_dict['availability'] == data['availability']
172179

173180
assert redirects_to(
174181
response, reverse("cfp:step2_add_speakers", args=[talk.uuid])
175182
)
176183

177184

185+
@mark.skipif(CONFERENCE_TIMESLOTS is None, reason='no timeslot defined')
186+
def test_if_user_cannot_submit_talk_if_availability_not_selected(user_client):
187+
STEP1_VALIDATION_FAIL_200 = 200
188+
189+
Conference.objects.create(
190+
code=settings.CONFERENCE_CONFERENCE,
191+
name=settings.CONFERENCE_CONFERENCE,
192+
cfp_start=timezone.now().date() - timedelta(days=2),
193+
cfp_end=timezone.now().date() + timedelta(days=1),
194+
)
195+
step1_url = reverse("cfp:step1_submit_proposal")
196+
197+
response = user_client.post(
198+
step1_url,
199+
{
200+
"type": TALK_TYPE_CHOICES.t_30,
201+
"abstract": "Abstract goes here",
202+
"title": "A title",
203+
"sub_title": "A sub title",
204+
"abstract_short": "Short abstract",
205+
"abstract_extra": "Abstract _extra",
206+
"tags": "abc, defg",
207+
"level": TALK_LEVEL.beginner,
208+
"domain_level": TALK_LEVEL.advanced,
209+
"i_accept_speaker_release": True,
210+
},
211+
)
212+
213+
assert response.status_code == STEP1_VALIDATION_FAIL_200
214+
215+
178216
def test_if_user_cannot_submit_talk_if_release_not_selected(user_client):
179217
STEP1_VALIDATION_FAIL_200 = 200
180218

@@ -469,21 +507,23 @@ def test_update_proposal_updates_proposal(user_client):
469507

470508
edit_url = reverse("cfp:update", args=[talk.uuid])
471509

472-
response = user_client.post(
473-
edit_url,
474-
{
475-
"type": TALK_TYPE_CHOICES.t_45,
476-
"abstract": "New abstract",
477-
"abstract_short": "New short abstract",
478-
"abstract_extra": "New extra abstract",
479-
"level": TALK_LEVEL.intermediate,
480-
"domain_level": TALK_LEVEL.advanced,
481-
"title": "New title",
482-
"sub_title": "New sub title",
483-
"tags": "Some, tags",
484-
"i_accept_speaker_release": True,
485-
},
486-
)
510+
data = {
511+
"type": TALK_TYPE_CHOICES.t_45,
512+
"abstract": "New abstract",
513+
"abstract_short": "New short abstract",
514+
"abstract_extra": "New extra abstract",
515+
"level": TALK_LEVEL.intermediate,
516+
"domain_level": TALK_LEVEL.advanced,
517+
"title": "New title",
518+
"sub_title": "New sub title",
519+
"tags": "Some, tags",
520+
"i_accept_speaker_release": True,
521+
}
522+
if CONFERENCE_TIMESLOTS and \
523+
isinstance(CONFERENCE_TIMESLOTS, (list, tuple)):
524+
data['availability'] = [CONFERENCE_TIMESLOTS[0][0], ]
525+
526+
response = user_client.post(edit_url, data)
487527

488528
assert response.status_code == 302
489529
talk.refresh_from_db()

tests/test_talks.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88

99
from conference.cfp import dump_relevant_talk_information_to_dict
1010
from conference.models import TALK_STATUS, TALK_LEVEL
11+
try:
12+
from pycon.settings import CONFERENCE_TIMESLOTS
13+
except ImportError:
14+
CONFERENCE_TIMESLOTS = None
1115
from tests.factories import (
1216
EventFactory,
1317
UserFactory,
@@ -136,6 +140,9 @@ def test_update_talk_post(user_client):
136140
"tags": ",".join(tag.name for tag in tags),
137141
"i_accept_speaker_release": True,
138142
}
143+
if CONFERENCE_TIMESLOTS and \
144+
isinstance(CONFERENCE_TIMESLOTS, (list, tuple)):
145+
post_data['availability'] = [CONFERENCE_TIMESLOTS[0][0], ]
139146
resp = user_client.post(url, data=post_data)
140147

141148
talk.refresh_from_db()

0 commit comments

Comments
 (0)