commit 56fdefeccdfb339c1d2f13ae470feb1e01b2a6e9 Author: Thierry Carrez Date: Thu Dec 20 16:11:01 2012 +0100 Initial import Initial import after cleanup. Previous history lives in https://code.launchpad.net/~ttx/+junk/odsreg diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..68c771a --- /dev/null +++ b/LICENSE @@ -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. + diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..d263b19 --- /dev/null +++ b/README.rst @@ -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 diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..22a3f41 --- /dev/null +++ b/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2011 Thierry Carrez +# 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. diff --git a/cfp/__init__.py b/cfp/__init__.py new file mode 100644 index 0000000..22a3f41 --- /dev/null +++ b/cfp/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2011 Thierry Carrez +# 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. diff --git a/cfp/admin.py b/cfp/admin.py new file mode 100644 index 0000000..1c4a55c --- /dev/null +++ b/cfp/admin.py @@ -0,0 +1,21 @@ +# Copyright 2011 Thierry Carrez +# 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) diff --git a/cfp/models.py b/cfp/models.py new file mode 100644 index 0000000..20f6ed4 --- /dev/null +++ b/cfp/models.py @@ -0,0 +1,117 @@ +# Copyright 2011 Thierry Carrez +# 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',) diff --git a/cfp/static/arrowBlank b/cfp/static/arrowBlank new file mode 100644 index 0000000..435fe4f Binary files /dev/null and b/cfp/static/arrowBlank differ diff --git a/cfp/static/arrowDown b/cfp/static/arrowDown new file mode 100644 index 0000000..93d8948 Binary files /dev/null and b/cfp/static/arrowDown differ diff --git a/cfp/static/arrowUp b/cfp/static/arrowUp new file mode 100644 index 0000000..7c35d2f Binary files /dev/null and b/cfp/static/arrowUp differ diff --git a/cfp/static/close.gif b/cfp/static/close.gif new file mode 100644 index 0000000..bd8f82f Binary files /dev/null and b/cfp/static/close.gif differ diff --git a/cfp/static/edit.gif b/cfp/static/edit.gif new file mode 100644 index 0000000..16c3ea9 Binary files /dev/null and b/cfp/static/edit.gif differ diff --git a/cfp/static/loop.gif b/cfp/static/loop.gif new file mode 100644 index 0000000..9a92efc Binary files /dev/null and b/cfp/static/loop.gif differ diff --git a/cfp/static/odsreg.css b/cfp/static/odsreg.css new file mode 100644 index 0000000..79d7321 --- /dev/null +++ b/cfp/static/odsreg.css @@ -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 */ + + diff --git a/cfp/static/openstack-logo.png b/cfp/static/openstack-logo.png new file mode 100644 index 0000000..146faec Binary files /dev/null and b/cfp/static/openstack-logo.png differ diff --git a/cfp/static/openstack-page-bkg.jpg b/cfp/static/openstack-page-bkg.jpg new file mode 100644 index 0000000..f788c41 Binary files /dev/null and b/cfp/static/openstack-page-bkg.jpg differ diff --git a/cfp/static/sorting.js b/cfp/static/sorting.js new file mode 100644 index 0000000..9d6898d --- /dev/null +++ b/cfp/static/sorting.js @@ -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;ti0){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' ++txt+'';} +for(var i=0;iError: Page not found +

This page does not exist.

+{% endblock %} diff --git a/cfp/templates/500.html b/cfp/templates/500.html new file mode 100644 index 0000000..c66a3d5 --- /dev/null +++ b/cfp/templates/500.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} +{% block content %} +

Error: Application error

+

This application encoutered an error.

+{% endblock %} diff --git a/cfp/templates/base.html b/cfp/templates/base.html new file mode 100644 index 0000000..bec7229 --- /dev/null +++ b/cfp/templates/base.html @@ -0,0 +1,52 @@ + + + + + OpenStack Grizzly Design Summit + + + + +
+ +
+ +
+ {% block content %} + {% endblock %} +
+ +
+
+
+

Help

+{% block helppage %}

