# HG changeset patch # User Madhusudan.C.S # Date 1251455656 -7200 # Node ID 21c222535654adbb923bd6f50ef82f5941194489 # Parent 387a3b80df053458ddf6d66e7b55a8961b3f839c Added several methods to the Task Logic module. These include the automatic tranistion methods. The methods for running updates , that for instance place comments, in a datastore transition to keep the Task synced with the comments. And a rudimentary way of updating the Tasks' history. Reviewed by: Lennard de Rijk diff -r 387a3b80df05 -r 21c222535654 app/soc/modules/ghop/logic/models/task.py --- a/app/soc/modules/ghop/logic/models/task.py Thu Aug 27 22:40:26 2009 +0530 +++ b/app/soc/modules/ghop/logic/models/task.py Fri Aug 28 12:34:16 2009 +0200 @@ -18,11 +18,19 @@ """ __authors__ = [ - '"Madhusudan.C.S" ' + '"Madhusudan.C.S" ', + '"Lennard de Rijk" ', ] -from soc.logic.models import linkable +import datetime + +from google.appengine.ext import db + +from django.utils import simplejson +from django.utils.translation import ugettext + +from soc.logic.models import base import soc.models.linkable @@ -30,10 +38,32 @@ import soc.modules.ghop.models.task -class Logic(linkable.Logic): +STATE_TRANSITIONS = { + 'Claimed': transitFromClaimed, + 'NeedsReview': transitFromNeedsReview, + 'ActionNeeded': transitFromActionNeeded, + 'NeedsWork': transitFromNeedsWork, + } + + +class Logic(base.Logic): """Logic methods for the GHOPTask model. """ + DEF_ACTION_NEEDED_MSG = ugettext( + '(The Melange Automated System has detected that the intial ' + 'deadline has been passed and it has set the task status to ' + 'ActionNeeded.)') + + DEF_NO_MORE_WORK_MSG = ugettext( + '(The Melange Automated System has detected that the deadline ' + 'has been passed and no more work can be submitted.)') + + DEF_REOPENED_MSG = ugettext( + '(The Melange Automated System has detected that the final ' + 'deadline has been passed and it has Reopened the task.)') + + def __init__(self, model=soc.modules.ghop.models.task.GHOPTask, base_model=soc.models.linkable.Linkable, scope_logic=soc.modules.ghop.logic.models.organization): @@ -43,5 +73,359 @@ super(Logic, self).__init__(model, base_model=base_model, scope_logic=scope_logic) + def updateEntityProperties(self, entity, entity_properties, + silent=False, store=True): + """See base.Logic.updateEntityProperties(). + + Also ensures that the history property of the task is updated in the same + datastore operation. + """ + + from soc.logic.models import base as base_logic + + # TODO: History needs a proper test drive and perhaps a refactoring + history = {} + + # we construct initial snapshot of the task when it is published + # for the first time. + if entity_properties and 'status' in entity_properties: + if entity.status == 'Unpublished' or entity.status == 'Unapproved': + if entity_properties['status'] == 'Open': + history = { + 'title': entity.title, + 'description': entity.description, + 'difficulty': entity.difficulty[0].tag, + 'task_type': [type.tag for type in entity.task_type], + 'time_to_complete': entity.time_to_complete, + 'mentors': [m_key.name() for m_key in entity.mentors], + 'user': '', + 'student': '', + 'status': entity.status, + 'deadline': '', + 'created_by': entity.created_by.key().name(), + 'created_on': str(entity.created_on), + 'modified_on': str(entity.modified_on), + } + + if entity.modified_by: + history['modified_by'] = entity.modified_by.key().name() + + # initialize history + task_history = {} + # extract the existing json history from the entity to update it + else: + task_history = simplejson.loads(entity.history) + + # we construct history for only changed entity properties + if entity_properties: + for property in entity_properties: + changed_val = getattr(entity, property) + if changed_val != entity_properties[property]: + if property == 'deadline': + history[property] = str(changed_val) + else: + history[property] = changed_val + + if history: + # create a dictionary for the new history update with timestamp as key + tstamp = str(datetime.datetime.now()) + new_history = {tstamp: history} + + # update existing history + task_history.update(new_history) + task_history_str = simplejson.dumps(task_history) + + # update the task's history property + history_property = { + 'history': task_history_str + } + entity_properties.update(history_property) + + # call the base logic method to store the updated Task entity + return super(Logic, self).updateEntityProperties( + entity, entity_properties, siltent=silent, store=store) + + def updateEntityPropertiesWithCWS(self, entity, entity_properties, + comment_properties=None, + ws_properties=None, silent=False): + """Updates the GHOPTask entity properties and creates a comment + entity. + + Args: + entity: a model entity + entity_properties: keyword arguments that correspond to entity + properties and their values + comment_properties: keyword arguments that correspond to the + GHOPTask's to be created comment entity + silent: iff True does not call post store methods. + """ + + from soc.modules.ghop.logic.models import comment as ghop_comment_logic + from soc.modules.ghop.logic.models import work_submission as \ + ghop_work_submission_logic + from soc.modules.ghop.models import comment as ghop_comment_model + from soc.modules.ghop.models import work_submission as \ + ghop_work_submission_model + + if entity_properties: + entity = self.updateEntityProperties(entity, entity_properties, + silent=silent, store=False) + + comment_entity = ghop_comment_model.GHOPComment(**comment_properties) + + ws_entity = None + if ws_properties: + ws_entity = ghop_work_submission_model.GHOPWorkSubmission( + **ws_properties) + + def comment_create(): + """Method to be run in transaction that stores Task, Comment and + WorkSubmission. + """ + entity.put() + if ws_entity: + ws_entity.put() + comment_entity.content = comment_entity.content % ( + ws_entity.key().id_or_name()) + comment_entity.put() + return entity, comment_entity, ws_entity + else: + comment_entity.put() + return entity, comment_entity, None + + entity, comment_entity, ws_entity = db.run_in_transaction( + comment_create) + + if not silent: + # call the _onCreate methods for the Comment and WorkSubmission + if comment_entity: + ghop_comment_logic.logic._onCreate(comment_entity) + + if ws_entity: + ghop_work_submission_logic._onCreate(ws_entity) + + return entity, comment_entity, ws_entity + + def updateOrCreateFromFields(self, properties, silent=False): + """See base.Logic.updateOrCreateFromFields(). + """ + + # TODO: History needs to be tested and perhaps refactored + if properties['status'] == 'Open': + history = { + 'title': properties['title'], + 'description': properties['description'], + 'difficulty': properties['difficulty']['tags'], + 'task_type': properties['type_tags'], + 'time_to_complete': properties['time_to_complete'], + 'mentors': [m_key.name() for m_key in properties['mentors']], + 'user': '', + 'student': '', + 'status': properties['status'], + 'deadline': '', + 'created_on': str(properties['created_on']), + 'modified_on': str(properties['modified_on']), + } + + if 'created_by' in properties and properties['created_by']: + history['created_by'] = properties['created_by'].key().name() + history['modified_by'] = properties['modified_by'].key().name() + + # Constructs new history from the _constructNewHistory method, assigns + # it as a value to the dictionary key with current timestamp and dumps + # a JSON string. + task_history_str = simplejson.dumps({ + str(datetime.datetime.now()): history, + }) + + # update the task's history property + history_property = { + 'history': task_history_str + } + properties.update(history_property) + + entity = super(Logic, self).updateOrCreateFromFields(properties, silent) + + if entity: + if properties.get('task_type'): + setattr(entity, 'task_type', properties['task_type']) + + if properties.get('difficulty'): + setattr(entity, 'difficulty', properties['difficulty']) + + return entity + + def getFromKeyFieldsWithCWSOr404(self, fields): + """Returns the Task, all Comments and all WorkSubmissions for the Task + specified by the fields argument. + + For args see base.getFromKeyFieldsOr404(). + """ + + from soc.modules.ghop.logic.models import comment as ghop_comment_logic + from soc.modules.ghop.logic.models import work_submission as \ + ghop_work_submission_logic + + entity = self.getFromKeyFieldsOr404(fields) + + comment_entities = ghop_comment_logic.logic.getForFields( + ancestors=[entity], order=['created_on']) + + ws_entities = ghop_work_submission_logic.logic.getForFields( + ancestors=[entity], order=['submitted_on']) + + return entity, comment_entities, ws_entities + + def updateTaskStatus(self, entity): + """Method used to transit a task from a state to another state + depending on the context. Whenever the deadline has passed. + + Args: + entity: The GHOPTask entity + + Returns: + Task entity and a Comment entity if the occurring transit created one. + """ + + from soc.modules.ghop.tasks import task_update + + if entity.deadline and datetime.datetime.now() > entity.deadline: + # calls a specific method to make a transition depending on the + # task's current state + transit_func = STATE_TRANSITIONS[entity.status] + update_dict = transit_func(entity) + + comment_properties = { + 'parent': entity, + 'scope_path': entity.key().name(), + 'created_by': None, + 'content': update_dict['content'], + 'changes': update_dict['changes'], + } + + entity, comment_entity, _ = self.updateEntityPropertiesWithCWS( + entity, update_dict['properties'], comment_properties) + + if entity.deadline: + # only if there is a deadline set we should schedule another task + task_update.spawnUpdateTask(entity) + else: + comment_entity=None + + return entity, comment_entity + + def transitFromClaimed(self, entity): + """Makes a state transition of a GHOP Task from Claimed state + to a relevant state. + + Args: + entity: The GHOPTask entity + """ + + # deadline is extended by 24 hours. + deadline = entity.deadline + datetime.timedelta( + hours=24) + + properties = { + 'status': 'ActionNeeded', + 'deadline': deadline, + } + + changes = [ugettext('User-MelangeAutomatic'), + ugettext('Action-Warned for action'), + ugettext('Status-%s' % (properties['status']))] + + content = self.DEF_ACTION_NEEDED_MSG + + update_dict = { + 'properties': properties, + 'changes': changes, + 'content': content, + } + + return update_dict + + def transitFromNeedsReview(self, entity): + """Makes a state transition of a GHOP Task from NeedsReview state + to a relevant state. + + Args: + entity: The GHOPTask entity + """ + + properties = { + 'deadline': None, + } + + changes = [ugettext('User-MelangeAutomatic'), + ugettext('Action-Deadline passed'), + ugettext('Status-%s' % (entity.status))] + + content = self.DEF_NO_MORE_WORK_MSG + + update_dict = { + 'properties': properties, + 'changes': changes, + 'content': content, + } + + return update_dict + + def transitFromActionNeeded(self, entity): + """Makes a state transition of a GHOP Task from ActionNeeded state + to a relevant state. + + Args: + entity: The GHOPTask entity + """ + + properties = { + 'user': None, + 'student': None, + 'status': 'Reopened', + 'deadline': None, + } + + changes = [ugettext('User-MelangeAutomatic'), + ugettext('Action-Forcibly reopened'), + ugettext('Status-Reopened')] + + content = self.DEF_REOPENED_MSG + + update_dict = { + 'properties': properties, + 'changes': changes, + 'content': content, + } + + return update_dict + + def transitFromNeedsWork(self, entity): + """Makes a state transition of a GHOP Task from NeedsWork state + to a relevant state. + + Args: + entity: The GHOPTask entity + """ + + properties = { + 'user': None, + 'student': None, + 'status': 'Reopened', + 'deadline': None, + } + + changes = [ugettext('User-MelangeAutomatic'), + ugettext('Action-Forcibly reopened'), + ugettext('Status-Reopened')] + + update_dict = { + 'properties': properties, + 'changes': changes, + 'content': None, + } + + return update_dict + logic = Logic()