# -*- coding: utf8 -*-
from __future__ import absolute_import, division, print_function
from __future__ import unicode_literals
from pylero._compatible import basestring
import suds
import os
import re
import copy
import json
from pylero.exceptions import PyleroLibException
from pylero.base_polarion import BasePolarion, Configuration
from pylero.approval import Approval
from pylero.approval import ArrayOfApproval
from pylero.attachment import Attachment
from pylero.attachment import ArrayOfAttachment
from pylero.user import User
from pylero.user import ArrayOfUser
from pylero.category import Category
from pylero.category import ArrayOfCategory
from pylero.comment import Comment
from pylero.comment import ArrayOfComment
from pylero.custom import Custom
from pylero.custom import ArrayOfCustom
from pylero.custom_field import CustomField
from pylero.custom_field_type import CustomFieldType
from pylero.enum_custom_field_type import EnumCustomFieldType
from pylero.text import Text
from pylero.externally_linked_work_item import ExternallyLinkedWorkItem
from pylero.externally_linked_work_item \
import ArrayOfExternallyLinkedWorkItem
from pylero.hyperlink import Hyperlink
from pylero.hyperlink import ArrayOfHyperlink
from pylero.revision import Revision
from pylero.revision import ArrayOfRevision
from pylero.linked_work_item import LinkedWorkItem
from pylero.linked_work_item import ArrayOfLinkedWorkItem
from pylero.subterra_uri import SubterraURI
from pylero.planning_constraint import PlanningConstraint
from pylero.planning_constraint import ArrayOfPlanningConstraint
from pylero.enum_option_id import EnumOptionId
# ArrayOfEnumOptionId is used in dynamic code for custom fields
from pylero.enum_option_id import ArrayOfEnumOptionId # NOQA
from pylero.priority_option_id import PriorityOptionId
from pylero.project import Project
from pylero.test_steps import TestSteps
from pylero.test_step import TestStep
from pylero.time_point import TimePoint
from pylero.work_record import WorkRecord
from pylero.work_record import ArrayOfWorkRecord
from pylero.workflow_action import WorkflowAction
from pylero.base_polarion import tx_wrapper
class _WorkItem(BasePolarion):
"""Object to handle the Polarion WSDL tns5:WorkItem class
Attributes:
approvals (ArrayOfApproval)
assignee (ArrayOfUser)
attachments (ArrayOfAttachment)
author (User)
auto_suspect (boolean)
categories (ArrayOfCategory)
comments (ArrayOfComment)
created (dateTime)
custom_fields (ArrayOfCustom)
description (Text)
due_date (date)
externally_linked_work_items (ArrayOfExternallyLinkedWorkItem)
hyperlinks (ArrayOfHyperlink)
initial_estimate (duration)
linked_revisions (ArrayOfRevision)
linked_revisions_derived (ArrayOfRevision)
linked_work_items (ArrayOfLinkedWorkItem)
linked_work_items_derived (ArrayOfLinkedWorkItem)
location (Location)
module_uri (SubterraURI)
outline_number (string)
planned_end (dateTime)
planned_in (ArrayOfPlan)
planned_start (dateTime)
planning_constraints (ArrayOfPlanningConstraint)
previous_status (EnumOptionId)
priority (PriorityOptionId)
project_id (Project)
remaining_estimate (duration)
resolution (EnumOptionId)
resolved_on (dateTime)
severity (EnumOptionId)
status (EnumOptionId)
time_point (TimePoint)
time_spent (duration)
title (string)
type (EnumOptionId)
updated (dateTime)
work_item_id (string)
work_records (ArrayOfWorkRecord)
"""
_cls_suds_map = {
"approvals":
{"field_name": "approvals",
"is_array": True,
"cls": Approval,
"arr_cls": ArrayOfApproval,
"inner_field_name": "Approval"},
"assignee":
{"field_name": "assignee",
"is_array": True,
"cls": User,
"arr_cls": ArrayOfUser,
"inner_field_name": "User"},
"attachments":
{"field_name": "attachments",
"is_array": True,
"cls": Attachment,
"arr_cls": ArrayOfAttachment,
"inner_field_name": "Attachment"},
"author":
{"field_name": "author",
"cls": User},
"auto_suspect": "autoSuspect",
"categories":
{"field_name": "categories",
"is_array": True,
"cls": Category,
"arr_cls": ArrayOfCategory,
"inner_field_name": "Category"},
"comments":
{"field_name": "comments",
"is_array": True,
"cls": Comment,
"arr_cls": ArrayOfComment,
"inner_field_name": "Comment"},
"created": "created",
# the custom field attribute has been changed to be a protected attr.
# All interaction with custom fields should be done directly with the
# derived attribute.
"_custom_fields":
{"field_name": "customFields",
"is_array": True,
"cls": Custom,
"arr_cls": ArrayOfCustom,
"inner_field_name": "Custom"},
"description":
{"field_name": "description",
"cls": Text},
"due_date": "dueDate",
"externally_linked_work_items":
{"field_name": "externallyLinkedWorkItems",
"is_array": True,
"cls": ExternallyLinkedWorkItem,
"arr_cls": ArrayOfExternallyLinkedWorkItem,
"inner_field_name": "ExternallyLinkedWorkItem"},
"hyperlinks":
{"field_name": "hyperlinks",
"is_array": True,
"cls": Hyperlink,
"arr_cls": ArrayOfHyperlink,
"inner_field_name": "Hyperlink"},
"initial_estimate": "initialEstimate",
"linked_revisions":
{"field_name": "linkedRevisions",
"is_array": True,
"cls": Revision,
"arr_cls": ArrayOfRevision,
"inner_field_name": "Revision"},
"linked_revisions_derived":
{"field_name": "linkedRevisionsDerived",
"is_array": True,
"cls": Revision,
"arr_cls": ArrayOfRevision,
"inner_field_name": "Revision"},
"linked_work_items":
{"field_name": "linkedWorkItems",
"is_array": True,
"cls": LinkedWorkItem,
"arr_cls": ArrayOfLinkedWorkItem,
"inner_field_name": "LinkedWorkItem"},
"linked_work_items_derived":
{"field_name": "linkedWorkItemsDerived",
"is_array": True,
"cls": LinkedWorkItem,
"arr_cls": ArrayOfLinkedWorkItem,
"inner_field_name": "LinkedWorkItem"},
"location": "location",
"module_uri":
{"field_name": "moduleURI",
"cls": SubterraURI},
"outline_number": "outlineNumber",
"planned_end": "plannedEnd",
# planned_in completed in the _fix_circular_imports func
"planned_in":
{"field_name": "plannedIn"},
"planned_start": "plannedStart",
"planning_constraints":
{"field_name": "planningConstraints",
"is_array": True,
"cls": PlanningConstraint,
"arr_cls": ArrayOfPlanningConstraint,
"inner_field_name": "PlanningConstraint"},
"previous_status":
{"field_name": "previousStatus",
"cls": EnumOptionId,
"enum_id": "status"},
# priority is technically an enum, but it also accepts other values,
# so we didn't include it here. If you want to enforce the enum in
# the code, add "enum_id": "priority" to the prioirity dict
"priority":
{"field_name": "priority",
"cls": PriorityOptionId},
"project_id":
{"field_name": "project",
"cls": Project},
"remaining_estimate": "remainingEstimate",
"resolution":
{"field_name": "resolution",
"cls": EnumOptionId,
"enum_id": "resolution"},
"resolved_on": "resolvedOn",
"severity":
{"field_name": "severity",
"cls": EnumOptionId,
"enum_id": "severity"},
"status":
{"field_name": "status",
"cls": EnumOptionId,
"enum_id": "status"},
"time_point":
{"field_name": "timePoint",
"cls": TimePoint},
"time_spent": "timeSpent",
"title": "title",
"type":
{"field_name": "type",
"cls": EnumOptionId,
"enum_id": "workitem-type",
"enum_override": ["heading"]},
"updated": "updated",
"work_item_id": "id",
"work_records":
{"field_name": "workRecords",
"is_array": True,
"cls": WorkRecord,
"arr_cls": ArrayOfWorkRecord,
"inner_field_name": "WorkRecord"},
"uri": "_uri",
"_unresolved": "_unresolved"}
_id_field = "work_item_id"
_obj_client = "tracker_client"
_obj_struct = "tns3:WorkItem"
@classmethod
def create(cls, project_id, wi_type, title, desc, status, **kwargs):
"""Creates a new work item with the given content. The project and the
type have to be set for the workitem for the creation to succeed. The
uri MUST NOT be set otherwise the creation will fail. To create a work
item in a specific location e.g. a LiveDoc set the location of the
work item to the desired target location. To create a work item in a
specific Module/Document set the Module/Document of the work item to
the desired target Module/Document.
Args:
project_id: id of project to create work item in
wi_type: type of work item (testcase,...)
title: title of WorkItem
desc: description of WorkItem
status: initial status of the WorkItem, draft by default
kwargs: named parameters that are added to the object and updated
after the object is created. Required custom fields must be
passed in.
Returns:
new _WorkItem
References:
Tracker.createWorkItem
"""
wi = cls()
wi.project_id = project_id
wi.type = wi_type
wi.title = title
wi.description = desc
wi.status = status
for field in kwargs:
setattr(wi, field, kwargs[field])
wi_uri = cls.session.tracker_client.service.createWorkItem(
wi._suds_object)
new_wi = cls(uri=wi_uri)
return new_wi
@classmethod
def get_query_result_count(cls, query):
"""Counts number of workitems returned by given query.
Args:
query: the lucene query to be used.
Returns
int
References:
Tracker.getWorkItemsCount
"""
return cls.session.tracker_client.service.getWorkItemsCount(query)
@classmethod
def get_defined_custom_field_types(cls, project_id, wi_type):
"""Gets all the custom fields defined for the specified wi_type.
the custom fields are all either of type CustomFieldType or
EnumCustomFieldType in the case where the field is an enumeration.
These 2 classes are mostly interchangeable.
Args:
project_id: the project to get the custom fields from
wi_type: The type of work item to get the custom fields for
Returns:
list of all the custom fields
References:
tracker.getDefinedCustomFieldTypes
"""
if not cls._cache["custom_field_types"].get(wi_type):
cfts = cls.session.tracker_client.service. \
getDefinedCustomFieldTypes(project_id, wi_type)
cls._cache["custom_field_types"][wi_type] = cfts
else:
cfts = cls._cache["custom_field_types"].get(wi_type)
results = [CustomFieldType(suds_object=item)
if isinstance(item,
CustomFieldType()._suds_object.__class__)
else EnumCustomFieldType(suds_object=item)
for item in cfts]
return results
@classmethod
def query(cls, query, is_sql=False, fields=["work_item_id"],
sort="work_item_id", limit=-1, baseline_revision=None,
query_uris=False):
"""Searches for Work Items.
Notes:
The query function only returns a partially populated object with
the fields passed in (by default work_item_id) and the uri field.
The uri field is the Polarion unique object identifier which can
be used (among other things) to instantiate objects. Because not
all fields are returned in the object, this can cause problems
when trying to update an object that was returned by the query
function. Another issue is that certain fields are references and
are not retrieved by using their standard field names.
Examples of problems are:
* trying to use custom fields
* updating an object that has required fields that were not
retrieved
* trying to retrieve the project_id field.
Because of these issues, recommended usage is as follows:
{WI_TYPE} is TestCase, Requirement, ...
query_results = {WI_TYPE}.query("query string")
for item in query_results:
wi = {WI_TYPE}(uri=item.uri)
... # do what you want with the object
Args:
query: query, either Lucene or SQL
is_sql (bool): determines if the query is SQL or Lucene
fields: array of field names to fill in the returned
WorkItems (can be null). For nested structures in
the lists you can use following syntax to include only
subset of fields: myList.LIST.key
(e.g. linkedWorkItems.LIST.role).
For custom fields you can specify which fields you want to
be filled using following syntax:
customFields.CUSTOM_FIELD_ID (e.g. customFields.risk).
Default: list containing "work_item_id".
sort: Lucene sort string (can be null), default: work_item_id
limit: how many results to return (-1 means everything, default)
baseline_revision (str): if populated, query done in specified rev
default: None
query_uris (bool): returns a list of URI of the WorkItems found,
default: False
Returns:
list of _WorkItem objects
References:
Tracker.queryWorkItemUris
Tracker.queryWorkItemUrisBySQL
Tracker.queryWorkItemUrisInBaseline
Tracker.queryWorkItemUrisInBaselineBySQL
Tracker.queryWorkItemUrisInBaselineLimited
Tracker.queryWorkItemUrisLimited
Tracker.queryWorkItems
Tracker.queryWorkItemsBySQL
Tracker.queryWorkItemsInBaseline
Tracker.queryWorkItemsInBaselineBySQL
Tracker.queryWorkItemsInBaselineLimited
Tracker.queryWorkItemsLimited
"""
parms = [query]
if not is_sql:
parms.append(sort)
if baseline_revision:
parms.append(baseline_revision)
if not query_uris:
p_fields = cls._convert_obj_fields_to_polarion(fields)
parms.append(p_fields)
if not is_sql and limit != -1:
parms.append(limit)
if not query_uris:
base_name = "queryWorkItems"
else:
base_name = "queryWorkItemUris"
if baseline_revision:
base_name += "InBaseline"
if is_sql:
base_name += "BySQL"
elif limit != -1:
# You can't have both SQL and limited.
base_name += "Limited"
wis = getattr(cls.session.tracker_client.service, base_name)(*parms)
if query_uris:
return wis
else:
lst_wi = [cls(suds_object=wi) for wi in wis]
return lst_wi
def __init__(self, project_id=None, work_item_id=None, suds_object=None,
uri=None, fields=None, revision=None):
"""WorkItem constructor.
Args:
project_id: the Polarion project that the _WorkItem is located
in.
work_item_id: when given, the object is populated with the
_WorkItem's data . Requires project_id parameter
suds_object: Polarion _WorkItem object. When given, the object
is populated by object data.
uri: the uri that references the Polarion _WorkItem
fields: the fields that are requested to be populated.
if this is null then it will return all fields.
revision: if given, get the _WorkItem in the specified revision
Is only relevant if URI is given.
Notes:
Either test_run_id and project or suds_object or uri can be passed
in or none of them. If none of the identifying parameters are
passed in an empty object is created
References:
Tracker.getWorkItemById
Tracker.getWorkItemByIdsWithFields
Tracker.getWorkItemByUri
Tracker.getWorkItemByUriInRevision
Tracker.getWorkItemByUriInRevisionWithFields
Tracker.getWorkItemByUriWithFields
"""
self._required_fields = getattr(self, "_required_fields", [])
self._changed_fields = getattr(self, "_changed_fields", {})
# because other classes inherit from this. If super uses self.__class__
# it will be a infinite loop for the derived class.
super(_WorkItem, self).__init__(work_item_id, suds_object)
p_fields = self._convert_obj_fields_to_polarion(fields)
if work_item_id or uri:
function_name = "getWorkItemBy"
parms = []
if work_item_id:
function_name += "Id" if not p_fields else "IdsWithFields"
parms = [project_id, work_item_id] + \
([p_fields] if p_fields else [])
elif uri:
function_name += "Uri" + ("InRevision" if revision else "") + \
("WithFields" if p_fields else "")
parms = [uri] + ([revision] if revision else []) + \
([p_fields] if p_fields else [])
self._suds_object = getattr(self.session.tracker_client.service,
function_name)(*parms)
if not suds_object:
if getattr(self._suds_object, "_unresolvable", True):
raise PyleroLibException(
"The WorkItem {0} was not found.".format(work_item_id))
# if it is a suds object, only relevant fields are passed in.
if not self.project_id and not suds_object:
self.project_id = self.default_project
def _fix_circular_refs(self):
# This module imports plan and plan imports this module.
# The module references itself as a class attribute, which is not
# allowed, so the self reference is defined here.
from pylero.plan import Plan
from pylero.plan import ArrayOfPlan
self._cls_suds_map["planned_in"]["is_array"] = True
self._cls_suds_map["planned_in"]["cls"] = Plan
self._cls_suds_map["planned_in"]["arr_cls"] = ArrayOfPlan
self._cls_suds_map["planned_in"]["inner_field_name"] = "Plan"
def add_approvee(self, approvee_id):
"""method add_approvee adds an approvee to the current _WorkItem.
The approvee passed in must be an allowed approver
Args:
approvee_id (str):er_id of approvee to add
Returns:
None
References:
Tracker.addApprovee
"""
self._verify_obj()
# verify that the user is allowed to be an approver.
allowed = self.get_allowed_approvers()
allowed_ids = [u.user_id for u in allowed]
if approvee_id not in (allowed_ids):
raise PyleroLibException("%s is not an allowed assignee" %
approvee_id)
self.session.tracker_client.service.addApprovee(self.uri, approvee_id)
def add_assignee(self, assignee_id):
"""method add_assignee adds an assignee to the current _WorkItem
The assignee passed in must be an allowed assignee
Args:
assignee_id (str): user_id of assignee to add
Returns:
bool
References:
Tracker.addAssignee
"""
self._verify_obj()
# verify that the user is allowed to be an assignee.
allowed = self.get_allowed_assignees()
allowed_ids = [u.user_id for u in allowed]
if assignee_id not in (allowed_ids):
raise PyleroLibException("%s is not an allowed assignee" %
assignee_id)
return self.session.tracker_client.service.addAssignee(self.uri,
assignee_id)
def add_category(self, category_id):
"""method add_category adds a category to the current _WorkItem
The category passed in must be a defined category in the current project
Args:
category_id (str): id of the category to add
Returns:
bool
References:
Tracker.addCategoy
"""
self._verify_obj()
proj = Project(self.project_id)
cats = proj.get_categories()
cat_ids = [cat.category_id for cat in cats]
if category_id not in cat_ids:
raise PyleroLibException("the category_id must be one of: %s" %
cat_ids)
return self.session.tracker_client.service.addCategory(self.uri,
category_id)
def add_external_linked_revision(self, repository_name, revision_id):
"""method add_external_linked_revision links a revision from external
repository.
Args:
repository_name (str): name of external repository
revision_id (str): the id of the revision to add
Returns:
bool
References:
Tracker.addExternalLinkedRevision
"""
self._verify_obj()
return self.session.tracker_client.service.addExternalLinkedRevision(
self.uri, repository_name, revision_id)
def add_hyperlink(self, url, role):
"""method add_hyperlink adds a hyperlink to a _WorkItem
Args:
url: the url of the hyperlink to add.
role: the role of the hyperlink to add.
Returns:
bool
References:
Tracker.addHyperlink
"""
self._verify_obj()
self.check_valid_field_values(role, "hyperlink-role", {})
suds_role = EnumOptionId(role)._suds_object
return self.session.tracker_client.service.addHyperlink(
self.uri, url, suds_role)
def add_linked_item(self, linked_work_item_id, role,
revision=None, suspect=None):
"""method add_linked_item adds a linked _WorkItem to current _WorkItem
The linking is done to the "child" object. For example, if you have a
Test Case that verifies a requirement, you would add the
linked item to the Test Case with the "verifies" role and
not the Requirement.
Args:
linked_work_item_id - the URI of the target work item the link
points to.
role (str): the role of the hyperlink to add.
revision (str): optional, specific revision for linked item
(None means HEAD revision)
default: None
suspect (bool): true if the link should be marked with suspect flag
Only valid if revision is set.
default: None
Returns:
bool
References:
Tracker.addLinkedItem
Tracker.addLinkedItemWithRev
"""
self._verify_obj()
# validates the role. Will raise PyleroLibException if invalid
self.check_valid_field_values(role, "workitem-link-role", {})
wi_linked = _WorkItem(work_item_id=linked_work_item_id,
project_id=self.project_id)
enum_role = EnumOptionId(role)._suds_object
function_name = "addLinkedItem"
parms = [self.uri, wi_linked.uri, enum_role]
if revision:
function_name += "WithRev"
parms += [revision, suspect]
return getattr(self.session.tracker_client.service,
function_name)(*parms)
def add_linked_revision(self, revision):
"""method add_linked_revision links a revision to the current _WorkItem
Args:
revision: the revision to add.
Returns:
bool
References:
Tracker.addLinkedRevision
"""
self._verify_obj()
return self.session.tracker_client.service.addLinkedRevision(self.uri,
revision)
def create_attachment(self, path, title):
"""method create_attachment adds the given attachment to the current
_WorkItem
Args:
path: file path to upload
title: u.User friendly name of the file
Notes:
Raises an error if the _WorkItem object is not populated
References:
Tracker.createAttachment
"""
self._verify_obj()
data = self._get_file_data(path)
filename = os.path.basename(path)
self.session.tracker_client.service. \
createAttachment(self.uri, filename, title, data)
def create_comment(self, content):
"""method create_comment adds a comment to the current _WorkItem
Args:
content (Text or str)
Returns:
None
References:
Tracker.createComment
"""
self._verify_obj()
if content:
if isinstance(content, basestring):
obj_content = Text(content)
suds_content = obj_content._suds_object
elif isinstance(content, Text):
suds_content = content._suds_object
else: # is a suds object
suds_content = content
else:
suds_content = suds.null()
self.session.tracker_client.service.createComment(self.uri,
suds_content)
def create_work_record(self, user_id, date_worked, time_spent,
record_type=None, record_comment=None):
"""Creates a work record
Args:
user_id: the user for the work record.
date_worked: the date of the work record.
time_spent: the time spent for the work record.
record_type: the type of the work record, default: None
record-comment: work record comment, default: None
Returns:
None
References:
Tracker.createWorkRecord
Tracker.createWorkRecordWithTypeAndComment
"""
self._verify_obj()
user = User(user_id=user_id)
function_name = "createWorkRecord"
parms = [self.uri, user._suds_object, date_worked]
if record_type or record_comment:
if not record_type:
record_type = suds.null()
if not record_comment:
record_comment = suds.null()
function_name += "WithTypeAndComment"
parms += [record_type, time_spent, record_comment]
else:
parms += [time_spent]
getattr(self.session.tracker_client.service, function_name)(*parms)
def delete_attachment(self, attachment_id):
"""method delete_attachment removes the specified attachment from the
current _WorkItem
Args:
attachment_id (str): the ID of the attachment to be removed.
Returns:
None
References:
Tracker.deleteAttachment
"""
self._verify_obj()
self.session.tracker_client.service.deleteAttachment(
self.uri, attachment_id)
def do_auto_suspect(self):
"""Triggers auto suspect.
Args:
None
Returns:
None
References:
Tracker.doAutoSuspect
"""
self._verify_obj()
self.session.tracker_client.service.doAutoSuspect(self.uri)
def do_auto_assign(self):
"""Triggers auto assignment.
Args:
None
Returns:
None
References:
Tracker.doAutoAssign
"""
self._verify_obj()
self.session.tracker_client.service.doAutoAssign(self.uri)
def edit_approval(self, approvee_id, status):
"""Changes the status of an approval.
Args:
approveeId: the user id of the approvee.
status: the new status to set.
Returns:
None
References:
Tracker.editApproval
"""
self._verify_obj()
# verify that the user exists.
User(approvee_id)
self.check_valid_field_values(status, "approval-status", {})
suds_status = EnumOptionId(status)._suds_object
self.session.tracker_client.service.editApproval(
self.uri, approvee_id, suds_status)
def get_allowed_approvers(self):
"""Gets all allowed approvers"
Args:
None
Returns:
list of u.Users
References:
Tracker.getAllowedApprovers
"""
users = []
for suds_user in self.session.tracker_client.service. \
getAllowedApprovers(self.uri):
users.append(User(suds_object=suds_user))
return users
def get_allowed_assignees(self):
"""Gets all allowed assignees"
Args:
None
Returns:
list of u.Users
References:
Tracker.getAllowedAssignees
"""
users = []
for suds_user in self.session.tracker_client.service. \
getAllowedAssignees(self.uri):
users.append(User(suds_object=suds_user))
return users
def get_available_actions(self):
"""Gets the actions that can be used on the workflow object in its
current state. Conditions of the action are checked and those with
failed condition(s) are not returned.
Args:
None
Returns:
list of WorkFlowActions
References:
Tracker.getAvailableActions
"""
self._verify_obj()
actions = []
for suds_action in self.session.tracker_client.service. \
getAvailableActions(self.uri):
actions.append(WorkflowAction(suds_object=suds_action))
return actions
def get_back_linked_work_items(self):
"""Gets the back linked work items, work items linking to the specified
work item.
Args:
None
Returns:
list of LinkedWorkItems
References:
Tracker.getbackLinkedWorkitems
"""
self._verify_obj()
linked_work_items = []
for suds_lwi in self.session.tracker_client.service. \
getBackLinkedWorkitems(self.uri):
linked_work_items.append(LinkedWorkItem(suds_object=suds_lwi))
return linked_work_items
def get_custom_field(self, key):
"""method get_custom_field gets a custom field of a work item.
Args:
key: The key of the custom field
Returns:
CustomField object
References:
Tracker.getCustomField
"""
self._verify_obj()
suds_custom = self.session.tracker_client.service.getCustomField(
self.uri, key)
return CustomField(suds_object=suds_custom)
def get_custom_field_keys(self):
"""method get_custom_field_keys Gets the names of defined custom
fields.
Args:
None
Returns:
list of keys
References:
Tracker.getCustomFieldKeys
"""
self._verify_obj()
return self.session.tracker_client.service.getCustomFieldKeys(self.uri)
def get_custom_field_type(self, key):
"""method get_custom_field_type gets custom field definition of a
work item.
Args:
key: The key of the custom field
Returns:
CustomFieldType object
References:
Tracker.getCustomFieldType
"""
self._verify_obj()
suds_custom = self.session.tracker_client.service.getCustomFieldType(
self.uri, key)
return CustomFieldType(suds_object=suds_custom)
def get_custom_field_types(self):
"""method get_custom_field_types gets all custom field definitions for
a specific workitem fields.
Args:
None
Returns:
list of CustomFieldType
References:
Tracker.getCustomFieldTypes
"""
self._verify_obj()
custom_types = []
for suds_custom in self.session.tracker_client.service. \
getCustomFieldTypes(self.uri):
custom_types.append(CustomFieldType(suds_object=suds_custom))
def get_enum_control_key_for_id(self, enum_id):
"""Gets the enumeration control key for the specified work item key.
Args:
enum_id: the id of the enumeration to get the control key for.
Returns:
Enumeration control key
References:
Tracker.getEnumControlKeyForId
"""
return self.session.tracker_client.service.getEnumControlKeyForId(
self.project_id, enum_id)
def get_enum_control_key_for_key(self, key):
"""Gets the enumeration control key for the specified work item key.
Args:
key: the key of the field containing the enumeration to get the
control key for
Returns:
Enumeration control key
References:
Tracker.getEnumControlKeyForId
"""
return self.session.tracker_client.service.getEnumControlKeyForId(
self.project_id, key)
def get_initial_workflow_action(self, work_item_type=None):
"""Gets the initial workflow action for the specified object, returns
None if there is no initial action for the corresponding workflow.
Args:
work_item_type: the type of the work item to get the
available actions from. can be None
default: None
Returns:
WorkFlowAction object
References:
Tracker.getInitialWorkflowAction
Tracker.getInitialWorkflowActionForProjectAndType
"""
self._verify_obj()
function_name = "getInitialWorkflowAction"
parm = [self.uri]
if work_item_type:
function_name += "ForProjectAndType"
parm += [work_item_type]
suds_action = getattr(self.session.tracker_client.service,
function_name)(*parm)
return WorkflowAction(suds_object=suds_action)
def get_test_steps(self):
"""method get_test_steps retrieves the test steps of the current
WorkItem. If the _WorkItem is not populated, it returns an exception.
Args:
None
Returns:
a TestSteps object
References:
Tracker.getTestSteps
"""
self._verify_obj()
suds_ts = self.session.test_management_client.service. \
getTestSteps(self.uri)
return TestSteps(suds_object=suds_ts)
def get_unavailable_actions(self):
"""Gets the actions that can not be used on the work item in the
current state because of unsatisfied condition(s). Conditions of the
action are checked and those with failed condition(s) are returned.
The reason of unavailability is returned by
WorkflowAction.getUnavailabilityMessage().
Args:
None
Returns:
list of WorkflowAction objects
References:
Tracker.getUnavailableActions
"""
self._verify_obj()
actions = []
for suds_action in self.session.tracker_client.service. \
getUnavailableActions(self.uri):
actions.append(WorkflowAction(suds_object=suds_action))
return actions
def perform_workflow_action(self, action_id):
"""Executes a workflow action. The actions that can be performed can be
received by _WorkItem.getAvailableActions(java.lang.String).
Args:
action_id: the id of the action to execute.
Retuns:
None
References:
Tracker.performWorkflowAction
"""
self._verify_obj()
self.session.tracker_client.service.performWorkflowAction(self.uri,
action_id)
def remove_assignee(self, assignee_id):
"""removes an assignee from the _WorkItem.
Args:
assignee_id: user id of the assignee to remove
Returns:
bool
References:
Tracker.removeAssignee
"""
self._verify_obj()
return self.session.tracker_client.service.removeAssignee(self.uri,
assignee_id)
def remove_category(self, category_id):
"""removes a category from the _WorkItem.
Args:
category_id: id of category to remove
Returns:
bool
References:
Tracker.removeCategory
"""
self._verify_obj()
return self.session.tracker_client.service.removeCategory(self.uri,
category_id)
def remove_external_linked_revision(self, repository_name, revision_id):
"""Removes a revision from external repository.
Args:
repository_name: the ID of the external repository.
revision_id: the ID of the revision to remove.
Returns:
bool
References:
Tracker.removeExternalLinkedRevision
"""
self._verify_obj()
return self.session.tracker_client.service. \
removeExternalLinkedRevision(self.uri, repository_name,
revision_id)
def remove_externally_linked_item(self, linked_external_workitem_id, role):
"""Removes an externally linked work item.
Args:
linked_external_workitem_id: the ID of the linked item to remove
role: the role of the linked item to remove
Returns:
bool
References:
Tracker.removeExternallyLinkedItem
"""
self._verify_obj()
external_wi = _WorkItem(uri=linked_external_workitem_id)
return self.session.tracker_client.service. \
removeExternallyLinkedItem(self.uri, external_wi.uri, role)
def remove_hyperlink(self, url):
"""Removes a hyperlink from the _WorkItem
Args:
url: the url of the hyperlink to remove
Returns:
bool
References:
Tracker.removeHyperlink
"""
self._verify_obj()
return self.session.tracker_client.service. \
removeHyperlink(self.uri, url)
def remove_linked_item(self, linked_item_id, role):
"""Removes a linked work item.
Args:
linked_item_id: the ID of the linked item to remove
role: the role of the linked item to remove
Returns:
bool
References:
Tracker.removeLinkedItem
"""
self._verify_obj()
wi_linked = _WorkItem(work_item_id=linked_item_id,
project_id=self.project_id)
enum_role = EnumOptionId(role)._suds_object
return self.session.tracker_client.service. \
removeLinkedItem(self.uri, wi_linked.uri, enum_role)
def remove_linked_revision(self, revision_id):
"""Removes a revision
Args:
revision_id: The ID of the revision to remove
Returns:
bool
References:
Tracker.removeLinkedRevision
"""
self._verify_obj()
return self.session.tracker_client.service. \
removeLinkedRevision(self.uri, revision_id)
def remove_planning_constraint(self, constraint_date, constraint):
"""Removes a planning constraint
Args:
constraint_date: the date of the planning constraint to remove.
constraint: the type of constraint to remove.
Returns:
bool
References:
Tracker.removePlaningConstraint
"""
self._verify_obj()
return self.session.tracker_client.service. \
removePlaningConstraint(self.uri, constraint_date, constraint)
def reset_workflow(self):
"""resets the workflow for the current object. Performs initial action
if exists and sets the initial status
Args:
None
Returns:
None
References:
Tracker.resetWorkflow
"""
self._verify_obj()
self.session.tracker_client.service.resetWorkflow(self.uri)
def _set_custom_field(self, key, value):
"""sends the custom field value to the server
Args:
key: the suds field name
value:
Returns:
None
References:
Tracker.setCustomField
"""
c = CustomField()
c.key = key
c.value = value
c.parent_item_uri = self.uri
self.session.tracker_client.service.setCustomField(c._suds_object)
def set_fields_null(self, fields):
"""sets the specified fields to Null.
Args:
fields: list of fields to set to null
Returns:
None
References:
Tracker.setFieldsNull
"""
self._verify_obj()
p_fields = self._convert_obj_fields_to_polarion(fields)
self.session.tracker_client.service.setFieldsNull(self.uri, p_fields)
def set_test_steps(self, test_steps=None):
"""method set_test-steps Adds Test Steps to the current Work Item (WI)
(add operation). If WI already has Test Steps, they will be completely
replaced (update operation). If the test_steps parameter is None, the
content of the Test Steps field will be emptied (delete operation).
Args:
test_steps: a list of TestStep objects. Default: None
Returns:
None
References:
Test_Management.setTestSteps
"""
self._verify_obj()
if not test_steps:
parm = suds.null()
elif isinstance(test_steps, list):
parm = []
if isinstance(test_steps[0], TestStep):
parm = [item._suds_object for item in test_steps]
elif isinstance(test_steps[0], TestStep().
_suds_object.__class__):
parm = test_steps
else:
raise PyleroLibException("Expecting a list of testStep objects")
self.session.test_management_client.service.setTestSteps(self.uri,
parm)
def update(self):
"""Update the server with the current _WorkItem data
Args:
None
Returns:
None
References:
Tracker.updateWorkItem
"""
self._verify_obj()
self.session.tracker_client.service.updateWorkItem(self._suds_object)
def update_attachment(self, attachment_id, path, title):
"""method update_attachment updates the specified attachment to the
current _WorkItem
Args:
attachment_id: the ID of the attachment to be updated
path: file path to upload
title: u.User friendly name of the file
Returns:
None
Notes:
Raises an error if the test run object is not populated.
References:
Tracker.updateAttachment
"""
self._verify_obj()
data = self._get_file_data(path)
filename = os.path.basename(path)
self.session.tracker_client.service. \
updateAttachment(self.uri, attachment_id, filename, title, data)
def verify_required(self):
for field in self._required_fields:
if not getattr(self, field):
raise PyleroLibException(
"{0} is a required field".format(field))
def which_test_runs(self):
"""Gives the user a list of TestRun objects that the current WorkItem
instance is contained in.
Args:
None
Returns:
list of TestRun objects
"""
# import done in the function so as not to cause circular refs
from pylero.test_run import TestRun
return TestRun.search(self.work_item_id)
class _SpecificWorkItem(_WorkItem):
"""specific work item is a class that contains the WorkItem implementation
that is different per WorkItem type. Classes that inherit from this class
must define the _wi_type class attribute as a minimum.
"""
_wi_type = ""
_got_custom_fields = False
_required_fields = []
_all_custom_fields = []
@classmethod
def create(cls, project_id, title, desc, status="draft", **kwargs):
"""Creates the specific type of work item, requiring the base fields to
be passed in and all required custom fields as key word args. If not
all required fields are passed in or key word fields that are not
custom, it raises an exception.
Args:
project_id: The project to create the WorkItem in
work_item_id: The unique id for the WorkItem
title: the title of the WorkItem
desc: the Description of the WorkItem
status: the initial status of the WorkItem, draft by default
kwargs: keyword arguments for custom fields. All required custom
fields must appear as keyword arguments.
"""
cls.get_custom_fields(project_id)
fields = ""
for req in cls._required_fields:
if req not in kwargs:
fields += (", " if fields else "") + req
if fields:
raise PyleroLibException("These parameters are required: {0}".
format(fields))
for field in kwargs:
if field not in cls._all_custom_fields and \
field not in cls._cls_suds_map:
fields += (", " if fields else "") + field
if fields:
raise PyleroLibException("These parameters are unknown: {0}".
format(fields))
return super(_SpecificWorkItem, cls).create(
project_id, cls._wi_type, title, desc, status, **kwargs)
@classmethod
def get_custom_fields(cls, project_id):
"""List of custom fields for the project and specific wi_type
Args:
project_id: project that the user is working with
Returns:
tuple containing:
a) list of all custom fields
b) list of all required fields
"""
cls._required_fields = []
cls._all_custom_fields = []
cfts = cls.get_defined_custom_field_types(project_id,
cls._wi_type)
for cft in cfts:
# convert the custom field name to use code convention, where
# possible
split_name = re.findall('[a-zA-Z][^A-Z]*', cft.cft_id)
local_name = "_".join(split_name).replace("_U_R_I", "_uri"). \
replace("_W_I", "_wi").replace("_I_D", "_id").lower()
cls._all_custom_fields.append(local_name)
cls._cls_suds_map[local_name] = {}
cls._cls_suds_map[local_name]["field_name"] = cft.cft_id
# types are returned in format:
# * nsX:obj_type for objects and
# * xsd:string for native types
# for all object types, I need special processing.
parse_type = cft.type.split(":")
if parse_type[0].startswith("ns"):
cls._cls_suds_map[local_name]["cls"] = \
globals()[parse_type[1]]
cls._cls_suds_map[local_name]["enum_id"] = getattr(cft,
"enum_id",
None)
cls._cls_suds_map[local_name]["is_custom"] = True
cls._cls_suds_map[local_name]["control"] = cls._wi_type
if cft.required:
cls._required_fields.append(local_name)
cls._got_custom_fields = True
return None
@classmethod
def query(cls, query, fields=["work_item_id"],
sort="work_item_id", limit=-1, baseline_revision=None,
query_uris=False, project_id=None):
"""Function overrides the query function in the _WorkItem class. It
only accepts Lucene queries, specifically queries the specific type of
work item and the default project. To search other projects, there is a
project_id parameter.
Notes:
The query function only returns a partially populated object with
the fields passed in (by default work_item_id) and the uri field.
The uri field is the Polarion unique object identifier which can
be used (among other things) to instantiate objects. Because not
all fields are returned in the object, this can cause problems
when trying to update an object that was returned by the query
function. Another issue is that certain fields are references and
are not retrieved by using their standard field names.
Examples of problems are:
* trying to use custom fields
* updating an object that has required fields that were not
retrieved
* trying to retrieve the project_id field.
Because of these issues, recommended usage is as follows:
{WI_TYPE} is TestCase, Requirement, ...
query_results = {WI_TYPE}.query("query string")
for item in query_results:
wi = {WI_TYPE}(uri=item.uri)
... # do what you want with the object
Args:
query: query, Lucene
fields: array of field names to fill in the returned
WorkItems (can be null). For nested structures in
the lists you can use following syntax to include only
subset of fields: myList.LIST.key
(e.g. linkedWorkItems.LIST.role).
For custom fields you can specify which fields you want to
be filled using following syntax:
customFields.CUSTOM_FIELD_ID (e.g. customFields.risk).
Default: list containing "work_item_id".
sort: Lucene sort string (can be null), default: work_item_id
limit: how many results to return (-1 means everything, default)
baseline_revision (str): if populated, query done in specified rev
default: None
query_uris (bool): returns a list of URI of the WorkItems found,
default: False
project_id (str): is used to pass in a specific project_id instead
of using the default. Default: None
Returns:
list of the specific WorkItem objects that were found.
"""
if not cls._got_custom_fields:
cls.get_custom_fields(project_id or cls.default_project)
if query:
query += " AND "
query += "type:%s AND project.id:%s" % \
(cls._wi_type, project_id or cls.default_project)
return super(_SpecificWorkItem, cls).query(
query, False, fields, sort, limit, baseline_revision, query_uris)
def __init__(self, project_id=None, work_item_id=None, suds_object=None,
uri=None, fields=None, revision=None):
"""In this constructor, it adds the custom fields per WorkItem type to
the _cls_suds_map along with the is_custom and is_enum fields.
In the property builder of the base class, it defines special behavior
for custom fields so they are treated like regular attributes
"""
if not project_id:
project_id = self.default_project
self._changed_fields = {}
self.get_custom_fields(project_id)
super(_SpecificWorkItem, self).__init__(project_id, work_item_id,
suds_object, uri, fields,
revision)
if not self.type:
self.type = self._wi_type
if self.type != self._wi_type:
raise PyleroLibException("This is of type {0}, not type {1}".
format(self.type, self._wi_type))
@tx_wrapper
def update(self):
"""calls update on changes to the work item.
It first verifies that required fields are all set, then calls update
on the object and then iterates the custom fields and updates each one
"""
self.verify_required()
super(_SpecificWorkItem, self).update()
for field in self._changed_fields:
if field == "testSteps":
self.set_test_steps(self._changed_fields[field].steps[0])
self._changed_fields = {}
# On import of the module, it will connect to the server
# and get a list of the workitem types and create those classes.
cfg = Configuration()
bp = BasePolarion()
vals = bp.get_valid_field_values("workitem-type")
workitems = {}
for item in bp._cache["enums"]["workitem-type"][None]:
workitems[item.id] = item.name.replace(" ", "")
for wi in workitems:
newclass = type(str(workitems[wi]),
(_SpecificWorkItem,),
{"_wi_type": wi,
"_cls_suds_map":
copy.deepcopy(_SpecificWorkItem._cls_suds_map)})
# Add the class to the module's namespace
globals()[str(workitems[wi])] = newclass