def _renderSharing(self, template, packaging, productseries, upstream, other_template, sourcepackagename): """Render a link to `template`. :param template: The target `POTemplate`. :return: HTML for the "sharing" status of `template`. """ # Testing is easier if we are willing to extract the sourcepackagename # from the template. if sourcepackagename is None: sourcepackagename = template.sourcepackagename # Build the edit link. escaped_source = html_escape(sourcepackagename.name) source_url = '+source/%s' % escaped_source details_url = source_url + '/+sharing-details' edit_link = ('<a class="sprite edit action-icon" href="%s">Edit</a>' % details_url) # If all the conditions are met for sharing... if packaging and upstream and other_template is not None: escaped_series = html_escape(productseries.name) escaped_template = html_escape(template.name) pot_url = ('/%s/%s/+pots/%s' % (escaped_source, escaped_series, escaped_template)) return (edit_link + '<a href="%s">%s/%s</a>' % (pot_url, escaped_source, escaped_series)) else: # Otherwise just say that the template isn't shared and give them # a link to change the sharing. return edit_link + 'not shared'
def _renderSharing(self, template, packaging, productseries, upstream, other_template, sourcepackagename): """Render a link to `template`. :param template: The target `POTemplate`. :return: HTML for the "sharing" status of `template`. """ # Testing is easier if we are willing to extract the sourcepackagename # from the template. if sourcepackagename is None: sourcepackagename = template.sourcepackagename # Build the edit link. escaped_source = html_escape(sourcepackagename.name) source_url = '+source/%s' % escaped_source details_url = source_url + '/+sharing-details' edit_link = ( '<a class="sprite edit action-icon" href="%s">Edit</a>' % details_url) # If all the conditions are met for sharing... if packaging and upstream and other_template is not None: escaped_series = html_escape(productseries.name) escaped_template = html_escape(template.name) pot_url = ('/%s/%s/+pots/%s' % (escaped_source, escaped_series, escaped_template)) return (edit_link + '<a href="%s">%s/%s</a>' % (pot_url, escaped_source, escaped_series)) else: # Otherwise just say that the template isn't shared and give them # a link to change the sharing. return edit_link + 'not shared'
def toTerm(self, watch): if watch.url.startswith('mailto:'): user = getUtility(ILaunchBag).user if user is None: title = html_escape( FormattersAPI(watch.bugtracker.title).obfuscate_email()) else: url = watch.url if url in watch.bugtracker.title: title = html_escape(watch.bugtracker.title).replace( html_escape(url), structured( '<a href="%s">%s</a>', url, url).escapedtext) else: title = structured( '%s <<a href="%s">%s</a>>', watch.bugtracker.title, url, url[7:]).escapedtext else: title = structured( '%s <a href="%s">#%s</a>', watch.bugtracker.title, watch.url, watch.remotebug).escapedtext # title is already HTML-escaped. return SimpleTerm(watch, watch.id, title)
def _validate(self, value): # import here to avoid circular import from lp.services.webapp import canonical_url from lazr.uri import URI super(DistroMirrorURIField, self)._validate(value) uri = URI(self.normalize(value)) # This field is also used when creating new mirrors and in that case # self.context is not an IDistributionMirror so it doesn't make sense # to try to get the existing value of the attribute. if IDistributionMirror.providedBy(self.context): orig_value = self.get(self.context) if orig_value is not None and URI(orig_value) == uri: return # url was not changed mirror = self.getMirrorByURI(str(uri)) if mirror is not None: message = _( 'The distribution mirror <a href="${url}">${mirror}</a> ' 'is already registered with this URL.', mapping={ 'url': html_escape(canonical_url(mirror)), 'mirror': html_escape(mirror.title) }) raise LaunchpadValidationError(structured(message))
def test_composeNameAndChangesLink_escapes_nonlinked_display_name(self): filename = 'name"&name' upload = self.factory.makeCustomPackageUpload(filename=filename) # Stop nameAndChangesLink from producing a link. upload.changesfile = None complete_upload = self.makeCompletePackageUpload(upload) self.assertIn( html_escape(filename), html_escape(complete_upload.composeNameAndChangesLink()))
def _check_email_availability(email): email_address = getUtility(IEmailAddressSet).getByEmail(email) if email_address is not None: person = email_address.person message = _('${email} is already registered in Launchpad and is ' 'associated with <a href="${url}">${person}</a>.', mapping={'email': html_escape(email), 'url': html_escape(canonical_url(person)), 'person': html_escape(person.displayname)}) raise LaunchpadValidationError(structured(message))
def _renderItem(self, index, text, value, name, cssClass, checked=False): """Render a checkbox and text without without label.""" kw = {} if checked: kw['checked'] = 'checked' value = html_escape(value) text = html_escape(text) id = '%s.%s' % (name, index) element = renderElement( u'input', value=value, name=name, id=id, cssClass=cssClass, type='checkbox', **kw) return self._joinButtonToMessageTemplate % (element, text)
def test_attachments(self): # The attachment comment has no content and it does not notify. token = self.process_extra_data("""\ MIME-Version: 1.0 Content-type: multipart/mixed; boundary=boundary --boundary Content-disposition: attachment; filename='attachment1' Content-type: text/plain; charset=utf-8 This is an attachment. --boundary Content-disposition: attachment; filename='attachment2' Content-description: Attachment description. Content-type: text/plain; charset=ISO-8859-1 This is another attachment, with a description. --boundary-- """) view = self.create_initialized_view() view.publishTraverse(view.request, token) with EventRecorder() as recorder: view.submit_bug_action.success(self.get_form()) # Subscribers are only notified about the new bug event; # The extra comment for the attchments was silent. self.assertEqual(1, len(recorder.events)) self.assertEqual(view.added_bug, recorder.events[0].object) transaction.commit() bug = view.added_bug attachments = [at for at in bug.attachments_unpopulated] self.assertEqual(2, len(attachments)) attachment = attachments[0] self.assertEqual('attachment1', attachment.title) self.assertEqual('attachment1', attachment.libraryfile.filename) self.assertEqual('text/plain; charset=utf-8', attachment.libraryfile.mimetype) self.assertEqual('This is an attachment.\n\n', attachment.libraryfile.read()) self.assertEqual(2, bug.messages.count()) self.assertEqual(2, len(bug.messages[1].bugattachments)) notifications = [ no.message for no in view.request.response.notifications ] self.assertContentEqual([ '<p class="last">Thank you for your bug report.</p>', html_escape( 'The file "attachment1" was attached to the bug report.'), html_escape( 'The file "attachment2" was attached to the bug report.') ], notifications)
def _check_email_availability(email): email_address = getUtility(IEmailAddressSet).getByEmail(email) if email_address is not None: person = email_address.person message = _( '${email} is already registered in Launchpad and is ' 'associated with <a href="${url}">${person}</a>.', mapping={ 'email': html_escape(email), 'url': html_escape(canonical_url(person)), 'person': html_escape(person.displayname) }) raise LaunchpadValidationError(structured(message))
def test_attachments(self): # The attachment comment has no content and it does not notify. token = self.process_extra_data("""\ MIME-Version: 1.0 Content-type: multipart/mixed; boundary=boundary --boundary Content-disposition: attachment; filename='attachment1' Content-type: text/plain; charset=utf-8 This is an attachment. --boundary Content-disposition: attachment; filename='attachment2' Content-description: Attachment description. Content-type: text/plain; charset=ISO-8859-1 This is another attachment, with a description. --boundary-- """) view = self.create_initialized_view() view.publishTraverse(view.request, token) with EventRecorder() as recorder: view.submit_bug_action.success(self.get_form()) # Subscribers are only notified about the new bug event; # The extra comment for the attchments was silent. self.assertEqual(1, len(recorder.events)) self.assertEqual(view.added_bug, recorder.events[0].object) transaction.commit() bug = view.added_bug attachments = [at for at in bug.attachments_unpopulated] self.assertEqual(2, len(attachments)) attachment = attachments[0] self.assertEqual('attachment1', attachment.title) self.assertEqual('attachment1', attachment.libraryfile.filename) self.assertEqual( 'text/plain; charset=utf-8', attachment.libraryfile.mimetype) self.assertEqual( 'This is an attachment.\n\n', attachment.libraryfile.read()) self.assertEqual(2, bug.messages.count()) self.assertEqual(2, len(bug.messages[1].bugattachments)) notifications = [ no.message for no in view.request.response.notifications] self.assertContentEqual( ['<p class="last">Thank you for your bug report.</p>', html_escape( 'The file "attachment1" was attached to the bug report.'), html_escape( 'The file "attachment2" was attached to the bug report.')], notifications)
def text_to_html(text, flags, space=TranslationConstants.SPACE_CHAR, newline=TranslationConstants.NEWLINE_CHAR): """Convert a unicode text to a HTML representation.""" if text is None: return None markup_lines = [] # Replace leading and trailing spaces on each line with special markup. if u'\r\n' in text: newline_chars = u'\r\n' elif u'\r' in text: newline_chars = u'\r' else: newline_chars = u'\n' for line in text.split(newline_chars): # Pattern: # - group 1: zero or more spaces: leading whitespace # - group 2: zero or more groups of (zero or # more spaces followed by one or more non-spaces): maximal string # which doesn't begin or end with whitespace # - group 3: zero or more spaces: trailing whitespace match = re.match(u'^( *)((?: *[^ ]+)*)( *)$', line) if match: format_segments = None if 'c-format' in flags: try: format_segments = parse_cformat_string(match.group(2)) except UnrecognisedCFormatString: pass if format_segments is not None: markup = '' for segment in format_segments: type, content = segment if type == 'interpolation': markup += (u'<code>%s</code>' % html_escape(content)) elif type == 'string': markup += html_escape(content) else: markup = html_escape(match.group(2)) markup_lines.append(space * len(match.group(1)) + markup + space * len(match.group(3))) else: raise AssertionError( "A regular expression that should always match didn't.") return expand_rosetta_escapes(newline.join(markup_lines))
def _renderSuggestionLabel(self, branch, index): """Render a label for the option based on a branch.""" # To aid usability there needs to be some text connected with the # radio buttons that is not a hyperlink in order to select the radio # button. It was decided not to have the entire text as a link, but # instead to have a separate link to the branch details. text = '%s (<a href="%s">branch details</a>)' % (html_escape( branch.displayname), html_escape(canonical_url(branch))) # If the branch is the development focus, say so. if branch == self.context.context.target.default_merge_target: text = text + "– <em>development focus</em>" label = u'<label for="%s" style="font-weight: normal">%s</label>' % ( self._optionId(index), text) return structured(label)
def _renderSuggestionLabel(self, branch, index): """Render a label for the option based on a branch.""" # To aid usability there needs to be some text connected with the # radio buttons that is not a hyperlink in order to select the radio # button. It was decided not to have the entire text as a link, but # instead to have a separate link to the branch details. text = '%s (<a href="%s">branch details</a>)' % ( html_escape(branch.displayname), html_escape(canonical_url(branch))) # If the branch is the development focus, say so. if branch == self.context.context.target.default_merge_target: text = text + "– <em>development focus</em>" label = u'<label for="%s" style="font-weight: normal">%s</label>' % ( self._optionId(index), text) return structured(label)
def test_existing_team_error(self): """ Do not allow a new team with the same name as an existing team. The ObjectReassignmentView displays radio buttons that give you the option to create a team as opposed to using an existing team. If the user tries to create a new team with the same name as an existing team, an error is displayed. """ a_team, b_team, c_team, owner = self._makeTeams() view = create_initialized_view( c_team, '+reassign', principal=owner) self.assertEqual( ['field.owner', 'field.existing'], list(w.name for w in view.widgets)) form = { 'field.owner': 'a-team', 'field.existing': 'new', 'field.actions.change': 'Change', } view = create_initialized_view( a_team, '+reassign', form=form, principal=owner) self.assertEqual( [html_escape( u"There's already a person/team with the name 'a-team' in " "Launchpad. Please choose a different name or select the " "option to make that person/team the new owner, if that's " "what you want.")], view.errors)
def test_too_short_branch_name(self): # error notification if the thing following +branch is a unique name # that's too short to be a real unique name. owner = self.factory.makePerson() requiredMessage = html_escape( u"Cannot understand namespace name: '%s'" % owner.name) self.assertDisplaysError('~%s' % owner.name, requiredMessage)
def test_composeIcon_title_defaults_to_alt_text(self): alt = self.factory.getUniqueString() icon = self.factory.getUniqueString() + ".png" html_text = html_escape( self.makeCompletePackageUpload().composeIcon(alt, icon)) img = html.fromstring(html_text) self.assertEqual(alt, img.get("title"))
def test_existing_team_error(self): """ Do not allow a new team with the same name as an existing team. The ObjectReassignmentView displays radio buttons that give you the option to create a team as opposed to using an existing team. If the user tries to create a new team with the same name as an existing team, an error is displayed. """ a_team, b_team, c_team, owner = self._makeTeams() view = create_initialized_view(c_team, '+reassign', principal=owner) self.assertEqual(['field.owner', 'field.existing'], list(w.name for w in view.widgets)) form = { 'field.owner': 'a-team', 'field.existing': 'new', 'field.actions.change': 'Change', } view = create_initialized_view(a_team, '+reassign', form=form, principal=owner) self.assertEqual([ html_escape( u"There's already a person/team with the name 'a-team' in " "Launchpad. Please choose a different name or select the " "option to make that person/team the new owner, if that's " "what you want.") ], view.errors)
def _renderItem(self, index, text, value, name, cssClass, checked=False): """Render a checkbox and text without without label.""" kw = {} if checked: kw['checked'] = 'checked' value = html_escape(value) text = html_escape(text) id = '%s.%s' % (name, index) element = renderElement(u'input', value=value, name=name, id=id, cssClass=cssClass, type='checkbox', **kw) return self._joinButtonToMessageTemplate % (element, text)
def linkify_changelog(user, changelog, preloaded_person_data=None): """Linkify the changelog. This obfuscates email addresses to anonymous users, linkifies them for non-anonymous and links to the bug page for any bug numbers mentioned. """ if changelog is None: return '' # Remove any email addresses if the user is not logged in. changelog = obfuscate_email(user, changelog) # CGI Escape the changelog here before further replacements # insert HTML. Email obfuscation does not insert HTML but can insert # characters that must be escaped. changelog = html_escape(changelog) # Any email addresses remaining in the changelog were not obfuscated, # so we linkify them here. changelog = linkify_email(changelog, preloaded_person_data) # Ensure any bug numbers are linkified to the bug page. changelog = linkify_bug_numbers(changelog) return changelog
def snippet(self): """Convert a widget input error to an html snippet If the error implements provides a snippet() method, just return it. Otherwise return the error message. >>> from zope.formlib.interfaces import WidgetInputError >>> from lp.services.webapp.escaping import structured >>> bold_error = LaunchpadValidationError(structured("<b>Foo</b>")) >>> err = WidgetInputError("foo", "Foo", bold_error) >>> view = WidgetInputErrorView(err, None) >>> view.snippet() u'<b>Foo</b>' >>> class TooSmallError(object): ... def doc(self): ... return "Foo input < 1" >>> err = WidgetInputError("foo", "Foo", TooSmallError()) >>> view = WidgetInputErrorView(err, None) >>> view.snippet() u'Foo input < 1' """ if (hasattr(self.context, 'errors') and ILaunchpadValidationError.providedBy(self.context.errors)): return self.context.errors.snippet() return html_escape(self.context.doc())
def channels_validator(channels): """Return True if the channels in a list are valid, or raise a LaunchpadValidationError. """ tracks = set() branches = set() for name in channels: try: track, risk, branch = split_channel_name(name) except ValueError: message = _( "Invalid channel name '${name}'. Channel names must be of the " "form 'track/risk/branch', 'track/risk', 'risk/branch', or " "'risk'.", mapping={'name': html_escape(name)}) raise LaunchpadValidationError(structured(message)) tracks.add(track) branches.add(branch) # XXX cjwatson 2018-05-08: These are slightly arbitrary restrictions, # but they make the UI much simpler. if len(tracks) != 1: message = _("Channels must belong to the same track.") raise LaunchpadValidationError(structured(message)) if len(branches) != 1: message = _("Channels must belong to the same branch.") raise LaunchpadValidationError(structured(message)) return True
def test_no_such_unique_name(self): # Traversing to /+branch/<unique_name> where 'unique_name' is for a # branch that doesn't exist will display an error message. branch = self.factory.makeAnyBranch() bad_name = branch.unique_name + 'wibble' required_message = html_escape( "No such branch: '%s'." % (branch.name + "wibble")) self.assertDisplaysError(bad_name, required_message)
def test_cannot_merge_person_with_itself(self): # A IPerson cannot be merged with itself. login_person(self.target) form = self.getForm(dupe_name=self.target.name) view = create_initialized_view( self.person_set, '+requestmerge', form=form) self.assertEqual( [html_escape("You can't merge target into itself.")], view.errors)
def test_no_such_unique_name(self): # Traversing to /+branch/<unique_name> where 'unique_name' is for a # branch that doesn't exist will display an error message. branch = self.factory.makeAnyBranch() bad_name = branch.unique_name + 'wibble' required_message = html_escape("No such branch: '%s'." % (branch.name + "wibble")) self.assertDisplaysError(bad_name, required_message)
def _renderItem(self, index, text, value, name, cssClass, checked=False): """Render a checkbox and text in a label with a style attribute.""" kw = {} if checked: kw['checked'] = 'checked' value = html_escape(value) text = html_escape(text) id = '%s.%s' % (name, index) elem = renderElement(u'input', value=value, name=name, id=id, cssClass=cssClass, type='checkbox', **kw) option_id = '%s.%s' % (self.name, index) return self._joinButtonToMessageTemplate % (option_id, elem, text)
def test_composeNameAndChangesLink_escapes_name_in_link(self): filename = 'name"&name' upload = self.factory.makeCustomPackageUpload(filename=filename) complete_upload = self.makeCompletePackageUpload(upload) link = html.fromstring( html_escape(complete_upload.composeNameAndChangesLink())) self.assertIn(filename, link.get("title")) self.assertEqual(filename, link.text)
def test_composeNameAndChangesLink_links_to_changes_file(self): complete_upload = self.makeCompletePackageUpload() link = html.fromstring( html_escape(complete_upload.composeNameAndChangesLink())) self.assertEqual( ProxiedLibraryFileAlias( complete_upload.changesfile, complete_upload.context).http_url, link.get("href"))
def test_private_branch(self): # If an attempt is made to access a private branch, display an error. branch = self.factory.makeProductBranch( information_type=InformationType.USERDATA) branch_unique_name = removeSecurityProxy(branch).unique_name login(ANONYMOUS) required_message = html_escape( "No such branch: '%s'." % branch_unique_name) self.assertDisplaysError(branch_unique_name, required_message)
def renderExtraHint(self): extra_hint_html = '' extra_hint_class = '' if self.extra_hint_class: extra_hint_class = ' class="%s"' % self.extra_hint_class if self.extra_hint: extra_hint_html = ('<div%s>%s</div>' % (extra_hint_class, html_escape(self.extra_hint))) return extra_hint_html
def test_cannot_merge_person_with_itself(self): # A IPerson cannot be merged with itself. login_person(self.target) form = self.getForm(dupe_name=self.target.name) view = create_initialized_view(self.person_set, '+requestmerge', form=form) self.assertEqual([html_escape("You can't merge target into itself.")], view.errors)
def test_private_branch(self): # If an attempt is made to access a private branch, display an error. branch = self.factory.makeProductBranch( information_type=InformationType.USERDATA) branch_unique_name = removeSecurityProxy(branch).unique_name login(ANONYMOUS) required_message = html_escape("No such branch: '%s'." % branch_unique_name) self.assertDisplaysError(branch_unique_name, required_message)
def test_composeIcon_escapes_alt_and_title(self): alt = 'alt"&' icon = self.factory.getUniqueString() + ".png" title = 'title"&' html_text = html_escape( self.makeCompletePackageUpload().composeIcon(alt, icon, title)) img = html.fromstring(html_text) self.assertEqual("[%s]" % alt, img.get("alt")) self.assertEqual(title, img.get("title"))
def test_error_on_duplicate_to_duplicate(self): # Test that a bug cannot be marked a duplicate of # a bug that is already itself a duplicate. msg = dedent(u""" Bug %s is already a duplicate of bug %s. You can only mark a bug report as duplicate of one that isn't a duplicate itself. """ % (self.dupe_bug.id, self.dupe_bug.duplicateof.id)) self.assertDuplicateError(self.possible_dupe, self.dupe_bug, html_escape(msg))
def test_getInputValue_package_invalid(self): # An error is raised when the package is not published in the distro. form = self.form form['field.target.package'] = 'non-existent' self.widget.request = LaunchpadTestRequest(form=form) message = ( "There is no package named 'non-existent' published in Fnord.") e = self.assertRaises(WidgetInputError, self.widget.getInputValue) self.assertEqual(LaunchpadValidationError(message), e.errors) self.assertEqual(html_escape(message), self.widget.error())
def test_add_empty_team_fail(self): empty_team = self.factory.makeTeam(owner=self.team.teamowner) self.team.teamowner.leave(empty_team) form = self.getForm(empty_team) view = create_initialized_view(self.team, "+addmember", form=form) self.assertEqual(1, len(view.errors)) self.assertEqual( html_escape( "You can't add a team that doesn't have any active members."), view.errors[0])
def __call__(self): time_zone = getUtility(ILaunchBag).time_zone if self._renderedValueSet(): value = self._data else: value = self.context.default if value == self.context.missing_value: return u"" value = value.astimezone(time_zone) return html_escape(value.strftime("%Y-%m-%d %H:%M:%S %Z"))
def addError(self, message): """Add a form wide error. The 'message' parameter is CGI-escaped in accordance with the `INotificationResponse.addNotification()` API. Please see it for details re: internationalized and markup text. """ cleanmsg = html_escape(message) self.form_wide_errors.append(cleanmsg) self.errors.append(cleanmsg)
def renderExtraHint(self): extra_hint_html = '' extra_hint_class = '' if self.extra_hint_class: extra_hint_class = ' class="%s"' % self.extra_hint_class if self.extra_hint: extra_hint_html = ( '<div%s>%s</div>' % (extra_hint_class, html_escape(self.extra_hint))) return extra_hint_html
def text_to_html(text, flags, space=TranslationConstants.SPACE_CHAR, newline=TranslationConstants.NEWLINE_CHAR): """Convert a unicode text to a HTML representation.""" if text is None: return None lines = [] # Replace leading and trailing spaces on each line with special markup. if u'\r\n' in text: newline_chars = u'\r\n' elif u'\r' in text: newline_chars = u'\r' else: newline_chars = u'\n' for line in html_escape(text).split(newline_chars): # Pattern: # - group 1: zero or more spaces: leading whitespace # - group 2: zero or more groups of (zero or # more spaces followed by one or more non-spaces): maximal string # which doesn't begin or end with whitespace # - group 3: zero or more spaces: trailing whitespace match = re.match(u'^( *)((?: *[^ ]+)*)( *)$', line) if match: lines.append( space * len(match.group(1)) + match.group(2) + space * len(match.group(3))) else: raise AssertionError( "A regular expression that should always match didn't.") if 'c-format' in flags: # Replace c-format sequences with marked-up versions. If there is a # problem parsing the c-format sequences on a particular line, that # line is left unformatted. for i in range(len(lines)): formatted_line = '' try: segments = parse_cformat_string(lines[i]) except UnrecognisedCFormatString: continue for segment in segments: type, content = segment if type == 'interpolation': formatted_line += (u'<code>%s</code>' % content) elif type == 'string': formatted_line += content lines[i] = formatted_line return expand_rosetta_escapes(newline.join(lines))
def renderItem(self, index, text, value, name, cssClass): """Render an item of the list.""" text = html_escape(text) id = '%s.%s' % (name, index) elem = renderElement(u'input', value=value, name=name, id=id, cssClass=cssClass, type='radio') return self._renderRow(text, value, id, elem)
def test_cannot_merge_person_with_ppa(self): # A person with a PPA cannot be merged. login_celebrity('admin') self.dupe_person.createPPA() view = self.getView() self.assertEqual( [html_escape( u"dupe-person has a PPA that must be deleted before it can " "be merged. It may take ten minutes to remove the deleted " "PPA's files.")], view.errors)
def validate_new_person_email(email): """Check that the given email is valid and not registered to another launchpad account. This validator is supposed to be used only when creating a new profile using the /people/+newperson page, as the message will say clearly to the user that the profile he's trying to create already exists, so there's no need to create another one. """ from lp.services.webapp.publisher import canonical_url from lp.registry.interfaces.person import IPersonSet _validate_email(email) owner = getUtility(IPersonSet).getByEmail(email) if owner is not None: message = _("The profile you're trying to create already exists: " '<a href="${url}">${owner}</a>.', mapping={'url': html_escape(canonical_url(owner)), 'owner': html_escape(owner.displayname)}) raise LaunchpadValidationError(structured(message)) return True
def _renderTemplateLink(self, template, url): """Render a link to `template`. :param template: The target `POTemplate`. :param url: The cached URL for `template`. :return: HTML for a link to `template`. """ text = '<a href="%s">%s</a>' % (url, html_escape(template.name)) if not template.iscurrent: text += ' (inactive)' return text
def test_error_on_duplicate_to_duplicate(self): # Test that a bug cannot be marked a duplicate of # a bug that is already itself a duplicate. msg = dedent(u""" Bug %s is already a duplicate of bug %s. You can only mark a bug report as duplicate of one that isn't a duplicate itself. """ % ( self.dupe_bug.id, self.dupe_bug.duplicateof.id)) self.assertDuplicateError( self.possible_dupe, self.dupe_bug, html_escape(msg))
def test_composeIcon_produces_image_tag(self): alt = self.factory.getUniqueString() icon = self.factory.getUniqueString() + ".png" title = self.factory.getUniqueString() html_text = html_escape( self.makeCompletePackageUpload().composeIcon(alt, icon, title)) img = html.fromstring(html_text) self.assertEqual("img", img.tag) self.assertEqual("[%s]" % alt, img.get("alt")) self.assertEqual("/@@/" + icon, img.get("src")) self.assertEqual(title, img.get("title"))