Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(384)

Side by Side Diff: appengine/monorail/tracker/tracker_views.py

Issue 1868553004: Open Source Monorail (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: Rebase Created 4 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « appengine/monorail/tracker/tracker_helpers.py ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # Copyright 2016 The Chromium Authors. All rights reserved.
2 # Use of this source code is govered by a BSD-style
3 # license that can be found in the LICENSE file or at
4 # https://developers.google.com/open-source/licenses/bsd
5
6 """View objects to help display tracker business objects in templates."""
7
8 import collections
9 import logging
10 import re
11 import time
12 import urllib
13
14 from google.appengine.api import app_identity
15 from third_party import ezt
16
17 from framework import filecontent
18 from framework import framework_constants
19 from framework import framework_helpers
20 from framework import framework_views
21 from framework import gcs_helpers
22 from framework import permissions
23 from framework import template_helpers
24 from framework import timestr
25 from framework import urls
26 from proto import tracker_pb2
27 from services import user_svc
28 from tracker import tracker_bizobj
29 from tracker import tracker_constants
30 from tracker import tracker_helpers
31
32
33 class IssueView(template_helpers.PBProxy):
34 """Wrapper class that makes it easier to display an Issue via EZT."""
35
36 def __init__(
37 self, issue, users_by_id, config, open_related=None,
38 closed_related=None, all_related=None):
39 """Store relevant values for later display by EZT.
40
41 Args:
42 issue: An Issue protocol buffer.
43 users_by_id: dict {user_id: UserViews} for all users mentioned in issue.
44 config: ProjectIssueConfig for this issue.
45 open_related: dict of visible open issues that are related to this issue.
46 closed_related: dict {issue_id: issue} of visible closed issues that
47 are related to this issue.
48 all_related: dict {issue_id: issue} of all blocked-on, blocking,
49 or merged-into issues referenced from this issue, regardless of
50 perms.
51 """
52 super(IssueView, self).__init__(issue)
53
54 # The users involved in this issue must be present in users_by_id if
55 # this IssueView is to be used on the issue detail or peek pages. But,
56 # they can be absent from users_by_id if the IssueView is used as a
57 # tile in the grid view.
58 self.owner = users_by_id.get(issue.owner_id)
59 self.derived_owner = users_by_id.get(issue.derived_owner_id)
60 self.cc = [users_by_id.get(cc_id) for cc_id in issue.cc_ids
61 if cc_id]
62 self.derived_cc = [users_by_id.get(cc_id)
63 for cc_id in issue.derived_cc_ids
64 if cc_id]
65 self.status = framework_views.StatusView(issue.status, config)
66 self.derived_status = framework_views.StatusView(
67 issue.derived_status, config)
68 # If we don't have a config available, we don't need to access is_open, so
69 # let it be True.
70 self.is_open = ezt.boolean(
71 not config or
72 tracker_helpers.MeansOpenInProject(
73 tracker_bizobj.GetStatus(issue), config))
74
75 self.components = sorted(
76 [ComponentValueView(component_id, config, False)
77 for component_id in issue.component_ids
78 if tracker_bizobj.FindComponentDefByID(component_id, config)] +
79 [ComponentValueView(component_id, config, True)
80 for component_id in issue.derived_component_ids
81 if tracker_bizobj.FindComponentDefByID(component_id, config)],
82 key=lambda cvv: cvv.path)
83
84 self.fields = [
85 MakeFieldValueView(
86 fd, config, issue.labels, issue.derived_labels, issue.field_values,
87 users_by_id)
88 # TODO(jrobbins): field-level view restrictions, display options
89 for fd in config.field_defs
90 if not fd.is_deleted]
91 self.fields = sorted(
92 self.fields, key=lambda f: (f.applicable_type, f.field_name))
93
94 field_names = [fd.field_name.lower() for fd in config.field_defs
95 if not fd.is_deleted] # TODO(jrobbins): restricts
96 self.labels = [
97 framework_views.LabelView(label, config)
98 for label in tracker_bizobj.NonMaskedLabels(issue.labels, field_names)]
99 self.derived_labels = [
100 framework_views.LabelView(label, config)
101 for label in issue.derived_labels
102 if not tracker_bizobj.LabelIsMaskedByField(label, field_names)]
103 self.restrictions = _RestrictionsView(issue)
104
105 # TODO(jrobbins): sort by order of labels in project config
106
107 self.short_summary = issue.summary[:tracker_constants.SHORT_SUMMARY_LENGTH]
108
109 if issue.closed_timestamp:
110 self.closed = timestr.FormatAbsoluteDate(issue.closed_timestamp)
111 else:
112 self.closed = ''
113
114 blocked_on_iids = issue.blocked_on_iids
115 blocking_iids = issue.blocking_iids
116
117 # Note that merged_into_str and blocked_on_str includes all issue
118 # references, even those referring to issues that the user can't view,
119 # so open_related and closed_related cannot be used.
120 if all_related is not None:
121 all_blocked_on_refs = [
122 (all_related[ref_iid].project_name, all_related[ref_iid].local_id)
123 for ref_iid in issue.blocked_on_iids]
124 all_blocked_on_refs.extend([
125 (r.project, r.issue_id) for r in issue.dangling_blocked_on_refs])
126 self.blocked_on_str = ', '.join(
127 tracker_bizobj.FormatIssueRef(
128 ref, default_project_name=issue.project_name)
129 for ref in all_blocked_on_refs)
130 all_blocking_refs = [
131 (all_related[ref_iid].project_name, all_related[ref_iid].local_id)
132 for ref_iid in issue.blocking_iids]
133 all_blocking_refs.extend([
134 (r.project, r.issue_id) for r in issue.dangling_blocking_refs])
135 self.blocking_str = ', '.join(
136 tracker_bizobj.FormatIssueRef(
137 ref, default_project_name=issue.project_name)
138 for ref in all_blocking_refs)
139 if issue.merged_into:
140 merged_issue = all_related[issue.merged_into]
141 merged_into_ref = merged_issue.project_name, merged_issue.local_id
142 else:
143 merged_into_ref = None
144 self.merged_into_str = tracker_bizobj.FormatIssueRef(
145 merged_into_ref, default_project_name=issue.project_name)
146
147 self.blocked_on = []
148 self.blocking = []
149 current_project_name = issue.project_name
150
151 if open_related is not None and closed_related is not None:
152 self.merged_into = IssueRefView(
153 current_project_name, issue.merged_into,
154 open_related, closed_related)
155
156 self.blocked_on = [
157 IssueRefView(current_project_name, iid, open_related, closed_related)
158 for iid in blocked_on_iids]
159 self.blocked_on.extend(
160 [DanglingIssueRefView(ref.project, ref.issue_id)
161 for ref in issue.dangling_blocked_on_refs])
162 self.blocked_on = [irv for irv in self.blocked_on if irv.visible]
163 # TODO(jrobbins): sort by irv project_name and local_id
164
165 self.blocking = [
166 IssueRefView(current_project_name, iid, open_related, closed_related)
167 for iid in blocking_iids]
168 self.blocking.extend(
169 [DanglingIssueRefView(ref.project, ref.issue_id)
170 for ref in issue.dangling_blocking_refs])
171 self.blocking = [irv for irv in self.blocking if irv.visible]
172 # TODO(jrobbins): sort by irv project_name and local_id
173
174 self.detail_relative_url = tracker_helpers.FormatRelativeIssueURL(
175 issue.project_name, urls.ISSUE_DETAIL, id=issue.local_id)
176
177
178 class _RestrictionsView(object):
179 """An EZT object for the restrictions associated with an issue."""
180
181 # Restrict label fragments that correspond to known permissions.
182 _VIEW = permissions.VIEW.lower()
183 _EDIT = permissions.EDIT_ISSUE.lower()
184 _ADD_COMMENT = permissions.ADD_ISSUE_COMMENT.lower()
185 _KNOWN_ACTION_KINDS = {_VIEW, _EDIT, _ADD_COMMENT}
186
187 def __init__(self, issue):
188 # List of restrictions that don't map to a known action kind.
189 self.other = []
190
191 restrictions_by_action = collections.defaultdict(list)
192 # We can't use GetRestrictions here, as we prefer to preserve
193 # the case of the label when showing restrictions in the UI.
194 for label in tracker_bizobj.GetLabels(issue):
195 if permissions.IsRestrictLabel(label):
196 _kw, action_kind, needed_perm = label.split('-', 2)
197 action_kind = action_kind.lower()
198 if action_kind in self._KNOWN_ACTION_KINDS:
199 restrictions_by_action[action_kind].append(needed_perm)
200 else:
201 self.other.append(label)
202
203 self.view = ' and '.join(restrictions_by_action[self._VIEW])
204 self.add_comment = ' and '.join(restrictions_by_action[self._ADD_COMMENT])
205 self.edit = ' and '.join(restrictions_by_action[self._EDIT])
206
207 self.has_restrictions = ezt.boolean(
208 self.view or self.add_comment or self.edit or self.other)
209
210
211 class IssueRefView(object):
212 """A simple object to easily display links to issues in EZT."""
213
214 def __init__(self, current_project_name, issue_id, open_dict, closed_dict):
215 """Make a simple object to display a link to a referenced issue.
216
217 Args:
218 current_project_name: string name of the current project.
219 issue_id: int issue ID of the target issue.
220 open_dict: dict {issue_id: issue} of pre-fetched open issues that the
221 user is allowed to view.
222 closed_dict: dict of pre-fetched closed issues that the user is
223 allowed to view.
224
225 Note, the target issue may be a member of either open_dict or
226 closed_dict, or neither one. If neither, nothing is displayed.
227 """
228 if (not issue_id or
229 issue_id not in open_dict and issue_id not in closed_dict):
230 # Issue not found or not visible to this user, so don't link to it.
231 self.visible = ezt.boolean(False)
232 return
233
234 self.visible = ezt.boolean(True)
235
236 if issue_id in open_dict:
237 related_issue = open_dict[issue_id]
238 self.is_open = ezt.boolean(True)
239 else:
240 related_issue = closed_dict[issue_id]
241 self.is_open = ezt.boolean(False)
242
243 if current_project_name == related_issue.project_name:
244 self.url = 'detail?id=%s' % related_issue.local_id
245 self.display_name = 'issue %s' % related_issue.local_id
246 else:
247 self.url = '/p/%s%s?id=%s' % (
248 related_issue.project_name, urls.ISSUE_DETAIL,
249 related_issue.local_id)
250 self.display_name = 'issue %s:%s' % (
251 related_issue.project_name, related_issue.local_id)
252
253 self.summary = related_issue.summary
254
255 def DebugString(self):
256 if not self.visible:
257 return 'IssueRefView(not visible)'
258
259 return 'IssueRefView(%s)' % self.display_name
260
261
262 class DanglingIssueRefView(object):
263
264 def __init__(self, project_name, issue_id):
265 """Makes a simple object to display a link to an issue still in Codesite.
266
267 Satisfies the same API and internal data members as IssueRefView,
268 excpet for the arguments to __init__.
269
270 Args:
271 project_name: The name of the project on Codesite
272 issue_id: The local id of the issue in that project
273 """
274 self.visible = True
275 self.is_open = True # TODO(agable) Make a call to Codesite to set this?
276 self.url = 'https://code.google.com/p/%s/issues/detail?id=%d' % (
277 project_name, issue_id)
278 self.display_name = 'issue %s:%d' % (project_name, issue_id)
279 self.short_name = 'issue %s:%d' % (project_name, issue_id)
280 self.summary = 'Issue %d in %s.' % (issue_id, project_name)
281
282 def DebugString(self):
283 return 'DanglingIssueRefView(%s)' % self.display_name
284
285
286 class IssueCommentView(template_helpers.PBProxy):
287 """Wrapper class that makes it easier to display an IssueComment via EZT."""
288
289 def __init__(
290 self, project_name, comment_pb, users_by_id, autolink,
291 all_referenced_artifacts, mr, issue, effective_ids=None):
292 """Get IssueComment PB and make its fields available as attrs.
293
294 Args:
295 project_name: Name of the project this issue belongs to.
296 comment_pb: Comment protocol buffer.
297 users_by_id: dict mapping user_ids to UserViews, including
298 the user that entered the comment, and any changed participants.
299 autolink: utility object for automatically linking to other
300 issues, svn revisions, etc.
301 all_referenced_artifacts: opaque object with details of referenced
302 artifacts that is needed by autolink.
303 mr: common information parsed from the HTTP request.
304 issue: Issue PB for the issue that this comment is part of.
305 effective_ids: optional set of int user IDs for the comment author.
306 """
307 super(IssueCommentView, self).__init__(comment_pb)
308
309 self.id = comment_pb.id
310 self.creator = users_by_id[comment_pb.user_id]
311
312 # TODO(jrobbins): this should be based on the issue project, not the
313 # request project for non-project views and cross-project.
314 if mr.project:
315 self.creator_role = framework_helpers.GetRoleName(
316 effective_ids or {self.creator.user_id}, mr.project)
317 else:
318 self.creator_role = None
319
320 time_tuple = time.localtime(comment_pb.timestamp)
321 self.date_string = timestr.FormatAbsoluteDate(
322 comment_pb.timestamp, old_format=timestr.MONTH_DAY_FMT)
323 self.date_relative = timestr.FormatRelativeDate(comment_pb.timestamp)
324 self.date_tooltip = time.asctime(time_tuple)
325 self.text_runs = _ParseTextRuns(comment_pb.content)
326 if autolink:
327 self.text_runs = autolink.MarkupAutolinks(
328 mr, self.text_runs, all_referenced_artifacts)
329
330 self.attachments = [AttachmentView(attachment, project_name)
331 for attachment in comment_pb.attachments]
332 self.amendments = [
333 AmendmentView(amendment, users_by_id, mr.project_name)
334 for amendment in comment_pb.amendments]
335 # Treat comments from banned users as being deleted.
336 self.is_deleted = (comment_pb.deleted_by or
337 (self.creator and self.creator.banned))
338 self.can_delete = False
339 if mr.auth.user_id and mr.project:
340 # TODO(jrobbins): pass through config, then I can do:
341 # granted_perms = tracker_bizobj.GetGrantedPerms(
342 # issue, mr.auth.effective_ids, config)
343 self.can_delete = permissions.CanDelete(
344 mr.auth.user_id, mr.auth.effective_ids, mr.perms,
345 comment_pb.deleted_by, comment_pb.user_id,
346 mr.project, permissions.GetRestrictions(issue))
347
348 # Prevent spammers from undeleting their own comments, but
349 # allow people with permission to undelete their own comments.
350 if comment_pb.is_spam and comment_pb.user_id == mr.auth.user_id:
351 self.can_delete = mr.perms.HasPerm(permissions.MODERATE_SPAM,
352 mr.auth.user_id, mr.project)
353
354 self.visible = self.can_delete or not self.is_deleted
355
356
357 _TEMPLATE_TEXT_RE = re.compile('^(<b>[^<]+</b>)', re.MULTILINE)
358
359
360 def _ParseTextRuns(content):
361 """Convert the user's comment to a list of TextRun objects."""
362 chunks = _TEMPLATE_TEXT_RE.split(content)
363 runs = [_ChunkToRun(chunk) for chunk in chunks]
364 return runs
365
366
367 def _ChunkToRun(chunk):
368 """Convert a substring of the user's comment to a TextRun object."""
369 if chunk.startswith('<b>') and chunk.endswith('</b>'):
370 return template_helpers.TextRun(chunk[3:-4], tag='b')
371 else:
372 return template_helpers.TextRun(chunk)
373
374
375 VIEWABLE_IMAGE_TYPES = ['image/jpeg', 'image/gif', 'image/png', 'image/x-png']
376 MAX_PREVIEW_FILESIZE = 4 * 1024 * 1024 # 4MB
377
378
379 class LogoView(template_helpers.PBProxy):
380 """Wrapper class to make it easier to display project logos via EZT."""
381
382 def __init__(self, project_pb):
383 if (not project_pb or
384 not project_pb.logo_gcs_id or
385 not project_pb.logo_file_name):
386 self.thumbnail_url = ''
387 self.viewurl = ''
388 return
389
390 object_path = ('/' + app_identity.get_default_gcs_bucket_name() +
391 project_pb.logo_gcs_id)
392 self.filename = project_pb.logo_file_name
393 self.mimetype = filecontent.GuessContentTypeFromFilename(self.filename)
394
395 self.thumbnail_url = gcs_helpers.SignUrl(object_path + '-thumbnail')
396 self.viewurl = (
397 gcs_helpers.SignUrl(object_path) + '&' + urllib.urlencode(
398 {'response-content-displacement':
399 ('attachment; filename=%s' % self.filename)}))
400
401
402 class AttachmentView(template_helpers.PBProxy):
403 """Wrapper class to make it easier to display issue attachments via EZT."""
404
405 def __init__(self, attach_pb, project_name):
406 """Get IssueAttachmentContent PB and make its fields available as attrs.
407
408 Args:
409 attach_pb: Attachment part of IssueComment protocol buffer.
410 project_name: string Name of the current project.
411 """
412 super(AttachmentView, self).__init__(attach_pb)
413 self.filesizestr = template_helpers.BytesKbOrMb(attach_pb.filesize)
414 self.downloadurl = 'attachment?aid=%s' % attach_pb.attachment_id
415
416 self.url = None
417 self.thumbnail_url = None
418 if IsViewableImage(attach_pb.mimetype, attach_pb.filesize):
419 self.url = self.downloadurl + '&inline=1'
420 self.thumbnail_url = self.url + '&thumb=1'
421 elif IsViewableText(attach_pb.mimetype, attach_pb.filesize):
422 self.url = tracker_helpers.FormatRelativeIssueURL(
423 project_name, urls.ISSUE_ATTACHMENT_TEXT,
424 aid=attach_pb.attachment_id)
425
426 self.iconurl = '/images/paperclip.png'
427
428
429 def IsViewableImage(mimetype_charset, filesize):
430 """Return true if we can safely display such an image in the browser.
431
432 Args:
433 mimetype_charset: string with the mimetype string that we got back
434 from the 'file' command. It may have just the mimetype, or it
435 may have 'foo/bar; charset=baz'.
436 filesize: int length of the file in bytes.
437
438 Returns:
439 True iff we should allow the user to view a thumbnail or safe version
440 of the image in the browser. False if this might not be safe to view,
441 in which case we only offer a download link.
442 """
443 mimetype = mimetype_charset.split(';', 1)[0]
444 return (mimetype in VIEWABLE_IMAGE_TYPES and
445 filesize < MAX_PREVIEW_FILESIZE)
446
447
448 def IsViewableText(mimetype, filesize):
449 """Return true if we can safely display such a file as escaped text."""
450 return (mimetype.startswith('text/') and
451 filesize < MAX_PREVIEW_FILESIZE)
452
453
454 class AmendmentView(object):
455 """Wrapper class that makes it easier to display an Amendment via EZT."""
456
457 def __init__(self, amendment, users_by_id, project_name):
458 """Get the info from the PB and put it into easily accessible attrs.
459
460 Args:
461 amendment: Amendment part of an IssueComment protocol buffer.
462 users_by_id: dict mapping user_ids to UserViews.
463 project_name: Name of the project the issue/comment/amendment is in.
464 """
465 # TODO(jrobbins): take field-level restrictions into account.
466 # Including the case where user is not allowed to see any amendments.
467 self.field_name = tracker_bizobj.GetAmendmentFieldName(amendment)
468 self.newvalue = tracker_bizobj.AmendmentString(amendment, users_by_id)
469 self.values = tracker_bizobj.AmendmentLinks(
470 amendment, users_by_id, project_name)
471
472
473 class ComponentDefView(template_helpers.PBProxy):
474 """Wrapper class to make it easier to display component definitions."""
475
476 def __init__(self, component_def, users_by_id):
477 super(ComponentDefView, self).__init__(component_def)
478
479 c_path = component_def.path
480 if '>' in c_path:
481 self.parent_path = c_path[:c_path.rindex('>')]
482 self.leaf_name = c_path[c_path.rindex('>') + 1:]
483 else:
484 self.parent_path = ''
485 self.leaf_name = c_path
486
487 self.docstring_short = template_helpers.FitUnsafeText(
488 component_def.docstring, 200)
489
490 self.admins = [users_by_id.get(admin_id)
491 for admin_id in component_def.admin_ids]
492 self.cc = [users_by_id.get(cc_id) for cc_id in component_def.cc_ids]
493 self.classes = 'all '
494 if self.parent_path == '':
495 self.classes += 'toplevel '
496 self.classes += 'deprecated ' if component_def.deprecated else 'active '
497
498
499 class ComponentValueView(object):
500 """Wrapper class that makes it easier to display a component value."""
501
502 def __init__(self, component_id, config, derived):
503 """Make the component name and docstring available as attrs.
504
505 Args:
506 component_id: int component_id to look up in the config
507 config: ProjectIssueConfig PB for the issue's project.
508 derived: True if this component was derived.
509 """
510 cd = tracker_bizobj.FindComponentDefByID(component_id, config)
511 self.path = cd.path
512 self.docstring = cd.docstring
513 self.docstring_short = template_helpers.FitUnsafeText(cd.docstring, 60)
514 self.derived = ezt.boolean(derived)
515
516
517 class FieldValueView(object):
518 """Wrapper class that makes it easier to display a custom field value."""
519
520 def __init__(
521 self, fd, config, values, derived_values, issue_types, applicable=None):
522 """Make several values related to this field available as attrs.
523
524 Args:
525 fd: field definition to be displayed (or not, if no value).
526 config: ProjectIssueConfig PB for the issue's project.
527 values: list of explicit field values.
528 derived_values: list of derived field values.
529 issue_types: set of lowered string values from issues' "Type-*" labels.
530 applicable: optional boolean that overrides the rule that determines
531 when a field is applicable.
532 """
533 self.field_def = FieldDefView(fd, config)
534 self.field_id = fd.field_id
535 self.field_name = fd.field_name
536 self.field_docstring = fd.docstring
537 self.field_docstring_short = template_helpers.FitUnsafeText(
538 fd.docstring, 60)
539
540 self.values = values
541 self.derived_values = derived_values
542
543 self.applicable_type = fd.applicable_type
544 if applicable is not None:
545 self.applicable = ezt.boolean(applicable)
546 else:
547 # A field is applicable to a given issue if it (a) applies to all issues,
548 # or (b) already has a value on this issue, or (c) says that it applies to
549 # issues with this type (or a prefix of it).
550 self.applicable = ezt.boolean(
551 not self.applicable_type or values or
552 any(type_label.startswith(self.applicable_type.lower())
553 for type_label in issue_types))
554 # TODO(jrobbins): also evaluate applicable_predicate
555
556 self.display = ezt.boolean( # or fd.show_empty
557 self.values or self.derived_values or self.applicable)
558
559
560 def MakeFieldValueView(
561 fd, config, labels, derived_labels, field_values, users_by_id):
562 """Return a view on the issue's field value."""
563 field_name_lower = fd.field_name.lower()
564 values = []
565 derived_values = []
566
567 if fd.field_type == tracker_pb2.FieldTypes.ENUM_TYPE:
568 label_docs = {wkl.label: wkl.label_docstring
569 for wkl in config.well_known_labels}
570 values = _ConvertLabelsToFieldValues(
571 labels, field_name_lower, label_docs)
572 derived_values = _ConvertLabelsToFieldValues(
573 derived_labels, field_name_lower, label_docs)
574
575 else:
576 values = FindFieldValues(
577 [fv for fv in field_values if not fv.derived],
578 fd.field_id, fd.field_type, users_by_id)
579 derived_values = FindFieldValues(
580 [fv for fv in field_values if fv.derived],
581 fd.field_id, fd.field_type, users_by_id)
582
583 issue_types = set()
584 for lab in list(derived_labels) + list(labels):
585 if lab.lower().startswith('type-'):
586 issue_types.add(lab.split('-', 1)[1].lower())
587
588 return FieldValueView(fd, config, values, derived_values, issue_types)
589
590
591 def FindFieldValues(field_values, field_id, field_type, users_by_id):
592 """Accumulate appropriate int, string, or user values in the given fields."""
593 result = []
594 for fv in field_values:
595 if fv.field_id != field_id:
596 continue
597
598 if field_type == tracker_pb2.FieldTypes.INT_TYPE:
599 val = fv.int_value
600 elif field_type == tracker_pb2.FieldTypes.STR_TYPE:
601 val = fv.str_value
602 elif field_type == tracker_pb2.FieldTypes.USER_TYPE:
603 if fv.user_id in users_by_id:
604 val = users_by_id[fv.user_id].email
605 else:
606 val = 'USER_%d' % fv.user_id # Should never be visible
607 else:
608 logging.error('unexpected field type %r', field_type)
609 val = ''
610
611 # Use ellipsis in the display val if the val is too long.
612 result.append(template_helpers.EZTItem(
613 val=val, docstring=val, idx=len(result)))
614
615 return result
616
617
618 def MakeBounceFieldValueViews(field_vals, config):
619 """Return a list of field values to display on a validation bounce page."""
620 field_value_views = []
621 for fd in config.field_defs:
622 if fd.field_id in field_vals:
623 # TODO(jrobbins): also bounce derived values.
624 val_items = [
625 template_helpers.EZTItem(val=v, docstring='', idx=idx)
626 for idx, v in enumerate(field_vals[fd.field_id])]
627 field_value_views.append(FieldValueView(
628 fd, config, val_items, [], None, applicable=True))
629
630 return field_value_views
631
632
633 def _ConvertLabelsToFieldValues(labels, field_name_lower, label_docs):
634 """Iterate through the given labels and pull out values for the field.
635
636 Args:
637 labels: a list of label strings.
638 field_name_lower: lowercase string name of the custom field.
639 label_docs: {label: docstring} for well-known labels in the project.
640
641 Returns:
642 A list of EZT items with val and docstring fields. One item is included
643 for each label that matches the given field name.
644 """
645 values = []
646 field_delim = field_name_lower + '-'
647 for idx, lab in enumerate(labels):
648 if lab.lower().startswith(field_delim):
649 val = lab[len(field_delim):]
650 # Use ellipsis in the display val if the val is too long.
651 val_short = template_helpers.FitUnsafeText(str(val), 20)
652 values.append(template_helpers.EZTItem(
653 val=val, val_short=val_short, docstring=label_docs.get(lab, ''),
654 idx=idx))
655
656 return values
657
658
659 class FieldDefView(template_helpers.PBProxy):
660 """Wrapper class to make it easier to display field definitions via EZT."""
661
662 def __init__(self, field_def, config, user_views=None):
663 super(FieldDefView, self).__init__(field_def)
664
665 self.type_name = str(field_def.field_type)
666
667 self.choices = []
668 if field_def.field_type == tracker_pb2.FieldTypes.ENUM_TYPE:
669 self.choices = tracker_helpers.LabelsMaskedByFields(
670 config, [field_def.field_name], trim_prefix=True)
671
672 self.docstring_short = template_helpers.FitUnsafeText(
673 field_def.docstring, 200)
674 self.validate_help = None
675
676 if field_def.min_value is not None:
677 self.min_value = field_def.min_value
678 self.validate_help = 'Value must be >= %d' % field_def.min_value
679 else:
680 self.min_value = None # Otherwise it would default to 0
681
682 if field_def.max_value is not None:
683 self.max_value = field_def.max_value
684 self.validate_help = 'Value must be <= %d' % field_def.max_value
685 else:
686 self.max_value = None # Otherwise it would default to 0
687
688 if field_def.min_value is not None and field_def.max_value is not None:
689 self.validate_help = 'Value must be between %d and %d' % (
690 field_def.min_value, field_def.max_value)
691
692 if field_def.regex:
693 self.validate_help = 'Value must match regex: %s' % field_def.regex
694
695 if field_def.needs_member:
696 self.validate_help = 'Value must be a project member'
697
698 if field_def.needs_perm:
699 self.validate_help = (
700 'Value must be a project member with permission %s' %
701 field_def.needs_perm)
702
703 self.admins = []
704 if user_views:
705 self.admins = [user_views.get(admin_id)
706 for admin_id in field_def.admin_ids]
707
708
709 class IssueTemplateView(template_helpers.PBProxy):
710 """Wrapper class to make it easier to display an issue template via EZT."""
711
712 def __init__(self, mr, template, user_service, config):
713 super(IssueTemplateView, self).__init__(template)
714
715 self.ownername = ''
716 try:
717 self.owner_view = framework_views.MakeUserView(
718 mr.cnxn, user_service, template.owner_id)
719 except user_svc.NoSuchUserException:
720 self.owner_view = None
721 if self.owner_view:
722 self.ownername = self.owner_view.email
723
724 self.admin_views = framework_views.MakeAllUserViews(
725 mr.cnxn, user_service, template.admin_ids).values()
726 self.admin_names = ', '.join(sorted([
727 admin_view.email for admin_view in self.admin_views]))
728
729 self.summary_must_be_edited = ezt.boolean(template.summary_must_be_edited)
730 self.members_only = ezt.boolean(template.members_only)
731 self.owner_defaults_to_member = ezt.boolean(
732 template.owner_defaults_to_member)
733 self.component_required = ezt.boolean(template.component_required)
734
735 component_paths = []
736 for component_id in template.component_ids:
737 component_paths.append(
738 tracker_bizobj.FindComponentDefByID(component_id, config).path)
739 self.components = ', '.join(component_paths)
740
741 self.can_view = ezt.boolean(permissions.CanViewTemplate(
742 mr.auth.effective_ids, mr.perms, mr.project, template))
743 self.can_edit = ezt.boolean(permissions.CanEditTemplate(
744 mr.auth.effective_ids, mr.perms, mr.project, template))
745
746 field_name_set = {fd.field_name.lower() for fd in config.field_defs
747 if not fd.is_deleted} # TODO(jrobbins): restrictions
748 non_masked_labels = [
749 lab for lab in template.labels
750 if not tracker_bizobj.LabelIsMaskedByField(lab, field_name_set)]
751
752 for i, label in enumerate(non_masked_labels):
753 setattr(self, 'label%d' % i, label)
754 for i in range(len(non_masked_labels), framework_constants.MAX_LABELS):
755 setattr(self, 'label%d' % i, '')
756
757 field_user_views = MakeFieldUserViews(mr.cnxn, template, user_service)
758 self.field_values = []
759 for fv in template.field_values:
760 self.field_values.append(template_helpers.EZTItem(
761 field_id=fv.field_id,
762 val=tracker_bizobj.GetFieldValue(fv, field_user_views),
763 idx=len(self.field_values)))
764
765 self.complete_field_values = [
766 MakeFieldValueView(
767 fd, config, template.labels, [], template.field_values,
768 field_user_views)
769 # TODO(jrobbins): field-level view restrictions, display options
770 for fd in config.field_defs
771 if not fd.is_deleted]
772
773 # Templates only display and edit the first value of multi-valued fields, so
774 # expose a single value, if any.
775 # TODO(jrobbins): Fully support multi-valued fields in templates.
776 for idx, field_value_view in enumerate(self.complete_field_values):
777 field_value_view.idx = idx
778 if field_value_view.values:
779 field_value_view.val = field_value_view.values[0].val
780 else:
781 field_value_view.val = None
782
783
784 def MakeFieldUserViews(cnxn, template, user_service):
785 """Return {user_id: user_view} for users in template field values."""
786 field_user_ids = [
787 fv.user_id for fv in template.field_values
788 if fv.user_id]
789 field_user_views = framework_views.MakeAllUserViews(
790 cnxn, user_service, field_user_ids)
791 return field_user_views
792
793
794 class ConfigView(template_helpers.PBProxy):
795 """Make it easy to display most fieds of a ProjectIssueConfig in EZT."""
796
797 def __init__(self, mr, services, config):
798 """Gather data for the issue section of a project admin page.
799
800 Args:
801 mr: MonorailRequest, including a database connection, the current
802 project, and authenticated user IDs.
803 services: Persist services with ProjectService, ConfigService, and
804 UserService included.
805 config: ProjectIssueConfig for the current project..
806
807 Returns:
808 Project info in a dict suitable for EZT.
809 """
810 super(ConfigView, self).__init__(config)
811 self.open_statuses = []
812 self.closed_statuses = []
813 for wks in config.well_known_statuses:
814 item = template_helpers.EZTItem(
815 name=wks.status,
816 name_padded=wks.status.ljust(20),
817 commented='#' if wks.deprecated else '',
818 docstring=wks.status_docstring)
819 if tracker_helpers.MeansOpenInProject(wks.status, config):
820 self.open_statuses.append(item)
821 else:
822 self.closed_statuses.append(item)
823
824 self.templates = [
825 IssueTemplateView(mr, tmpl, services.user, config)
826 for tmpl in config.templates]
827 for index, template in enumerate(self.templates):
828 template.index = index
829
830 self.field_names = [ # TODO(jrobbins): field-level controls
831 fd.field_name for fd in config.field_defs if not fd.is_deleted]
832 self.issue_labels = tracker_helpers.LabelsNotMaskedByFields(
833 config, self.field_names)
834 self.excl_prefixes = [
835 prefix.lower() for prefix in config.exclusive_label_prefixes]
836 self.restrict_to_known = ezt.boolean(config.restrict_to_known)
837
838 self.default_col_spec = (
839 config.default_col_spec or tracker_constants.DEFAULT_COL_SPEC)
OLDNEW
« no previous file with comments | « appengine/monorail/tracker/tracker_helpers.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698