Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

First commit.

- fixed a bug with sorting desc / asc.
- added allocating money for financial suport.
- added PAYMENTS_INCTUCTIONS on how to process payments.
  • Loading branch information...
commit d9007b6e2762a205d21fd1c768dfc45c5261e47e 0 parents
@alexksikes authored
Showing with 3,379 additions and 0 deletions.
  1. +57 −0 INSTALL
  2. +165 −0 LICENSE
  3. +53 −0 PAYMENTS_INSTRUCTIONS
  4. +6 −0 README
  5. +24 −0 TODO
  6. 0  __init__.py
  7. 0  app/__init__.py
  8. 0  app/controllers/__init__.py
  9. +126 −0 app/controllers/account.py
  10. +78 −0 app/controllers/actions.py
  11. +75 −0 app/controllers/browse.py
  12. +17 −0 app/controllers/public.py
  13. +94 −0 app/controllers/settings.py
  14. +256 −0 app/controllers/submit_application.py
  15. +51 −0 app/controllers/submit_reference.py
  16. 0  app/helpers/__init__.py
  17. +151 −0 app/helpers/email_templates.py
  18. +100 −0 app/helpers/misc.py
  19. +72 −0 app/helpers/paging.py
  20. +68 −0 app/helpers/session.py
  21. +52 −0 app/helpers/utils.py
  22. 0  app/models/__init__.py
  23. +142 −0 app/models/applicants.py
  24. +23 −0 app/models/comments.py
  25. +79 −0 app/models/submissions.py
  26. +34 −0 app/models/users.py
  27. +34 −0 app/models/votes.py
  28. +13 −0 app/tests/test_sessions.py
  29. +63 −0 app/tests/test_uploads.py
  30. +32 −0 app/views/account.html
  31. +245 −0 app/views/applicant.html
  32. +178 −0 app/views/applicants.html
  33. +44 −0 app/views/application_form.html
  34. +34 −0 app/views/application_form_simple.html
  35. +32 −0 app/views/base.html
  36. +14 −0 app/views/help.html
  37. +81 −0 app/views/layout.html
  38. +31 −0 app/views/reference_form.html
  39. +34 −0 app/views/settings.html
  40. +61 −0 application.py
  41. +35 −0 config_example.py
  42. +88 −0 public/css/application_forms.css
  43. +503 −0 public/css/defaults.css
  44. BIN  public/img/accept.png
  45. BIN  public/img/bullet_add.png
  46. BIN  public/img/bullet_delete.png
  47. BIN  public/img/bullet_error.png
  48. BIN  public/img/cambridge.png
  49. BIN  public/img/cancel.png
  50. BIN  public/img/comment.png
  51. BIN  public/img/comment_add.png
  52. BIN  public/img/comments.png
  53. BIN  public/img/delete.png
  54. BIN  public/img/error.png
  55. BIN  public/img/external.png
  56. BIN  public/img/favicon.ico
  57. BIN  public/img/logo_mini.png
  58. BIN  public/img/money.png
  59. BIN  public/img/money_add.png
  60. BIN  public/img/pencil_add.png
  61. BIN  public/img/stars/empty.gif
  62. BIN  public/img/stars/empty_small.gif
  63. BIN  public/img/stars/full.gif
  64. BIN  public/img/stars/full_green.gif
  65. BIN  public/img/stars/full_small.gif
  66. BIN  public/img/stars/full_small_green.gif
  67. BIN  public/img/stars/half.gif
  68. BIN  public/img/stars/half_small.gif
  69. BIN  public/img/trophy.png
  70. BIN  public/img/weather_cloudy.png
  71. +53 −0 public/js/defaults.js
  72. +81 −0 schema.sql
