projectal.entities.task

  1import datetime
  2
  3import projectal
  4from projectal.entity import Entity
  5from projectal.enums import DateLimit
  6from projectal.linkers import *
  7
  8
  9class Task(Entity, ResourceLinker, SkillLinker, FileLinker, StageLinker,
 10           StaffLinker, RebateLinker, NoteLinker, TagLinker):
 11    """
 12    Implementation of the [Task](https://projectal.com/docs/latest/#tag/Task) API.
 13    """
 14    _path = 'task'
 15    _name = 'task'
 16    _links = [ResourceLinker, SkillLinker, FileLinker, StageLinker, StaffLinker, RebateLinker,
 17              NoteLinker, TagLinker]
 18
 19    @classmethod
 20    def create(cls, holder, entities):
 21        """Create a Task
 22
 23        `holder`: An instance or the `uuId` of the owner
 24
 25        `entities`: `dict` containing the fields of the entity to be created,
 26        or a list of such `dict`s to create in bulk.
 27        """
 28        holder_id = holder['uuId'] if isinstance(holder, dict) else holder
 29        params = "?holder=" + holder_id
 30        out = super().create(entities, params)
 31
 32        # Tasks should always refer to their parent and project. We don't get this information
 33        # from the creation api method, but we can insert them ourselves because we know what
 34        # they are.
 35        def add_fields(obj):
 36            obj.set_readonly('projectRef', holder_id)
 37            obj.set_readonly('parent', obj.get('parent', holder_id))
 38
 39        if isinstance(out, dict):
 40            add_fields(out)
 41        if isinstance(out, list):
 42            for obj in out:
 43                add_fields(obj)
 44        return out
 45
 46    def update_order(self, order_at_uuId, order_as=True):
 47        url = "/api/task/update?order-at={}&order-as={}".format(
 48            order_at_uuId, 'true' if order_as else 'false')
 49        return api.put(url, [{'uuId': self['uuId']}])
 50
 51    def link_predecessor_task(self, predecessor_task):
 52        return self.__plan(self, predecessor_task, 'add')
 53
 54    def relink_predecessor_task(self, predecessor_task):
 55        return self.__plan(self, predecessor_task, 'update')
 56
 57    def unlink_predecessor_task(self, predecessor_task):
 58        return self.__plan(self, predecessor_task, 'delete')
 59
 60    @classmethod
 61    def __plan(cls, from_task, to_task, operation):
 62        url = '/api/task/plan/task/{}'.format(operation)
 63        payload = {
 64            'uuId': from_task['uuId'],
 65            'taskList': [to_task]
 66        }
 67        api.post(url, payload=payload)
 68        return True
 69
 70    def parents(self):
 71        """
 72        Return an ordered list of [name, uuId] pairs of this task's parents, up to
 73        (but not including) the root of the project.
 74        """
 75        payload = {
 76            "name": "Task Parents",
 77            "type": "msql",
 78            "start": 0,
 79            "limit": -1,
 80            "holder": "{}".format(self['uuId']),
 81            "select": [
 82                [
 83                    "TASK(one).PARENT_ALL_TASK.name"
 84                ],
 85                [
 86                    "TASK(one).PARENT_ALL_TASK.uuId"
 87                ]
 88            ],
 89        }
 90        list = api.query(payload)
 91        # Results come back in reverse order. Flip them around
 92        list.reverse()
 93        return list
 94
 95    def project_uuId(self):
 96        """Return the `uuId` of the Project that holds this Task."""
 97        payload = {
 98            "name": "Project that holds this task",
 99            "type": "msql", "start": 0, "limit": 1,
100            "holder": "{}".format(self['uuId']),
101            "select": [
102                ["TASK.PROJECT.uuId"]
103            ],
104        }
105        projects = api.query(payload)
106        for t in projects:
107            return t[0]
108        return None
109
110    @classmethod
111    def add_task_template(cls, project, template):
112        """Insert TaskTemplate `template` into Project `project`"""
113        url = '/api/task/task_template/add?override=false&group=false'
114        payload = {
115            'uuId': project['uuId'],
116            'templateList': [template]
117        }
118        api.post(url, payload)
119
120    def reset_duration(self, calendars=None):
121        """Set this task's duration based on its start and end dates while
122        taking into account the calendar for weekends and scheduled time off.
123
124        calendars is expected to be the list of calendar objects for the
125        location of the project that holds this task. You may provide this
126        list yourself for efficiency (recommended) - if not provided, it
127        will be fetched for you by issuing requests to the server.
128        """
129        if not calendars:
130            if 'projectRef' not in self:
131                task = projectal.Task.get(self)
132                project_ref = task['projectRef']
133            else:
134                project_ref = self['projectRef']
135            project = projectal.Project.get(project_ref, links=['LOCATION'])
136            for location in project.get('locationList', []):
137                calendars = location.calendar()
138                break
139
140        start = self.get('startTime')
141        end = self.get('closeTime')
142        if not start or start == DateLimit.Min:
143            return 0
144        if not end or end == DateLimit.Max:
145            return 0
146
147        # Build a list of weekday names that are non-working
148        base_non_working = set()
149        location_non_working = set()
150        for calendar in calendars:
151            if calendar['name'] == "base_calendar":
152                for item in calendar['calendarList']:
153                    if not item['isWorking']:
154                        base_non_working.add(item['type'])
155
156            if calendar['name'] == "location":
157                for item in calendar['calendarList']:
158                    if not item['isWorking']:
159                        # TODO: handle multi-day exceptions and times
160                        location_non_working.add(item['startDate'])
161
162        start = datetime.datetime.fromtimestamp(start / 1000)
163        end = datetime.datetime.fromtimestamp(end / 1000)
164        hours = 0
165        current = start
166        while current <= end:
167            if current.strftime('%A') in base_non_working:
168                current += datetime.timedelta(days=1)
169                continue
170            if current.strftime('%Y-%m-%d') in location_non_working:
171                current += datetime.timedelta(days=1)
172                continue
173            hours += 8
174            current += datetime.timedelta(days=1)
175
176        self['duration'] = hours * 60
 10class Task(Entity, ResourceLinker, SkillLinker, FileLinker, StageLinker,
 11           StaffLinker, RebateLinker, NoteLinker, TagLinker):
 12    """
 13    Implementation of the [Task](https://projectal.com/docs/latest/#tag/Task) API.
 14    """
 15    _path = 'task'
 16    _name = 'task'
 17    _links = [ResourceLinker, SkillLinker, FileLinker, StageLinker, StaffLinker, RebateLinker,
 18              NoteLinker, TagLinker]
 19
 20    @classmethod
 21    def create(cls, holder, entities):
 22        """Create a Task
 23
 24        `holder`: An instance or the `uuId` of the owner
 25
 26        `entities`: `dict` containing the fields of the entity to be created,
 27        or a list of such `dict`s to create in bulk.
 28        """
 29        holder_id = holder['uuId'] if isinstance(holder, dict) else holder
 30        params = "?holder=" + holder_id
 31        out = super().create(entities, params)
 32
 33        # Tasks should always refer to their parent and project. We don't get this information
 34        # from the creation api method, but we can insert them ourselves because we know what
 35        # they are.
 36        def add_fields(obj):
 37            obj.set_readonly('projectRef', holder_id)
 38            obj.set_readonly('parent', obj.get('parent', holder_id))
 39
 40        if isinstance(out, dict):
 41            add_fields(out)
 42        if isinstance(out, list):
 43            for obj in out:
 44                add_fields(obj)
 45        return out
 46
 47    def update_order(self, order_at_uuId, order_as=True):
 48        url = "/api/task/update?order-at={}&order-as={}".format(
 49            order_at_uuId, 'true' if order_as else 'false')
 50        return api.put(url, [{'uuId': self['uuId']}])
 51
 52    def link_predecessor_task(self, predecessor_task):
 53        return self.__plan(self, predecessor_task, 'add')
 54
 55    def relink_predecessor_task(self, predecessor_task):
 56        return self.__plan(self, predecessor_task, 'update')
 57
 58    def unlink_predecessor_task(self, predecessor_task):
 59        return self.__plan(self, predecessor_task, 'delete')
 60
 61    @classmethod
 62    def __plan(cls, from_task, to_task, operation):
 63        url = '/api/task/plan/task/{}'.format(operation)
 64        payload = {
 65            'uuId': from_task['uuId'],
 66            'taskList': [to_task]
 67        }
 68        api.post(url, payload=payload)
 69        return True
 70
 71    def parents(self):
 72        """
 73        Return an ordered list of [name, uuId] pairs of this task's parents, up to
 74        (but not including) the root of the project.
 75        """
 76        payload = {
 77            "name": "Task Parents",
 78            "type": "msql",
 79            "start": 0,
 80            "limit": -1,
 81            "holder": "{}".format(self['uuId']),
 82            "select": [
 83                [
 84                    "TASK(one).PARENT_ALL_TASK.name"
 85                ],
 86                [
 87                    "TASK(one).PARENT_ALL_TASK.uuId"
 88                ]
 89            ],
 90        }
 91        list = api.query(payload)
 92        # Results come back in reverse order. Flip them around
 93        list.reverse()
 94        return list
 95
 96    def project_uuId(self):
 97        """Return the `uuId` of the Project that holds this Task."""
 98        payload = {
 99            "name": "Project that holds this task",
100            "type": "msql", "start": 0, "limit": 1,
101            "holder": "{}".format(self['uuId']),
102            "select": [
103                ["TASK.PROJECT.uuId"]
104            ],
105        }
106        projects = api.query(payload)
107        for t in projects:
108            return t[0]
109        return None
110
111    @classmethod
112    def add_task_template(cls, project, template):
113        """Insert TaskTemplate `template` into Project `project`"""
114        url = '/api/task/task_template/add?override=false&group=false'
115        payload = {
116            'uuId': project['uuId'],
117            'templateList': [template]
118        }
119        api.post(url, payload)
120
121    def reset_duration(self, calendars=None):
122        """Set this task's duration based on its start and end dates while
123        taking into account the calendar for weekends and scheduled time off.
124
125        calendars is expected to be the list of calendar objects for the
126        location of the project that holds this task. You may provide this
127        list yourself for efficiency (recommended) - if not provided, it
128        will be fetched for you by issuing requests to the server.
129        """
130        if not calendars:
131            if 'projectRef' not in self:
132                task = projectal.Task.get(self)
133                project_ref = task['projectRef']
134            else:
135                project_ref = self['projectRef']
136            project = projectal.Project.get(project_ref, links=['LOCATION'])
137            for location in project.get('locationList', []):
138                calendars = location.calendar()
139                break
140
141        start = self.get('startTime')
142        end = self.get('closeTime')
143        if not start or start == DateLimit.Min:
144            return 0
145        if not end or end == DateLimit.Max:
146            return 0
147
148        # Build a list of weekday names that are non-working
149        base_non_working = set()
150        location_non_working = set()
151        for calendar in calendars:
152            if calendar['name'] == "base_calendar":
153                for item in calendar['calendarList']:
154                    if not item['isWorking']:
155                        base_non_working.add(item['type'])
156
157            if calendar['name'] == "location":
158                for item in calendar['calendarList']:
159                    if not item['isWorking']:
160                        # TODO: handle multi-day exceptions and times
161                        location_non_working.add(item['startDate'])
162
163        start = datetime.datetime.fromtimestamp(start / 1000)
164        end = datetime.datetime.fromtimestamp(end / 1000)
165        hours = 0
166        current = start
167        while current <= end:
168            if current.strftime('%A') in base_non_working:
169                current += datetime.timedelta(days=1)
170                continue
171            if current.strftime('%Y-%m-%d') in location_non_working:
172                current += datetime.timedelta(days=1)
173                continue
174            hours += 8
175            current += datetime.timedelta(days=1)
176
177        self['duration'] = hours * 60

