Merge lp:~cjohnston/summit/registration-form into lp:summit

Proposed by Chris Johnston
Status: Merged
Approved by: Chris Johnston
Approved revision: 522
Merged at revision: 523
Proposed branch: lp:~cjohnston/summit/registration-form
Merge into: lp:summit
Diff against target: 595 lines (+503/-1)
10 files modified
EXTERNALS (+7/-0)
summit/common/widgets.py (+122/-0)
summit/media/css/style.css (+17/-0)
summit/media/js/events-ui.js (+11/-0)
summit/schedule/forms.py (+31/-0)
summit/schedule/templates/schedule/form.html (+41/-0)
summit/schedule/tests/__init__.py (+1/-0)
summit/schedule/tests/registration.py (+222/-0)
summit/schedule/views.py (+46/-1)
summit/urls.py (+5/-0)
To merge this branch: bzr merge lp:~cjohnston/summit/registration-form
Reviewer Review Type Date Requested Status
Adnane Belmadiaf Approve
Review via email: mp+156978@code.launchpad.net

This proposal supersedes a proposal from 2013-04-03.

Commit message

Adds a registration form to register as attending a sprint.

Description of the change

This adds a registration form as well as a generic form.html template, which I hope to be able to reuse for more (all) forms instead of having to have a custom template for each form. Note: This does not include a link to the registration page. summit.html will need some re-work for that, so I will be doing that in a separate branch.

To post a comment you must log in.
Revision history for this message
Adnane Belmadiaf (daker) wrote :

The timewidget doesn't select the correct value( see bug 1164296 ), for the forms.Select there should an initial value if the user have already set his availability.

519. By Chris Johnston

Use async loading

520. By Chris Johnston

Fixes issue with select widget

521. By Chris Johnston

Update from trunk

522. By Chris Johnston

Removes css/js

Revision history for this message
Adnane Belmadiaf (daker) wrote :

