Plugins

How to
Create a Plugin

Plugins activation

The plugins activation works in two steps. As the plugins will be maintained inside Noosfero's tree, all plugins will be available inside the folder "plugins". The first step is to run the script noosfero-plugins. With this script you will be able to manage the plugins of your Noosfero installation: list all available plugins, see the status of each plugin, enable/disable them and also create a new plugin. To see the script options run:
$ script/noosfero-plugins
(nice information goes here)
To enable some plugin, run:
$ script/noosfero-plugins enable PLUGIN
To enable all plugins, run:
$ script/noosfero-plugins enableall
This will make activated plugin's code to be loaded when the application starts. You'll need to restart our webserver.

Noosfero's plugins are also activated per environment, as Noosfero supports multiples environments per application. The second step is to activate the plugin using an administrator account in the administration panel of your network. Go to /admin/plugins and mark it as enabled.

Basic Structure

Noosfero plugins' structure is built over an event-based paradigm. What's does it means? It means that Noosfero fires an event in a determined point and all plugins interested in that event are able to act. Then, every new event must be well documented so that every new plugin can define its actions over each event without need to rewrite Noosfero. This events are called hotspots.

In this world of plugins, we'll have 2 basic roles:
  • Noosfero's developer
  • Plugin's developer
Each of these guys will have very different concerns but in the end the feature will be a combination of their jobs, so the "interfaces" between them must be well defined.

Noosfero's developer's view

First of all, let's take a look at the Noosfero's developer's side in a plugin creation. Before the creation of any plugin, this developer must create the hotspots to which the plugins can register and start to add new cool features to Noosfero. =D

Defining hotspots

As said before, events that Noosfero fires to the plugins are called hotspots. These are the points where the plugins can make some action. To define a hotspot the developer must define 4 important points:
  1. Plugin's interface
  2. Noosfero's side interpretation
  3. Context variables
  4. Tests the hotspots (tests must be always there! ^^)

Plugin interface

To create a new event, first of all, the developer must create a method in the class Noosfero::Plugin that will work as the interface between Noosfero and the plugins. This method should have a brief documentation with it's description and the format of the return value. This method must return nil. The plugins that want to register to this event might override this method according to its specifications. Let's show an example implemented in Mezuro (the first Noosfero plugin):

lib/noosfero/plugin.rb
  # -> Adds buttons to the control panel
  #   returns = { :title => title, :icon => icon, :url => url }
  #   title = name that will be displayed.
  #   icon  = css class name (for customized icons include them in a css file).
  #   url   = url or route to which the button will redirect.
  def control_panel_buttons
    nil
  end

As you can see, the developer created a new event called "control_panel_buttons". This event allow the plugins to add new buttons to the control panel. Also, as a good practice, the developer specified the format of the return value over the event. Every new event should be documented with its parameters and its return values. In this case, the format of the return value is a hash containing a title, an icon and an url to which the button will redirect. Remember to always define return values in a declarative way.

Note that the event name is in a plural form. Thats the format every event should be. The plugins can return a single answer or a list of them. But don't worry, all this things are treated in the manager. Just remember to always pluralize your events.

Noosfero side interpretation

Here is the point where the developer must treat the plugin information. After defining the event, the developer must treat all the plugins that are registered to that event in the correct place. To deal with this, Noosfero implemented a Manager. This manager is reloaded in every new request with new instances of each plugin. You can access this manager in every view or controller through the variable @plugins. To make it easier to the developer, was also created a method to this manager called map. This method receives a symbol specifing the event that should be alerted and will return a list with the answers of all plugins registered to that event. Let's see how this is done with the event "control_panel_buttons" we saw in the last example:

app/views/profile_editor/index.rhtml
<% @plugins.map(:control_panel_buttons).each do |button| %>
  <%= control_panel_button(button[:title], button[:icon], button[:url]) %>
<% end %>

So, here the developer triggers the event with the method map and this method returns all the answers of the registered plugins to that event. With this list, the developer creates each buttons that every plugins specified. Pretty easy isn't it?! ^^

Context variable

Noosfero's plugin's structure offers a context variable that contains some important information about the context in which the event was triggered. This information is accessible to the plugins so they can analyze the context before defining what's going to be their action. The basic informations today are: profile, request, response, environment and params. If your new hotspot needs, you can define new informations on "lib/noosfero/plugin/context.rb" file. Remember that all this informations might be taken from the controller.

