Hello,

please review attached patches.

The rpc module is now separated from display layer
and changing activity text while loading metadata.

https://fedorahosted.org/freeipa/ticket/6144

--
Pavel^3 Vomacka

From e8253e5327c008f291e1401c81f68f4f3b194ded Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Thu, 28 Jul 2016 15:29:23 +0200
Subject: [PATCH 1/2] Refactoring of rpc module

The rpc module is now separated from display layer.

There are two new global topics:
- 'rpc-start' for showing the widget which indicates execution of rpc calls
- 'rpc-end' for hiding the widget which indicates execution of rpc calls.
These two global topics replace the original methods IPA.display_activity_icon() and
IPA.hide_activity_icon().

There is also new property of a command (notify_globally), which allows to turn off the widget
which indicates network activity. Instead of classic activity indicator there can be
called custom function at the beginning and at the end of network activity.

There are also changes in internal communication in rpc.js module. There are four new
events, two for calling on_success and on_error methods and two for calling custom functions
at the beginning and at the end of network activity.

https://fedorahosted.org/freeipa/ticket/6144
---
 install/ui/src/freeipa/certificate.js           | 60 +++++++++++++----
 install/ui/src/freeipa/ipa.js                   | 55 ++++++----------
 install/ui/src/freeipa/plugins/login.js         |  2 +-
 install/ui/src/freeipa/rpc.js                   | 85 +++++++++++++++++++------
 install/ui/src/freeipa/widget.js                | 16 +++--
 install/ui/src/freeipa/widgets/SyncOTPScreen.js |  7 +-
 6 files changed, 152 insertions(+), 73 deletions(-)

