diff -r 2be8f6b8379e -r f064654837f7 app/soc/modules/ghop/views/models/task.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/soc/modules/ghop/views/models/task.py Tue Sep 08 20:54:19 2009 +0200 @@ -0,0 +1,1421 @@ +#!/usr/bin/python2.5 +# +# Copyright 2009 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 +# +# 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. + +"""Views for Tasks. +""" + +__authors__ = [ + '"Madhusudan.C.S" ' + ] + + +import datetime +import time + +from google.appengine.ext import db + +from django import forms +from django import http +from django.utils.translation import ugettext + +from soc.logic import cleaning +from soc.logic import dicts +from soc.logic.models import host as host_logic +from soc.logic.models import student as student_logic +from soc.logic.models import user as user_logic +from soc.views import helper +from soc.views import out_of_band +from soc.views.helper import decorators +from soc.views.helper import dynaform +from soc.views.helper import lists +from soc.views.helper import params as params_helper +from soc.views.helper import requests +from soc.views.helper import redirects +from soc.views.helper import responses +from soc.views.helper import widgets +from soc.views.models import base + +from soc.modules.ghop.logic import cleaning as ghop_cleaning +from soc.modules.ghop.logic.models import comment as ghop_comment_logic +from soc.modules.ghop.logic.models import mentor as ghop_mentor_logic +from soc.modules.ghop.logic.models import organization as ghop_org_logic +from soc.modules.ghop.logic.models import org_admin as ghop_org_admin_logic +from soc.modules.ghop.logic.models import program as ghop_program_logic +from soc.modules.ghop.logic.models import task as ghop_task_logic +from soc.modules.ghop.models import task as ghop_task_model +from soc.modules.ghop.views.helper import access +from soc.modules.ghop.views.helper import redirects as ghop_redirects +from soc.modules.ghop.views.models import organization as ghop_org_view +from soc.modules.ghop.tasks import task_update + +import soc.modules.ghop.logic.models.task + + +class View(base.View): + """View methods for the Tasks. + """ + + DEF_AWAITING_REG_MSG = ugettext( + 'The task is open but you cannot claim this task since you ' + 'have not completed the student signup after finishing your ' + 'first task.') + + DEF_CAN_EDIT_TASK_MSG = ugettext( + 'The task can be edited by clicking on the edit link ' + 'next to the title above.') + + DEF_MAX_TASK_LIMIT_MSG_FMT = ugettext( + 'The task is open but you cannot claim this task since you ' + 'either have already claimed %d task.') + + DEF_SIGNIN_TO_COMMENT_MSG = ugettext( + 'Sign in to perform any action or comment on ' + 'this task.') + + DEF_STUDENT_SIGNUP_MSG = ugettext( + 'You have successfully completed this task. Sign up as a student ' + 'before you proceed further.') + + DEF_TASK_CLAIMED_BY_YOU_MSG = ugettext( + 'This task has been claimed by you!') + + DEF_TASK_CLAIMED_BY_STUDENT_MSG = ugettext( + 'This task has been claimed by a student!') + + DEF_TASK_CLAIMED_MSG = ugettext( + 'The task is already claimed and the work is in progress.') + + DEF_TASK_CLAIM_REQUESTED_MSG = ugettext( + 'A student has requested to claim this task. Accept or ' + 'Reject the request.') + + DEF_TASK_CLOSED_MSG = ugettext('The task is closed.') + + DEF_TASK_CMPLTD_BY_YOU_MSG = ugettext( + 'You have successfully completed this task!') + + DEF_TASK_NO_MORE_SUBMIT_MSG = ugettext( + 'You have submitted the work to this task, but deadline has passed ' + 'You cannot submit any more work until your mentor extends the ' + 'deadline.') + + DEF_TASK_MENTOR_REOPENED_MSG = ugettext( + 'The task has been reopened.') + + DEF_TASK_NEEDS_REVIEW_MSG = ugettext( + 'Student has submitted his work for this task! It needs review.') + + DEF_TASK_OPEN_MSG = ugettext( + 'This task is open. If you are GHOP student, you can claim it!') + + DEF_TASK_REOPENED_MSG = ugettext( + 'This task has been reopened. If you are a GHOP student, ' + 'you can clam it!') + + DEF_TASK_REQ_CLAIMED_BY_YOU_MSG = ugettext( + 'You have requested to claim this task!') + + DEF_TASK_UNPUBLISHED_MSG = ugettext( + 'The task is not yet published. It can be edited by clicking on ' + 'the edit button below.') + + DEF_WS_MSG_FMT = ugettext( + '(To see the work submitted click here.)') + + def __init__(self, params=None): + """Defines the fields and methods required for the task View class + to provide the user with the necessary views. + + Params: + params: a dict with params for this View + """ + + rights = access.GHOPChecker(params) + # TODO: create and suggest_task don't need access checks which use state + # also feels like roles are being checked twice? + rights['create'] = [ + ('checkCanOrgAdminOrMentorEdit', ['scope_path', True]), + ('checkRoleAndStatusForTask', + [['ghop/org_admin'], ['active'], + []])] + rights['edit'] = [ + ('checkCanOrgAdminOrMentorEdit', ['scope_path', False]), + ('checkRoleAndStatusForTask', + [['ghop/org_admin'], ['active'], + ['Unapproved', 'Unpublished', 'Open']])] + rights['delete'] = ['checkIsDeveloper'] + rights['show'] = ['checkStatusForTask'] + rights['list_org_tasks'] = [ + ('checkCanOrgAdminOrMentorEdit', ['scope_path', False])] + rights['suggest_task'] = [ + ('checkCanOrgAdminOrMentorEdit', ['scope_path', True]), + ('checkRoleAndStatusForTask', + [['ghop/org_admin', 'ghop/mentor'], ['active'], + []])] + rights['search'] = ['allow'] + + new_params = {} + new_params['logic'] = soc.modules.ghop.logic.models.task.logic + new_params['rights'] = rights + + new_params['name'] = "Task" + new_params['module_name'] = "task" + new_params['sidebar_grouping'] = 'Tasks' + + new_params['module_package'] = 'soc.modules.ghop.views.models' + new_params['url_name'] = 'ghop/task' + + new_params['scope_view'] = ghop_org_view + new_params['scope_redirect'] = redirects.getCreateRedirect + + new_params['list_heading'] = 'modules/ghop/task/list/heading.html' + new_params['list_row'] = 'modules/ghop/task/list/row.html' + + new_params['extra_dynaexclude'] = ['task_type', 'mentors', 'user', + 'student', 'program', 'status', + 'deadline', 'created_by', + 'created_on', 'modified_by', + 'modified_on', 'history', + 'link_id', 'difficulty'] + + patterns = [] + patterns += [ + (r'^%(url_name)s/(?Psuggest_task)/%(scope)s$', + '%(module_package)s.%(module_name)s.suggest_task', + 'Mentors suggest %(name)s'), + (r'^%(url_name)s/(?Psuggest_task)/%(key_fields)s$', + '%(module_package)s.%(module_name)s.suggest_task', + 'Mentors edit a %(name)s'), + (r'^%(url_name)s/(?Plist_org_tasks)/%(scope)s$', + '%(module_package)s.%(module_name)s.list_org_tasks', + 'List Organization %(name)s'), + (r'^%(url_name)s/(?Psearch)/' + '(?P%(ulnp)s)/%(lnp)s$', + '%(module_package)s.%(module_name)s.search', + 'Search for %(name)ss'), + ] + + new_params['extra_django_patterns'] = patterns + + new_params['create_dynafields'] = [ + {'name': 'arbit_tags', + 'base': forms.fields.CharField, + 'label': 'Arbitrary Tags', + 'required': False, + 'group': ugettext('Tags'), + 'help_text': ugettext( + 'Enter arbitrary Tags for this task separated by comma.\n' + 'Note: Tag names are case sensitive. If the tag is same ' + 'as the program mandated tag, it will be considered as a ' + 'mandatory tag.') + }, + ] + + new_params['create_extra_dynaproperties'] = { + 'description': forms.fields.CharField(required=True, + widget=widgets.FullTinyMCE(attrs={'rows': 25, 'cols': 100})), + 'scope_path': forms.CharField(widget=forms.HiddenInput, + required=True), + 'time_to_complete': forms.IntegerField(min_value=1, + required=True), + 'clean_description': cleaning.clean_html_content('description'), + 'clean_arbit_tags': cleaning.str2set('arbit_tags'), + } + + new_params['edit_extra_dynaproperties'] = { + 'link_id': forms.CharField(widget=forms.HiddenInput) + } + + new_params['public_template'] = 'modules/ghop/task/public.html' + + params = dicts.merge(params, new_params, sub_merge=True) + + super(View, self).__init__(params=params) + + # holds the base form for the task creation and editing + self._params['base_create_form'] = self._params['create_form'] + self._params['base_edit_form'] = self._params['edit_form'] + + # extend create and edit form for org admins + dynafields = [ + {'name': 'mentors_list', + 'required': True, + 'base': forms.fields.CharField, + 'label': 'Assign mentors', + 'help_text': 'Assign mentors to this task by ' + 'giving their link_ids separated by comma.', + }, + {'name': 'published', + 'required': False, + 'initial': False, + 'base': forms.fields.BooleanField, + 'label': 'Publish the task', + 'help_text': ugettext('By ticking this box, the task will be' + 'made public and can be claimed by students.'), + } + ] + + dynaproperties = params_helper.getDynaFields(dynafields) + + dynaproperties['clean_mentors_list'] = ghop_cleaning.cleanMentorsList( + 'mentors_list') + + create_form = dynaform.extendDynaForm( + dynaform=self._params['create_form'], + dynaproperties=dynaproperties) + + self._params['create_form'] = create_form + + edit_form = dynaform.extendDynaForm( + dynaform=self._params['edit_form'], + dynaproperties=dynaproperties) + + self._params['edit_form'] = edit_form + + # create the comment form + dynafields = [ + {'name': 'comment', + 'base': forms.CharField, + 'widget': widgets.FullTinyMCE(attrs={'rows': 10, 'cols': 40}), + 'label': 'Comment', + 'required': False, + 'example_text': 'Comment is optional.
' + 'Choose an appropriate Action below and Save your ' + 'changes.

