From de3eba82ddef6f780c7e1cb7da3a9110b05cf978 Mon Sep 17 00:00:00 2001 From: Magnus Hagander Date: Sun, 18 Nov 2018 17:56:27 +0100 Subject: [PATCH] Reimplement multiple speaker handling in call for papers Instead of the awkward way with a subform, we now use selectize to populate a "dynamic" list of speakers. This is the same technology we've arleady used in the backend for some time with good results. This adds a dependency on jquery to the conference templates. If a conference template does not include jquery, multiple speakers will not work (but the rest of the form will keep working fine). This should be included before the pagescript block in the template to make sure it's available when the page runs. --- postgresqleu/confreg/README | 13 ++- postgresqleu/confreg/forms.py | 43 ++++++--- postgresqleu/confreg/views.py | 61 ++++++------- postgresqleu/urls.py | 1 + template.jinja/confreg/callforpapersform.html | 88 +++++++++++-------- 5 files changed, 121 insertions(+), 85 deletions(-) diff --git a/postgresqleu/confreg/README b/postgresqleu/confreg/README index 9728b94..cf6005c 100644 --- a/postgresqleu/confreg/README +++ b/postgresqleu/confreg/README @@ -41,8 +41,11 @@ subdirectory. Base template ------------- -In the base template, the entire HTML structure should be defined, and -also the blocks: +In the base template, the entire HTML structure should be defined. The +skin can do anything it wants with HTML, css and javascript. Note that +some pages rely on JQuery being present, so it should always be included. + +The following blocks should be defined in the base template: title This block gets the page title. Normally just defined as the @@ -50,7 +53,11 @@ title title, such as the name of the conference. extrahead This block must be declared inside , so the other templates - can insert javascript and css. By default it should be empty. + can insert e.g. css. By default it should be empty. +pagescript + This block must be declared either inside the or the , + but must be *after* jquery is loaded. This is where the pages will + insert local javascript. By default it should be empty. content This block gets the main content of the page. It should be declared inside whatever container elements are used. By default it should be diff --git a/postgresqleu/confreg/forms.py b/postgresqleu/confreg/forms.py index b62155f..e125be7 100644 --- a/postgresqleu/confreg/forms.py +++ b/postgresqleu/confreg/forms.py @@ -434,26 +434,37 @@ class SpeakerProfileForm(forms.ModelForm): return self.cleaned_data['fullname'] -class CallForPapersSpeakerForm(forms.Form): - email = forms.EmailField() - - def clean_email(self): - if not Speaker.objects.filter(user__email=self.cleaned_data['email']).exists(): - raise ValidationError("No speaker profile for user with email %s exists." % self.cleaned_data['email']) - return self.cleaned_data['email'] - -class CallForPapersSubmissionForm(forms.Form): - title = forms.CharField(required=True, max_length=200, min_length=10) - class CallForPapersForm(forms.ModelForm): class Meta: model = ConferenceSession - fields = ('title', 'abstract', 'skill_level', 'track', 'submissionnote',) + fields = ('title', 'abstract', 'skill_level', 'track', 'speaker', 'submissionnote') + + def __init__(self, currentspeaker, *args, **kwargs): + self.currentspeaker = currentspeaker - def __init__(self, *args, **kwargs): super(CallForPapersForm, self).__init__(*args, **kwargs) + + # Extra speakers should at this point only contain the ones that are already + # there. More are added by the javascript code, but we don't want to populate + # with a list of everything (too easy to scrape as well). + if self.instance.id: + vals = [s.pk for s in self.instance.speaker.all()] + else: + vals = [s.pk for s in self.initial['speaker']] + # We may also have received a POST that contains new speakers not already on this + # record. In this case, we have to add them as possible values, so the validation + # doesn't fail. + if 'data' in kwargs and u'speaker' in kwargs['data']: + vals.extend([int(x) for x in kwargs['data'].getlist('speaker')]) + + self.fields['speaker'].queryset = Speaker.objects.filter(pk__in=vals) + self.fields['speaker'].label_from_instance = lambda x: u"{0} <{1}>".format(x.fullname, x.email) + self.fields['speaker'].required = True + self.fields['speaker'].help_text = "Type the beginning of a speakers email address to add more speakers" + if not self.instance.conference.skill_levels: del self.fields['skill_level'] + if not self.instance.conference.track_set.filter(incfp=True).count() > 0: del self.fields['track'] else: @@ -470,6 +481,12 @@ class CallForPapersForm(forms.ModelForm): raise ValidationError("Please choose the track that is the closest match to your talk") return self.cleaned_data.get('track') + def clean_speaker(self): + if not self.currentspeaker in self.cleaned_data.get('speaker'): + raise ValidationError("You cannot remove yourself as a speaker!") + return self.cleaned_data.get('speaker') + + class SessionCopyField(forms.ModelMultipleChoiceField): def label_from_instance(self, obj): return u"{0}: {1} ({2})".format(obj.conference, obj.title, obj.status_string) diff --git a/postgresqleu/confreg/views.py b/postgresqleu/confreg/views.py index 3065d60..4f88b85 100644 --- a/postgresqleu/confreg/views.py +++ b/postgresqleu/confreg/views.py @@ -26,7 +26,7 @@ from models import STATUS_CHOICES from models import ConferenceNews from forms import ConferenceRegistrationForm, RegistrationChangeForm, ConferenceSessionFeedbackForm from forms import ConferenceFeedbackForm, SpeakerProfileForm -from forms import CallForPapersForm, CallForPapersSpeakerForm +from forms import CallForPapersForm from forms import CallForPapersCopyForm, PrepaidCreateForm, BulkRegistrationForm from forms import EmailSendForm, EmailSessionForm, CrossConferenceMailForm from forms import AttendeeMailForm, WaitlistOfferForm, WaitlistSendmailForm, TransferRegForm @@ -1296,53 +1296,50 @@ def callforpapers_edit(request, confname, sessionid): 'slides': ConferenceSessionSlides.objects.filter(session=session), }) - SpeakerFormset = formsets.formset_factory(CallForPapersSpeakerForm, can_delete=True, extra=1) - - if sessionid != 'new': - # Get all additional speakers (that means all speakers who isn't the current one) - speaker_initialdata = [{'email': s.user.email} for s in session.speaker.exclude(user=request.user)] + if session.id: + initial = {} else: - speaker_initialdata = None + initial = { + 'speaker': [speaker, ], + } if request.method == 'POST': # Save it! - form = CallForPapersForm(data=request.POST, instance=session) - speaker_formset = SpeakerFormset(data=request.POST, initial=speaker_initialdata, prefix="extra_speakers") - if form.is_valid() and speaker_formset.is_valid(): + form = CallForPapersForm(speaker, data=request.POST, instance=session, initial=initial) + if form.is_valid(): form.save() - # Explicitly add the submitter as a speaker - session.speaker.add(speaker) - - if speaker_formset.has_changed(): - # Additional speaker either added or removed - for f in speaker_formset: - # There is at least one empty form at the end, so skip it - if not getattr(f, 'cleaned_data', False): continue - # Somehow we can end up with an unspecified email. Not sure how it can happen, - # since the field is mandatory, but if it does just ignore it. - if not 'email' in f.cleaned_data: continue - - # Speaker always exist, since the form has validated - spk = Speaker.objects.get(user__email=f.cleaned_data['email']) - - if f.cleaned_data['DELETE']: - session.speaker.remove(spk) - else: - session.speaker.add(spk) messages.info(request, "Your session '%s' has been saved!" % session.title) return HttpResponseRedirect("../") else: # GET --> render empty form - form = CallForPapersForm(instance=session) - speaker_formset = SpeakerFormset(initial=speaker_initialdata, prefix="extra_speakers") + form = CallForPapersForm(speaker, instance=session, initial=initial) return render_conference_response(request, conference, 'cfp', 'confreg/callforpapersform.html', { 'form': form, - 'speaker_formset': speaker_formset, 'session': session, }) +@login_required +def public_speaker_lookup(request, confname): + conference = get_object_or_404(Conference, urlname=confname) + speaker = get_object_or_404(Speaker, user=request.user) + + # This is a lookup for speakers that's public. To avoid harvesting, we allow + # only *prefix* matching of email addresses, and you have to type at least 6 characters + # before you get anything. + prefix = request.GET['query'] + if len(prefix) > 5: + vals = [{ + 'id': s.id, + 'value': u"{0} <{1}>".format(s.fullname, s.email), + } for s in Speaker.objects.filter(user__email__startswith=prefix).exclude(fullname='')] + else: + vals = [] + return HttpResponse(json.dumps({ + 'values': vals, + }), content_type='application/json') + @login_required @transaction.atomic def callforpapers_copy(request, confname): diff --git a/postgresqleu/urls.py b/postgresqleu/urls.py index 3de4a75..c2ece47 100644 --- a/postgresqleu/urls.py +++ b/postgresqleu/urls.py @@ -88,6 +88,7 @@ urlpatterns = [ url(r'^events/([^/]+)/callforpapers/copy/$', postgresqleu.confreg.views.callforpapers_copy), url(r'^events/([^/]+)/callforpapers/(\d+)/delslides/(\d+)/$', postgresqleu.confreg.views.callforpapers_delslides), url(r'^events/([^/]+)/callforpapers/(\d+)/speakerconfirm/$', postgresqleu.confreg.views.callforpapers_confirm), + url(r'^events/([^/]+)/callforpapers/lookups/speakers/$', postgresqleu.confreg.views.public_speaker_lookup), url(r'^events/([^/]+)/register/confirm/$', postgresqleu.confreg.views.confirmreg), url(r'^events/([^/]+)/register/waitlist_signup/$', postgresqleu.confreg.views.waitlist_signup), url(r'^events/([^/]+)/register/waitlist_cancel/$', postgresqleu.confreg.views.waitlist_cancel), diff --git a/template.jinja/confreg/callforpapersform.html b/template.jinja/confreg/callforpapersform.html index 88d8039..5496fdd 100644 --- a/template.jinja/confreg/callforpapersform.html +++ b/template.jinja/confreg/callforpapersform.html @@ -1,5 +1,50 @@ {%extends "base.html" %} {%block title%}Call for Papers - {{conference}}{%endblock%} +{%block pagescript%} + + + + + +{%endblock%} + +{%block extrahead%} + +{%endblock%} + {%block content%}

Call for Papers - {{conference}}

@@ -20,43 +65,12 @@ Please complete the following fields. You may use markdown in the abstract. {% endif %}
{{ csrf_input }} -{{form.as_table()}} - - - - +{%for f in form%} + + + + +{%endfor%}
-

- If there will be more than one speaker of this session, please add them below by - entering their email addresses. Note that all speakers must set up their - own speaker profile before they can be added. -

- {{ speaker_formset.non_form_errors().as_ul() }} - {{ speaker_formset.management_form }} - - {%for f in speaker_formset.forms%} - {%if loop.first%} - - {%for field in f.visible_fields()%} - - {%endfor%} - - {%endif%} - - {%for field in f.visible_fields()%} - - {%endfor%} - - {%endfor%} -
{{field.label}}
- {%if loop.first%} - {%for hidden in form.hidden_fields()%} - {{hidden}} - {%endfor%} - {%endif%} - {{field.errors.as_ul()}} - {{field}} -
-
{{f.label_tag()}}{{f}}{%if f.errors%}{{f.errors}}{%endif%}
-- 2.39.5