Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Plugin Development

simplysoft edited this page · 18 revisions

This page covers the creation of a Concerto Plugin from scratch. For an overview of Concerto Plugins, see the Concerto Plugins Page.

An invaluable resource for creating Rails engines is the official Rails Guide on the topic. You may also like to refer to the ConcertoHardware repository, which we are attempting to maintain as an exemplar Concerto Plugin.

Creating a Plugin

Let's create a plugin called MyPlugin. Start with the following command.

rails plugin new my_plugin --mountable.

Note that you need to have the rails gem accessible to run this command. If you only have Concerto's gems installed to concerto/vendor/bundle, you can do the following:

export BUNDLE_GEMFILE=/path/to/concerto/Gemfile
bundle exec rails plugin new my_plugin --mountable

Now we need to make some quick modifications. Go into the my_plugin directory, and open app/controllers/my_plugin/application_controller.rb. Rather than inheriting from ActionController::Base, it is preferable to inherit from Concerto's application controller. This makes Concerto's layout and authorization methods available to your engine's controllers. It should now look like this:

module MyPlugin
  class ApplicationController < ::ApplicationController
  end
end

Next, go into lib/my_plugin/engine.rb. It is usually desirable to add a short engine name, so insert the following after the isolate_namespace line:

    engine_name 'my_plugin'

In the same file, we need to provide a way for Concerto to read back plugin information. Within the Engine class, add the following code (just a stub for now):

    # Define plugin information for the Concerto application to read.
    # Do not modify @plugin_info outside of this static configuration block.
    def plugin_info(plugin_info_class)
      @plugin_info ||= plugin_info_class.new do
        # We will add configuration options here as needed.
      end # plugin_info intializer
    end # plugin_info

Before you go on, this is a good time for some housekeeping. As desired:

  • Edit my_plugin.gemspec to include your name and a valid email address
  • Update any license info. Rails drops in the MIT license by default, but Concerto tends to use the Apache License, Version 2.0.
  • If you will be using version control, initialize your repository and make the initial commit

Adding Resources

A general case for engines includes providing resources, which may have a database table, a corresponding model, a corresponding controller, and a set of views which need routing to. This is just like generating a resource in a rails application.

You will need to run the rails command to do this. If you are working with bundled rather than system-wide resources, do something like this to run rails and rake tasks. We'll just refer to running the rails command below, so substitute as needed.

export BUNDLE_GEMFILE=/path/to/concerto/Gemfile
bundle exec rails <argument>

Now, to generate the resource.

 rails generate scaffold Post name:string body:text

This is a pretty familiar command. The resulting files are just like the output in a rails app, except that here everything is namespaced to the module for your engine (MyPlugin for example). Let's walk through each piece of the output and see what needs to be changed or modified. There are a couple of important subtleties!

Routes

If you take a look at config/routes.rb in your engine, you'll notice that your route has been added to the engine.

MyPlugin::Engine.routes.draw do
  resources :posts
end

However, this does not automatically add your routes to Concerto - the plugin will need to be mounted by Concerto first. Importantly, plugins must never modify or require modification of Concerto. Instead, we'll request the route through the plugin API. Open lib/my_plugin/engine.rb, and add a line like the following inside the plugin_info_class.new block (which we created in the Creating a Plugin tutorial above).

       add_route("myplugin", MyPlugin::Engine)

Now, when your plugin is enabled, your plugins routes will be mounted at http://concerto_url/myplugin. So posts will be available at http://concerto_url/myplugin/posts. You can use a standard root :to => statement in your routes.rb file to place a view at http://concerto_url/myplugin.

Migration

The table corresponding to your resource is created with the new migration file in <timestamp>_create_my_plugin_posts.rb. Note that your plugin's name has been included in the table name to avoid namespace conflicts with tables from Concerto or other plugins. Once your plugin is installed, and Concerto is restarted, this migration (and any others in db/migrate) will be automatically copied to the Concerto installation (with a new timestamp) and the migration will be run.

