app/soc/modules/ghop/logic/models/task.py
author Daniel Hans <Daniel.M.Hans@gmail.com>
Sat, 14 Nov 2009 23:58:20 +0100
changeset 3092 beeb5d111318
parent 3091 a48f4e860f7b
permissions -rw-r--r--
Changes in tags are saved to the data store. Also, when a task is created, its arbit tags are stored. Issue 696 fixed.

#!/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.

"""GHOPTask (Model) query functions.
"""

__authors__ = [
    '"Madhusudan.C.S" <madhusudancs@gmail.com>',
    '"Daniel Hans" <daniel.m.hans@gmail.com>',
    '"Lennard de Rijk" <ljvderijk@gmail.com>',
  ]


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
from soc.logic import tags

from soc.modules.ghop.logic.models import comment as ghop_comment_logic

import soc.models.linkable

import soc.modules.ghop.logic.models.organization
import soc.modules.ghop.models.task


STATE_TRANSITIONS = {
    'Claimed': 'transitFromClaimed',
    'NeedsReview': 'transitFromNeedsReview',
    'ActionNeeded': 'transitFromActionNeeded',
    'NeedsWork': 'transitFromNeedsWork',
    }

TAG_NAMES = ['arbit_tag', 'difficulty', 'task_type']

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):
    """Defines the name, key_name and model for this entity.
    """

    self.tags_service = tags.TagsService(TAG_NAMES)

    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
    elif entity.history:
      task_history = simplejson.loads(entity.history)
    else:
      task_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)

    entity = self.tags_service.setTagValuesForEntity(entity, entity_properties)

    # call the base logic method to store the updated Task entity
    return super(Logic, self).updateEntityProperties(
        entity, entity_properties, silent=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.comment import logic as \
        ghop_comment_logic
    from soc.modules.ghop.logic.models.work_submission import logic 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._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)

    self.tags_service.setTagValuesForEntity(entity, properties)

    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.comment import logic as \
        ghop_comment_logic
    from soc.modules.ghop.logic.models.work_submission import logic as \
        ghop_work_submission_logic
 
    entity = self.getFromKeyFieldsOr404(fields)

    comment_entities = ghop_comment_logic.getForFields(
        ancestors=[entity], order=['created_on'])

    ws_entities = ghop_work_submission_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 = getattr(self, 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

  def delete(self, entity):
    """Delete existing entity from datastore.
    """
    
    def task_delete_txn(entity):
      """Performs all necessary operations in a single transaction when a task
      is deleted.
      """

      to_delete = []    
      to_delete += ghop_comment_logic.logic.getForFields(ancestors=[entity])
      to_delete += [entity]
    
      db.delete(to_delete)
  
    self.tags_service.removeAllTagsForEntity(entity)
    db.run_in_transaction(task_delete_txn, entity)


logic = Logic()