Initial import

Initial import after cleanup.
Previous history lives in https://code.launchpad.net/~ttx/+junk/odsreg
This commit is contained in:
Thierry Carrez 2012-12-20 16:11:01 +01:00
commit 56fdefeccd
49 changed files with 2504 additions and 0 deletions

176
LICENSE Normal file
View File

@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.

40
README.rst Normal file
View File

@ -0,0 +1,40 @@
odsreg - The OpenStack Design Summit session management system
==============================================================
odsreg is the Django app used for the OpenStack Design Summit
session proposal and scheduling.
It has the following features:
* Session proposal
* Session review
* Ability to merge sessions and add a cover description
* Drag-and-drop scheduling
* Synchronization to sched.org event schedule
* Launchpad SSO integration
Prerequisites
-------------
You'll need the following Python modules installed:
- django (1.4+)
- python-django-auth-openid
Configuration and Usage
-----------------------
Copy local_settings.py.sample to local_settings.py and change
settings there.
Create empty database:
./manage.py syncdb
Copy slots.json.sample to slots.json and edit the file to match
the topics, rooms and time slots for each topic. Then run:
./manage.py loadtopics slots.json
Then run a dev server using:
./manage.py runserver

14
__init__.py Normal file
View File

@ -0,0 +1,14 @@
# Copyright 2011 Thierry Carrez <thierry@openstack.org>
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

14
cfp/__init__.py Normal file
View File

@ -0,0 +1,14 @@
# Copyright 2011 Thierry Carrez <thierry@openstack.org>
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

21
cfp/admin.py Normal file
View File

@ -0,0 +1,21 @@
# Copyright 2011 Thierry Carrez <thierry@openstack.org>
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from odsreg.cfp.models import Topic, Proposal
from django.contrib import admin
admin.site.register(Topic)
admin.site.register(Proposal)

117
cfp/models.py Normal file
View File

@ -0,0 +1,117 @@
# Copyright 2011 Thierry Carrez <thierry@openstack.org>
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import urllib
from django.db import models
from django.forms import ModelForm
from django.core.exceptions import ValidationError
from django.contrib.auth.models import User
def is_valid_lp_name(value):
return value.replace('-', '').isalnum()
def validate_bp(value):
bps = value.split()
for bp in bps:
members = bp.split("/")
if len(members) != 2:
raise ValidationError(u'Blueprints should be specified under'
' the form project/blueprint-name')
(project, bpname) = list(members)
if not is_valid_lp_name(project):
raise ValidationError(u'Incorrect project name: %s' % project)
if not is_valid_lp_name(bpname):
raise ValidationError(u'Incorrect blueprint name: %s' % bpname)
f = urllib.urlopen("https://api.launchpad.net/devel/%s/+spec/%s"
% (project, bpname))
f.close()
if f.getcode() != 200:
raise ValidationError(u'No such blueprint: %s/%s'
' -- did you create it on Launchpad ?'
% (project, bpname))
class Topic(models.Model):
name = models.CharField(max_length=40)
lead_username = models.CharField(max_length=40)
description = models.TextField(blank=True)
def __unicode__(self):
return self.name
class Proposal(models.Model):
STATUSES = (
('U', 'Unreviewed'),
('I', 'Incomplete'),
('A', 'Preapproved'),
('R', 'Refused'),
)
proposer = models.ForeignKey(User)
title = models.CharField(max_length=50,
help_text="The title of your proposed session. This is mandatory.")
description = models.TextField(
help_text="The detailed subject and goals for your proposed session. "
"This is mandatory.")
topic = models.ForeignKey(Topic,
help_text="The topic the session belongs in. Click 'Help' below"
" for more details. This is mandatory.")
blueprints = models.CharField(max_length=400, blank=True,
validators=[validate_bp],
help_text="Links to Launchpad blueprints. "
"For example 'nova/accounting' would link to a nova "
"blueprint called 'accounting'. You can specify multiple "
"links, separated by spaces. This field is optional.")
status = models.CharField(max_length=1, choices=STATUSES)
proposer_notes = models.TextField(blank=True,
help_text="Notes from the proposer to the evaluation committee. "
"Those notes will not appear in the public description. "
"This field is optional.")
reviewer_notes = models.TextField(blank=True)
scheduled = models.BooleanField(default=False)
last_modified = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-last_modified']
def __unicode__(self):
return self.title
class ProposalForm(ModelForm):
class Meta:
model = Proposal
exclude = ('proposer', 'reviewer_notes', 'status', 'scheduled')
class ProposalEditForm(ModelForm):
class Meta:
model = Proposal
exclude = ('topic', 'proposer', 'reviewer_notes', 'status',
'scheduled')
class ProposalReviewForm(ModelForm):
class Meta:
model = Proposal
fields = ('status', 'reviewer_notes')
class ProposalSwitchForm(ModelForm):
class Meta:
model = Proposal
fields = ('topic',)

BIN
cfp/static/arrowBlank Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 B

BIN
cfp/static/arrowDown Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

BIN
cfp/static/arrowUp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 B

BIN
cfp/static/close.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 B

BIN
cfp/static/edit.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 B

BIN
cfp/static/loop.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 B

636
cfp/static/odsreg.css Normal file
View File

