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
title, such as the name of the conference.
extrahead
This block must be declared inside <head>, 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 <head> or the <body>,
+ 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
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:
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)
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
'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):
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),
{%extends "base.html" %}
{%block title%}Call for Papers - {{conference}}{%endblock%}
+{%block pagescript%}
+<script type="text/javascript" src="/media/js/selectize.min.js"></script>
+<link rel="stylesheet" href="/media/css/selectize.css" />
+<link rel="stylesheet" href="/media/css/selectize.default.css" />
+
+<script language="javascript">
+$(function() {
+ /* Re-enable the speaker field, and turn it into selectize */
+ $('tr#tr_speaker').css({'display': 'table-row'});
+ $('#id_speaker').selectize({
+ plugins: ['remove_button'],
+ valueField: 'id',
+ labelField: 'value',
+ searchField: 'value',
+ load: function(query, callback) {
+ if (!query.length) return callback();
+ $.ajax({
+ 'url': '/events/{{conference.urlname}}/callforpapers/lookups/speakers/',
+ 'type': 'GET',
+ 'dataType': 'json',
+ 'data': {
+ 'query': query,
+ },
+ 'error': function() { callback(); },
+ 'success': function(res) { callback(res.values);},
+ });
+ },
+ });
+});
+</script>
+{%endblock%}
+
+{%block extrahead%}
+<style>
+tr.err {
+ background-color: #ffb6b6;
+}
+
+/* Hide the speaker field for non-javascript sessions */
+tr#tr_speaker {
+ display:none;
+}
+</style>
+{%endblock%}
+
{%block content%}
<h1>Call for Papers - {{conference}}</h1>
{% endif %}
<form class="pgeucfpform" method="post" action=".">{{ csrf_input }}
<table id="cfp_table">
-{{form.as_table()}}
-<tr>
- <th><label for="id_not_real">Additional speakers:</label></th>
- <td>
- <p>
- 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.
- </p>
- {{ speaker_formset.non_form_errors().as_ul() }}
- {{ speaker_formset.management_form }}
- <table id="cfp_form_speakers_list">
- {%for f in speaker_formset.forms%}
- {%if loop.first%}
- <tr>
- {%for field in f.visible_fields()%}
- <th>{{field.label}}</th>
- {%endfor%}
- </tr>
- {%endif%}
- <tr>
- {%for field in f.visible_fields()%}
- <td>
- {%if loop.first%}
- {%for hidden in form.hidden_fields()%}
- {{hidden}}
- {%endfor%}
- {%endif%}
- {{field.errors.as_ul()}}
- {{field}}
- </td>
- {%endfor%}
- </tr>
- {%endfor%}
- </table>
- </td>
-</tr>
+{%for f in form%}
+ <tr{%if f.errors%} class="err"{%endif%} id="tr_{{f.name}}">
+ <th>{{f.label_tag()}}</th>
+ <td>{{f}}{%if f.errors%}{{f.errors}}{%endif%}</td>
+ </tr>
+{%endfor%}
</table>
<input type="submit" value="Save">
<input class="button" type="button" onclick="window.location.href = '../'" value="Cancel" />