+1 from me :)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'EXTERNALS'
2--- EXTERNALS 2012-04-22 20:20:10 +0000
3+++ EXTERNALS 2013-04-08 21:02:19 +0000
4@@ -19,3 +19,10 @@
5
6 - Twitter JS
7 http://twitter.com/javascripts/blogger.js
8+
9+- Date Picker
10+provided by the JQuery UI project. See http://jqueryui.com/
11+
12+- DateTimeWidget inspired by:
13+http://copiesofcopies.org/webl/2010/04/26/a-better-datetime-widget-for-django/
14+
15
16=== added file 'summit/common/widgets.py'
17--- summit/common/widgets.py 1970-01-01 00:00:00 +0000
18+++ summit/common/widgets.py 2013-04-08 21:02:19 +0000
19@@ -0,0 +1,122 @@
20+# -*- coding: utf-8 -*-
21+from datetime import time
22+from time import strptime, strftime
23+
24+from django import forms
25+
26+
27+class DateWidget(forms.DateInput):
28+ """
29+ A more-friendly date widget with a pop-up calendar.
30+ """
31+ def __init__(self, attrs=None):
32+ self.date_class = 'datepicker'
33+ if not attrs:
34+ attrs = {}
35+ if 'date_class' in attrs:
36+ self.date_class = attrs.pop('date_class')
37+ if 'class' not in attrs:
38+ attrs['class'] = 'date'
39+
40+ super(DateWidget, self).__init__(attrs=attrs)
41+
42+ def render(self, name, value, attrs=None):
43+ return '<span class="%s">%s</span>' % (
44+ self.date_class,
45+ super(DateWidget, self).render(name, value, attrs)
46+ )
47+
48+
49+class TimeWidget(forms.MultiWidget):
50+ """
51+ A more-friendly time widget.
52+ """
53+ def __init__(self, attrs=None):
54+ self.time_class = 'timepicker'
55+ if not attrs:
56+ attrs = {}
57+ if 'time_class' in attrs:
58+ self.time_class = attrs.pop('time_class')
59+ if 'class' not in attrs:
60+ attrs['class'] = 'time'
61+
62+ widgets = (
63+ forms.Select(
64+ attrs=attrs,
65+ choices=[(i + 1, "%02d" % (i + 1)) for i in range(0, 12)],
66+ ),
67+ forms.Select(
68+ attrs=attrs,
69+ choices=[(i, "%02d" % i) for i in range(00, 60, 15)],
70+ ),
71+ forms.Select(
72+ attrs=attrs,
73+ choices=[('AM', 'AM'), ('PM', 'PM')],
74+ )
75+ )
76+
77+ super(TimeWidget, self).__init__(widgets, attrs)
78+
79+ def decompress(self, value):
80+ if isinstance(value, str):
81+ value = strptime(value, '%I:%M %p')
82+ hour = int(value.tm_hour)
83+ minute = int(value.tm_min)
84+ if hour < 12:
85+ meridian = 'AM'
86+ else:
87+ meridian = 'PM'
88+ hour -= 12
89+ return (hour, minute, meridian)
90+
91+ elif isinstance(value, time):
92+ hour = int(value.strftime("%I"))
93+ minute = int(value.strftime("%M"))
94+ meridian = value.strftime("%p")
95+ return (hour, minute, meridian)
96+ return (None, None, None)
97+
98+ def value_from_datadict(self, data, files, name):
99+ value = super(TimeWidget, self).value_from_datadict(data, files, name)
100+ t = strptime(
101+ "%02d:%02d %s" % (
102+ int(value[0]),
103+ int(value[1]),
104+ value[2]
105+ ),
106+ "%I:%M %p",
107+ )
108+ return strftime("%H:%M:%S", t)
109+
110+ def format_output(self, rendered_widgets):
111+ return '<span class="%s">%s%s%s</span>' % (
112+ self.time_class,
113+ rendered_widgets[0], rendered_widgets[1], rendered_widgets[2]
114+ )
115+
116+
117+class DateTimeWidget(forms.SplitDateTimeWidget):
118+ """
119+ A more-friendly date/time widget.
120+
121+ Inspired by:
122+
123+ http://copiesofcopies.org/webl/2010/04/26/a-better-datetime-widget-for-django/
124+ """
125+ def __init__(self, attrs=None, date_format=None, time_format=None):
126+ super(DateTimeWidget, self).__init__(attrs, date_format, time_format)
127+ self.widgets = (
128+ DateWidget(attrs=attrs),
129+ TimeWidget(attrs=attrs),
130+ )
131+
132+ def decompress(self, value):
133+ if value:
134+ d = strftime("%Y-%m-%d", value.timetuple())
135+ t = strftime("%I:%M %p", value.timetuple())
136+ return (d, t)
137+ else:
138+ return (None, None)
139+
140+ def format_output(self, rendered_widgets):
141+ return '%s%s' % (rendered_widgets[0], rendered_widgets[1])
142
143=== modified file 'summit/media/css/style.css'
144--- summit/media/css/style.css 2012-01-22 18:36:40 +0000
145+++ summit/media/css/style.css 2013-04-08 21:02:19 +0000
146@@ -66,3 +66,20 @@
147 .summit-columns ul li h3 a:hover { text-decoration: underline; }
148 .summit-columns ul li img { padding: 10px 0 5px; }
149 .summit-columns p { margin-bottom: 5px; }
150+
151+#id_start_utc_0, #id_start_utc_1,
152+#id_end_utc_0, #id_end_utc_1,
153+#id_start_utc_1_0, #id_start_utc_1_1, #id_start_utc_1_2,
154+#id_end_utc_1_0, #id_end_utc_1_1, #id_end_utc_1_2 {
155+ width: 100px;
156+ display: inline;
157+}
158+
159+#id_start_utc_1_0, #id_start_utc_1_1, #id_start_utc_1_2,
160+#id_end_utc_1_0, #id_end_utc_1_1, #id_end_utc_1_2 {
161+ width: 60px;
162+}
163+
164+#id_name, #id_announce, #id_registration {
165+ width: 350px;
166+}
167
168=== added file 'summit/media/js/events-ui.js'
169--- summit/media/js/events-ui.js 1970-01-01 00:00:00 +0000
170+++ summit/media/js/events-ui.js 2013-04-08 21:02:19 +0000
171@@ -0,0 +1,11 @@
172+$(document).ready(function(){
173+
174+ $.datepicker.setDefaults({
175+ showOn: 'focus',
176+ dateFormat: 'yy-mm-dd',
177+ });
178+
179+ $("#id_start_utc_0").datepicker();
180+ $("#id_end_utc_0").datepicker();
181+
182+});
183
184=== modified file 'summit/schedule/forms.py'
185--- summit/schedule/forms.py 2013-03-09 05:14:19 +0000
186+++ summit/schedule/forms.py 2013-04-08 21:02:19 +0000
187@@ -14,6 +14,7 @@
188 # You should have received a copy of the GNU Affero General Public License
189 # along with this program. If not, see <http://www.gnu.org/licenses/>.
190
191+from django.conf import settings
192 from django import forms
193
194 from summit.schedule.models.meetingmodel import Meeting
195@@ -21,6 +22,7 @@
196 from summit.schedule.models.participantmodel import Participant
197
198 from common.forms import RenderableMixin
199+from common.widgets import DateTimeWidget
200
201
202 class MultipleAttendeeField(forms.ModelMultipleChoiceField):
203@@ -226,3 +228,32 @@
204 fields = ('hangout_url', 'broadcast_url')
205
206 broadcast_url = YouTubeEmbedURL(label='Broadcast URL')
207+
208+
209+class Registration(forms.ModelForm, RenderableMixin):
210+ class Meta:
211+ model = Attendee
212+ fields = (
213+ 'start_utc',
214+ 'end_utc',
215+ 'crew',
216+ )
217+
218+ def __init__(self, *args, **kargs):
219+ super(Registration, self).__init__(*args, **kargs)
220+ self.fields['start_utc'].widget = DateTimeWidget()
221+ self.fields['end_utc'].widget = DateTimeWidget()
222+
223+ def clean(self):
224+ begin = self.cleaned_data.get('start_utc')
225+ end = self.cleaned_data.get('end_utc')
226+ if begin and end and begin > end:
227+ raise forms.ValidationError(
228+ "Availability can not end before it starts."
229+ )
230+ return self.cleaned_data
231+
232+ def save(self, commit=True):
233+ instance = super(Registration, self).save(commit)
234+ instance.from_launchpad = False
235+ instance.save()
236
237=== added file 'summit/schedule/templates/schedule/form.html'
238--- summit/schedule/templates/schedule/form.html 1970-01-01 00:00:00 +0000
239+++ summit/schedule/templates/schedule/form.html 2013-04-08 21:02:19 +0000
240@@ -0,0 +1,41 @@
241+{% extends "base.html" %}
242+
243+{% block page_name %}{{ form_title }} - {{ summit.title }}{%endblock %}
244+{% block sub_nav %}{% endblock %}
245+
246+{% block closure %}
247+<script type="text/javascript"><!--
248+$(document).ready(function(){
249+ $('span[rel*=help]').colorTip({color:'orange'});
250+});
251+--></script>
252+<style>
253+form ul {
254+ height: 12em;
255+ overflow-y: scroll;
256+ overflow-x: hidden;
257+}
258+</style>
259+{% endblock %}
260+
261+
262+{% block content %}
263+<div class="row">
264+ <article id="form" class="span-8">
265+ {% if form.errors %}
266+ <p style="color: red;">
267+ Please correct the error{{ form.errors|pluralize }} below.
268+ </p>
269+ {% endif %}
270+
271+ <form action="{{ request.path_info }}" method="POST">{% csrf_token %}
272+ <fieldset>
273+ <h3>{{ form_title }}</h3>
274+ {{ form.as_template }}
275+ {% if is_popup %}<input type="hidden" name="_popup" value="1">{% endif %}
276+ <input type="submit" name="submit" value="Create" class="submit-button" />
277+ </fieldset>
278+ </form>
279+ </article>
280+</div>
281+{% endblock %}
282
283=== modified file 'summit/schedule/tests/__init__.py'
284--- summit/schedule/tests/__init__.py 2013-03-14 20:52:12 +0000
285+++ summit/schedule/tests/__init__.py 2013-04-08 21:02:19 +0000
286@@ -31,3 +31,4 @@
287 from schedule import *
288 from summit_model import *
289 from propose_meeting import *
290+from registration import RegistrationTestCase
291
292=== added file 'summit/schedule/tests/registration.py'
293--- summit/schedule/tests/registration.py 1970-01-01 00:00:00 +0000
294+++ summit/schedule/tests/registration.py 2013-04-08 21:02:19 +0000
295@@ -0,0 +1,222 @@
296+# The Summit Scheduler web application
297+# Copyright (C) 2008 - 2013 Ubuntu Community, Canonical Ltd
298+#
299+# This program is free software: you can redistribute it and/or modify
300+# it under the terms of the GNU Affero General Public License as
301+# published by the Free Software Foundation, either version 3 of the
302+# License, or (at your option) any later version.
303+#
304+# This program is distributed in the hope that it will be useful,
305+# but WITHOUT ANY WARRANTY; without even the implied warranty of
306+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
307+# GNU Affero General Public License for more details.
308+#
309+# You should have received a copy of the GNU Affero General Public License
310+# along with this program. If not, see <http://www.gnu.org/licenses/>.
311+
312+import datetime
313+
314+from django import test as djangotest
315+from django.core.urlresolvers import reverse
316+
317+from django.contrib.auth.models import User
318+from django.test.client import Client
319+
320+from model_mommy import mommy as factory
321+
322+from summit.schedule.models import (
323+ Summit,
324+ Attendee,
325+)
326+
327+
328+class RegistrationTestCase(djangotest.TestCase):
329+ """
330+ Tests for registering to attend a Summit
331+ """
332+ c = Client()
333+
334+ def setUp(self):
335+ self.now = datetime.datetime.utcnow()
336+ self.one_hour = datetime.timedelta(0, 3600)
337+ self.one_day = datetime.timedelta(days=1)
338+ self.week = datetime.timedelta(days=5)
339+ self.end_summit = self.now + self.week
340+ self.summit = factory.make_one(
341+ Summit,
342+ name='test-summit',
343+ title='Test Summit',
344+ virtual_summit=True,
345+ date_start=self.now,
346+ date_end=self.now + self.week,
347+ timezone='UTC',
348+ )
349+
350+ self.user1 = factory.make_one(
351+ User,
352+ username='testuser',
353+ first_name='Test',
354+ last_name='User',
355+ is_active=True,
356+ is_superuser=False,
357+ )
358+ self.user1.set_password('password')
359+ self.user1.save()
360+
361+ def create_attendee(self):
362+ self.attendee1 = factory.make_one(
363+ Attendee,
364+ summit=self.summit,
365+ user=self.user1,
366+ start_utc=self.now,
367+ end_utc=self.now+self.week
368+ )
369+
370+ def tearDown(self):
371+ self.client.logout()
372+
373+ def login(self):
374+ logged_in = self.c.login(
375+ username='testuser',
376+ password='password')
377+ self.assertTrue(logged_in)
378+
379+ def get_attendee(self):
380+ attendee = Attendee.objects.get(user=self.user1)
381+ return attendee
382+
383+ def with_summit_registration_times(self):
384+ """
385+ Using the start and end of the summit as the registration times
386+ """
387+ self.start_date = self.now.strftime("%Y-%m-%d")
388+ self.start_hour = self.now.strftime("%I")
389+ self.start_minute = self.now.strftime("%M")
390+ self.start_period = self.now.strftime("%p")
391+ self.end_hour = self.end_summit.strftime("%I")
392+ self.end_minute = self.end_summit.strftime("%M")
393+ self.end_period = self.end_summit.strftime("%p")
394+ self.end_date = self.end_summit.strftime("%Y-%m-%d")
395+ self.edit_registration_form()
396+ attendee = self.get_attendee()
397+ self.assertEqual(
398+ attendee.start_utc.replace(second=0),
399+ self.now.replace(second=0, microsecond=0),
400+ )
401+ self.assertEqual(
402+ attendee.end_utc.replace(second=0, microsecond=0),
403+ self.end_summit.replace(second=0, microsecond=0),
404+ )
405+
406+ def with_custom_registration_times(self):
407+ """
408+ Using custom start and end registration times
409+ """
410+ start = self.now + self.one_day
411+ self.start_date = start.strftime("%Y-%m-%d")
412+ self.start_hour = self.now.strftime("%I")
413+ self.start_minute = self.now.strftime("%M")
414+ self.start_period = self.now.strftime("%p")
415+ self.end_hour = self.end_summit.strftime("%I")
416+ self.end_minute = self.end_summit.strftime("%M")
417+ self.end_period = self.end_summit.strftime("%p")
418+ self.end_date = self.end_summit.strftime("%Y-%m-%d")
419+ self.edit_registration_form()
420+ attendee = self.get_attendee()
421+ self.assertEqual(
422+ attendee.start_utc.replace(second=0),
423+ start.replace(second=0, microsecond=0),
424+ )
425+ self.assertEqual(
426+ attendee.end_utc.replace(second=0, microsecond=0),
427+ self.end_summit.replace(second=0, microsecond=0),
428+ )
429+
430+ def edit_registration_form(self):
431+ """
432+ Tests that a user can register for a Summit
433+ """
434+ # Define data to fill our the form
435+
436+ # Post the form
437+ post = self.c.post(
438+ reverse(
439+ 'registration',
440+ args=(self.summit.name,)
441+ ),
442+ data={
443+ 'end_utc_0': self.end_date,
444+ 'end_utc_1_0': self.end_hour,
445+ 'end_utc_1_1': self.end_minute,
446+ 'end_utc_1_2': self.end_period,
447+ 'start_utc_1_0': self.start_hour,
448+ 'start_utc_1_1': self.start_minute,
449+ 'start_utc_1_2': self.start_period,
450+ 'start_utc_0': self.start_date,
451+ }
452+ )
453+
454+ # A successful post should redirect to the summit page
455+ response = reverse(
456+ 'summit.schedule.views.summit',
457+ args=(self.summit.name,)
458+ )
459+ self.assertEqual(post.status_code, 302)
460+ self.assertRedirects(post, response)
461+ attendee = self.get_attendee()
462+ self.assertEqual(attendee.user, self.user1)
463+ self.assertEqual(attendee.from_launchpad, False)
464+
465+ def test_non_attendee_registers(self):
466+ self.login()
467+ self.assertRaises(Attendee.DoesNotExist, lambda: self.get_attendee())
468+ rev_args = [self.summit.name, ]
469+ response = self.c.get(reverse('registration', args=rev_args))
470+ self.assertEqual(response.status_code, 200)
471+ self.assertTemplateUsed(response, 'schedule/form.html')
472+ self.assertIn('Register for ' + self.summit.title, response.content)
473+ self.with_summit_registration_times()
474+
475+ def test_attendee_updates_registration(self):
476+ self.create_attendee()
477+ self.login()
478+ self.assertEquals(
479+ self.user1.username,
480+ self.get_attendee().user.username,
481+ )
482+ rev_args = [self.summit.name, ]
483+ response = self.c.get(reverse('registration', args=rev_args))
484+ self.assertEqual(response.status_code, 200)
485+ self.assertTemplateUsed(response, 'schedule/form.html')
486+ self.assertIn(
487+ 'Update registration for ' + self.summit.title,
488+ response.content
489+ )
490+ self.with_custom_registration_times()
491+
492+ def test_update_registration_from_launchpad_true(self):
493+ """
494+ update resgistration with from_launchpad=True
495+ to save from_launchpad=False
496+ """
497+ self.create_attendee()
498+ self.attendee1.from_launchpad = True
499+ self.attendee1.save()
500+ self.login()
501+ self.assertEquals(
502+ self.user1.username,
503+ self.get_attendee().user.username,
504+ )
505+ self.assertEquals(
506+ True,
507+ self.get_attendee().from_launchpad,
508+ )
509+ rev_args = [self.summit.name, ]
510+ response = self.c.get(reverse('registration', args=rev_args))
511+ self.assertEqual(response.status_code, 200)
512+ self.assertTemplateUsed(response, 'schedule/form.html')
513+ self.assertIn(
514+ 'Update registration for ' + self.summit.title,
515+ response.content
516+ )
517+ self.with_custom_registration_times()
518
519=== modified file 'summit/schedule/views.py'
520--- summit/schedule/views.py 2013-03-15 01:06:10 +0000
521+++ summit/schedule/views.py 2013-04-08 21:02:19 +0000
522@@ -52,7 +52,8 @@
523 MeetingReview,
524 AttendMeeting,
525 OrganizerChangeAttend,
526- EditMeetingHangout
527+ EditMeetingHangout,
528+ Registration,
529 )
530
531 __all__ = (
532@@ -1173,3 +1174,47 @@
533 context,
534 RequestContext(request)
535 )
536+
537+
538+@login_required
539+@summit_required
540+def registration_form(request, summit, attendee):
541+ registration_args = dict()
542+
543+ if attendee is None:
544+ attendee = Attendee(
545+ user=request.user,
546+ summit=summit,
547+ from_launchpad=False,
548+ start_utc=summit.start or summit.date_start,
549+ end_utc=summit.end or summit.date_end,
550+ )
551+ registration_args['instance'] = attendee
552+ form_title="Register for %s" % summit.title
553+ else:
554+ registration_args['instance'] = attendee
555+ form_title="Update registration for %s" % summit.title
556+
557+ if request.method == 'POST':
558+ form = Registration(data=request.POST, **registration_args)
559+ if form.is_valid():
560+ form.save()
561+ return HttpResponseRedirect(
562+ reverse(
563+ 'summit.schedule.views.summit',
564+ args=(summit.name,)
565+ )
566+ )
567+ else:
568+ form = Registration(**registration_args)
569+
570+ context = {
571+ 'summit': summit,
572+ 'form': form,
573+ 'form_title': form_title,
574+ }
575+ return render_to_response(
576+ 'schedule/form.html',
577+ context,
578+ RequestContext(request)
579+ )
580
581=== modified file 'summit/urls.py'
582--- summit/urls.py 2013-02-26 19:31:21 +0000
583+++ summit/urls.py 2013-04-08 21:02:19 +0000
584@@ -68,6 +68,11 @@
585 (r'^(?P<summit_name>[\w-]+)/$', 'summit'),
586 (r'^(?P<summit_name>[\w-]+)/mobile/$', 'mobile'),
587 (r'^(?P<summit_name>[\w-]+)/search/$', 'search'),
588+ url(
589+ r'^(?P<summit_name>[\w-]+)/registration/$',
590+ 'registration_form',
591+ name='registration',
592+ ),
593 (r'^(?P<summit_name>[\w-]+)/propose_meeting/$', 'propose_meeting'),
594 (r'^(?P<summit_name>[\w-]+)/edit_meeting/(?P<meeting_id>\d+)/(?P<meeting_slug>[%+\.\w-]+)/$',
595 'edit_meeting'),

Subscribers

People subscribed via source and target branches