from rtcclient.base import RTCBase
import logging
import xmltodict
import os
import jinja2
import jinja2.meta
from rtcclient import exception
from rtcclient import _search_path
import six
[docs]class Templater(RTCBase):
"""A wrapped class used to generate and render templates
from some copied workitems
:param rtc_obj: a reference to the
:class:`rtcclient.client.RTCClient` object
:param searchpath: the folder to store your templates.
If `None`, the default search path
(/your/site-packages/rtcclient/templates) will be loaded automatically.
"""
log = logging.getLogger("template.Templater")
def __init__(self, rtc_obj, searchpath=None):
self.rtc_obj = rtc_obj
RTCBase.__init__(self, self.rtc_obj.url)
if searchpath is None:
self.searchpath = _search_path
else:
self.searchpath = searchpath
self.loader = jinja2.FileSystemLoader(searchpath=self.searchpath)
self.environment = jinja2.Environment(loader=self.loader,
trim_blocks=True)
def __str__(self):
return "Templater for %s" % self.rtc_obj
def get_rtc_obj(self):
return self.rtc_obj
[docs] def render(self, template, **kwargs):
"""Renders the template
:param template: The template to render.
The template is actually a file, which is usually generated
by :class:`rtcclient.template.Templater.getTemplate`
and can also be modified by user accordingly.
:param kwargs: The `kwargs` dict is used to fill the template.
These two parameter are mandatory:
* description
* title
Some of below parameters (which may not be included in some
customized workitem type ) are mandatory if `keep` (parameter in
:class:`rtcclient.template.Templater.getTemplate`) is set to
`False`; Optional for otherwise.
* teamArea (Team Area)
* ownedBy (Owned By)
* plannedFor(Planned For)
* severity(Severity)
* priority(Priority)
* filedAgainst(Filed Against)
Actually all these needed keywords/attributes/fields can be
retrieved by :class:`rtcclient.template.Templater.listFields`
:return: the :class:`string` object
:rtype: string
"""
try:
temp = self.environment.get_template(template)
return temp.render(**kwargs)
except AttributeError:
err_msg = "Invalid value for 'template'"
self.log.error(err_msg)
raise exception.BadValue(err_msg)
[docs] def renderFromWorkitem(self, copied_from, keep=False,
encoding="UTF-8", **kwargs):
"""Render the template directly from some to-be-copied
:class:`rtcclient.workitem.Workitem` without saving to a file
:param copied_from: the to-be-copied
:class:`rtcclient.workitem.Workitem` id
:param keep (default is False): If `True`, some of the below fields
will remain unchangeable with the to-be-copied
:class:`rtcclient.workitem.Workitem`.
Otherwise for `False`.
* teamArea (Team Area)
* ownedBy (Owned By)
* plannedFor(Planned For)
* severity(Severity)
* priority(Priority)
* filedAgainst(Filed Against)
:param encoding (default is "UTF-8"): coding format
:param kwargs: The `kwargs` dict is used to fill the template.
These two parameter are mandatory:
* description
* title
Some of below parameters (which may not be included in some
customized workitem type ) are mandatory if `keep` is set to
`False`; Optional for otherwise.
* teamArea (Team Area)
* ownedBy (Owned By)
* plannedFor(Planned For)
* severity(Severity)
* priority(Priority)
* filedAgainst(Filed Against)
Actually all these needed keywords/attributes/fields can be
retrieved by
:class:`rtcclient.template.Templater.listFieldsFromWorkitem`
:return: the :class:`string` object
:rtype: string
"""
temp = jinja2.Template(self.getTemplate(copied_from,
template_name=None,
template_folder=None,
keep=keep,
encoding=encoding))
return temp.render(**kwargs)
[docs] def listFields(self, template):
"""List all the attributes to be rendered from the template file
:param template: The template to render.
The template is actually a file, which is usually generated
by :class:`rtcclient.template.Templater.getTemplate` and can also
be modified by user accordingly.
:return: a :class:`set` contains all the needed attributes
:rtype: set
"""
try:
temp_source = self.environment.loader.get_source(self.environment,
template)
return self.listFieldsFromSource(temp_source)
except AttributeError:
err_msg = "Invalid value for 'template'"
self.log.error(err_msg)
raise exception.BadValue(err_msg)
[docs] def listFieldsFromWorkitem(self, copied_from, keep=False):
"""List all the attributes to be rendered directly from some
to-be-copied :class:`rtcclient.workitem.Workitem`
:param copied_from: the to-be-copied
:class:`rtcclient.workitem.Workitem` id
:param keep: (default is False) If `True`, some of below parameters
(which will not be included in some customized
:class:`rtcclient.workitem.Workitem` type ) will remain
unchangeable with the to-be-copied
:class:`rtcclient.workitem.Workitem`.
Otherwise for `False`.
* teamArea (Team Area)
* ownedBy (Owned By)
* plannedFor(Planned For)
* severity(Severity)
* priority(Priority)
* filedAgainst(Filed Against)
:return: a :class:`set` contains all the needed attributes
:rtype: set
"""
temp_source = self.getTemplate(copied_from,
template_name=None,
template_folder=None,
keep=keep)
return self.listFieldsFromSource(temp_source)
[docs] def listFieldsFromSource(self, template_source):
"""List all the attributes to be rendered directly from template
source
:param template_source: the template source (usually represents the
template content in string format)
:return: a :class:`set` contains all the needed attributes
:rtype: set
"""
ast = self.environment.parse(template_source)
return jinja2.meta.find_undeclared_variables(ast)
[docs] def getTemplate(self, copied_from, template_name=None,
template_folder=None, keep=False, encoding="UTF-8"):
"""Get template from some to-be-copied
:class:`rtcclient.workitem.Workitem`
The resulting XML document is returned as a :class:`string`, but if
`template_name` (a string value) is specified,
it is written there instead.
:param copied_from: the to-be-copied
:class:`rtcclient.workitem.Workitem` id (integer or
equivalent string)
:param template_name: the template file name
:param template_folder: the folder to store template file
:param keep: (default is False) If `True`, some of below parameters
(which may not be included in some customized
:class:`rtcclient.workitem.Workitem` type ) will remain
unchangeable with the to-be-copied
:class:`rtcclient.workitem.Workitem`.
Otherwise for `False`.
* teamArea (Team Area)
* ownedBy (Owned By)
* plannedFor(Planned For)
* severity(Severity)
* priority(Priority)
* filedAgainst(Filed Against)
:param encoding: (default is "UTF-8") coding format
:return:
* a :class:`string` object: if `template_name` is not specified
* write the template to file `template_name`: if `template_name` is
specified
"""
try:
if isinstance(copied_from, bool) or isinstance(copied_from, float):
raise ValueError()
if isinstance(copied_from, six.string_types):
copied_from = int(copied_from)
if not isinstance(copied_from, int):
raise ValueError()
except ValueError:
err_msg = "Please input a valid workitem id you want to copy from"
self.log.error(err_msg)
raise exception.BadValue(err_msg)
self.log.info("Fetch the template from <Workitem %s> with [keep]=%s",
copied_from, keep)
if template_folder is None:
template_folder = self.searchpath
# identify whether output to a file
if template_name is not None:
template_file_path = os.path.join(template_folder,
template_name)
output = open(template_file_path, "w")
else:
template_file_path = None
output = None
workitem_url = "/".join([self.url,
"oslc/workitems/%s" % copied_from])
resp = self.get(workitem_url,
verify=False,
proxies=self.rtc_obj.proxies,
headers=self.rtc_obj.headers)
raw_data = xmltodict.parse(resp.content)
# pre-adjust the template:
# remove some attribute to avoid being overwritten, which will only be
# generated when the workitem is created
wk_raw_data = raw_data.get("oslc_cm:ChangeRequest")
self._remove_long_fields(wk_raw_data)
# Be cautious when you want to modify these fields
# These fields have been tested as must-removed one
remove_fields = ["@rdf:about",
"dc:created",
"dc:creator",
"dc:identifier",
"rtc_cm:contextId",
"rtc_cm:comments",
"rtc_cm:state",
"dc:type",
"rtc_cm:subscribers",
"dc:modified",
"rtc_cm:modifiedBy",
"rtc_cm:resolved",
"rtc_cm:resolvedBy",
"rtc_cm:resolution",
"rtc_cm:startDate",
"rtc_cm:timeSpent",
"rtc_cm:progressTracking",
"rtc_cm:projectArea",
"oslc_cm:relatedChangeManagement",
"oslc_cm:trackedWorkItem",
"oslc_cm:tracksWorkItem",
"rtc_cm:timeSheet",
"oslc_pl:schedule"]
for remove_field in remove_fields:
try:
wk_raw_data.pop(remove_field)
self.log.debug("Successfully remove field [%s] from the "
"template originated from <Workitem %s>",
remove_field,
copied_from)
except:
self.log.warning("No field named [%s] in this template "
"from <Workitem %s>", remove_field,
copied_from)
continue
wk_raw_data["dc:description"] = "{{ description }}"
wk_raw_data["dc:title"] = "{{ title }}"
if keep:
if template_file_path:
self.log.info("Writing the template to file %s",
template_file_path)
return xmltodict.unparse(raw_data, output=output,
encoding=encoding,
pretty=True)
replace_fields = [("rtc_cm:teamArea", "{{ teamArea }}"),
("rtc_cm:ownedBy", "{{ ownedBy }}"),
("rtc_cm:plannedFor", "{{ plannedFor }}"),
("rtc_cm:foundIn", "{{ foundIn }}"),
("oslc_cm:severity", "{{ severity }}"),
("oslc_cm:priority", "{{ priority }}"),
("rtc_cm:filedAgainst", "{{ filedAgainst }}")]
for field in replace_fields:
try:
wk_raw_data[field[0]]["@rdf:resource"] = field[1]
self.log.debug("Successfully replace field [%s] with [%s]",
field[0], field[1])
except:
self.log.warning("Cannot replace field [%s]", field[0])
continue
if template_file_path:
self.log.info("Writing the template to file %s",
template_file_path)
return xmltodict.unparse(raw_data, output=output,
encoding=encoding,
pretty=True)
def _remove_long_fields(self, wk_raw_data):
"""Remove long fields: These fields are can only customized after
the workitems are created
"""
match_str_list = ["rtc_cm:com.ibm.",
"calm:"]
for key in wk_raw_data.keys():
for match_str in match_str_list:
if key.startswith(match_str):
try:
wk_raw_data.pop(key)
self.log.debug("Successfully remove field [%s] from "
"the template", key)
except:
self.log.warning("Cannot remove field [%s] from the "
"template", key)
continue
[docs] def getTemplates(self, workitems, template_folder=None,
template_names=None, keep=False, encoding="UTF-8"):
"""Get templates from a group of to-be-copied :class:`Workitems` and
write them to files named after the names in `template_names`
respectively.
:param workitems: a :class:`list`/:class:`tuple`/:class:`set`
contains the ids (integer or equivalent string) of some
to-be-copied :class:`Workitems`
:param template_names: a :class:`list`/:class:`tuple`/:class:`set`
contains the template file names for copied :class:`Workitems`.
If `None`, the new template files will be named after the
:class:`rtcclient.workitem.Workitem` id with "`.template`" as a
postfix
:param template_folder: refer to
:class:`rtcclient.template.Templater.getTemplate`
:param keep: (default is False) refer to
:class:`rtcclient.template.Templater.getTemplate`
:param encoding: (default is "UTF-8") refer to
:class:`rtcclient.template.Templater.getTemplate`
"""
if (not workitems or isinstance(workitems, six.string_types) or
isinstance(workitems, int) or
isinstance(workitems, float) or
not hasattr(workitems, "__iter__")):
error_msg = "Input parameter 'workitems' is not iterable"
self.log.error(error_msg)
raise exception.BadValue(error_msg)
if template_names is not None:
if not hasattr(template_names, "__iter__"):
error_msg = "Input parameter 'template_names' is not iterable"
self.log.error(error_msg)
raise exception.BadValue(error_msg)
if len(workitems) != len(template_names):
error_msg = "".join(["Input parameters 'workitems' and ",
"'template_names' have different length"])
self.log.error(error_msg)
raise exception.BadValue(error_msg)
for index, wk_id in enumerate(workitems):
try:
if template_names is not None:
template_name = template_names[index]
else:
template_name = ".".join([wk_id, "template"])
self.getTemplate(wk_id,
template_name=template_name,
template_folder=template_folder,
keep=keep,
encoding=encoding)
except Exception as excp:
self.log.error("Exception occurred when fetching"
"template from <Workitem %s>: %s",
str(wk_id), excp)
continue
self.log.info("Successfully fetch all the templates from "
"workitems: %s", workitems)