diff --git a/install/ui/src/freeipa/certificate.js b/install/ui/src/freeipa/certificate.js
index 232bdbf2fa95c3a68943539cd80129d481d8563a..ee8e1d4e0e14604bcbb45d0d63fc3acd00b03bfd 100755
--- a/install/ui/src/freeipa/certificate.js
+++ b/install/ui/src/freeipa/certificate.js
@@ -720,18 +720,34 @@ IPA.cert.request_action = function(spec) {
 
 IPA.cert.perform_revoke = function(spec, sn, revocation_reason) {
 
-    spec.hide_activity_icon = spec.hide_activity_icon || false;
+    /**
+     * Sets whether activity notification box will be shown
+     * during executing command or not.
+     */
+    spec.notify_globally = spec.notify_globally === undefined ? true :
+            spec.notify_globally;
+
+
+    /**
+     * Specifies function which will be called before command execution starts.
+     */
+    spec.start_handler = spec.start_handler || null;
+
+    /**
+     * Specifies function which will be called after command execution ends.
+     */
+    spec.end_handler = spec.end_handler || null;
 
     rpc.command({
         entity: 'cert',
         method: 'revoke',
-        hide_activity_icon: spec.hide_activity_icon,
         args: [ sn ],
         options: {
             'revocation_reason': revocation_reason
         },
-        notify_activity_start: spec.notify_activity_start,
-        notify_activity_end: spec.notify_activity_end,
+        notify_globally: spec.notify_globally,
+        start_handler: spec.start_handler,
+        end_handler: spec.end_handler,
         on_success: spec.on_success,
         on_error: spec.on_error
     }).execute();
@@ -844,11 +860,33 @@ IPA.cert.remove_hold_action = function(spec) {
 
 IPA.cert.perform_remove_hold = function(spec, sn) {
 
+    /**
+     * Sets whether activity notification box will be shown
+     * during executing command or not.
+     */
+    spec.notify_globally = spec.notify_globally === undefined ? true :
+            spec.notify_globally;
+
+
+    /**
+     * Specifies function which will be called before command execution starts.
+     */
+    spec.start_handler = spec.start_handler || null;
+
+    /**
+     * Specifies function which will be called after command execution ends.
+     */
+    spec.end_handler = spec.end_handler || null;
+
+
     rpc.command({
         entity: 'cert',
         method: 'remove_hold',
         args: [sn],
-        on_success: spec.on_success
+        on_success: spec.on_success,
+        notify_globally: spec.notify_globally,
+        start_handler: spec.start_handler,
+        end_handler: spec.end_handler
     }).execute();
 };
 
@@ -1344,11 +1382,11 @@ IPA.cert.cert_widget = function(spec) {
             on_ok: function() {
 
                 var command_spec = {
-                    hide_activity_icon: true,
-                    notify_activity_end: function() {
+                    notify_globally: false,
+                    end_handler: function() {
                         that.spinner.emit('hide-spinner');
                     },
-                    notify_activity_start: function() {
+                    start_handler: function() {
                         that.spinner.emit('display-spinner');
                     },
                     on_success: function() {
@@ -1376,11 +1414,11 @@ IPA.cert.cert_widget = function(spec) {
             ok_label: '@i18n:buttons.remove_hold',
             on_ok: function () {
                 var command_spec = {
-                    hide_activity_icon: true,
-                    notify_activity_end: function() {
+                    notify_globally: false,
+                    end_handler: function() {
                         that.spinner.emit('hide-spinner');
                     },
-                    notify_activity_start: function() {
+                    start_handler: function() {
                         that.spinner.emit('display-spinner');
                     },
                     on_success: function() {
diff --git a/install/ui/src/freeipa/ipa.js b/install/ui/src/freeipa/ipa.js
index e8ad832c3aacc0fcd824af3e1956ff621ae761ed..178bd03f6ab84d61ed4a74bbaf1a7157a51d2889 100644
--- a/install/ui/src/freeipa/ipa.js
+++ b/install/ui/src/freeipa/ipa.js
@@ -340,29 +340,6 @@ var IPA = function () {
         return reg.entity.get(name);
     };
 
-    /**
-     * Display network activity indicator
-     */
-    that.display_activity_icon = function() {
-        that.network_call_count++;
-        if (that.network_call_count === 1) {
-            topic.publish('network-activity-start');
-        }
-    };
-
-    /**
-     * Hide network activity indicator
-     *
-     * - based on network_call_count
-     */
-    that.hide_activity_icon = function() {
-        that.network_call_count--;
-
-        if (0 === that.network_call_count) {
-            topic.publish('network-activity-end');
-        }
-    };
-
     that.obj_cls = declare([Evented]);
 
     return that;
@@ -391,13 +368,13 @@ IPA.get_credentials = function() {
 
     function error_handler(xhr, text_status, error_thrown) {
         d.resolve(xhr.status);
-        IPA.hide_activity_icon();
+        topic.publish('rpc-end');
     }
 
     function success_handler(data, text_status, xhr) {
         auth.current.set_authenticated(true, 'kerberos');
         d.resolve(xhr.status);
-        IPA.hide_activity_icon();
+        topic.publish('rpc-end');
     }
 
     var request = {
@@ -407,7 +384,9 @@ IPA.get_credentials = function() {
         success: success_handler,
         error: error_handler
     };
-    IPA.display_activity_icon();
+
+    topic.publish('rpc-start');
+
     $.ajax(request);
 
     return d.promise;
@@ -439,7 +418,8 @@ IPA.logout = function() {
     }
 
     function success_handler(data, text_status, xhr) {
-        IPA.hide_activity_icon();
+        topic.publish('rpc-end');
+
         if (data && data.error) {
             show_error(data.error.message);
         } else {
@@ -448,7 +428,8 @@ IPA.logout = function() {
     }
 
     function error_handler(xhr, text_status, error_thrown) {
-        IPA.hide_activity_icon();
+        topic.publish('rpc-end');
+
         if (xhr.status === 401) {
             reload();
         } else {
@@ -467,7 +448,8 @@ IPA.logout = function() {
         success: success_handler,
         error: error_handler
     };
-    IPA.display_activity_icon();
+    topic.publish('rpc-start');
+
     $.ajax(request);
 };
 
@@ -485,7 +467,8 @@ IPA.login_password = function(username, password) {
     var d = new Deferred();
 
     function success_handler(data, text_status, xhr) {
-        IPA.hide_activity_icon();
+        topic.publish('rpc-end');
+
         result = 'success';
         auth.current.set_authenticated(true, 'password');
         d.resolve(result);
@@ -493,7 +476,8 @@ IPA.login_password = function(username, password) {
 
     function error_handler(xhr, text_status, error_thrown) {
 
-        IPA.hide_activity_icon();
+        topic.publish('rpc-end');
+
         if (xhr.status === 401) {
             var reason = xhr.getResponseHeader("X-IPA-Rejection-Reason");
 
@@ -526,7 +510,8 @@ IPA.login_password = function(username, password) {
         error: error_handler
     };
 
-    IPA.display_activity_icon();
+    topic.publish('rpc-start');
+
     $.ajax(request);
 
     return d.promise;
@@ -558,6 +543,8 @@ IPA.reset_password = function(username, old_password, new_password, otp) {
 
     function success_handler(data, text_status, xhr) {
 
+        topic.publish('rpc-end');
+
         result.status = xhr.getResponseHeader("X-IPA-Pwchange-Result") || status;
 
         if (result.status === 'policy-error') {
@@ -571,6 +558,7 @@ IPA.reset_password = function(username, old_password, new_password, otp) {
     }
 
     function error_handler(xhr, text_status, error_thrown) {
+        topic.publish('rpc-end');
         return result;
     }
 
@@ -596,9 +584,8 @@ IPA.reset_password = function(username, old_password, new_password, otp) {
         error: error_handler
     };
 
-    IPA.display_activity_icon();
+    topic.publish('rpc-start');
     $.ajax(request);
-    IPA.hide_activity_icon();
 
     return result;
 };
diff --git a/install/ui/src/freeipa/plugins/login.js b/install/ui/src/freeipa/plugins/login.js
index c555ed2d9c15f4199e98f80e58ea22b29a6bd459..2a2cf6900a96af5590099c807424d0f77e8670cb 100644
--- a/install/ui/src/freeipa/plugins/login.js
+++ b/install/ui/src/freeipa/plugins/login.js
@@ -97,4 +97,4 @@ define(['dojo/_base/declare',
     });
 
     return login;
-});
\ No newline at end of file
+});
diff --git a/install/ui/src/freeipa/rpc.js b/install/ui/src/freeipa/rpc.js
index a185585f4176658e299e7e92434522c936cc36b4..c24c19815994f9e24c10627976a12244bb5ca125 100644
--- a/install/ui/src/freeipa/rpc.js
+++ b/install/ui/src/freeipa/rpc.js
@@ -25,13 +25,15 @@
 define([
     'dojo/_base/lang',
     'dojo/Deferred',
+    'dojo/on',
+    'dojo/topic',
     './auth',
     './ipa',
     './text',
     './util',
     'exports'
    ],
-   function(lang, Deferred, auth, IPA, text, util, rpc /*exports*/) {
+   function(lang, Deferred, on, topic, auth, IPA, text, util, rpc /*exports*/) {
 
 /**
  * Call an IPA command over JSON-RPC.
@@ -99,9 +101,10 @@ rpc.command = function(spec) {
     /**
      * Allow turning off the activity icon.
      *
-     * @property {Boolean} show=true
+     * @property {Boolean} notify_globally=true
      */
-    that.hide_activity_icon = spec.hide_activity_icon || false;
+    that.notify_globally = spec.notify_globally === undefined ? true :
+        spec.notify_globally;
 
     /**
      * Allow set function which will be called when the activity of the command
@@ -109,7 +112,7 @@ rpc.command = function(spec) {
      *
      * @property {Function}
      */
-    that.notify_activity_start = spec.notify_activity_start || null;
+    that.start_handler = spec.start_handler || null;
 
     /**
      * Allow set function which will be called when the activity of the command
@@ -117,7 +120,7 @@ rpc.command = function(spec) {
      *
      * @property {Function}
      */
-    that.notify_activity_end = spec.notify_activity_end || null;
+    that.end_handler = spec.end_handler || null;
 
     /** @property {string} error_message Default error message */
     that.error_message = text.get(spec.error_message || '@i18n:dialogs.batch_error_message', 'Some operations failed.');
@@ -222,11 +225,11 @@ rpc.command = function(spec) {
     };
 
     that.handle_notify_execution_end = function() {
-        if (that.hide_activity_icon) {
-            if (that.notify_activity_end) that.notify_activity_end();
+        if (that.notify_globally) {
+            topic.publish('rpc-end');
         }
         else {
-            IPA.hide_activity_icon();
+            that.emit('end');
         }
     };
 
@@ -284,7 +287,7 @@ rpc.command = function(spec) {
 
             var self = this;
             function proceed() {
-                // error_handler() calls IPA.hide_activity_icon()
+                // error_handler() publishes 'rpc-end'
                 error_handler.call(self, xhr, text_status, error_thrown);
             }
 
@@ -368,7 +371,7 @@ rpc.command = function(spec) {
         function success_handler(data, text_status, xhr) {
 
             if (!data) {
-                // error_handler() calls IPA.hide_activity_icon()
+                // error_handler() publishes 'rpc-end'
                 error_handler.call(this, xhr, text_status, /* error_thrown */ {
                     name: text.get('@i18n:errors.http_error', 'HTTP Error')+' '+xhr.status,
                     url: this.url,
@@ -382,7 +385,7 @@ rpc.command = function(spec) {
                 window.location.reload();
 
             } else if (data.error) {
-                // error_handler() calls IPA.hide_activity_icon()
+                // error_handler() publishes 'rpc-end'
                 error_handler.call(this, xhr, text_status,  /* error_thrown */ {
                     name: text.get('@i18n:errors.ipa_error', 'IPA Error') + ' ' +
                           data.error.code + ': ' + data.error.name,
@@ -411,14 +414,24 @@ rpc.command = function(spec) {
 
                     dialog.on_ok = function() {
                         dialog.close();
-                        if (that.on_success) that.on_success.call(ajax, data, text_status, xhr);
+                        that.emit('success', {
+                            that: ajax,
+                            data: data,
+                            text_status: text_status,
+                            xhr: xhr
+                        });
                     };
 
                     dialog.open();
 
                 } else {
                     //custom success handling, maintaining AJAX call's context
-                    if (that.on_success) that.on_success.call(this, data, text_status, xhr);
+                    that.emit('success', {
+                        that: this,
+                        data: data,
+                        text_status: text_status,
+                        xhr: xhr
+                    });
                 }
                 that.process_warnings(data.result);
                 deferred.resolve({
@@ -445,11 +458,11 @@ rpc.command = function(spec) {
             error: error_handler_login
         };
 
-        if (that.hide_activity_icon) {
-            if (that.notify_activity_start) that.notify_activity_start();
+        if (that.notify_globally) {
+            topic.publish('rpc-start');
         }
         else {
-            IPA.display_activity_icon();
+            that.emit('start');
         }
 
         $.ajax(that.request);
@@ -531,6 +544,26 @@ rpc.command = function(spec) {
         return string;
     };
 
+    that.register_handlers = function() {
+        on(that, 'start', function() {
+            if (that.start_handler) that.start_handler();
+        });
+
+        on(that, 'end', function() {
+            if (that.end_handler) that.end_handler();
+        });
+
+        on(that, 'success', function(e) {
+            if (that.on_success) that.on_success(e.data, e.text_status, e.xhr);
+        });
+
+        on(that, 'error', function(xhr, text_status, error_thrown) {
+            if (that.on_error) that.on_error(xhr, text_status, error_thrown);
+        });
+    };
+
+    that.register_handlers();
+
     return that;
 };
 
@@ -665,11 +698,14 @@ rpc.batch_command = function(spec) {
                 var failed = that.get_failed(command, result, text_status, xhr);
                 that.errors.add_range(failed);
 
-                if (command.on_success) command.on_success.call(this, result, text_status, xhr);
+                command.emit('success', {
+                    data: result,
+                    text_status: text_status,
+                    xhr: xhr
+                });
             }
         }
 
-        //check for partial errors and show error dialog
         if (that.show_error && that.errors.errors.length > 0) {
             var ajax = this;
             var dialog = IPA.error_dialog({
@@ -686,13 +722,22 @@ rpc.batch_command = function(spec) {
 
             dialog.on_ok = function() {
                 dialog.close();
-                if (that.on_success) that.on_success.call(ajax, data, text_status, xhr);
+                that.emit('success', {
+                    that: ajax,
+                    data: data,
+                    text_status: text_status,
+                    xhr: xhr
+                });
             };
 
             dialog.open();
 
         } else {
-            if (that.on_success) that.on_success.call(this, data, text_status, xhr);
+            that.emit('success', {
+                data: data,
+                text_status: text_status,
+                xhr: xhr
+            });
         }
     };
 
diff --git a/install/ui/src/freeipa/widget.js b/install/ui/src/freeipa/widget.js
index 9151ebac9438e9e674f81bfb1ccfe7a63872b1ae..aa1fc381d670a7f701c0123e3510f0f9da3e95ee 100644
--- a/install/ui/src/freeipa/widget.js
+++ b/install/ui/src/freeipa/widget.js
@@ -6974,6 +6974,8 @@ exp.activity_widget = IPA.activity_widget = function(spec) {
 
     that.icon = spec.icon || 'fa fa-spinner fa-spin';
 
+    that.connection_counter = 0;
+
     /**
      * Operation mode
      *
@@ -6983,8 +6985,8 @@ exp.activity_widget = IPA.activity_widget = function(spec) {
      */
     that.mode = spec.mode || "dots";
 
-    that.activate_event = spec.activate_event || 'network-activity-start';
-    that.deactivate_event = spec.deactivate_event || 'network-activity-end';
+    that.activate_event = spec.activate_event || 'rpc-start';
+    that.deactivate_event = spec.deactivate_event || 'rpc-end';
 
     that.create = function(container) {
         that.widget_create(container);
@@ -7009,10 +7011,15 @@ exp.activity_widget = IPA.activity_widget = function(spec) {
         }
         that.set_visible(that.visible);
         topic.subscribe(that.activate_event, function() {
-            that.show();
+            ++that.connection_counter;
+
+            if (that.connection_counter === 1) that.show();
         });
         topic.subscribe(that.deactivate_event, function() {
-            that.hide();
+            --that.connection_counter;
+
+            if (that.connection_counter === 0) that.hide();
+            if (that.connection_counter < 0) that.connection_counter = 0;
         });
     };
 
@@ -7033,6 +7040,7 @@ exp.activity_widget = IPA.activity_widget = function(spec) {
         that.toggle_class('closed', true);
         that.row_node.detach(); // to save CPU time (spinner icon)
         that.toggle_timer(false);
+
     };
 
     that.show = function() {
diff --git a/install/ui/src/freeipa/widgets/SyncOTPScreen.js b/install/ui/src/freeipa/widgets/SyncOTPScreen.js
index 09d1254419cc2fa7981cf03421ebbf8d3197eee0..afb913753df60890b8018e1e556dcbefe2927e17 100644
--- a/install/ui/src/freeipa/widgets/SyncOTPScreen.js
+++ b/install/ui/src/freeipa/widgets/SyncOTPScreen.js
@@ -23,6 +23,7 @@ define(['dojo/_base/declare',
         'dojo/dom-construct',
         'dojo/dom-style',
         'dojo/query',
+        'dojo/topic',
         'dojo/on',
         '../ipa',
         '../auth',
@@ -32,7 +33,7 @@ define(['dojo/_base/declare',
         '../util',
         './LoginScreenBase'
        ],
-       function(declare, Deferred, construct, dom_style, query, on,
+       function(declare, Deferred, construct, dom_style, query, topic, on,
                 IPA, auth, reg, FieldBinder, text, util, LoginScreenBase) {
 
 
@@ -150,7 +151,7 @@ define(['dojo/_base/declare',
             var handler = function(data, text_status, xhr) {
                 var result = xhr.getResponseHeader("X-IPA-TokenSync-Result");
                 result = result || 'error';
-                IPA.hide_activity_icon();
+                topic.publish('rpc-end');
                 d.resolve(result);
             };
 
@@ -165,7 +166,7 @@ define(['dojo/_base/declare',
                 error: handler
             };
 
-            IPA.display_activity_icon();
+            topic.publish('rpc-start');
             $.ajax(request);
             return d.promise;
         },
-- 
2.5.5

From 2bf16885faaeb58d544c3a988fa7857cdd36d339 Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Tue, 9 Aug 2016 12:43:09 +0200
Subject: [PATCH 2/2] Change activity text while loading metadata

After log in into webui there was 'Authenticating' sign even during loading metadata.
No while data are loading there is 'Loading data' text. This change requires new global
topic 'change-activity-text' of activity widget. So for now there is possibility to change
every activity string during running phase just by publishing 'change-activity-text' topic
and setting old text as first parameter and the new one as second param.

Part of: https://fedorahosted.org/freeipa/ticket/6144
---
 install/ui/src/freeipa/ipa.js      | 3 +++
 install/ui/src/freeipa/widget.js   | 5 +++++
 install/ui/test/data/ipa_init.json | 1 +
 ipaserver/plugins/internal.py      | 1 +
 4 files changed, 10 insertions(+)

diff --git a/install/ui/src/freeipa/ipa.js b/install/ui/src/freeipa/ipa.js
index 178bd03f6ab84d61ed4a74bbaf1a7157a51d2889..300d1ffc8c3a3a7a956eff8a825ae80ef7fcf2c6 100644
--- a/install/ui/src/freeipa/ipa.js
+++ b/install/ui/src/freeipa/ipa.js
@@ -276,6 +276,9 @@ var IPA = function () {
      * @param {Function} params.on_error
      */
     that.init_metadata = function(params) {
+        var loading = text.get('@i18n:login.loading_md');
+
+        topic.publish('change-activity-text', 'Authenticating', loading);
 
         var objects = rpc.command({
             name: 'ipa_init_objects',
diff --git a/install/ui/src/freeipa/widget.js b/install/ui/src/freeipa/widget.js
index aa1fc381d670a7f701c0123e3510f0f9da3e95ee..8badb9b68c18157b1504b129248a26cdc30e819f 100644
--- a/install/ui/src/freeipa/widget.js
+++ b/install/ui/src/freeipa/widget.js
@@ -6987,6 +6987,7 @@ exp.activity_widget = IPA.activity_widget = function(spec) {
 
     that.activate_event = spec.activate_event || 'rpc-start';
     that.deactivate_event = spec.deactivate_event || 'rpc-end';
+    that.change_text = 'change-activity-text';
 
     that.create = function(container) {
         that.widget_create(container);
@@ -7021,6 +7022,10 @@ exp.activity_widget = IPA.activity_widget = function(spec) {
             if (that.connection_counter === 0) that.hide();
             if (that.connection_counter < 0) that.connection_counter = 0;
         });
+
+        topic.subscribe(that.change_text, function(old_text, new_text) {
+            if (that.text === old_text) that.text = new_text;
+        });
     };
 
     that.toggle_timer = function(start) {
diff --git a/install/ui/test/data/ipa_init.json b/install/ui/test/data/ipa_init.json
index 77d6fce4e9ca0cf281d89e09f803d6a1a81c6870..176fac4ed715cbc695639b1f9338b37d38d937b2 100644
--- a/install/ui/test/data/ipa_init.json
+++ b/install/ui/test/data/ipa_init.json
@@ -186,6 +186,7 @@
                         "form_auth": "<i class=\"fa fa-info-circle\"></i> To login with <strong>username and password</strong>, enter them in the corresponding fields, then click Login.",
                         "header": "Logged In As",
                         "krb_auth_msg": "<i class=\"fa fa-info-circle\"></i> To login with <strong>Kerberos</strong>, please make sure you have valid tickets (obtainable via kinit) and <a href='http://${host}/ipa/config/unauthorized.html'>configured</a> the browser correctly, then click Login.",
+                        "loading_md": "Loading data",
                         "login": "Login",
                         "logout": "Logout",
                         "logout_error": "Logout error",
diff --git a/ipaserver/plugins/internal.py b/ipaserver/plugins/internal.py
index ff29262180a967b2611db17271a742c5e472a19f..cc713f0373334c711de618ab9329881b136e0b70 100644
--- a/ipaserver/plugins/internal.py
+++ b/ipaserver/plugins/internal.py
@@ -335,6 +335,7 @@ class i18n_messages(Command):
             "form_auth": _("<i class=\"fa fa-info-circle\"></i> To login with <strong>username and password</strong>, enter them in the corresponding fields, then click Login."),
             "header": _("Logged In As"),
             "krb_auth_msg": _("<i class=\"fa fa-info-circle\"></i> To login with <strong>Kerberos</strong>, please make sure you have valid tickets (obtainable via kinit) and <a href='http://${host}/ipa/config/unauthorized.html'>configured</a> the browser correctly, then click Login."),
+            "loading_md": _("Loading data"),
             "login": _("Login"),
             "logout": _("Logout"),
             "logout_error": _("Logout error"),
-- 
2.5.5

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to