← Back to team overview

launchpad-reviewers team mailing list archive

[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();
+});
+});