Sorry, no help is provided for this screen.

{% endblock %} +Close +
+
+ +
+ +
+ + diff --git a/cfp/templates/cfpcreate.html b/cfp/templates/cfpcreate.html new file mode 100644 index 0000000..01d7d3c --- /dev/null +++ b/cfp/templates/cfpcreate.html @@ -0,0 +1,20 @@ +{% extends "regform.html" %} +{% block helppage %} +

This page lets you suggest a session topic for the Design Summit.

+

Fill the Title, Description and Topic 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.

+

You should pay special attention to the Topic, as you can't change it yourself later. Available topics are:

+{% for topic in topics %} +

{{topic.name}}: {{topic.description}}

+{% endfor %} +

When you've completed the form, click on the Suggest 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.

+{% endblock %} +{% block formtitle %} +

Suggest a session

+
+{% endblock %} +{% block formfooter %} + +Cancel +{% endblock %} diff --git a/cfp/templates/cfpdelete.html b/cfp/templates/cfpdelete.html new file mode 100644 index 0000000..648219d --- /dev/null +++ b/cfp/templates/cfpdelete.html @@ -0,0 +1,12 @@ +{% extends "regform.html" %} +{% block helppage %} +

This screen lets you confirm the deletion of a proposal you created.

+{% endblock %} +{% block formtitle %} +

Do you really want to delete session '{{proposal.title}}' ?

+ +{% endblock %} +{% block formfooter %} + +Cancel +{% endblock %} diff --git a/cfp/templates/cfpdetails.html b/cfp/templates/cfpdetails.html new file mode 100644 index 0000000..49b5fab --- /dev/null +++ b/cfp/templates/cfpdetails.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} +{% block helppage %} +

This screen lets you see the details of a proposed session.

+