Caution, you will not be able ' + 'to edit your comment!', + }, + ] + + dynaproperties = params_helper.getDynaFields(dynafields) + dynaproperties['clean_comment'] = cleaning.clean_html_content('comment') + dynaproperties['clean'] = ghop_cleaning.clean_comment( + 'comment', 'action', 'work_submission') + + comment_form = dynaform.newDynaForm(dynamodel=None, + dynabase=helper.forms.BaseForm, dynainclude=None, + dynaexclude=None, dynaproperties=dynaproperties) + self._params['comment_form'] = comment_form + + def _getTagsForProgram(self, form_name, params, **kwargs): + """Extends form dynamically from difficulty levels in program entity. + + Args: + form_name: the Form entry in params to extend + params: the params for the view + """ + + # obtain program_entity using scope_path which holds + # the org_entity key_name + org_entity = ghop_org_logic.logic.getFromKeyName(kwargs['scope_path']) + program_entity = ghop_program_logic.logic.getFromKeyName( + org_entity.scope_path) + + # get a list difficulty levels stored for the program entity + tds = ghop_task_model.TaskDifficultyTag.get_by_scope( + program_entity) + + difficulties = [] + for td in tds: + difficulties.append((td.tag, td.tag)) + + # get a list of task type tags stored for the program entity + tts = ghop_task_model.TaskTypeTag.get_by_scope(program_entity) + + type_tags = [] + for tt in tts: + type_tags.append((tt.tag, tt.tag)) + + # create the difficultly level field containing the choices + # defined in the program entity + dynafields = [ + {'name': 'difficulty', + 'base': forms.ChoiceField, + 'label': 'Difficulty level', + 'required': True, + 'passthrough': ['initial', 'required', 'choices', + 'label', 'help_text'], + 'choices': difficulties, + 'group': ugettext('Tags'), + 'help_text': ugettext('Difficulty Level of this task.'), + }, + {'name': 'type_tags', + 'base': forms.MultipleChoiceField, + 'label': 'Task Types', + 'required': True, + 'passthrough': ['initial', 'required', 'choices', + 'label', 'help_text'], + 'choices': type_tags, + 'group': ugettext('Tags'), + 'help_text': ugettext('Task Type tags mandated by the program. You ' + 'must select one or more of them.'), + }, + ] + + dynaproperties = params_helper.getDynaFields(dynafields) + + extended_form = dynaform.extendDynaForm( + dynaform=params[form_name], + dynaproperties=dynaproperties) + + return extended_form + + @decorators.check_access + def create(self, request, access_type, + page_name=None, params=None, **kwargs): + """Replaces the create Form with the dynamic one. + + For args see base.View.create(). + """ + + params = dicts.merge(params, self._params) + + # extend create_form to include difficulty levels + params['create_form'] = self._getTagsForProgram( + 'create_form', params, **kwargs) + + return super(View, self).create(request, 'allow', page_name=page_name, + params=params, **kwargs) + + @decorators.merge_params + @decorators.check_access + def edit(self, request, access_type, + page_name=None, params=None, seed=None, **kwargs): + """See base.View.edit(). + """ + + logic = params['logic'] + + context = helper.responses.getUniversalContext(request) + helper.responses.useJavaScript(context, params['js_uses_all']) + context['page_name'] = page_name + + try: + entity = logic.getFromKeyFieldsOr404(kwargs) + except out_of_band.Error, error: + msg = self.DEF_CREATE_NEW_ENTITY_MSG_FMT % { + 'entity_type_lower' : params['name'].lower(), + 'entity_type' : params['name'], + 'create' : params['missing_redirect'] + } + error.message_fmt = error.message_fmt + msg + return helper.responses.errorResponse( + error, request, context=context) + + user_account = user_logic.logic.getForCurrentAccount() + + filter = { + 'user': user_account, + } + + # extend edit_form to include difficulty levels + params['edit_form'] = self._getTagsForProgram( + 'edit_form', params, **kwargs) + + org_admin_entities = ghop_org_admin_logic.logic.getForFields(filter) + + if entity and entity.status == 'Unapproved': + approval_req = True + for org_admin_entity in org_admin_entities: + if org_admin_entity.key().name() == entity.created_by.key().name(): + approval_req = False + break + + if approval_req: + dynafields = [ + {'name': 'approved', + 'required': False, + 'initial': False, + 'base': forms.fields.BooleanField, + 'label': 'Approve the task', + 'help_text': 'By ticking this box, the task will be' + 'will be approved for publishing.', + } + ] + + dynaproperties = params_helper.getDynaFields(dynafields) + + edit_form = dynaform.extendDynaForm( + dynaform=params['edit_form'], + dynaproperties=dynaproperties) + + params['edit_form'] = edit_form + + if request.method == 'POST': + return self.editPost(request, entity, context, params=params) + else: + return self.editGet(request, entity, context, params=params) + + def _editGet(self, request, entity, form): + """See base.View._editGet(). + """ + + if entity.task_type: + form.fields['type_tags'].initial = entity.tags_string( + entity.task_type, ret_list=True) + if entity.arbit_tag: + form.fields['arbit_tags'].initial = entity.tags_string( + entity.arbit_tag) + + if entity.difficulty: + form.fields['difficulty'].initial = entity.tags_string( + entity.difficulty) + + if entity.mentors and 'mentors_list' in form.fields: + mentor_entities = db.get(entity.mentors) + mentors_list = [] + for mentor in mentor_entities: + mentors_list.append(mentor.link_id) + form.fields['mentors_list'].initial = ', '.join(mentors_list) + + form.fields['link_id'].initial = entity.link_id + + # checks if the task is already approved or not and sets + # the form approved field + if 'approved' in form.fields: + if entity.status == 'Unapproved': + form.fields['approved'].initial = False + else: + form.fields['approved'].initial = True + + # checks if the task is already published or not and sets + # the form published field + if 'published' in form.fields: + if entity.status == 'Unapproved' or entity.status == 'Unpublished': + form.fields['published'].initial = False + else: + form.fields['published'].initial = True + + return super(View, self)._editGet(request, entity, form) + + def _editPost(self, request, entity, fields): + """See base._editPost(). + """ + + super(View, self)._editPost(request, entity, fields) + + # TODO: this method can be made more clear, it seems a bit of a mess + + if not entity: + program_entity = fields['scope'].scope + fields['program'] = program_entity + else: + program_entity = entity.program + + user_account = user_logic.logic.getForCurrentAccount() + + filter = { + 'user': user_account, + 'scope': fields['scope'], + 'status': 'active' + } + + role_entity = ghop_org_admin_logic.logic.getForFields( + filter, unique=True) + + if role_entity: + # this user can publish/approve the task + if fields.get('approved') and fields.get('published'): + fields['status'] = 'Open' + elif not fields.get('approved'): + fields['status'] = 'Unapproved' + else: + fields['status'] = 'Unpublished' + + fields['mentors'] = [] + if fields.get('mentors_list'): + + for mentor_link_id in fields['mentors_list']: + properties = { + 'scope_path': fields['scope_path'], + 'link_id': mentor_link_id, + } + + mentor_entity = ghop_mentor_logic.logic.getFromKeyFields(properties) + fields['mentors'].append(mentor_entity.key()) + else: + role_entity = ghop_mentor_logic.logic.getForFields( + filter, unique=True) + if not entity: + # creating a new task + fields['status'] = 'Unapproved' + + # explicitly change the last_modified_on since the content has been edited + fields['modified_on'] = datetime.datetime.now() + + if not entity: + fields['link_id'] = 't%i' % (int(time.time()*100)) + fields['modified_by'] = role_entity + fields['created_by'] = role_entity + fields['created_on'] = datetime.datetime.now() + else: + fields['link_id'] = entity.link_id + fields['modified_by'] = role_entity + if 'status' not in fields: + fields['status'] = entity.status + + fields['difficulty'] = { + 'tags': fields['difficulty'], + 'scope': program_entity + } + + fields['task_type'] = { + 'tags': fields['type_tags'], + 'scope': program_entity + } + + fields['arbit_tag'] = { + 'tags': fields['arbit_tags'], + 'scope': program_entity + } + + return + + @decorators.merge_params + @decorators.check_access + def suggestTask(self, request, access_type, page_name=None, + params=None, **kwargs): + """View used to allow mentors to create or edit a task. + + Tasks created by mentors must be approved by org admins + before they are published. + """ + + params = dicts.merge(params, self._params) + + if 'link_id' in kwargs: + # extend create_form to include difficulty levels + params['mentor_form'] = self._getTagsForProgram( + 'base_edit_form', params, **kwargs) + try: + entity = self._logic.getFromKeyFieldsOr404(kwargs) + except out_of_band.Error, error: + return helper.responses.errorResponse( + error, request, template=params['error_public']) + else: + # extend create_form to include difficulty levels + params['mentor_form'] = self._getTagsForProgram( + 'base_create_form', params, **kwargs) + entity = None + + context = helper.responses.getUniversalContext(request) + helper.responses.useJavaScript(context, params['js_uses_all']) + context['page_name'] = page_name + + if request.method == 'POST': + return self.suggestTaskPost(request, context, + params, entity) + else: + return self.suggestTaskGet(request, context, + params, entity, **kwargs) + + def suggestTaskPost(self, request, context, params, entity): + """Handles the POST request for the suggest task view. + """ + + form = params['mentor_form'](request.POST) + + if not form.is_valid(): + return self._constructResponse(request, None, context, form, params) + + _, fields = helper.forms.collectCleanedFields(form) + # TODO: this non-standard view shouldn't call _editPost but its own method + self._editPost(request, entity, fields) + + logic = params['logic'] + if entity: + entity = logic.updateEntityProperties(entity, fields) + else: + entity = logic.updateOrCreateFromFields(fields) + + page_params = params['edit_params'] + + redirect = ghop_redirects.getSuggestTaskRedirect( + entity, params) + + return http.HttpResponseRedirect(redirect) + + def suggestTaskGet(self, request, context, params, entity, **kwargs): + """Handles the GET request for the suggest task view. + """ + + if entity: + # populate form with the existing entity + form = params['mentor_form'](instance=entity) + + self._editGet(request, entity, form) + else: + form = params['mentor_form']() + + return self._constructResponse(request, entity, context, form, params) + + @decorators.merge_params + @decorators.check_access + def listOrgTasks(self, request, access_type, + page_name=None, params=None, **kwargs): + """See base.View.list() + """ + if request.method == 'POST': + return self.listOrgTasksPost(request, params, **kwargs) + else: # request.method == 'GET' + return self.listOrgTasksGet(request, page_name, params, **kwargs) + + def listOrgTasksPost(self, request, params, **kwargs): + """Handles the POST request for the list tasks view. + """ + + # update the status of task entities that have been approved + # and published + task_entities = [] + for key_name, published in request.POST.items(): + task_entity = ghop_task_logic.logic.getFromKeyName(key_name) + if task_entity: + task_entity.status = 'Open' + + task_entities.append(task_entity) + + # bulk update the task_entities + # TODO: Have to be replaced by Task Queue APIs later + db.put(task_entities) + + # redirect to the same page + return http.HttpResponseRedirect('') + + def listOrgTasksGet(self, request, page_name, params, **kwargs): + """Handles the GET request for the list tasks view. + """ + + from soc.modules.ghop.views.helper import list_info as list_info_helper + + org_entity = ghop_org_logic.logic.getFromKeyNameOr404( + kwargs['scope_path']) + + contents = [] + context = {} + + user_account = user_logic.logic.getForCurrentAccount() + + fields = { + 'user': user_account, + 'scope': org_entity, + 'status': 'active' + } + + up_params = params.copy() + # give the capability to approve tasks for the org_admins + if ghop_org_admin_logic.logic.getForFields(fields, unique=True): + up_params['list_template'] = 'modules/ghop/task/approve/approve.html' + up_params['list_heading'] = 'modules/ghop/task/approve/heading.html' + up_params['list_row'] = 'modules/ghop/task/approve/row.html' + + up_params['list_action'] = (redirects.getPublicRedirect, + up_params) + + up_params['list_description'] = ugettext( + 'List of Unapproved or Unpublished tasks') + + filter = { + 'scope': org_entity, + 'status': ['Unapproved', 'Unpublished'], + } + + up_list = lists.getListContent(request, up_params, filter, + order=order, idx=0, + need_content=True) + + if up_list: + up_mentors_list = {} + for task_entity in up_list['data']: + up_mentors_list[task_entity.key()] = db.get(task_entity.mentors) + + up_list['info'] = (list_info_helper.getTasksInfo(up_mentors_list), None) + + contents.append(up_list) + context['up_list'] = True + + ap_params = up_params.copy() + ap_params['list_template'] = 'soc/models/list.html' + ap_params['list_heading'] = 'modules/ghop/task/list/heading.html' + ap_params['list_row'] = 'modules/ghop/task/list/row.html' + + ap_params['list_action'] = (redirects.getPublicRedirect, + ap_params) + + ap_params['list_description'] = ugettext( + 'List of published tasks') + + filter = { + 'scope': org_entity, + 'status': ['Open', 'Reopened', 'ClaimRequested', 'Claimed', + 'ActionNeeded', 'Closed', 'AwaitingRegistration', + 'NeedsWork', 'NeedsReview'], + } + + ap_list = lists.getListContent(request, ap_params, filter, + order=order, idx=1, + need_content=True) + + if ap_list: + ap_mentors_list = {} + for task_entity in ap_list['data']: + ap_mentors_list[task_entity.key()] = db.get(task_entity.mentors) + + ap_list['info'] = (list_info_helper.getTasksInfo(ap_mentors_list), None) + + contents.append(ap_list) + + # call the _list method from base to display the list + return self._list(request, up_params, contents, page_name, context) + + @decorators.merge_params + @decorators.check_access + def public(self, request, access_type, + page_name=None, params=None, **kwargs): + """See base.View.public(). + """ + + # create default template context for use with any templates + context = helper.responses.getUniversalContext(request) + helper.responses.useJavaScript(context, params['js_uses_all']) + context['page_name'] = page_name + entity = None + logic = params['logic'] + + try: + entity, comment_entities, ws_entities = ( + logic.getFromKeyFieldsWithCWSOr404(kwargs)) + except out_of_band.Error, error: + return helper.responses.errorResponse( + error, request, template=params['error_public'], context=context) + + if entity.status in ['Claimed', 'NeedsReview', + 'ActionNeeded', 'NeedsWork']: + entity, comment_entity, ws_entity = ( + ghop_task_logic.logic.updateTaskStatus(entity)) + if comment_entity: + comment_entities.append(comment_entity) + if ws_entity: + ws_entities.append(ws_entity) + + context['entity'] = entity + context['entity_key_name'] = entity.key().id_or_name() + context['entity_type'] = params['name'] + context['entity_type_url'] = params['url_name'] + + user_account = user_logic.logic.getForCurrentAccount() + + # get some entity specific context + self.updatePublicContext(context, entity, comment_entities, + ws_entities, user_account, params) + + validation = self._constructActionsList(context, entity, + user_account, params) + + context = dicts.merge(params['context'], context) + + if request.method == 'POST': + return self.publicPost(request, context, params, entity, + user_account, validation, **kwargs) + else: # request.method == 'GET' + return self.publicGet(request, context, params, entity, + user_account, **kwargs) + + def publicPost(self, request, context, params, entity, + user_account=None, validation=None, **kwargs): + """Handles the POST request for the entity's public page. + + Args: + entity: the task entity + rest: see base.View.public() + """ + + from soc.modules.ghop.logic.models import student as ghop_student_logic + + form = params['comment_form'](request.POST) + + if not form.is_valid(): + template = params['public_template'] + context['comment_form'] = form + return self._constructResponse(request, entity, context, + form, params, template=template) + + _, fields = helper.forms.collectCleanedFields(form) + + changes = [] + + action = fields['action'] + + properties = None + ws_properties = None + + # TODO: this can be separated into several methods that handle the changes + if validation == 'claim_request' and action == 'request': + deadline = datetime.datetime.now() + datetime.timedelta( + hours=entity.time_to_complete) + + properties = { + 'status': 'ClaimRequested', + 'user': user_account, + 'deadline': deadline, + } + + st_filter = { + 'user': user_account, + 'scope': entity.program, + 'status': 'active' + } + student_entity = ghop_student_logic.logic.getForFields( + st_filter, unique=True) + + if student_entity: + properties['student'] = student_entity + + changes.extend([ugettext('User-Student'), + ugettext('Action-Claim Requested'), + ugettext('Status-%s' % (properties['status'])) + ]) + elif (validation == 'claim_withdraw' or + validation == 'needs_review') and action == 'withdraw': + properties = { + 'user': None, + 'student': None, + 'status': 'Reopened', + 'deadline': None, + } + + changes.extend([ugettext('User-Student'), + ugettext('Action-Withdrawn'), + ugettext('Status-%s' % (properties['status'])) + ]) + elif validation == 'needs_review' and action == 'needs_review': + properties = { + 'status': 'NeedsReview', + } + + changes.extend([ + ugettext('User-Student'), + ugettext('Action-Submitted work and Requested for review'), + ugettext('Status-%s' % (properties['status']))]) + + ws_properties = { + 'parent': entity, + 'link_id': 't%i' % (int(time.time()*100)), + 'scope_path': entity.key().name(), + 'scope': entity.scope, + 'user': user_account, + 'information': fields['comment'], + 'url_to_work': fields['work_submission'], + 'submitted_on': datetime.datetime.now(), + } + elif validation == 'accept_claim': + if action == 'accept': + deadline = datetime.datetime.now() + datetime.timedelta( + hours=entity.time_to_complete) + + properties = { + 'status': 'Claimed', + 'deadline': deadline, + } + + changes.extend([ugettext('User-Mentor'), + ugettext('Action-Claim Accepted'), + ugettext('Status-%s' % (properties['status'])) + ]) + + task_update.spawnUpdateTask(entity) + if action == 'reject': + properties = { + 'user': None, + 'student': None, + 'status': 'Reopened', + 'deadline': None, + } + + changes.extend([ugettext('User-Mentor'), + ugettext('Action-Claim Rejected'), + ugettext('Status-%s' % (properties['status'])) + ]) + elif validation == 'close': + if action == 'needs_work': + properties = { + 'status': 'NeedsWork', + } + + changes.extend([ugettext('User-Mentor'), + ugettext('Action-Requested more work'), + ugettext('Status-%s' % (properties['status'])) + ]) + + if fields['extended_deadline'] > 0: + deadline = entity.deadline + datetime.timedelta( + hours=fields['extended_deadline']) + + properties['deadline'] = deadline + + changes.append(ugettext('DeadlineExtendedBy-%d hrs to %s' % ( + fields['extended_deadline'], deadline.strftime( + '%d %B %Y, %H :%M')))) + + task_update.spawnUpdateTask(entity) + else: + changes.append(ugettext('NoDeadlineExtensionGiven')) + elif action == 'reopened': + properties = { + 'user': None, + 'student': None, + 'status': 'Reopened', + 'deadline': None, + } + + changes.extend([ugettext('User-Mentor'), + ugettext('Action-Reopened'), + ugettext('Status-%s' % (properties['status'])) + ]) + elif action == 'closed': + properties = { + 'deadline': None, + } + + if entity.student: + properties['status'] = 'Closed' + else: + properties['status'] = 'AwaitingRegistration' + + changes.extend([ugettext('User-Mentor'), + ugettext('Action-Closed the task'), + ugettext('Status-%s' % (properties['status'])) + ]) + + comment_properties = { + 'parent': entity, + 'scope_path': entity.key().name(), + 'created_by': user_account, + 'changes': changes, + } + + if ws_properties: + comment_properties['content'] = self.DEF_WS_MSG_FMT + else: + comment_properties['content'] = fields['comment'] + + ghop_task_logic.logic.updateEntityPropertiesWithCWS( + entity, properties, comment_properties, ws_properties) + + # redirect to the same page + return http.HttpResponseRedirect('') + + def publicGet(self, request, context, params, entity, + user_account, **kwargs): + """Handles the GET request for the entity's public page. + + Args: + entity: the task entity + rest see base.View.public() + """ + + context['comment_form'] = params['comment_form']() + + template = params['public_template'] + + return responses.respond(request, template, context=context) + + def updatePublicContext(self, context, entity, comment_entities, + ws_entities, user_account, params): + """Updates the context for the public page with information. + + Args: + context: the context that should be updated + entity: a task used to set context + user_account: user entity of the logged in user + params: dict with params for the view using this context + """ + + mentor_entities = db.get(entity.mentors) + mentors_str = "" + for mentor in mentor_entities: + mentors_str += mentor.name() + ", " + + if mentors_str: + context['mentors_str'] = mentors_str[:-2] + else: + context['mentors_str'] = "Not Assigned" + + context['difficulty_str'] = entity.tags_string(entity.difficulty) + + context['task_type_str'] = entity.tags_string(entity.task_type) + + if entity.deadline: + # TODO: it should be able to abuse Django functionality for this + ttc = entity.deadline - datetime.datetime.now() + (ttc_min, ttc_hour) = ((ttc.seconds / 60), (ttc.seconds / 3600)) + if ttc_min >= 60: + ttc_min = ttc_min % 60 + if ttc_hour > 1: + if ttc_min == 0: + ttc_str = '%d hours' % (ttc_hour) + else: + ttc_str = '%d:%02d hours' % (ttc_hour, ttc_min) + if ttc.days == 1: + ttc_str = '%d day, %s' % (ttc.days, ttc_str) + elif ttc.days > 1: + ttc_str = '%d days, %s' % (ttc.days, ttc_str) + else: + ttc_str = '%d mins' % (ttc_min) + context['time_to_complete'] = ttc_str + else: + if entity.status == 'NeedsReview': + context['time_to_complete'] = 'No Time Left' + else: + context['time_to_complete'] = '%d hours' % (entity.time_to_complete) + + context['comments'] = comment_entities + + context['work_submissions'] = ws_entities + + def _constructActionsList(self, context, entity, + user_account, params): + """Constructs a list of actions for the task page and extends + the comment form with this list. + + This method also returns the validation used by POST method to + validate the user input data. + + Args: + context: the context that should be updated + entity: a task used to set context + user_account: user entity of the logged in user + params: dict with params for the view using this context + """ + + # variable that holds what kind of validation this user + # and task combination pass. + validation = None + + # The following header messages are shown for non-logged in + # general public, logged in public and the student. + if entity.status is 'Closed': + context['header_msg'] = self.DEF_TASK_CLOSED_MSG + validation = 'closed' + + if entity.status == 'Open': + context['header_msg'] = self.DEF_TASK_OPEN_MSG + elif entity.status == 'Reopened': + context['header_msg'] = self.DEF_TASK_REOPENED_MSG + + if user_account: + actions = [('noaction', 'Comment without action')] + + # if the user is logged give him the permission to claim + # the task only if he none of program host, org admin or mentor + filter = { + 'user': user_account, + } + + host_entity = host_logic.logic.getForFields(filter, unique=True) + + filter['scope_path'] = entity.scope_path + org_admin_entity = ghop_org_admin_logic.logic.getForFields( + filter, unique=True) + mentor_entity = ghop_mentor_logic.logic.getForFields( + filter, unique=True) + + if host_entity or org_admin_entity or mentor_entity: + validation, mentor_actions = self._constructMentorActions( + context, entity) + actions += mentor_actions + if entity.status in ['Unapproved', 'Unpublished', 'Open']: + if host_entity or org_admin_entity: + context['edit_link'] = redirects.getEditRedirect(entity, params) + elif mentor_entity: + context['suggest_link'] = ghop_redirects.getSuggestTaskRedirect( + entity, params) + else: + validation, student_actions = self._constructStudentActions( + context, entity, user_account) + actions += student_actions + + # create the difficultly level field containing the choices + # defined in the program entity + dynafields = [ + {'name': 'action', + 'base': forms.ChoiceField, + 'label': 'Action', + 'required': False, + 'passthrough': ['initial', 'required', 'choices'], + 'choices': actions, + }, + ] + + if validation == 'needs_review': + dynafields.append( + {'name': 'work_submission', + 'base': forms.URLField, + 'label': 'Submit Work', + 'required': False, + 'help_text': 'Provide a link to your work in this box. ' + 'Please use the comment box if you need to explain ' + 'of your work.', + }) + + if validation == 'close': + dynafields.append( + {'name': 'extended_deadline', + 'base': forms.IntegerField, + 'min_value': 1, + 'label': 'Extend deadline by', + 'required': False, + 'passthrough': ['min_value', 'required', 'help_text'], + 'help_text': 'Optional: Specify the number of hours by ' + 'which you want to extend the deadline for the task ' + 'for this student. ', + }) + + dynaproperties = params_helper.getDynaFields(dynafields) + if validation == 'needs_review': + dynaproperties['clean_work_submission'] = cleaning.clean_url( + 'work_submission') + + extended_comment_form = dynaform.extendDynaForm( + dynaform=params['comment_form'], + dynaproperties=dynaproperties) + + params['comment_form'] = extended_comment_form + else: + # list of statuses a task can be in after it is requested to be + # claimed before closing or re-opening + claim_status = ['ClaimRequested', 'Claimed', 'ActionNeeded', + 'NeedsWork', 'NeedsReview'] + if entity.status in claim_status: + context['header_msg'] = self.DEF_TASK_CLAIMED_MSG + elif entity.status in ['AwaitingRegistration', 'Closed']: + context['header_msg'] = self.DEF_TASK_CLOSED_MSG + + context['signin_comment_msg'] = self.DEF_SIGNIN_TO_COMMENT_MSG % ( + context['sign_in']) + + return validation + + def _constructMentorActions(self, context, entity): + """Constructs the list of actions for mentors, org admins and + hosts. + """ + + # variable that holds what kind of validation this user + # and task combination pass. + validation = None + + actions = [] + + if entity.status in ['Unapproved', 'Unpublished']: + context['header_msg'] = self.DEF_TASK_UNPUBLISHED_MSG + context['comment_disabled'] = True + elif entity.status == 'Open': + context['header_msg'] = self.DEF_CAN_EDIT_TASK_MSG + elif entity.status == 'Reopened': + context['header_msg'] = self.DEF_TASK_MENTOR_REOPENED_MSG + elif entity.status == 'ClaimRequested': + actions.extend([('accept', 'Accept claim request'), + ('reject', 'Reject claim request')]) + context['header_msg'] = self.DEF_TASK_CLAIM_REQUESTED_MSG + validation = 'accept_claim' + elif entity.status == 'Claimed': + context['header_msg'] = self.DEF_TASK_CLAIMED_BY_STUDENT_MSG + elif entity.status == 'NeedsReview': + context['header_msg'] = self.DEF_TASK_NEEDS_REVIEW_MSG + actions.extend([('needs_work', 'Needs More Work'), + ('reopened', 'Reopen the task'), + ('closed', 'Close the task')]) + validation = 'close' + elif entity.status in ['AwaitingRegistration', 'Closed']: + context['header_msg'] = self.DEF_TASK_CLOSED_MSG + + return validation, actions + + def _constructStudentActions(self, context, entity, user_account): + """Constructs the list of actions for students. + """ + + # variable that holds what kind of validation this user + # and task combination pass. + validation = None + + actions = [] + + if entity.status in ['Open', 'Reopened']: + task_filter = { + 'user': user_account, + 'status': ['ClaimRequested', 'Claimed', 'ActionNeeded', + 'NeedsWork', 'NeedsReview'] + } + task_entities = ghop_task_logic.logic.getForFields(task_filter) + + if len(task_entities) >= entity.program.nr_simultaneous_tasks: + context['header_msg'] = self.DEF_MAX_TASK_LIMIT_MSG_FMT % ( + entity.program.nr_simultaneous_tasks) + validation = 'claim_ineligible' + return validation, actions + + task_filter['status'] = 'AwaitingRegistration' + task_entities = ghop_task_logic.logic.getForFields(task_filter) + + if task_entities: + context['header_msg'] = self.DEF_AWAITING_REG_MSG + validation = 'claim_ineligible' + else: + actions.append(('request', 'Request to claim the task')) + validation = 'claim_request' + + # TODO: lot of double information here that can be simplified + if entity.user and user_account.key() == entity.user.key(): + if entity.status == 'ClaimRequested': + context['header_msg'] = self.DEF_TASK_REQ_CLAIMED_BY_YOU_MSG + actions.append(('withdraw', 'Withdraw from the task')) + validation = 'claim_withdraw' + elif entity.status in ['Claimed', 'NeedsWork', + 'NeedsReview', 'ActionNeeded']: + context['header_msg'] = self.DEF_TASK_CLAIMED_BY_YOU_MSG + actions.extend([ + ('withdraw', 'Withdraw from the task'), + ('needs_review', 'Submit work and Request for review')]) + validation = 'needs_review' + elif entity.status == 'NeedsReview': + context['header_msg'] = self.DEF_TASK_NO_MORE_SUBMIT_MSG + actions.append(('withdraw', 'Withdraw from the task')) + if datetime.datetime.now < entity.deadline: + actions.append( + ('needs_review', 'Submit work and Request for review')) + validation = 'needs_review' + elif entity.status == 'AwaitingRegistration': + context['header_msg'] = self.DEF_STUDENT_SIGNUP_MSG + elif entity.status == 'Closed': + context['header_msg'] = self.DEF_TASK_CMPLTD_BY_YOU_MSG + else: + if entity.status in ['ClaimRequested', 'Claimed', + 'ActionNeeded', 'NeedsWork', + 'NeedsReview']: + context['header_msg'] = self.DEF_TASK_CLAIMED_MSG + if entity.status in ['AwaitingRegistration', 'Closed']: + context['header_msg'] = self.DEF_TASK_CLOSED_MSG + + return validation, actions + + @decorators.merge_params + @decorators.check_access + def search(self, request, access_type, page_name=None, + params=None, filter=None, order=None,**kwargs): + """View method to search for GHOP Tasks. + + Args: + request: the standard Django HTTP request object + access_type : the name of the access type which should be checked + page_name: the page name displayed in templates as page and header title + params: a dict with params for this View + kwargs: the Key Fields for the specified entity + """ + + from soc.modules.ghop.views.helper import list_info as list_info_helper + + get_params = request.GET + + contents = [] + context = {} + if not filter: + filter = {} + + public_status = ['Open', 'Reopened', 'ClaimRequested', 'Claimed', + 'ActionNeeded', 'Closed', 'AwaitingRegistration', + 'NeedsWork', 'NeedsReview'] + + task_params = params.copy() + task_params['list_template'] = 'modules/ghop/task/search/search.html' + task_params['list_heading'] = 'modules/ghop/task/search/heading.html' + task_params['list_row'] = 'modules/ghop/task/search/row.html' + + task_params['list_action'] = (redirects.getPublicRedirect, + task_params) + + task_params['list_description'] = ugettext( + 'Search results: ') + + program_entity = ghop_program_logic.logic.getFromKeyFields(kwargs) + + org_fields = { + 'scope': program_entity, + } + + org_entities = ghop_org_logic.logic.getForFields(org_fields) + org_names = [] + for org in org_entities: + org_names.append(org.name) + + df_entities = ghop_task_model.TaskDifficultyTag.get_by_scope( + program_entity) + difficulties = [] + for df_entity in df_entities: + difficulties.append(df_entity.tag) + + tt_entities = ghop_task_model.TaskTypeTag.get_by_scope(program_entity) + task_types = [] + for tt_entity in tt_entities: + task_types.append(tt_entity.tag) + + context['org_entities'] = org_names + context['public_status'] = public_status + context['difficulties'] = difficulties + context['tags'] = task_types + + org_filter = get_params.getlist('Organization') + status_filter = get_params.getlist('Status') + df_filter = get_params.getlist('Difficulty') + tag_filter = get_params.getlist('Tags') + + if org_filter: + org_fields = { + 'scope': program_entity, + 'name': org_filter, + } + org_entities = ghop_org_logic.logic.getForFields(org_fields) + filter['scope'] = org_entities + if status_filter: + filter['status']= status_filter + else: + filter['status'] = public_status + if df_filter: + filter['difficulty'] = df_filter + if tag_filter: + filter['task_type'] = tag_filter + + filter['program'] = program_entity + + task_list = lists.getListContent(request, task_params, filter, + order=order, idx=0, need_content=True) + + if task_list: + contents.append(task_list) + + # call the _list method from base to display the list + return self._list(request, task_params, contents, page_name, context) + + +view = View() + +create = decorators.view(view.create) +delete = decorators.view(view.delete) +edit = decorators.view(view.edit) +list = decorators.view(view.list) +list_org_tasks = decorators.view(view.listOrgTasks) +suggest_task = decorators.view(view.suggestTask) +public = decorators.view(view.public) +search = decorators.view(view.search)