Implementation of the Task API.

@classmethod
def create(cls, holder, entities):
20    @classmethod
21    def create(cls, holder, entities):
22        """Create a Task
23
24        `holder`: An instance or the `uuId` of the owner
25
26        `entities`: `dict` containing the fields of the entity to be created,
27        or a list of such `dict`s to create in bulk.
28        """
29        holder_id = holder['uuId'] if isinstance(holder, dict) else holder
30        params = "?holder=" + holder_id
31        out = super().create(entities, params)
32
33        # Tasks should always refer to their parent and project. We don't get this information
34        # from the creation api method, but we can insert them ourselves because we know what
35        # they are.
36        def add_fields(obj):
37            obj.set_readonly('projectRef', holder_id)
38            obj.set_readonly('parent', obj.get('parent', holder_id))
39
40        if isinstance(out, dict):
41            add_fields(out)
42        if isinstance(out, list):
43            for obj in out:
44                add_fields(obj)
45        return out

Create a Task

holder: An instance or the uuId of the owner

entities: dict containing the fields of the entity to be created, or a list of such dicts to create in bulk.

def update_order(self, order_at_uuId, order_as=True):
47    def update_order(self, order_at_uuId, order_as=True):
48        url = "/api/task/update?order-at={}&order-as={}".format(
49            order_at_uuId, 'true' if order_as else 'false')
50        return api.put(url, [{'uuId': self['uuId']}])
def parents(self):
71    def parents(self):
72        """
73        Return an ordered list of [name, uuId] pairs of this task's parents, up to
74        (but not including) the root of the project.
75        """
76        payload = {
77            "name": "Task Parents",
78            "type": "msql",
79            "start": 0,
80            "limit": -1,
81            "holder": "{}".format(self['uuId']),
82            "select": [
83                [
84                    "TASK(one).PARENT_ALL_TASK.name"
85                ],
86                [
87                    "TASK(one).PARENT_ALL_TASK.uuId"
88                ]
89            ],
90        }
91        list = api.query(payload)
92        # Results come back in reverse order. Flip them around
93        list.reverse()
94        return list

