def _ParseChoicesIntoWellKnownLabels( choices_text, field_name, config, field_type_str): """Parse a field's possible choices and integrate them into the config. Args: choices_text: string with one label and optional docstring per line. field_name: string name of the field definition being edited. config: ProjectIssueConfig PB of the current project. field_type_str: string name of the new field's type. None if an existing field is being updated Returns: A revised list of labels that can be used to update the config. """ fd = tracker_bizobj.FindFieldDef(field_name, config) matches = framework_constants.IDENTIFIER_DOCSTRING_RE.findall(choices_text) maskingFieldNames = [] # wkls should only be masked by the field if it is an enum_type. if (field_type_str == 'enum_type') or ( fd and fd.field_type is tracker_pb2.FieldTypes.ENUM_TYPE): maskingFieldNames.append(field_name.lower()) new_labels = [ ('%s-%s' % (field_name, label), choice_docstring.strip(), False) for label, choice_docstring in matches] kept_labels = [ (wkl.label, wkl.label_docstring, wkl.deprecated) for wkl in config.well_known_labels if not tracker_bizobj.LabelIsMaskedByField( wkl.label, maskingFieldNames)] revised_labels = kept_labels + new_labels return revised_labels
def testLabelIsMaskedByField(self): self.assertIsNone(tracker_bizobj.LabelIsMaskedByField('UI', [])) self.assertIsNone(tracker_bizobj.LabelIsMaskedByField('P-1', [])) field_names = ['priority', 'size'] self.assertIsNone(tracker_bizobj.LabelIsMaskedByField( 'UI', field_names)) self.assertIsNone(tracker_bizobj.LabelIsMaskedByField( 'OS-All', field_names)) self.assertEqual( 'size', tracker_bizobj.LabelIsMaskedByField('size-xl', field_names)) self.assertEqual( 'size', tracker_bizobj.LabelIsMaskedByField('Size-XL', field_names))
def _ParseChoicesIntoWellKnownLabels(choices_text, field_name, config): """Parse a field's possible choices and integrate them into the config. Args: choices_text: string with one label and optional docstring per line. field_name: string name of the field definition being edited. config: ProjectIssueConfig PB of the current project. Returns: A revised list of labels that can be used to update the config. """ matches = framework_constants.IDENTIFIER_DOCSTRING_RE.findall(choices_text) new_labels = [('%s-%s' % (field_name, label), choice_docstring.strip(), False) for label, choice_docstring in matches] kept_labels = [(wkl.label, wkl.label_docstring, False) for wkl in config.well_known_labels if not tracker_bizobj.LabelIsMaskedByField( wkl.label, [field_name.lower()])] revised_labels = kept_labels + new_labels return revised_labels
def _LabelsMaskedOrNot(config, field_names, invert=False, trim_prefix=False): """Return EZTItems for labels that'd be masked. Or not, when invert=True.""" field_names = [fn.lower() for fn in field_names] result = [] for wkl in config.well_known_labels: masked_by = tracker_bizobj.LabelIsMaskedByField(wkl.label, field_names) if (masked_by and not invert) or (not masked_by and invert): display_name = wkl.label if trim_prefix: display_name = display_name[len(masked_by) + 1:] result.append( template_helpers.EZTItem( name=display_name, name_padded=display_name.ljust(20), commented='#' if wkl.deprecated else '', docstring=wkl.label_docstring, docstring_short=template_helpers.FitUnsafeText( wkl.label_docstring, 40), idx=len(result))) return result
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 mr.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) with work_env.WorkEnv(mr, self.services) as we: userprefs = we.GetUserPrefs(mr.auth.user_id) code_font = any( pref for pref in userprefs.prefs if pref.name == 'code_font' and pref.value == 'true') template = self._GetTemplate(mr.cnxn, config, mr.template_name, is_member) if template.summary: initial_summary = template.summary initial_summary_must_be_edited = template.summary_must_be_edited else: initial_summary = PLACEHOLDER_SUMMARY initial_summary_must_be_edited = True if template.status: initial_status = template.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 template.component_ids: component_paths.append( tracker_bizobj.FindComponentDefByID(component_id, config).path) initial_components = ', '.join(component_paths) if template.owner_id: initial_owner = framework_views.MakeUserView( mr.cnxn, self.services.user, template.owner_id) elif template.owner_defaults_to_member and page_perms.EditIssue: initial_owner = mr.auth.user_view else: initial_owner = None if initial_owner: initial_owner_name = initial_owner.email owner_avail_state = initial_owner.avail_state owner_avail_message_short = initial_owner.avail_message_short else: initial_owner_name = '' owner_avail_state = None owner_avail_message_short = None # 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, template) # If the user followed a link that specified the template name, make sure # that it is also in the menu as the current choice. # TODO(jeffcarp): Unit test this. config_view.template_view.can_view = ezt.boolean(True) # TODO(jeffcarp): Unit test this. offer_templates = len(config_view.template_names) > 1 restrict_to_known = config.restrict_to_known enum_field_name_set = { fd.field_name.lower() for fd in config.field_defs if fd.field_type is tracker_pb2.FieldTypes.ENUM_TYPE and not fd.is_deleted } # TODO(jrobbins): restrictions link_or_template_labels = mr.GetListParam('labels', template.labels) labels = [ lab for lab in link_or_template_labels if not tracker_bizobj.LabelIsMaskedByField(lab, enum_field_name_set) ] # Corp-mode users automatically add R-V-G. with work_env.WorkEnv(mr, self.services) as we: userprefs = we.GetUserPrefs(mr.auth.user_id) corp_mode = any( up.name == 'restrict_new_issues' and up.value == 'true' for up in userprefs.prefs) if corp_mode: if not any(lab.lower().startswith('restrict-view-') for lab in labels): labels.append(CORP_RESTRICTION_LABEL) field_user_views = tracker_views.MakeFieldUserViews( mr.cnxn, template, self.services.user) approval_ids = [av.approval_id for av in template.approval_values] field_views = tracker_views.MakeAllFieldValueViews( config, link_or_template_labels, [], template.field_values, field_user_views, parent_approval_ids=approval_ids, phases=template.phases) # TODO(jojwang): monorail:6305, remove this hack when Edit perms for field # values are implemented. field_views = [ view for view in field_views if view.field_name.lower() not in RESTRICTED_FLT_FIELDS ] # TODO(jrobbins): remove "or []" after next release. (prechecked_approvals, required_approval_ids, phases) = issue_tmpl_helpers.GatherApprovalsPageData( template.approval_values or [], template.phases, config) approvals = [ view for view in field_views if view.field_id in approval_ids ] 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': template.content, 'template_name': template.name, 'component_required': ezt.boolean(template.component_required), 'initial_status': initial_status, 'initial_owner': initial_owner_name, 'owner_avail_state': owner_avail_state, 'owner_avail_message_short': owner_avail_message_short, 'initial_components': initial_components, 'initial_cc': '', 'initial_blocked_on': '', 'initial_blocking': '', 'initial_hotlists': '', '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), 'is_member': ezt.boolean(is_member), 'code_font': ezt.boolean(code_font), # The following are necessary for displaying phases that come with # this template. These are read-only. 'allow_edit': ezt.boolean(False), 'initial_phases': phases, 'approvals': approvals, 'prechecked_approvals': prechecked_approvals, 'required_approval_ids': required_approval_ids, # See monorail:4692 and the use of PHASES_WITH_MILESTONES # in elements/flt/mr-launch-overview/mr-phase.js 'issue_phase_names': list({ phase.name.lower() for phase in phases if phase.name in PHASES_WITH_MILESTONES }), } return page_data
def __init__(self, mr, template, user_service, config): super(IssueTemplateView, self).__init__(template) self.ownername = '' try: self.owner_view = framework_views.MakeUserView( mr.cnxn, user_service, template.owner_id) except exceptions.NoSuchUserException: self.owner_view = None if self.owner_view: self.ownername = self.owner_view.email self.admin_views = list( framework_views.MakeAllUserViews(mr.cnxn, user_service, template.admin_ids).values()) self.admin_names = ', '.join( sorted([admin_view.email 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( template.owner_defaults_to_member) self.component_required = ezt.boolean(template.component_required) component_paths = [] for component_id in template.component_ids: component_paths.append( 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 fd.field_type is tracker_pb2.FieldTypes.ENUM_TYPE and 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: self.field_values.append( template_helpers.EZTItem(field_id=fv.field_id, val=tracker_bizobj.GetFieldValue( fv, field_user_views), idx=len(self.field_values))) self.complete_field_values = MakeAllFieldValueViews( config, template.labels, [], template.field_values, field_user_views) # 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 else: field_value_view.val = None
def ProcessSubtabForm(self, post_data, mr): """Process changes to labels and custom field definitions. Args: post_data: HTML form data for the HTTP request being processed. mr: commonly used info parsed from the request. Returns: The URL of the page to show after processing. """ if not self.CheckPerm(mr, permissions.EDIT_PROJECT): raise permissions.PermissionException( 'Only project owners may edit the label definitions') wkl_text = post_data.get('predefinedlabels', '') wkl_matches = framework_constants.IDENTIFIER_DOCSTRING_RE.findall( wkl_text) wkl_tuples = [(label.lstrip('#'), docstring.strip(), label.startswith('#')) for label, docstring in wkl_matches] if not wkl_tuples: mr.errors.label_defs = 'A project cannot have zero labels' label_counter = collections.Counter(wkl[0].lower() for wkl in wkl_tuples) for lab, count in label_counter.items(): if count > 1: mr.errors.label_defs = 'Duplicate label: %s' % lab config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id) field_names = [ fd.field_name for fd in config.field_defs if fd.field_type is tracker_pb2.FieldTypes.ENUM_TYPE and not fd.is_deleted ] masked_labels = tracker_helpers.LabelsMaskedByFields( config, field_names) field_names_lower = [field_name.lower() for field_name in field_names] for wkl in wkl_tuples: conflict = tracker_bizobj.LabelIsMaskedByField( wkl[0], field_names_lower) if conflict: mr.errors.label_defs = ( 'Label "%s" should be defined in enum "%s"' % (wkl[0], conflict)) wkl_tuples.extend([(masked.name, masked.docstring, False) for masked in masked_labels]) excl_prefix_text = post_data.get('excl_prefixes', '') excl_prefixes = framework_constants.IDENTIFIER_RE.findall( excl_prefix_text) if mr.errors.AnyErrors(): self.PleaseCorrect(mr, labels_text=wkl_text) return self.services.config.UpdateConfig(mr.cnxn, mr.project, well_known_labels=wkl_tuples, excl_label_prefixes=excl_prefixes) # TODO(jrobbins): define a "strict" mode that affects only labels. return urls.ADMIN_LABELS
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. Args: 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 perms. """ 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) self.cc = [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_helpers.MeansOpenInProject( 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 = [ MakeFieldValueView( fd, config, issue.labels, issue.derived_labels, issue.field_values, users_by_id) # 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) else: 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] all_blocked_on_refs.extend([ (r.project, r.issue_id) for r in issue.dangling_blocked_on_refs]) self.blocked_on_str = ', '.join( tracker_bizobj.FormatIssueRef( 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] all_blocking_refs.extend([ (r.project, r.issue_id) for r in issue.dangling_blocking_refs]) self.blocking_str = ', '.join( tracker_bizobj.FormatIssueRef( 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 else: merged_into_ref = None self.merged_into_str = tracker_bizobj.FormatIssueRef( merged_into_ref, default_project_name=issue.project_name) self.blocked_on = [] self.has_dangling = ezt.boolean(self.dangling_blocked_on_refs) 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] self.blocked_on.extend( [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] self.blocking.extend( [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.multiple_blocked_on = ezt.boolean(len(self.blocked_on) >= 2) self.detail_relative_url = tracker_helpers.FormatRelativeIssueURL( issue.project_name, urls.ISSUE_DETAIL, id=issue.local_id)
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) elif wkp.owner_defaults_to_member and page_perms.EditIssue: initial_owner = mr.auth.user_view else: initial_owner = None if initial_owner: initial_owner_name = initial_owner.email owner_avail_state = initial_owner.avail_state owner_avail_message_short = initial_owner.avail_message_short else: initial_owner_name = '' owner_avail_state = None owner_avail_message_short = None # 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, 'owner_avail_state': owner_avail_state, 'owner_avail_message_short': owner_avail_message_short, '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