blob: 402f92c6cd5d2ee78b0f208c3922a023ac3c1937 [file] [log] [blame]
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is govered by a BSD-style
# license that can be found in the LICENSE file or at
"""View objects to help display tracker business objects in templates."""
import collections
import logging
import re
import time
import urllib
from google.appengine.api import app_identity
from third_party import ezt
from framework import filecontent
from framework import framework_constants
from framework import framework_helpers
from framework import framework_views
from framework import gcs_helpers
from framework import permissions
from framework import template_helpers
from framework import timestr
from framework import urls
from proto import tracker_pb2
from services import user_svc
from tracker import tracker_bizobj
from tracker import tracker_constants
from tracker import tracker_helpers
class IssueView(template_helpers.PBProxy):
"""Wrapper class that makes it easier to display an Issue via EZT."""
def __init__(
self, issue, users_by_id, config, open_related=None,
closed_related=None, all_related=None):
"""Store relevant values for later display by EZT.
issue: An Issue protocol buffer.
users_by_id: dict {user_id: UserViews} for all users mentioned in issue.
config: ProjectIssueConfig for this issue.
open_related: dict of visible open issues that are related to this issue.
closed_related: dict {issue_id: issue} of visible closed issues that
are related to this issue.
all_related: dict {issue_id: issue} of all blocked-on, blocking,
or merged-into issues referenced from this issue, regardless of
super(IssueView, self).__init__(issue)
# The users involved in this issue must be present in users_by_id if
# this IssueView is to be used on the issue detail or peek pages. But,
# they can be absent from users_by_id if the IssueView is used as a
# tile in the grid view.
self.owner = users_by_id.get(issue.owner_id)
self.derived_owner = users_by_id.get(issue.derived_owner_id) = [users_by_id.get(cc_id) for cc_id in issue.cc_ids
if cc_id]
self.derived_cc = [users_by_id.get(cc_id)
for cc_id in issue.derived_cc_ids
if cc_id]
self.status = framework_views.StatusView(issue.status, config)
self.derived_status = framework_views.StatusView(
issue.derived_status, config)
# If we don't have a config available, we don't need to access is_open, so
# let it be True.
self.is_open = ezt.boolean(
not config or
tracker_bizobj.GetStatus(issue), config))
self.components = sorted(
[ComponentValueView(component_id, config, False)
for component_id in issue.component_ids
if tracker_bizobj.FindComponentDefByID(component_id, config)] +
[ComponentValueView(component_id, config, True)
for component_id in issue.derived_component_ids
if tracker_bizobj.FindComponentDefByID(component_id, config)],
key=lambda cvv: cvv.path)
self.fields = [
fd, config, issue.labels, issue.derived_labels, issue.field_values,
# TODO(jrobbins): field-level view restrictions, display options
for fd in config.field_defs
if not fd.is_deleted]
self.fields = sorted(
self.fields, key=lambda f: (f.applicable_type, f.field_name))
field_names = [fd.field_name.lower() for fd in config.field_defs
if not fd.is_deleted] # TODO(jrobbins): restricts
self.labels = [
framework_views.LabelView(label, config)
for label in tracker_bizobj.NonMaskedLabels(issue.labels, field_names)]
self.derived_labels = [
framework_views.LabelView(label, config)
for label in issue.derived_labels
if not tracker_bizobj.LabelIsMaskedByField(label, field_names)]
self.restrictions = _RestrictionsView(issue)
# TODO(jrobbins): sort by order of labels in project config
self.short_summary = issue.summary[:tracker_constants.SHORT_SUMMARY_LENGTH]
if issue.closed_timestamp:
self.closed = timestr.FormatAbsoluteDate(issue.closed_timestamp)
self.closed = ''
blocked_on_iids = issue.blocked_on_iids
blocking_iids = issue.blocking_iids
# Note that merged_into_str and blocked_on_str includes all issue
# references, even those referring to issues that the user can't view,
# so open_related and closed_related cannot be used.
if all_related is not None:
all_blocked_on_refs = [
(all_related[ref_iid].project_name, all_related[ref_iid].local_id)
for ref_iid in issue.blocked_on_iids]
(r.project, r.issue_id) for r in issue.dangling_blocked_on_refs])
self.blocked_on_str = ', '.join(
ref, default_project_name=issue.project_name)
for ref in all_blocked_on_refs)
all_blocking_refs = [
(all_related[ref_iid].project_name, all_related[ref_iid].local_id)
for ref_iid in issue.blocking_iids]
(r.project, r.issue_id) for r in issue.dangling_blocking_refs])
self.blocking_str = ', '.join(
ref, default_project_name=issue.project_name)
for ref in all_blocking_refs)
if issue.merged_into:
merged_issue = all_related[issue.merged_into]
merged_into_ref = merged_issue.project_name, merged_issue.local_id
merged_into_ref = None
self.merged_into_str = tracker_bizobj.FormatIssueRef(
merged_into_ref, default_project_name=issue.project_name)
self.blocked_on = []
self.blocking = []
current_project_name = issue.project_name
if open_related is not None and closed_related is not None:
self.merged_into = IssueRefView(
current_project_name, issue.merged_into,
open_related, closed_related)
self.blocked_on = [
IssueRefView(current_project_name, iid, open_related, closed_related)
for iid in blocked_on_iids]
[DanglingIssueRefView(ref.project, ref.issue_id)
for ref in issue.dangling_blocked_on_refs])
self.blocked_on = [irv for irv in self.blocked_on if irv.visible]
# TODO(jrobbins): sort by irv project_name and local_id
self.blocking = [
IssueRefView(current_project_name, iid, open_related, closed_related)
for iid in blocking_iids]
[DanglingIssueRefView(ref.project, ref.issue_id)
for ref in issue.dangling_blocking_refs])
self.blocking = [irv for irv in self.blocking if irv.visible]
# TODO(jrobbins): sort by irv project_name and local_id
self.detail_relative_url = tracker_helpers.FormatRelativeIssueURL(
issue.project_name, urls.ISSUE_DETAIL, id=issue.local_id)
class _RestrictionsView(object):
"""An EZT object for the restrictions associated with an issue."""
# Restrict label fragments that correspond to known permissions.
_VIEW = permissions.VIEW.lower()
_EDIT = permissions.EDIT_ISSUE.lower()
_ADD_COMMENT = permissions.ADD_ISSUE_COMMENT.lower()
def __init__(self, issue):
# List of restrictions that don't map to a known action kind.
self.other = []
restrictions_by_action = collections.defaultdict(list)
# We can't use GetRestrictions here, as we prefer to preserve
# the case of the label when showing restrictions in the UI.
for label in tracker_bizobj.GetLabels(issue):
if permissions.IsRestrictLabel(label):
_kw, action_kind, needed_perm = label.split('-', 2)
action_kind = action_kind.lower()
if action_kind in self._KNOWN_ACTION_KINDS:
self.view = ' and '.join(restrictions_by_action[self._VIEW])
self.add_comment = ' and '.join(restrictions_by_action[self._ADD_COMMENT])
self.edit = ' and '.join(restrictions_by_action[self._EDIT])
self.has_restrictions = ezt.boolean(
self.view or self.add_comment or self.edit or self.other)
class IssueRefView(object):
"""A simple object to easily display links to issues in EZT."""
def __init__(self, current_project_name, issue_id, open_dict, closed_dict):
"""Make a simple object to display a link to a referenced issue.
current_project_name: string name of the current project.
issue_id: int issue ID of the target issue.
open_dict: dict {issue_id: issue} of pre-fetched open issues that the
user is allowed to view.
closed_dict: dict of pre-fetched closed issues that the user is
allowed to view.
Note, the target issue may be a member of either open_dict or
closed_dict, or neither one. If neither, nothing is displayed.
if (not issue_id or
issue_id not in open_dict and issue_id not in closed_dict):
# Issue not found or not visible to this user, so don't link to it.
self.visible = ezt.boolean(False)
self.visible = ezt.boolean(True)
if issue_id in open_dict:
related_issue = open_dict[issue_id]
self.is_open = ezt.boolean(True)
related_issue = closed_dict[issue_id]
self.is_open = ezt.boolean(False)
if current_project_name == related_issue.project_name:
self.url = 'detail?id=%s' % related_issue.local_id
self.display_name = 'issue %s' % related_issue.local_id
self.url = '/p/%s%s?id=%s' % (
related_issue.project_name, urls.ISSUE_DETAIL,
self.display_name = 'issue %s:%s' % (
related_issue.project_name, related_issue.local_id)
self.summary = related_issue.summary
def DebugString(self):
if not self.visible:
return 'IssueRefView(not visible)'
return 'IssueRefView(%s)' % self.display_name
class DanglingIssueRefView(object):
def __init__(self, project_name, issue_id):
"""Makes a simple object to display a link to an issue still in Codesite.
Satisfies the same API and internal data members as IssueRefView,
excpet for the arguments to __init__.
project_name: The name of the project on Codesite
issue_id: The local id of the issue in that project
self.visible = True
self.is_open = True # TODO(agable) Make a call to Codesite to set this?
self.url = '' % (
project_name, issue_id)
self.display_name = 'issue %s:%d' % (project_name, issue_id)
self.short_name = 'issue %s:%d' % (project_name, issue_id)
self.summary = 'Issue %d in %s.' % (issue_id, project_name)
def DebugString(self):
return 'DanglingIssueRefView(%s)' % self.display_name
class IssueCommentView(template_helpers.PBProxy):
"""Wrapper class that makes it easier to display an IssueComment via EZT."""
def __init__(
self, project_name, comment_pb, users_by_id, autolink,
all_referenced_artifacts, mr, issue, effective_ids=None):
"""Get IssueComment PB and make its fields available as attrs.
project_name: Name of the project this issue belongs to.
comment_pb: Comment protocol buffer.
users_by_id: dict mapping user_ids to UserViews, including
the user that entered the comment, and any changed participants.
autolink: utility object for automatically linking to other
issues, svn revisions, etc.
all_referenced_artifacts: opaque object with details of referenced
artifacts that is needed by autolink.
mr: common information parsed from the HTTP request.
issue: Issue PB for the issue that this comment is part of.
effective_ids: optional set of int user IDs for the comment author.
super(IssueCommentView, self).__init__(comment_pb) =
self.creator = users_by_id[comment_pb.user_id]
# TODO(jrobbins): this should be based on the issue project, not the
# request project for non-project views and cross-project.
if mr.project:
self.creator_role = framework_helpers.GetRoleName(
effective_ids or {self.creator.user_id}, mr.project)
self.creator_role = None
time_tuple = time.localtime(comment_pb.timestamp)
self.date_string = timestr.FormatAbsoluteDate(
comment_pb.timestamp, old_format=timestr.MONTH_DAY_FMT)
self.date_relative = timestr.FormatRelativeDate(comment_pb.timestamp)
self.date_tooltip = time.asctime(time_tuple)
self.text_runs = _ParseTextRuns(comment_pb.content)
if autolink:
self.text_runs = autolink.MarkupAutolinks(
mr, self.text_runs, all_referenced_artifacts)
self.attachments = [AttachmentView(attachment, project_name)
for attachment in comment_pb.attachments]
self.amendments = [
AmendmentView(amendment, users_by_id, mr.project_name)
for amendment in comment_pb.amendments]
# Treat comments from banned users as being deleted.
self.is_deleted = (comment_pb.deleted_by or
(self.creator and self.creator.banned))
self.can_delete = False
if mr.auth.user_id and mr.project:
# TODO(jrobbins): pass through config, then I can do:
# granted_perms = tracker_bizobj.GetGrantedPerms(
# issue, mr.auth.effective_ids, config)
self.can_delete = permissions.CanDelete(
mr.auth.user_id, mr.auth.effective_ids, mr.perms,
comment_pb.deleted_by, comment_pb.user_id,
mr.project, permissions.GetRestrictions(issue))
# Prevent spammers from undeleting their own comments, but
# allow people with permission to undelete their own comments.
if comment_pb.is_spam and comment_pb.user_id == mr.auth.user_id:
self.can_delete = mr.perms.HasPerm(permissions.MODERATE_SPAM,
mr.auth.user_id, mr.project)
self.visible = self.can_delete or not self.is_deleted
_TEMPLATE_TEXT_RE = re.compile('^(<b>[^<]+</b>)', re.MULTILINE)
def _ParseTextRuns(content):
"""Convert the user's comment to a list of TextRun objects."""
chunks = _TEMPLATE_TEXT_RE.split(content)
runs = [_ChunkToRun(chunk) for chunk in chunks]
return runs
def _ChunkToRun(chunk):
"""Convert a substring of the user's comment to a TextRun object."""
if chunk.startswith('<b>') and chunk.endswith('</b>'):
return template_helpers.TextRun(chunk[3:-4], tag='b')
return template_helpers.TextRun(chunk)
VIEWABLE_IMAGE_TYPES = ['image/jpeg', 'image/gif', 'image/png', 'image/x-png']
MAX_PREVIEW_FILESIZE = 4 * 1024 * 1024 # 4MB
class LogoView(template_helpers.PBProxy):
"""Wrapper class to make it easier to display project logos via EZT."""
def __init__(self, project_pb):
if (not project_pb or
not project_pb.logo_gcs_id or
not project_pb.logo_file_name):
self.thumbnail_url = ''
self.viewurl = ''
object_path = ('/' + app_identity.get_default_gcs_bucket_name() +
self.filename = project_pb.logo_file_name
self.mimetype = filecontent.GuessContentTypeFromFilename(self.filename)
self.thumbnail_url = gcs_helpers.SignUrl(object_path + '-thumbnail')
self.viewurl = (
gcs_helpers.SignUrl(object_path) + '&' + urllib.urlencode(
('attachment; filename=%s' % self.filename)}))
class AttachmentView(template_helpers.PBProxy):
"""Wrapper class to make it easier to display issue attachments via EZT."""
def __init__(self, attach_pb, project_name):
"""Get IssueAttachmentContent PB and make its fields available as attrs.
attach_pb: Attachment part of IssueComment protocol buffer.
project_name: string Name of the current project.
super(AttachmentView, self).__init__(attach_pb)
self.filesizestr = template_helpers.BytesKbOrMb(attach_pb.filesize)
self.downloadurl = 'attachment?aid=%s' % attach_pb.attachment_id
self.url = None
self.thumbnail_url = None
if IsViewableImage(attach_pb.mimetype, attach_pb.filesize):
self.url = self.downloadurl + '&inline=1'
self.thumbnail_url = self.url + '&thumb=1'
elif IsViewableText(attach_pb.mimetype, attach_pb.filesize):
self.url = tracker_helpers.FormatRelativeIssueURL(
project_name, urls.ISSUE_ATTACHMENT_TEXT,
self.iconurl = '/images/paperclip.png'
def IsViewableImage(mimetype_charset, filesize):
"""Return true if we can safely display such an image in the browser.
mimetype_charset: string with the mimetype string that we got back
from the 'file' command. It may have just the mimetype, or it
may have 'foo/bar; charset=baz'.
filesize: int length of the file in bytes.
True iff we should allow the user to view a thumbnail or safe version
of the image in the browser. False if this might not be safe to view,
in which case we only offer a download link.
mimetype = mimetype_charset.split(';', 1)[0]
return (mimetype in VIEWABLE_IMAGE_TYPES and
def IsViewableText(mimetype, filesize):
"""Return true if we can safely display such a file as escaped text."""
return (mimetype.startswith('text/') and
class AmendmentView(object):
"""Wrapper class that makes it easier to display an Amendment via EZT."""
def __init__(self, amendment, users_by_id, project_name):
"""Get the info from the PB and put it into easily accessible attrs.
amendment: Amendment part of an IssueComment protocol buffer.
users_by_id: dict mapping user_ids to UserViews.
project_name: Name of the project the issue/comment/amendment is in.
# TODO(jrobbins): take field-level restrictions into account.
# Including the case where user is not allowed to see any amendments.
self.field_name = tracker_bizobj.GetAmendmentFieldName(amendment)
self.newvalue = tracker_bizobj.AmendmentString(amendment, users_by_id)
self.values = tracker_bizobj.AmendmentLinks(
amendment, users_by_id, project_name)
class ComponentDefView(template_helpers.PBProxy):
"""Wrapper class to make it easier to display component definitions."""
def __init__(self, component_def, users_by_id):
super(ComponentDefView, self).__init__(component_def)
c_path = component_def.path
if '>' in c_path:
self.parent_path = c_path[:c_path.rindex('>')]
self.leaf_name = c_path[c_path.rindex('>') + 1:]
self.parent_path = ''
self.leaf_name = c_path
self.docstring_short = template_helpers.FitUnsafeText(
component_def.docstring, 200)
self.admins = [users_by_id.get(admin_id)
for admin_id in component_def.admin_ids] = [users_by_id.get(cc_id) for cc_id in component_def.cc_ids]
self.classes = 'all '
if self.parent_path == '':
self.classes += 'toplevel '
self.classes += 'deprecated ' if component_def.deprecated else 'active '
class ComponentValueView(object):
"""Wrapper class that makes it easier to display a component value."""
def __init__(self, component_id, config, derived):
"""Make the component name and docstring available as attrs.
component_id: int component_id to look up in the config
config: ProjectIssueConfig PB for the issue's project.
derived: True if this component was derived.
cd = tracker_bizobj.FindComponentDefByID(component_id, config)
self.path = cd.path
self.docstring = cd.docstring
self.docstring_short = template_helpers.FitUnsafeText(cd.docstring, 60)
self.derived = ezt.boolean(derived)
class FieldValueView(object):
"""Wrapper class that makes it easier to display a custom field value."""
def __init__(
self, fd, config, values, derived_values, issue_types, applicable=None):
"""Make several values related to this field available as attrs.
fd: field definition to be displayed (or not, if no value).
config: ProjectIssueConfig PB for the issue's project.
values: list of explicit field values.
derived_values: list of derived field values.
issue_types: set of lowered string values from issues' "Type-*" labels.
applicable: optional boolean that overrides the rule that determines
when a field is applicable.
self.field_def = FieldDefView(fd, config)
self.field_id = fd.field_id
self.field_name = fd.field_name
self.field_docstring = fd.docstring
self.field_docstring_short = template_helpers.FitUnsafeText(
fd.docstring, 60)
self.values = values
self.derived_values = derived_values
self.applicable_type = fd.applicable_type
if applicable is not None:
self.applicable = ezt.boolean(applicable)
# A field is applicable to a given issue if it (a) applies to all issues,
# or (b) already has a value on this issue, or (c) says that it applies to
# issues with this type (or a prefix of it).
self.applicable = ezt.boolean(
not self.applicable_type or values or
for type_label in issue_types))
# TODO(jrobbins): also evaluate applicable_predicate
self.display = ezt.boolean( # or fd.show_empty
self.values or self.derived_values or self.applicable)
def MakeFieldValueView(
fd, config, labels, derived_labels, field_values, users_by_id):
"""Return a view on the issue's field value."""
field_name_lower = fd.field_name.lower()
values = []
derived_values = []
if fd.field_type == tracker_pb2.FieldTypes.ENUM_TYPE:
label_docs = {wkl.label: wkl.label_docstring
for wkl in config.well_known_labels}
values = _ConvertLabelsToFieldValues(
labels, field_name_lower, label_docs)
derived_values = _ConvertLabelsToFieldValues(
derived_labels, field_name_lower, label_docs)
values = FindFieldValues(
[fv for fv in field_values if not fv.derived],
fd.field_id, fd.field_type, users_by_id)
derived_values = FindFieldValues(
[fv for fv in field_values if fv.derived],
fd.field_id, fd.field_type, users_by_id)
issue_types = set()
for lab in list(derived_labels) + list(labels):
if lab.lower().startswith('type-'):
issue_types.add(lab.split('-', 1)[1].lower())
return FieldValueView(fd, config, values, derived_values, issue_types)
def FindFieldValues(field_values, field_id, field_type, users_by_id):
"""Accumulate appropriate int, string, or user values in the given fields."""
result = []
for fv in field_values:
if fv.field_id != field_id:
if field_type == tracker_pb2.FieldTypes.INT_TYPE:
val = fv.int_value
elif field_type == tracker_pb2.FieldTypes.STR_TYPE:
val = fv.str_value
elif field_type == tracker_pb2.FieldTypes.USER_TYPE:
if fv.user_id in users_by_id:
val = users_by_id[fv.user_id].email
val = 'USER_%d' % fv.user_id # Should never be visible
logging.error('unexpected field type %r', field_type)
val = ''
# Use ellipsis in the display val if the val is too long.
val=val, docstring=val, idx=len(result)))
return result
def MakeBounceFieldValueViews(field_vals, config):
"""Return a list of field values to display on a validation bounce page."""
field_value_views = []
for fd in config.field_defs:
if fd.field_id in field_vals:
# TODO(jrobbins): also bounce derived values.
val_items = [
template_helpers.EZTItem(val=v, docstring='', idx=idx)
for idx, v in enumerate(field_vals[fd.field_id])]
fd, config, val_items, [], None, applicable=True))
return field_value_views
def _ConvertLabelsToFieldValues(labels, field_name_lower, label_docs):
"""Iterate through the given labels and pull out values for the field.
labels: a list of label strings.
field_name_lower: lowercase string name of the custom field.
label_docs: {label: docstring} for well-known labels in the project.
A list of EZT items with val and docstring fields. One item is included
for each label that matches the given field name.
values = []
field_delim = field_name_lower + '-'
for idx, lab in enumerate(labels):
if lab.lower().startswith(field_delim):
val = lab[len(field_delim):]
# Use ellipsis in the display val if the val is too long.
val_short = template_helpers.FitUnsafeText(str(val), 20)
val=val, val_short=val_short, docstring=label_docs.get(lab, ''),
return values
class FieldDefView(template_helpers.PBProxy):
"""Wrapper class to make it easier to display field definitions via EZT."""
def __init__(self, field_def, config, user_views=None):
super(FieldDefView, self).__init__(field_def)
self.type_name = str(field_def.field_type)
self.choices = []
if field_def.field_type == tracker_pb2.FieldTypes.ENUM_TYPE:
self.choices = tracker_helpers.LabelsMaskedByFields(
config, [field_def.field_name], trim_prefix=True)
self.docstring_short = template_helpers.FitUnsafeText(
field_def.docstring, 200)
self.validate_help = None
if field_def.min_value is not None:
self.min_value = field_def.min_value
self.validate_help = 'Value must be >= %d' % field_def.min_value
self.min_value = None # Otherwise it would default to 0
if field_def.max_value is not None:
self.max_value = field_def.max_value
self.validate_help = 'Value must be <= %d' % field_def.max_value
self.max_value = None # Otherwise it would default to 0
if field_def.min_value is not None and field_def.max_value is not None:
self.validate_help = 'Value must be between %d and %d' % (
field_def.min_value, field_def.max_value)
if field_def.regex:
self.validate_help = 'Value must match regex: %s' % field_def.regex
if field_def.needs_member:
self.validate_help = 'Value must be a project member'
if field_def.needs_perm:
self.validate_help = (
'Value must be a project member with permission %s' %
self.admins = []
if user_views:
self.admins = [user_views.get(admin_id)
for admin_id in field_def.admin_ids]
class IssueTemplateView(template_helpers.PBProxy):
"""Wrapper class to make it easier to display an issue template via EZT."""
def __init__(self, mr, template, user_service, config):
super(IssueTemplateView, self).__init__(template)
self.ownername = ''
self.owner_view = framework_views.MakeUserView(
mr.cnxn, user_service, template.owner_id)
except user_svc.NoSuchUserException:
self.owner_view = None
if self.owner_view:
self.ownername =
self.admin_views = framework_views.MakeAllUserViews(
mr.cnxn, user_service, template.admin_ids).values()
self.admin_names = ', '.join(sorted([ for admin_view in self.admin_views]))
self.summary_must_be_edited = ezt.boolean(template.summary_must_be_edited)
self.members_only = ezt.boolean(template.members_only)
self.owner_defaults_to_member = ezt.boolean(
self.component_required = ezt.boolean(template.component_required)
component_paths = []
for component_id in template.component_ids:
tracker_bizobj.FindComponentDefByID(component_id, config).path)
self.components = ', '.join(component_paths)
self.can_view = ezt.boolean(permissions.CanViewTemplate(
mr.auth.effective_ids, mr.perms, mr.project, template))
self.can_edit = ezt.boolean(permissions.CanEditTemplate(
mr.auth.effective_ids, mr.perms, mr.project, template))
field_name_set = {fd.field_name.lower() for fd in config.field_defs
if not fd.is_deleted} # TODO(jrobbins): restrictions
non_masked_labels = [
lab for lab in template.labels
if not tracker_bizobj.LabelIsMaskedByField(lab, field_name_set)]
for i, label in enumerate(non_masked_labels):
setattr(self, 'label%d' % i, label)
for i in range(len(non_masked_labels), framework_constants.MAX_LABELS):
setattr(self, 'label%d' % i, '')
field_user_views = MakeFieldUserViews(mr.cnxn, template, user_service)
self.field_values = []
for fv in template.field_values:
val=tracker_bizobj.GetFieldValue(fv, field_user_views),
self.complete_field_values = [
fd, config, template.labels, [], template.field_values,
# TODO(jrobbins): field-level view restrictions, display options
for fd in config.field_defs
if not fd.is_deleted]
# Templates only display and edit the first value of multi-valued fields, so
# expose a single value, if any.
# TODO(jrobbins): Fully support multi-valued fields in templates.
for idx, field_value_view in enumerate(self.complete_field_values):
field_value_view.idx = idx
if field_value_view.values:
field_value_view.val = field_value_view.values[0].val
field_value_view.val = None
def MakeFieldUserViews(cnxn, template, user_service):
"""Return {user_id: user_view} for users in template field values."""
field_user_ids = [
fv.user_id for fv in template.field_values
if fv.user_id]
field_user_views = framework_views.MakeAllUserViews(
cnxn, user_service, field_user_ids)
return field_user_views
class ConfigView(template_helpers.PBProxy):
"""Make it easy to display most fieds of a ProjectIssueConfig in EZT."""
def __init__(self, mr, services, config):
"""Gather data for the issue section of a project admin page.
mr: MonorailRequest, including a database connection, the current
project, and authenticated user IDs.
services: Persist services with ProjectService, ConfigService, and
UserService included.
config: ProjectIssueConfig for the current project..
Project info in a dict suitable for EZT.
super(ConfigView, self).__init__(config)
self.open_statuses = []
self.closed_statuses = []
for wks in config.well_known_statuses:
item = template_helpers.EZTItem(
commented='#' if wks.deprecated else '',
if tracker_helpers.MeansOpenInProject(wks.status, config):
self.templates = [
IssueTemplateView(mr, tmpl, services.user, config)
for tmpl in config.templates]
for index, template in enumerate(self.templates):
template.index = index
self.field_names = [ # TODO(jrobbins): field-level controls
fd.field_name for fd in config.field_defs if not fd.is_deleted]
self.issue_labels = tracker_helpers.LabelsNotMaskedByFields(
config, self.field_names)
self.excl_prefixes = [
prefix.lower() for prefix in config.exclusive_label_prefixes]
self.restrict_to_known = ezt.boolean(config.restrict_to_known)
self.default_col_spec = (
config.default_col_spec or tracker_constants.DEFAULT_COL_SPEC)