Models, Controllers, and Views

Due to the isolated namespace, Models, Controllers, and Views work just like in any Rails app, very few special calls are needed. Note that everything is in the namespace MyPlugin, and hence lives in subdirectories called my_plugin. You can refer to Models from both the main app and your plugin.

Link helpers are a little bit of a special case. If you need to link to a controller in the main application, you will need to use the main_app routing helper. Here are three examples of how to do this from views:

<%= link_to 'Concerto home page', main_app.root_url %>
<%= link_to 'All Screens', main_app.screens_path %>
<%= link_to @screen.name, [main_app, @screen] %>

Assets

Plugins can provide assets as well, but we haven't tried using them yet!

i18n

Although you do not need to specify the i18n gem in your plugin's Gemfile, you do need to specify the isolated namespace when referencing your plugin's models. The example below is from the concerto-hardware plugin.

<%= link_to t(:edit_model, :model => ConcertoHardware::Player.model_name.human), edit_player_path(@player), :class => "btn" %>

You localization file should be placed under config/locales. When specifying activerecord translations, you need to specify the isolated namespace as well. Here is an example from the concerto-hardware plugin's config/locales/en.yml file.

en:
  activerecord:
    models:
      concerto_hardware/player: 'Player'
    attributes:
      concerto_hardware/player:
        activated: 'Active'
        ip_address: 'IP Address'
        wkday_on_time: 'Weekday turn On time'
        wkday_off_time: 'Weekday turn Off time'
        wknd_disable: 'Disable on weekends'
        wknd_on_time: 'Weekend turn On time'
        wknd_off_time: 'Weekend turn Off time'

Adding Configuration Objects

If your plugin has customizable parameters, Concerto can take care of storing and retrieving them for you. Your plugin must specify a default value, which administrators may then modify through the Concerto Dashboard. Each configuration object should be listed in the plugin_info_class.new that we added to your plugin's engine.rb in the steps above. The format for specifying a configuration object is as follows:

        add_config("poll_interval", "60", 
                   :value_type => "integer",
                   :description => "Client polling interval in seconds")