Testing the hotspots

Noosfero is developed over a TDD development process, so we need to test every little part of it. This isn't different to plugins interfaces like the events. Besides the functional and unit tests that you might need to create with a new definition of event, Noosfero offers an easy way to deal with the cucumber integration tests to the events on Noosfero's side. In the example we saw before, the developer created the interface for the plugins to add new buttons to Noosfero's control panel. Let's see how we can make a simple test to check if this new feature is working as expected:

Background:
  Given the following plugin
    | klass       |
    | TestPlugin  |
  And the following events of TestPlugin
    | event                 | body                                                                  |
    | control_panel_buttons | lambda { {:title => 'Test plugin button', :icon => '', :url => ''} }  |

Scenario: a user must see the plugin\'s button in the control panel if the plugin is enabled
  Given plugin TestPlugin is enabled on environment
  And I go to Joao Silva's control panel
  Then I should see "Test plugin button"

Scenario: a user must not see the plugin\'s button in the control panel if the plugin is disabled
  Given plugin TestPlugin is disabled on environment
  And I go to Joao Silva's control panel
  Then I should not see "Test plugin button"

Let's see this tests through parts:
  1. First of all, you see that Noofero offers the step "the following plugin" where you can define each plugin that is enabled on the test system. You need only to define the plugin class_name.
  2. With the plugin created, now you can specify each event to which the plugin is registered through the step "following events of TestPlugin". You must define the event name and the body of the event. Note that this body must be a callable block of code.
  3. The last feature is the step "plugin <plugin-class-name> is enabled/disabled on environment".
That's it. Now you have a basic plugin to make any test you need in your new events.

Plugin's developer's view

Now let's take a look in the plugin's developer's side. In this part we will study closely the following points of a plugin creation:

How To Crate a Noosfero Plugin

There is a important structure inside the plugin directory. You must not bore with that. Just run:
$ script/noosfero-plugins new cool_feature
The CoolFeaturePlugin was created and enabled. You must restart your personal dev server.

You will find you base code in plugin/cool_feature/lib/cool_feature_plugin.rb with this content:
class CoolFeaturePlugin < Noosfero::Plugin

  def self.plugin_name
    # FIXME
    "Cool Feature" # ⬅ you can change it freely, but the word "plugin" is overabundant
  end

  def self.plugin_description
    # FIXME
    _("A plugin that does this and that.")
  end

end

To make it easier to understand the next steps, we will use the first Noosfero's plugin created, Mezuro Plugin, as an example.

Definition

Now you might be saying: "So here we are, Noosfero's developers created lots of cool hotspots and now I want to create a new plugin that will use several of this hotspots, what should I do??" As a good book I read once a long time ago used to say "Don't panic!". ^^

Let's go:
  1. Create a folder with the name of your plugin.
  2. Inside this folder create a file called "init.rb" and a folder called "lib".
  3. Inside the folder "lib" create a file called "<your-plugin-name>_plugin.rb". In our case, this file is going to be called "mezuro_plugin.rb".
  4. Now open the "init.rb" file.
  5. Inside of this file write "require '<your-plugin-name>_plugin'". In our case, "require 'mezuro_plugin'". This init file is the one that will be called on Noosfero's load. So it's a good idea to separate your plugin's load of your plugin's definition.
  6. Now that we did the plugin load, let's specify our plugin. Open the file 'lib/<your-plugin-name>_plugin.rb'.

Noosfero's plugin structure offers the possibility to the plugin to specify it's meta-informations. In this moment, the supported meta-informations are name and description. This informations will be used do describe your plugin on Noosfero, so it's really important that you define them. To check all meta-informations available on Noosfero take a look at the file 'lib/noosfero/plugin.rb'. In the case of Mezuro, we defined the meta-informations like this:
def self.plugin_name
  "Mezuro"
end

def self.plugin_description
  _("A metric analizer plugin.")
end
NOTE: the '_()' is used to translate the text if there is any available translation.

Anatomy

