blob: 155ece0ddd4f3543ed01b50c2aeb571114950684 [file] [log] [blame]
#!/usr/bin/env python2.5
# Copyright 2011 the Melange authors.
# 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
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# See the License for the specific language governing permissions and
# limitations under the License.
"""Views for the GCI Task view page.
import datetime
from google.appengine.ext import blobstore
from google.appengine.ext import db
from django import forms as django_forms
from django.forms.util import ErrorDict
from django.utils.translation import ugettext
from soc.logic import cleaning
from soc.logic.exceptions import RedirectRequest
from soc.views.helper import blobstore as bs_helper
from soc.views.template import Template
from soc.modules.gci.logic import comment as comment_logic
from soc.modules.gci.logic import profile as profile_logic
from soc.modules.gci.logic import task as task_logic
from soc.modules.gci.logic.helper import timeline as timeline_helper
from soc.modules.gci.models.comment import GCIComment
from soc.modules.gci.models.task import ACTIVE_CLAIMED_TASK
from soc.modules.gci.models.task import CLAIMABLE
from soc.modules.gci.models.task import SEND_FOR_REVIEW_ALLOWED
from soc.modules.gci.models.task import TASK_IN_PROGRESS
from soc.modules.gci.models.task import UNPUBLISHED
from soc.modules.gci.models.work_submission import GCIWorkSubmission
from soc.modules.gci.views import forms as gci_forms
from soc.modules.gci.views.base import RequestHandler
from soc.modules.gci.views.helper import url_patterns
from soc.modules.gci.views.helper.url_patterns import url
from soc.modules.gci.views.helper import url_names
'You are not allowed to operate the button named %s')
'You are not allowed to upload work')
DEF_NO_UPLOAD = ugettext(
'An error occurred, please upload a file.')
DEF_NO_URL = ugettext(
'An error occurred, please submit a valid URL.')
DEF_NO_WORK_FOUND = ugettext('No submission found with id %i')
'You are not allowed to delete this submission')
'Only a task that you own and that has submitted work can be send in '
'for review.')
class CommentForm(gci_forms.GCIModelForm):
"""Django form for the comment.
class Meta:
model = GCIComment
css_prefix = 'gci_comment'
fields = ['title', 'content']
def idSuffix(self, field):
if != 'content':
return ''
if not self.reply:
return ''
return "-%d" % self.reply
def __init__(self, reply, *args, **kwargs):
super(CommentForm, self).__init__(*args, **kwargs)
self.reply = reply
# For UI purposes we need to set this required, validation does not pick
# it up.
self.fields['title'].required = True
self.fields['content'].required = True
def clean_content(self):
content = cleaning.clean_html_content('content')(self)
if content:
return content
raise django_forms.ValidationError(
ugettext('Comment content cannot be empty.'), code='invalid')
def clean_title(self):
title = self.cleaned_data.get('title')
if not title:
raise django_forms.ValidationError(
ugettext('Comment title cannot be empty.'), code='invalid')
return title
class WorkSubmissionFileForm(gci_forms.GCIModelForm):
"""Django form for submitting work as file.
class Meta:
model = GCIWorkSubmission
css_prefix = 'gci_work_submission'
fields = ['upload_of_work']
upload_of_work = django_forms.FileField(
label='Upload work', required=False)
def addFileRequiredError(self):
"""Appends a form error message indicating that this field is required.
if not self._errors:
self._errors = ErrorDict()
self._errors["upload_of_work"] = self.error_class([DEF_NO_UPLOAD])
def clean_upload_of_work(self):
"""Ensure that file field has data.
cleaned_data = self.cleaned_data
upload = cleaned_data.get('upload_of_work')
# Although we need the ValidationError exception the message there
# is dummy because it won't pass through the Appengine's Blobstore
# API. We use the same error message when adding the form error.
# See self.addFileRequiredError method.
if not upload:
raise gci_forms.ValidationError(DEF_NO_UPLOAD)
return upload
class WorkSubmissionURLForm(gci_forms.GCIModelForm):
"""Django form for submitting work as URL.
class Meta:
model = GCIWorkSubmission
css_prefix = 'gci_work_submission'
fields = ['url_to_work']
def clean_url_to_work(self):
"""Ensure that at least one of the fields has data.
cleaned_data = self.cleaned_data
url = cleaned_data.get('url_to_work')
if not url:
raise gci_forms.ValidationError(DEF_NO_URL)
return url
class TaskViewPage(RequestHandler):
"""View for the GCI Task view page where all the actions happen.
def djangoURLPatterns(self):
"""URL pattern for this view.
return [
url(r'task/view/%s$' % url_patterns.TASK, self,
def checkAccess(self):
"""Checks whether this task is visible to the public and any other checks
if it is a POST request.
self.mutator.taskFromKwargs(comments=True, work_submissions=True) = self.check.isTaskVisible()
if task_logic.updateTaskStatus(
# The task logic updated the status of the task since the deadline passed
# and the GAE task was late to run. Reload the page.
raise RedirectRequest('')
if self.request.method == 'POST':
# Access checks for the different forms on this page. Note that there
# are no elif clauses because one could add multiple GET params :).
# Tasks for non-active organizations cannot be touched
if 'reply' in
# checks for posting comments
# valid tasks and profile are already checked.
if 'submit_work' in
if not task_logic.canSubmitWork(,
if 'button' in
# check for any of the buttons
button_name = self._buttonName()
buttons = {}
if not buttons.get(button_name): % button_name)
if 'send_for_review' in
if not task_logic.isOwnerOfTask(, or \
if 'delete_submission' in
id = self._submissionId()
work = GCIWorkSubmission.get_by_id(id,
if not work: %id)
time_expired = work.submitted_on -
if work.user.key() != or \
time_expired > task_logic.DELETE_EXPIRATION:
def jsonContext(self):
url = '%s?submit_work' %('gci_view_task'))
return {
'upload_link': blobstore.create_upload_url(url),
def context(self):
"""Returns the context for this view.
task =
context = {
'page_name': '%s - %s' %(task.title,,
'task': task,
'task_info': TaskInformation(,
if task.deadline:
context['complete_percentage'] = timeline_helper.completePercentage(
end=task.deadline, duration=(task.time_to_complete*3600))
context['work_submissions'] = WorkSubmissions(
context['comment_ids'] = [i.key().id() for i in]
context['comments'] = CommentsTemplate(
if not context['is_mentor']:
# Programmatically change css for non-mentors, to for instance show
# the open cog when a task can be claimed.
if task.status == 'Closed':
block_type = 'completed'
elif task_logic.isOwnerOfTask(task,
block_type = 'owned'
elif task.status in ACTIVE_CLAIMED_TASK:
block_type = 'claimed'
block_type = 'open'
context['block_task_type'] = block_type
return context
def post(self):
"""Handles all POST calls for the TaskViewPage.
if and 'reply' in
return self._postComment()
elif 'button' in
return self._postButton()
elif 'send_for_review' in
return self._postSendForReview()
elif 'delete_submission' in
return self._postDeleteSubmission()
elif 'work_file_submit' in or 'submit_work' in
return self._postSubmitWork()
def _postComment(self):
"""Handles the POST call for the form that creates comments.
reply ='reply', '')
reply = int(reply) if reply.isdigit() else None
comment_form = CommentForm(reply,
if not comment_form.is_valid():
return self.get()
comment_form.cleaned_data['reply'] = reply
comment_form.cleaned_data['created_by'] =
comment_form.cleaned_data['modified_by'] =
comment = comment_form.create(commit=False,
# TODO(ljvderijk): Indicate that a comment was successfully created to the
# user.'gci_view_task')
def _postButton(self):
"""Handles the POST call for any of the control buttons on the task page.
button_name = self._buttonName()
task =
task_key = task.key()
if button_name == 'button_unpublish':
task_logic.setTaskStatus(task.key(), 'Unpublished')
elif button_name == 'button_publish':
task_logic.setTaskStatus(task.key(), 'Open')
elif button_name == 'button_edit':
r ='gci_edit_task')
elif button_name == 'button_delete':
elif button_name == 'button_assign':
task_logic.assignTask(task, task.student,
elif button_name == 'button_unassign':
elif button_name == 'button_close':
elif button_name == 'button_needs_work':
elif button_name == 'button_extend_deadline':
hours ='hours', '')
hours = int(hours) if hours.isdigit() else 0
if hours > 0:
delta = datetime.timedelta(hours=hours)
task_logic.extendDeadline(task, delta,
elif button_name == 'button_claim':
elif button_name == 'button_unclaim':
elif button_name == 'button_subscribe':
profile_key =
def txn():
task = db.get(task_key)
if profile_key not in task.subscribers:
elif button_name == 'button_unsubscribe':
profile_key =
def txn():
task = db.get(task_key)
if profile_key in task.subscribers:
def _buttonName(self):
"""Returns the name of the button specified in the POST dict.
for key in
if key.startswith('button'):
return key
return None
def _postSubmitWork(self):
"""POST handler for the work submission form.
if 'url_to_work' in
form = WorkSubmissionURLForm(
if not form.is_valid():
return self.get()
form = WorkSubmissionFileForm(,
if not form.is_valid():
# we are not storing this form, remove the uploaded blob from the cloud
for f in
return'gci_view_task', extra=['file=0'])
logging.warning('Neither the URL nor the files were provided for work '
return'gci_view_task', extra=['ws_error=1'])
task =
# TODO(ljvderijk): Add a non-required profile property?
form.cleaned_data['user'] =
form.cleaned_data['org'] =
form.cleaned_data['program'] = task.program
# store the submission, parented by the task
def _postSendForReview(self):
"""POST handler for the mark as complete button.
def _postDeleteSubmission(self):
"""POST handler to delete a GCIWorkSubmission.
id = self._submissionId()
work = GCIWorkSubmission.get_by_id(id,
if not work:
return self.error(400, DEF_NO_WORK_FOUND %id)
# Deletion of blobs always runs separately from transaction so it has no
# added value to use it here.
upload = work.upload_of_work
if upload:
def _submissionId(self):
"""Retrieves the submission id from the POST data.
for key in
if key.isdigit():
return int(key)
return -1
def templatePath(self):
return 'v2/modules/gci/task/public.html'
class TaskInformation(Template):
"""Template that contains the details of a task.
def context(self):
"""Returns the context for the current template.
task =
mentors = [m.public_name for m in db.get(task.mentors)]
profile =
# We count everyone from the org as a mentor, the mentors property
# is just who best to contact about this task
context = {
'task': task,
'mentors': mentors,
'is_task_mentor': profile.key() in task.mentors if profile else None,
'is_owner': task_logic.isOwnerOfTask(task,,
'is_claimed': task.status in ACTIVE_CLAIMED_TASK,
if task.deadline:
rdays, rhrs, rmins = timeline_helper.remainingTimeSplit(task.deadline)
context['remaining_days'] = rdays
context['remaining_hours'] = rhrs
context['remaining_minutes'] = rmins
return context
def setButtonControls(self, context):
"""Enables buttons on the TaskInformation block based on status and the
context: Context dictionary which to write to.
profile =
if not profile:
# no buttons for someone without a profile
# no buttons after all reviews has stopped
task =
is_org_admin =
is_mentor =
is_student =
is_owner = task_logic.isOwnerOfTask(task, profile)
if is_org_admin:
can_unpublish = (task.status in CLAIMABLE) and not task.student
context['button_unpublish'] = can_unpublish
can_publish = task.status in UNPUBLISHED
context['button_publish'] = can_publish
context['button_delete'] = not task.student
if is_mentor:
context['button_edit'] = task.status in \
context['button_assign'] = task.status == 'ClaimRequested'
context['button_unassign'] = task.status in ACTIVE_CLAIMED_TASK
context['button_close'] = task.status == 'NeedsReview'
context['button_needs_work'] = task.status == 'NeedsReview'
context['button_extend_deadline'] = task.status in TASK_IN_PROGRESS
if is_student:
if not
if not profile_logic.hasStudentFormsUploaded(profile.student_info):
context['student_forms_link'] =
# TODO(lennie): Separate the access check out in to different
# methods and add a context variable to show separate messages.
context['button_claim'] = task_logic.canClaimRequestTask(
task, profile)
if is_owner:
if not
context['button_unclaim'] = task.status in ACTIVE_CLAIMED_TASK
if task.status != 'Closed':
context['button_subscribe'] = not profile.key() in task.subscribers
context['button_unsubscribe'] = profile.key() in task.subscribers
def templatePath(self):
"""Returns the path to the template that should be used in render().
return 'v2/modules/gci/task/_task_information.html'
class WorkSubmissions(Template):
"""Template to render all the GCIWorkSubmissions.
Contains the form to upload work and contains the "Mark task as complete"
button for students.
def _buildWorkSubmissionContext(self):
"""Builds a list containing the info related to each work submission.
submissions = []
source =
for submission in sorted(source, key=lambda e: e.submitted_on):
submission_info = {
'entity': submission
upload_of_work = submission.upload_of_work
submission_info['upload_of_work'] = upload_of_work
if upload_of_work:
uploaded_blob = blobstore.BlobInfo.get(upload_of_work.key())
submission_info['is_blob_valid'] = True if uploaded_blob else False
return submissions
def context(self):
"""Returns the context for the current template.
context = {
'submissions': self._buildWorkSubmissionContext(),
task =
is_owner = task_logic.isOwnerOfTask(task,
if is_owner:
context['send_for_review'] = and \
deleteable = []
for work in
if work.user.key() ==
# Ensure that it is the work from the current user in case the task
# got re-assigned.
time_expired = work.submitted_on -
if time_expired < task_logic.DELETE_EXPIRATION:
context['deleteable'] = deleteable
if task_logic.canSubmitWork(task,
if and 'submit_work' in
# File form doesn't have any POST parameters so it should not be
# passed while reconstructing the form. So only URL form is
# constructed from POST data
context['work_url_form'] = WorkSubmissionURLForm(
context['work_url_form'] = WorkSubmissionURLForm()
# As mentioned in the comment above since there is no POST data to
# be passed to the file form, it is constructed in the same way
# in either cases.
context['work_file_form'] = WorkSubmissionFileForm()
if'file', None) == '0':
if'ws_error', None) == '1':
context['ws_error'] = True
url = '%s?submit_work' %('gci_view_task'))
context['direct_post_url'] = url
return context
def templatePath(self):
"""Returns the path to the template that should be used in render().
return 'v2/modules/gci/task/_work_submissions.html'
class CommentsTemplate(Template):
"""Template for rendering and adding comments.
class CommentItem(object):
def __init__(self, entity, form, author_link):
self.entity = entity
self.form = form
self.author_link = author_link
def context(self):
"""Returns the context for the current template.
comments = []
reply ='reply')
for comment in
# generate Reply form, if needed
form = None
if self._commentingAllowed():
comment_id = comment.key().id()
if and reply == str(comment_id):
form = CommentForm(comment_id,
form = CommentForm(comment_id)
# generate author link, if comment sent by a student
author_link = None
author = comment.created_by
if author:
profile = profile_logic.queryProfileForUserAndProgram(
if profile and profile.is_student:
author_link =
item = self.CommentItem(comment, form, author_link)
context = {
'comments': comments,
.urlOf('create_gci_profile', secure=True),
if self._commentingAllowed():
if and reply == 'self':
context['comment_form'] = CommentForm(None,
context['comment_form'] = CommentForm(None)
return context
def _commentingAllowed(self):
"""Returns true iff the comments are allowed to be posted at this time.
return not or (
not and
def templatePath(self):
"""Returns the path to the template that should be used in render().
return 'v2/modules/gci/task/_comments.html'
class WorkSubmissionDownload(RequestHandler):
"""Request handler for downloading blobs from a GCIWorkSubmission.
def djangoURLPatterns(self):
"""URL pattern for this view.
return [
url(r'work/download/%s$' % url_patterns.TASK, self,
def checkAccess(self):
"""Checks whether this task is visible to the public.
def get(self):
"""Attempts to download the blob in the worksubmission that is specified
in the GET argument.
id_s = self.request.GET.get('id', '')
id = int(id_s) if id_s.isdigit() else -1
work = GCIWorkSubmission.get_by_id(id,
if not work or not work.upload_of_work:
return self.error(400, DEF_NO_WORK_FOUND %id)
upload = work.upload_of_work
self.response = bs_helper.sendBlob(upload)