The first argument is a unique identifier for the configuration item (scoped to your plugin, so don't worry about conflicting with other plugins). The second is the default value.

The configuration item is actually always stored as a string, but you may set value_type to make sure that the correct control is displayed in the dashboard. It can be "string", "integer", or "boolean". The description is also just for display in the dashboard.

Any configuration items that you create can be easily referenced from controller or view code. Here is an example from a view:

    <%= ConcertoConfig[:poll_interval] %>

For boolean values, simply set the string to "true" or "false" depending on the desired value, and compare to the string "true" to test the value.

Adding Initialization Code

Rather than directly adding an initializer, Plugins should add their initialization code through the Concerto Plugin API. It will be run in the Concerto Plugins initializer as long as the plugin is enabled.

Open lib/my_plugin/engine.rb, and add a section like the following inside the plugin_info_class.new block (which we created in the Creating a Plugin tutorial above).

        # Some code to run at app boot
        init do
          Rails.logger.info "MyPlugin: Initialization code is running"
        end

You can put arbitrary code in the init block. There can only be one init block per engine. The code will be run in the root context, meaning that any references to classes in your plugin will need to explicitly reference them through the MyPlugin:: namespace.

Adding Hooks

Hooks allow your plugin to integrate with the main Concerto Interface. The hook architecture and the Concerto side of things is discussed on a separate page. This section deals with how to attach to a Concerto hook from your plugin.

There are two types of hooks, controller hooks and view hooks. A common case would be to fetch some data at the controller stage using a controller hook, and then to display it somewhere in the Concerto UI using a view hook.

Controller Hooks

Controller hook code is specified as a block for a particular hook within a particular controller. To attach to a hook called :show defined in the ScreensController, simply add the following to the plugin_info_class.new block your engine.rb.

        add_controller_hook "ScreensController", :show, :before do
          @post = MyPlugin::Post.find_by_screen_id(@screen.id)
        end

Note the the hook name is not always going to be the same as the action name; in fact there may be multiple hooks per action. Each plugin can add as many blocks as it likes to any hook.

The code inside the hook is executed as a callback in the context of the specified controller. It has access to any instance variables defined so far, and any it creates or modifies will be available to the rest of the action and to the corresponding view. Explicitly scoping references to your constants to your plugin's module is recommended.

If code is added for a controller name or hook name that does not exist, the block will simply be ignored.

View Hooks

View hooks specify code to be rendered into a view at a particular point. Like controller hooks, you will need to specify a controller name and a hook name. The hook names are scoped to the controller, so the name may not match a particular view and in fact the hook may be displayed in more than one view.

One way to specify your view hook code is as a block that returns a string.

        add_view_hook "ScreensController", :screen_details do
          "<p><b>Name via View Hook:</b> "+@screen.name+"</p>"
        end

(:screen_details is the defined name of a hook in the show page for Screens in Concerto)

Another way is to create a partial. The partial simply needs to exist somewhere in your plugin, for example at my_plugin/app/views/my_plugin/screens/_screen_link.html.erb.

        add_view_hook "ScreensController", :screen_details, :partial => "my_plugin/screens/screen_link"

Finally, if the string is known in advance (that is, no variables or interpolation are needed), you can insert static text directly:

        add_view_hook "ScreensController", :screen_details, :text => "<p><b>All systems:</b> go</p>"

With both the block and partial methods (but not static text), instance variables and helpers available to the view are available to the partial (and the partial can modify them, so be careful!). However, there is one important caveat: linking.

Linking from view hooks

As we said, the context is the Concerto view, not your plugin, so your plugin's normal routing helpers are not available. Simply writing <% link_to 'Post', @post %> will not work! To link to something in your plugin, you need to use the routing helper that Rails creates for your plugin. The name of the routing helper is the engine_name we specified in engine.rb earlier. Here are some examples for worry-free linking:

<%= link_to 'All Posts', my_plugin.posts_path %>
<%= link_to @post.name, [my_plugin, @post] %>
<%= form_for(my_plugin, @post) do |f| %>

Adding Authorization

Authentication comes built-in: the current accessor is stored in current_user and is accessible the same way it would be in the main application.

The main application's authorization rules also come built-in. You can check, for example, whether the current user is allowed to edit a particular screen just like if you were in the main Concerto app.

In addition, you will likely need to protect resources served up by your plugin. To add custom authorization rules, open a new file at my_plugin/app/models/my_plugin.rb. Use this file to extend Concerto's ability class into a new class for your own controller. For a very simple example:

module MyPlugin
  # The enigne's Ability class simply extends the existing Ability
  # class for the application. We rely on the fact that it already
  # includes CanCan::Ability.
  class Ability < ::Ability
    def initialize(accessor)
      super # Get the main application's rules
      # Note the inherited rules give Admins rights to manage everything

      # For now lets make all Posts readable
      can :read, Post
    end
  end # class Ability
end # module MyPlugin

You can look at Concerto's Ability.rb for more ideas on adding more complicated rules to target specific users and groups.

The rules defined here will be available to your views, view hooks, and controllers (though not controller hooks). They are not available to the main application.

To add authorization to a controller, add the auth! method into all of the actions after fetching the resources, just like you would do in a normal Concerto controller.

To add authorization to a View, or a partial which will be included as a view hook, simply proceed as you normally would with CanCan. For example, in a view,

    <% if can? :update, @post %>
      (<%=link_to "edit", edit_post_path(@post) %>)
    <% end %>

Refer to the CanCan documentation on this topic for more examples.

Adding Public Activity Tracking

In your model, include the PublicActivity::Common class.

    include PublicActivity::Common if defined? PublicActivity::Common

In your controller, call the process_notification method, specifying the key as (shown below).

    # POST /schedules
    # POST /schedules.json
    def create
      @schedule = Schedule.new(schedule_params)
      auth! :action => :update, :object => @schedule.screen
      respond_to do |format|
        if @schedule.save
          process_notification(@schedule, 
            {:screen_id => @schedule.screen_id, 
              :screen_name => @schedule.screen.name,
              :template_id => @schedule.template.id, 
              :template_name => @schedule.template.name }, 
            :key => 'concerto_template_scheduling.schedule.create', 
            :owner => current_user, 
            :action => 'create')

          format.html { redirect_to @schedule, notice: 'Schedule was successfully created.' }
          format.json { render json: @schedule, status: :created, location: @schedule }
        else
          format.html { render action: "new" }
          format.json { render json: @schedule.errors, status: :unprocessable_entity }
        end
      end
    end

If you don't specify the key, it will append the table name to the plugin name, making the naming of the view paths cumbersome. Create the views in your plugin under app/views/public_activity/pluginname/tablename as _action.html.erb. A sample create action view is shown below.

<% public_owner = User.find(activity.owner) %>
<%= link_to public_owner.name, public_owner %> <%=t('.scheduled_template')%>  
<% if ::Template.exists?(activity.parameters[:template_id]) %>
  <%= link_to activity.parameters[:template_name], main_app.template_path(activity.parameters[:template_id]) %> 
<% else %>
  <%= activity.parameters[:template_name] %> 
<% end %>
<% if activity.trackable && defined?(template_scheduling) %>
  <%= t('concerto_template_scheduling.starting') %> 
  <%= link_to l(activity.trackable.start_time.to_date, :format => :short), template_scheduling.schedule_path(activity.trackable) %>
<% end %>
  <%= t('concerto_template_scheduling.for') %> 
<% if Screen.exists?(activity.parameters[:screen_id]) %>
  <%= link_to activity.parameters[:screen_name], main_app.screen_path(activity.parameters[:screen_id]) %> 
<% else %>
  <%= activity.parameters[:screen_name] %> 
<% end %>

Notice the check for defined?(template_scheduling) to determine whether or not the plugin is enabled. This prevents problems arising from referring to the plugin's routes (such as template_scheduling.schedule_path(activity.trackable)) when the plugin is not loaded.

Extending Models

If you need to extend existing concerto models, e.g. to add a has_many relation for a new model of your plugin, you can do it using ActiveSupport::Concern and extend_model

Each concern extension must be listed in the plugin_info_class.new that we added to your plugin's engine.rb in the steps above. The format for specifying a configuration object is as follows:

extend_model(Screen, ConcertoCustomPlugin::Concerns::AddCustomizationToScreen)

The first argument is the model you want to extend. The second argument is the concern that defines the extended behaviour.

Here is an example of a simple concern that adds a has_many relation to the screen.

module ConcertoCustomPlugin
  module Concerns
    module AddCustomizationToScreen
      extend ActiveSupport::Concern
      included do
        has_many :things, :class_name => 'ConcertoCustomPlugin::Thing', :dependent => :destroy
      end
    end
  end
end

Of course a concern is capable of doing much more (you can find more at rubyonrails.org), but make sure that you do not overwrite existing behaviour, otherwise you might break other plugins

Add cron jobs

If you need your plugin to perform some recurring tasks, you can add your own cron jobs to concerto.

Each job must be listed in the plugin_info_class.new that we added to your plugin's engine.rb in the steps above. The format for specifying a configuration object is as follows:

perform_job_every 1.day, "Hello", :at => "00:00" do
    puts "Hello new day"
end

The first argument is recurring period of the task, the second argument is the name of your task. The third argument is optional, and allows you to define certain options when the task should be run. You can find all available options at clockwork. Finally you can provide a block that is executed every time at your configured time

Something went wrong with that request. Please try again.