plugins/nice_feature/        The NiceFeaturePlugin root dir. 
├── install.rb               An optional script to run when noosfero-plugin enable this. 
├── controllers              Create new controllers for Noosfero at this plugin route. 
│   └── nice_feature_admin_controller.rb   This plugin configuration controller. 
├── lib                      The main plugin folder. Where the magic happens. 
│   ├── ext                  Where to place extensions (or monkey-patches) for core Noosfero models. 
│   │   └── environment.rb   A example extension. 
│   └── nice_feature_plugin.rb   The main plugin file. 
│   ├── nice_feature_plugin   Where to place name-spaced classes 
│   │   └── nice_model.rb    This model will be called "NiceFeaturePlugin::NiceModel" 
│   └── presenters           Where to set new file presenters 
│       └── file_type.rb     A presenter sub-class to display some mimetype 
├── locale                   The place for compiled localization files. (not for humans) 
├── po                       The localization root. 
│   ├── piwik.pot            The template localization file. The source of strings to translate. 
│   └── pt                   A language directory. 
│       └── piwik.po         A localization file with translated strings. 
├── public                   Where to place public files 
│   ├── images               Yeah... 
│   │   └── pic.png          Yeaaah... 
│   ├── style.css            This will be auto loaded when you return true from plugins's "stylesheet?" method. 
│   └── some-script.js       This must be required in a view. 
├── test                     Shame on you, if you forgot this directory! 
│   ├── functional           Test controllers. 
│   │   └── some_test.rb      
│   └── unit                 Test class methods directly. 
│       └── some_test.rb      
└── views                    Where to place templates to be rendered. 
    ├── nice_feature_plugin_admin   Where to place this plugin configuration controller's action views. 
    │   └── index.html.erb   The view to the index action 
    ├── some_web_ui.html.erb   well... 
    └── file_presenter       The presenter views 
        └── _file_type.html.erb   The view for some mime-type. 

Pluging and extending the core

Events

After defining the meta-informations, it's time to deal with the hotspots. To make your plugin register to an event, you need only to define a method overriding the event to which you want to register. In this case, we will use the "control_panel_buttons" event. Before anything we must see the format definition of this event. Let's take a look on the file "lib/noosfero/plugin.rb":

lib/noosfero/plugin.rb
# -> Adds buttons to the control panel
# returns = { :title => title, :icon => icon, :url => url }
#   title = name that will be displayed.
#   icon  = css class name (for customized icons include them in a css file).
#   url   = url or route to which the button will redirect.
def control_panel_buttons
  nil
end

As we expected, the developer created a well documented hotspot. Now we know that our plugin must return a hash containing the title of the button, the css class name to define our button's icon (take a look at the public file section) and the url to which the button will redirect. Really clear! This is the power of the declarative definition. We must only worry about the information of the button and not how it's going to be implemented. So, let's see how Mezuro implemented this hotspot:

def control_panel_buttons
  if context.profile.community?
    { :title => 'Mezuro projects', :icon => 'mezuro', :url => {:controller => 'mezuro_plugin_myprofile', :action => 'index'} }
  end
end

Here we have some new things to take a look.

The first is the variable context. This variable is offered by Noosfero to the plugins containing some information about the context on where the event is being triggered. In this example we check if the profile is a community before adding the new button. Today, the available informations are: profile, request, response, environment and params. To check the available information take a look at "lib/noosfero/plugin/context.rb". If you need some information that is not available in the context contact any Noosfero developer or feel free to send a patch!

The second is the route we used in the variable :url. This route is a plugin route. We will discuss this point better in the "Controllers and routes" section.

Note that in all of this events, the plugin can return a single answer or a list of them. Thats why every event must be in plural. This is all treated in the Manager.

Now we are done! With only this steps you have a plugin that adds a new button to the communities' control panel! But what this button does? To where it redirects? What we can do with it? Be calm. We are going through all this questions in the following steps. ^^

Extending core classes

In many moments, your plugin will want to extend or redefine core models features. Usually you should do this through a defined hotspot, but sometimes a hotspot is just too overkill for simpler things or not enough for bigger ones. In these cases you can extend the core models by adding these new definitions on the folder "ext" that should be created inside the folder "lib". So if we would like to extend the core model Article, we just need to create the file lib/ext/article.rb with a content more or less like this:

require_dependency 'article'

class Article
  def hit_with_mezuro_plugin_extension
    hit_without_mezuro_plugin_extension
    puts "This is the hit new extension!"
  end
  alias_method_chain :hit :mezuro_plugin_extension

  def mezuro_plugin_new_method
    puts "Also adding a new method!"
  end
end

First of all, notice that we require the article model on the first line. This is necessary since rails has the lazy loading way of loading the core classes, so you must explicitly require the article model to ensure that the extension will work. The 'require_dependency' is useful to avoid multiples loads of the article model.

