You are here: Noosfero>Dev>PluginsArchitecture (28 Apr 2012, AurelioAHeckert) EditAttach

Plugins

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:

To make it easier to understand, we will use the first Noosfero's plugin created, Mezuro Plugin, as an example. You may create a new plugin using script/noosfero-plugins new pluginname

Basic steps

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'. To define your plugin here, you must pay attention to 2 points:
    • Meta-informations
    • Events

Meta-informations

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.

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. ^^

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 "_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

For the moment, the plugins doesn't have the completely independent route system. But to allow the plugins to have a good interaction with Noosfero, 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
environment => AdminController

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! ^^

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:

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 yous 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

This is another point in which we aren't done yet. The idea is to integrate the plugins tests with Noosfero tests. This way, every time that we run Noosfero tests the plugins test will be ran too. For the moment, if you create any plugin, please add tests too. They will be integrated with Noosfero tests after. Sorry!

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

Translations

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

Extending core models

Let's say we need to change the Enterprise model of Noosfero.

First, you need to add in the beggining of your lib/pluginname_plugin.rb

require_dependency 'ext/enterprise'

Then, create the file lib/ext/enterprise.rb and add some code as this:

require_dependency 'enterprise'                                                                 
                                                                                                
class Enterprise                                                                                
  has_many :input_categories, :through => :inputs, :source => :product_category, :uniq => true
  has_many :product_categories, :through => :products, :source => :product_category, :uniq => true
end

You may check out the ShoppingCart? plugin code as an example.

Plugins' instalation

The plugins instalation process is very easy. As the plugins will be maintained inside Noosfero's tree, all plugins will be available inside the folder "plugins". To activate it on your Noosfero's instalation you just need to run the script noosfero-plugins. With this script you will be able to manage the plugins of your Noosfero instalation: list all available plugins, see the status of each plugin, enable/disable them and also create a new plugin. To see the script options:
script/noosfero-plugins

Add comment
You need to login to be able to comment.
 

Topic revision: r15 - 28 Apr 2012 - 13:41:01 - AurelioAHeckert
 
Translations: English
Global Search:
   
ActionItem Search:

Copyright © 2007-2011 Noosfero
Colivre - Cooperativa de Tecnologias Livres