Developing a new module of Gobierto

Gobierto modules implement well defined and isolated features of the application. Examples of modules are:

  • Gobierto Budgets: municipalities budgets visualization.
  • Gobierto People: senior officials official information and agenda publication.
  • Gobierto Observatory: indicators and statics of a municipality.
  • Gobierto Plans: plans

Modules can be activated or disabled by the manager, but their code will be included in all the installations of Gobierto.

This page describes the steps needed to create a new module and integrate it in the application. After following this steps don't forget to review the module checklist at the end of this section.

Module basic attributes

Every module has a name and defines a namespace. These attributes are defined in config/application.yml:

default: &default
  site_modules:
    -
      name: Gobierto Development
      namespace: GobiertoDevelopment
    -
      name: Gobierto Budgets
      namespace: GobiertoBudgets

When you create a module, you must define the root_path in config/routes.rb

# Gobierto People module
namespace :gobierto_people, path: "/" do
  constraints GobiertoSiteConstraint.new do
    get "cargos-y-agendas" => "welcome#index", as: :root
  end
end

Also, in the module file you should define a method self.root_path, which receives the current site as an argument:

def self.root_path(current_site)
  if current_site.gobierto_budgets_settings && current_site.gobierto_budgets_settings.settings["budgets_elaboration"]
    Rails.application.routes.url_helpers.gobierto_budgets_budgets_elaboration_path
  else
    Rails.application.routes.url_helpers.gobierto_budgets_budgets_path(GobiertoBudgets::SearchEngineConfiguration::Year.last)
  end
end

Nowadays, we have 5 modules (Gobierto Budgets, Gobierto People, Gobierto Participation, Gobierto Observatory and Gobierto Indicators) that have root_path to be the home page.

default: &default
  site_modules_with_root_path:
    -
      name: Gobierto Budgets
      namespace: GobiertoBudgets
    -
      name: Gobierto People
      namespace: GobiertoPeople
    -
      name: Gobierto Participation
      namespace: GobiertoParticipation
    -
      name: Gobierto Observatory
      namespace: GobiertoObservatory
    -
      name: Gobierto Indicators
      namespace: GobiertoIndicators
    -
      name: Gobierto Plans
      namespace: GobiertoPlans

This namespace is applied to models, assets, helpers, controllers, views, I18n keys, tests, and the routes. This guide covers the steps you need to follow on each of those resources to enable the new module.

If the module needs special configuration, create an entry in the application.yml file with the name of the module:

  gobierto_budgets:
    data_note_url: https://presupuestos.gobierto.es/about#method
  gobierto_people:
    gifts_service_url: <%= ENV["PEOPLE_GIFTS_SERVICE_URL"] %>
    travels_service_url: <%= ENV["PEOPLE_TRAVELS_SERVICE_URL"] %>

Models

If your module implements models, create a folder to store them with the module name, and remember to declare the class under the Ruby module name.

For example:

# app/models/gobierto_people/person.rb

require_dependency "gobierto_people"

module GobiertoPeople
  class Person < ApplicationRecord
    include ::GobiertoCommon::DynamicContent
    include User::Subscribable
...

In case your model implements a relationship with other model outside the module, you shouldn't include the module name in the name of the relationship for readability reasons, unless there is a name conflict.

Example:

# preferred way
class User < AppplicationRecord
   has_many :posts, class_name: 'GobiertoPublications::Post'
end

vs.

# we don't like this
class User < AppplicationRecord
   has_many :gobierto_publications_posts
end

Define a table name prefix for your module in app/models/<name of your module>. For example:

# app/models/gobierto_people.rb

module GobiertoPeople
  def self.table_name_prefix
    'gp_'
  end
end

In the module you can declare the following class methods:

  • table_name_prefix
  • classes_with_vocabularies
  • classes_with_custom_fields
  • searchable_models
  • module_submodules
  • custom_engine_resources
  • doc_url

If you create unit tests, define them in a folder with the module name, i.e. test/models/gobierto_people/person_test.rb

Controllers

Declare your controllers in a specific folder for the module, and declare your classes under the Ruby module namespace.

Also, declare an ApplicationController inside the new module, to define the layout.

For example:

# app/controllers/gobierto_people/application_controller.rb

module GobiertoPeople
  class ApplicationController < ::ApplicationController
    include User::SessionHelper

    layout "gobierto_people/layouts/application"
  end
end

Admin controllers

The new module might have admin actions. Declare these actions in controllers under GobiertoAdmin module. Each admin controller needs to be protected by two filters:

  • module_enabled!: checks if the module is enabled in the site configuration
  • module_allowed!: checks if the current admin has permissions on the current module

For example:

before_action { module_enabled!(current_site, "GobiertoBudgetConsultations") }
before_action { module_allowed!(current_admin, "GobiertoBudgetConsultations") }

In GobiertoAdmin::BaseController a default_modules_home_paths helper method is defined with the default home path of each module. Regular admins are redirected to the home path of their first module enabled and these paths are also used to generate the links for each module in the application layout.

# app/controllers/gobierto_admin/base_controller.rb

