16 |
16 |
17 """GHOPTask (Model) query functions. |
17 """GHOPTask (Model) query functions. |
18 """ |
18 """ |
19 |
19 |
20 __authors__ = [ |
20 __authors__ = [ |
21 '"Madhusudan.C.S" <madhusudancs@gmail.com>' |
21 '"Madhusudan.C.S" <madhusudancs@gmail.com>', |
|
22 '"Lennard de Rijk" <ljvderijk@gmail.com>', |
22 ] |
23 ] |
23 |
24 |
24 |
25 |
25 from soc.logic.models import linkable |
26 import datetime |
|
27 |
|
28 from google.appengine.ext import db |
|
29 |
|
30 from django.utils import simplejson |
|
31 from django.utils.translation import ugettext |
|
32 |
|
33 from soc.logic.models import base |
26 |
34 |
27 import soc.models.linkable |
35 import soc.models.linkable |
28 |
36 |
29 import soc.modules.ghop.logic.models.organization |
37 import soc.modules.ghop.logic.models.organization |
30 import soc.modules.ghop.models.task |
38 import soc.modules.ghop.models.task |
31 |
39 |
32 |
40 |
33 class Logic(linkable.Logic): |
41 STATE_TRANSITIONS = { |
|
42 'Claimed': transitFromClaimed, |
|
43 'NeedsReview': transitFromNeedsReview, |
|
44 'ActionNeeded': transitFromActionNeeded, |
|
45 'NeedsWork': transitFromNeedsWork, |
|
46 } |
|
47 |
|
48 |
|
49 class Logic(base.Logic): |
34 """Logic methods for the GHOPTask model. |
50 """Logic methods for the GHOPTask model. |
35 """ |
51 """ |
|
52 |
|
53 DEF_ACTION_NEEDED_MSG = ugettext( |
|
54 '(The Melange Automated System has detected that the intial ' |
|
55 'deadline has been passed and it has set the task status to ' |
|
56 'ActionNeeded.)') |
|
57 |
|
58 DEF_NO_MORE_WORK_MSG = ugettext( |
|
59 '(The Melange Automated System has detected that the deadline ' |
|
60 'has been passed and no more work can be submitted.)') |
|
61 |
|
62 DEF_REOPENED_MSG = ugettext( |
|
63 '(The Melange Automated System has detected that the final ' |
|
64 'deadline has been passed and it has Reopened the task.)') |
|
65 |
36 |
66 |
37 def __init__(self, model=soc.modules.ghop.models.task.GHOPTask, |
67 def __init__(self, model=soc.modules.ghop.models.task.GHOPTask, |
38 base_model=soc.models.linkable.Linkable, |
68 base_model=soc.models.linkable.Linkable, |
39 scope_logic=soc.modules.ghop.logic.models.organization): |
69 scope_logic=soc.modules.ghop.logic.models.organization): |
40 """Defines the name, key_name and model for this entity. |
70 """Defines the name, key_name and model for this entity. |
41 """ |
71 """ |
42 |
72 |
43 super(Logic, self).__init__(model, base_model=base_model, |
73 super(Logic, self).__init__(model, base_model=base_model, |
44 scope_logic=scope_logic) |
74 scope_logic=scope_logic) |
45 |
75 |
|
76 def updateEntityProperties(self, entity, entity_properties, |
|
77 silent=False, store=True): |
|
78 """See base.Logic.updateEntityProperties(). |
|
79 |
|
80 Also ensures that the history property of the task is updated in the same |
|
81 datastore operation. |
|
82 """ |
|
83 |
|
84 from soc.logic.models import base as base_logic |
|
85 |
|
86 # TODO: History needs a proper test drive and perhaps a refactoring |
|
87 history = {} |
|
88 |
|
89 # we construct initial snapshot of the task when it is published |
|
90 # for the first time. |
|
91 if entity_properties and 'status' in entity_properties: |
|
92 if entity.status == 'Unpublished' or entity.status == 'Unapproved': |
|
93 if entity_properties['status'] == 'Open': |
|
94 history = { |
|
95 'title': entity.title, |
|
96 'description': entity.description, |
|
97 'difficulty': entity.difficulty[0].tag, |
|
98 'task_type': [type.tag for type in entity.task_type], |
|
99 'time_to_complete': entity.time_to_complete, |
|
100 'mentors': [m_key.name() for m_key in entity.mentors], |
|
101 'user': '', |
|
102 'student': '', |
|
103 'status': entity.status, |
|
104 'deadline': '', |
|
105 'created_by': entity.created_by.key().name(), |
|
106 'created_on': str(entity.created_on), |
|
107 'modified_on': str(entity.modified_on), |
|
108 } |
|
109 |
|
110 if entity.modified_by: |
|
111 history['modified_by'] = entity.modified_by.key().name() |
|
112 |
|
113 # initialize history |
|
114 task_history = {} |
|
115 # extract the existing json history from the entity to update it |
|
116 else: |
|
117 task_history = simplejson.loads(entity.history) |
|
118 |
|
119 # we construct history for only changed entity properties |
|
120 if entity_properties: |
|
121 for property in entity_properties: |
|
122 changed_val = getattr(entity, property) |
|
123 if changed_val != entity_properties[property]: |
|
124 if property == 'deadline': |
|
125 history[property] = str(changed_val) |
|
126 else: |
|
127 history[property] = changed_val |
|
128 |
|
129 if history: |
|
130 # create a dictionary for the new history update with timestamp as key |
|
131 tstamp = str(datetime.datetime.now()) |
|
132 new_history = {tstamp: history} |
|
133 |
|
134 # update existing history |
|
135 task_history.update(new_history) |
|
136 task_history_str = simplejson.dumps(task_history) |
|
137 |
|
138 # update the task's history property |
|
139 history_property = { |
|
140 'history': task_history_str |
|
141 } |
|
142 entity_properties.update(history_property) |
|
143 |
|
144 # call the base logic method to store the updated Task entity |
|
145 return super(Logic, self).updateEntityProperties( |
|
146 entity, entity_properties, siltent=silent, store=store) |
|
147 |
|
148 def updateEntityPropertiesWithCWS(self, entity, entity_properties, |
|
149 comment_properties=None, |
|
150 ws_properties=None, silent=False): |
|
151 """Updates the GHOPTask entity properties and creates a comment |
|
152 entity. |
|
153 |
|
154 Args: |
|
155 entity: a model entity |
|
156 entity_properties: keyword arguments that correspond to entity |
|
157 properties and their values |
|
158 comment_properties: keyword arguments that correspond to the |
|
159 GHOPTask's to be created comment entity |
|
160 silent: iff True does not call post store methods. |
|
161 """ |
|
162 |
|
163 from soc.modules.ghop.logic.models import comment as ghop_comment_logic |
|
164 from soc.modules.ghop.logic.models import work_submission as \ |
|
165 ghop_work_submission_logic |
|
166 from soc.modules.ghop.models import comment as ghop_comment_model |
|
167 from soc.modules.ghop.models import work_submission as \ |
|
168 ghop_work_submission_model |
|
169 |
|
170 if entity_properties: |
|
171 entity = self.updateEntityProperties(entity, entity_properties, |
|
172 silent=silent, store=False) |
|
173 |
|
174 comment_entity = ghop_comment_model.GHOPComment(**comment_properties) |
|
175 |
|
176 ws_entity = None |
|
177 if ws_properties: |
|
178 ws_entity = ghop_work_submission_model.GHOPWorkSubmission( |
|
179 **ws_properties) |
|
180 |
|
181 def comment_create(): |
|
182 """Method to be run in transaction that stores Task, Comment and |
|
183 WorkSubmission. |
|
184 """ |
|
185 entity.put() |
|
186 if ws_entity: |
|
187 ws_entity.put() |
|
188 comment_entity.content = comment_entity.content % ( |
|
189 ws_entity.key().id_or_name()) |
|
190 comment_entity.put() |
|
191 return entity, comment_entity, ws_entity |
|
192 else: |
|
193 comment_entity.put() |
|
194 return entity, comment_entity, None |
|
195 |
|
196 entity, comment_entity, ws_entity = db.run_in_transaction( |
|
197 comment_create) |
|
198 |
|
199 if not silent: |
|
200 # call the _onCreate methods for the Comment and WorkSubmission |
|
201 if comment_entity: |
|
202 ghop_comment_logic.logic._onCreate(comment_entity) |
|
203 |
|
204 if ws_entity: |
|
205 ghop_work_submission_logic._onCreate(ws_entity) |
|
206 |
|
207 return entity, comment_entity, ws_entity |
|
208 |
|
209 def updateOrCreateFromFields(self, properties, silent=False): |
|
210 """See base.Logic.updateOrCreateFromFields(). |
|
211 """ |
|
212 |
|
213 # TODO: History needs to be tested and perhaps refactored |
|
214 if properties['status'] == 'Open': |
|
215 history = { |
|
216 'title': properties['title'], |
|
217 'description': properties['description'], |
|
218 'difficulty': properties['difficulty']['tags'], |
|
219 'task_type': properties['type_tags'], |
|
220 'time_to_complete': properties['time_to_complete'], |
|
221 'mentors': [m_key.name() for m_key in properties['mentors']], |
|
222 'user': '', |
|
223 'student': '', |
|
224 'status': properties['status'], |
|
225 'deadline': '', |
|
226 'created_on': str(properties['created_on']), |
|
227 'modified_on': str(properties['modified_on']), |
|
228 } |
|
229 |
|
230 if 'created_by' in properties and properties['created_by']: |
|
231 history['created_by'] = properties['created_by'].key().name() |
|
232 history['modified_by'] = properties['modified_by'].key().name() |
|
233 |
|
234 # Constructs new history from the _constructNewHistory method, assigns |
|
235 # it as a value to the dictionary key with current timestamp and dumps |
|
236 # a JSON string. |
|
237 task_history_str = simplejson.dumps({ |
|
238 str(datetime.datetime.now()): history, |
|
239 }) |
|
240 |
|
241 # update the task's history property |
|
242 history_property = { |
|
243 'history': task_history_str |
|
244 } |
|
245 properties.update(history_property) |
|
246 |
|
247 entity = super(Logic, self).updateOrCreateFromFields(properties, silent) |
|
248 |
|
249 if entity: |
|
250 if properties.get('task_type'): |
|
251 setattr(entity, 'task_type', properties['task_type']) |
|
252 |
|
253 if properties.get('difficulty'): |
|
254 setattr(entity, 'difficulty', properties['difficulty']) |
|
255 |
|
256 return entity |
|
257 |
|
258 def getFromKeyFieldsWithCWSOr404(self, fields): |
|
259 """Returns the Task, all Comments and all WorkSubmissions for the Task |
|
260 specified by the fields argument. |
|
261 |
|
262 For args see base.getFromKeyFieldsOr404(). |
|
263 """ |
|
264 |
|
265 from soc.modules.ghop.logic.models import comment as ghop_comment_logic |
|
266 from soc.modules.ghop.logic.models import work_submission as \ |
|
267 ghop_work_submission_logic |
|
268 |
|
269 entity = self.getFromKeyFieldsOr404(fields) |
|
270 |
|
271 comment_entities = ghop_comment_logic.logic.getForFields( |
|
272 ancestors=[entity], order=['created_on']) |
|
273 |
|
274 ws_entities = ghop_work_submission_logic.logic.getForFields( |
|
275 ancestors=[entity], order=['submitted_on']) |
|
276 |
|
277 return entity, comment_entities, ws_entities |
|
278 |
|
279 def updateTaskStatus(self, entity): |
|
280 """Method used to transit a task from a state to another state |
|
281 depending on the context. Whenever the deadline has passed. |
|
282 |
|
283 Args: |
|
284 entity: The GHOPTask entity |
|
285 |
|
286 Returns: |
|
287 Task entity and a Comment entity if the occurring transit created one. |
|
288 """ |
|
289 |
|
290 from soc.modules.ghop.tasks import task_update |
|
291 |
|
292 if entity.deadline and datetime.datetime.now() > entity.deadline: |
|
293 # calls a specific method to make a transition depending on the |
|
294 # task's current state |
|
295 transit_func = STATE_TRANSITIONS[entity.status] |
|
296 update_dict = transit_func(entity) |
|
297 |
|
298 comment_properties = { |
|
299 'parent': entity, |
|
300 'scope_path': entity.key().name(), |
|
301 'created_by': None, |
|
302 'content': update_dict['content'], |
|
303 'changes': update_dict['changes'], |
|
304 } |
|
305 |
|
306 entity, comment_entity, _ = self.updateEntityPropertiesWithCWS( |
|
307 entity, update_dict['properties'], comment_properties) |
|
308 |
|
309 if entity.deadline: |
|
310 # only if there is a deadline set we should schedule another task |
|
311 task_update.spawnUpdateTask(entity) |
|
312 else: |
|
313 comment_entity=None |
|
314 |
|
315 return entity, comment_entity |
|
316 |
|
317 def transitFromClaimed(self, entity): |
|
318 """Makes a state transition of a GHOP Task from Claimed state |
|
319 to a relevant state. |
|
320 |
|
321 Args: |
|
322 entity: The GHOPTask entity |
|
323 """ |
|
324 |
|
325 # deadline is extended by 24 hours. |
|
326 deadline = entity.deadline + datetime.timedelta( |
|
327 hours=24) |
|
328 |
|
329 properties = { |
|
330 'status': 'ActionNeeded', |
|
331 'deadline': deadline, |
|
332 } |
|
333 |
|
334 changes = [ugettext('User-MelangeAutomatic'), |
|
335 ugettext('Action-Warned for action'), |
|
336 ugettext('Status-%s' % (properties['status']))] |
|
337 |
|
338 content = self.DEF_ACTION_NEEDED_MSG |
|
339 |
|
340 update_dict = { |
|
341 'properties': properties, |
|
342 'changes': changes, |
|
343 'content': content, |
|
344 } |
|
345 |
|
346 return update_dict |
|
347 |
|
348 def transitFromNeedsReview(self, entity): |
|
349 """Makes a state transition of a GHOP Task from NeedsReview state |
|
350 to a relevant state. |
|
351 |
|
352 Args: |
|
353 entity: The GHOPTask entity |
|
354 """ |
|
355 |
|
356 properties = { |
|
357 'deadline': None, |
|
358 } |
|
359 |
|
360 changes = [ugettext('User-MelangeAutomatic'), |
|
361 ugettext('Action-Deadline passed'), |
|
362 ugettext('Status-%s' % (entity.status))] |
|
363 |
|
364 content = self.DEF_NO_MORE_WORK_MSG |
|
365 |
|
366 update_dict = { |
|
367 'properties': properties, |
|
368 'changes': changes, |
|
369 'content': content, |
|
370 } |
|
371 |
|
372 return update_dict |
|
373 |
|
374 def transitFromActionNeeded(self, entity): |
|
375 """Makes a state transition of a GHOP Task from ActionNeeded state |
|
376 to a relevant state. |
|
377 |
|
378 Args: |
|
379 entity: The GHOPTask entity |
|
380 """ |
|
381 |
|
382 properties = { |
|
383 'user': None, |
|
384 'student': None, |
|
385 'status': 'Reopened', |
|
386 'deadline': None, |
|
387 } |
|
388 |
|
389 changes = [ugettext('User-MelangeAutomatic'), |
|
390 ugettext('Action-Forcibly reopened'), |
|
391 ugettext('Status-Reopened')] |
|
392 |
|
393 content = self.DEF_REOPENED_MSG |
|
394 |
|
395 update_dict = { |
|
396 'properties': properties, |
|
397 'changes': changes, |
|
398 'content': content, |
|
399 } |
|
400 |
|
401 return update_dict |
|
402 |
|
403 def transitFromNeedsWork(self, entity): |
|
404 """Makes a state transition of a GHOP Task from NeedsWork state |
|
405 to a relevant state. |
|
406 |
|
407 Args: |
|
408 entity: The GHOPTask entity |
|
409 """ |
|
410 |
|
411 properties = { |
|
412 'user': None, |
|
413 'student': None, |
|
414 'status': 'Reopened', |
|
415 'deadline': None, |
|
416 } |
|
417 |
|
418 changes = [ugettext('User-MelangeAutomatic'), |
|
419 ugettext('Action-Forcibly reopened'), |
|
420 ugettext('Status-Reopened')] |
|
421 |
|
422 update_dict = { |
|
423 'properties': properties, |
|
424 'changes': changes, |
|
425 'content': None, |
|
426 } |
|
427 |
|
428 return update_dict |
|
429 |
46 |
430 |
47 logic = Logic() |
431 logic = Logic() |