After that, we rewrite the hit execution by chaining some new code and we also include a new method in the Article model. Notice that both the chain extension and the method name are prefixed with the plugin's name. This is important to avoid name conflicts. You may extend the models with every power a model allows you to use, but always remember: "with great powers come great responsabilities". Use this feature with caution and remember to use the principle of namespacing whenever you see would be necessary.

Extending core controllers

Put on plugins/my_plugin/lib/my_plugin.rb:
Rails.configuration.to_prepare do
  SearchController.send :include, SolrPlugin::FacetsBrowse
  SearchController.helper SolrPlugin::SearchHelper
end
FacetsBrowse methods' then became actions on the controller.

Scoping

As plugins code's, be it Ruby, JS or CSS is loaded at every request and page, it is important to scope these codes so that it won't conflict with others. For further explanations, consider the plugin name is MyPlugin (my_plugin).

Ruby
Prefix class/modules names with MyPlugin or put them in the namespace of that class.

Javascript
Use JS's prototype to define objects:

myplugin = {

  value: 1,

  f: function() {
    console.log('hello');
  },

}
myplugin.f();

CSS
Prefix class names or use a parent selector to avoid conflicts with others styles.

#myplugin-parent .button {
}

.myplugin-button {
}

Add stuff to user data

Plugins can also add extra values to the user data hash, that is loaded by Ajax on every page load. It can be done easily by following only the two steps below:

Tables and records

Migrations

If your plugin needs to create new tables, Noosfero offers support to it. To add new migrations to Noosfero's load, define your migrations (as in a rails application) inside a folder "db/migrate/". You must note 3 important points:
  1. All of your migrations must not depend on Noosfero migrations.
  2. All of your migrations must use the datetime format before the migration name to avoid conflicts with Noosfero's migrations. Ex: 20101209151530_create_projects.rb
  3. All new tables that you create must be namespaced with "<plugin-name>_plugin". This will avoid conflict with other tables. In the case of Mezuro, we needed to create 2 new tables: projects and metrics. So, here are the migrations:
class CreateProjects < ActiveRecord::Migration
  def self.up
    create_table :mezuro_plugin_projects do |t|
      t.string      :name
      t.string      :identifier
      t.string      :personal_webpage
      t.text        :description
      t.string      :repository_url
      t.string      :svn_error
      t.boolean     :with_tab
      t.references  :profile

      t.timestamps
    end
  end

  def self.down
    drop_table :mezuro_plugin_projects
  end
end
class CreateMetrics < ActiveRecord::Migration
  def self.up
    create_table :mezuro_plugin_metrics do |t|
      t.string  :name
      t.float   :value
      t.integer :metricable_id
      t.string  :metricable_type

      t.timestamps
    end
  end

  def self.down
    drop_table :mezuro_plugin_metrics
  end
end

Discard the changes in db/schema.rb using git checkout, as plugins migrations are executed depending whether the plugin is enabled or not.

Attention: avoid at all costs addind new attributes do existing tables from plugin migrations. Suppose you want to add a foo_id attribute to the profiles table to be able to add a belongs_to association to Profile. Instead, add a profile_id to the foos table, and add a has_one relation to Profile instead.

Models

Defining models to your plugin is very important when you have a huge plugin that deals with complex aspects. So let's see how you can define new models that will work together with Noosfero.

First of all, all your models must be inside a module named "<your-plugin-name-camelcased>Plugin". In the case of Mezuro it would be "MezuroPlugin". This is necessary because we must guarantee that your new models won't conflicts with any other plugin, or even Noosfero's, models. So, if you need new models, create a new folder called "<your-plugin>_plugin" (mezuro_plugin) inside the folder "lib". Inside this new folder you must define all of your new models. Mezuro plugin needed to create a new model called Project. So we created the file "lib/mezuro_plugin/project.rb". To specify that this model is inside the module MezuroPlugin we must define it this way:

class MezuroPlugin::Project < ActiveRecord::Base
end

But this isn't enough. As you needed a new model, you probably needed to create a new table to this model and as you read on the section "Migrations" (if you didn't read it now!) all the plugins' tables are namespaced with "<plugin-name>_plugin". The problem is that Rails ActiveRecord::Base class has it's own way to map the class_name to the table_name. So Noosfero created a new ActiveRecord::Base that is exactly the same as Rails ActiveRecord::Base but redefining the method table_name to work as specified. So all your models, instead of inheriting from ActiveRecord::Base, must inherit from Noosfero::Plugin::!ActiveRecord. Then the new model Project that we are creating would be like this:

class MezuroPlugin::Project < Noosfero::Plugin::ActiveRecord
end

Remember that always when you want to access this model from other places you must refer to it as MezuroPlugin::Project instead of just Project.

Controllers and routes

The plugin infrastructure offers a generic way to provide routes for the plugins based on the folder where the controller of the plugin is placed. There are 4 folders available, each one defining one base route. So, to create a controller you must create the folder "controllers" and inside this folder create the apropriate folder do define the context of your controller and inside this one the controller file itself. The controller file name must be in the following format: <plugin-name>_plugin_<desired-controller>_controller.rb. These routes are as follows (using Mezuro as an example):
  • Profile route
    • Context: profile
    • Access: public
    • Folder: mezuro/controllers/profile
    • Route: /profile/:profile/plugins/mezuro/<controller-name>/:action/:id
    • Controller Heritage: ProfileController
  • Myprofile route
    • Context: profile control panel
    • Access: profile edition permission
    • Folder: mezuro/controllers/myprofile
    • Route: /myprofile/:profile/plugin/mezuro/<controller-name>/:action/:id
    • Controller Heritage: MyprofileController
  • Admin route
    • Context: administration
    • Access: environment edition permission
    • Folder: mezuro/controllers/admin
    • Route: /admin/plugin/mezuro/<controller-name>/:action/:id
    • Controller Heritage: AdminController
  • PUblic route
    • Context: environment
    • Access: public
    • Folder: mezuro/controllers/public
    • Route: /plugin/mezuro/<controller-name>/:action/:id
    • Controller Heritage: PublicController

Before these generic routes, there were only 3 available controller for the plugins to use (as described below). These routes are still available although deprecated now. Noosfero offers 3 routes to the plugins: profile, myprofile and environment routes. These routes are as follows (using Mezuro as an example):
  • Profile route
    • Context: profile
    • Access: public
    • Route: /profile/:profile/plugins/mezuro/:action/:id {:controller=>"mezuro_plugin_profile"}
  • Myprofile route
    • Context: profile control panel
    • Access: profile edition permission
    • Route: /myprofile/:profile/plugin/mezuro/:action/:id {:controller=>"mezuro_plugin_myprofile"}
  • Environment route
    • Context: environment
    • Access: environment edition permission
    • Route: /plugin/mezuro/:action/:id => {:controller=>"mezuro_plugin_environment"}

With this routes, you are able to use any of this 3 controllers according to the situation. To use them, create the folder "controllers" and inside this folder create the controller file. The controller file name must be in the following format: <plugin-name>_plugin_<desired-controller>_controller.rb. In the case of Mezuro, we only needed to use the Myprofile controller, so we created the controller "controllers/mezuro_plugin_myprofile_controller.rb". Let's take a peak at this controller:

class MezuroPluginMyprofileController < MyProfileController
  append_view_path File.join(File.dirname(__FILE__) + '/../views')

  def index
    @projects = MezuroPlugin::Project.by_profile(profile)
  end

end

Looking at the first line, you can see that the controller must inherit from the respective NooferoController in the following way:
myprofile => MyProfileController
profile => ProfileController
admin => AdminController
public => PublicController

In the second line is the only way we found to make the relation between the controllers and the views in the moment. We plan to fix this but, for the moment, you must add this line in all of your controllers... =/

The rest is quite the same as a Rails controller. Note that, as we said in the "Models" section, we need to access the models through the module namespace (MezuroPlugin::Project).

Remember the button we created in the event "control_panel_buttons"? Let's take a look at him again:

def control_panel_buttons
  if context.profile.community?
    { :title => 'Mezuro projects', :icon => 'mezuro', :url => {:controller => 'mezuro_plugin_myprofile', :action => 'index'} }
  end
end

Now our button will redirect to the action "index" in the controller we just created! How about adding some visualization to this action now? Checkout the "Views" section.

Views

The views of your plugin will work exactly the same way as the normal Rails' views. You just need to create a folder called "views" and inside of it create a new folder for every controller you have with the following views for each action. Easy as pie. ^^