@ -0,0 +1,636 @@
@font-face {
font-family: 'PT Sans';
font-style: normal;
font-weight: normal;
src: local('PT Sans'), local('PTSans-Regular'), url('http://themes.googleusercontent.com/static/fonts/ptsans/v1/LKf8nhXsWg5ybwEGXk8UBQ.woff') format('woff');
}
/* reset.css */
html, body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, code, del, dfn, em, img, q, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, dialog, figure, footer, header, hgroup, nav, section {margin:0;padding:0;border:0;font-weight:inherit;font-style:inherit;font-size:100%;font-family:inherit;vertical-align:baseline;}
article, aside, dialog, figure, footer, header, hgroup, nav, section {display:block;}
body {line-height:1.5;}
table {border-collapse:separate;border-spacing:0;}
caption, th, td {text-align:left;font-weight:normal;}
table, td, th {vertical-align:middle;}
blockquote:before, blockquote:after, q:before, q:after {content:"";}
blockquote, q {quotes:"" "";}
a img {border:none;}
/* typography.css */
html {font-size:100.01%;}
body {font-size:75%;color:#222;background:#fff;font-family:"Helvetica Neue", Arial, Helvetica, sans-serif;}
h1, h2, h3, h4, h5, h6 {font-weight:normal;color:#111;}
h1 {font-size:3em;line-height:1;margin-bottom:0.5em;}
h2 {font-size:2em;margin-bottom:0.75em;}
h3 {font-size:1.5em;line-height:1;margin-bottom:1em;}
h4 {font-size:1.2em;line-height:1.25;margin-bottom:1.25em;}
h5 {font-size:1em;font-weight:bold;margin-bottom:1.5em;}
h6 {font-size:1em;font-weight:bold;}
h1 img, h2 img, h3 img, h4 img, h5 img, h6 img {margin:0;}
p {margin:0 0 1.5em;}
p img.left {float:left;margin:1.5em 1.5em 1.5em 0;padding:0;}
p img.right {float:right;margin:1.5em 0 1.5em 1.5em;}
a:focus, a:hover {color:#000;}
a {color:#009;text-decoration:underline;}
blockquote {margin:1.5em;color:#666;font-style:italic;}
strong {font-weight:bold;}
em, dfn {font-style:italic;}
dfn {font-weight:bold;}
sup, sub {line-height:0;}
abbr, acronym {border-bottom:1px dotted #666;}
address {margin:0 0 1.5em;font-style:italic;}
del {color:#666;}
pre {margin:1.5em 0;white-space:pre-wrap;}
pre, code, tt {font:1em 'andale mono', 'lucida console', monospace;line-height:1.5;}
li ul, li ol {margin:0;}
ul, ol {margin:0 1.5em 1.5em 0;padding-left:3.333em;}
ul {list-style-type:disc;}
ol {list-style-type:decimal;}
dl {margin:0 0 1.5em 0;}
dl dt {font-weight:bold;}
dd {margin-left:1.5em;}
table {margin-bottom:1.4em;width:100%;}
th {font-weight:bold;}
thead th {background:#c3d9ff;}
th, caption {padding:4px 10px 4px 5px;}
td {padding:4px 10px 0px 5px;}
tr.even td {background:#e5ecf9;}
tfoot {font-style:italic;}
caption {background:#eee;}
.small {font-weight:normal;font-size:.8em;margin-bottom:1.875em;line-height:1.875em;}
.large {font-size:1.2em;line-height:2.5em;margin-bottom:1.25em;}
.hide {display:none;}
.quiet {color:#666;}
.loud {color:#000;}
.highlight {background:#ff0;}
.added {background:#060;color:#fff;}
.removed {background:#900;color:#fff;}
.first {margin-left:0;padding-left:0;}
.last {margin-right:0;padding-right:0;}
.top {margin-top:0;padding-top:0;}
.bottom {margin-bottom:0;padding-bottom:0;}
/* forms.css */
label {font-weight:bold;float:left;padding-top:8px;padding-right:3px;}
fieldset {padding:1.4em;margin:0 0 1.5em 0;border:1px solid #ccc;}
legend {font-weight:bold;font-size:1.2em;}
input[type=text], input[type=password], input.text, input.title, textarea, select {background-color:#fff;border:1px solid #bbb;}
input[type=text]:focus, input[type=password]:focus, input.text:focus, input.title:focus, textarea:focus, select:focus {border-color:#666;}
input[type=text], input[type=password], input.text, input.title, textarea, select {margin:0.5em 0;}
input[type=text] {width:500px;}
input.title {font-size:1.5em;}
textarea {width:500px;height:150px;padding:5px;}
input[type=checkbox], input[type=radio], input.checkbox, input.radio {position:relative;top:.25em;}
form.inline {line-height:3;}
form.inline p {margin-bottom:0;}
.error, .notice, .success {padding:.6em;margin-bottom:0em;border:2px solid #ddd;}
.error {background:#FBE3E4;color:#8a1f11;border-color:#FBC2C4;}
.notice {background:#FFF6BF;color:#514721;border-color:#FFD324;}
.success {background:#E6EFC2;color:#264409;border-color:#C6D880;}
.error a {color:#8a1f11;}
.notice a {color:#514721;}
.success a {color:#264409;}
/* grid.css */
.container {margin-left:20px; margin-right:20px;}
.showgrid {background:url(src/grid.png);}
.column, .span-1, .span-2, .span-3, .span-4, .span-5, .span-6, .span-7, .span-8, .span-9, .span-10, .span-11, .span-12, .span-13, .span-14, .span-15, .span-16, .span-17, .span-18, .span-19, .span-20, .span-21, .span-22, .span-23, .span-24 {float:left;margin-right:10px;}
.last {margin-right:0;}
.span-1 {width:30px;}
.span-2 {width:70px;}
.span-3 {width:110px;}
.span-4 {width:150px;}
.span-5 {width:190px;}
.span-6 {width:230px;}
.span-7 {width:270px;}
.span-8 {width:310px;}
.span-9 {width:350px;}
.span-10 {width:390px;}
.span-11 {width:430px;}
.span-12 {width:470px;}
.span-13 {width:510px;}
.span-14 {width:550px;}
.span-15 {width:590px;}
.span-16 {width:630px;}
.span-17 {width:670px;}
.span-18 {width:710px;}
.span-19 {width:750px;}
.span-20 {width:790px;}
.span-21 {width:830px;}
.span-22 {width:870px;}
.span-23 {width:910px;}
.span-24 {width:950px;margin-right:0;}
input.span-1, textarea.span-1, input.span-2, textarea.span-2, input.span-3, textarea.span-3, input.span-4, textarea.span-4, input.span-5, textarea.span-5, input.span-6, textarea.span-6, input.span-7, textarea.span-7, input.span-8, textarea.span-8, input.span-9, textarea.span-9, input.span-10, textarea.span-10, input.span-11, textarea.span-11, input.span-12, textarea.span-12, input.span-13, textarea.span-13, input.span-14, textarea.span-14, input.span-15, textarea.span-15, input.span-16, textarea.span-16, input.span-17, textarea.span-17, input.span-18, textarea.span-18, input.span-19, textarea.span-19, input.span-20, textarea.span-20, input.span-21, textarea.span-21, input.span-22, textarea.span-22, input.span-23, textarea.span-23, input.span-24, textarea.span-24 {border-left-width:1px;border-right-width:1px;padding-left:5px;padding-right:5px;}
input.span-1, textarea.span-1 {width:18px;}
input.span-2, textarea.span-2 {width:58px;}
input.span-3, textarea.span-3 {width:98px;}
input.span-4, textarea.span-4 {width:138px;}
input.span-5, textarea.span-5 {width:178px;}
input.span-6, textarea.span-6 {width:218px;}
input.span-7, textarea.span-7 {width:258px;}
input.span-8, textarea.span-8 {width:298px;}
input.span-9, textarea.span-9 {width:338px;}
input.span-10, textarea.span-10 {width:378px;}
input.span-11, textarea.span-11 {width:418px;}
input.span-12, textarea.span-12 {width:458px;}
input.span-13, textarea.span-13 {width:498px;}
input.span-14, textarea.span-14 {width:538px;}
input.span-15, textarea.span-15 {width:578px;}
input.span-16, textarea.span-16 {width:618px;}
input.span-17, textarea.span-17 {width:658px;}
input.span-18, textarea.span-18 {width:698px;}
input.span-19, textarea.span-19 {width:738px;}
input.span-20, textarea.span-20 {width:778px;}
input.span-21, textarea.span-21 {width:818px;}
input.span-22, textarea.span-22 {width:858px;}
input.span-23, textarea.span-23 {width:898px;}
input.span-24, textarea.span-24 {width:938px;}
.append-1 {padding-right:40px;}
.append-2 {padding-right:80px;}
.append-3 {padding-right:120px;}
.append-4 {padding-right:160px;}
.append-5 {padding-right:200px;}
.append-6 {padding-right:240px;}
.append-7 {padding-right:280px;}
.append-8 {padding-right:320px;}
.append-9 {padding-right:360px;}
.append-10 {padding-right:400px;}
.append-11 {padding-right:440px;}
.append-12 {padding-right:480px;}
.append-13 {padding-right:520px;}
.append-14 {padding-right:560px;}
.append-15 {padding-right:600px;}
.append-16 {padding-right:640px;}
.append-17 {padding-right:680px;}
.append-18 {padding-right:720px;}
.append-19 {padding-right:760px;}
.append-20 {padding-right:800px;}
.append-21 {padding-right:840px;}
.append-22 {padding-right:880px;}
.append-23 {padding-right:920px;}
.prepend-1 {padding-left:40px;}
.prepend-2 {padding-left:80px;}
.prepend-3 {padding-left:120px;}
.prepend-4 {padding-left:160px;}
.prepend-5 {padding-left:200px;}
.prepend-6 {padding-left:240px;}
.prepend-7 {padding-left:280px;}
.prepend-8 {padding-left:320px;}
.prepend-9 {padding-left:360px;}
.prepend-10 {padding-left:400px;}
.prepend-11 {padding-left:440px;}
.prepend-12 {padding-left:480px;}
.prepend-13 {padding-left:520px;}
.prepend-14 {padding-left:560px;}
.prepend-15 {padding-left:600px;}
.prepend-16 {padding-left:640px;}
.prepend-17 {padding-left:680px;}
.prepend-18 {padding-left:720px;}
.prepend-19 {padding-left:760px;}
.prepend-20 {padding-left:800px;}
.prepend-21 {padding-left:840px;}
.prepend-22 {padding-left:880px;}
.prepend-23 {padding-left:920px;}
.border {padding-right:4px;margin-right:5px;border-right:1px solid #eee;}
.colborder {padding-right:24px;margin-right:25px;border-right:1px solid #eee;}
.pull-1 {margin-left:-40px;}
.pull-2 {margin-left:-80px;}
.pull-3 {margin-left:-120px;}
.pull-4 {margin-left:-160px;}
.pull-5 {margin-left:-200px;}
.pull-6 {margin-left:-240px;}
.pull-7 {margin-left:-280px;}
.pull-8 {margin-left:-320px;}
.pull-9 {margin-left:-360px;}
.pull-10 {margin-left:-400px;}
.pull-11 {margin-left:-440px;}
.pull-12 {margin-left:-480px;}
.pull-13 {margin-left:-520px;}
.pull-14 {margin-left:-560px;}
.pull-15 {margin-left:-600px;}
.pull-16 {margin-left:-640px;}
.pull-17 {margin-left:-680px;}
.pull-18 {margin-left:-720px;}
.pull-19 {margin-left:-760px;}
.pull-20 {margin-left:-800px;}
.pull-21 {margin-left:-840px;}
.pull-22 {margin-left:-880px;}
.pull-23 {margin-left:-920px;}
.pull-24 {margin-left:-960px;}
.pull-1, .pull-2, .pull-3, .pull-4, .pull-5, .pull-6, .pull-7, .pull-8, .pull-9, .pull-10, .pull-11, .pull-12, .pull-13, .pull-14, .pull-15, .pull-16, .pull-17, .pull-18, .pull-19, .pull-20, .pull-21, .pull-22, .pull-23, .pull-24 {float:left;position:relative;}
.push-1 {margin:0 -40px 1.5em 40px;}
.push-2 {margin:0 -80px 1.5em 80px;}
.push-3 {margin:0 -120px 1.5em 120px;}
.push-4 {margin:0 -160px 1.5em 160px;}
.push-5 {margin:0 -200px 1.5em 200px;}
.push-6 {margin:0 -240px 1.5em 240px;}
.push-7 {margin:0 -280px 1.5em 280px;}
.push-8 {margin:0 -320px 1.5em 320px;}
.push-9 {margin:0 -360px 1.5em 360px;}
.push-10 {margin:0 -400px 1.5em 400px;}
.push-11 {margin:0 -440px 1.5em 440px;}
.push-12 {margin:0 -480px 1.5em 480px;}
.push-13 {margin:0 -520px 1.5em 520px;}
.push-14 {margin:0 -560px 1.5em 560px;}
.push-15 {margin:0 -600px 1.5em 600px;}
.push-16 {margin:0 -640px 1.5em 640px;}
.push-17 {margin:0 -680px 1.5em 680px;}
.push-18 {margin:0 -720px 1.5em 720px;}
.push-19 {margin:0 -760px 1.5em 760px;}
.push-20 {margin:0 -800px 1.5em 800px;}
.push-21 {margin:0 -840px 1.5em 840px;}
.push-22 {margin:0 -880px 1.5em 880px;}
.push-23 {margin:0 -920px 1.5em 920px;}
.push-24 {margin:0 -960px 1.5em 960px;}
.push-1, .push-2, .push-3, .push-4, .push-5, .push-6, .push-7, .push-8, .push-9, .push-10, .push-11, .push-12, .push-13, .push-14, .push-15, .push-16, .push-17, .push-18, .push-19, .push-20, .push-21, .push-22, .push-23, .push-24 {float:right;position:relative;}
.prepend-top {margin-top:1.5em;}
.append-bottom {margin-bottom:1.5em;}
.box {padding:1.5em;margin-bottom:1.5em;background:#E5ECF9;}
hr {background:#ddd;color:#ddd;clear:both;float:none;width:100%;height:.1em;margin:0 0 1.45em;border:none;}
hr.space {background:#fff;color:#fff;visibility:hidden;}
.clearfix:after, .container:after {content:"\0020";display:block;height:0;clear:both;visibility:hidden;overflow:hidden;}
.clearfix, .container {display:block;}
.clear {clear:both;}
html { overflow-y: scroll; }
body {
background: white url(openstack-page-bkg.jpg) no-repeat scroll 0px 0px;
border-top: 3px solid #255e6e;
color: #535353;
}
h3 {
margin-top: 10px;
margin-bottom: 0px;
}
h4 {
margin-top: 25px;
margin-bottom: 8px;
}
p {
margin-top: 0px;
margin-bottom: 5px;
}
p.last {
margin-bottom: 20px;
}
a, a:visited {
color: #bc1518;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
h1, h2, h3, h4 {
font-family: 'PT Sans', serif;
font-style: normal;
letter-spacing: -0.076em;
line-height: 1em;
color: #264d69;
}
h1 {
font-size: 28px;
margin-top:15px;
margin-bottom:5px;
}
h2 {
font-size: 20px;
margin-top:5px;
}
h3 {
font-size: 14px;
margin-top:0px;
}
#visualization {
margin-top: 10px;
float: right;
width: 540px;
height: 70px;
}
table, th {
border-collapse: collapse;
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
clear: both;
}
th {
background-color: #eee;
}
input.timeslot { width:40px; margin-right:5px; }
a.nolink { color: #535353; text-decoration: none;}
td {
border-top: 1px dotted #ddd;
}
th.right, td.right {
text-align: right;
}
#logo a {
display: block;
margin-top: 8px;
margin-bottom: 20px;
text-indent: -1000em;
background: url(openstack-logo.png) no-repeat left center;
height: 54px;
width: 177px;
margin-left: -10px;
}
#header {
margin-bottom: 12px;
margin-top: 20px;
}
.sortkey,.revsortkey{display:none}
/* @group Buttons */
a.button, input.action {
font-family: 'PT Sans', serif;
border: 1px solid #ccc;
padding: 3px 30px;
color: #525252;
text-decoration: none;
font-size: 14px;
line-height: 3em;
background: #ddd;
box-shadow: 1px 1px 2px rgba(0,0,0,.5);
-webkit-box-shadow: 1px 1px 2px rgba(0,0,0,.5);
-moz-box-shadow: 1px 1px 2px rgba(0,0,0,.5);
text-shadow: #fff 0px 1px 1px;
background: -webkit-gradient(linear, left top, left bottom, from(#eeeeee), to(#bebebe));
background: -moz-linear-gradient(top, #eeeeee, #bebebe);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#bebebe');
}
a.button:hover, input.action:hover {
color: black;
-webkit-transition:color 1s ease-out;
}
a.button:active, input.action:active {
background: #ababab;
box-shadow: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
border-color: #ababab;
border-top-color: #636363;
background: -webkit-gradient(linear, left top, left bottom, from(#bebebe), to(#dddddd));
background: -moz-linear-gradient(top, #bebebe, #eeeeee);
-webkit-transition:none;
padding: 4px 30px 3px 31px;
}
/* @end */
/* @group Rounded Buttons */
a.roundedButton, input.roundedButton {
font-family: 'PT Sans', serif;
border: 1px solid #e2e2e2;
padding: 4px 15px;
color: black;
text-decoration: none;
font-size: 12.5px;
text-transform: ;
line-height: 3em;
background: #FFFFFF; /* old browsers */
background: -moz-linear-gradient(top, #FFFFFF 0%, #F3F3F3 50%, #EBEBEB 100%); /* firefox */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFFFFF), color-stop(50%,#F3F3F3), color-stop(100%,#EBEBEB)); /* webkit */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#FFFFFF', endColorstr='#EBEBEB',GradientType=0 ); /* ie */
box-shadow: 0px 1px 1px rgba(0,0,0,.5);
-webkit-box-shadow: 0px 1px 2px rgba(0,0,0,.2);
-moz-box-shadow: 0px 1px 2px rgba(0,0,0,.2);
text-shadow: #fff 0px 1px 1px;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-bottom-color: #a0a0a0;
border-right-color: #bababa;
border-left-color: #bababa;
}
a.roundedButton:hover, input.action:hover {
color: black;
-webkit-transition:color 1s ease-out;
}
a.roundedButton:active, input.action:active {
background: #ababab;
box-shadow: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
border-color: #ababab;
border-top-color: #636363;
background: -webkit-gradient(linear, left top, left bottom, from(#bebebe), to(#dddddd));
background: -moz-linear-gradient(top, #bebebe, #eeeeee);
-webkit-transition:none;
padding: 4px 15px;
}
/* @end */
.subhead {
color: #cf2f19;
font-size: 16px;
border-bottom: 1px dotted;
padding-bottom: 5px;
border-color: #c5e2ea;
margin-bottom: 20px;
}
#footer {
margin-top: 15px;
}
#schedulegrid {
margin-right: 10px;
float: left;
}
#scheduleform {
float:left;
}
#schedulegrid table {
border-left: 1px solid #ddd;
border-right: 1px solid #ddd;
}
#schedulegrid tr, #schedulegrid td {
border-left: 1px dotted #ddd;
}
#schedulegrid td.farright {
/* background: #ff5555;*/
}
#schedulegrid span.pos {
position: relative;
}
#schedulegrid td span.tip {
display: none; /* so is this */
}
#schedulegrid td:hover span.tip {
display: block;
z-index: 100;
position: absolute;
top: 1.6em;
left: 0;
width: auto;
padding: 3px 7px 4px 6px;
border: 1px solid #336;
background-color: #f7f7ee;
font: normal 0.9em/1.2em arial, helvetica, sans-serif;
text-align: left;
color: #000;
}
#overlay {
position: fixed; top: 0; left: 0;
width: 100%; height: 100%;
z-index: 10;
visibility: hidden;
}
#greyoverlay {
background-color: #000;
opacity: .7;
filter: alpha(opacity=70);
position: fixed; top: 0; left: 0;
width: 100%; height: 100%;
z-index: 10;
text-align:center;
}
#helppage {
background-color: #fff;
border-width: 5px;
border-color: #000;
border-radius: 20px;
padding: 20px;
padding-top: 10px;
padding-bottom: 15px;
position: fixed; top: 20%; left: 20%;
width: 60%;
opacity: 1.0;
filter: alpha(opacity=100);
z-index: 11;
}
/* @group Tables */
.tabContent table {
margin: 20px;
width: 670px;
}
.tabContent table td {
border-bottom: 1px solid #d8d8d8;
vertical-align: top;
padding: 10px 10px 20px 0;
}
.tabContent table td p {
margin: 0px;
}
.tabContent table tr:last-child td {
border-bottom: none;
}
.tabContent table th {
font-family: 'PT Sans', serif;
font-style: normal;
font-weight: normal;
font-size: 18px;
letter-spacing: -0.076em;
line-height: 1em;
color: #264d69;
padding-left: 0px;
}
.tabContent table a:active, .tabContent table a:visited, .tabContent table a {
color: inherit;
text-decoration: underline;
}
p.fnote {
margin-left: 20px;
}
.tip {
font-size: 9px;
padding-bottom: 20px;
}
.sessionlistitem {
margin-top: 20px;
margin-right: 20px;
}
.sessionlist {
margin-right: 20px;
float: left;
}
.sessionlist a {
text-decoration: none;
color: #000;
background: #eee;
padding: 5px;
border: 3px dashed #999;
}
.schedulelistitem {
margin-top: 10px;
margin-right: 20px;
text-decoration: none;
color: #000;
background: #eee;
padding: 5px;
border: 3px solid #000;
}
.schedulelist {
float: left;
}
.over {
border: 3px dotted #000;
}
*[draggable=true] {
-moz-user-select:none;
-khtml-user-drag: element;
cursor: move;
}
*:-khtml-drag {
background-color: rgba(238,238,238, 0.5);
}
/* @end */

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

45
cfp/static/sorting.js Normal file
View File

@ -0,0 +1,45 @@
// sorttable/sorttable-min.js
var SORT_COLUMN_INDEX;var arrowUp="/media/arrowUp";var arrowDown="/media/arrowDown";var arrowBlank="/media/arrowBlank";function trim(str){return str.replace(/^\s*|\s*$/g,"");}
function sortables_init(){if(!document.getElementsByTagName)return;tbls=document.getElementsByTagName("table");for(ti=0;ti<tbls.length;ti++){thisTbl=tbls[ti];if(((' '+thisTbl.className+' ').indexOf(" sortable ")!=-1)&&(thisTbl.id)){ts_makeSortable(thisTbl);}}}
function ts_makeSortable(table){if(table.tHead&&table.tHead.rows&&table.tHead.rows.length>0){var firstRow=table.tHead.rows[0];}else if(table.rows&&table.rows.length>0){var firstRow=table.rows[0];}
if(!firstRow)return;for(var i=0;i<firstRow.cells.length;i++){var cell=firstRow.cells[i];var txt=ts_getInnerText(cell);cell.innerHTML='<a href="#" class="sortheader" onclick="ts_resortTable(this); return false;">'
+txt+'<img class="sortarrow" src="'+arrowBlank+'" height="6" width="9"></a>';}
for(var i=0;i<firstRow.cells.length;i++){var cell=firstRow.cells[i];var lnk=ts_firstChildByName(cell,'A');var img=ts_firstChildByName(lnk,'IMG')
if((' '+cell.className+' ').indexOf(" default-sort ")!=-1){ts_arrowDown(img);}
if((' '+cell.className+' ').indexOf(" default-revsort ")!=-1){ts_arrowUp(img);}
if((' '+cell.className+' ').indexOf(" initial-sort ")!=-1){ts_resortTable(lnk);}}}
function ts_getInnerText(el){if(typeof el=="string")return el;if(typeof el=="undefined"){return el};/*if(el.innerText)return el.innerText*/;var str="";var cs=el.childNodes;var l=cs.length;for(var i=0;i<l;i++){node=cs[i];switch(node.nodeType){case 1:if(node.className=="sortkey"){return ts_getInnerText(node);}else if(node.className=="revsortkey"){return"-"+ts_getInnerText(node);}else{str+=ts_getInnerText(node);break;}
case 3:str+=node.nodeValue;break;}}
return str;}
function ts_firstChildByName(el,name){for(var ci=0;ci<el.childNodes.length;ci++){if(el.childNodes[ci].tagName&&el.childNodes[ci].tagName.toLowerCase()==name.toLowerCase())
return el.childNodes[ci];}}
function ts_arrowUp(img){img.setAttribute('sortdir','up');img.src=arrowUp;}
function ts_arrowDown(img){img.setAttribute('sortdir','down');img.src=arrowDown;}
function ts_resortTable(lnk){var img=ts_firstChildByName(lnk,'IMG')
var td=lnk.parentNode;var column=td.cellIndex;var table=getParent(td,'TABLE');if(table.rows.length<=1)return;SORT_COLUMN_INDEX=column;while(td.previousSibling!=null){td=td.previousSibling;if(td.nodeType!=1){continue}
colspan=td.getAttribute("colspan");if(colspan){SORT_COLUMN_INDEX+=parseInt(colspan)-1;}}
var itm=ts_getInnerText(table.rows[1].cells[SORT_COLUMN_INDEX]);itm=trim(itm);sortfn=ts_sort_caseinsensitive;if(itm.match(/^\d\d[\/-]\d\d[\/-]\d\d\d\d$/))sortfn=ts_sort_date;if(itm.match(/^\d\d[\/-]\d\d[\/-]\d\d$/))sortfn=ts_sort_date;if(itm.match(/^[£$]/))sortfn=ts_sort_currency;if(itm.match(/^-?[\d\.]+$/))sortfn=ts_sort_numeric;var firstRow=new Array();var newRows=new Array();for(i=0;i<table.rows[0].length;i++){firstRow[i]=table.rows[0][i];}
for(j=1;j<table.rows.length;j++){newRows[j-1]=table.rows[j];newRows[j-1].oldPosition=j-1;}
newRows.sort(ts_stableSort(sortfn));if(img.getAttribute("sortdir")=='down'){newRows.reverse();ts_arrowUp(img);}else{ts_arrowDown(img);}
for(i=0;i<newRows.length;i++){if(!newRows[i].className||(newRows[i].className&&(newRows[i].className.indexOf('sortbottom')==-1)))
table.tBodies[0].appendChild(newRows[i]);}
for(i=0;i<newRows.length;i++){if(newRows[i].className&&(newRows[i].className.indexOf('sortbottom')!=-1))
table.tBodies[0].appendChild(newRows[i]);}
var allimgs=document.getElementsByTagName("img");for(var ci=0;ci<allimgs.length;ci++){var one_img=allimgs[ci];if(one_img!=img&&one_img.className=='sortarrow'&&getParent(one_img,"table")==getParent(lnk,"table")){one_img.src=arrowBlank;one_img.setAttribute('sortdir','');}}}
function getParent(el,pTagName){if(el==null)
return null;else if(el.nodeType==1&&el.tagName.toLowerCase()==pTagName.toLowerCase())
return el;else
return getParent(el.parentNode,pTagName);}
function ts_stableSort(sortfn){function stableSort(a,b){var cmp=sortfn(a,b);if(cmp!=0){return cmp;}else{return a.oldPosition-b.oldPosition;}}
return stableSort;}
function ts_sort_date(a,b){aa=trim(ts_getInnerText(a.cells[SORT_COLUMN_INDEX]));bb=trim(ts_getInnerText(b.cells[SORT_COLUMN_INDEX]));if(aa.length==10){dt1=aa.substr(6,4)+aa.substr(3,2)+aa.substr(0,2);}else{yr=aa.substr(6,2);if(parseInt(yr)<50){yr='20'+yr;}else{yr='19'+yr;}
dt1=yr+aa.substr(3,2)+aa.substr(0,2);}
if(bb.length==10){dt2=bb.substr(6,4)+bb.substr(3,2)+bb.substr(0,2);}else{yr=bb.substr(6,2);if(parseInt(yr)<50){yr='20'+yr;}else{yr='19'+yr;}
dt2=yr+bb.substr(3,2)+bb.substr(0,2);}
if(dt1==dt2)return 0;if(dt1<dt2)return-1;return 1;}
function ts_sort_currency(a,b){aa=ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g,'');bb=ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g,'');return parseFloat(aa)-parseFloat(bb);}
function ts_sort_numeric(a,b){aa=parseFloat(ts_getInnerText(a.cells[SORT_COLUMN_INDEX]));if(isNaN(aa))aa=0;bb=parseFloat(ts_getInnerText(b.cells[SORT_COLUMN_INDEX]));if(isNaN(bb))bb=0;return aa-bb;}
function ts_sort_caseinsensitive(a,b){aa=ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).toLowerCase();bb=ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).toLowerCase();if(aa==bb)return 0;if(aa<bb)return-1;return 1;}
function ts_sort_default(a,b){aa=ts_getInnerText(a.cells[SORT_COLUMN_INDEX]);bb=ts_getInnerText(b.cells[SORT_COLUMN_INDEX]);if(aa==bb)return 0;if(aa<bb)return-1;return 1;}
function addEvent(elm,evType,fn,useCapture){if(elm.addEventListener){elm.addEventListener(evType,fn,useCapture);return true;}else if(elm.attachEvent){var r=elm.attachEvent("on"+evType,fn);return r;}else{alert("Handler could not be removed");}}

5
cfp/templates/404.html Normal file
View File

@ -0,0 +1,5 @@
{% extends "base.html" %}
{% block content %}
<h2>Error: Page not found</h2>
<p>This page does not exist.</p>
{% endblock %}

5
cfp/templates/500.html Normal file
View File

@ -0,0 +1,5 @@
{% extends "base.html" %}
{% block content %}
<h2>Error: Application error</h2>
<p>This application encoutered an error.</p>
{% endblock %}

52
cfp/templates/base.html Normal file
View File

@ -0,0 +1,52 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>OpenStack Grizzly Design Summit</title>
<link href="/media/odsreg.css" rel="stylesheet" type="text/css">
</head>
<body>
<script>
function overlay() {
el = document.getElementById("overlay");
el.style.visibility = (el.style.visibility == "visible") ? "hidden" : "visible";
}
</script>
<div class="container">
<div id="header">
<div class="span-5">
<h1 id="logo"><a href="/">OpenStack</a></h1>
</div>
<h1>Sessions for the Grizzly Design Summit</h1>
<h3>OpenStack Conference, San Diego, Oct 15-18, 2012</h3>
</div>
</div>
<div class="container">
{% block content %}
{% endblock %}
</div>
<div id="overlay">
<div id="greyoverlay"></div>
<div id="helppage">
<h2>Help</h2>
{% block helppage %}<p>Sorry, no help is provided for this screen.</p>{% endblock %}
<a class=roundedButton href='#' onclick='overlay()'>Close</A>
</div>
</div>
<div class="container">
<div id="footer">
<hr>
Need <a href='#' onclick='overlay()'>Help</a>?</br>
{% block extrafooter %}{% endblock %}
{% if req.user.is_authenticated %}
You are logged in as {{ req.user.username }}. <a href=/logout>Logout</A>
{% else %}
You are not logged in. <a href="/openid/login?next={{ req.path }}">Log in</A>
{% endif %}
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,20 @@
{% extends "regform.html" %}
{% block helppage %}
<p>This page lets you suggest a session topic for the Design Summit.</p>
<p>Fill the <i>Title</i>, <i>Description</i> and <i>Topic</i> mandatory fields. You can also add links to existing Launchpad blueprints you created on the same subject, or add additional notes to further explain your idea to the PTL or lead reponsible for selecting the content.</p>
<p>You should pay special attention to the <i>Topic</i>, as you can't change it yourself later. Available topics are:</p>
{% for topic in topics %}
<p><i>{{topic.name}}</i>: {{topic.description}}</p>
{% endfor %}
<p>When you've completed the form, click on the <i>Suggest</i> button to
suggest it to the topic manager who will be reviewing the submissions.
You'll be notified of status changes on your proposal by email notification.</p>
{% endblock %}
{% block formtitle %}
<h2>Suggest a session</h2>
<form action="/cfp/create" method="post">
{% endblock %}
{% block formfooter %}
<input id="toggleButton" class="roundedButton" type="submit" value="Suggest" />
<a class=roundedButton href=/>Cancel</A>
{% endblock %}

View File

@ -0,0 +1,12 @@
{% extends "regform.html" %}
{% block helppage %}
<p>This screen lets you confirm the deletion of a proposal you created.</p>
{% endblock %}
{% block formtitle %}
<h2>Do you really want to delete session '{{proposal.title}}' ?</h2>
<form action="/cfp/delete/{{ proposal.id }}" method="post">
{% endblock %}
{% block formfooter %}
<input id="toggleButton" class="roundedButton" type="submit" value="Yes" />
<a class=roundedButton href="/{{ req.session.lastlist }}">Cancel</A>
{% endblock %}

View File

@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block helppage %}
<p>This screen lets you see the details of a proposed session.</p>
<p>Note that you can only edit sessions that you suggested yourself (or if you're the topic lead). Sessions in <i>Preapproved</i> state cannot be changed.</p>
{% endblock %}
{% block content %}
<h2>{{ proposal.title }}</h2>
<p>Proposed by <b>{{ proposal.proposer }}</b>
in topic <b>{{ proposal.topic }}</b></p>
<h4>Description</h4>
<pre>{{ proposal.description }}</pre>
{% if blueprints %}
<h4>Related blueprints</h4>
<ul>
{% for name, link in blueprints.items %}
<li><a href="{{ link }}">{{ name }}</a></li>
{% endfor %}
</ul>
{% endif %}
<h4>Status</h4>
<p>This proposal is in <b>{{ proposal.get_status_display }}</b> state.</p>
<a class=roundedButton href="/{{ req.session.lastlist }}">Back</A>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends "regform.html" %}
{% block helppage %}
<p>This screen lets you change the details of a proposed session.</p>
<p>Note that you cannot change the topic you proposed your session in. You'll have to ask the topic lead to redirect your suggestion to another topic.</p>
{% endblock %}
{% block formtitle %}
<h2>Edit proposed {{proposal.topic.name}} session</h2>
{% if proposal.reviewer_notes %}
<h4>Reviewer notes:</h4>
<p>{{ proposal.reviewer_notes }}</p>
{% endif %}
<form action="/cfp/edit/{{ proposal.id }}" method="post">
{% endblock %}
{% block formfooter %}
<input id="toggleButton" class="roundedButton" type="submit" value="Modify" />
{% if proposal.proposer == req.user and proposal.status != 'A' and proposal.status != 'S' %}
<a class=roundedButton href="/cfp/delete/{{ proposal.id }}">Delete</A>
{% endif %}
<a class=roundedButton href="/{{ req.session.lastlist }}">Cancel</A>
{% endblock %}

View File

@ -0,0 +1,54 @@
{% extends "base.html" %}
{% block helppage %}
<p>Welcome to the Design Summit session suggestion system.</p>
<p>This is the main screen. It lists all sessions suggested so far.</p>
<p>Each session has a topic, a title and a proposer. You can see the details of a proposed session by clicking on the title. You can sort the results by clicking on the corresponding table headers.</p>
<p>If you want to suggest your own session subject, click on <i>Suggest session</i>. If you're a topic lead, you will see the <i>Review topic</i> button that lets you review sessions suggested for your topic.</p>
{% endblock %}
{% block content %}
<script type="text/javascript" src="/media/sorting.js"></script>
<a class=roundedButton href=/cfp/create>Suggest session</A>
{% for topic in reviewable_topics %}
<a class=roundedButton href="/cfp/topic/{{ topic.id }}">Review topic: {{ topic.name }}</A>
{% endfor %}
<table>
<tr>
<th><a class="nolink"
href="" onclick="ts_resortTable(this); return false;">Topic<img
class="sortarrow" src="/media/arrowBlank" height="6" width="9"></a></th>
<th><a class="nolink"
href="" onclick="ts_resortTable(this); return false;">Title<img
class="sortarrow" src="/media/arrowBlank" height="6" width="9"></a>
<span class=small>(Click to view/edit)</span></th>
<th><a class="nolink"
href="" onclick="ts_resortTable(this); return false;">Proposer<img
class="sortarrow" src="/media/arrowBlank" height="6" width="9"></a></th>
<th class="right"><a class="nolink"
href="" onclick="ts_resortTable(this); return false;">Status<img
class="sortarrow" src="/media/arrowBlank" height="6" width="9"></a></th>
</tr>
{% for proposal in proposals %}
<tr>
<td>{{ proposal.topic.name }}</td>
<td>
{% if proposal.proposer == req.user and proposal.status != 'A' %}
<a href="/cfp/edit/{{ proposal.id }}">
{% else %}
<a href="/cfp/details/{{ proposal.id }}">
{% endif %}
{{ proposal.title }}</a>
</td>
<td>{{ proposal.proposer.first_name }} {{ proposal.proposer.last_name }}</td>
<td class="right"><span class="sortkey">{{ proposal.status }}</span>
{% if proposal.scheduled %}
Scheduled
{% else %}
{{ proposal.get_status_display }}
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% endblock %}
{% block extrafooter %}
{% endblock %}

View File

@ -0,0 +1,35 @@
{% extends "regform.html" %}
{% block helppage %}
<p>This is the session review screen.</p>
<p>Here you can change the status of the session and add a few comments. The following statuses are available:</p>
<p><i>Unreviewed</i>: you haven't had time to look into this one yet</p>
<p><i>Incomplete</i>: you would like to see changes made to this description before making a decision. Add details on the Reviewer's notes textfield: those will be sent to the proposer.</p>
<p><i>Preapproved</i>: proposed session looks good, you'll schedule it. You may still merge it with another session at scheduling time though. Title and description for preapproved sessions are frozen.</p>
<p><i>Rejected</i>: session is not appropriate, off-topic, not applicable to Grizzly, or not a design summit discussion.</p>
{% endblock %}
{% block formtitle %}
<h2>Review proposed session</h2>
<form action="/cfp/review/{{ proposal.id }}" method="post">
{% endblock %}
{% block formfooter %}
<p>If you change status, an email notification will be sent to the proposer, along with the reviewer's notes.</p>
<input id="toggleButton" class="roundedButton" type="submit" value="Modify" />
<a class=roundedButton href="/cfp/topic/{{proposal.topic.id}}">Cancel</A>
<h4>{{ proposal.title }}</h4>
<p>Proposed by <b>{{ proposal.proposer }}</b>
in topic <b>{{ proposal.topic }}</b></p>
<h4>Description</h4>
<pre>{{ proposal.description }}</pre>
{% if blueprints %}
<h4>Related blueprints</h4>
<ul>
{% for name, link in blueprints.items %}
<li><a href="{{ link }}">{{ name }}</a></li>
{% endfor %}
</ul>
{% endif %}
{% if proposal.proposer_notes %}
<h4>Proposer notes:</h4>
<p>{{ proposal.proposer_notes }}</p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends "regform.html" %}
{% block helppage %}
<p>This screen lets you change the topic for a proposed session.</p>
<p>The status for that session will be set back to <i>Unreviewed</i>.</p>
{% endblock %}
{% block formtitle %}
<h2>Switch topic for proposed session</h2>
<form action="/cfp/switch/{{ proposal.id }}" method="post">
{% endblock %}
{% block formfooter %}
<input id="toggleButton" class="roundedButton" type="submit" value="Switch" />
<a class=roundedButton href="/{{ req.session.lastlist }}">Cancel</A>
{% endblock %}

View File

@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block content %}
{% block formtitle %}
{% endblock %}
{% csrf_token %}
{% for field in form %}
<div class="fieldWrapper">
{% if field.errors %}
<div class="error">
{% for error in field.errors %}
{{ error|escape }}<br/>
{% endfor %}
{% endif %}
<label>{{ field.label }}:</label>
{{ field }}
{% if field.errors %}
</div>
{% endif %}
<div class="tip">{{ field.help_text }}</div>
</div>
{% endfor %}
{% block formfooter %}
{% endblock %}
</form>
{% endblock %}

View File

@ -0,0 +1,62 @@
{% extends "base.html" %}
{% block helppage %}
<p>This is the topic lead review screen. It lists all session suggestions for your topic so far.</p>
<p>A graph shows you how many sessions you have proposed, preapproved and scheduled against the number of available slots you have for your topic.</p>
<p>You can see the details of a proposed session (or edit sessions that are not in <i>Preapproved</i> state yet) by clicking on its title. You can sort the results by clicking on the corresponding table headers.</p>
<p>To change the status for a given session, click on the <i>Status</i> you want to change.</p>
<p>Finally, when you're ready to do the scheduling of your topic, you can click on the <i>Scheduling</i> button.</p>
{% endblock %}
{% block content %}
<script type="text/javascript" src="/media/sorting.js"></script>
<div class="span-8">
<h2>{{ topic.name }}</h2>
<a class=roundedButton href=/>Back to proposals list</A>
<a class=roundedButton href="/scheduling/{{ topic.id }}">Scheduling</A>
</div>
<div id="vis"><iframe frameborder=0 width=580 height=100 src=/scheduling/graph/{{ topic.id }}></iframe></div>
<table>
<tr>
<th>Topic</th>
<th><a class="nolink"
href="" onclick="ts_resortTable(this); return false;">Title<img
class="sortarrow" src="/media/arrowBlank" height="6" width="9"></a>
<span class=small>(Click to edit)</span></th>
<th><a class="nolink"
href="" onclick="ts_resortTable(this); return false;">Proposer<img
class="sortarrow" src="/media/arrowBlank" height="6" width="9"></a></th>
<th class="right"><a class="nolink"
href="" onclick="ts_resortTable(this); return false;">Status<img
class="sortarrow" src="/media/arrowBlank" height="6" width="9"></a>
<span class=small>(Click to review)</span></th>
</tr>
{% for proposal in proposals %}
<tr>
<td>
{% if proposal.scheduled %}
{{ proposal.topic.name }}
{% else %}
<a href="/cfp/switch/{{ proposal.id }}">{{ proposal.topic.name }}</a>
{% endif %}
</td>
<td>
{% if proposal.scheduled %}
<a href="/cfp/details/{{ proposal.id }}">{{ proposal.title }}</a>
{% else %}
<a href="/cfp/edit/{{ proposal.id }}">{{ proposal.title }}</a>
{% endif %}
</td>
<td>{{ proposal.proposer.first_name }} {{ proposal.proposer.last_name }}</td>
<td class="right"><span class="sortkey">{{ proposal.status }}</span>
{% if proposal.scheduled %}
Scheduled
{% else %}
<a href="/cfp/review/{{ proposal.id }}">
{{ proposal.get_status_display }}</a>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% endblock %}
{% block extrafooter %}
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block content %}
{% for topic in topics %}
<div class="span-5">
<h2>{{ topic.name }}</h2>
</div>
<div style="clear:right">
<iframe frameborder=0 width=580 height=100 src=/scheduling/graph/{{ topic.id }}></iframe>
</div>
{% endfor %}
{% endblock %}
{% block extrafooter %}
{% endblock %}

28
cfp/urls.py Normal file
View File

@ -0,0 +1,28 @@
# Copyright 2011 Thierry Carrez <thierry@openstack.org>
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls.defaults import *
urlpatterns = patterns('odsreg.cfp.views',
(r'^details/(\d+)$', 'details'),
(r'^create$', 'create'),
(r'^edit/(\d+)$', 'edit'),
(r'^review/(\d+)$', 'review'),
(r'^switch/(\d+)$', 'switch'),
(r'^delete/(\d+)$', 'delete'),
(r'^topic/(\d+)$', 'topiclist'),
(r'^topicstatus$', 'topicstatus'),
)

211
cfp/views.py Normal file
View File

@ -0,0 +1,211 @@
# Copyright 2011 Thierry Carrez <thierry@openstack.org>
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.shortcuts import render_to_response
from django.conf import settings
from django.contrib.auth import logout
from django.core.mail import EmailMessage
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.utils.encoding import smart_str
from odsreg.cfp.models import Proposal, Topic
from odsreg.cfp.models import ProposalForm, ProposalEditForm
from odsreg.cfp.models import ProposalReviewForm, ProposalSwitchForm
def linkify(blueprints):
links = {}
for bp in blueprints.split():
(project, name) = bp.split('/')
links[bp] = "https://blueprints.launchpad.net/%s/+spec/%s" \
% (project, name)
return links
def topiclead(user, topic):
return (user.username == topic.lead_username) or user.is_staff
def forbidden():
return HttpResponseForbidden("Forbidden")
@login_required
def list(request):
proposals = Proposal.objects.all()
reviewable_topics = Topic.objects.filter(
lead_username=request.user.username)
request.session['lastlist'] = ""
return render_to_response("cfplist.html",
{'req': request,
'proposals': proposals,
'reviewable_topics': reviewable_topics})
@login_required
def topiclist(request, topicid):
topic = Topic.objects.get(id=topicid)
if not topiclead(request.user, topic):
return forbidden()
proposals = Proposal.objects.filter(topic=topicid)
request.session['lastlist'] = "cfp/topic/%s" % topicid
return render_to_response("topiclist.html",
{'req': request,
'proposals': proposals,
'topic': topic})
@login_required
def topicstatus(request):
topics = Topic.objects.all()
return render_to_response("topicstatus.html",
{'req': request,
'topics': topics})
@login_required
def details(request, proposalid):
proposal = Proposal.objects.get(id=proposalid)
return render_to_response("cfpdetails.html",
{'req': request,
'proposal': proposal,
'blueprints': linkify(proposal.blueprints)})
@login_required
def create(request):
if request.method == 'POST':
form = ProposalForm(request.POST)
if form.is_valid():
proposal = form.save(commit=False)
proposal.proposer = request.user
proposal.status = 'U'
proposal.save()
return list(request)
else:
form = ProposalForm()
topics = Topic.objects.all()
return render_to_response('cfpcreate.html',
{'req': request,
'topics': topics,
'form': form})
@login_required
def edit(request, proposalid):
proposal = Proposal.objects.get(id=proposalid)
if (((proposal.proposer != request.user) or proposal.status in ['A', 'S'])
and not topiclead(request.user, proposal.topic)):
return forbidden()
if request.method == 'POST':
form = ProposalEditForm(request.POST, instance=proposal)
if form.is_valid():
form.save()
return HttpResponseRedirect('/%s' % request.session['lastlist'])
else:
form = ProposalEditForm(instance=proposal)
return render_to_response('cfpedit.html',
{'req': request,
'form': form,
'proposal': proposal})
@login_required
def delete(request, proposalid):
proposal = Proposal.objects.get(id=proposalid)
if ((proposal.proposer != request.user) or proposal.status in ['A', 'S']):
return forbidden()
if request.method == 'POST':
proposal.delete()
return HttpResponseRedirect('/%s' % request.session['lastlist'])
return render_to_response('cfpdelete.html',
{'req': request,
'proposal': proposal})
@login_required
def switch(request, proposalid):
proposal = Proposal.objects.get(id=proposalid)
if ((proposal.proposer != request.user)
and not topiclead(request.user, proposal.topic)) or proposal.scheduled:
return forbidden()
if request.method == 'POST':
form = ProposalSwitchForm(request.POST, instance=proposal)
if form.is_valid():
form.save()
proposal = Proposal.objects.get(id=proposalid)
proposal.status = 'U'
proposal.save()
return HttpResponseRedirect('/%s' % request.session['lastlist'])
else:
form = ProposalSwitchForm(instance=proposal)
return render_to_response('cfpswitch.html',
{'req': request,
'form': form,
'proposal': proposal})
@login_required
def review(request, proposalid):
proposal = Proposal.objects.get(id=proposalid)
if not topiclead(request.user, proposal.topic):
return forbidden()
current_status = proposal.status
status_long = proposal.get_status_display()
if request.method == 'POST':
form = ProposalReviewForm(request.POST, instance=proposal)
if form.is_valid():
form.save()
if (settings.SEND_MAIL and current_status != proposal.status):
lead = User.objects.get(username=proposal.topic.lead_username)
if (lead.email and proposal.proposer.email):
message = """
This is an automated email.
If needed, you should reply directly to the topic lead (%s).
On your session proposal: %s
The topic lead (%s) changed status from %s to %s.
Reviewer's notes:
%s
You can edit your proposal at: %s/cfp/edit/%s""" \
% (proposal.topic.lead_username,
smart_str(proposal.title),
proposal.topic.lead_username,
status_long, proposal.get_status_display(),
smart_str(proposal.reviewer_notes),
settings.SITE_ROOT, proposalid)
email = EmailMessage(settings.EMAIL_PREFIX +
"Status change on your session proposal",
message, settings.EMAIL_FROM,
[proposal.proposer.email, ], [],
headers={'Reply-To': lead.email})
email.send()
return HttpResponseRedirect('/cfp/topic/%d' % proposal.topic.id)
else:
form = ProposalReviewForm(instance=proposal)
return render_to_response('cfpreview.html',
{'req': request,
'form': form,
'proposal': proposal,
'blueprints': linkify(proposal.blueprints)})
def dologout(request):
logout(request)
return HttpResponseRedirect('/')

47
local_settings.py.sample Normal file
View File

@ -0,0 +1,47 @@
# Django settings for odsreg project.
#
# Copyright 2011 Thierry Carrez <thierry@openstack.org>
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# Override application settings
# Real database location
DATABASES = {
'default': {
'NAME': 'summit.db',
'ENGINE': 'django.db.backends.sqlite3',
}
}
SECRET_KEY = 'generateRandomOneHere'
# Run in production
DEBUG = False
TEMPLATE_DEBUG = DEBUG
#OPENID_USE_AS_ADMIN_LOGIN = True
# Change to match your Sched event
SCHED_URL = "essexdesignsummit"
SCHED_API_KEY = "getThisFromSched"
# Emails
SEND_MAIL = False
EMAIL_PREFIX = "[DesignSummit] "
EMAIL_FROM = "noreply@devnull.com"
EMAIL_HOST = "mail.devnull.com"
EMAIL_PORT = 25
#EMAIL_HOST_USER = SMTPUsername
#EMAIL_HOST_PASSWORD = SMTPPassword
#EMAIL_USE_TLS = True

29
manage.py Executable file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env python
# Copyright 2011 Thierry Carrez <thierry@openstack.org>
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.core.management import execute_manager
try:
import settings # Assumed to be in the same directory.
except ImportError:
import sys
sys.stderr.write("Error: Can't find the file 'settings.py'\n")
sys.exit(1)
if __name__ == "__main__":
execute_manager(settings)

14
scheduling/__init__.py Normal file
View File

@ -0,0 +1,14 @@
# Copyright 2011 Thierry Carrez <thierry@openstack.org>
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

25
scheduling/admin.py Normal file
View File

@ -0,0 +1,25 @@
# Copyright 2011 Thierry Carrez <thierry@openstack.org>
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from odsreg.scheduling.models import Slot, Room
from django.contrib import admin
class RoomAdmin(admin.ModelAdmin):
list_display = ('code', 'name')
admin.site.register(Room, RoomAdmin)
admin.site.register(Slot)

View File

@ -0,0 +1,14 @@
# Copyright 2011 Thierry Carrez <thierry@openstack.org>
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

View File

@ -0,0 +1,14 @@
# Copyright 2011 Thierry Carrez <thierry@openstack.org>
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

View File

@ -0,0 +1,60 @@
# Copyright 2011 Thierry Carrez <thierry@openstack.org>
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
from django.core.management.base import BaseCommand, CommandError
from scheduling.models import Slot, Room
from cfp.models import Topic
class Command(BaseCommand):
args = '<description.json>'
help = 'Create slots from JSON description'
def handle(self, *args, **options):
if len(args) != 1:
raise CommandError('Incorrect arguments')
try:
with open(args[0]) as f:
data = json.load(f)
except ValueError as exc:
raise CommandError("Malformed JSON: %s" % exc.message)
def slot_generator(mydata):
for d in mydata['slots']:
for h in d['hours']:
yield (d['day'], h)
for roomcode, roomdesc in data['rooms'].iteritems():
r = Room(code=roomcode, name=roomdesc)
r.save()
for topicname, desc in data['topics'].iteritems():
started = False
t = Topic(name=topicname, lead_username=desc['lead_username'],
description=desc['description'])
room = Room.objects.get(code=desc['room'])
t.save()
for (d, h) in slot_generator(data):
if (d == desc['start_day'] and h == desc['first_slot']):
started = True
if started:
s = Slot(start_time="%s %s" % (d, h), room=room, topic=t)
s.save()
if (d == desc['end_day'] and h == desc['last_slot']):
break

56
scheduling/models.py Normal file
View File

@ -0,0 +1,56 @@
# Copyright 2011 Thierry Carrez <thierry@openstack.org>
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.db import models
from django.forms import ModelForm
from odsreg.cfp.models import Proposal, Topic
class Room(models.Model):
code = models.CharField(max_length=1, primary_key=True)
name = models.CharField(max_length=40)
class Meta:
ordering = ['code']
def __unicode__(self):
return self.code
class Slot(models.Model):
start_time = models.CharField(max_length=16)
room = models.ForeignKey(Room)
topic = models.ForeignKey(Topic)
proposals = models.ManyToManyField(Proposal, blank=True, null=True)
title = models.CharField(max_length=60, blank=True,
verbose_name="Override title with",
help_text="Default title is the title of the first proposal. You can"
" override this default with the above-provided title")
description = models.TextField(blank=True,
verbose_name="Preface description with",
help_text='Text to preface the description of the session with. It'
' will be followed by the following session descriptions:')
class Meta:
ordering = ["start_time"]
def __unicode__(self):
return "%s %s %s" % (self.topic.name, self.room.code, self.start_time)
class SlotForm(ModelForm):
class Meta:
model = Slot
fields = ('title', 'description')

View File

@ -0,0 +1,40 @@
<script type="text/javascript" src="http://www.google.com/jsapi"></script>
<script type="text/javascript">
google.load('visualization', '1', {packages: ['corechart']});
function drawVisualization() {
// Create and populate the data table.
var data = new google.visualization.DataTable();
data.addColumn('string', 'Type');
data.addColumn('number', 'Available')
data.addColumn('number', 'Preapproved')
data.addColumn('number', 'Unreviewed')
data.addColumn('number', 'Incomplete')
data.addColumn('number', 'Scheduled')
data.addRows(2);
data.setValue(0,0,'Proposed')
data.setValue(1,0,'Available')
data.setValue(0,2, {{ stats.A }}); //Accepted
data.setValue(0,3, {{ stats.U }}); //Unreviewed
data.setValue(0,4, {{ stats.I }}); //Incomplete
data.setValue(0,5, {{ stats.S }}); //Scheduled
data.setValue(1,1, {{ stats.avail }}); //Available */
// Create and draw the visualization.
new google.visualization.BarChart(document.getElementById('visualization')).
draw(data,
{ chartArea: {height:36,width:400}, width:540, height:70,
series: {0:{visibleInLegend: false},
1:{color: '#109618'},
2:{color: '#aaaaaa'},
3:{color: '#ff9900'}},
hAxis: {textPosition: 'none', maxValue: {{stats.max}},
gridlines: {count: {{stats.max}} +1 }},
enableInteractivity: false,
isStacked: true, legend: 'bottom'}
);
}
google.setOnLoadCallback(drawVisualization);
</script>
<div id="visualization"></div>

View File

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block content %}
{% if list_calls %}
<h2>Would have pushed to sched.org:</h2>
{{ list_calls|safe }}
{% else %}
<h2>Sched.org synchronization successful</h2>
{% endif %}
<p>
<a class=roundedButton href="/scheduling/{{topic.id}}">Back to scheduling</A>
{% endblock %}

View File

@ -0,0 +1,97 @@
{% extends "base.html" %}
{% block helppage %}
<p>This is the topic lead scheduling screen.</p>
<p>On the left side you'll see the <i>Proposals to schedule</i>. That's the list of sessions that are in <i>Preapproved</i> state that you need to include in your schedule. On the right side you have the resulting <i>Schedule</i>, with one rectangle for each time slot you have available in your topic.</p>
<p>You assign sessions to slots simply by drag-and-dropping from the left column to the right column. You remove a session from a slot by clicking the <img src=/media/close.gif> icon that appears next to it.</p>
<p>Assigned sessions switch to <i>Scheduled</i> state, and their status cannot be changed anymore from the topic review screen. You have to remove them from the schedule if you want to change their status.</p>
<p>You can assign multiple sessions in a single slot. If you do that, it's generally a good idea to also rename the merged session title and provide a small description that will appear before each merged session descriptions. You can do so by clicking on the <img src=/media/edit.gif> icon in the slot.</p>
<p>You can also swap slots positions in your schedule, while keeping their content intact, by clicking on the <img src=/media/loop.gif> icon.</p>
<p>Once your schedule is complete, you can click on the <i>Push to Sched</i> button to sync your schedule with the general sched.org online schedule.</p>
{% endblock %}
{% block content %}
<h2>Scheduling ({{topic.name}})</h2>
<div id="scheduleform" class="span-24">
<form id="theform" method="post" action="">
<input type=hidden name="action"/>
<input type=hidden name="proposal"/>
<input type=hidden name="slot"/>
<a class=roundedButton href="/cfp/topic/{{topic.id}}">Back to track review</A>
<a class=roundedButton href="/scheduling/publish/{{topic.id}}">Push to Sched</A>
</form>
</div>
<div class="span-24">
<div class="sessionlist" class="span-8">
<h4>Proposals to schedule:</h4>
{% for line in accepted %}
<div class="sessionlistitem"><a class="accepted" id="{{line.id}}" href="#">{{ line.title }}</a></div>
{% endfor %}
</div>
<div class="schedulelist" class="span-8">
<h4>Schedule:</h4>
{% for line in schedule %}
<div class="schedulelistitem" id="{{line.id}}">{{line.start_time}}
<a href="/scheduling/swap/{{ line.id }}"><img src=/media/loop.gif></A>
{% if line.title %}
: <b>{{line.title}}</b>
{% endif %}
{% for proposal in line.proposals.all %}
{% if forloop.counter == 1 %}
<a href="/scheduling/edit/{{ line.id }}"><img src=/media/edit.gif></A></br>
{% endif %}
- {{proposal.title}} <a href="#" onClick="action('del', {{proposal.id}}, {{line.id}})"><img src=/media/close.gif></A></br>
{% endfor %}
</div>
{% endfor %}
</div>
</div>
<script>
function action(act, proposal, slot)
{
var theform = document.querySelector('#theform');
theform.action.value = act;
theform.proposal.value = proposal;
theform.slot.value = slot;
theform.submit();
}
var links = document.querySelectorAll('a.accepted'), el = null;
for (var i = 0; i < links.length; i++) {
el = links[i];
el.setAttribute('draggable', 'true');
el.addEventListener('dragstart', function (e) {
e.dataTransfer.effectAllowed = 'copy';
e.dataTransfer.setData('Text', this.id);
}, false);
}
var links = document.querySelectorAll('div.schedulelistitem'), el = null;
for (var i = 0; i < links.length; i++) {
el = links[i];
el.addEventListener('dragover', function (e) {
if (e.preventDefault) e.preventDefault(); // allows us to drop
this.classList.add('over');
e.dataTransfer.dropEffect = 'copy';
return false;
}, false);
// to get IE to work
el.addEventListener('dragenter', function (e) {
this.classList.add('over');
return false;
}, false);
el.addEventListener('dragleave', function (e) {
this.classList.remove('over');
}, false);
el.addEventListener('drop', function (e) {
if (e.stopPropagation) e.stopPropagation();
var source = document.getElementById(e.dataTransfer.getData('Text'));
action('add', source.id, this.id);
return false;
}, true);
}
</script>
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends "regform.html" %}
{% block helppage %}
<p>This screen lets you override the title for a slot, and provide a small description that will appear <i>before</i> all the session descriptions suggested by users.</p>
<p>This is especially useful when you merge multiple suggested sessions into a single discussion slot.</p>
{% endblock %}
{% block formtitle %}
<h2>Edit slot '{{ title }}'</h2>
<form action="/scheduling/edit/{{ slot.id }}" method="post">
{% endblock %}
{% block formfooter %}
<pre>{{ full_desc }}</pre>
<input id="toggleButton" class="roundedButton" type="submit" value="Modify" />
<a class=roundedButton href="/scheduling/{{ slot.topic.id }}">Cancel</A>
{% endblock %}

View File

@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block helppage %}
<p>This screen lets you swap slot positions in your schedule, while keeping slot contents intact.</p>
<p>Just select the slot you want to swap this slot with.</p>
{% endblock %}
{% block content %}
<h2>Swap slot positions</h2>
<form action="/scheduling/swap/{{ oldslot.id }}" method="post">
<p>
{% if title %}
This slot ("{{title}}")
{% else %}
This empty slot
{% endif %}
is currently positioned at {{ oldslot.start_time }}.</p>
<p>Select a new position:
<select name="newslotid">
{% for newslot_time, newslot_id, newslot_title in newslots %}
<option value="{{ newslot_id }}">{{ newslot_time }}
{% if newslot_title %}({{newslot_title}}){% endif%}</option>
{% endfor %}
</select></p>
<input id="toggleButton" class="roundedButton" type="submit" value="Swap" />
<a class=roundedButton href="/scheduling/{{ oldslot.topic.id }}">Cancel</A>
{% endblock %}

25
scheduling/urls.py Normal file
View File

@ -0,0 +1,25 @@
# Copyright 2011 Thierry Carrez <thierry@openstack.org>
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls.defaults import *
urlpatterns = patterns('odsreg.scheduling.views',
(r'^(\d+)$', 'scheduling'),
(r'^edit/(\d+)$', 'edit'),
(r'^swap/(\d+)$', 'swap'),
(r'^publish/(\d+)$', 'publish'),
(r'^graph/(\d+)$', 'graph'),
)

221
scheduling/views.py Normal file
View File

@ -0,0 +1,221 @@
# Copyright 2011 Thierry Carrez <thierry@openstack.org>
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import urllib
import urllib2
from django.shortcuts import render_to_response
from django.conf import settings
from django.http import HttpResponseRedirect
from django.utils.encoding import smart_str
from odsreg.cfp.models import Proposal, Topic
from odsreg.cfp.views import topiclead, forbidden
from odsreg.scheduling.models import Slot, SlotForm
def combined_id(slot):
return slot.proposals.order_by('id')[0].id
def combined_title(slot):
if slot.title:
return slot.title
proposals = slot.proposals.all()
if len(proposals) > 0:
return proposals[0].title
return ""
def combined_description(slot):
full_desc = ""
proposals = slot.proposals.all()
if len(proposals) > 1 or slot.title:
full_desc = "This session will include the following subject(s):\n\n"
for p in slot.proposals.all():
if len(proposals) > 1 or slot.title:
full_desc = full_desc + p.title + ":\n\n"
full_desc = full_desc + p.description + "\n\n"
full_desc += "(Session proposed by %s %s)\n\n" % (
p.proposer.first_name, p.proposer.last_name)
return full_desc
def htmlize(desc):
return desc.replace('\n', '<br />')
def full_description(slot):
desc = ""
if slot.description:
desc = slot.description + "\n\n"
desc += combined_description(slot)
return desc
def scheduling(request, topicid):
topic = Topic.objects.get(id=topicid)
if not topiclead(request.user, topic):
return forbidden()
if request.method == 'POST':
action = request.POST['action']
proposal = Proposal.objects.get(id=request.POST['proposal'])
slot = Slot.objects.get(id=request.POST['slot'])
already_scheduled = slot.proposals.all()
if action == "add":
if proposal not in already_scheduled:
slot.proposals.add(proposal)
slot.save()
proposal.scheduled = True
proposal.save()
if action == "del":
if proposal in already_scheduled:
if len(already_scheduled) == 1:
slot.title = ""
slot.description = ""
slot.proposals.remove(proposal)
slot.save()
proposal.scheduled = False
proposal.save()
accepted = Proposal.objects.filter(status='A', scheduled=False,
topic=topic)
schedule = Slot.objects.filter(topic=topic)
return render_to_response("scheduling.html",
{'req': request,
'accepted': accepted,
'schedule': schedule,
'topic': topic})
def end_time(start_time):
"""Rough calculation of end time.
Works because we don't start at 08:00 and align on 10's of minutes"""
end_minute = int(start_time[-2:]) + 40
if end_minute >= 60:
end_hour = str(int(start_time[-5:-3]) + 1)
end_minute = end_minute - 60
if end_minute == 0:
return start_time[:-5] + end_hour + ":00"
else:
return start_time[:-5] + end_hour + ":" + str(end_minute)
else:
return start_time[:-2] + str(end_minute)
def publish(request, topicid):
topic = Topic.objects.get(id=topicid)
if not topiclead(request.user, topic):
return forbidden()
list_calls = ""
baseurl = "http://%s.sched.org/api/session/" % settings.SCHED_URL
for slot in Slot.objects.filter(topic=topicid):
if len(slot.proposals.all()) > 0:
values = {'api_key': settings.SCHED_API_KEY,
'session_key': "slot-%d" % combined_id(slot),
'name': smart_str(combined_title(slot)),
'session_start': slot.start_time,
'session_end': end_time(slot.start_time),
'session_type': 'Design Summit',
'session_subtype': slot.topic,
'venue': slot.room.name,
'description': htmlize(smart_str(
full_description(slot)))}
data = urllib.urlencode(values)
if settings.SCHED_API_KEY == "getThisFromSched":
list_calls += "%s<P>" % data
else:
f = urllib2.urlopen(baseurl + "mod", data)
if f.readline().startswith("ERR:"):
f.close()
f = urllib2.urlopen(baseurl + "add", data)
f.close()
return render_to_response("sched.html",
{'req': request,
'list_calls': list_calls,
'topic': topic})
def edit(request, slotid):
slot = Slot.objects.get(id=slotid)
if not topiclead(request.user, slot.topic):
return forbidden()
if request.method == 'POST':
form = SlotForm(request.POST, instance=slot)
if form.is_valid():
form.save()
return HttpResponseRedirect('/scheduling/%s' % slot.topic.id)
else:
form = SlotForm(instance=slot)
return render_to_response('slotedit.html',
{'req': request,
'form': form,
'title': combined_title(slot),
'full_desc': combined_description(slot),
'slot': slot})
def swap(request, slotid):
oldslot = Slot.objects.get(id=slotid)
if not topiclead(request.user, oldslot.topic):
return forbidden()
if request.method == 'POST':
newslotid = int(request.POST['newslotid'])
newslot = Slot.objects.get(id=newslotid, topic=oldslot.topic)
new_start_time = newslot.start_time
new_room = newslot.room
newslot.start_time = oldslot.start_time
newslot.room = oldslot.room
oldslot.start_time = new_start_time
oldslot.room = new_room
newslot.save()
oldslot.save()
return HttpResponseRedirect('/scheduling/%s' % oldslot.topic.id)
newslots = []
available_slots = Slot.objects.filter(
topic=oldslot.topic).exclude(id=slotid)
for slot in available_slots:
triplet = (slot.start_time, slot.id, combined_title(slot))
newslots.append(triplet)
return render_to_response('slotswap.html',
{'req': request,
'title': combined_title(oldslot),
'oldslot': oldslot,
'newslots': newslots})
def graph(request, topicid):
topic = Topic.objects.get(id=topicid)
unsched_proposals = Proposal.objects.filter(topic=topic, scheduled=False)
slots = Slot.objects.filter(topic=topic)
nbscheduled = 0
nbavail = 0
for slot in slots:
nbavail = nbavail + 1
if len(slot.proposals.all()) > 0:
nbscheduled = nbscheduled + 1
stats = {'U': 0, 'I': 0, 'A': 0,
'S': nbscheduled,
'avail': nbavail}
nbproposed = 0
for proposal in unsched_proposals:
if proposal.status != 'R':
stats[proposal.status] += 1
nbproposed += 1
stats['max'] = max(stats['avail'], nbproposed + nbscheduled)
return render_to_response("graph.html",
{'req': request,
'stats': stats,
'topic': topic})

110
settings.py Normal file
View File

@ -0,0 +1,110 @@
# Django settings for odsreg project.
#
# Copyright 2011 Thierry Carrez <thierry@openstack.org>
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
PROJECT_PATH = os.path.dirname(os.path.abspath(__file__))
DEBUG = True
TEMPLATE_DEBUG = DEBUG
SERVE_STATIC = True
SITE_ROOT = 'http://summit.openstack.org'
DATABASES = {
'default': {
'NAME': 'summit.db',
'ENGINE': 'django.db.backends.sqlite3',
}
}
SCHED_URL = "essexdesignsummit"
SCHED_API_KEY = "getThisFromSched"
SITE_ID = 1
STATIC_URL = '/media/'
# Make this unique, and don't share it with anybody.
SECRET_KEY = 'changemeInLocalSettings'
# List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)
TEMPLATE_CONTEXT_PROCESSORS = (
"django.contrib.auth.context_processors.auth",
"django.core.context_processors.debug",
"django.core.context_processors.i18n",
"django.core.context_processors.media",
"django.core.context_processors.request",
)
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
)
ROOT_URLCONF = 'odsreg.urls'
INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_openid_auth',
'django.contrib.admin',
'odsreg.cfp',
'odsreg.scheduling',
]
AUTHENTICATION_BACKENDS = (
'django_openid_auth.auth.OpenIDBackend',
'django.contrib.auth.backends.ModelBackend',
)
# Should users be created when new OpenIDs are used to log in?
OPENID_CREATE_USERS = True
OPENID_STRICT_USERNAMES = True
# Can we reuse existing users?
OPENID_REUSE_USERS = True
# When logging in again, should we overwrite user details based on
# data received via Simple Registration?
OPENID_UPDATE_DETAILS_FROM_SREG = True
# If set, always use this as the identity URL rather than asking the
# user. This only makes sense if it is a server URL.
OPENID_SSO_SERVER_URL = 'https://login.launchpad.net/'
# Tell django.contrib.auth to use the OpenID signin URLs.
LOGIN_URL = '/openid/login'
LOGIN_REDIRECT_URL = '/'
# Override settings with local ones.
try:
from local_settings import *
except ImportError:
pass

32
slots.json.sample Normal file
View File

@ -0,0 +1,32 @@
{
"rooms": {
"A": "Annie AB",
"P": "Ballroom"
},
"slots": [
{
"day": "2012-09-20",
"hours": [
"09:30",
"10:00",
"10:30"
]
},
{
"day": "2012-09-21",
"hours": [
"09:30",
"10:00",
"10:30"
]
}
],
"topics": {
"Swift": {
"description": "Sessions about OpenStack Object Storage (Swift)",
"lead_username": "notmyname",
"room": "A",
"start_day": "2012-09-20", "first_slot": "09:30",
"end_day": "2012-09-21", "last_slot": "10:00" }
}
}

29
urls.py Normal file
View File

@ -0,0 +1,29 @@
# Copyright 2011 Thierry Carrez <thierry@openstack.org>
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls.defaults import *
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
(r'^openid/', include('django_openid_auth.urls')),
(r'^$', 'odsreg.cfp.views.list'),
(r'^cfp/', include('odsreg.cfp.urls')),
(r'^scheduling/', include('odsreg.scheduling.urls')),
url(r'^admin/', include(admin.site.urls)),
(r'^logout$', 'odsreg.cfp.views.dologout'),
)