def default_modules_home_paths
  @default_modules_home_paths ||= {
    # ...
    gobierto_people: admin_people_people_path,
    # ...
  }.with_indifferent_access
end

Views

Declare your views in a specific folder for the module. Include a layouts folder to define the module layout. Use the nested layout syntax of Rails.

At least include:

  1. The javascript application file of your module
  2. Custom breadcrumb items
  3. A render to the main layout
# app/views/gobierto_people/layouts/application.html.erb

<% content_for :javascript_module_link do %>
  <%= javascript_packs_with_chunks_tag 'module', 'data-turbolinks-track' => true  %>
<% end %>

<% content_for :stylesheet_module_link do %>
  <%= stylesheet_packs_with_chunks_tag 'module', 'data-turbolinks-track' => true  %>
<% end %>

<%= render template: "layouts/application" %>

Also, you need to define a couple of files for the menus:

  • _navigation.main.html.erb

  • _navigation.sub.html.erb

Admin menu

If the admin has the module enabled and an entry is included in the GobiertoAdmin::BaseController#default_modules_home_paths described in the previous section, a new item is added automatically to the admin menu. Remember to add translations for the module name (see I18n keys section).

Also you can create manually a new link adding the necessary permissions in app/views/gobierto_admin/layouts/application.html.erb. Example:

# app/views/gobierto_admin/layouts/application.html.erb

<% if managing_site? %>
  <li>
    <%= link_to t('.edit_site'), edit_admin_site_path(current_site) %>
      <ul>
        <% if current_admin.can_edit_vocabularies? %>
          <li><%= link_to t(".vocabularies"), admin_common_vocabularies_path %></li>
        <% end %>
      </ul>
  </li>
<% end %>

Routes

Module routes must be declared under a namespace specific for the module. Example:

namespace :gobierto_budgets, path: '', module: 'gobierto_budgets' do
  constraints GobiertoSiteConstraint.new do
    get 'site' => 'sites#show'
    ...
  end
end

Exports

GobiertoExports is a module that exposes a page where the user can download data in a reusable format (JSON and CSV). This page is composed by the exports exposed by each module. If your module wants to expose some exports in this page you need to add a folder named exports inside your module views folder with two partials:

  • _nav_item.html.erb with the link to include in the module submenu, for example:
<%= link_to t("gobierto_people.layouts.application.title"), gobierto_exports_root_path(anchor: 'section-people') %>
  • _index.html.erb containing the html to include in the open data page, for example:
<div class="pure-g data_block" id="section-people">

  <div class="pure-u-1 pure-u-md-7-24">
    <h2><%= t("gobierto_people.layouts.application.title") %></h2>
    <p></p>
  </div>

  <div class="pure-u-1 pure-u-md-17-24 main_content">

    <div class="data_item">
      <h3><%= t("gobierto_people.exports.index.people.title") %></h3>
      <%= link_to 'CSV', gobierto_people_people_path(format: :csv), class: 'button small' %>
      <%= link_to 'JSON', gobierto_people_people_path(format: :json), class: 'button small' %>
      <p><%= t("gobierto_people.exports.index.people.description") %></p>
    </div>

    <div class="data_item">
      <h3><%= t("gobierto_people.exports.index.events.title") %></h3>
      <%= link_to 'CSV', gobierto_people_events_path(format: :csv), class: 'button small' %>
      <%= link_to 'JSON', gobierto_people_events_path(format: :json), class: 'button small' %>
      <p><%= t("gobierto_people.exports.index.events.description") %></p>
    </div>

  </div>

</div>

Expose some endpoints in json and csv format, you can use a block like this

respond_to do |format|
  format.html
  format.json { render json: @events }
  format.csv  { render csv: GobiertoExports::CSVRenderer.new(@events).to_csv, filename: 'events' }
end

For the json format put your module serializers into app/serializers/<module_name>.

For the csv define use the GobiertoExports::CSVRenderer with a relation and define two methods in the corresponding module, a class method named csv_columns that returns the csv headers and an instance method named as_csv that returns that record as an array of values to include in th csv.

Example:

def self.csv_columns
  [:id, :name, :email, :charge, :bio, :bio_url, :avatar_url, :category, :political_group, :party, :created_at, :updated_at]
end

def as_csv
  political_group_name = political_group.try(:name)

  [id, name, email, charge, bio, bio_url, avatar_url, category, political_group_name, party, created_at, updated_at]
end

Activities

Gobierto has an activities log that save the admin events in the application. For example, to save this
information we must generate into app/pub_sub/ the publisher and the subscriber to events like
created page, updated page or deleted page.

module Publishers
  class GobiertoCmsPageActivity
    include Publisher

    self.pub_sub_namespace = 'activities/gobierto_cms_pages'
  end