57 INSTALL
@@ -0,0 +1,57 @@
+1) Download the latest tar ball: http://github.com/alexksikes/mlss/tarball/master.
+
+2) tar xvzf "the tar ball"
+
+3) Download install webpy, follow instructions here: http://webpy.org/install.
+
+4) Setup lighttpd, here is a part your config file:
+
+ ...
+
+ name = "mlss_admin"
+ script = "path to ./application.py"
+
+ server.document-root = "path to ./public/"
+
+ # make sure users can access the application forms but not the admin
+ $HTTP["url"] !~ "/(submit_application|submit_reference|css|img|js)/?" {
+ auth.require = ( "" =>
+ (
+ "method" => "digest",
+ "realm" => "Authorized users only",
+ "require" => "valid-user",
+ ))
+ }
+
+ url.rewrite += (
+ # Commented for development
+ #"^/img/(.*)$" => "/img/$1",
+ #"^/css/(.*)$" => "/css/$1",
+ #"^/js/(.*)$" => "/js/$1",
+
+ "^/resumes/(.*)$" => "/resumes/$1",
+ "^/(.*)$" => script + "/$1",
+ )
+
+ fastcgi.server += ( script =>
+ ((
+ "socket" => "/tmp/" + name + var.PID + ".socket",
+ "bin-path" => script,
+ "check-local" => "disable",
+ "max-procs" => 1,
+ "bin-environment" => (
+ "REAL_SCRIPT_NAME" => ""
+ ),
+ ))
+ )
+
+ ...
+
+5) Setup your database: mysql -p mlss < ./schema.sql. Note you first need to create a database called mlss.
+
+6) Make sure lighttpd has write access to the directory ./public/resumes/
+You'd do something like this: sudo chgrp www-data ./public/resumes; chmod 775 ./public/resumes;
+
+7) Go over your site settings ./config_example.py and rename this file ./config.py
+
+8) Restart lighttpd and you're done.
165 LICENSE
@@ -0,0 +1,165 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
53 PAYMENTS_INSTRUCTIONS
@@ -0,0 +1,53 @@
+Here are the final instructions in order to process the payments for the MLSS:
+
+1) A feature was added to allocate money to applicants needing
+financial support. So go through each applicant needing financial
+support or maybe order the applicants by ratings and page through the
+results one by one allocating money as needed.
+
+2) Open a Google Checkout seller account for a given bank account:
+https://checkout.google.com/sell/ It's very easy to open an account.
+Use the mlss09cam@googlemail.com Google account to setup the account.
+
+3) I will then send you a list of admitted applicants containing the
+following fields:
+
+applicant email, name, affiliated with cambridge, industry
+researcher?, money given for financial support if any, money applicant
+has to pay
+
+The money the applicant has to pay depends on whether he was given
+financial support, is an industry researcher or is affiliated with
+Cambridge. Should this amount be in pounds or in dollars? The amount
+we asked for financial support was in dollars, so should I do the
+conversion?
+
+4) Go to Tools > Send an invoice on your newly created Google Checkout
+account. Send an email to each admitted applicant making sure the
+right amount due is put. There should probably be a paragraph
+explaining the amount due depending on whether the applicant received
+financial support, is an industry researcher or is affiliated with
+Cambridge. IMPORTANT make sure you check the option "Send me a copy of
+this email". This email is the only way we can keep track of whether
+the applicant has paid or not. There is also the Merchant admin
+(https://checkout.google.com/sell/orders) but I think there isn't an
+exact match between the applicant's email and the actual payee.
+
+5) I will send you coma separated list of all undecided applicant's
+emails (should we put these people on a waiting list?). The same with
+all rejected applicants. A group bcc email could be sent to each group
+using the MLSS Google email address.
+
+6) After some deadline we then go through the emails received by
+Google when a payment has been made. I could add a quick feature in
+the admin to mark that the applicant has actually paid which will be
+useful when making the badges. We can also start processing the
+applicants who are on the waiting list.
+
+This process could have been automatic using a low level API provided
+by Google but I ran out of time. However if the admin is used for
+subsequent MLSS then I'd be happy to add these features along with
+with some simple stats such as admitted applicants by countries,
+expertise etc ... as a pie.
+
+Alex
6 README
@@ -0,0 +1,6 @@
+This is the system we used in order to process the vast number of applicants for the machine learning summer shcool (MLSS) at Cambridge.
+This code has been made available open source so it could be re-used for subsequent summer schools.
+
+Read INSTALL on how to install and PAYMENTS_INSTRUCTIONS on to how process payments.
+
+For questions / suggestion please email Alex Ksikes (ak469 at cam dot ac dot uk).
24 TODO
@@ -0,0 +1,24 @@
+BUG:
+- bug when no data in applicant table (add an applicant go to /submit_application).
+- paging by single result does not behave as exptected when adding a comment or a rating.
+
+QUICK:
+- dummy data useful for a demo.
+- update the source code link.
+- put explanation on resume.
+- test on IE (fix bug with form input rendering)
+
+MINOR ADDITIONS:
+- ratings could also be removed by the user if he wants to.
+- fix bug no data is in the applicants table.
+- trim out white spaces in references etc...
+
+MORE ADDITONS:
+
+- have flag country for affiliation and nationality (flag for affiliation could be next university field).
+>> use http://opencountrycodes.appspot.com/python/ and for icons //www.famfamfam.com/lab/icons/silk
+- integration with Google Checkout
+>> need SSL certificates, look into gchecky: http://code.google.com/p/gchecky/wiki/TutorialForLevel2
+- use Google Chart APIS (http://code.google.com/apis/chart/) for rough statistics of applicants (countries of admitted applicants, background ...)
+- automate emails sent to accepted / rejected / waiting list applicants.
+- lightbox (modal box) for signups.
0  __init__.py
No changes.
0  app/__init__.py
No changes.
0  app/controllers/__init__.py
No changes.
126 app/controllers/account.py
@@ -0,0 +1,126 @@
+# Author: Alex Ksikes
+
+import web
+from web import form
+
+from app.models import users
+from app.helpers import session
+from app.helpers import email_templates
+
+from config import view
+
+vemail = form.regexp(r'.+@.+', 'Please enter a valid email address')
+login_form = form.Form(
+ form.Textbox('email',
+ form.notnull, vemail,
+ description='Your email:'),
+ form.Password('password',
+ form.notnull,
+ description='Your password:'),
+
+ form.Button('submit', type='submit', value='Login'),
+ validators = [
+ form.Validator('Incorrect email / password combination.',
+ lambda i: users.is_correct_password(i.email, i.password))
+ ]
+)
+
+register_form = form.Form(
+ form.Textbox('email',
+ form.notnull, vemail,
+ form.Validator('This email address is already taken.',
+ lambda x: users.is_email_available(x)),
+ description='Your email:'),
+ form.Password('password',
+ form.notnull,
+ form.Validator('Your password must at least 5 characters long.',
+ lambda x: users.is_valid_password(x)),
+ description='Choose a password:'),
+ form.Textbox('nickname',
+ form.notnull,
+ description='Choose a nickname:'),
+
+ form.Button('submit', type='submit', value='Register'),
+)
+
+forgot_password_form = form.Form(
+ form.Textbox('email',
+ form.notnull, vemail,
+ form.Validator('There is no record of this email in our database.',
+ lambda x: not users.is_email_available(x)),
+ description='Your email:'),
+
+ form.Button('submit', type='submit', value='Register'),
+)
+
+def render_account(show='all',
+ login_form=login_form(), register_form=register_form(), forgot_password_form=forgot_password_form(),
+ on_success_message=''):
+
+ return view.base(
+ view.account(show, login_form, register_form, forgot_password_form, on_success_message))
+
+class index:
+ def GET(self):
+ return render_account(show='all')
+
+class login:
+ def GET(self):
+ return render_account(show='login_only')
+
+ def POST(self):
+ f = self.form()
+ if not f.validates(web.input(_unicode=False)):
+ show = web.input(show='all').show
+ return render_account(show, login_form=f)
+ else:
+ session.login(f.d.email)
+ raise web.seeother('/')
+ #raise web.seeother(session.get_last_visited_url())
+
+ def form(self):
+ return login_form()
+
+class register:
+ def GET(self):
+ return render_account(show='register_only')
+
+ def POST(self):
+ f = self.form()
+ if not f.validates(web.input(_unicode=False)):
+ show = web.input(show='all').show
+ return render_account(show, register_form=f)
+ else:
+ users.create_account(f.d.email, f.d.password, f.d.nickname)
+ session.login(f.d.email)
+ raise web.seeother('/')
+
+ def form(self):
+ return register_form()
+
+class resend_password:
+ def GET(self):
+ return render_account(show='forgot_password_only')
+
+ def POST(self):
+ f = self.form()
+ show = web.input(show='all').show
+ if not f.validates(web.input(_unicode=False)):
+ return render_account(show, forgot_password_form=f)
+ else:
+ user = users.get_user_by_email(f.d.email)
+ email_templates.resend_password(user)
+ return render_account(show,
+ on_success_message='Login information succesfully emailed.')
+
+ def form(self):
+ return forgot_password_form()
+
+class logout:
+ def GET(self):
+ session.logout()
+ raise web.seeother(web.ctx.environ['HTTP_REFERER'])
+
+class help:
+ def GET(self):
+ return view.base(view.help())
78 app/controllers/actions.py
@@ -0,0 +1,78 @@
+# Author: Alex Ksikes
+
+import web
+import config
+
+from app.helpers import paging
+from app.helpers import session
+
+from app.models import applicants
+from app.models import comments
+from app.models import votes
+
+from config import view
+
+class admit:
+ @session.login_required
+ def POST(self):
+ i = web.input(context='', id=[])
+
+ if i.id:
+ applicants.admit(i.id, session.get_user_id())
+ raise web.seeother(web.ctx.environ['HTTP_REFERER'])
+
+class reject:
+ @session.login_required
+ def POST(self):
+ i = web.input(context='', id=[])
+
+ if i.id:
+ applicants.reject(i.id, session.get_user_id())
+ raise web.seeother(web.ctx.environ['HTTP_REFERER'])
+
+class undecide:
+ @session.login_required
+ def POST(self):
+ i = web.input(context='', id=[])
+
+ if i.id:
+ applicants.undecide(i.id, session.get_user_id())
+ raise web.seeother(web.ctx.environ['HTTP_REFERER'])
+
+class rate:
+ @session.login_required
+ def POST(self):
+ i = web.input(context='', id=[], score=[])
+ score = web.intget(i.score[0] or i.score[1], '')
+
+ if i.id and score:
+# applicants.rate(i.id, score, session.get_user_id())
+ votes.add(i.id, score, session.get_user_id())
+ raise web.seeother(web.ctx.environ['HTTP_REFERER'])
+
+class comment:
+ @session.login_required
+ def POST(self, id):
+ i = web.input(comment='')
+
+ if i.id and i.comment:
+ comments.add_comment(session.get_user_id(), id, i.comment)
+
+ raise web.seeother(web.ctx.environ['HTTP_REFERER'])
+
+class delete_comment:
+ @session.login_required
+ def GET(self, comment_id):
+ comments.delete_comment(session.get_user_id(), comment_id)
+
+ raise web.seeother(web.ctx.environ['HTTP_REFERER'])
+
+class grant:
+ @session.login_required
+ def POST(self, id):
+ i = web.input(amount='')
+
+ if i.id and i.amount:
+ applicants.grant(session.get_user_id(), id, i.amount)
+
+ raise web.seeother(web.ctx.environ['HTTP_REFERER'])
75 app/controllers/browse.py
@@ -0,0 +1,75 @@
+# Author: Alex Ksikes
+
+# TODO:
+# - see for a better paging module
+# - no need to have two separate login_required_for_reviews and login_required
+
+import web
+import config
+
+from app.helpers import paging
+from app.helpers import session
+
+from app.models import applicants
+from app.models import comments
+from app.models import votes
+
+from config import view
+
+results_per_page = 50
+default_order = 'id'
+
+class list:
+ @session.login_required_for_reviews
+ def GET(self, context):
+ i = web.input(start=0, order=default_order, desc='desc', query='')
+ start = int(i.start)
+ context = context or 'all'
+ user_id = session.is_logged() and session.get_user_id()
+
+ results, num_results = applicants.query(query=i.query, context=context,
+ offset=start, limit=results_per_page, order=i.order + ' ' + i.desc,
+ user_id=user_id)
+
+ pager = web.storage(paging.get_paging(start, num_results,
+ results_per_page=results_per_page, window_size=1))
+
+ counts = applicants.get_counts()
+
+ user = session.get_session()
+
+ stats = applicants.get_stats()
+
+ return view.layout(
+ view.applicants(results, context, pager, i),
+ user, context, counts, i.query, stats)
+
+class show:
+ @session.login_required_for_reviews
+ def GET(self, id):
+ i = web.input(context='all', start=0, order=default_order, desc='desc', query='')
+ start = int(i.start)
+ user_id = session.is_logged() and session.get_user_id()
+
+ results, num_results = applicants.query(query=i.query, context=i.context,
+ offset=start and start - 1, limit=results_per_page+2, order=i.order + ' ' + i.desc,
+ user_id=user_id)
+
+ pager = web.storage(paging.get_paging_results(start, num_results,
+ int(id), results, results_per_page))
+
+ counts = applicants.get_counts()
+
+ user = session.get_session()
+
+ applicant = applicants.get_by_id(id)
+
+ _comments = comments.get_comments(applicant.id)
+
+ _votes = votes.get_votes(applicant.id)
+
+ stats = applicants.get_stats()
+
+ return view.layout(
+ view.applicant(applicant, _comments, _votes, user, pager, i),
+ user, i.context, counts, i.query, stats)
17 app/controllers/public.py
@@ -0,0 +1,17 @@
+# Author: Alex Ksikes
+
+import web
+import mimetypes
+
+class public:
+ def GET(self):
+ public_dir = 'public'
+ try:
+ file_name = web.ctx.path.split('/')[-1]
+ web.header('Content-type', mime_type(file_name))
+ return open(public_dir + web.ctx.path, 'rb').read()
+ except IOError:
+ raise web.notfound()
+
+def mime_type(filename):
+ return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
94 app/controllers/settings.py
@@ -0,0 +1,94 @@
+# Author: Alex Ksikes
+
+import web
+import config
+
+from web import form
+
+from app.models import applicants
+from app.models import users
+
+from app.helpers import session
+from config import view
+
+password_form = form.Form(
+ form.Password('password',
+ form.notnull,
+ form.Validator('Your password must at least 5 characters long.',
+ lambda x: users.is_valid_password(x)),
+ description='Your new password:'),
+ form.Button('submit', type='submit', value='Change password')
+)
+
+nickname_form = form.Form(
+ form.Textbox('nickname',
+ form.notnull,
+ description='Your new nickname:'),
+ form.Button('submit', type='submit', value='Change your nickname')
+)
+
+vemail = form.regexp(r'.+@.+', 'Please enter a valid email address')
+email_form = form.Form(
+ form.Textbox('email',
+ form.notnull, vemail,
+ form.Validator('This email address is already taken.',
+ lambda x: users.is_email_available(x)),
+ description='Your new email:'),
+ form.Button('submit', type='submit', value='Change your email')
+)
+
+def render_settings(nickname_form=nickname_form(), email_form=email_form(), password_form=password_form(), on_success_message=''):
+ counts = applicants.get_counts()
+ user = session.get_session()
+
+ return view.layout(
+ view.settings(user, nickname_form, email_form, password_form, on_success_message),
+ user, 'settings', counts)
+
+class index:
+ @session.login_required
+ def GET(self):
+ return render_settings()
+
+class change_nickname:
+ @session.login_required
+ def POST(self):
+ f = self.form()
+ if not f.validates(web.input(_unicode=False)):
+ return render_settings(nickname_form=f)
+ else:
+ users.update(session.get_user_id(), nickname=f.d.nickname)
+ session.reset()
+ raise web.seeother('/settings')
+
+ def form(self):
+ return nickname_form()
+
+class change_email:
+ @session.login_required
+ def POST(self):
+ f = self.form()
+ if not f.validates(web.input(_unicode=False)):
+ return render_settings(email_form=f)
+ else:
+ users.update(session.get_user_id(), email=f.d.email)
+ session.reset()
+ raise web.seeother('/settings')
+
+ def form(self):
+ return email_form()
+
+class change_password:
+ @session.login_required
+ def POST(self):
+ f = self.form()
+ if not f.validates(web.input(_unicode=False)):
+ return render_settings(password_form=f)
+ else:
+ users.update(session.get_user_id(), password=f.d.password)
+ session.reset()
+ return render_settings(on_success_message='Your password has been successfuly changed.')
+
+ def form(self):
+ return password_form()
+
256 app/controllers/submit_application.py
@@ -0,0 +1,256 @@
+# Author: Alex Ksikes
+
+import web
+import config
+
+from app.models import submissions
+
+from config import view
+from web import form
+
+vemail = form.regexp(r'.+@.+', 'Please enter a valid email address')
+vurl = form.regexp('(^\s*$)|(^http://.*$)',
+ 'Please provide a valid website url (eg, http://www.example.com).')
+
+application_form = form.Form(
+ form.Textbox('first_name',
+ form.notnull,
+ description='First Name *'),
+ form.Textbox('last_name',
+ form.notnull,
+ description='Last Name *'),
+ form.Radio('gender',
+ ('Male', 'Female'),
+ form.notnull,
+ description='Gender *'),
+ form.Textbox('nationality',
+ form.notnull,
+ description='Nationality *'),
+ form.Textbox('country',
+ form.notnull,
+ description='Country of Current Affiliation *'),
+
+ form.Textbox('email',
+ form.notnull, vemail,
+ form.Validator('This email address is already taken (Have you already submitted an application?)',
+ lambda x: submissions.is_email_available(x)),
+ description='Your Email *'),
+ form.Textbox('email_again',
+ form.notnull, vemail,
+ description='Your Email Again *'),
+
+ form.Textbox('affiliation',
+ form.notnull,
+ description='Affiliation *',
+ pre='<label class="help">Your university or the company you work for.</label>'),
+ form.Textbox('department',
+ form.notnull,
+ description='Affiliation (Department) *'),
+ form.Textbox('interests',
+ form.notnull,
+ description='Interests *',
+ pre='<label class="help">Please provide 3-10 keywords/keyphrases, separated by commas.</label>'),
+ form.Textbox('website',
+ vurl,
+ description='Personal Website (optional)',
+ pre='<label class="help">If you have a personal website, please provide its url with http.</label>'),
+
+ form.Dropdown('degree',
+ ('', 'Highschool', 'Bachelors', 'Masters', 'PhD', 'Other'),
+ form.notnull,
+ description='Highest Degree Attained *'),
+ form.Dropdown('occupation',
+ ('', 'Undergraduate student', 'PhD student', 'Postdoc', 'Faculty', 'Industry Researcher', 'Other'),
+ form.notnull,
+ description='Current Occupation *'),
+ form.File('resume',
+ form.notnull,
+ description='Upload your resume in pdf. *'),
+
+ form.Radio('pascal_member',
+ ('Yes', 'No'),
+ description='Pascal Member (optional)',
+ pre='<label class="help">Are you a member of the PASCAL European network?</label>'),
+ form.Textbox('poster_title',
+ description='Poster Title (optional)',
+ pre='<label class="help">We will probably have a poster session for students to present their work. Tentative title of your poster if you were able to present at the MLSS.</label>'),
+ form.Textarea('abstract',
+ description='Abstract (optional)',
+ pre='<label class="help">If you plan to present a poster, please include in this box the <strong>abstract of your poster</strong>.</label>'),
+ form.Textarea('cover_letter',
+ description='Comments (optional)',
+ pre='<label class="help">Additional comments in support of your application (max 200 words) -- You can use this box to highlight reasons why the MLSS is appropriate for you and any specific achievements you feel are relevant.</label>'),
+
+ form.Textbox('referee_name',
+ form.notnull,
+ description='Referee Name *',
+ pre='<label class="help">Please ensure the referee is willing to submit a letter by <strong>June 11</strong>.</label>'),
+ form.Textbox('referee_email',
+ form.notnull, vemail,
+ description='Referee Email *'),
+ form.Textbox('referee_email_again',
+ form.notnull, vemail,
+ description='Referee Email Again *'),
+ form.Textbox('referee_affiliation',
+ form.notnull,
+ description='Referee Affiliation *'),
+
+ form.Checkbox('travel_support',
+ description='Travel Support (optional)',
+ post='<span>I wish to apply for financial support.</span>'),
+
+ form.Textbox('travel_support_budget',
+ form.Validator('Please enter an integer for your financial support budget estimate.',
+ lambda x: not x or isinstance(int(x), int)),
+ description='Enter budget estimate (in dollars)'),
+
+ form.Button('submit', type='submit', value='Apply'),
+ validators = [
+ form.Validator('If you are applying for financial support, make sure you complete all the fields.',
+ lambda i: check_travel_support(i)),
+ form.Validator('Your email addresses did not match!',
+ lambda i: i.email == i.email_again),
+ form.Validator('Referee email addresses did not match!',
+ lambda i: i.referee_email == i.referee_email_again),
+ ]
+)
+
+# This form is only used for applicant who are affiliated to the university.
+# For those applicants the application is simple (no letter of reference and financial support).
+application_form_simple = form.Form(
+ form.Radio('affiliated',
+ ('University of Cambridge', 'Microsoft Research Cambridge', 'Other (explain below)'),
+ form.notnull,
+ description='Affiliation with Cambridge *'),
+
+ form.Textbox('first_name',
+ form.notnull,
+ description='First Name *'),
+ form.Textbox('last_name',
+ form.notnull,
+ description='Last Name *'),
+ form.Radio('gender',
+ ('Male', 'Female'),
+ form.notnull,
+ description='Gender *'),
+ form.Textbox('nationality',
+ form.notnull,
+ description='Nationality *'),
+ form.Textbox('country',
+ form.notnull,
+ description='Country of Current Affiliation *'),
+
+ form.Textbox('email',
+ form.notnull, vemail,
+ form.Validator('This email address is already taken (Have you already submitted an application?)',
+ lambda x: submissions.is_email_available(x)),
+ description='Your Email *'),
+ form.Textbox('email_again',
+ form.notnull, vemail,
+ description='Your Email Again *'),
+
+ form.Textbox('affiliation',
+ form.notnull,
+ description='Affiliation *',
+ pre='<label class="help">Your university or the company you work for.</label>'),
+ form.Textbox('department',
+ form.notnull,
+ description='Affiliation (Department) *'),
+ form.Textbox('interests',
+ form.notnull,
+ description='Interests *',
+ pre='<label class="help">Please provide 3-10 keywords/keyphrases, separated by commas.</label>'),
+ form.Textbox('website',
+ vurl,
+ description='Personal Website (optional)',
+ pre='<label class="help">If you have a personal website, please provide its url with http.</label>'),
+
+ form.Dropdown('degree',
+ ('', 'Highschool', 'Bachelors', 'Masters', 'PhD', 'Other'),
+ form.notnull,
+ description='Highest Degree Attained *'),
+ form.Dropdown('occupation',
+ ('', 'Undergraduate student', 'PhD student', 'Postdoc', 'Faculty', 'Industry Researcher', 'Other'),
+ form.notnull,
+ description='Current Occupation *'),
+ form.File('resume',
+ form.notnull,
+ description='Upload your resume in pdf. *'),
+
+ form.Radio('pascal_member',
+ ('Yes', 'No'),
+ description='Pascal Member (optional)',
+ pre='<label class="help">Are you a member of the PASCAL European network?</label>'),
+ form.Textbox('poster_title',
+ description='Poster Title (optional)',
+ pre='<label class="help">We will probably have a poster session for students to present their work. Tentative title of your poster if you were able to present at the MLSS.</label>'),
+ form.Textarea('abstract',
+ description='Abstract (optional)',
+ pre='<label class="help">If you plan to present a poster, please include in this box the <strong>abstract of your poster</strong>.</label>'),
+ form.Textarea('cover_letter',
+ description='Comments (optional)',
+ pre='<label class="help">Additional comments in support of your application (max 200 words) -- You can use this box to highlight reasons why the MLSS is appropriate for you and any specific achievements you feel are relevant.</label>'),
+
+ form.Button('submit', type='submit', value='Apply'),
+ validators = [
+ form.Validator('Your email addresses did not match!',
+ lambda i: i.email == i.email_again),
+ ]
+)
+
+class apply:
+ def GET(self):
+ success = web.input(success='').success
+ return view.application_form(self.form(), success)
+
+ def POST(self):
+ f = self.form()
+
+ # due a bug in webpy we have to do web.input(_unicode=False)
+ # when having a File input element in a form
+ if not f.validates(web.input(_unicode=False)):
+ return view.application_form(f)
+ else:
+ success = handle_post(f)
+ raise web.seeother('/submit_application?success=%s' % success)
+
+ def form(self):
+ return application_form()
+
+# This is for applicants who are affiliated within cambridge.
+# They do not need a reference and financial support (have their own housing, etc...)
+class apply_simple:
+ def GET(self):
+ success = web.input(success='').success
+ return view.application_form_simple(self.form(), success)
+
+ def POST(self):
+ f = self.form()
+
+ # due a bug in webpy we have to do web.input(_unicode=False)
+ # when having a File input element in a form
+ if not f.validates(web.input(_unicode=False)):
+ return view.application_form_simple(f)
+ else:
+ success = handle_post(f, simple=True)
+ raise web.seeother('/affiliated/submit_application?success=%s' % success)
+
+ def form(self):
+ return application_form_simple()
+
+def handle_post(f, simple=False):
+ resume = web.input(resume={}).resume
+ success = True
+ try:
+ submissions.submit_application(resume, f.d, simple)
+ except:
+ raise
+ success = False
+ return success
+
+# In a browser, if a checkbox is not checked, it isn't sent in the request
+# (see http://www.w3.org/TR/html401/interact/forms.html#h-17.2.1)
+def check_travel_support(i):
+ # note that i.travel_support leads to an error!
+ i.travel_support = i.get('travel_support', False)
+ return (not i.travel_support and not i.travel_support_budget) or (i.travel_support and i.travel_support_budget)
51 app/controllers/submit_reference.py
@@ -0,0 +1,51 @@
+# Author: Alex Ksikes
+
+import web
+import config
+
+from app.models import submissions
+from app.models import applicants
+
+from config import view
+from web import form
+
+class refer:
+ def GET(self, secret_md5):
+ applicant = applicants.get_by_secret_md5(secret_md5)
+
+ if not applicant and secret_md5 == '0' * 32:
+ applicant = applicants.get_dummy_record()
+
+ return view.reference_form(self.form(), applicant, web.input(success='').success)
+
+ def POST(self, secret_md5):
+ applicant = applicants.get_by_secret_md5(secret_md5)
+
+ f = self.form()
+ if not f.validates(web.input(_unicode=False)):
+ return view.reference_form(f, applicant)
+ else:
+ success = True
+ try:
+ submissions.submit_reference(applicant, f.d)
+ except:
+ raise
+ success = False
+ raise web.seeother('/submit_reference/%s?success=%s' % (secret_md5, success))
+
+ def form(self):
+ return form.Form(
+ form.Dropdown('referee_rating',
+ ('',
+ ('1', '* - Not recommended'),
+ ('2', '** - Has reserve'),
+ ('3', '*** - Average'),
+ ('4', '**** - Strong applicant'),
+ ('5', '***** - Outstanding')),
+ form.notnull,
+ description='How would you score this applicant?'),
+ form.Textarea('reference',
+ form.notnull,
+ description='Please describe in less than 500 words why you recommmend this applicant.'),
+ form.Button('submit', type='submit', value='Submit reference'),
+ )
0  app/helpers/__init__.py
No changes.
151 app/helpers/email_templates.py
@@ -0,0 +1,151 @@
+# Author: Alex Ksikes
+
+# TODO:
+# - this should be moved to a DB.
+
+import web
+import config
+
+_from = config.mail_sender
+bcc = 'alex.ksikes@gmail.com'
+
+msg_to_applicant = \
+'''$def with (applicant)
+Dear $applicant.first_name $applicant.last_name,
+
+Your application for the Machine Learning Summer School in
+Cambridge has been received. We will contact $applicant.referee_name
+and ask for a letter of reference. Both you and the referee
+will receive notification once the letter has been received.
+Decisions on applications will be emailed by the end of June.
+
+Thank you very much for applying.
+
+With regards
+
+The Organisers
+Machine Learning Summer School 2009, University of Cambridge
+'''
+
+msg_to_applicant_simple = \
+'''$def with (applicant)
+Dear $applicant.first_name $applicant.last_name,
+
+Your application for the Machine Learning Summer School in
+Cambridge has been received. Decisions on applications will be
+emailed by the end of June.
+
+Thank you very much for applying.
+
+With regards
+
+The Organisers
+Machine Learning Summer School 2009, University of Cambridge
+'''
+
+msg_to_referee = \
+'''$def with (applicant)
+Dear $applicant.referee_name,
+
+$applicant.first_name $applicant.last_name has applied for the Machine Learning Summer School
+2009 in Cambridge and named you as referee.
+
+We expect to receive more applications for the summer school than
+there are places available, and will select participants based on
+their academic background. We would ask you to submit a letter of
+reference that helps us evaluate the applicant's qualification.
+Your letter should address the applicant's background and potential
+in Machine Learning, academic standing compared to other students,
+and how the student would benefit from attending MLSS 2009.
+
+If the applicant wishes to apply for financial support (we will be
+able to provide support to a limited number of participants),
+please include a paragraph explaining why such support is required.
+Otherwise, we will not consider the applicant for financial support.
+
+Your letter can be entered as plain text under the following URL:
+
+http://%s/submit_reference/$applicant.secret_md5
+
+For more information about the summer school, please refer to
+
+http://mlg.eng.cam.ac.uk/mlss09/
+
+Thank you very much.
+
+With regards
+
+The Organisers
+Machine Learning Summer School 2009, University of Cambridge
+''' % config.site_domain
+
+msg_notify_applicant = \
+'''$def with (applicant)
+Dear $applicant.first_name $applicant.last_name,
+
+This is to inform you that your letter of reference for the
+Machine Learning Summer School 2009 has been received. You should
+receive a decision on your application by the end of June, 2009.
+
+With regards
+
+The Organisers
+Machine Learning Summer School 2009, University of Cambridge
+'''
+
+msg_notify_referee = \
+'''$def with (applicant)
+Dear $applicant.referee_name
+
+Your letter of reference for $applicant.first_name, $applicant.last_name has been received.
+Thank you very much for your assistance.
+
+With regards
+
+The Organisers
+Machine Learning Summer School 2009, University of Cambridge
+'''
+
+msg_resend_password = \
+'''$def with (user)
+Hello $user.nickname,
+
+Here are the login information you have requested:
+
+login: $user.email
+password: $user.password
+
+Thank you,
+
+The Machine Learning Summer School admin at Cambridge.
+'''
+
+def to_applicant(applicant):
+ subject = 'MLSS 2009: Thank You for Your Application'
+ msg = web.template.Template(msg_to_applicant)(applicant)
+ web.sendmail(_from, applicant.email, subject, msg, bcc=bcc)
+
+def to_applicant_simple(applicant):
+ subject = 'MLSS 2009: Thank You for Your Application'
+ msg = web.template.Template(msg_to_applicant_simple)(applicant)
+ web.sendmail(_from, applicant.email, subject, msg, bcc=bcc)
+
+def to_referee(applicant):
+ subject = 'Request for Reference Letter for MLSS 2009 - Deadline: June 11'
+ msg = web.template.Template(msg_to_referee)(applicant)
+ web.sendmail(_from, applicant.referee_email, subject, msg, bcc=bcc)
+
+def notify_applicant(applicant):
+ subject = 'MLSS 2009: Reference Letter'
+ msg = web.template.Template(msg_notify_applicant)(applicant)
+ web.sendmail(_from, applicant.email, subject, msg, bcc=bcc)
+
+def notify_referee(applicant):
+ subject = 'MLSS 2009: Reference Letter Received'
+ msg = web.template.Template(msg_notify_referee)(applicant)
+ web.sendmail(_from, applicant.referee_email, subject, msg, bcc=bcc)
+
+def resend_password(user):
+ subject = 'MLSS - Password request'
+ msg = web.template.Template(msg_resend_password)(user)
+ web.sendmail(_from, user.email, subject, msg, bcc=bcc)
100 app/helpers/misc.py
@@ -0,0 +1,100 @@
+# Author: Alex Ksikes
+
+# TODO:
+# - we should subclass form.Form instead.
+
+import web, re, urlparse, datetime, urllib, utils, string
+
+def format_date(d, f):
+ return d.strftime(f)
+
+def url_quote(url):
+ return web.urlquote(url)
+
+def html_quote(html):
+ return web.htmlquote(url)
+
+def url_encode(query, clean=True, doseq=True, **kw):
+ query = web.dictadd(query, kw)
+ if clean is True:
+ for q, v in query.items():
+ if not v:
+ del query[q]
+ return urllib.urlencode(query, doseq)
+
+def cut_length(s, max=40, tooltip=False):
+ s_cut = s[0:]
+ if len(s) > max:
+ s_cut = s[0:max] + '...'
+ if tooltip:
+ s_cut = '<span title="%s">%s</span>' % (s, s_cut)
+
+ return s_cut
+
+def get_nice_url(url):
+ host, path = urlparse.urlparse(url)[1:3]
+ if path == '/':
+ path = ''
+ return cut_length(host+path)
+
+def capitalize_first(str):
+ if not str:
+ str = ''
+ return ' '.join(map(string.capitalize, str.lower().split()))
+
+def text2html(s):
+ s = replace_breaks(s)
+ s = replace_indents(s)
+ return replace_links(s)
+
+def replace_breaks(s):
+ return re.sub('\n', '<br />', s)
+
+def replace_indents(s):
+ s = re.sub('\t', 4*' ', s)
+ return re.sub('\s{2}', '&nbsp;'*2, s)
+
+def replace_links(s):
+ return re.sub('(http://[^\s]+)', r'<a rel="nofollow" href="\1">' + get_nice_url(r'\1') + '</a>', s, re.I)
+
+# we may need to get months ago as well
+def how_long(d):
+ return web.datestr(d, datetime.datetime.now())
+
+def split(pattern, str):
+ return re.split(pattern, str)
+
+def sub(pattern, rpl, str):
+ p = re.compile(pattern, re.I)
+ return p.sub(rpl, str)
+
+def render_form(form, from_input, to_input):
+ # do each input from from_input to to_input
+ inputs = list(form.inputs)
+ def index(inputs, name):
+ for n, input in enumerate(inputs):
+ if input.name == name:
+ return n
+ return -1
+
+ start = index(inputs, from_input)
+ till = index(inputs, to_input)
+ form.inputs = inputs[start:till+1]
+
+ # render the top note ourselves
+ html = ''
+ if start == 0 and not form.valid:
+ if form.note:
+ html = '<div class="wrong">%s</div>\n' % form.note
+ else:
+ html = '<div class="wrong">Oups looks like you\'ve made a couple of errors. Please correct the errors below and try again.</div>\n'
+ form.note = None
+
+ html += form.render_css()
+ form.inputs = tuple(inputs)
+
+ return html
+
+def get_site_domain():
+ import config
+ return config.site_domain
72 app/helpers/paging.py
@@ -0,0 +1,72 @@
+# Author : Alex Ksikes
+
+import urllib
+
+def get_paging(start, max_results, query=False, results_per_page=15, window_size=15):
+ c_page = start / results_per_page + 1
+ if not start:
+ c_page = 1
+ nb_pages = max_results / results_per_page
+ if max_results % results_per_page != 0:
+ nb_pages += 1
+
+ left_a = right_a = ''
+ if c_page > 1:
+ left_a = (c_page - 2) * results_per_page
+ if c_page < nb_pages:
+ right_a = start + results_per_page
+
+ leftmost_a = rightmost_a = ''
+ if (c_page - 3) >= 0:
+ leftmost_a = 0
+ if (c_page + 1) < nb_pages:
+ rightmost_a = (nb_pages -1) * results_per_page
+
+ left = c_page - window_size / 2
+ if left < 1:
+ left = 1
+ right = left + window_size - 1
+ if right > nb_pages:
+ right = nb_pages
+
+ pages = []
+ for i in range(left, right + 1):
+ pages.append({
+ 'number' : i,
+ 'start' : (i - 1) * results_per_page
+ })
+
+ return {
+ 'start' : start,
+ 'max_results' : max_results,
+ 'c_page' : c_page,
+ 'nb_pages' : nb_pages,
+ 'pages' : pages,
+ 'leftmost_a' : leftmost_a,
+ 'left_a' : left_a,
+ 'right_a' : right_a,
+ 'rightmost_a' : rightmost_a,
+ 'query_enc' : query and urllib.quote(query) or ''
+ }
+
+def get_paging_results(start, max_results, id, results, results_per_page):
+ index = 0
+ results = list(results)
+ for i, r in enumerate(results):
+ if r.id == id: index = i
+ pager = dict(left_start=start, right_start=start, max_results=max_results,
+ number=start + index, left=False, middle=results[index], right=False)
+
+ if index > 0 or start > 0:
+ pager['left'] = results[index-1]
+ if index < len(results) - 1:
+ pager['right'] = results[index+1]
+ if index == results_per_page - 1:
+ pager['right_start'] = start + results_per_page
+ if index == 0 and start > 0:
+ pager['left_start'] = start - results_per_page
+ if start != 0:
+ pager['number'] = start + index -1
+
+ return pager
+
68 app/helpers/session.py
@@ -0,0 +1,68 @@
+# Author: Alex Ksikes
+
+# TODO:
+# - webpy session module is ineficient and makes 5 db calls per urls!
+# - because sessions are attached to an app, every user has sessions whether they atually need it or not.
+# - login required decorator should save intended user action before asking to login.
+
+import web
+from config import db
+
+from app.models import users
+from app.models import applicants
+
+def add_sessions_to_app(app):
+ if web.config.get('_session') is None:
+ store = web.session.DBStore(db, 'sessions')
+ session = web.session.Session(app, store,
+ initializer={'is_logged' : False})
+ web.config._session = session
+ else:
+ session = web.config._session
+
+def get_session():
+ return web.config._session
+
+def is_logged():
+ return get_session().is_logged
+
+def login(email):
+ s = get_session()
+ for k, v in users.get_user_by_email(email).items():
+ s[k] = v
+ s.is_logged = True
+
+def logout():
+ get_session().kill()
+
+def reset():
+ user = users.get_user_by_id(get_user_id())
+ login(user.email)
+
+def get_user_id():
+ return get_session().id
+
+def get_last_visited_url():
+ redirect_url = web.cookies(redirect_url='/').redirect_url
+ web.setcookie('redirect_url', '', expires='')
+ return redirect_url
+
+def set_last_visited_url():
+ url = web.ctx.get('path')
+ if url:
+ web.setcookie('redirect_url', url)
+
+def login_required(meth):
+ def new(*args):
+ if not is_logged():
+ return web.redirect('/account')
+ return meth(*args)
+ return new
+
+def login_required_for_reviews(meth):
+ def new(*l):
+ context = l[1] or web.input(context='').context
+ if not is_logged() and context == 'reviewed':
+ return web.redirect('/account')
+ return meth(*l)
+ return new
52 app/helpers/utils.py
@@ -0,0 +1,52 @@
+# Author: Alex Ksikes
+
+import web
+import config
+
+import md5, time, types
+
+def dict_remove(d, *keys):
+ for k in keys:
+ if d.has_key(k):
+ del d[k]
+
+def capitalize_first(str):
+ if not str:
+ str = ''
+ return ' '.join(map(string.capitalize, str.lower().split()))
+
+def make_unique_md5():
+ return md5.md5(time.ctime() + config.encryption_key).hexdigest()
+
+def get_all_functions(module):
+ functions = {}
+ for f in [module.__dict__.get(a) for a in dir(module)
+ if isinstance(module.__dict__.get(a), types.FunctionType)]:
+ functions[f.__name__] = f
+ return functions
+
+def sqlands(left, lst):
+ """Similar to webpy sqlors but for ands"""
+ if isinstance(lst, web.utils.iters):
+ lst = list(lst)
+ ln = len(lst)
+ if ln == 0:
+ return web.SQLQuery("1!=2")
+ if ln == 1:
+ lst = lst[0]
+ if isinstance(lst, web.utils.iters):
+ return web.SQLQuery(['('] +
+ sum([[left, web.sqlparam(x), ' AND '] for x in lst], []) +
+ ['1!=2)']
+ )
+ else:
+ return left + web.sqlparam(lst)
+
+def get_ip():
+ return web.ctx.get('ip', '000.000.000.000')
+
+def store(tablename, _test=False, **values):
+ try:
+ db.insert(tablename, **values)
+ except:
+ db.update(tablename, **values)
0  app/models/__init__.py
No changes.
142 app/models/applicants.py
@@ -0,0 +1,142 @@
+# Author: Alex Ksikes
+
+# TODO:
+# - search should be improved, currently does not allow to search across all fields.
+
+import web
+from config import db
+
+def get_where(context='', query=None, user_id=None):
+ where = 'status is NULL or status is not NULL'
+
+ # if a context is entered
+ if context == 'new':
+ where = 'status is NULL and (referee_rating is not NULL or affiliated)'
+ elif context == 'pending':
+ where = 'referee_rating is NULL'
+ elif context == 'admitted':
+ where = 'status = "admitted"'
+ elif context == 'rejected':
+ where = 'status = "rejected"'
+ elif user_id and context == 'reviewed':
+ user_id = web.sqlquote(str(user_id))
+ where = 'a.id = v.applicant_id and v.user_id = %s' % user_id
+# (a.id = c.applicant_id and c.user_id = %s)''' % (user_id, user_id)
+
+ # if in search mode
+ if query:
+ columns = 'first_name, last_name, a.email, affiliation, department, referee_name, status, \
+ occupation, website, interests'.split(', ')
+ query = web.sqlquote('%%' + query.encode('utf-8') + '%%')
+
+ where = (' like %s or ' % query).join(columns)
+ where += ' like %s' % query
+ where += ' or concat(first_name, " ", last_name) like ' + query
+
+ return where
+
+def query(context='', query=None, offset=0, limit=50, order='creation_ts desc', user_id=None):
+ where = get_where(context, query, user_id)
+
+ what = '''
+ a.id, a.first_name, a.last_name, a.email, a.website, a.affiliation, a.affiliated, a.department, a.interests, a.degree, a.occupation, a.secret_md5, a.referee_name, a.referee_email, a.referee_affiliation, a.referee_rating, a.creation_ts, a.update_ts, a.status, a.decided_by_user_id, a.calculated_vote, a.nationality, a.country, a.pascal_member, a.travel_support, a.travel_support_budget, a.gender, a.grant_amount,
+ (select group_concat(u.nickname separator ', ') from votes as v, users as u
+ where v.user_id = u.id and v.applicant_id = a.id) as voters,
+ (select count(*) from votes as v, users as u
+ where v.user_id = u.id and v.applicant_id = a.id) as number_votes,
+ (select group_concat(distinct u.nickname separator ', ') from comments as c, users as u
+ where c.user_id = u.id and c.applicant_id = a.id) as commenters,
+ (select count(*) from comments as c, users as u
+ where c.user_id = u.id and c.applicant_id = a.id) as number_comments,
+ (select nickname from users as u
+ where u.id = a.decided_by_user_id) as decided_by_nickname'''
+
+ table = 'applicants as a'
+ if user_id and context == 'reviewed':
+ table = 'applicants as a, votes as v, comments as c'
+
+ results = db.select(table,
+ what = what,
+ where = where,
+ offset = offset,
+ limit = limit,
+ order = order)
+
+ count = int(db.select(table, what='count(distinct a.id) as c', where=where)[0].c)
+
+ return (results, count)
+
+def get_by_id(id):
+ return web.listget(
+ db.select('applicants as a, users as u',
+ vars=dict(id=id),
+ what = 'a.*, nickname',
+ where='a.id=$id and (u.id = a.decided_by_user_id or a.decided_by_user_id is NULL)'), 0, {})
+
+def admit(ids, user_id):
+ ids = web.sqllist(map(str, ids))
+ db.update('applicants',
+ where = 'id in (%s)' % ids,
+ status = 'admitted', decided_by_user_id=user_id)
+
+def reject(ids, user_id):
+ ids = web.sqllist(map(str, ids))
+ db.update('applicants',
+ where = 'id in (%s)' % ids,
+ status = 'rejected', decided_by_user_id=user_id)
+
+def undecide(ids, user_id):
+ ids = web.sqllist(map(str, ids))
+ db.update('applicants',
+ where = 'id in (%s)' % ids,
+ status = None, decided_by_user_id=user_id)
+
+def grant(user_id, id, amount):
+ db.update('applicants',
+ where = 'id = %s' % id,
+ grant_amount = amount, granted_by_user_id=user_id)
+
+def add_comment(id, comment):
+ db.update('applicants',
+ vars = dict(id=id),
+ where = 'id = $id',
+ comment = comment)
+
+def rate(ids, score):
+ ids = web.sqllist(map(str, ids))
+ db.update('applicants',
+ where = 'id in (%s)' % ids,
+ score = score)
+
+def get_counts():
+ counts = web.storage(new=0, pending=0, all=0, admitted=0, rejected=0, reviewed=0)
+ for context in counts.keys():
+ counts[context] = db.select('applicants',
+ what = 'count(*) as c',
+ where = get_where(context))[0].c
+ return counts
+
+def get_by_secret_md5(secret_md5):
+ return web.listget(db.select(
+ 'applicants',
+ vars = dict(md5=secret_md5),
+ where = 'secret_md5 = $md5'), 0, False)
+
+def get_dummy_record():
+ return web.storage(first_name='Applicant', last_name='Name', email='', referee_name='Referee Name')
+
+def get_stats():
+ stats = web.storage(undecided=0, admitted=0, rejected=0, allocated_amount=0)
+
+ results = db.select('applicants',
+ what = 'status, count(id) as c',
+ group = 'status')
+
+ for r in results:
+ if not r.status: r.status = 'undecided'
+ stats[r.status] = r.c
+
+ stats.allocated_amount = db.select('applicants',
+ what = 'sum(grant_amount) as amount')[0].amount
+
+ return stats
23 app/models/comments.py
@@ -0,0 +1,23 @@
+# Author: Alex Ksikes
+
+import web
+from config import db
+
+def add_comment(user_id, applicant_id, comment):
+ db.insert('comments',
+ user_id=user_id, applicant_id=applicant_id, comment=comment)
+
+def delete_comment(user_id, id):
+ db.delete('comments',
+ vars = dict(user_id=user_id, id=id),
+ where = 'user_id = $user_id and id = $id')
+
+def get_comments(applicant_id):
+ return db.query(
+ '''select c.*, nickname, score \
+ from comments as c
+ left join users as u on c.user_id = u.id
+ left join votes as v on u.id = v.user_id and v.applicant_id = $id
+ where c.applicant_id = $id
+ order by c.creation_ts desc''',
+ vars = dict(id=applicant_id))
79 app/models/submissions.py
@@ -0,0 +1,79 @@
+# Author: Alex Ksikes
+
+import web
+from config import db
+
+from app.helpers import utils
+from app.helpers import email_templates
+
+from cStringIO import StringIO
+from datetime import date
+
+resume_dir = 'public/resumes/'
+
+def submit_application(resume, d, simple=False):
+ # clean up some fields
+ clean_up_data(d)
+ # handle the file upload
+ d.resume_fn = save_resume(resume, d.first_name, d.last_name)
+ # in simple mode, no reference is required
+ if simple:
+ # save the data in the db
+ add_applicant(d)
+ # email user and referee
+ email_templates.to_applicant_simple(d)
+ else:
+ # make secret md5
+ d.secret_md5 = utils.make_unique_md5()
+ # save the data in the db
+ add_applicant(d)
+ # email user and referee
+ email_templates.to_applicant(d)
+ email_templates.to_referee(d)
+
+def clean_up_data(d):
+ utils.dict_remove(d, 'resume', 'submit', 'referee_email_again', 'email_again')
+ d.gender = d.gender.lower()
+ d.pascal_member = d.pascal_member == 'Yes';
+ d.travel_support = d.get('travel_support', None) and True or None;
+ d.affiliated = d.get('affiliated', False) != False;
+
+def is_email_available(email):
+ return not db.select(
+ 'applicants',
+ vars = dict(email=email),
+ what = 'count(id) as c',
+ where = 'email = $email')[0].c
+
+def save_resume(resume, first_name, last_name):
+ ext = resume.filename.split('.')[-1]
+ fname = '%s.%s.%s.resume.%s' % (first_name, last_name, date.today(), ext)
+ fname = fname.replace(' ', '-')
+ open(resume_dir + fname, 'wb').write(resume.value)
+ return fname
+
+def add_applicant(d):
+ db.insert('applicants', **d)
+
+def submit_reference(applicant, d):
+ # clean up some fields
+ utils.dict_remove(d, 'submit')
+ # add the provided reference
+ add_reference(applicant.secret_md5, d)
+ # notify user and referee by email
+ email_templates.notify_applicant(applicant)
+ email_templates.notify_referee(applicant)
+
+def get_by_secret_md5(secret_md5):
+ return web.listget(db.select(
+ 'applicants',
+ vars = dict(md5=secret_md5),
+ where = 'secret_md5 = $md5'), 0, False)
+
+# TODO: set update_ts as well
+def add_reference(secret_md5, d):
+ db.update(
+ 'applicants',
+ vars = dict(md5=secret_md5),
+ where = 'secret_md5=$md5',
+ **d)
34 app/models/users.py
@@ -0,0 +1,34 @@
+# Author: Alex Ksikes
+
+import web
+from config import db
+
+def create_account(email, password, nickname):
+ db.insert('users', email=email, password=password, nickname=nickname)
+
+def get_user_by_email(email):
+ return web.listget(
+ db.select('users', vars=dict(email=email),
+ where='email = $email'), 0, {})
+
+def is_email_available(email):
+ return not db.select(
+ 'users',
+ vars = dict(email=email),
+ what = 'count(id) as c',
+ where = 'email = $email')[0].c
+
+def is_valid_password(password):
+ return len(password) >= 5
+
+def is_correct_password(email, password):
+ user = get_user_by_email(email)
+ return user.get('password', False) == password
+
+def update(id, **kw):
+ db.update('users', vars=dict(id=id), where='id = $id', **kw)
+
+def get_user_by_id(id):
+ return web.listget(
+ db.select('users', vars=dict(id=id),
+ where='id = $id'), 0, {})
34 app/models/votes.py
@@ -0,0 +1,34 @@
+# Author: Alex Ksikes
+
+import web
+from config import db
+
+def add(ids, score, user_id):
+ if not 0 <= score <= 5 or not user_id or not ids: return
+ for id in ids:
+ db.query(
+ 'insert votes (user_id, applicant_id, score)' +
+ ' values ($user_id, $applicant_id, $score)' +
+ ' on duplicate key update' +
+ ' user_id = $user_id, applicant_id = $applicant_id, score = $score',
+ vars = dict(user_id=user_id, applicant_id=id, score=score))
+ update_calculated_votes(id)
+
+def update_calculated_votes(applicant_id):
+ calculated = \
+ db.select('votes',
+ vars = dict(applicant_id=applicant_id),
+ what = 'sum(score) / count(score) as vote, count(score) as vote_counts',
+ where = 'applicant_id = $applicant_id')[0]
+
+ db.update('applicants',
+ vars = dict(id=applicant_id),
+ where = 'id = $id',
+ calculated_vote = calculated.vote, calculated_vote_counts = calculated.vote_counts)
+
+def get_votes(applicant_id):
+ return db.select('votes as v, users as u',
+ vars = dict(id=applicant_id),
+ what = 'v.*, nickname',
+ where = 'applicant_id = $id and v.user_id = u.id',
+ order = 'creation_ts')
13 app/tests/test_sessions.py
@@ -0,0 +1,13 @@
+# Author: Alex Ksikes
+
+import web
+
+class test_session:
+ def GET(self):
+ session = web.config._session
+ print 'session', session
+ session.count += 1
+ session.new = 'hello'
+ session.kill()
+ return ('it\'s cool it works yeah yeah ...' +
+ 'Hello, %s! %s' % (session.count, session.new))
63 app/tests/test_uploads.py
@@ -0,0 +1,63 @@
+# Author: Alex Ksikes
+
+import web
+import config
+
+from config import view
+from web import form
+
+
+class upload:
+ def GET(self):
+ return r'''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+ <html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <title>Apply</title>
+ <link href="mlss.css" rel="stylesheet" type="text/css" />
+ </head>
+
+ <body>
+
+ <div class="submitted">
+ </div>
+ <form action="/apply" enctype="multipart/form-data" method="post" name='theform'>
+ <fieldset>
+ <legend>Application to MLSS 2009: (All fields are required)</legend>
+ <label for="resume_fn">Upload your resume in PDF</label><input type="file" name="resume_fn" id="resume_fn" />
+ <label for="submit"></label><button name="submit" type="submit" id="submit">submit</button>
+ </fieldset>
+ </form>
+ </body>
+ </html>'''
+
+ def POST(self):
+ return 'OK'
+
+class upload2:
+ def GET(self):
+ yield r'''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+ <html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ </head>
+ <body>
+ <form action="" enctype="multipart/form-data" method="post">'''
+ yield self.form().render_css()
+ yield '<form action="" enctype="multipart/form-data" method="post">'
+ yield '</form></body></html>'
+
+ def POST(self):
+ f = self.form()
+ f.validates(web.input(_unicode=False))
+ yield f.d.resume_fn
+ if not f.valid:
+ yield 'Missing something'
+ yield 'You will never see this text if you upload a file.'
+
+ def form(self):
+ return form.Form(
+ form.File('resume_fn',
+ form.notnull,
+ description='It will take forever if we validate the form'),
+ form.Button('submit', type='submit', value='Apply'))
32 app/views/account.html
@@ -0,0 +1,32 @@
+$def with (show, login_form, register_form, forgot_password_form, on_success_message='')
+
+$if show in ['login_only', 'all']:
+ <form method="post" class="default_form" action="/account/login">
+ <fieldset>
+ <legend>Login:</legend>
+ $:login_form.render_css()
+ <input type="hidden" name="show" value="$show"/>
+ </fieldset>
+ </form>
+
+$if show in ['register_only', 'all']:
+ <form method="post" class="default_form" action="/account/register">
+ <fieldset>
+ $if show == 'all':
+ <legend>Or create an account:</legend>
+ $else:
+ <legend>Create an account:</legend>
+ $:register_form.render_css()
+ <input type="hidden" name="show" value="$show"/>
+ </fieldset>
+ </form>
+
+$if show in ['login_only', 'register_only', 'forgot_password_only', 'all']:
+ <form method="post" class="default_form forgot_password_form" action="/account/resend_password">
+ <fieldset>
+ <legend>Forgot your password?</legend>
+ <span class="on_success_message">$on_success_message</span>
+ $:forgot_password_form.render_css()
+ <input type="hidden" name="show" value="$show"/>
+ </fieldset>
+ </form>
245 app/views/applicant.html
@@ -0,0 +1,245 @@
+$def with (applicant, comments, votes, user, pager, inputs)
+
+$def show_actions():
+ $ to = dict(pending='No reference', new='review', admitted='Admitted', all='All Applicants', rejected='Rejected', search='Search', reviewed='My reviews')[inputs.context]
+ $#<a class="back" href="/$context?start=$pager.left_start&order=$order_selected&desc=$desc_selected&query=$query"><strong><< Back $to.lower()</strong></a>
+ <a class="back" href="/$inputs.context?$url_encode(inputs)"><strong>&laquo; Back $to.lower()</strong></a>
+ $if inputs.context in ['all', 'pending', 'rejected', 'new', 'search', 'reviewed']:
+ <input class="admit" type="button" value="Admit"
+ onClick="document.theform.action = '/admit'; document.theform.submit();"/>
+ $if inputs.context in ['all', 'pending', 'admitted', 'new', 'search', 'reviewed']:
+ <input class="reject" type="button" value="Reject"
+ onClick="document.theform.action = '/reject'; document.theform.submit();"/>
+ $if inputs.context in ['all', 'pending', 'admitted', 'rejected', 'new', 'search', 'reviewed']:
+ <input type="button" value="Undecide"
+ onClick="document.theform.action = '/undecide'; document.theform.submit();"/>
+
+ <select class="score" name="score"
+ onChange="document.theform.action = '/rate'; document.theform.submit();">
+ <option value="">Rate applicants</option>
+ <option value="" disabled="">---------------------------------------------------</option>
+ <option value="5">5 stars: definite accept,&nbsp;&nbsp;&nbsp;&nbsp;top 10%</option>
+ <option value="4">4 stars: as many as we can,&nbsp;&nbsp;&nbsp;&nbsp;10%</option>
+ <option value="3">3 stars: maybe,&nbsp;&nbsp;&nbsp;&nbsp;10%</option>
+ <option value="2">2 stars: probably reject,&nbsp;&nbsp;&nbsp;&nbsp;20%</option>
+ <option value="1">1 star: definitely reject,&nbsp;&nbsp;&nbsp;&nbsp;bottom 50%</option>
+ </select>
+
+$def show_paging():
+ $if pager.left:
+ $#<a href="/applicant/$pager.left.id?context=$context&start=$pager.left_start&order=$order_selected&desc=$desc_selected&query=$query">< Prev</a>
+ <a href="/applicant/$pager.left.id?$url_encode(inputs, start=pager.left_start)">&#8249; Previous</a>
+ $(pager.number + 1) of $pager.max_results
+ $if pager.right:
+ $#<a href="/applicant/$pager.right.id?context=$context&start=$pager.right_start&order=$order_selected&desc=$desc_selected&query=$query">Next ></a>
+ <a href="/applicant/$pager.right.id?$url_encode(inputs, start=pager.right_start)">Next &#8250;</a>
+
+$def show_stars(rating, color='', small=''):
+ $if small:
+ $ small = '_small'
+
+ $for i in range(1,6):
+ $if rating >= i:
+ $if color == 'green':
+ <img src='/img/stars/full$(small)_green.gif'/>\
+ $else:
+ <img src='/img/stars/full$(small).gif'/>\
+ $else:
+ $if rating >= i - 0.5:
+ <img src='/img/stars/half$(small).gif'/>\
+ $else:
+ <img src='/img/stars/empty$(small).gif'/>\
+
+$def google_link(name, kind=''):
+ $ name = '"' + name + '"'
+ $if kind == 'scholar':
+ <a href="http://scholar.google.com/scholar?q=$url_quote(name)">scholar</a>
+ $else:
+ <a href="http://www.google.com/search?q=$url_quote(name)">google</a>
+
+$def cloud_link(name):
+ $ name = '(@author %s)' % sub('^(prof|doc|dr)[.\w]*', '', name)
+ $ dblp_domain = 'dblp.cloudmining.net'
+ <a class="cloud_link" href="http://$dblp_domain/search?query=$url_quote(name)">dblp cloud</a>
+
+<form name="theform" method="post">
+ <input type="hidden" name="context" value="$inputs.context">
+ <div class="actionbar">
+ $:show_actions()
+
+ <span class="paging">
+ $:show_paging()
+ </span>
+ </div>
+
+ <div class="result_wrapper">
+ <div class="applicant">
+ $ name = applicant.first_name + ' ' + applicant.last_name
+ <input type="hidden" name="id" value="$applicant.id">
+
+ <h1>$name, $applicant.occupation</h1>
+ <h2>$applicant.department, $applicant.affiliation</h2>
+
+ $if applicant.affiliated:
+ <div class="affiliated">Affiliated to the university (no reference needed, no travel/accomodation support and charged a smaller fee).</div>
+
+ <div class="applicant_score">
+ $if applicant.calculated_vote:
+ $:show_stars(applicant.calculated_vote) ($applicant.calculated_vote / 5) -
+ <strong>
+ $if applicant.status == 'admitted':
+ <img src="/img/accept.png"/> Admitted
+ $elif applicant.status == 'rejected':
+ <img src="/img/delete.png"/> Rejected
+ $else:
+ <img src="/img/error.png"/> Undecided
+ </strong>
+ $if applicant.decided_by_user_id:
+ by $applicant.nickname
+ </div>
+
+ $if votes:
+ <div class="applicant_score_details">
+ Rated $applicant.calculated_vote_counts times by
+ $for vote in votes:
+ $vote.nickname $:show_stars(vote.score, small=True)
+ $if not loop.last: ,
+ </div>
+
+ $if applicant.grant_amount:
+ <div class="scholarship">
+ <img src="/img/money.png"/> Allocated \$$applicant.grant_amount of financial support.
+ </div>
+
+ <div class="simple_box">
+ <a href="/resumes/$applicant.resume_fn"><strong>Download resume</strong></a> uploaded $how_long(applicant.creation_ts) - highest degree received is <strong>$applicant.degree</strong>.
+ </div>
+
+ <div class="simple_box">
+ Contact info:
+ $if applicant.website:
+ <a class="website" href='$applicant.website'>website</a> /
+ <a href="mailto:$applicant.email">$applicant.email</a> / $:google_link(name) / $:google_link(name, 'scholar') / $:cloud_link(name)
+ </div>
+
+ <div class="simple_box">
+ Interested in:
+ $for i in split('[,;:]+', applicant.interests):
+ <a class='animated' href="/search?query=$url_quote(i)">$i</a>&nbsp;
+ </div>
+
+ <div class="simple_box">
+ Addtional info:
+ $applicant.gender.capitalize() of nationality $applicant.nationality.capitalize() and country of affiliation $applicant.country.capitalize()
+ $if applicant.pascal_member: who is a pascal member
+ </div>
+
+ $if applicant.travel_support:
+ <div class="simple_box" id='travel_support'>
+ Financial support: This applicant needs \$$applicant.travel_support_budget.
+ $if applicant.grant_amount:
+ Received <strong>\$$applicant.grant_amount</strong>.
+
+ <div class="grant">
+ <input type="text" name="amount" value="">
+ <input type="button" value="Allocate"
+ onClick="document.theform.action = '/grant/$applicant.id'; document.theform.submit();"/>
+ </div>
+ </div>
+
+ $if not applicant.affiliated or applicant.referee_email:
+ <div class="box">
+ <div class="box_title">Referred by <strong>$applicant.referee_name</strong>, $applicant.referee_affiliation
+ $if applicant.update_ts:
+ on
+ $how_long(applicant.update_ts)
+ </div>
+
+ <div class="sub_title">
+ <a href="mailto:$applicant.referee_email">contact referee</a> /
+ $:google_link(applicant.referee_name) / $:google_link(applicant.referee_name, 'scholar') / $:cloud_link(applicant.referee_name)
+ </div>
+ <div class="box_content">
+ $if applicant.referee_rating:
+ <div class="referee_score">$:show_stars(applicant.referee_rating, 'green')</div>
+ $:text2html(applicant.reference)
+ $else:
+ Waiting for the referee to send the letter. (<strong>pending</strong>)
+
+ $if not applicant.referee_rating:
+ <p>The secret url to submit the reference for $name is:
+ <br/>
+ <strong>
+ <a href="http://$get_site_domain()/submit_reference/$applicant.secret_md5">
+ http://$get_site_domain()/submit_reference/$applicant.secret_md5</a>
+ </strong>
+ </p>
+
+ </div>
+ </div>
+
+ $if applicant.poster_title:
+ <div class="box">
+ <div class="box_title">Tentative poster title: <strong>$applicant.poster_title</strong></div>
+ $if applicant.abstract:
+ <div class="box_content">
+ $:text2html(applicant.abstract)
+ </div>
+ </div>
+
+ $if applicant.cover_letter:
+ <div class="box">
+ <div class="box_title">Additional comments in support of this application:</div>
+ <div class="box_content">
+ $:text2html(applicant.cover_letter)
+ </div>
+ </div>
+ </div>
+
+ <div class="comments">
+ $ display_add_handle = 'block'
+ $if not comments:
+ $ display_add_handle = 'none'
+
+
+ <a id="add_handle" href="javascript: replace_elm('add_handle', 'add_comment');"
+ $if not comments:
+ style="display:none;"
+ >Add a comment</a>
+
+ <div id="add_comment"
+ $if comments:
+ style="display:none;"
+ $else:
+ style="display:block;"
+ >
+ <div class="box_title">Add a comment:</div>
+ <textarea id="comment" name="comment"></textarea>
+ <input type="button" value="Add comment"
+ onClick="document.theform.action = '/applicant/$applicant.id/comment'; document.theform.submit();"/>
+ </div>
+
+ $for c in comments:
+ <div class="box">
+ <div class="box_title">
+ <strong>$c.nickname</strong>, $how_long(c.creation_ts)
+ $if user.is_logged and user.id == c.user_id:
+ (<a href="/delete_comment/$c.id">delete</a>)
+ </div>
+
+ <div class="box_content">
+ $:text2html(c.comment)
+ </div>
+ </div>
+ </div>
+
+ </div>
+
+ <div class="actionbar">
+ $:show_actions()
+
+ <span class="paging">
+ $:show_paging()
+ </span>
+ </div>
+</form>
178 app/views/applicants.html
@@ -0,0 +1,178 @@
+$def with (results, context, pager, inputs)
+
+$# This is no longer used.
+$def show_sort_by():
+ $for order_name, order_by in [('Applicant Name', 'first_name'), ('Occupation', 'occupation'), ('Affiliation', 'affiliation'), ('Referee Rating', 'referee_rating'), ('Overall Rating', 'score'), ('Submission Date', 'creation_ts'), ('Status', 'status')]:
+ $ new_desc = dict(desc='asc', asc='desc')[inputs.desc]
+ $if order_by == inputs.order:
+ <a class="selected" href="/$context?$url_encode(inputs, desc=new_desc)">$order_name
+ $# href="/$url_quote(inputs, order=order_by, desc=new_desc)"
+ $if inputs.desc == 'desc':
+ <small>&#9660;</small>
+ $else:
+ <small>&#9650;</small>
+ </a>
+ $else:
+ $#<a href="/$context?order=$order_by&desc=desc&query=$query">$order_name</a>
+ <a href="/$context?$url_encode(inputs, order=order_by)">$order_name</a>
+
+$def show_table_header():
+ <tr>
+ <th class="checkbox"></th>
+ $for order_name, order_by in [('ID', 'id'), ('Applicant Name', 'first_name'), ('Occupation', 'occupation'), ('Affiliation', 'affiliation'), ('Interests', 'interests'), ('FS ($)', 'travel_support_budget'), ('FSA ($)', 'grant_amount'), ('Referee Rating', 'referee_rating'), ('Comments', 'number_comments'), ('Overall Rating', 'calculated_vote'), ('Status', 'status')]:
+ $ new_desc = dict(desc='asc', asc='desc')[inputs.desc]
+ $if order_by == inputs.order:
+ <th id="$order_by"><a class="selected" href="/$context?$url_encode(inputs, start=0, desc=new_desc)">$order_name
+ $if inputs.desc == 'desc':
+ <small>&#9660;</small>
+ $else:
+ <small>&#9650;</small>
+ </a></th>
+ $else:
+ <th id="$order_by"><a href="/$context?$url_encode(inputs, start=0, desc='desc', order=order_by)">$order_name</a></th>
+ </tr>
+
+$def show_actions():
+ $if context in ['all', 'pending', 'rejected', 'new', 'search', 'reviewed']:
+ <input class="admit" type="button" value="Admit"
+ onClick="document.theform.action = '/admit'; document.theform.submit();">
+ $if context in ['all', 'pending', 'admitted', 'new', 'search', 'reviewed']:
+ <input class="reject" type="button" value="Reject"
+ onClick="document.theform.action = '/reject'; document.theform.submit();">
+ $if context in ['all', 'pending', 'admitted', 'rejected', 'new', 'search', 'reviewed']:
+ <input type="button" value="Undecide"
+ onClick="document.theform.action = '/undecide'; document.theform.submit();">
+
+ <select class="score" name="score"
+ onChange="document.theform.action = '/rate'; document.theform.submit();">
+ <option value="">Rate applicants</option>
+ <option value="" disabled="">---------------------------------------------------</option>
+
+ <option value="5">5 stars: definite accept,&nbsp;&nbsp;&nbsp;&nbsp;top 10%</option>
+ <option value="4">4 stars: as many as we can,&nbsp;&nbsp;&nbsp;&nbsp;10%</option>
+ <option value="3">3 stars: maybe,&nbsp;&nbsp;&nbsp;&nbsp;10%</option>
+ <option value="2">2 stars: probably reject,&nbsp;&nbsp;&nbsp;&nbsp;20%</option>
+ <option value="1">1 star: definitely reject,&nbsp;&nbsp;&nbsp;&nbsp;bottom 50%</option>
+
+ </select>
+
+$def show_paging():
+ $if pager.leftmost_a or pager.leftmost_a == 0:
+ <a href="/$context?$url_encode(inputs, start=pager.leftmost_a)">&laquo; Start</a>
+ $if pager.left_a or pager.left_a == 0:
+ <a href="/$context?$url_encode(inputs, start=pager.left_a)">&#8249; Previous</a>
+ $(pager.start + 1) -
+ $if pager.right_a:
+ $pager.right_a
+ $else:
+ $pager.max_results
+ of $pager.max_results
+ $if pager.right_a:
+ <a href="/$context?$url_encode(inputs, start=pager.right_a)">Next &#8250;</a>
+ $if pager.rightmost_a:
+ <a href="/$context?$url_encode(inputs, start=pager.rightmost_a)">End &raquo;</a>
+
+$def show_stars(rating, color=''):
+ $for i in range(1,6):
+ $if rating >= i:
+ $if color == 'green':
+ <img src='/img/stars/full_small_green.gif'/>\
+ $else:
+ <img src='/img/stars/full_small.gif'/>\
+ $else:
+ $if rating >= i - 0.5:
+ <img src='/img/stars/half_small.gif'/>\
+ $else:
+ <img src='/img/stars/empty_small.gif'/>\
+
+<form name="theform" method="post">
+<input type="hidden" name="context" value="$context">
+ <div class="actionbar">
+ $:show_actions()
+
+ $if pager and pager.max_results:
+ <span class="paging">
+ $:show_paging()
+ </span>
+ </div>
+
+ <div class="results">
+ <table>
+ $:show_table_header()
+ $for r in results:
+ <tr class=
+ $if not r.status and (r.referee_rating or r.affiliated):
+ "updates"
+ >
+ <td class="checkbox"><input name="id" type="checkbox" value="$r.id" onclick="highlight_row(this)"></td>
+ <td>$r.id</td>
+ <td>
+ $ name = r.first_name + ' ' + r.last_name
+ <a href="/applicant/$r.id?$url_encode(inputs, context=context, start=pager.start)">$:cut_length(name, 20, tooltip=True)</a>
+ $if r.website:
+ - <a href="$r.website" title="homepage: $r.website"><img src="/img/external.png"/></a>
+ </td>
+ <td>$r.occupation</td>
+ <td>
+ $if r.affiliated:
+ <span class="affiliated" title="Affiliated to the university (no reference needed, no travel/accomodation support and charged a smaller fee).">$cut_length(r.affiliation, 25)</span>
+ $else:
+ $:cut_length(r.affiliation, 25, tooltip=True)
+ </td>
+ <td><em>$:cut_length(r.interests, 100, tooltip=True)</em></td>
+ <td class="score_column">
+ $if r.travel_support:
+ \$$r.travel_support_budget
+ $else:
+ -
+ </td>
+
+ <td class="score_column">
+ $if r.grant_amount:
+ \$$r.grant_amount
+ $else:
+ -
+ </td>
+
+ <td class="score_column">
+ $if not r.referee_rating:
+ -
+ $else:
+ <a title="Referred by $r.referee_name">$:show_stars(r.referee_rating, color='green')</a>
+
+ <td class="score_column">
+ $if r.commenters:
+ <a title="Commented by $r.commenters"><img src="/img/comments.png"/> ($r.number_comments)</a>
+ $else:
+ -
+ </td>
+
+ <td class="score_column">
+ $if r.calculated_vote:
+ <a title="$r.calculated_vote / 5: rated by $r.voters">$:show_stars(r.calculated_vote) (out of $r.number_votes)</a>
+ $else:
+ -
+ </td>
+ <td class="score_column">
+ $if r.status == 'admitted':
+ <a title="Admitted by $r.decided_by_nickname"><img src="/img/bullet_add.png"/></a>
+ $elif r.status == 'rejected':
+ <a title="Rejected by $r.decided_by_nickname"><img src="/img/bullet_delete.png"/></a>
+ $else:
+ -
+ </td>
+
+ $#<td class="update_time">$how_long(r.creation_ts)</td>
+ </tr>
+ </table>
+ </div>
+
+ <div class="actionbar">
+ $:show_actions()
+
+ $if pager and pager.max_results:
+ <span class="paging">
+ $:show_paging()
+ </span>
+ </div>
+</form>
44 app/views/application_form.html
@@ -0,0 +1,44 @@
+$def with (form, success='')
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+<title>Machine Learning Summer School at Cambridge - Application</title>
+<link href="/css/application_forms.css" rel="stylesheet" type="text/css" />
+<link rel="icon" type="image/png" href="/img/favicon.ico" />
+</head>
+
+<body>
+
+<div class="submitted">
+$if success == 'False':
+ <p>Some <strong>error occured</strong>, <a href="/submit_application">go back</a> and try again. If the problem persists <a href="mailto:ak469@cam.ac.uk?subject=Report an error [MLSS website]">send us an email</a>. Thank you.</p>
+$elif success == 'True':
+ <p>Thank you for <strong>submitting your application</strong>. You should receive a confirmation email very soon.</p>
+ <p><a href="http://mlg.eng.cam.ac.uk/mlss09/index.html">Back to the Machine Learning Summer School website</a></p>
+</div>
+
+$if not success:
+ <form method="post" enctype="multipart/form-data">
+ <p>Welcome to the <strong>Machine Learning Summer School</strong> at Cambridge University. Please fill out the form below to submit your application. All fields are required unless explicitly specified.</p>
+ <hr/>
+ $:render_form(form, 'first_name', 'resume')
+ <hr/>
+ $:render_form(form, 'pascal_member', 'cover_letter')
+ <hr/>
+ <p>Your referee will receive an email asking him or her to submit a letter of reference.</p>
+ $:render_form(form, 'referee_name', 'referee_affiliation')
+ <hr/>
+ <p>We have a limited number of <strong>scholarships</strong> to cover travel and/or registration expenses. These will be awarded preferably to accepted applicants who would not be otherwise able to attend (e.g. from developing countries, or departments with limited travel budgets). Acceptance to the summer school will be judged independently of financial need. If you wish to apply for financial support, please tick the checkbox and provide the following additional information.</p>
+ <ol>
+ <li>Your <strong>referee must provide a paragraph</strong> in their letter of recommendation justifying the need for financial support.</li>
+
+ <li>Please give an <strong>estimate of the amount required</strong> (fees + travel expenses - amount available from other sources) below.</li>
+ </ol>
+
+ $:render_form(form, 'travel_support', 'submit')
+ </form>
+
+</body>
+</html>
34 app/views/application_form_simple.html
@@ -0,0 +1,34 @@
+$def with (form, success='')
+