Note that you can only edit sessions that you suggested yourself (or if you're the topic lead). Sessions in Preapproved state cannot be changed.

+{% endblock %} +{% block content %} +

{{ proposal.title }}

+

Proposed by {{ proposal.proposer }} +in topic {{ proposal.topic }}

+

Description

+
{{ proposal.description }}
+{% if blueprints %} +

Related blueprints

+
    +{% for name, link in blueprints.items %} +
  • {{ name }}
  • +{% endfor %} +
+{% endif %} +

Status

+

This proposal is in {{ proposal.get_status_display }} state.

+Back +{% endblock %} diff --git a/cfp/templates/cfpedit.html b/cfp/templates/cfpedit.html new file mode 100644 index 0000000..d6efbd8 --- /dev/null +++ b/cfp/templates/cfpedit.html @@ -0,0 +1,20 @@ +{% extends "regform.html" %} +{% block helppage %} +

This screen lets you change the details of a proposed session.

+

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.

+{% endblock %} +{% block formtitle %} +

Edit proposed {{proposal.topic.name}} session

+{% if proposal.reviewer_notes %} +

Reviewer notes:

+

{{ proposal.reviewer_notes }}

+{% endif %} + +{% endblock %} +{% block formfooter %} + +{% if proposal.proposer == req.user and proposal.status != 'A' and proposal.status != 'S' %} +Delete +{% endif %} +Cancel +{% endblock %} diff --git a/cfp/templates/cfplist.html b/cfp/templates/cfplist.html new file mode 100644 index 0000000..76fd30d --- /dev/null +++ b/cfp/templates/cfplist.html @@ -0,0 +1,54 @@ +{% extends "base.html" %} +{% block helppage %} +

Welcome to the Design Summit session suggestion system.

+

This is the main screen. It lists all sessions suggested so far.

+

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.

+

If you want to suggest your own session subject, click on Suggest session. If you're a topic lead, you will see the Review topic button that lets you review sessions suggested for your topic.

+{% endblock %} +{% block content %} + +Suggest session +{% for topic in reviewable_topics %} +Review topic: {{ topic.name }} +{% endfor %} + + + + + + + +{% for proposal in proposals %} + + + + + + +{% endfor %} +
TopicTitle + (Click to view/edit)ProposerStatus
{{ proposal.topic.name }} +{% if proposal.proposer == req.user and proposal.status != 'A' %} + +{% else %} + +{% endif %} +{{ proposal.title }} +{{ proposal.proposer.first_name }} {{ proposal.proposer.last_name }}{{ proposal.status }} +{% if proposal.scheduled %} +Scheduled +{% else %} +{{ proposal.get_status_display }} +{% endif %} +
+{% endblock %} +{% block extrafooter %} +{% endblock %} diff --git a/cfp/templates/cfpreview.html b/cfp/templates/cfpreview.html new file mode 100644 index 0000000..e6bb403 --- /dev/null +++ b/cfp/templates/cfpreview.html @@ -0,0 +1,35 @@ +{% extends "regform.html" %} +{% block helppage %} +

This is the session review screen.

+

Here you can change the status of the session and add a few comments. The following statuses are available:

+

Unreviewed: you haven't had time to look into this one yet

+

Incomplete: 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.

+

Preapproved: 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.

+

Rejected: session is not appropriate, off-topic, not applicable to Grizzly, or not a design summit discussion.

+{% endblock %} +{% block formtitle %} +

Review proposed session

+ +{% endblock %} +{% block formfooter %} +

If you change status, an email notification will be sent to the proposer, along with the reviewer's notes.

+ +Cancel +

{{ proposal.title }}

+

Proposed by {{ proposal.proposer }} +in topic {{ proposal.topic }}

+

Description

+
{{ proposal.description }}
+{% if blueprints %} +

Related blueprints

+
    +{% for name, link in blueprints.items %} +
  • {{ name }}
  • +{% endfor %} +
+{% endif %} +{% if proposal.proposer_notes %} +

Proposer notes:

+

{{ proposal.proposer_notes }}

+{% endif %} +{% endblock %} diff --git a/cfp/templates/cfpswitch.html b/cfp/templates/cfpswitch.html new file mode 100644 index 0000000..deccf9d --- /dev/null +++ b/cfp/templates/cfpswitch.html @@ -0,0 +1,13 @@ +{% extends "regform.html" %} +{% block helppage %} +

This screen lets you change the topic for a proposed session.

+

The status for that session will be set back to Unreviewed.

+{% endblock %} +{% block formtitle %} +

Switch topic for proposed session

+ +{% endblock %} +{% block formfooter %} + +Cancel +{% endblock %} diff --git a/cfp/templates/regform.html b/cfp/templates/regform.html new file mode 100644 index 0000000..9db0b04 --- /dev/null +++ b/cfp/templates/regform.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% block content %} +{% block formtitle %} +{% endblock %} +{% csrf_token %} + {% for field in form %} +
+{% if field.errors %} +
+ {% for error in field.errors %} +{{ error|escape }}
+ {% endfor %} +{% endif %} + +{{ field }} +{% if field.errors %} +
+{% endif %} +
{{ field.help_text }}
+
+ {% endfor %} +{% block formfooter %} +{% endblock %} +
+{% endblock %} diff --git a/cfp/templates/topiclist.html b/cfp/templates/topiclist.html new file mode 100644 index 0000000..aee7468 --- /dev/null +++ b/cfp/templates/topiclist.html @@ -0,0 +1,62 @@ +{% extends "base.html" %} +{% block helppage %} +

This is the topic lead review screen. It lists all session suggestions for your topic so far.

+

A graph shows you how many sessions you have proposed, preapproved and scheduled against the number of available slots you have for your topic.

+

You can see the details of a proposed session (or edit sessions that are not in Preapproved state yet) by clicking on its title. You can sort the results by clicking on the corresponding table headers.

+

To change the status for a given session, click on the Status you want to change.

+

Finally, when you're ready to do the scheduling of your topic, you can click on the Scheduling button.

+{% endblock %} +{% block content %} + +
+

{{ topic.name }}

