Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 874 lines (772 sloc) 34.04 kB
f0c7d81 @jace Initial version
jace authored
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 """
5 Website server for doctypehtml5.in
6 """
7
2a56a75 @jace Updated schedule and added organiser logo.
jace authored
8 from __future__ import with_statement
6f70612 @jace New browser stats page.
jace authored
9 from collections import defaultdict
11aa1b8 @jace Forgot to put the event's date on the site. Also, user registration d…
jace authored
10 from datetime import datetime
65ec18a @jace New gallery section and partial work on user accounts.
jace authored
11 from uuid import uuid4
12 from base64 import b64encode
845c789 @jace Don't reveal the full email address in the venue sign-in sheet.
jace authored
13 import re
9fefc71 @jace Initial admin backend.
jace authored
14 from flask import Flask, abort, request, render_template, redirect, url_for
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
15 from flask import flash, session, g
6f70612 @jace New browser stats page.
jace authored
16 from werkzeug import generate_password_hash, check_password_hash, UserAgent
d1a4f92 @jace Added working registration form.
jace authored
17 from flaskext.sqlalchemy import SQLAlchemy
ee61a2a @jace Added participant approval system.
jace authored
18 from flaskext.mail import Mail, Message
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
19 from flaskext.wtf import Form, TextField, TextAreaField, PasswordField
f9091be @jace Updated website for Chennai and Pune. Bangalore description moved to …
jace authored
20 from flaskext.wtf import SelectField, Required, Email, ValidationError
d617645 @jace More admin pages.
jace authored
21 from pytz import utc, timezone
ee61a2a @jace Added participant approval system.
jace authored
22 from markdown import markdown
6f70612 @jace New browser stats page.
jace authored
23 import pygooglechart
ee61a2a @jace Added participant approval system.
jace authored
24 try:
25 from greatape import MailChimp, MailChimpError
26 except ImportError:
27 MailChimp = None
f0c7d81 @jace Initial version
jace authored
28
29 app = Flask(__name__)
d1a4f92 @jace Added working registration form.
jace authored
30 db = SQLAlchemy(app)
ee61a2a @jace Added participant approval system.
jace authored
31 mail = Mail(app)
d1a4f92 @jace Added working registration form.
jace authored
32
65ec18a @jace New gallery section and partial work on user accounts.
jace authored
33 # ---------------------------------------------------------------------------
34 # Static data
35
36 USER_CATEGORIES = [
f9091be @jace Updated website for Chennai and Pune. Bangalore description moved to …
jace authored
37 ('0', u'Unclassified'),
38 ('1', u'Student or Trainee'),
39 ('2', u'Developer'),
40 ('3', u'Designer'),
41 ('4', u'Manager, Senior Developer/Designer'),
42 ('5', u'CTO, CIO, CEO'),
43 ('6', u'Entrepreneur'),
44 ]
45
46 USER_CITIES = [
47 ('', ''),
48 ('bangalore', 'Bangalore - October 9, 2010 (over!)'),
9d81707 @jace Closed registrations for Pune.
jace authored
49 ('chennai', 'Chennai - November 27, 2010 (over!)'),
4b7634a @jace Updated for upcoming Hyderabad and Ahmedabad editions.
jace authored
50 ('pune', 'Pune - December 4, 2010 (over!)'),
adf73ee @jace Closed Ahmedabad registrations
jace authored
51 ('hyderabad', 'Hyderabad - January 23, 2011 (over!)'),
f4575cb @jace Bundle of updates: DocType HTML5 series is over.
jace authored
52 ('ahmedabad', 'Ahmedabad - February 5, 2011 (over!)'),
65ec18a @jace New gallery section and partial work on user accounts.
jace authored
53 ]
54
55 TSHIRT_SIZES = [
f9091be @jace Updated website for Chennai and Pune. Bangalore description moved to …
jace authored
56 ('', u''),
57 ('1', u'XS'),
58 ('2', u'S'),
59 ('3', u'M'),
60 ('4', u'L'),
61 ('5', u'XL'),
62 ('6', u'XXL'),
63 ('7', u'XXXL'),
65ec18a @jace New gallery section and partial work on user accounts.
jace authored
64 ]
65
66 REFERRERS = [
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
67 ('', u''),
68 ('1', u'Twitter'),
69 ('2', u'Facebook'),
70 ('3', u'LinkedIn'),
71 ('10', u'Discussion Group or List'),
72 ('4', u'Google/Bing Search'),
73 ('5', u'Google Buzz'),
74 ('6', u'Blog'),
75 ('7', u'Email/IM from Friend'),
76 ('8', u'Colleague at Work'),
77 ('9', u'Other'),
65ec18a @jace New gallery section and partial work on user accounts.
jace authored
78 ]
79
80 GALLERY_SECTIONS = [
81 (u'Basics', u'basics'),
82 (u'Business', u'biz'),
83 (u'Accessibility', u'accessibility'),
84 (u'Typography', u'typography'),
85 (u'CSS3', u'css'),
86 (u'Audio', u'audio'),
87 (u'Video', u'video'),
88 (u'Canvas', u'canvas'),
89 (u'Vector Graphics', u'svg'),
90 (u'Geolocation', u'geolocation'),
91 (u'Mobile', u'mobile'),
e336afd @jace New gallery category for websockets.
jace authored
92 (u'Websockets', u'websockets'),
65ec18a @jace New gallery section and partial work on user accounts.
jace authored
93 (u'Toolkits', u'toolkit'),
94 (u'Showcase', u'showcase'),
95 ]
96
845c789 @jace Don't reveal the full email address in the venue sign-in sheet.
jace authored
97 hideemail = re.compile('.{1,3}@')
98
65ec18a @jace New gallery section and partial work on user accounts.
jace authored
99 # ---------------------------------------------------------------------------
100 # Utility functions
101
102 def newid():
103 """
104 Return a new random id that is exactly 22 characters long.
105 """
106 return b64encode(uuid4().bytes, altchars=',-').replace('=', '')
d1a4f92 @jace Added working registration form.
jace authored
107
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
108
109 def currentuser():
110 """
111 Get the current user, or None if user isn't logged in.
112 """
113 if 'userid' in session and session['userid']:
114 return User.query.filter_by(email=session['userid']).first()
115 else:
116 return None
117
118
119 def getuser(f):
120 """
121 Decorator for routes that need a logged-in user.
122 """
123 def wrapped(*args, **kw):
124 g.user = currentuser()
125 return f(*args, **kw)
126 wrapped.__name__ = f.__name__
127 return wrapped
128
129
d1a4f92 @jace Added working registration form.
jace authored
130 # ---------------------------------------------------------------------------
131 # Data models and forms
132
133 class Participant(db.Model):
65ec18a @jace New gallery section and partial work on user accounts.
jace authored
134 """
135 Participant data, as submitted from the registration form.
136 """
d1a4f92 @jace Added working registration form.
jace authored
137 __tablename__ = 'participant'
138 id = db.Column(db.Integer, primary_key=True)
139 #: User's full name
140 fullname = db.Column(db.Unicode(80), nullable=False)
141 #: User's email address
142 email = db.Column(db.Unicode(80), nullable=False)
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
143 #: Edition of the event they'd like to attend
144 edition = db.Column(db.Unicode(80), nullable=False)
d1a4f92 @jace Added working registration form.
jace authored
145 #: User's company name
146 company = db.Column(db.Unicode(80), nullable=False)
147 #: User's job title
148 jobtitle = db.Column(db.Unicode(80), nullable=False)
149 #: User's twitter id (optional)
150 twitter = db.Column(db.Unicode(80), nullable=True)
4d9634d @jace Added Twitter widget
jace authored
151 #: T-shirt size (XS, S, M, L, XL, XXL, XXXL)
65ec18a @jace New gallery section and partial work on user accounts.
jace authored
152 tshirtsize = db.Column(db.Integer, nullable=False, default=0)
153 #: How did the user hear about this event?
154 referrer = db.Column(db.Integer, nullable=False, default=0)
d1a4f92 @jace Added working registration form.
jace authored
155 #: User's reason for wanting to attend
156 reason = db.Column(db.Text, nullable=False)
65ec18a @jace New gallery section and partial work on user accounts.
jace authored
157 #: User category, defined by a reviewer
158 category = db.Column(db.Integer, nullable=False, default=0)
f9091be @jace Updated website for Chennai and Pune. Bangalore description moved to …
jace authored
159 #: User agent with which the user registered
160 useragent = db.Column(db.Unicode(250), nullable=True)
11aa1b8 @jace Forgot to put the event's date on the site. Also, user registration d…
jace authored
161 #: Date the user registered
162 regdate = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
163 #: Submitter's IP address, for logging (45 chars to accommodate an IPv6 address)
164 ipaddr = db.Column(db.Text(45), nullable=False)
d1a4f92 @jace Added working registration form.
jace authored
165 #: Has the user's application been approved?
166 approved = db.Column(db.Boolean, default=False, nullable=False)
167 #: RSVP status codes:
168 #: A = Awaiting Response
169 #: Y = Yes, Attending
170 #: M = Maybe Attending
171 #: N = Not Attending
172 rsvp = db.Column(db.Unicode(1), default='A', nullable=False)
6311ea6 @jace Support for at-venue registrations.
jace authored
173 #: Did the participant attend the event?
174 attended = db.Column(db.Boolean, default=False, nullable=False)
175 #: Datetime the participant showed up
176 attenddate = db.Column(db.DateTime, nullable=True)
177 #: Did the participant agree to subscribe to the newsletter?
178 subscribe = db.Column(db.Boolean, default=False, nullable=False)
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
179 #: User_id
180 user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True, unique=False)
181 #: Link to user account
182 user = db.relation('User', backref='participants')
d1a4f92 @jace Added working registration form.
jace authored
183
184
65ec18a @jace New gallery section and partial work on user accounts.
jace authored
185 class User(db.Model):
186 """
187 User account. This is different from :class:`Participant` because the email
188 address here has been verified and is unique. The email address in
189 :class:`Participant` cannot be unique as that is unverified. Anyone may
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
190 submit using any email address. Participant objects link to user objects
65ec18a @jace New gallery section and partial work on user accounts.
jace authored
191 """
192 __tablename__ = 'user'
193 id = db.Column(db.Integer, primary_key=True)
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
194 #: User's name
195 fullname = db.Column(db.Unicode(80), nullable=False)
65ec18a @jace New gallery section and partial work on user accounts.
jace authored
196 #: Email id (repeated from participant.email, but unique here)
197 email = db.Column(db.Unicode(80), nullable=False, unique=True)
198 #: Private key, for first-time access without password
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
199 privatekey = db.Column(db.String(22), nullable=False, unique=True, default=newid)
65ec18a @jace New gallery section and partial work on user accounts.
jace authored
200 #: Public UID; not clear what this could be used for
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
201 uid = db.Column(db.String(22), nullable=False, unique=True, default=newid)
65ec18a @jace New gallery section and partial work on user accounts.
jace authored
202 #: Password hash
203 pw_hash = db.Column(db.String(80))
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
204 #: Is this account active?
205 active = db.Column(db.Boolean, nullable=False, default=False)
206 #: Date of creation
207 created_date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
208 #: Date of first login
209 firstuse_date = db.Column(db.DateTime, nullable=True)
65ec18a @jace New gallery section and partial work on user accounts.
jace authored
210
211 def _set_password(self, password):
212 if password is None:
213 self.pw_hash = None
214 else:
215 self.pw_hash = generate_password_hash(password)
216
217 password = property(fset=_set_password)
218
219 def check_password(self, password):
220 return check_password_hash(self.pw_hash, password)
221
222 def __repr__(self):
223 return '<User %s>' % (self.email)
2a56a75 @jace Updated schedule and added organiser logo.
jace authored
224
225
d1a4f92 @jace Added working registration form.
jace authored
226 class RegisterForm(Form):
227 fullname = TextField('Full name', validators=[Required()])
228 email = TextField('Email address', validators=[Required(), Email()])
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
229 edition = SelectField('Edition', validators=[Required()], choices=USER_CITIES)
230 company = TextField('Company name (or school/college)', validators=[Required()])
d1a4f92 @jace Added working registration form.
jace authored
231 jobtitle = TextField('Job title', validators=[Required()])
232 twitter = TextField('Twitter id (optional)')
f9091be @jace Updated website for Chennai and Pune. Bangalore description moved to …
jace authored
233 tshirtsize = SelectField('T-shirt size', validators=[Required()], choices=TSHIRT_SIZES)
234 referrer = SelectField('How did you hear about this event?', validators=[Required()], choices=REFERRERS)
d1a4f92 @jace Added working registration form.
jace authored
235 reason = TextAreaField('Your reasons for attending', validators=[Required()])
236
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
237 def validate_edition(self, field):
c39abb5 @jace Fixed at-venue registration system.
jace authored
238 if hasattr(self, '_venuereg'):
239 if field.data != self._venuereg:
240 raise ValidationError, "You can't register for that"
241 else:
242 return # Register at venue even if public reg is closed
adf73ee @jace Closed Ahmedabad registrations
jace authored
243 if field.data in [u'bangalore', u'chennai', u'pune', u'hyderabad', u'ahmedabad']:
4e53ec5 @jace Updated schedule icons and RSVP page, and closed Chennai registrations.
jace authored
244 raise ValidationError, "Registrations are closed for this edition"
f9091be @jace Updated website for Chennai and Pune. Bangalore description moved to …
jace authored
245
f0c7d81 @jace Initial version
jace authored
246
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
247 class AccessKeyForm(Form):
f7cc649 @jace Fixed dupe checking and upgraded RSVP for multi-edition support.
jace authored
248 key = PasswordField('Access Key', validators=[Required()])
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
249
250
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
251 class LoginForm(Form):
252 email = TextField('Email address', validators=[Required(), Email()])
253 password = PasswordField('Password', validators=[Required()])
254
255 def getuser(self, name):
256 return User.query.filter_by(email=name).first()
257
258 def validate_username(self, field):
259 existing = self.getuser(field.data)
260 if existing is None:
261 raise ValidationError, "No user account for that email address"
262 if not existing.active:
263 raise ValidationError, "This user account is disabled"
264
265 def validate_password(self, field):
266 user = self.getuser(self.email.data)
267 if user is None or not user.check_password(field.data):
268 raise ValidationError, "Incorrect password"
269 self.user = user
270
271
d1a4f92 @jace Added working registration form.
jace authored
272 # ---------------------------------------------------------------------------
273 # Routes
274
275 @app.route('/', methods=['GET'])
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
276 @getuser
277 def index(**forms):
278 regform = forms.get('regform', RegisterForm())
279 loginform = forms.get('loginform', LoginForm())
280 return render_template('index.html',
281 regform=regform,
282 loginform=loginform,
283 gallery_sections=GALLERY_SECTIONS)
284
285
f4575cb @jace Bundle of updates: DocType HTML5 series is over.
jace authored
286 @app.route('/sitemap.xml')
287 def sitemap():
288 """
289 Return a sitemap. There is only one page for web crawlers.
290 """
291 return """<?xml version="1.0" encoding="UTF-8"?>
292 <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
293 <url>
294 <loc>http://%s%s</loc>
295 </url>
296 </urlset>""" % (request.host, url_for('index'))
297
298
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
299 @app.route('/login')
300 def loginkey():
301 """
302 Login (via access key only)
303 """
304 key = request.args.get('key')
305 if key is None:
306 return redirect(url_for('index'), code=303)
307 user = User.query.filter_by(privatekey=key).first()
308 if user is None:
309 flash("Invalid access key", 'error')
310 return redirect(url_for('index'), code=303)
311 if not user.active:
312 flash("This account is disabled", 'error')
313 return redirect(url_for('index'), code=303)
314 if user.pw_hash != '':
315 # User already has a password. Can't login by key now.
316 flash("This access key is not valid anymore", 'error')
317 return redirect(url_for('index'), code=303)
318 if user.firstuse_date is None:
319 user.firstuse_date = datetime.utcnow()
320 db.session.commit()
321 g.user = user
322 session['userid'] = user.email
323 flash("You are now logged in", 'info')
324 return redirect(url_for('index'), code=303)
325
326
327 @app.route('/logout')
328 def logout():
329 g.user = None
330 del session['userid']
331 return redirect(url_for('index'), code=303)
332
333
f7cc649 @jace Fixed dupe checking and upgraded RSVP for multi-edition support.
jace authored
334 @app.route('/rsvp/<edition>')
335 def rsvp(edition):
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
336 key = request.args.get('key')
337 choice = request.args.get('rsvp')
338 if key is None:
339 flash(u"You need an access key to RSVP.", 'error')
340 return redirect(url_for('index'), code=303)
341 if choice not in ['Y', 'N', 'M']:
342 flash(u"You need to RSVP with Yes, No or Maybe: Y, N or M.", 'error')
343 return redirect(url_for('index'), code=303)
344 user = User.query.filter_by(privatekey=key).first()
345 if user is None:
346 flash(u"Sorry, that access key is not in our records.", 'error')
347 return redirect(url_for('index'), code=303)
f7cc649 @jace Fixed dupe checking and upgraded RSVP for multi-edition support.
jace authored
348 participant = Participant.query.filter_by(user=user, edition=edition).first()
349 if participant:
350 participant.rsvp = choice
351 else:
352 flash(u"You did not register for this edition, %s." % user.fullname, 'error')
353 return redirect(url_for('index'), code=303)
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
354 if choice == 'Y':
f7cc649 @jace Fixed dupe checking and upgraded RSVP for multi-edition support.
jace authored
355 flash(u"Yay! So glad you will be joining us, %s." % user.fullname , 'info')
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
356 elif choice == 'N':
f7cc649 @jace Fixed dupe checking and upgraded RSVP for multi-edition support.
jace authored
357 flash(u"Sorry you can't make it, %s. Hope you’ll join us next time." % user.fullname, 'error') # Fake 'error' for frowny icon
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
358 elif choice == 'M':
f7cc649 @jace Fixed dupe checking and upgraded RSVP for multi-edition support.
jace authored
359 flash(u"We recorded you as Maybe Attending, %s. When you know better, could you select Yes or No?" % user.fullname, 'info')
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
360 db.session.commit()
361 return redirect(url_for('index'), code=303)
362
f0c7d81 @jace Initial version
jace authored
363
364 @app.route('/favicon.ico')
365 def favicon():
366 return redirect(url_for('static', filename='favicon.ico'), code=301)
367
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
368
d4764ba @jace Unrolled #! URLs since the spec calls for more responsibilities. Now …
jace authored
369 @app.route('/robots.txt')
370 def robots():
371 # Disable support for indexing fragments, since there's no backing code
372 return "Disallow: /*_escaped_fragment_\n"
d1a4f92 @jace Added working registration form.
jace authored
373
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
374
d1a4f92 @jace Added working registration form.
jace authored
375 # ---------------------------------------------------------------------------
376 # Form submission
377
378 @app.route('/', methods=['POST'])
379 def submit():
380 # There's only one form, so we don't need to check which one was submitted
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
381 formid = request.form.get('form.id')
382 if formid == 'regform':
383 return submit_register()
384 elif formid == 'login':
385 return submit_login()
386 else:
387 flash("Unknown form", 'error')
388 redirect(url_for('index'), code=303)
389
390
391 def submit_register():
392 # This function doesn't need parameters because Flask provides everything
393 # via thread globals.
d1a4f92 @jace Added working registration form.
jace authored
394 form = RegisterForm()
395 if form.validate_on_submit():
396 participant = Participant()
397 form.populate_obj(participant)
11aa1b8 @jace Forgot to put the event's date on the site. Also, user registration d…
jace authored
398 participant.ipaddr = request.environ['REMOTE_ADDR']
f9091be @jace Updated website for Chennai and Pune. Bangalore description moved to …
jace authored
399 participant.useragent = request.user_agent.string
d1a4f92 @jace Added working registration form.
jace authored
400 db.session.add(participant)
401 db.session.commit()
402 return render_template('regsuccess.html')
403 else:
404 if request.is_xhr:
405 return render_template('regform.html',
406 regform=form, ajax_re_register=True)
407 else:
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
408 flash("Please check your details and try again.", 'error')
409 return index(regform=form)
410
411
412 def submit_login():
413 form = LoginForm()
414 if form.validate_on_submit():
415 user = form.user
416 g.user = user
417 session['userid'] = user.email
418 if user.firstuse_date is None:
419 user.firstuse_date = datetime.utcnow()
420 db.session.commit()
421 flash("You are now logged in", 'info')
422 return redirect(url_for('index'), code=303)
423 else:
424 if request.is_xhr:
425 return render_template('loginform.html',
426 loginform=form, ajax_re_register=True)
427 else:
428 flash("Please check your details and try again", 'error')
429 return index(loginform=form)
d1a4f92 @jace Added working registration form.
jace authored
430
d617645 @jace More admin pages.
jace authored
431
9fefc71 @jace Initial admin backend.
jace authored
432 # ---------------------------------------------------------------------------
433 # Admin backend
434
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
435 def adminkey(keyname):
436 def decorator(f):
437 def inner(edition):
438 form = AccessKeyForm()
439 keylist = app.config[keyname]
440 # check for key and call f or return form
441 if 'key' in request.values:
442 if request.values.get('key') in keylist:
443 session[keyname] = request.values['key']
444 return redirect(request.base_url, code=303) # FIXME: Redirect to self URL
445 else:
446 flash("Invalid access key", 'error')
447 return render_template('accesskey.html', keyform=form)
448 elif keyname in session and session[keyname] in keylist:
449 return f(edition)
450 else:
451 return render_template('accesskey.html', keyform=form)
452 inner.__name__ = f.__name__
453 return inner
454 return decorator
455
456
457 @app.route('/admin/reasons/<edition>', methods=['GET', 'POST'])
458 @adminkey('ACCESSKEY_REASONS')
459 def admin_reasons(edition):
460 headers = [('no', u'Sl No'), ('reason', u'Reason')] # List of (key, label)
461 data = ({'no': i+1, 'reason': p.reason} for i, p in
462 enumerate(Participant.query.filter_by(edition=edition)))
463 return render_template('datatable.html', headers=headers, data=data,
464 title=u'Reasons for attending')
465
466
467 @app.route('/admin/list/<edition>', methods=['GET', 'POST'])
468 @adminkey('ACCESSKEY_LIST')
469 def admin_list(edition):
470 headers = [('no', u'Sl No'), ('name', u'Name'), ('company', u'Company'),
471 ('jobtitle', u'Job Title'), ('twitter', 'Twitter'),
24a5890 @jace Google Analytics for ajax pages and better checking for already appro…
jace authored
472 ('approved', 'Approved'), ('rsvp', 'RSVP'), ('attended', 'Attended')]
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
473 data = ({'no': i+1, 'name': p.fullname, 'company': p.company,
474 'jobtitle': p.jobtitle,
475 'twitter': p.twitter,
24a5890 @jace Google Analytics for ajax pages and better checking for already appro…
jace authored
476 'approved': p.approved,
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
477 'rsvp': {'Y': 'Yes', 'N': 'No', 'M': 'Maybe', 'A': 'Awaiting'}[p.rsvp],
478 'attended': ['No', 'Yes'][p.attended]
479 } for i, p in enumerate(Participant.query.order_by('fullname').filter_by(edition=edition)))
480 return render_template('datatable.html', headers=headers, data=data,
481 title=u'List of participants')
482
6f70612 @jace New browser stats page.
jace authored
483 @app.route('/admin/rsvp/<edition>', methods=['GET', 'POST'])
484 @adminkey('ACCESSKEY_LIST')
485 def admin_rsvp(edition):
486 rsvp_yes = Participant.query.filter_by(edition=edition, approved=True, rsvp='Y').count()
487 rsvp_no = Participant.query.filter_by(edition=edition, approved=True, rsvp='N').count()
488 rsvp_maybe = Participant.query.filter_by(edition=edition, approved=True, rsvp='M').count()
489 rsvp_awaiting = Participant.query.filter_by(edition=edition, approved=True, rsvp='A').count()
490
491 return render_template('rsvp.html', yes=rsvp_yes, no=rsvp_no,
492 maybe=rsvp_maybe, awaiting=rsvp_awaiting,
493 title=u'RSVP Statistics')
494
495
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
496 @app.route('/admin/stats/<edition>', methods=['GET', 'POST'])
497 @adminkey('ACCESSKEY_LIST')
4e53ec5 @jace Updated schedule icons and RSVP page, and closed Chennai registrations.
jace authored
498 def admin_stats(edition):
499
6f70612 @jace New browser stats page.
jace authored
500 # Chart sizes
501 CHART_X = 800
502 CHART_Y = 370
503
504 all_browsers = defaultdict(int)
505 all_brver = defaultdict(int)
506 all_platforms = defaultdict(int)
507 present_browsers = defaultdict(int)
508 present_brver = defaultdict(int)
509 present_platforms = defaultdict(int)
510
511 c_all = 0
512 c_present = 0
513
514 for p in Participant.query.filter_by(edition=edition):
515 if p.useragent:
516 c_all += 1
517 ua = UserAgent(p.useragent)
518 all_browsers[ua.browser] += 1
99486bc @jace Edge case: HTTP user agent with no version number.
jace authored
519 if ua.version is None:
520 all_brver[ua.browser] += 1
521 else:
522 all_brver['%s %s' % (ua.browser, ua.version.split('.')[0])] += 1
6f70612 @jace New browser stats page.
jace authored
523 all_platforms[ua.platform] += 1
524 for p in Participant.query.filter_by(edition=edition, attended=True):
525 if p.useragent:
526 c_present += 1
527 ua = UserAgent(p.useragent)
528 present_browsers[ua.browser] += 1
529 present_brver['%s %s' % (ua.browser, ua.version.split('.')[0])] += 1
530 present_platforms[ua.platform] += 1
531
f6b30f8 @jace Divide by zero fix.
jace authored
532 if c_all != 0: # Avoid divide by zero situation
533 f_all = 100.0 / c_all
534 else:
535 f_all = 1
536
537 if c_present != 0: # Avoid divide by zero situation
538 f_present = 100.0 / c_present
539 else:
540 f_present = 1
6f70612 @jace New browser stats page.
jace authored
541
542 # Now make charts
543 # All registrations
544 c_all_browsers = pygooglechart.PieChart2D(CHART_X, CHART_Y)
545 c_all_browsers.add_data(all_browsers.values())
546 c_all_browsers.set_pie_labels(['%s (%.2f%%)' % (key, all_browsers[key]*f_all) for key in all_browsers.keys()])
547
548 c_all_brver = pygooglechart.PieChart2D(CHART_X, CHART_Y)
549 c_all_brver.add_data(all_brver.values())
550 c_all_brver.set_pie_labels(['%s (%.2f%%)' % (key, all_brver[key]*f_all) for key in all_brver.keys()])
551
552 c_all_platforms = pygooglechart.PieChart2D(CHART_X, CHART_Y)
553 c_all_platforms.add_data(all_platforms.values())
554 c_all_platforms.set_pie_labels(['%s (%.2f%%)' % (key, all_platforms[key]*f_all) for key in all_platforms.keys()])
555
556 # Present at venue
557 c_present_browsers = pygooglechart.PieChart2D(CHART_X, CHART_Y)
558 c_present_browsers.add_data(present_browsers.values())
559 c_present_browsers.set_pie_labels(['%s (%.2f%%)' % (key, present_browsers[key]*f_present) for key in present_browsers.keys()])
560
561 c_present_brver = pygooglechart.PieChart2D(CHART_X, CHART_Y)
562 c_present_brver.add_data(present_brver.values())
563 c_present_brver.set_pie_labels(['%s (%.2f%%)' % (key, present_brver[key]*f_present) for key in present_brver.keys()])
564
565 c_present_platforms = pygooglechart.PieChart2D(CHART_X, CHART_Y)
566 c_present_platforms.add_data(present_platforms.values())
567 c_present_platforms.set_pie_labels(['%s (%.2f%%)' % (key, present_platforms[key]*f_present) for key in present_platforms.keys()])
568
569 return render_template('stats.html',
570 all_browsers = c_all_browsers.get_url(),
571 all_brver = c_all_brver.get_url(),
572 all_platforms = c_all_platforms.get_url(),
573 present_browsers = c_present_browsers.get_url(),
574 present_brver = c_present_brver.get_url(),
575 present_platforms = c_present_platforms.get_url()
576 )
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
577
578
579 @app.route('/admin/data/<edition>', methods=['GET', 'POST'])
580 @adminkey('ACCESSKEY_DATA')
581 def admin_data(edition):
582 d_tshirt = dict(TSHIRT_SIZES)
583 d_referrer = dict(REFERRERS)
584 d_category = dict(USER_CATEGORIES)
585 tz = timezone(app.config['TIMEZONE'])
586 headers = [('no', u'Sl No'),
587 ('regdate', u'Date'),
588 ('name', u'Name'),
589 ('email', u'Email'),
590 ('company', u'Company'),
591 ('jobtitle', u'Job Title'),
592 ('twitter', u'Twitter'),
593 ('tshirt', u'T-shirt Size'),
594 ('referrer', u'Referrer'),
595 ('category', u'Category'),
596 ('ipaddr', u'IP Address'),
597 ('approved', u'Approved'),
598 ('RSVP', u'RSVP'),
599 ('agent', u'User Agent'),
600 ('reason', u'Reason'),
601 ]
602 data = ({'no': i+1,
603 'regdate': utc.localize(p.regdate).astimezone(tz).strftime('%Y-%m-%d %H:%M'),
604 'name': p.fullname,
605 'email': p.email,
606 'company': p.company,
607 'jobtitle': p.jobtitle,
608 'twitter': p.twitter,
609 'tshirt': d_tshirt.get(str(p.tshirtsize), p.tshirtsize),
610 'referrer': d_referrer.get(str(p.referrer), p.referrer),
611 'category': d_category.get(str(p.category), p.category),
612 'ipaddr': p.ipaddr,
613 'approved': {True: 'Yes', False: 'No'}[p.approved],
614 'rsvp': {'A': u'', 'Y': u'Yes', 'M': u'Maybe', 'N': u'No'}[p.rsvp],
615 'agent': p.useragent,
616 'reason': p.reason,
617 } for i, p in enumerate(Participant.query.filter_by(edition=edition)))
618 return render_template('datatable.html', headers=headers, data=data,
619 title=u'Participant data')
620
621
622 @app.route('/admin/classify/<edition>', methods=['GET', 'POST'])
623 @adminkey('ACCESSKEY_APPROVE')
624 def admin_classify(edition):
625 if request.method == 'GET':
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
626 tz = timezone(app.config['TIMEZONE'])
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
627 return render_template('classify.html', participants=Participant.query.filter_by(edition=edition),
c39abb5 @jace Fixed at-venue registration system.
jace authored
628 utc=utc, tz=tz, enumerate=enumerate, edition=edition)
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
629 elif request.method == 'POST':
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
630 p = Participant.query.get(request.form['id'])
631 if p:
632 p.category = request.form['category']
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
633 # TODO: Return status
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
634
635
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
636 @app.route('/admin/approve/<edition>', methods=['GET', 'POST'])
637 @adminkey('ACCESSKEY_APPROVE')
638 def admin_approve(edition):
639 if request.method == 'GET':
ee61a2a @jace Added participant approval system.
jace authored
640 tz = timezone(app.config['TIMEZONE'])
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
641 return render_template('approve.html', participants=Participant.query.filter_by(edition=edition),
642 utc=utc, tz=tz, enumerate=enumerate, edition=edition)
643 elif request.method == 'POST':
ee61a2a @jace Added participant approval system.
jace authored
644 p = Participant.query.get(request.form['id'])
645 if not p:
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
646 status = "No such user"
ee61a2a @jace Added participant approval system.
jace authored
647 else:
648 if 'action.undo' in request.form:
649 p.approved = False
f7cc649 @jace Fixed dupe checking and upgraded RSVP for multi-edition support.
jace authored
650 p.user = None
ee61a2a @jace Added participant approval system.
jace authored
651 status = 'Undone!'
652 # Remove from MailChimp
653 if MailChimp is not None and app.config['MAILCHIMP_API_KEY'] and app.config['MAILCHIMP_LIST_ID']:
654 mc = MailChimp(app.config['MAILCHIMP_API_KEY'])
655 try:
656 mc.listUnsubscribe(
657 id = app.config['MAILCHIMP_LIST_ID'],
658 email_address = p.email,
659 send_goodbye = False,
660 send_notify = False,
661 )
662 pass
663 except MailChimpError, e:
664 status = e.msg
665 db.session.commit()
666 elif 'action.approve' in request.form:
24a5890 @jace Google Analytics for ajax pages and better checking for already appro…
jace authored
667 if p.approved:
668 status = "Already approved"
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
669 else:
24a5890 @jace Google Analytics for ajax pages and better checking for already appro…
jace authored
670 # Check for dupe participant (same email, same edition)
671 dupe = False
f7cc649 @jace Fixed dupe checking and upgraded RSVP for multi-edition support.
jace authored
672 for other in Participant.query.filter_by(edition=p.edition, email=p.email):
673 if other.id != p.id:
674 if other.user:
24a5890 @jace Google Analytics for ajax pages and better checking for already appro…
jace authored
675 dupe = True
676 break
677 if dupe == False:
678 p.approved = True
679 status = "Tada!"
680 # 1. Make user account and activate it
681 user = makeuser(p)
682 user.active = True
683 # 2. Add to MailChimp
684 if MailChimp is not None and app.config['MAILCHIMP_API_KEY'] and app.config['MAILCHIMP_LIST_ID']:
685 mc = MailChimp(app.config['MAILCHIMP_API_KEY'])
686 addmailchimp(mc, p)
687 # 3. Send notice of approval
688 msg = Message(subject="Your registration has been approved",
689 recipients = [p.email])
690 msg.body = render_template("approve_notice_%s.md" % edition, p=p)
691 msg.html = markdown(msg.body)
692 with app.open_resource("static/doctypehtml5-%s.ics" % edition) as ics:
693 msg.attach("doctypehtml5.ics", "text/calendar", ics.read())
694 mail.send(msg)
695 db.session.commit()
696 else:
697 status = "Dupe"
ee61a2a @jace Added participant approval system.
jace authored
698 else:
699 status = 'Unknown action'
700 if request.is_xhr:
701 return status
702 else:
c39abb5 @jace Fixed at-venue registration system.
jace authored
703 return redirect(url_for('admin_approve', edition=edition), code=303)
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
704 else:
705 abort(401)
706
707
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
708 @app.route('/admin/venue/<edition>', methods=['GET', 'POST'])
709 @adminkey('ACCESSKEY_APPROVE')
710 def admin_venue(edition):
c39abb5 @jace Fixed at-venue registration system.
jace authored
711 if request.method == 'GET' and 'email' not in request.args:
712 return render_template('venuereg.html', edition=edition)
713 elif request.method =='POST' or 'email' in request.args:
6311ea6 @jace Support for at-venue registrations.
jace authored
714 if 'email' in request.args:
c39abb5 @jace Fixed at-venue registration system.
jace authored
715 formid = 'venueregemail'
6311ea6 @jace Support for at-venue registrations.
jace authored
716 else:
c39abb5 @jace Fixed at-venue registration system.
jace authored
717 formid = request.form.get('form.id')
6311ea6 @jace Support for at-venue registrations.
jace authored
718 if formid == 'venueregemail':
c39abb5 @jace Fixed at-venue registration system.
jace authored
719 email = request.values.get('email')
6311ea6 @jace Support for at-venue registrations.
jace authored
720 if email:
c39abb5 @jace Fixed at-venue registration system.
jace authored
721 p = Participant.query.filter_by(edition=edition, email=email).first()
6311ea6 @jace Support for at-venue registrations.
jace authored
722 if p is not None:
723 if p.attended: # Already signed in
724 flash("You have already signed in. Next person please.")
c39abb5 @jace Fixed at-venue registration system.
jace authored
725 return redirect(url_for('admin_venue', edition=edition), code=303)
6311ea6 @jace Support for at-venue registrations.
jace authored
726 else:
c39abb5 @jace Fixed at-venue registration system.
jace authored
727 return render_template('venueregdetails.html', edition=edition, p=p)
6311ea6 @jace Support for at-venue registrations.
jace authored
728 # Unknown email address. Ask for new registration
729 regform = RegisterForm()
730 regform.email.data = email
c39abb5 @jace Fixed at-venue registration system.
jace authored
731 regform.edition.data = edition
732 return render_template('venueregnew.html', edition=edition, regform=regform)
6311ea6 @jace Support for at-venue registrations.
jace authored
733 elif formid == 'venueregconfirm':
734 id = request.form['id']
735 subscribe = request.form.get('subscribe')
736 p = Participant.query.get(id)
737 if subscribe:
738 p.subscribe = True
739 else:
740 p.subscribe = False
741 p.attended = True
742 p.attenddate = datetime.utcnow()
743 db.session.commit()
744 flash("You have been signed in. Next person please.", 'info')
c39abb5 @jace Fixed at-venue registration system.
jace authored
745 return redirect(url_for('admin_venue', edition=edition), code=303)
6311ea6 @jace Support for at-venue registrations.
jace authored
746 elif formid == 'venueregform':
747 # Validate form and register
748 regform = RegisterForm()
c39abb5 @jace Fixed at-venue registration system.
jace authored
749 regform._venuereg = edition
6311ea6 @jace Support for at-venue registrations.
jace authored
750 if regform.validate_on_submit():
751 participant = Participant()
752 regform.populate_obj(participant)
753 participant.ipaddr = request.environ['REMOTE_ADDR']
6f70612 @jace New browser stats page.
jace authored
754 # Do not record participant.useragent since it's a venue computer, not user's.
c39abb5 @jace Fixed at-venue registration system.
jace authored
755 makeuser(participant)
6311ea6 @jace Support for at-venue registrations.
jace authored
756 db.session.add(participant)
c39abb5 @jace Fixed at-venue registration system.
jace authored
757 if MailChimp is not None and app.config['MAILCHIMP_API_KEY'] and app.config['MAILCHIMP_LIST_ID']:
758 mc = MailChimp(app.config['MAILCHIMP_API_KEY'])
759 addmailchimp(mc, participant)
6311ea6 @jace Support for at-venue registrations.
jace authored
760 db.session.commit()
c39abb5 @jace Fixed at-venue registration system.
jace authored
761 return render_template('venueregsuccess.html', edition=edition, p=participant)
6311ea6 @jace Support for at-venue registrations.
jace authored
762 else:
c39abb5 @jace Fixed at-venue registration system.
jace authored
763 return render_template('venueregform.html', edition=edition,
6311ea6 @jace Support for at-venue registrations.
jace authored
764 regform=regform, ajax_re_register=True)
765 else:
766 flash("Unknown form submission", 'error')
c39abb5 @jace Fixed at-venue registration system.
jace authored
767 return redirect(url_for('admin_venue', edition=edition), code=303)
6311ea6 @jace Support for at-venue registrations.
jace authored
768
a9460bd @jace New at-venue sign-in sheet.
jace authored
769 @app.route('/admin/venuesheet/<edition>', methods=['GET', 'POST'])
770 @adminkey('ACCESSKEY_APPROVE')
771 def admin_venuesheet(edition):
772 if request.method == 'GET':
773 tz = timezone(app.config['TIMEZONE'])
774 return render_template('venuesheet.html', participants=Participant.query.order_by('fullname').filter_by(edition=edition),
845c789 @jace Don't reveal the full email address in the venue sign-in sheet.
jace authored
775 utc=utc, tz=tz, enumerate=enumerate, hideemail=hideemail, edition=edition)
a9460bd @jace New at-venue sign-in sheet.
jace authored
776 elif request.method == 'POST' and 'id' in request.form:
777 # Register this participant id
778 id = request.form['id']
779 p = Participant.query.get(id)
780 if not p.attended:
781 p.attended = True
782 p.attenddate = datetime.utcnow()
783 # XXX: makeuser does not add to MailChimp, to move folks along faster.
784 # MailChimp must be manually updated later.
785 makeuser(p)
786 db.session.commit()
787 return 'Signed in'
788 else:
789 return 'Already signed in'
790 else:
791 return 'Unknown form submission'
6311ea6 @jace Support for at-venue registrations.
jace authored
792
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
793 # ---------------------------------------------------------------------------
794 # Admin helper functions
795
796 def makeuser(participant):
797 """
798 Convert a participant into a user. Returns User object.
799 """
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
800 if participant.user:
801 return participant.user
802 else:
803 user = User.query.filter_by(email=participant.email).first()
804 if user is not None:
805 participant.user = user
806 else:
807 user = User(fullname=participant.fullname, email=participant.email)
808 participant.user = user
809 # These defaults don't get auto-added until the session is committed,
810 # but we need them before, so we have to manually assign values here.
811 user.privatekey = newid()
812 user.uid = newid()
813 db.session.add(user)
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
814 return user
815
816
817 def _makeusers():
818 """
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
819 Helper function to create user accounts. Meant for one-time use only.
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
820 """
821 if MailChimp is not None and app.config['MAILCHIMP_API_KEY'] and app.config['MAILCHIMP_LIST_ID']:
822 mc = MailChimp(app.config['MAILCHIMP_API_KEY'])
823 else:
824 mc = None
825 for p in Participant.query.all():
826 if p.approved:
827 # Make user, but don't make account active
828 user = makeuser(p)
829 if mc is not None:
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
830 addmailchimp(mc, p)
4452fcf @jace Updated schedule, removed registration form and call for sponsors, an…
jace authored
831 db.session.commit()
ee61a2a @jace Added participant approval system.
jace authored
832
d617645 @jace More admin pages.
jace authored
833
1f7dd47 @jace Reintroduced schedule and venue to page. Massive changes to backend, …
jace authored
834 def addmailchimp(mc, p):
835 """
836 Add user to mailchimp list
837 """
838 editions = [ap.edition for ap in p.user.participants if p.user]
839 groups = {'Editions': {'name': 'Editions', 'groups': ','.join(editions)}}
840 mc.listSubscribe(
841 id = app.config['MAILCHIMP_LIST_ID'],
842 email_address = p.email,
843 merge_vars = {'FULLNAME': p.fullname,
844 'JOBTITLE': p.jobtitle,
845 'COMPANY': p.company,
846 'TWITTER': p.twitter,
847 'PRIVATEKEY': p.user.privatekey,
848 'UID': p.user.uid,
849 'GROUPINGS': groups},
850 double_optin = False,
851 update_existing = True
852 )
853
854
d1a4f92 @jace Added working registration form.
jace authored
855 # ---------------------------------------------------------------------------
856 # Config and startup
857
f0c7d81 @jace Initial version
jace authored
858 app.config.from_object(__name__)
7227564 @jace Added settings.py and start of sammy.js-based navigation
jace authored
859 try:
860 app.config.from_object('settings')
861 except ImportError:
862 import sys
863 print >> sys.stderr, "Please create a settings.py with the necessary settings. See settings-sample.py."
864 print >> sys.stderr, "You may use the site without these settings, but some features may not work."
f0c7d81 @jace Initial version
jace authored
865
d1a4f92 @jace Added working registration form.
jace authored
866 # Create database table
867 db.create_all()
868
f0c7d81 @jace Initial version
jace authored
869 if __name__ == '__main__':
ee61a2a @jace Added participant approval system.
jace authored
870 if MailChimp is None:
871 import sys
872 print >> sys.stderr, "greatape is not installed. MailChimp support will be disabled."
a9460bd @jace New at-venue sign-in sheet.
jace authored
873 app.run('0.0.0.0', 4000, debug=True)
Something went wrong with that request. Please try again.