end
module Subscribers
  class GobiertoCmsPageActivity < ::Subscribers::Base
    def page_created(event)
      create_activity_from_event(event, 'gobierto_cms.page_created')
    end

    def page_updated(event)
      create_activity_from_event(event, 'gobierto_cms.page_updated')
    end

    def page_deleted(event)
      create_activity_from_event(event, 'gobierto_cms.page_deleted')
    end

    private

    def create_activity_from_event(event, action)
      Activity.create! subject: event.payload[:subject],
                       author: event.payload[:author],
                       subject_ip: event.payload[:ip],
                       action: action,
                       site_id: event.payload[:site_id],
                       admin_activity: true
    end
  end
end

If you want the activities to be viewed from the "Activity log", you should make
sure to mark:

admin_activity: true

These events have to be called from the controller with track_create_activity:

# app/controllers/gobierto_admin/gobierto_participation/issues_controller.rb

module GobiertoAdmin
  module GobiertoParticipation
    class IssuesController < BaseController

      ...

      def create
        @issue_form = IssueForm.new(issue_params.merge(site_id: current_site.id))

        if @issue_form.save
          track_create_activity

          redirect_to(
            admin_issues_path(@issue),
            notice: t(".success")
          )
        else
          render :new_modal, layout: false and return if request.xhr?
          render :new
        end
      end

      ...

      private

      def track_create_activity
        Publishers::GobiertoParticipationIssueActivity.broadcast_event("issues.issue_created", default_activity_params.merge({subject: @issue_form.issue}))
      end
      ...
    end
  end
end

Assets

Stylesheets

Declare a file module-<name of your module>.scss in the Rails folder app/assets/stylesheets.

Javascripts

Create a specific folder for your module, and create an application.js file. If the module needs vendor libraries add them to vendor/assets/javascripts.

I18n keys

Declare a folder for the module in config/locales/, but only controllers and views can be defined on it. Models and routes must be defined outside the module (this is because how Rails uses the namespace of I18n).

To add module name translations to be used in admin menu layout use the module name removing the "gobierto_" prefix as key. For example, for the gobierto_people module just add:

# config/locales/gobierto_admin/views/layouts/en.yml
    modules:
      people: Officers and agendas
    notifications: Notifications

Migrations

In case your module needs migrations, every migration you define needs to be preffixed with the namespace. Example:

rails g migration gobierto_budgets_add_budget_line_description

Tests

There are many type of tests, just create a subfolder with the name of the module depending on the type of test.

Admin permissions

In app/models/gobierto_admin/permission/ a subclass of Admin::Permission will be implemented, defining a scope, which will limit the access.

Example:

module GobiertoAdmin
  class Permission::GobiertoBudgetConsultations < Permission
    default_scope -> do
      where(namespace: "site_module", resource_name: "gobierto_budget_consultations")
    end
  end
end

Seeds

Sometimes modules need some database data to exist. For example, a configuration entry, a list of DynamicContentBlocks preconfigured. For that reason, we have created a seeds structure and two seeds runner classes:

  • ModuleSeeder: seeds a module
  • ModuleSiteSeeder: seeds a module for a specific Gobierto installation. It uses the attribute site.name from config/application.yml

Both seed types can be found in db/seeds/modules and db/seeds/sites.

These seeds are executed when a module is activated in a site. De-activating the module doesn't run the seeder. Keep this in mind in order to write idempotent scripts.

Here's a template of the seed class:

module GobiertoSeeds
  class Recipe
    def self.run(site)
      # Your code goes here
    end
  end
end

Notifications

Does the new module expose resources to be subscribed to? Examples of subscribable resources are people agendas, or a participatory process. If so you need to add it to the constant MODULES_WITH_NOTIFICATIONS.

Search concern

If your model implements any resource that should be searchable, you just need expose the entities in the main module model under searchable_models class method.

For example, GobiertoPeople module exposes these models:

  def self.searchable_models
    [ GobiertoPeople::Person, GobiertoPeople::PersonPost, GobiertoPeople::PersonStatement ]
  end

Once made a model searchable, include the translations of the model names in the module app/javascript/lib/shared/modules/module-search.js

switch(d['class_name']){
      case 'GobiertoPeople::Person':
        return I18n.t("layouts.search.person_item");
      case 'GobiertoPeople::PersonPost':
        return I18n.t("layouts.search.person_post_item");
    }

Module checklist

When you implement a new module, don't forget to check it against this list of items to verify the implementation is complete:

  • routes: did you add a root path for the module?
  • layout: is the module properly integrated in the layout and the navigation menus?
  • application configuration: did you enabled the module in the application.yml? Did you update this file in Ansible?
  • scoping: did you scope the database tables and the Ruby code in the proper scopes?
  • admin permissions: did you configure permission for regular admins?
      • module configured: did you protect the module actions with the module_enabled filter?
  • javascript: did you create a new javascript module?
  • data export: is the module generating data that should be exportable? Did you include it in the exports sections?
  • admin sidebar: is the module properly distributed in the admin sidebar?
  • admin activities: does the module generates comprehensive activities for admins?
  • documentation: have you documented the module in the Admin manual?
  • search: are there items in the module that should be enabled for search?
  • seeds: did you add new values for this module in the seeds?
  • doc: does the module has documentation? Have you linked it in the doc_url method of the module?