+Back to proposals list +Scheduling +
+
+ + + + + + + +{% for proposal in proposals %} + + + + + + +{% endfor %} +
TopicTitle + (Click to edit)ProposerStatus + (Click to review)
+{% if proposal.scheduled %} +{{ proposal.topic.name }} +{% else %} +{{ proposal.topic.name }} +{% endif %} + +{% if proposal.scheduled %} +{{ proposal.title }} +{% else %} +{{ proposal.title }} +{% endif %} +{{ proposal.proposer.first_name }} {{ proposal.proposer.last_name }}{{ proposal.status }} +{% if proposal.scheduled %} +Scheduled +{% else %} + +{{ proposal.get_status_display }} +{% endif %} +
+{% endblock %} +{% block extrafooter %} +{% endblock %} diff --git a/cfp/templates/topicstatus.html b/cfp/templates/topicstatus.html new file mode 100644 index 0000000..dd4fb33 --- /dev/null +++ b/cfp/templates/topicstatus.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} +{% block content %} +{% for topic in topics %} +
+

{{ topic.name }}

+
+
+ +
+{% endfor %} +{% endblock %} +{% block extrafooter %} +{% endblock %} diff --git a/cfp/urls.py b/cfp/urls.py new file mode 100644 index 0000000..2ac3bc4 --- /dev/null +++ b/cfp/urls.py @@ -0,0 +1,28 @@ +# Copyright 2011 Thierry Carrez +# 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'), +) diff --git a/cfp/views.py b/cfp/views.py new file mode 100644 index 0000000..6c7e287 --- /dev/null +++ b/cfp/views.py @@ -0,0 +1,211 @@ +# Copyright 2011 Thierry Carrez +# 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('/') diff --git a/local_settings.py.sample b/local_settings.py.sample new file mode 100644 index 0000000..0f4465f --- /dev/null +++ b/local_settings.py.sample @@ -0,0 +1,47 @@ +# Django settings for odsreg project. +# +# Copyright 2011 Thierry Carrez +# 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 diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..303b70a --- /dev/null +++ b/manage.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python + +# Copyright 2011 Thierry Carrez +# 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) diff --git a/scheduling/__init__.py b/scheduling/__init__.py new file mode 100644 index 0000000..22a3f41 --- /dev/null +++ b/scheduling/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2011 Thierry Carrez +# 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. diff --git a/scheduling/admin.py b/scheduling/admin.py new file mode 100644 index 0000000..8a93fd5 --- /dev/null +++ b/scheduling/admin.py @@ -0,0 +1,25 @@ +# Copyright 2011 Thierry Carrez +# 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) diff --git a/scheduling/management/__init__.py b/scheduling/management/__init__.py new file mode 100644 index 0000000..22a3f41 --- /dev/null +++ b/scheduling/management/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2011 Thierry Carrez +# 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. diff --git a/scheduling/management/commands/__init__.py b/scheduling/management/commands/__init__.py new file mode 100644 index 0000000..22a3f41 --- /dev/null +++ b/scheduling/management/commands/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2011 Thierry Carrez +# 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. diff --git a/scheduling/management/commands/loadtopics.py b/scheduling/management/commands/loadtopics.py new file mode 100644 index 0000000..564574e --- /dev/null +++ b/scheduling/management/commands/loadtopics.py @@ -0,0 +1,60 @@ +# Copyright 2011 Thierry Carrez +# 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 = '' + 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 diff --git a/scheduling/models.py b/scheduling/models.py new file mode 100644 index 0000000..818947b --- /dev/null +++ b/scheduling/models.py @@ -0,0 +1,56 @@ +# Copyright 2011 Thierry Carrez +# 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') diff --git a/scheduling/templates/graph.html b/scheduling/templates/graph.html new file mode 100644 index 0000000..3bb4853 --- /dev/null +++ b/scheduling/templates/graph.html @@ -0,0 +1,40 @@ + + +
diff --git a/scheduling/templates/sched.html b/scheduling/templates/sched.html new file mode 100644 index 0000000..98460bf --- /dev/null +++ b/scheduling/templates/sched.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} +{% block content %} +{% if list_calls %} +

Would have pushed to sched.org:

+{{ list_calls|safe }} +{% else %} +

