launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #03897
[Merge] lp:~danilo/launchpad/bug-772754-other-subscribers-lp-names into lp:launchpad
Данило Шеган has proposed merging lp:~danilo/launchpad/bug-772754-other-subscribers-lp-names into lp:launchpad with lp:~danilo/launchpad/bug-772754-other-subscribers-sections as a prerequisite.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #772754 in Launchpad itself: "After better-bug-notification changes, list of bug subscribers is confusing"
https://bugs.launchpad.net/launchpad/+bug/772754
For more details, see:
https://code.launchpad.net/~danilo/launchpad/bug-772754-other-subscribers-lp-names/+merge/64178
= Bug 772754: Other subscribers list, part 2 =
This branch provides some helper functions that are used in bug 772754
other subscribers implementation, but that are generally useful as well:
functions to convert from Launchpad valid_name to CSS class name and back.
I plan to announce on-list once they land so people can consider using
them in the future instead of always coming up with their own variants
which might be less complete and non-bijective.
== Tests ==
lib/lp/app/javascript/tests/test_lp_names.html
== Demo and Q/A ==
N/A
= Launchpad lint =
Checking for conflicts and issues in changed files.
Linting changed files:
lib/lp/app/javascript/lp-names.js
lib/lp/app/javascript/tests/test_lp_names.html
lib/lp/app/javascript/tests/test_lp_names.js
--
https://code.launchpad.net/~danilo/launchpad/bug-772754-other-subscribers-lp-names/+merge/64178
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~danilo/launchpad/bug-772754-other-subscribers-lp-names into lp:launchpad.
=== added file 'lib/lp/app/javascript/lp-names.js'
--- lib/lp/app/javascript/lp-names.js 1970-01-01 00:00:00 +0000
+++ lib/lp/app/javascript/lp-names.js 2011-06-10 13:18:29 +0000
@@ -0,0 +1,122 @@
+/* Copyright 2011 Canonical Ltd. This software is licensed under the
+ * GNU Affero General Public License version 3 (see the file LICENSE).
+ *
+ * Utility functions for converting valid Launchpad names for use in
+ * CSS classes and back.
+ *
+ * Launchpad account names match the pattern [a-z0-9][a-z0-9+.-]*.
+ * CSS class names roughly match the pattern -?[_a-z][_a-z0-9-]*.
+ *
+ * These method uses the fact that CSS allows '_' anywhere whereas LP
+ * does not use it to escape starting digits and '+' or '.'.
+ *
+ * When no exceptions are thrown,
+ * css_to_launchpad(launchpad_to_css(string))
+ * is idempotent.
+ *
+ * See `lp.app.validators.name.valid_name_pattern` and
+ * http://www.w3.org/TR/CSS21/grammar.html#scanner
+ *
+ * @module lp
+ * @submodule names
+ */
+
+YUI.add('lp.names', function(Y) {
+
+var namespace = Y.namespace('lp.names');
+
+/**
+ * Gets a name suitable to be used as a CSS class for the "valid"
+ * Launchpad name.
+ *
+ * Throws an exception if the `name` is not a valid Launchpad name.
+ *
+ * This is a bijective function with the inverse provided by
+ * css_to_launchpad().
+ *
+ * @method launchpad_to_css
+ * @param name {String} A valid Launchpad name (eg. a person or project name).
+ * @return {String} A converted `name` usable in a CSS class directly.
+ */
+function launchpad_to_css(name) {
+
+ // Ensure we're being asked to convert the valid LP name.
+ if (!name.match(/^[a-z0-9][a-z0-9\+\.\-]*$/)) {
+ Y.error(
+ 'Passed value "' + name + '" is not a valid Launchpad name.');
+ return;
+ }
+
+ if (name.match(/^[a-z][a-z0-9\-]*$/)) {
+ // This is an intersection between valid LP and CSS names.
+ return name;
+ } else {
+ // Do the conversion.
+ var first_char = name.charAt(0);
+ if (first_char >= '0' && first_char <= '9') {
+ name = '_' + name;
+ }
+
+ // In the rest of the string, we convert all "+"s with "_y" and all
+ // "."s with "_z".
+ name = name.replace(/\+/g, '_y');
+ name = name.replace(/\./g, '_z');
+ }
+ return name;
+}
+namespace.launchpad_to_css = launchpad_to_css;
+
+/**
+ * Convert the CSS name as gotten by launchpad_to_css to
+ * it's originating Launchpad name.
+ *
+ * Throws an exception if the `name` is not a valid CSS class name
+ * and in the format as produced by launchpad_to_css.
+ * WARNING: this won't produce a valid Launchpad name for arbitrary
+ * CSS class names.
+ *
+ * This is an inverse function of the function
+ * launchpad_to_css().
+ *
+ * @method css_to_launchpad
+ * @param name {String} A valid CSS class name, but usually the result of
+ * launchpad_to_css() call.
+ * @return {String} A converted `name` that is identical to the originating
+ * Launchpad name passed into launchpad_to_css().
+ * In practice, starting '_a', '_b', ..., '_j' are replaced with
+ * '0', '1', ..., '9' and '_y' and '_z' are replaced with '+' and '.'
+ * throughout the string.
+ */
+function css_to_launchpad(name) {
+ if (!name.match(/^-?[_a-z][_a-z0-9\-]*$/)) {
+ Y.error(
+ 'Passed value "' + name + '" is not a valid CSS class name.');
+ }
+ if (!name.match(/^((_[0-9yz])|[a-z])([a-z0-9\-]|(_[yz]))*$/)) {
+ Y.error(
+ 'Passed value "' + name +
+ '" is not produced by launchpad_to_css.');
+ }
+
+ if (name.match(/^[a-z][a-z0-9\-]*$/)) {
+ // This is an intersection between valid LP and CSS names.
+ return name;
+ }
+
+ if (name.charAt(0) === '_') {
+ // It may start with a digit (iow, '_0' to '_9' for digits,
+ // or '_y', '_z' for '+', '.' but we don't care about these [yet]).
+ var second_char = name.charAt(1);
+ if (second_char >= '0' && second_char <= '9') {
+ name = name.substr(1);
+ }
+ }
+ // Replace escaped variants of '+' and '.' back.
+ name = name.replace(/_y/g, '+');
+ name = name.replace(/_z/g, '.');
+
+ return name;
+}
+namespace.css_to_launchpad = css_to_launchpad;
+
+}, "0.1", {});
=== added file 'lib/lp/app/javascript/tests/test_lp_names.html'
--- lib/lp/app/javascript/tests/test_lp_names.html 1970-01-01 00:00:00 +0000
+++ lib/lp/app/javascript/tests/test_lp_names.html 2011-06-10 13:18:29 +0000
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+ <head>
+ <title>Launchpad Name Conversion</title>
+
+ <!-- YUI 3.0 Setup -->
+ <script type="text/javascript" src="../../../../canonical/launchpad/icing/yui/yui/yui.js"></script>
+ <script type="text/javascript" src="../../../../canonical/launchpad/icing/lazr/build/lazr.js"></script>
+ <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssreset/reset.css"/>
+ <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssfonts/fonts.css"/>
+ <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssbase/base.css"/>
+ <link rel="stylesheet" href="../../../../canonical/launchpad/javascript/test.css" />
+
+ <!-- The module under test -->
+ <script type="text/javascript" src="../lp-names.js"></script>
+
+ <!-- The test suite -->
+ <script type="text/javascript" src="test_lp_names.js"></script>
+</head>
+<body class="yui3-skin-sam">
+ <div id="container-of-stuff">
+ </div>
+</body>
+</html>
=== added file 'lib/lp/app/javascript/tests/test_lp_names.js'
--- lib/lp/app/javascript/tests/test_lp_names.js 1970-01-01 00:00:00 +0000
+++ lib/lp/app/javascript/tests/test_lp_names.js 2011-06-10 13:18:29 +0000
@@ -0,0 +1,252 @@
+YUI({
+ base: '../../../../canonical/launchpad/icing/yui/',
+ filter: 'raw', combine: false, fetchCSS: false
+}).use('test', 'console', 'lp.names',
+ function(Y) {
+
+var suite = new Y.Test.Suite("lp.names Tests");
+var names = Y.lp.names;
+
+/**
+ * Test conversion of Launchpad names to CSS classes.
+ */
+suite.add(new Y.Test.Case({
+ name: 'lp.names.launchpad_to_css() test',
+
+ _should: {
+ error: {
+ test_not_lp_name_error:
+ new Error('Passed value "~" is not a valid Launchpad name.'),
+ test_start_with_plus_error:
+ new Error('Passed value "+name" is not a valid Launchpad name.'),
+ test_start_with_dot_error:
+ new Error('Passed value ".name" is not a valid Launchpad name.'),
+ test_start_with_dash_error:
+ new Error('Passed value "-name" is not a valid Launchpad name.')
+ }
+ },
+
+ test_not_lp_name_error: function() {
+ // Anything but a-z0-9+-. is not allowed in a LP name.
+ // This triggers an exception.
+ names.launchpad_to_css('~');
+ },
+
+ test_start_with_plus_error: function() {
+ // Strings starting with plus character are
+ // invalid LP names and throw an exception.
+ names.launchpad_to_css('+name');
+ },
+
+ test_start_with_dot_error: function() {
+ // Strings starting with dot character are
+ // invalid LP names and throw an exception.
+ names.launchpad_to_css('.name');
+ },
+
+ test_start_with_dash_error: function() {
+ // Strings starting with dash character are
+ // invalid LP names and throw an exception.
+ names.launchpad_to_css('-name');
+ },
+
+ test_valid_in_both: function() {
+ // If a name is both a valid LP name and CSS class name,
+ // it is returned unmodified.
+ var name = 'name123-today';
+ var expected = name;
+ Y.Assert.areEqual(expected,
+ names.launchpad_to_css(name));
+ },
+
+ test_starts_with_digits: function() {
+ var name = '2name';
+ var expected = '_2name';
+ Y.Assert.areEqual(expected,
+ names.launchpad_to_css(name));
+ },
+
+ test_middle_digits: function() {
+ // Digits in the middle and end of string are not touched.
+ var name = 'na2me4';
+ var expected = name;
+ Y.Assert.areEqual(expected,
+ names.launchpad_to_css(name));
+ },
+
+ test_plus_sign: function() {
+ // Plus sign is allowed in the Launchpad name, but
+ // not in the CSS class name. It is replaced with '_y'.
+ var name = 'name+lastname';
+ var expected = 'name_ylastname';
+ Y.Assert.areEqual(expected,
+ names.launchpad_to_css(name));
+ },
+
+ test_multiple_pluses: function() {
+ // Even multiple plus characters are replaced with '_y'.
+ var name = 'name+middle+lastname';
+ var expected = 'name_ymiddle_ylastname';
+ Y.Assert.areEqual(expected,
+ names.launchpad_to_css(name));
+ },
+
+ test_dot_sign: function() {
+ // Dot sign is allowed in the Launchpad name, but
+ // not in the CSS class name. It is replaced with '_z'.
+ var name = 'name.lastname';
+ var expected = 'name_zlastname';
+ Y.Assert.areEqual(expected,
+ names.launchpad_to_css(name));
+ },
+
+ test_multiple_dots: function() {
+ // Even multiple dot characters are replaced with '_z'.
+ var name = 'name.middle.lastname';
+ var expected = 'name_zmiddle_zlastname';
+ Y.Assert.areEqual(expected,
+ names.launchpad_to_css(name));
+ }
+}));
+
+/**
+ * Test conversion of CSS class names as gotten by launchpad_to_css back
+ * to Launchpad names.
+ */
+suite.add(new Y.Test.Case({
+ name: 'lp.names.css_to_launchpad() test',
+
+ _should: {
+ error: {
+ test_not_css_class_error:
+ new Error('Passed value "+" is not a valid CSS class name.'),
+ test_start_with_digit_error:
+ new Error('Passed value "1name" is not a valid CSS class name.'),
+ test_non_lp_converted_name_error:
+ new Error(
+ 'Passed value "_name" is not produced by launchpad_to_css.'),
+ test_non_lp_converted_name_error2:
+ new Error(
+ 'Passed value "na_me" is not produced by launchpad_to_css.')
+ }
+ },
+
+ test_not_css_class_error: function() {
+ // Anything but a-z0-9_-. is not allowed in a LP name.
+ // This triggers an exception.
+ names.css_to_launchpad('+');
+ },
+
+ test_start_with_digit_error: function() {
+ // Strings starting with underscore are not valid CSS class names.
+ names.css_to_launchpad('1name');
+ },
+
+ test_non_lp_converted_name_error: function() {
+ // Strings which are otherwise valid CSS class names, but
+ // could not be the result of the launchpad_to_css conversion
+ // are rejected with an exception.
+ names.css_to_launchpad('_name');
+ },
+
+ test_non_lp_converted_name_error2: function() {
+ // Strings which are otherwise valid CSS class names, but
+ // could not be the result of the launchpad_to_css conversion
+ // are rejected with an exception.
+ names.css_to_launchpad('na_me');
+ },
+
+ test_valid_in_both: function() {
+ // If a name is both a valid LP name and CSS class name,
+ // it is returned unmodified.
+ var name = 'name123-today';
+ var expected = name;
+ Y.Assert.areEqual(expected,
+ names.css_to_launchpad(name));
+ },
+
+ test_starts_with_digits: function() {
+ var name = '_2name';
+ var expected = '2name';
+ Y.Assert.areEqual(expected,
+ names.css_to_launchpad(name));
+ },
+
+ test_middle_digits: function() {
+ // Digits in the middle and end of string are not touched.
+ var name = 'na2me4';
+ var expected = name;
+ Y.Assert.areEqual(expected,
+ names.css_to_launchpad(name));
+ },
+
+ test_plus_sign: function() {
+ // Plus sign is represented as '_y' in the CSS class name.
+ var name = 'name_ylastname';
+ var expected = 'name+lastname';
+ Y.Assert.areEqual(expected,
+ names.css_to_launchpad(name));
+ },
+
+ test_multiple_pluses: function() {
+ // Even multiple plus characters ('_y' strings) are handled.
+ var name = 'name_ymiddle_ylastname';
+ var expected = 'name+middle+lastname';
+ Y.Assert.areEqual(expected,
+ names.css_to_launchpad(name));
+ },
+
+ test_dot_sign: function() {
+ // Dot sign is represented as '_z' in the CSS class name.
+ var name = 'name_zlastname';
+ var expected = 'name.lastname';
+ Y.Assert.areEqual(expected,
+ names.css_to_launchpad(name));
+ },
+
+ test_multiple_dots: function() {
+ // Even multiple dot characters ('_z' strings) are handled.
+ var name = 'name_zmiddle_zlastname';
+ var expected = 'name.middle.lastname';
+ Y.Assert.areEqual(expected,
+ names.css_to_launchpad(name));
+ }
+}));
+
+/**
+ * Test idempotency of css_to_launchpad(launchpad_to_css()).
+ */
+suite.add(new Y.Test.Case({
+ name: 'Combined idempotency css_to_launchpad(launchpad_to_css()) test',
+
+ test_simple_name: function() {
+ var name = 'name';
+ Y.Assert.areEqual(
+ name,
+ names.css_to_launchpad(names.launchpad_to_css(name)));
+ },
+
+ test_complex_name: function() {
+ var name = '0+name.lastname-44-';
+ Y.Assert.areEqual(
+ name,
+ names.css_to_launchpad(names.launchpad_to_css(name)));
+ }
+}));
+
+
+var handle_complete = function(data) {
+ status_node = Y.Node.create(
+ '<p id="complete">Test status: complete</p>');
+ Y.one('body').appendChild(status_node);
+ };
+Y.Test.Runner.on('complete', handle_complete);
+Y.Test.Runner.add(suite);
+
+var console = new Y.Console({newestOnTop: false});
+console.render('#log');
+
+Y.on('domready', function() {
+ Y.Test.Runner.run();
+});
+});