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

Unified Diff: appengine/monorail/tracker/issueentry.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 side-by-side diff with in-line comments
Download patch
« no previous file with comments | « appengine/monorail/tracker/issuedetail.py ('k') | appengine/monorail/tracker/issueexport.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: appengine/monorail/tracker/issueentry.py
diff --git a/appengine/monorail/tracker/issueentry.py b/appengine/monorail/tracker/issueentry.py
new file mode 100644
index 0000000000000000000000000000000000000000..bbc35444bca8e1d37af11c016fc40aaaa48c811f
--- /dev/null
+++ b/appengine/monorail/tracker/issueentry.py
@@ -0,0 +1,410 @@
+# 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
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Servlet that implements the entry of new issues."""
+
+import logging
+import time
+from third_party import ezt
+
+from features import notify
+from framework import actionlimit
+from framework import framework_bizobj
+from framework import framework_constants
+from framework import framework_helpers
+from framework import framework_views
+from framework import permissions
+from framework import servlet
+from framework import template_helpers
+from framework import urls
+from tracker import field_helpers
+from tracker import tracker_bizobj
+from tracker import tracker_constants
+from tracker import tracker_helpers
+from tracker import tracker_views
+
+PLACEHOLDER_SUMMARY = 'Enter one-line summary'
+
+
+class IssueEntry(servlet.Servlet):
+ """IssueEntry shows a page with a simple form to enter a new issue."""
+
+ _PAGE_TEMPLATE = 'tracker/issue-entry-page.ezt'
+ _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_ISSUES
+ _CAPTCHA_ACTION_TYPES = [actionlimit.ISSUE_COMMENT]
+
+ def AssertBasePermission(self, mr):
+ """Check whether the user has any permission to visit this page.
+
+ Args:
+ mr: commonly used info parsed from the request.
+ """
+ super(IssueEntry, self).AssertBasePermission(mr)
+ if not self.CheckPerm(mr, permissions.CREATE_ISSUE):
+ raise permissions.PermissionException(
+ 'User is not allowed to enter an issue')
+
+ def GatherPageData(self, mr):
+ """Build up a dictionary of data values to use when rendering the page.
+
+ Args:
+ mr: commonly used info parsed from the request.
+
+ Returns:
+ Dict of values used by EZT for rendering the page.
+ """
+ with self.profiler.Phase('getting config'):
+ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
+
+ # In addition to checking perms, we adjust some default field values for
+ # project members.
+ is_member = framework_bizobj.UserIsInProject(
+ mr.project, mr.auth.effective_ids)
+ page_perms = self.MakePagePerms(
+ mr, None,
+ permissions.CREATE_ISSUE,
+ permissions.SET_STAR,
+ permissions.EDIT_ISSUE,
+ permissions.EDIT_ISSUE_SUMMARY,
+ permissions.EDIT_ISSUE_STATUS,
+ permissions.EDIT_ISSUE_OWNER,
+ permissions.EDIT_ISSUE_CC)
+
+ wkp = _SelectTemplate(mr.template_name, config, is_member)
+
+ if wkp.summary:
+ initial_summary = wkp.summary
+ initial_summary_must_be_edited = wkp.summary_must_be_edited
+ else:
+ initial_summary = PLACEHOLDER_SUMMARY
+ initial_summary_must_be_edited = True
+
+ if wkp.status:
+ initial_status = wkp.status
+ elif is_member:
+ initial_status = 'Accepted'
+ else:
+ initial_status = 'New' # not offering meta, only used in hidden field.
+
+ component_paths = []
+ for component_id in wkp.component_ids:
+ component_paths.append(
+ tracker_bizobj.FindComponentDefByID(component_id, config).path)
+ initial_components = ', '.join(component_paths)
+
+ if wkp.owner_id:
+ initial_owner = framework_views.MakeUserView(
+ mr.cnxn, self.services.user, wkp.owner_id)
+ initial_owner_name = initial_owner.email
+ elif wkp.owner_defaults_to_member and page_perms.EditIssue:
+ initial_owner_name = mr.auth.user_view.email
+ else:
+ initial_owner_name = ''
+
+ # Check whether to allow attachments from the entry page
+ allow_attachments = tracker_helpers.IsUnderSoftAttachmentQuota(mr.project)
+
+ config_view = tracker_views.ConfigView(mr, self.services, config)
+ # If the user followed a link that specified the template name, make sure
+ # that it is also in the menu as the current choice.
+ for template_view in config_view.templates:
+ if template_view.name == mr.template_name:
+ template_view.can_view = ezt.boolean(True)
+
+ offer_templates = len(list(
+ tmpl for tmpl in config_view.templates if tmpl.can_view)) > 1
+ restrict_to_known = config.restrict_to_known
+ field_name_set = {fd.field_name.lower() for fd in config.field_defs
+ if not fd.is_deleted} # TODO(jrobbins): restrictions
+ link_or_template_labels = mr.GetListParam('labels', wkp.labels)
+ labels = [lab for lab in link_or_template_labels
+ if not tracker_bizobj.LabelIsMaskedByField(lab, field_name_set)]
+
+ field_user_views = tracker_views.MakeFieldUserViews(
+ mr.cnxn, wkp, self.services.user)
+ field_views = [
+ tracker_views.MakeFieldValueView(
+ fd, config, link_or_template_labels, [], wkp.field_values,
+ field_user_views)
+ # TODO(jrobbins): field-level view restrictions, display options
+ for fd in config.field_defs
+ if not fd.is_deleted]
+
+ page_data = {
+ 'issue_tab_mode': 'issueEntry',
+ 'initial_summary': initial_summary,
+ 'template_summary': initial_summary,
+ 'clear_summary_on_click': ezt.boolean(
+ initial_summary_must_be_edited and
+ 'initial_summary' not in mr.form_overrides),
+ 'must_edit_summary': ezt.boolean(initial_summary_must_be_edited),
+
+ 'initial_description': wkp.content,
+ 'template_name': wkp.name,
+ 'component_required': ezt.boolean(wkp.component_required),
+ 'initial_status': initial_status,
+ 'initial_owner': initial_owner_name,
+ 'initial_components': initial_components,
+ 'initial_cc': '',
+ 'initial_blocked_on': '',
+ 'initial_blocking': '',
+ 'labels': labels,
+ 'fields': field_views,
+
+ 'any_errors': ezt.boolean(mr.errors.AnyErrors()),
+ 'page_perms': page_perms,
+ 'allow_attachments': ezt.boolean(allow_attachments),
+ 'max_attach_size': template_helpers.BytesKbOrMb(
+ framework_constants.MAX_POST_BODY_SIZE),
+
+ 'offer_templates': ezt.boolean(offer_templates),
+ 'config': config_view,
+
+ 'restrict_to_known': ezt.boolean(restrict_to_known),
+ }
+
+ return page_data
+
+ def GatherHelpData(self, mr, _page_data):
+ """Return a dict of values to drive on-page user help.
+
+ Args:
+ mr: commonly used info parsed from the request.
+ _page_data: Dictionary of base and page template data.
+
+ Returns:
+ A dict of values to drive on-page user help, to be added to page_data.
+ """
+ is_privileged_domain_user = framework_bizobj.IsPriviledgedDomainUser(
+ mr.auth.user_pb.email)
+ cue = None
+ if (mr.auth.user_id and
+ 'privacy_click_through' not in mr.auth.user_pb.dismissed_cues):
+ cue = 'privacy_click_through'
+
+ return {
+ 'is_privileged_domain_user': ezt.boolean(is_privileged_domain_user),
+ 'cue': cue,
+ }
+
+ def ProcessFormData(self, mr, post_data):
+ """Process the issue entry form.
+
+ Args:
+ mr: commonly used info parsed from the request.
+ post_data: The post_data dict for the current request.
+
+ Returns:
+ String URL to redirect the user to after processing.
+ """
+ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
+ parsed = tracker_helpers.ParseIssueRequest(
+ mr.cnxn, post_data, self.services, mr.errors, mr.project_name)
+ bounce_labels = parsed.labels[:]
+ bounce_fields = tracker_views.MakeBounceFieldValueViews(
+ parsed.fields.vals, config)
+ field_helpers.ShiftEnumFieldsIntoLabels(
+ parsed.labels, parsed.labels_remove, parsed.fields.vals,
+ parsed.fields.vals_remove, config)
+ field_values = field_helpers.ParseFieldValues(
+ mr.cnxn, self.services.user, parsed.fields.vals, config)
+
+ labels = _DiscardUnusedTemplateLabelPrefixes(parsed.labels)
+ component_ids = tracker_helpers.LookupComponentIDs(
+ parsed.components.paths, config, mr.errors)
+
+ reporter_id = mr.auth.user_id
+ self.CheckCaptcha(mr, post_data)
+
+ if not parsed.summary.strip():
+ mr.errors.summary = 'Summary is required'
+
+ if not parsed.comment.strip():
+ mr.errors.comment = 'A description is required'
+
+ if len(parsed.comment) > tracker_constants.MAX_COMMENT_CHARS:
+ mr.errors.comment = 'Comment is too long'
+ if len(parsed.summary) > tracker_constants.MAX_SUMMARY_CHARS:
+ mr.errors.summary = 'Summary is too long'
+
+ if parsed.users.owner_id is None:
+ mr.errors.owner = 'Invalid owner username'
+ else:
+ valid, msg = tracker_helpers.IsValidIssueOwner(
+ mr.cnxn, mr.project, parsed.users.owner_id, self.services)
+ if not valid:
+ mr.errors.owner = msg
+
+ if None in parsed.users.cc_ids:
+ mr.errors.cc = 'Invalid Cc username'
+
+ field_helpers.ValidateCustomFields(
+ mr, self.services, field_values, config, mr.errors)
+
+ new_local_id = None
+
+ if not mr.errors.AnyErrors():
+ try:
+ if parsed.attachments:
+ new_bytes_used = tracker_helpers.ComputeNewQuotaBytesUsed(
+ mr.project, parsed.attachments)
+ self.services.project.UpdateProject(
+ mr.cnxn, mr.project.project_id,
+ attachment_bytes_used=new_bytes_used)
+
+ template_content = ''
+ for wkp in config.templates:
+ if wkp.name == parsed.template_name:
+ template_content = wkp.content
+ marked_comment = _MarkupDescriptionOnInput(
+ parsed.comment, template_content)
+ has_star = 'star' in post_data and post_data['star'] == '1'
+
+ new_local_id = self.services.issue.CreateIssue(
+ mr.cnxn, self.services,
+ mr.project_id, parsed.summary, parsed.status, parsed.users.owner_id,
+ parsed.users.cc_ids, labels, field_values,
+ component_ids, reporter_id, marked_comment,
+ blocked_on=parsed.blocked_on.iids, blocking=parsed.blocking.iids,
+ attachments=parsed.attachments)
+ self.services.project.UpdateRecentActivity(
+ mr.cnxn, mr.project.project_id)
+
+ issue = self.services.issue.GetIssueByLocalID(
+ mr.cnxn, mr.project_id, new_local_id)
+
+ if has_star:
+ self.services.issue_star.SetStar(
+ mr.cnxn, self.services, config, issue.issue_id, reporter_id, True)
+
+ except tracker_helpers.OverAttachmentQuota:
+ mr.errors.attachments = 'Project attachment quota exceeded.'
+
+ counts = {actionlimit.ISSUE_COMMENT: 1,
+ actionlimit.ISSUE_ATTACHMENT: len(parsed.attachments)}
+ self.CountRateLimitedActions(mr, counts)
+
+ if mr.errors.AnyErrors():
+ component_required = False
+ for wkp in config.templates:
+ if wkp.name == parsed.template_name:
+ component_required = wkp.component_required
+ self.PleaseCorrect(
+ mr, initial_summary=parsed.summary, initial_status=parsed.status,
+ initial_owner=parsed.users.owner_username,
+ initial_cc=', '.join(parsed.users.cc_usernames),
+ initial_components=', '.join(parsed.components.paths),
+ initial_comment=parsed.comment, labels=bounce_labels,
+ fields=bounce_fields,
+ initial_blocked_on=parsed.blocked_on.entered_str,
+ initial_blocking=parsed.blocking.entered_str,
+ component_required=ezt.boolean(component_required))
+ return
+
+ notify.PrepareAndSendIssueChangeNotification(
+ mr.project_id, new_local_id, mr.request.host,
+ reporter_id, 0) # Initial description is comment 0.
+
+ notify.PrepareAndSendIssueBlockingNotification(
+ mr.project_id, mr.request.host, new_local_id,
+ parsed.blocked_on.iids, reporter_id)
+
+ # format a redirect url
+ return framework_helpers.FormatAbsoluteURL(
+ mr, urls.ISSUE_DETAIL, id=new_local_id)
+
+
+def _MarkupDescriptionOnInput(content, tmpl_text):
+ """Return HTML for the content of an issue description or comment.
+
+ Args:
+ content: the text sumbitted by the user, any user-entered markup
+ has already been escaped.
+ tmpl_text: the initial text that was put into the textarea.
+
+ Returns:
+ The description content text with template lines highlighted.
+ """
+ tmpl_lines = tmpl_text.split('\n')
+ tmpl_lines = [pl.strip() for pl in tmpl_lines if pl.strip()]
+
+ entered_lines = content.split('\n')
+ marked_lines = [_MarkupDescriptionLineOnInput(line, tmpl_lines)
+ for line in entered_lines]
+ return '\n'.join(marked_lines)
+
+
+def _MarkupDescriptionLineOnInput(line, tmpl_lines):
+ """Markup one line of an issue description that was just entered.
+
+ Args:
+ line: string containing one line of the user-entered comment.
+ tmpl_lines: list of strings for the text of the template lines.
+
+ Returns:
+ The same user-entered line, or that line highlighted to
+ indicate that it came from the issue template.
+ """
+ for tmpl_line in tmpl_lines:
+ if line.startswith(tmpl_line):
+ return '<b>' + tmpl_line + '</b>' + line[len(tmpl_line):]
+
+ return line
+
+
+def _DiscardUnusedTemplateLabelPrefixes(labels):
+ """Drop any labels that end in '-?'.
+
+ Args:
+ labels: a list of label strings.
+
+ Returns:
+ A list of the same labels, but without any that end with '-?'.
+ Those label prefixes in the new issue templates are intended to
+ prompt the user to enter some label with that prefix, but if
+ nothing is entered there, we do not store anything.
+ """
+ return [lab for lab in labels
+ if not lab.endswith('-?')]
+
+
+def _SelectTemplate(requested_template_name, config, is_member):
+ """Return the template to show to the user in this situation.
+
+ Args:
+ requested_template_name: name of template requested by user, or None.
+ config: ProjectIssueConfig for this project.
+ is_member: True if user is a project member.
+
+ Returns:
+ A Template PB with info needed to populate the issue entry form.
+ """
+ if requested_template_name:
+ for template in config.templates:
+ if requested_template_name == template.name:
+ return template
+ logging.info('Issue template name %s not found', requested_template_name)
+
+ # No template was specified, or it was not found, so go with a default.
+ if is_member:
+ default_id = config.default_template_for_developers
+ else:
+ default_id = config.default_template_for_users
+
+ # Newly created projects have no default templates specified, use hard-coded
+ # positions of the templates that are defined in tracker_constants.
+ if default_id == 0:
+ if is_member:
+ return config.templates[0]
+ elif len(config.templates) > 1:
+ return config.templates[1]
+
+ # This project has a relevant default template ID that we can use.
+ for template in config.templates:
+ if template.template_id == default_id:
+ return template
+
+ # If it was not found, just go with a template that we know exists.
+ return config.templates[0]
« no previous file with comments | « appengine/monorail/tracker/issuedetail.py ('k') | appengine/monorail/tracker/issueexport.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698