app/soc/modules/ghop/logic/models/task.py
author Mario Ferraro <fadinlight@gmail.com>
Sun, 15 Nov 2009 22:12:20 +0100
changeset 3093 d1be59b6b627
parent 3092 beeb5d111318
permissions -rw-r--r--
GMaps related JS changed to use new google namespace. Google is going to change permanently in the future the way to load its services, so better stay safe. Also this commit shows uses of the new melange.js module. Fixes Issue 634.

#!/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()