/
task.py
3267 lines (2777 loc) · 125 KB
/
task.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
import datetime
import logging
import os
from sqlalchemy import (Table, Column, Integer, ForeignKey, Boolean, Enum,
Float, event, CheckConstraint)
from sqlalchemy.exc import UnboundExecutionError, OperationalError, InvalidRequestError, ProgrammingError
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import relationship, validates, synonym, reconstructor
from stalker.db.declarative import Base
from stalker.models.entity import Entity
from stalker.models.mixins import (DateRangeMixin, StatusMixin, ReferenceMixin,
ScheduleMixin, DAGMixin)
from stalker.log import logging_level
logger = logging.getLogger(__name__)
logger.setLevel(logging_level)
# schedule constraints
CONSTRAIN_NONE = 0
CONSTRAIN_START = 1
CONSTRAIN_END = 2
CONSTRAIN_BOTH = 3
class TimeLog(Entity, DateRangeMixin):
"""Holds information about the uninterrupted time spent on a specific
:class:`.Task` by a specific :class:`.User`.
It is so important to note that the TimeLog reports the **uninterrupted**
time interval that is spent for a Task. Thus it doesn't care about the
working time attributes like daily working hours, weekly working days or
anything else. Again it is the uninterrupted time which is spent for a
task.
Entering a time log for 2 days will book the resource for 48 hours and not,
2 * daily working hours.
TimeLogs are created per resource. It means, you need to record all the
works separately for each resource. So there is only one resource in a
TimeLog instance.
A :class:`.TimeLog` instance needs to be initialized with a :class:`.Task`
and a :class:`.User` instances.
Adding overlapping time log for a :class:`.User` will raise a
:class:`.OverBookedError`.
.. ::
TimeLog instances automatically extends the :attr:`.Task.schedule_timing`
of the assigned Task if the :attr:`.Task.total_logged_seconds` is getting
bigger than the :attr:`.Task.schedule_timing` after this TimeLog.
:param task: The :class:`.Task` instance that this time log belongs to.
:param resource: The :class:`.User` instance that this time log is created
for.
"""
__auto_name__ = True
__tablename__ = "TimeLogs"
__mapper_args__ = {"polymorphic_identity": "TimeLog"}
__table_args__ = (
CheckConstraint('"end" > start'), # this will be ignored in SQLite3
)
time_log_id = Column("id", Integer, ForeignKey("Entities.id"),
primary_key=True)
task_id = Column(
Integer, ForeignKey("Tasks.id"), nullable=False,
doc="""The id of the related task."""
)
task = relationship(
"Task",
primaryjoin="TimeLogs.c.task_id==Tasks.c.id",
uselist=False,
back_populates="time_logs",
doc="""The :class:`.Task` instance that this time log is created for"""
)
resource_id = Column(Integer, ForeignKey("Users.id"), nullable=False)
resource = relationship(
"User",
primaryjoin="TimeLogs.c.resource_id==Users.c.id",
uselist=False,
back_populates="time_logs",
doc="""The :class:`.User` instance that this time_log is created for"""
)
def __init__(
self,
task=None,
resource=None,
start=None,
end=None,
duration=None,
**kwargs):
super(TimeLog, self).__init__(**kwargs)
kwargs['start'] = start
kwargs['end'] = end
kwargs['duration'] = duration
DateRangeMixin.__init__(self, **kwargs)
self.task = task
self.resource = resource
@validates("task")
def _validate_task(self, key, task):
"""validates the given task value
"""
if not isinstance(task, Task):
raise TypeError(
"%s.task should be an instance of stalker.models.task.Task "
"not %s" %
(self.__class__.__name__, task.__class__.__name__)
)
if task.is_container:
raise ValueError(
'%(task)s (id: %(id)s) is a container task, and it is not '
'allowed to create TimeLogs for a container task' % {
'task': task.name,
'id': task.id
}
)
# check status
logger.debug('checking task status!')
from stalker.db.session import DBSession
with DBSession.no_autoflush:
task_status_list = task.status_list
WFD = task_status_list['WFD']
RTS = task_status_list['RTS']
WIP = task_status_list['WIP']
PREV = task_status_list['PREV']
HREV = task_status_list['HREV']
DREV = task_status_list['DREV']
OH = task_status_list['OH']
STOP = task_status_list['STOP']
CMPL = task_status_list['CMPL']
if task.status in [WFD, OH, STOP, CMPL]:
from stalker.exceptions import StatusError
raise StatusError(
'%(task)s is a %(status)s task, and it is not allowed to '
'create TimeLogs for a %(status)s task, please supply a '
'RTS, WIP, HREV or DREV task!' % {
'task': task.name,
'status': task.status.code
}
)
elif task.status in [RTS, HREV]:
# update task status
logger.debug('updating task status to WIP!')
task.status = WIP
# check dependent tasks
logger.debug('checking dependent task statuses')
for task_dependencies in task.task_depends_to:
dep_task = task_dependencies.depends_to
dependency_target = task_dependencies.dependency_target
raise_violation_error = False
violation_date = None
if dependency_target == 'onend':
# time log can not start before the end date of this task
if self.start < dep_task.end:
raise_violation_error = True
violation_date = dep_task.end
elif dependency_target == 'onstart':
if self.start < dep_task.start:
raise_violation_error = True
violation_date = dep_task.start
if raise_violation_error:
from stalker.exceptions import DependencyViolationError
raise DependencyViolationError(
'It is not possible to create a TimeLog before '
'%s, which violates the dependency relation of '
'"%s" to "%s"' % (
violation_date,
task.name,
dep_task.name,
)
)
# this may need to be in an external event, it needs to trigger a flush
# to correctly function
task.update_parent_statuses()
return task
@validates("resource")
def _validate_resource(self, key, resource):
"""validates the given resource value
"""
if resource is None:
raise TypeError(
"%s.resource can not be None" % self.__class__.__name__
)
from stalker import User
if not isinstance(resource, User):
raise TypeError(
"%s.resource should be a stalker.models.auth.User instance "
"not %s" %
(self.__class__.__name__, resource.__class__.__name__)
)
# check for overbooking
clashing_time_log_data = None
from stalker.db.session import DBSession
with DBSession.no_autoflush:
try:
from sqlalchemy import or_, and_
clashing_time_log_data = \
DBSession.query(TimeLog.start, TimeLog.end)\
.filter(TimeLog.id != self.id)\
.filter(TimeLog.resource_id == resource.id)\
.filter(
or_(
and_(
TimeLog.start <= self.start,
self.start < TimeLog.end
),
and_(
TimeLog.start < self.end,
self.end <= TimeLog.end
)
)
)\
.first()
except (UnboundExecutionError, OperationalError) as e:
# fallback to Python
for time_log in resource.time_logs:
if time_log != self:
if time_log.start == self.start or \
time_log.end == self.end or \
time_log.start < self.end < time_log.end or \
time_log.start < self.start < time_log.end:
clashing_time_log_data = \
[time_log.start, time_log.end]
if clashing_time_log_data:
from stalker.exceptions import OverBookedError
raise OverBookedError(
"The resource has another TimeLog between %s and %s" % (
clashing_time_log_data[0], clashing_time_log_data[1]
)
)
return resource
def __eq__(self, other):
"""equality of TimeLog instances
"""
return isinstance(other, TimeLog) and self.task is other.task and \
self.resource is other.resource and self.start == other.start and \
self.end == other.end and self.name == other.name
# TODO: Consider contracting a Task with TimeLogs, what will happen when the
# task has logged in time
# TODO: Check, what happens when a task has TimeLogs and will have child task
# later on, will it be ok with TJ
class Task(Entity, StatusMixin, DateRangeMixin, ReferenceMixin, ScheduleMixin, DAGMixin):
"""Manages Task related data.
**Introduction**
Tasks are the smallest unit of work that should be accomplished to complete
a :class:`.Project`. Tasks define a certain amount of time needed to be
spent for a purpose. They also define a complex hierarchy of relation.
Stalker follows and enhances the concepts stated in TaskJuggler_.
.. _TaskJuggler : http://www.taskjuggler.org/
.. note::
.. versionadded:: 0.2.0
References in Tasks
Tasks can now have References.
**Initialization**
Tasks are a part of a bigger Project, that's way a Task needs to be created
with a :class:`.Project` instance. It is possible to create a task without
a project, if it is created to be a child of another task. And it is also
possible to pass both a project and a parent task.
But because passing both a project and a parent task may create an
ambiguity, Stalker will raise a RuntimeWarning, if both project and task
are given and the owner project of the given parent task is different then
the supplied project instance. But again Stalker will warn the user but
will continue to use the task as the parent and will correctly use the
project of the given task as the project of the newly created task.
The following codes are a couple of examples for creating Task instances::
# with a project instance
>>> from stalker import Project
>>> project1 = Project(name='Test Project 1') # simplified
>>> task1 = Task(name='Schedule', project=project1)
# with a parent task
>>> task2 = Task(name='Documentation', parent=task1)
# or both
>>> task3 = Task(name='Test', project=project1, parent=task1)
# this will create a RuntimeWarning
>>> project2 = Project(name='Test Project 2')
>>> task4 = Task(name='Test', project=project2, parent=task1)
# task1 is not a # task of proj2
>>> assert task4.project == project1
# Stalker uses the task1.project for task4
# this will also create a RuntimeError
>>> task3 = Task(name='Failure 2') # no project no parent, this is an
# orphan task.
Also initially Stalker will pin point the :attr:`.start` value and then
will calculate proper :attr:`.end` and :attr:`.duration` values by using
the :attr:`.schedule_timing` and :attr:`.schedule_unit` attributes. But
these values (start, end and duration) are temporary values for an
unscheduled task. The final date values will be calculated by TaskJuggler
in the `auto scheduling` phase.
**Auto Scheduling**
Stalker uses TaskJuggler for task scheduling. After defining all the tasks,
Stalker will convert them to a single tjp file along with the recorded
:class:`.TimeLog` s :class:`.Vacation` s etc. and let TaskJuggler to
solve the scheduling problem.
During the auto scheduling (with TaskJuggler), the calculation of task
duration, start and end dates are effected by the working hours setting of
the :class:`.Studio`, the effort that needs to spend for that task and the
availability of the resources assigned to the task.
A good practice for creating a project plan is to supply the parent/child
and dependency relation between tasks and the effort and resource
information per task and leave the start and end date calculation to
TaskJuggler.
The default :attr:`.schedule_model` for Stalker tasks is 'effort`, the
default :attr:`.schedule_unit` is ``hour`` and the default value of
:attr:`.schedule_timing` is defined by the
:attr:`stalker.config.Config.timing_resolution`. So for a config where the
``timing_resolution`` is set to 1 hour the schedule_timing is 1.
It is also possible to use the ``length`` or ``duration`` values for
:attr:`.schedule_model` (set it to 'effort', 'length' or 'duration' to get
the desired scheduling model).
To convert a Task instance to a TaskJuggler compatible string use the
:attr:`.to_tjp`` attribute. It will try to create a good representation of
the Task by using the resources, schedule_model, schedule_timing and
schedule_constraint attributes.
** Alternative Resources**
.. versionadded:: 0.2.5
Alternative Resources
Stalker now supports alternative resources per task. You can specify
alternative resources by using the :attr:`.alternative_resources`
attribute. The number of resources and the number of alternative resources
are not related. So you can define only 1 resource and more than one
alternative resources, or you can define 2 resources and only one
alternative resource.
.. warning::
As opposed to TaskJuggler alternative resources are not per resource
based. So Stalker will use the alternatives list for all of the
resources in the resources list. Per resource alternative will be
supported in future versions of Stalker.
Stalker will pass the data to TaskJuggler and TJ will compute a list of
resources that are assigned to the task in the report time frame and
Stalker will store the resultant list of users in
:attr:`.computed_resources` attribute.
.. warning::
When TaskJuggler computes the resources, the returned list may contain
resources which are not in the :attr:`.resources` nor in
:attr:`.alternative_resources` list anymore. Stalker will silently
filter those resources and will only store resources (in
:attr:`.computed_resources`) those are still available as a direct or
alternative resource to that particular task.
The selection strategy of the alternative resource is defined by the
:attr:`.allocation_strategy` attribute. The `allocation_strategy`
attribute value should be one of [minallocated, maxloaded, minloaded,
order, random]. The following description is from TaskJuggler
documentation:
+--------------+--------------------------------------------------------+
| minallocated | Pick the resource that has the smallest allocation |
| | factor. The allocation factor is calculated from the |
| | various allocations of the resource across the tasks. |
| | This is the default setting.) |
+--------------+--------------------------------------------------------+
| maxloaded | Pick the available resource that has been used the |
| | most so far. |
+--------------+--------------------------------------------------------+
| minloaded | Pick the available resource that has been used the |
| | least so far. |
+--------------+--------------------------------------------------------+
| order | Pick the first available resource from the list. |
+--------------+--------------------------------------------------------+
| random | Pick a random resource from the list. |
+--------------+--------------------------------------------------------+
As in TaskJuggler the default for :attr:`.allocation_strategy` attribute is
"minallocated".
Also the allocation of the resources are effected by the
:attr:`.persistent_allocation` attribute. The persistent_allocation
attribute refers to the ``persistent`` attribute in TJ. The documentation
of ``persistent`` in TJ is as follows:
Specifies that once a resource is picked from the list of alternatives
this resource is used for the whole task. This is useful when several
alternative resources have been specified. Normally the selected resource
can change after each break. A break is an interval of at least one
timeslot where no resources were available.
:attr:`.persistent_allocation` attribute is True by default.
For a not yet scheduled task the :attr:`.computed_resources` attribute will
be the same as the :attr:`.resources` list. After the task is scheduled the
content of the :attr:`.computed_resources` will purely come from TJ.
Updating the resources list will not update the :attr:`.computed_resources`
list if the task :attr:`.is_scheduled`.
**Task to Task Relation**
.. note::
.. versionadded:: 0.2.0
Task to Task Relation
Tasks can have child Tasks. So you can create complex relations of Tasks to
comply with your project needs.
A Task is called a ``container task`` if it has at least one child Task.
And it is called a ``leaf task`` if it doesn't have any children Tasks.
Tasks which doesn't have a parent called ``root_task``.
As opposed to TaskJuggler where the resource information is passed through
parent to child, in Stalker the resources in a container task is
meaningless, cause the resources are defined by the child tasks.
.. note::
Although, the ``tjp_task_template`` variable is not coded in that way in
the default config, if you want to populate resource information through
children tasks as it is in TaskJuggler, you can change the
``tjp_task_template`` variable with a local **config.py** file. See
`configuring stalker`_
.. _configuring stalker: ../configure.html
Although the values are not very important after TaskJuggler schedules a
task, the :attr:`~.start` and :attr:`~.end` values for a container
task is gathered from the child tasks. The start is equal to the earliest
start value of the children tasks, and the end is equal to the latest end
value of the children tasks. Of course, these values are going to be
ignored by TaskJuggler, but for interactive gantt charts these are good toy
attributes to play with.
Stalker will check if there will be a cycle if one wants to parent a Task
to a child Task of its own or the dependency relation creates a cycle.
In Gantt Charts the ``computed_start``, ``computed_end`` and
``computed_resources`` attributes will be used if the task
:attr:`.is_scheduled`.
**Task Responsible**
.. note::
.. versionadded:: 0.2.0
Task Responsible
.. note::
.. versionadded:: 0.2.5
Multiple Responsible Per Task
Tasks have a **responsible** which is a list of :class:`.User` instances
who are responsible of the assigned task and all the hierarchy under it.
If a task doesn't have any responsible, it will start looking to its
parent tasks and will return a copy of the responsible of its parent and it
will be an empty list if non of its parents has a responsible.
You can create complex responsibility chains for different branches of
Tasks.
**Percent Complete Calculation** .. versionadded:: 0.2.0
Tasks can calculate how much it is completed based on the
:attr:`.schedule_seconds` and :attr:`.total_logged_seconds` attributes.
For a parent task, the calculation is based on the total
:attr:`.schedule_seconds` and :attr:`.total_logged_seconds` attributes of
their children.
.. versionadded:: 0.2.14
Because duration tasks do not need time logs there is no way to
calculate the percent complete by using the time logs. And Percent
Complete on a duration task is calculated directly from the
:attr:`.start` and :attr:`.end` and ``datetime.datetime.now(pytz.utc)``.
.. versionadded:: 0.2.26
For parent tasks that have both effort based and duration based children
tasks the percent complete is calculated as if the
:attr:`.total_logged_seconds` is properly filled for duration based
tasks proportinal to the elapsed time from the :attr:`.start` attr
value.
Even tough, the percent_complete attribute of a task is
100% the task may not be considered as completed, because it may not be
reviewed and approved by the responsible yet.
**Task Review Workflow**
.. versionadded:: 0.2.5
Task Review Workflow
Starting with Stalker v0.2.5 tasks are reviewed by their responsible users.
The reviews done by responsible users will set the task status according to
the supplied reviews. Please see the :class:`.Review` class documentation
for more details.
**Task Status Workflow**
.. note::
.. versionadded:: 0.2.5
Task Status Workflow
Task statuses now follow a workflow called "Task Status Workflow".
The "Task Status Workflow" defines the different statuses that a Task will
have along its normal life cycle. Container and leaf tasks will have
different workflow using nearly the same set of statuses (container tasks
have only 4 statuses where as leaf tasks have 9).
The following diagram shows the status workflow for leaf tasks:
.. image:: ../../../docs/source/_static/images/Task_Status_Workflow.png
:width: 637 px
:height: 611 px
:align: center
The workflow defines the following statuses at described situations:
+-----------------------------------------------------------------------+
| LEAF TASK STATUS WORKFLOW |
+------------------+----------------------------------------------------+
| Status Name | Description |
+------------------+----------------------------------------------------+
| Waiting For | If a task has uncompleted dependencies then it |
| Dependency (WFD) | will have its status to set to WFD. A WFD Task can |
| | not have a TimeLog or a review request can not be |
| | made for it. |
+------------------+----------------------------------------------------+
| Ready To Start | A task is set to RTS when there are no |
| (RTS) | dependencies or all of its dependencies are |
| | completed, so there is nothing preventing it to be |
| | started. An RTS Task can have new TimeLogs. A |
| | review can not be requested at this stage cause no |
| | work is done yet. |
+------------------+----------------------------------------------------+
| Work In Progress | A task is set to WIP when a TimeLog has been |
| (WIP) | created for that task. A WIP task can have new |
| | TimeLogs and a review can be requested for that |
| | task. |
+------------------+----------------------------------------------------+
| Pending Review | A task is set to PREV when a new set of Review |
| (PREV) | instances created for it by using the |
| | :meth:`.Task.request_review` method. And it is |
| | possible to request a review only for a task with |
| | status WIP. A PREV task can not have new TimeLogs |
| | nor a new request can be made because it is in |
| | already in review. |
+------------------+----------------------------------------------------+
| Has Revision | A task is set to HREV when one of its Reviews |
| (HREV) | completed by requesting a review by using the |
| | :meth:`.Review.request_review` method. A HREV Task |
| | can have new TimeLogs, and it will be converted to |
| | a WIP or DREV depending to its dependency task |
| | statuses. |
+------------------+----------------------------------------------------+
| Dependency Has | If the dependent task of a WIP, PREV, HREV, DREV |
| Revision (DREV) | or CMPL task has a revision then the statuses of |
| | the tasks are set to DREV which means both of the |
| | dependee and the dependent tasks can work at the |
| | same time. For a DREV task a review request can |
| | not be made until it is set to WIP again by |
| | setting the depending task to CMPL again. |
+------------------+----------------------------------------------------+
| On Hold (OH) | A task is set to OH when the resource needs to |
| | work for another task, and the :meth:`Task.hold` |
| | is called. An OH Task can be resumed by calling |
| | :meth:`.Task.resume` method and depending to its |
| | :attr:`.Task.time_logs` attribute it will have its |
| | status set to RTS or WIP. |
+------------------+----------------------------------------------------+
| Stopped (STOP) | A task is set to STOP when no more work needs to |
| | done for that task and it will not be used |
| | anymore. Call :meth:`.Task.stop` method to do it |
| | properly. Only applicable to WIP tasks. |
| | |
| | The schedule values of the task will be capped to |
| | current time spent on it, so Task Juggler will not |
| | reserve any more resources for it. |
| | |
| | Also STOP tasks are treated as if they are dead. |
+------------------+----------------------------------------------------+
| Completed (CMPL) | A task is set to CMPL when all of the Reviews are |
| | completed by approving the task. It is not |
| | possible to create any new TimeLogs and no new |
| | review can be requested for a CMPL Task. |
+------------------+----------------------------------------------------+
Container "Task Status Workflow" defines a set of statuses where the
container task status will only change according to its children task
statuses:
+-----------------------------------------------------------------------+
| CONTAINER TASK STATUS WORKFLOW |
+------------------+----------------------------------------------------+
| Status Name | Description |
+------------------+----------------------------------------------------+
| Waiting For | If all of the child tasks are in WFD status then |
| Dependency (WFD) | the container task is also WFD. |
+------------------+----------------------------------------------------+
| Ready To Start | A container task is set to RTS when children tasks |
| (RTS) | have statuses of only WFD and RTS. |
+------------------+----------------------------------------------------+
| Work In Progress | A container task is set to WIP when one of its |
| (WIP) | children tasks have any of the statuses of RTS, |
| | WIP, PREV, HREV, DREV or CMPL. |
+------------------+----------------------------------------------------+
| Completed (CMPL) | A container task is set to CMPL when all of its |
| | children tasks are CMPL. |
+------------------+----------------------------------------------------+
Even though, users are encouraged to use the actions (like
:meth:`.Task.create_time_log`, :meth:`.Task.hold`, :meth:`.Task.stop`,
:meth:`.Task.resume`, :meth:`.Task.request_revision`,
:meth:`.Task.request_review`, :meth:`.Task.approve`) to update the task
statuses , setting the :attr:`.Task.status` will also update the dependent
tasks or will check the new status against dependencies or the current
status of the task.
Thus in some situations setting the :attr:`.Task.status` will not change
the status of the task. For example, setting the task status to WFD when
there are no dependencies will not update the task status to WFD,
also updating a PREV task status to STOP or HOLD or RTS is not possible.
And it is not possible to set a task to WIP if there are no TimeLogs
entered for that task.
So the task will strictly follow the Task Status Workflow diagram above.
.. warning::
**Dependency Relation in Task Status Workflow**
Because the Task Status Workflow heavily effected by the dependent task
statuses, and the main reason of having dependency relation is to let
TaskJuggler to schedule the tasks correctly, and any task status other
than WFD or RTS means that a TimeLog has been created for a task (which
means that you can not change the :attr:`.computed_start` anymore), it
is only allowed to change the dependencies of a WFD and RTS tasks.
.. warning::
**Resuming a STOP Task**
Resuming a STOP Task will be treated as if a revision has been made to
that task, and all the statuses of the tasks depending to this
particular task will be updated accordingly.
.. warning::
**Initial Status of a Task**
.. versionadded:: 0.2.5
Because of the Task Status Workflow, supplying a status with the
**status** argument may not set the status of the Task to the desired
value. A Task starts with WFD status by default, and updated to RTS if
it doesn't have any dependencies or all of the dependencies are STOP or
CMPL.
.. note::
.. versionadded:: 0.2.5.2
Task.path and Task.absolute_path properties
Task instances now have two new properties called :attr:`.path` and
:attr:`.absolute_path`\ . The value of these attributes are the
rendered version of the related :class:`.FilenameTemplate` which
has its target_entity_type attribute set to "Task" (or "Asset",
"Shot" or "Sequence" or anything matching to the derived class name,
so it can be used in :class:`.Asset`, :class:`.Shot` and
:class:`.Sequences` or anything that is derived from Task class) in
the :class:`.Project` that this task belongs to. This property has
been added to make it easier to write custom template codes for
Project :class:`.Structure` s.
The :attr:`.path` attribute is a repository relative path, where as
the :attr:`.absolute_path` is the full path and includs the OS
dependent repository path.
.. versionadded: 0.2.13
Task to :class:`.Good` relation. It is now possible to define the
related Good to this task, to be able to filter tasks that are related
to the same :class:`.Good`.
Its main purpose of existence is to be able to generate
:class:`.BugdetEntry` instances from the tasks that are related to the
same :class:`.Good` and because the Goods are defining the cost and MSRP
of different things, it is possible to create BudgetEntries and thus
:class;`.Budget` s with this information.
**Arguments**
:param project: A Task which doesn't have a parent (a root task) should be
created with a :class:`.Project` instance. If it is skipped an no
:attr:`.parent` is given then Stalker will raise a RuntimeError. If both
the ``project`` and the :attr:`.parent` argument contains data and the
project of the Task instance given with parent argument is different than
the Project instance given with the ``project`` argument then a
RuntimeWarning will be raised and the project of the parent task will be
used.
:type project: :class:`.Project`
:param parent: The parent Task or Project of this Task. Every Task in
Stalker should be related with a :class:`.Project` instance. So if no
parent task is desired, at least a Project instance should be passed as
the parent of the created Task or the Task will be an orphan task and
Stalker will raise a RuntimeError.
:type parent: :class:`.Task`
:param depends: A list of :class:`.Task` s that this :class:`.Task` is
depending on. A Task can not depend to itself or any other Task which are
already depending to this one in anyway or a CircularDependency error
will be raised.
:type depends: [:class:`.Task`]
:param resources: The :class:`.User` s assigned to this :class:`.Task`. A
:class:`.Task` without any resource can not be scheduled.
:type resources: [:class:`.User`]
:param responsible: A list of :class:`.User` instances that is responsible
of this task.
:type responsible: [:class:`.User`]
:param watchers: A list of :class:`.User` those are added this Task
instance to their watchlist.
:type watchers: [:class:`.User`]
:param start: The start date and time of this task instance. It is only
used if the :attr:`.schedule_constraint` attribute is set to
:attr:`.CONSTRAIN_START` or :attr:`.CONSTRAIN_BOTH`. The default value
is `datetime.datetime.now(pytz.utc)`.
:type start: :class:`datetime.datetime`
:param end: The end date and time of this task instance. It is only used if
the :attr:`.schedule_constraint` attribute is set to
:attr:`.CONSTRAIN_END` or :attr:`.CONSTRAIN_BOTH`. The default value is
`datetime.datetime.now(pytz.utc)`.
:type end: :class:`datetime.datetime`
:param int schedule_timing: The value of the schedule timing.
:param str schedule_unit: The unit value of the schedule timing. Should be
one of 'min', 'h', 'd', 'w', 'm', 'y'.
:param int schedule_constraint: The schedule constraint. It is the index
of the schedule constraints value in
:class:`stalker.config.Config.task_schedule_constraints`.
:param int bid_timing: The initial bid for this Task. It can be used in
measuring how accurate the initial guess was. It will be compared against
the total amount of effort spend doing this task. Can be set to None,
which will be set to the schedule_timing_day argument value if there is
one or 0.
:param str bid_unit: The unit of the bid value for this Task. Should be one
of the 'min', 'h', 'd', 'w', 'm', 'y'.
:param bool is_milestone: A bool (True or False) value showing if this task
is a milestone which doesn't need any resource and effort.
:param int priority: It is a number between 0 to 1000 which defines the
priority of the :class:`.Task`. The higher the value the higher its
priority. The default value is 500. Mainly used by TaskJuggler.
Higher priority tasks will be scheduled to an early date or at least will
tried to be scheduled to an early date then a lower priority task (a task
that is using the same resources).
In complex projects, a task with a lower priority task may steal
resources from a higher priority task, this is due to the internals of
TaskJuggler, it tries to increase the resource utilization by letting the
lower priority task to be completed earlier than the higher priority
task. This is done in that way if the lower priority task is dependent of
more important tasks (tasks in critical path or tasks with critical
resources). Read TaskJuggler documentation for more information on how
TaskJuggler schedules tasks.
:param allocation_strategy: Defines the allocation strategy for resources
of a task with alternative resources. Should be one of ['minallocated',
'maxloaded', 'minloaded', 'order', 'random'] and the default value is
'minallocated'. For more information read the :class:`.Task` class
documetation.
:param persistent_allocation: Specifies that once a resource is picked from
the list of alternatives this resource is used for the whole task. The
default value is True. For more information read the :class:`.Task` class
documentation.
:param good: It is possible to attach a good to this Task to be able to
filter and group them later on.
"""
from stalker import defaults
__auto_name__ = False
__tablename__ = "Tasks"
__mapper_args__ = {'polymorphic_identity': "Task"}
task_id = Column(
"id", Integer, ForeignKey('Entities.id'), primary_key=True,
doc="""The ``primary_key`` attribute for the ``Tasks`` table used by
SQLAlchemy to map this Task in relationships.
"""
)
__id_column__ = 'task_id'
project_id = Column(
'project_id', Integer, ForeignKey('Projects.id'),
doc="""The id of the owner :class:`.Project` of this Task. This
attribute is mainly used by **SQLAlchemy** to map a :class:`.Project`
instance to a Task.
"""
)
_project = relationship(
'Project',
primaryjoin='Tasks.c.project_id==Projects.c.id',
back_populates='tasks',
uselist=False,
post_update=True,
)
tasks = synonym(
'children',
doc="""A synonym for the :attr:`.children` attribute used by the
descendants of the :class:`Task` class (currently :class:`.Asset`,
:class:`.Shot` and :class:`.Sequence` classes).
"""
)
is_milestone = Column(
Boolean,
doc="""Specifies if this Task is a milestone.
Milestones doesn't need any duration, any effort and any resources. It
is used to create meaningful dependencies between the critical stages
of the project.
"""
)
depends = association_proxy(
'task_depends_to',
'depends_to',
creator=lambda n: TaskDependency(depends_to=n)
)
dependent_of = association_proxy(
'task_dependent_of',
'task',
creator=lambda n: TaskDependency(task=n)
)
task_depends_to = relationship(
'TaskDependency',
back_populates='task',
cascade="all, delete-orphan",
primaryjoin='Tasks.c.id==Task_Dependencies.c.task_id',
doc="""A list of :class:`.Task` s that this one is depending on.
A CircularDependencyError will be raised when the task dependency
creates a circular dependency which means it is not allowed to create
a dependency for this Task which is depending on another one which in
some way depends to this one again."""
)
task_dependent_of = relationship(
'TaskDependency',
back_populates='depends_to',
cascade="all, delete-orphan",
primaryjoin='Tasks.c.id==Task_Dependencies.c.depends_to_id',
doc="""A list of :class:`.Task` s that this one is being depended by.
A CircularDependencyError will be raised when the task dependency
creates a circular dependency which means it is not allowed to create
a dependency for this Task which is depending on another one which in
some way depends to this one again.
"""
)
resources = relationship(
"User",
secondary="Task_Resources",
primaryjoin="Tasks.c.id==Task_Resources.c.task_id",
secondaryjoin="Task_Resources.c.resource_id==Users.c.id",
back_populates="tasks",
doc="The list of :class:`.User` s assigned to this Task."
)
alternative_resources = relationship(
"User",
secondary="Task_Alternative_Resources",
primaryjoin="Tasks.c.id==Task_Alternative_Resources.c.task_id",
secondaryjoin="Task_Alternative_Resources.c.resource_id==Users.c.id",
backref="alternative_resource_in_tasks",
doc="The list of :class:`.User` s assigned to this Task as an "
"alternative resource."
)
allocation_strategy = Column(
Enum(*defaults.allocation_strategy,
name='ResourceAllocationStrategy'),
default=defaults.allocation_strategy[0],
nullable=False,
doc="Please read :class:`.Task` class documentation for details."
)
persistent_allocation = Column(
Boolean,
default=True,
nullable=False,
doc="Please read :class:`.Task` class documentation for details."
)
watchers = relationship(
'User',
secondary='Task_Watchers',
primaryjoin='Tasks.c.id==Task_Watchers.c.task_id',
secondaryjoin='Task_Watchers.c.watcher_id==Users.c.id',
back_populates='watching',
doc="The list of :class:`.User` s watching this Task."
)
_responsible = relationship(
'User',
secondary='Task_Responsible',
primaryjoin='Tasks.c.id==Task_Responsible.c.task_id',
secondaryjoin='Task_Responsible.c.responsible_id==Users.c.id',
back_populates='responsible_of',
doc="The list of :class:`.User` s responsible from this Task."
)
priority = Column(
Integer,
doc="""An integer number between 0 and 1000 used by TaskJuggler to
determine the priority of this Task. The default value is 500."""
)
time_logs = relationship(
"TimeLog",
primaryjoin="TimeLogs.c.task_id==Tasks.c.id",
back_populates="task",
cascade='all, delete-orphan',
doc="""A list of :class:`.TimeLog` instances showing who and when has
spent how much effort on this task."""
)
versions = relationship(
"Version",
primaryjoin="Versions.c.task_id==Tasks.c.id",
back_populates="task",
cascade='all, delete-orphan',
doc="""A list of :class:`.Version` instances showing the files created