So if we want to add a visualization to the action "index" of our controller "MezuroPluginMyProfileController" it's easy. We just need to create the folder "views/mezuro_plugin_myprofile" and inside of it create our view "index.html.erb". The view will work as you already know. Here is a simple example:

<h1> <%= _("%s's Mezuro projects") % profile.name %>  </h1>

<% if @projects.blank? %>
  <%= _("%s has no projects registered.") % profile.name %>
<% else %>
  <%= _('Projects: ') %>
  <ul>
    <% @projects.each do |project|   %>
      <li><%= project.name %></li>
    <% end %>
  </ul>
<% end %>

Note that this view will run with the scope of a MyProfileController so we have access to things like profile. Another thing to have in mind is that the page will be in the context of this controller too, so your view will be yielded by some adequate template.

We are done! Now when the user open a community's control panel he will see your button and when he click it he will be redirected to a page that shows all projects of that profile. Pretty neat! ^^

Translations

The translations are added togheter with the core's code translations at the po directory.

1) Define what are the values that will be returned by the hash by defining the method user_data_extra in your plugin, for example:

def user_data_extras
  { :test => 'This is a test' }
end

So, the key "test" will be available in the user data hash with the value "This is a test". You can add as many key-value pairs as you desire.

2) Deal with the data

To deal with the data returned, you just need to bind the event userDataLoaded inside your plugin, for example:

<script type="text/javascript">
  jQuery(window).bind("userDataLoaded", function(event, data) { alert(data.test); });
</script>

When the user visits the page that contains this script, an alert box will be shown with the message "This is a test".

Public files

You may add a public directory on your plugin root, where you can add stylesheets, images and other files to be directly accessed bi clients.

To enable this you need the symlink of your plugin's public folder inside noosfero_root/public/plugins, but don't worry, the noosfero-plugins enable will do all the needed work for you.
script/noosfero-plugins enable some_plugin

Then you can referrer to your plugin files like this: http://localhost:3000/plugins/some_plugin/some-file

That is needed to the method stylesheet?. When returning true, the file some_plugin/public/style.css will be added to the application theme header.

Tests

Tests structure follows the same structure as in the Noosfero application. So just place tests in test/{units,funcionals} and features directories. As usual, tests load a test helper (e.g. test_helper.rb in unit tests). For your plugin you can define your own test_helper and it may require core's test_helper.

Tasks

Noosfero offers some rake tasks so that you can run the plugins' tests without the need of running Noosfero's tests too. As the tasks are generated dynamically, might be a little stressfull to understand through the code what tasks are being created. (But if you want to take a look: lib/tasks/plugins_tasks.rb) So here they are:

(Note: all the following commands have 'rake' before)

  • test:noosfero_plugins AND test:noosfero_plugins:available
    • Runs all available plugins' tests.
  • test:noosfero_plugins:enabled
    • Runs all enabled plugins' tests.
  • test:noosfero_plugins:
    • Runs all tests of the selected plugin

All of these tasks can have units/functionals/integration in the end to specify the type of the tests. So, to run all the unit tests of all enabled plugins you can run: rake test:noosfero_plugins:enabled:units

Dependencies and Installation

Noosfero also has a smart way to allow the plugins to define its own installation process and dependencies. For so, there are, respectively, the install.rb and dependencies.rb files. The install.rb is a ruby code where the plugin may take every necessary step to prepare it's environment so that it gets ready to run, at least, on the test (with all tests passing of course!) and development environment. The dependencies.rb requires each of the libs the plugin needs which allows Noosfero to know if the plugin is ready to run after its installation. The install.rb file is ran everytime the plugin is enabled, so it's important that its process is Idempotent. Here are some examples of install.rb and dependencies.rb files:

install.rb
system "gem install --user-install net-ldap -v 0.3.1"
puts "\nWARNING: This plugin is not setting up a ldap test server automatically.
Some tests may not be running. If you want to fully test this plugin, please
setup the ldap test server and make the proper configurations on
fixtures/ldap.yml.\n\n"

dependencies.rb
require 'rubygems'
require 'net/ldap'

Add comment
You need to login to be able to comment.
 
Topic revision: r33 - 27 Apr 2017, AurelioAHeckert

irc Talk with Devs Now!

 
Translations: English
Search on Docs:
   
ActionItem Search:

Copyright © 2007-2018 by the Noosfero contributors
Colivre - Cooperativa de Tecnologias Livres