projectal.entities.task API documentation

# projectal.entities.task

View Source

      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

class Task(projectal.entity.Entity, projectal.linkers.ResourceLinker, projectal.linkers.SkillLinker, projectal.linkers.FileLinker, projectal.linkers.StageLinker, projectal.linkers.StaffLinker, projectal.linkers.RebateLinker, projectal.linkers.NoteLinker, projectal.linkers.TagLinker): View Source

     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.

@classmethod
def create(cls, holder, entities): View Source

    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.

def update_order(self, order_at_uuId, order_as=True): View Source

    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"]}])

def link_predecessor_task(self, predecessor_task): View Source

    71    def link_predecessor_task(self, predecessor_task):
    72        return self.__plan(self, predecessor_task, "add")

def relink_predecessor_task(self, predecessor_task): View Source

    74    def relink_predecessor_task(self, predecessor_task):
    75        return self.__plan(self, predecessor_task, "update")

def unlink_predecessor_task(self, predecessor_task): View Source

    77    def unlink_predecessor_task(self, predecessor_task):
    78        return self.__plan(self, predecessor_task, "delete")

def parents(self): View Source

     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.

def project_uuId(self): View Source

    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.

@classmethod
def add_task_template(cls, project, template): View Source

    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`

def reset_duration(self, calendars=None): View Source

    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
projectal.linkers.ResourceLinker
  link_resource
  relink_resource
  unlink_resource
projectal.linkers.SkillLinker
  link_skill
  relink_skill
  unlink_skill
projectal.linkers.FileLinker
  link_file
  unlink_file
projectal.linkers.StageLinker
  link_stage
  unlink_stage
projectal.linkers.StaffLinker
  link_staff
  relink_staff
  unlink_staff
projectal.linkers.RebateLinker
  link_rebate
  unlink_rebate
projectal.linkers.TagLinker
  link_tag
  unlink_tag
builtins.dict
  setdefault
  pop
  popitem
  keys
  items
  values
  fromkeys
  clear
  copy