Sched.org synchronization successful

+{% endif %} +

+Back to scheduling +{% endblock %} diff --git a/scheduling/templates/scheduling.html b/scheduling/templates/scheduling.html new file mode 100644 index 0000000..abe5b0a --- /dev/null +++ b/scheduling/templates/scheduling.html @@ -0,0 +1,97 @@ +{% extends "base.html" %} +{% block helppage %} +

This is the topic lead scheduling screen.

+

On the left side you'll see the Proposals to schedule. That's the list of sessions that are in Preapproved state that you need to include in your schedule. On the right side you have the resulting Schedule, with one rectangle for each time slot you have available in your topic.

+

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 icon that appears next to it.

+

Assigned sessions switch to Scheduled 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.

+

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 icon in the slot.

+

You can also swap slots positions in your schedule, while keeping their content intact, by clicking on the icon.

+

Once your schedule is complete, you can click on the Push to Sched button to sync your schedule with the general sched.org online schedule.

+{% endblock %} +{% block content %} +

Scheduling ({{topic.name}})

+ +
+
+

Proposals to schedule:

+{% for line in accepted %} + +{% endfor %} +
+
+

Schedule:

+{% for line in schedule %} +
{{line.start_time}} + +{% if line.title %} +: {{line.title}} +{% endif %} +{% for proposal in line.proposals.all %} +{% if forloop.counter == 1 %} +
+{% endif %} +- {{proposal.title}}
+{% endfor %} +
+{% endfor %} +
+
+ +{% endblock %} diff --git a/scheduling/templates/slotedit.html b/scheduling/templates/slotedit.html new file mode 100644 index 0000000..783ec5e --- /dev/null +++ b/scheduling/templates/slotedit.html @@ -0,0 +1,14 @@ +{% extends "regform.html" %} +{% block helppage %} +

This screen lets you override the title for a slot, and provide a small description that will appear before all the session descriptions suggested by users.

+

This is especially useful when you merge multiple suggested sessions into a single discussion slot.

+{% endblock %} +{% block formtitle %} +

Edit slot '{{ title }}'

+
+{% endblock %} +{% block formfooter %} +
{{ full_desc }}
+ +Cancel +{% endblock %} diff --git a/scheduling/templates/slotswap.html b/scheduling/templates/slotswap.html new file mode 100644 index 0000000..e034965 --- /dev/null +++ b/scheduling/templates/slotswap.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% block helppage %} +

This screen lets you swap slot positions in your schedule, while keeping slot contents intact.

+

Just select the slot you want to swap this slot with.

+{% endblock %} +{% block content %} +

Swap slot positions

+ +

+{% if title %} +This slot ("{{title}}") +{% else %} +This empty slot +{% endif %} +is currently positioned at {{ oldslot.start_time }}.

+

Select a new position: +

+ +Cancel +{% endblock %} diff --git a/scheduling/urls.py b/scheduling/urls.py new file mode 100644 index 0000000..9146b8b --- /dev/null +++ b/scheduling/urls.py @@ -0,0 +1,25 @@ +# Copyright 2011 Thierry Carrez +# 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'), +) diff --git a/scheduling/views.py b/scheduling/views.py new file mode 100644 index 0000000..5d47609 --- /dev/null +++ b/scheduling/views.py @@ -0,0 +1,221 @@ +# Copyright 2011 Thierry Carrez +# 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', '
') + + +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

" % 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}) diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..546f3bd --- /dev/null +++ b/settings.py @@ -0,0 +1,110 @@ +# Django settings for odsreg project. +# +# Copyright 2011 Thierry Carrez +# 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 diff --git a/slots.json.sample b/slots.json.sample new file mode 100644 index 0000000..bd748b1 --- /dev/null +++ b/slots.json.sample @@ -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" } + } +} diff --git a/urls.py b/urls.py new file mode 100644 index 0000000..eff48f9 --- /dev/null +++ b/urls.py @@ -0,0 +1,29 @@ +# Copyright 2011 Thierry Carrez +# 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'), +)