Return an ordered list of [name, uuId] pairs of this task's parents, up to (but not including) the root of the project.

def project_uuId(self):
 96    def project_uuId(self):
 97        """Return the `uuId` of the Project that holds this Task."""
 98        payload = {
 99            "name": "Project that holds this task",
100            "type": "msql", "start": 0, "limit": 1,
101            "holder": "{}".format(self['uuId']),
102            "select": [
103                ["TASK.PROJECT.uuId"]
104            ],
105        }
106        projects = api.query(payload)
107        for t in projects:
108            return t[0]
109        return None

Return the uuId of the Project that holds this Task.

@classmethod
def add_task_template(cls, project, template):
111    @classmethod
112    def add_task_template(cls, project, template):
113        """Insert TaskTemplate `template` into Project `project`"""
114        url = '/api/task/task_template/add?override=false&group=false'
115        payload = {
116            'uuId': project['uuId'],
117            'templateList': [template]
118        }
119        api.post(url, payload)

Insert TaskTemplate template into Project project

def reset_duration(self, calendars=None):
121    def reset_duration(self, calendars=None):
122        """Set this task's duration based on its start and end dates while
123        taking into account the calendar for weekends and scheduled time off.
124
125        calendars is expected to be the list of calendar objects for the
126        location of the project that holds this task. You may provide this
127        list yourself for efficiency (recommended) - if not provided, it
128        will be fetched for you by issuing requests to the server.
129        """
130        if not calendars:
131            if 'projectRef' not in self:
132                task = projectal.Task.get(self)
133                project_ref = task['projectRef']
134            else:
135                project_ref = self['projectRef']
136            project = projectal.Project.get(project_ref, links=['LOCATION'])
137            for location in project.get('locationList', []):
138                calendars = location.calendar()
139                break
140
141        start = self.get('startTime')
142        end = self.get('closeTime')
143        if not start or start == DateLimit.Min:
144            return 0
145        if not end or end == DateLimit.Max:
146            return 0
147
148        # Build a list of weekday names that are non-working
149        base_non_working = set()
150        location_non_working = set()
151        for calendar in calendars:
152            if calendar['name'] == "base_calendar":
153                for item in calendar['calendarList']:
154                    if not item['isWorking']:
155                        base_non_working.add(item['type'])
156
157            if calendar['name'] == "location":
158                for item in calendar['calendarList']:
159                    if not item['isWorking']:
160                        # TODO: handle multi-day exceptions and times
161                        location_non_working.add(item['startDate'])
162
163        start = datetime.datetime.fromtimestamp(start / 1000)
164        end = datetime.datetime.fromtimestamp(end / 1000)
165        hours = 0
166        current = start
167        while current <= end:
168            if current.strftime('%A') in base_non_working:
169                current += datetime.timedelta(days=1)
170                continue
171            if current.strftime('%Y-%m-%d') in location_non_working:
172                current += datetime.timedelta(days=1)
173                continue
174            hours += 8
175            current += datetime.timedelta(days=1)
176
177        self['duration'] = hours * 60

Set this task's duration based on its start and end dates while taking into account the calendar for weekends and scheduled time off.

calendars is expected to be the list of calendar objects for the location of the project that holds this task. You may provide this list yourself for efficiency (recommended) - if not provided, it will be fetched for you by issuing requests to the server.