diff --git a/cfp/forms.py b/cfp/forms.py new file mode 100644 index 0000000..940c6bf --- /dev/null +++ b/cfp/forms.py @@ -0,0 +1,49 @@ +# 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.forms import ModelForm, CharField, Textarea + +from odsreg.cfp.models import Comment, Proposal + + +class CommentForm(ModelForm): + class Meta: + model = Comment + exclude = ('proposal', 'posted_date', 'author') + + +class ProposalForm(ModelForm): + class Meta: + model = Proposal + exclude = ('proposer', 'status', 'scheduled') + + +class ProposalEditForm(ModelForm): + class Meta: + model = Proposal + exclude = ('topic', 'proposer', 'status', 'scheduled') + + +class ProposalReviewForm(ModelForm): + comment = CharField(widget=Textarea) + class Meta: + model = Proposal + fields = ('status',) + + +class ProposalSwitchForm(ModelForm): + class Meta: + model = Proposal + fields = ('topic',) diff --git a/cfp/models.py b/cfp/models.py index 5c81899..aba12b5 100644 --- a/cfp/models.py +++ b/cfp/models.py @@ -13,36 +13,10 @@ # License for the specific language governing permissions and limitations # under the License. -import urllib from django.db import models -from django.forms import ModelForm, CharField, Textarea -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)) +from cfp.utils import validate_bp class Topic(models.Model): @@ -95,34 +69,3 @@ class Comment(models.Model): class Meta: ordering = ['posted_date'] - - -class CommentForm(ModelForm): - class Meta: - model = Comment - exclude = ('proposal', 'posted_date', 'author') - - -class ProposalForm(ModelForm): - class Meta: - model = Proposal - exclude = ('proposer', 'status', 'scheduled') - - -class ProposalEditForm(ModelForm): - class Meta: - model = Proposal - exclude = ('topic', 'proposer', 'status', 'scheduled') - - -class ProposalReviewForm(ModelForm): - comment = CharField(widget=Textarea) - class Meta: - model = Proposal - fields = ('status',) - - -class ProposalSwitchForm(ModelForm): - class Meta: - model = Proposal - fields = ('topic',) diff --git a/cfp/utils.py b/cfp/utils.py new file mode 100644 index 0000000..f5b3269 --- /dev/null +++ b/cfp/utils.py @@ -0,0 +1,61 @@ +# 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.core.exceptions import ValidationError + + +def is_editable(proposal, user): + return ((not proposal.scheduled) and + ((proposal.proposer == user and proposal.status != 'A') or + topiclead(user, proposal.topic))) + + +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 _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)) diff --git a/cfp/views.py b/cfp/views.py index 9772af4..89b744a 100644 --- a/cfp/views.py +++ b/cfp/views.py @@ -21,26 +21,11 @@ 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, Comment -from odsreg.cfp.models import ProposalForm, ProposalEditForm, CommentForm -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") +from odsreg.cfp.forms import ProposalForm, ProposalEditForm, CommentForm +from odsreg.cfp.forms import ProposalReviewForm, ProposalSwitchForm +from odsreg.cfp.utils import linkify, is_editable, topiclead @login_required @@ -58,7 +43,7 @@ def list(request): def topiclist(request, topicid): topic = Topic.objects.get(id=topicid) if not topiclead(request.user, topic): - return forbidden() + return HttpResponseForbidden("Forbidden") proposals = Proposal.objects.filter(topic=topicid) request.session['lastlist'] = "cfp/topic/%s" % topicid return render(request, "topiclist.html", @@ -89,12 +74,6 @@ def create(request): return render(request, 'cfpcreate.html', {'topics': topics, 'form': form}) -def is_editable(proposal, user): - return ((not proposal.scheduled) and - ((proposal.proposer == user and proposal.status != 'A') or - topiclead(user, proposal.topic))) - - @login_required def details(request, proposalid): proposal = Proposal.objects.get(id=proposalid) @@ -120,7 +99,7 @@ def details(request, proposalid): def edit(request, proposalid): proposal = Proposal.objects.get(id=proposalid) if not is_editable(proposal, request.user): - return forbidden() + return HttpResponseForbidden("Forbidden") if request.method == 'POST': form = ProposalEditForm(request.POST, instance=proposal) if form.is_valid(): @@ -136,7 +115,7 @@ def edit(request, proposalid): def delete(request, proposalid): proposal = Proposal.objects.get(id=proposalid) if ((proposal.proposer != request.user) or proposal.status in ['A', 'S']): - return forbidden() + return HttpResponseForbidden("Forbidden") if request.method == 'POST': proposal.delete() return HttpResponseRedirect('/%s' % request.session['lastlist']) @@ -148,7 +127,7 @@ 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() + return HttpResponseForbidden("Forbidden") if request.method == 'POST': form = ProposalSwitchForm(request.POST, instance=proposal) if form.is_valid(): @@ -166,9 +145,8 @@ def switch(request, proposalid): @login_required def review(request, proposalid): proposal = Proposal.objects.get(id=proposalid) - #TODO Allow comment while reviewing to be included in email if not topiclead(request.user, proposal.topic): - return forbidden() + return HttpResponseForbidden("Forbidden") current_status = proposal.status status_long = proposal.get_status_display() if request.method == 'POST': diff --git a/scheduling/forms.py b/scheduling/forms.py new file mode 100644 index 0000000..d2d33f3 --- /dev/null +++ b/scheduling/forms.py @@ -0,0 +1,24 @@ +# 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.forms import ModelForm + +from odsreg.scheduling.models import Slot + + +class SlotForm(ModelForm): + class Meta: + model = Slot + fields = ('title', 'description') diff --git a/scheduling/models.py b/scheduling/models.py index 818947b..50b6b8a 100644 --- a/scheduling/models.py +++ b/scheduling/models.py @@ -14,7 +14,7 @@ # under the License. from django.db import models -from django.forms import ModelForm + from odsreg.cfp.models import Proposal, Topic @@ -48,9 +48,3 @@ class Slot(models.Model): 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/utils.py b/scheduling/utils.py new file mode 100644 index 0000000..35fedbe --- /dev/null +++ b/scheduling/utils.py @@ -0,0 +1,67 @@ +# 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. + +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 full_description(slot): + desc = "" + if slot.description: + desc = slot.description + "\n\n" + desc += combined_description(slot) + return desc + + +def htmlize(desc): + return desc.replace('\n', '
') + + +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) diff --git a/scheduling/views.py b/scheduling/views.py index b1552b1..5315cdd 100644 --- a/scheduling/views.py +++ b/scheduling/views.py @@ -18,56 +18,21 @@ import urllib2 from django.shortcuts import render from django.conf import settings -from django.http import HttpResponseRedirect +from django.http import HttpResponseRedirect, HttpResponseForbidden 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 +from odsreg.cfp.utils import topiclead +from odsreg.scheduling.forms import SlotForm +from odsreg.scheduling.models import Slot +from odsreg.scheduling.utils import combined_id, combined_title +from odsreg.scheduling.utils import combined_description, full_description +from odsreg.scheduling.utils import htmlize, end_time def scheduling(request, topicid): topic = Topic.objects.get(id=topicid) if not topiclead(request.user, topic): - return forbidden() + return HttpResponseForbidden("Forbidden") if request.method == 'POST': action = request.POST['action'] proposal = Proposal.objects.get(id=request.POST['proposal']) @@ -97,25 +62,10 @@ def scheduling(request, topicid): '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() + return HttpResponseForbidden("Forbidden") list_calls = "" baseurl = "http://%s.sched.org/api/session/" % settings.SCHED_URL for slot in Slot.objects.filter(topic=topicid): @@ -147,7 +97,7 @@ def publish(request, topicid): def edit(request, slotid): slot = Slot.objects.get(id=slotid) if not topiclead(request.user, slot.topic): - return forbidden() + return HttpResponseForbidden("Forbidden") if request.method == 'POST': form = SlotForm(request.POST, instance=slot) if form.is_valid(): @@ -165,7 +115,7 @@ def edit(request, slotid): def swap(request, slotid): oldslot = Slot.objects.get(id=slotid) if not topiclead(request.user, oldslot.topic): - return forbidden() + return HttpResponseForbidden("Forbidden") if request.method == 'POST': newslotid = int(request.POST['newslotid']) newslot = Slot.objects.get(id=newslotid, topic=oldslot.topic)