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