ActiveManageable
ActiveManageable provides a framework from which to create business logic "manager" classes in your Ruby on Rails application. Thus extending the MVC pattern to incorporate a business logic layer that sits between the controllers and models.
Moving your busines logic into a separate layer provides benefits including:
- skinny controllers & models
- reusable code that reduces duplication across application & API controllers and background jobs
- isolated unit tests for the business logic, allowing system & integration tests to remain true to their purpose of testing user interaction and the workflow of the application
- clear separation of concerns with controllers responsible for managing requests, views dealing with presentation and models handling attribute level validation and persistence
- clear & consistent interface
ActiveManageable business logic manager classes
- include methods for the seven standard CRUD actions: index, show, new, create, edit, update, and destroy
- can be configured to incorporate authentication, search and pagination logic
- enable specification of the associations to eager load, default attribute values, scopes & order when retrieving records and more
- perform advanced parsing of parameter values for date/datetime/numeric attributes
To show how ActiveManageable manager classes can be used to create DRY code in skinny controllers, we’ll refactor the following controller index method that retrieves records with an eager loaded association using Pundit, Ransack & Kaminari.
def index
search = policy_scope(User).ransack(params[:q])
search.sorts = "name asc" if q.sorts.empty?
authorize(User)
@users = search.result.includes(:address).page(params[:page])
endWith ActiveManageable configured to use the Pundit, Ransack & Kaminari libraries, the following manager class includes the standard CRUD methods and sets the default order and association to eager load in the index method.
class UserManager < ActiveManageable::Base
manageable ActiveManageable::ALL_METHODS
default_order :name
default_includes :address, methods: :index
endUsing the manager class, the controller index method can now be rewritten to only include a single call to the index method.
def index
@users = UserManager.new.index(options: {search: params[:q], page: {number: params[:page]}})
endThe manager classes provide standard implementations of the seven core CRUD methods. These can be extended or overwritten to perform custom business logic and the classes can also be extended to include the business logic for additional actions, both making use of the internal ActiveManageable methods and variables described in the Adding Bespoke Methods section.
With an Activity model in a CRM application to manage meetings & tasks, a complete action may be required. This could be implemented as follows:
class ActivityManager < ActiveManageable::Base
manageable ActiveManageable::ALL_METHODS
def complete(id:)
initialize_state
@target = model_class.find(id)
authorize(record: @target, action: :complete?)
@target.update(completed_by: current_user.id, completed_at: Time.zone.now)
end
endThe controller method can then call the manager method, retrieve the activity that was completed and act on the result.
def complete
result = manager.complete(id: params[:id])
@activity = manager.object
# now redirect based on the result
endInstallation
Add this line to your application's Gemfile:
gem 'active_manageable'And then execute:
bundle installOr install it yourself as:
gem install active_manageableTable of Contents
- Configuration
- Current User
- Authorization
- Class Definition
- Manageable Method
- Default Includes
- Default Attribute Values
- Default Select
- Default Order
- Default Scopes
- Unique Search
- Default Page Size
- Paginate Without Count
- Current Method
- Index Method
- Authorization Scope
- Search Option
- Page Option
- Order Option
- Scopes Option
- Includes Option
- Select Option
- Distinct
- Show Method
- Includes Option
- Select Option
- New Method
- Create Method
- Edit Method
- Includes Option
- Update Method
- Includes Option
- Destroy Method
- Includes Option
- Extending the CRUD Methods
- Build and Retrieve Scoped Records
- Attribute Value Parsing
- Date and DateTime Attribute Values
- Numeric Attribute Values
- ActiveManageable Attributes
- Adding Bespoke Methods
- Development
- Contributing
- License
- Code of Conduct
Configuration
Create an initializer to configure the optional in-built authorization, search and pagination libraries to use.
ActiveManageable.config do |config|
config.authorization_library = :pundit # or :cancancan
config.search_library = :ransack
config.pagination_library = :kaminari
endThese library configuration options can also be set to a module in order to use a custom authorization, search or pagination implementation.
When eager loading associations the includes method is used by default but this can be changed via a configuration option that accepts :includes, :preload or :eager_load
ActiveManageable.config do |config|
config.default_loading_method = :preload
endActiveManageable will attempt to determine the model class to use based on the class name and subclass_suffix configuration option. So if the class is named "AlbumManager" and an Album constant exists that will be used as the model class. If you want to use a suffix other than "Manager", the configuration option can be changed or alternatively each class can specify the model class to use when calling the manageable method.
ActiveManageable.config do |config|
config.subclass_suffix = "Concern"
endclass BusinessLogic < ActiveManageable::Base
manageable ActiveManageable::ALL_METHODS, model_class: Album
endCurrent User
ActiveManageable uses its own current_user per-thread module attribute when performing authorization with one of the configuration libraries. This needs to be set before using its methods, for example in an ApplicationController filter.
around_action :setup_request
def setup_request
ActiveManageable.current_user = current_user
yield
ActiveManageable.current_user = nil
endThe current_user can also be set or overridden for a block using the with_current_user method.
manager = AlbumManager.new
manager.with_current_user(user) do
manager.show(id: 1)
endAnd is accessible via an instance method.
manager = AlbumManager.new
manager.current_userAuthorization
When using one of the configuration authorization libraries, each of the methods will perform authorization for the current user, method and either model class or record. If authorization fails an exception will be raised so you may choose to rescue the relevant exception.
Pundit - Pundit::NotAuthorizedError
CanCanCan - CanCan::AccessDenied
Class Definition
Manageable Method
Create a class that inherits from ActiveManageable::Base then use the manageable method to specify which methods should be included. Use the ActiveManageable::ALL_METHODS constant to include all methods (ie. :index, :show, :new, :create, :edit, :update and :destroy) or pass the required method name symbol(s).
class AlbumManager < ActiveManageable::Base
manageable ActiveManageable::ALL_METHODS
endclass SongManager < ActiveManageable::Base
manageable :index, :show
endDefault Includes
The default_includes method sets the default associations to eager load when fetching records in the index, show, edit, update and destroy methods. These defaults are only used if the :options argument for those methods does not contain a :includes key.
class AlbumManager < ActiveManageable::Base
manageable ActiveManageable::ALL_METHODS
default_includes :songs
endIt accepts a single, array or hash of association names, optional :methods in which to eager load the associations and optional :loading_method if this needs to be different to the configuration :default_loading_method. It also accepts a lambda/proc to execute to return associations with optional :methods.
default_includes :songs, :artist, methods: [:index, :show]
default_includes songs: :artist, loading_method: :preload, methods: [:edit, :update]
default_includes -> { destroy_includes }, methods: :destroy
def destroy_includes
[:songs, {artist: :songs}]
endDefault Attribute Values
The default_attribute_values the default attribute values to use when building a model object in the new and create methods. These defaults are combined with the attribute values from :attributes argument for those methods. When default and argument values contain the same attribute key, the value from the argument is used.
class AlbumManager < ActiveManageable::Base
manageable ActiveManageable::ALL_METHODS
default_attribute_values genre: "pop"
endIt accepts either a hash of attribute values or a lambda/proc to execute to return a hash of attribute values and optional :methods in which in which to use the attribute values.
default_attribute_values genre: "pop", released_at: Date.current, methods: :new
default_attribute_values -> { create_attrs } , methods: :create
def create_attrs
{genre: "electronic", published_at: Date.current}
endDefault Select
The default_select method sets the attributes to return in the SELECT statement used when fetching records in the index, show and edit methods. These defaults are only used if the :options argument for those methods does not contain a :select key.
class AlbumManager < ActiveManageable::Base
manageable ActiveManageable::ALL_METHODS
default_select :id, :name
endIt accepts either an array of attribute names or a lambda/proc to execute to return an array of attribute names and optional :methods in which to use the attributes.
default_select :id, :name, :genre, methods: :show
default_select -> { select_attributes }, methods: [:index, :edit]
def select_attributes
[:id, :name, :genre, :released_at]
endDefault Order
The default_order method sets the default order to use when fetching records in the index method. These defaults are only used if the :options argument for the method does not contain an :order key.
class AlbumManager < ActiveManageable::Base
manageable ActiveManageable::ALL_METHODS
default_order :name
endIt accepts attributes in the same formats as the ActiveRecord order method or a lambda/proc to execute to return attributes in the recognised formats.
default_order "name DESC"default_order [:name, :id]default_order -> { order_attributes }
def order_attributes
["name DESC", "id"]
endDefault Scopes
The default_scopes method sets the default scope(s) to use when fetching records in the index method. These defaults are only used if the :options argument for the method does not contain a :scopes key.
class AlbumManager < ActiveManageable::Base
manageable ActiveManageable::ALL_METHODS
default_scopes :electronic
endIt accepts a scope name, a hash containing scope name and argument, or an array of names/hashes. It also accepting a lambda/proc to execute to return a scope name, hash or array.
default_scopes {released_in_year: "1980"}default_scopes :rock, :electronic, {released_in_year: "1980"}default_scopes -> { index_scopes }
def index_scopes
[:rock, :electronic]
endUnique Search
The has_unique_search method specifies whether to use the distinct method when fetching records in the index method.
class AlbumManager < ActiveManageable::Base
manageable ActiveManageable::ALL_METHODS
has_unique_search
endIt accepts no argument to always return unique records or a hash with :if or :unless keyword and a method name or lambda/proc to execute each time the index method is called.
has_unique_search if: :method_namehas_unique_search unless: -> { lambda }Default Page Size
When using the Kaminari pagination library, the default_page_size method sets default page size to use when fetching records in the index method. The default is only used if the :options argument for the method does not contain a :page hash with a :size key.
class AlbumManager < ActiveManageable::Base
manageable ActiveManageable::ALL_METHODS
default_page_size 5
endPaginate Without Count
When using the Kaminari pagination library, the paginate_without_count method will result in the index method using the Kaminari without_count mode to create a paginatable collection without counting the total number of records.
class AlbumManager < ActiveManageable::Base
manageable :index
paginate_without_count
endIt is also possible to control whether to use the without_count mode globally by setting the paginate_without_count configuration option.
ActiveManageable.config do |config|
config.paginate_without_count = true
endAnd it is also possible to control whether to use the without_count mode for an individual index method call by including the :without_count key within the options argument :page hash.
manager.index(options: {page: {number: 2, size: 10, without_count: true}})Current Method
ActiveManageable includes a current_method attribute which returns the name of the method being executed as a symbol, which can potentially be used within methods in conjunction with a lambda for the default methods described above. Additionally, the method argument options and attributes are also accessible as attributes.
default_includes -> { method_includes }
def method_includes
case current_method
when :index
{songs: :artist}
when :show
[:label, :songs]
when :edit, :update
options.key?(:xyz) ? [:label, songs: :artists] : [:label, :songs]
else
:songs
end
endIndex Method
The index method has an optional options keyword argument. The options hash can contain :search, :order, :scopes, :page, :includes and :select keys. The method performs authorization for the current user, method and model class using the configuration library; invokes a block (if provided); retrieves records using the various options described below; and returns the records which are also accessible via the collection attribute.
manager.indexIndex Authorization Scope
When using one of the configuration authorization libraries, the method retrieves records that the current user is authorized to access. For the Pundit authorization library, the method retrieves records filtered using the model's policy scope. For the CanCanCan authorization library, the method retrieves records filtered using the accessible_by scope for the current user's ability.
Index Search Option
When using the Ransack search library, the options argument :search key is used to set the Ransack filter and sorting. If either the :search key or its sorts :s key is not present, the method will order the records using the standard approach described below. The Ransack search object is accessible via the ransack attribute.
manager.index(options: {search: {artist_id_eq: 1, s: "name ASC"}})
ransack_search = manager.ransackIndex Page Option
When using the Kaminari pagination library, the options argument :page hash is used to set the page number and size of records to retrieve. The page number is set using the :number key value and page size is set using the :size key value. If the :size key is not present, the class default is used and if a class default has not been set then the Kaminari application default is used.
manager.index(options: {page: {number: 2, size: 10}})Index Order Option
The options argument :order key provides the ability to specify the order in which to retrieve records and accepts attributes in the same formats as the ActiveRecord order method. When the :order key is not present, any class defaults are used.
manager.index(options: {order: "name DESC"})Index Scopes Option
The options argument :scopes key provides the ability to specify the scopes to use when retrieving records and accepts a scope name, a hash containing scope name and argument, or an array of names/hashes. When the :scopes key is not present, any class defaults are used.
manager.index(options: {scopes: :electronic})manager.index(options: {scopes: {released_in_year: "1980"}})manager.index(options: {scopes: [:rock, :electronic, {released_in_year: "1980"}]})Index Includes Option
The options argument :includes key provides the ability to specify associations to eager load and accepts associations names in the same formats as the AR includes method eg. a single association name, an array of names or a hash of names. When the :includes key is not present, any class defaults are used.
manager.index(options: {includes: [:artist, :songs]})The :includes key can also be used to vary the method used to eager load associations by providing :associations and :loading_method keys. When the :loading_method key is not present the method will use either the class default method (set using default_includes) or the configuration default_loading_method.
manager.index(options: {includes: {associations: :songs, loading_method: :preload}})Index Select Option
The options argument :select key provides the ability to limit the attributes returned in the SELECT statement. When the :select key is not present, any class defaults are used.
manager.index(options: {select: [:id, :name, :artist_id, :released_at]})Index Distinct
If the class has_unique_search method has been used then this will be evaluated to determine whether to use the distinct method when fetching the records.
Show Method
The show method has id and optional options keyword arguments. The options hash can contain :includes and :select keys. The method invokes a block (if provided); retrieves a record; performs authorization for the current user, method and record using the configuration library; and returns the record which is also accessible via the object attribute.
manager.show(id: 1)Show Includes Option
The options argument :includes key provides the ability to specify associations to eager load and accepts associations names in the same formats as the AR includes method eg. a single association name, an array of names or a hash of names. When the :includes key is not present, any class defaults are used.
manager.show(id: 1, options: {includes: [:artist, :songs]})The :includes key can also be used to vary the method used to eager load associations by providing :associations and :loading_method keys. When the :loading_method key is not present the method will use either the class default method (set using default_includes) or the configuration default_loading_method.
manager.show(id: 1, options: {includes: {associations: :songs, loading_method: :preload}})Show Select Option
The options argument :select key provides the ability to limit the attributes returned in the SELECT statement. When the :select key is not present, any class defaults are used.
manager.show(id: 1, options: {select: [:id, :name, :artist_id, :released_at]})New Method
The new method has an optional attributes keyword argument. The attributes argument is for an ActionController::Parameters or hash of attribute names and values to use when building the record. The method builds a record; performs authorization for the current user, method and record using the configuration library; invokes a block (if provided); and returns the record which is also accessible via the object attribute.
manager.newThe attributes argument values are combined with the class default values and when the default and argument values contain the same attribute key, the value from the argument is used.
manager.new(attributes: {genre: "electronic", published_at: Date.current})Create Method
The create method has an attributes keyword argument. The attributes argument is for an ActionController::Parameters or hash of attribute names and values to use when building the record. The method builds a record; performs authorization for the current user, method and record using the configuration library; invokes a block (if provided) and attempts to save the record within a transaction; and returns the save result. The record is also accessible via the object attribute.
manager.create(attributes: {name: "Substance", genre: "electronic", published_at: Date.current})The attributes argument values are combined with the class default values and when the default and argument values contain the same attribute key, the value from the argument is used.
Edit Method
The edit method has id and optional options keyword arguments. The options hash can contain :includes and :select keys. The method invokes a block (if provided); retrieves a record; performs authorization for the current user, method and record using the configuration library; and returns the record which is also accessible via the object attribute.
manager.edit(id: 1)Edit Includes Option
The options argument :includes key provides the ability to specify associations to eager load and accepts associations names in the same formats as the AR includes method eg. a single association name, an array of names or a hash of names. The :select key provides the ability to limit the attributes returned in the SELECT statement. When the :includes and :select keys are not present, any class defaults are used.
manager.edit(id: 1, options: {includes: [:artist, :songs], select: [:id, :name, :artist_id, :released_at]})The :includes key can also be used to vary the method used to eager load associations by providing :associations and :loading_method keys. When the :loading_method key is not present the method will use either the class default method (set using default_includes) or the configuration default_loading_method.
manager.edit(id: 1, options: {includes: {associations: :songs, loading_method: :preload}})Update Method
The update method has id, attributes and optional options keyword arguments. The attributes argument is for an ActionController::Parameters or hash of attribute names and values to use when updating the record. The options hash can contain an :includes key. The method retrieves a record; performs authorization for the current user, method and record using the configuration library; assigns the attributes; invokes a block (if provided) and attempts to save the record within a transaction; and returns the save result. The record is also accessible via the object attribute.
manager.update(id: 1, attributes: {genre: "electronic", published_at: Date.current})Update Includes Option
The options argument :includes key provides the ability to specify associations to eager load and accepts associations names in the same formats as the AR includes method eg. a single association name, an array of names or a hash of names. When the :includes key is not present, any class defaults are used.
manager.update(id: 1, attributes: {published_at: Date.current}, options: {includes: [:artist]})The :includes key can also be used to vary the method used to eager load associations by providing :associations and :loading_method keys. When the :loading_method key is not present the method will use either the class default method (set using default_includes) or the configuration default_loading_method.
manager.update(id: 1, attributes: {published_at: Date.current}, options: {includes: {associations: :songs, loading_method: :preload}})Destroy Method
The destroy method has id and optional options keyword arguments. The options hash can contain an :includes key. The method retrieves a record; performs authorization for the current user, method and record using the configuration library; invokes a block (if provided) and attempts to destroy the record within a transaction; and returns the destroy result. The record is accessible via the object attribute.
manager.destroy(id: 1)Destroy Includes Option
The options argument :includes key provides the ability to specify associations to eager load and accepts associations names in the same formats as the AR includes method eg. a single association name, an array of names or a hash of names. When the :includes key is not present, any class defaults are used.
manager.destroy(id: 1, options: {includes: [:artist]})The :includes key can also be used to vary the method used to eager load associations by providing :associations and :loading_method keys. When the :loading_method key is not present the method will use either the class default method (set using default_includes) or the configuration default_loading_method.
manager.destroy(id: 1, options: {includes: {associations: :songs, loading_method: :preload}})Extending the CRUD Methods
Each of the seven core CRUD methods include a yield so it is possible to extend the methods by passing a block.
def create(attributes:)
super do
@target.description = "change the object before it is created using a block"
end
endThe logic within each of the methods has also been split into subsidiary methods so it is possible to extend or override these methods.
def create_object
@target.description = "change the object before it is created via an override"
super
endBuild and Retrieve Scoped Records
Each of the seven core CRUD methods build and retrieve records using the action_scope method which by default returns the model class. In order to build or retrieve records using a scope it is possible to override this method.
def action_scope
current_user.albums
endAttribute Value Parsing
Date and DateTime Attribute Values
If you have users in the US where the date format is month/day/year you'll be aware that ActiveRecord does not support that string format. The issue is further complicated if you also have users in other countries that use the day/month/year format.
I18n.locale = :"en-US"
Album.new(published_at: "12/22/2022 14:21").published_at # => nilActiveManageable caters for these different formats and provides greater flexibility to accept a wider variety of formats by parsing date and datetime values using the Flexitime gem before setting a model object's attribute values. Flexitime uses the rails-i18n gem to determine whether the first date part is day or month and then returns an ActiveSupport TimeZone object. ActiveManageable updates the attributes argument for the new, create and update methods to replace the value for any attributes with a data type of date or datetime and also updates the attributes values for any associations within the attributes hash.
I18n.locale = :"en-US"
ActiveManageable.current_user = User.first
manager = AlbumManager.new
manager.new(attributes: {published_at: "12/01/2022 14:21", songs_attributes: [{published_at: "12/01/2022 14:21"}]})
manager.object.published_at # => Thu, 01 Dec 2022 14:21:00.000000000 UTC +00:00
manager.object.songs.first.published_at # => Thu, 01 Dec 2022 14:21:00.000000000 UTC +00:00
manager.attributes # => {"published_at"=>Wed, 12 Jan 2022 14:21:00.000000000 UTC +00:00, ... }]}I18n.locale = :"en-GB"
ActiveManageable.current_user = User.first
manager = AlbumManager.new
manager.new(attributes: {published_at: "12/01/2022 14:21", songs_attributes: [{published_at: "12/01/2022 14:21"}]})
manager.object.published_at # => Wed, 12 Jan 2022 14:21:00.000000000 UTC +00:00
manager.object.songs.first.published_at # => Wed, 12 Jan 2022 14:21:00.000000000 UTC +00:00
manager.attributes # => {"published_at"=>Wed, 12 Jan 2022 14:21:00.000000000 UTC +00:00, ... }]}By default, the Flexitime gem parse method returns time objects with a minute precision so to persist datetime values with seconds or milliseconds it is necessary to set the Flexitime configuration option accordingly.
ActiveManageable.current_user = User.first
manager = AlbumManager.new
manager.new(attributes: {published_at: "12/01/2022 14:21:45"})
manager.object.published_at # => Wed, 12 Jan 2022 14:21:00.000000000 UTC +00:00
Flexitime.precision = :sec
manager.new(attributes: {published_at: "12/01/2022 14:21:45"})
manager.object.published_at # => Wed, 12 Jan 2022 14:21:45.000000000 UTC +00:00Numeric Attribute Values
If you have users in the Netherlands or other countries that use a comma number separator then you ideally want to allow them to enter numeric values using that separator rather than a point separator. Unfortunately ActiveRecord does not support such a separator when setting attributes values.
I18n.locale = :nl
Album.new(length: "6,55").length.to_s # => "6.0"ActiveManageable caters for the comma number separator by replacing the comma with a point before setting a model object's attribute values. It uses the rails-i18n gem to determine if the locale number separator is a comma. It then updates the attributes argument for the new, create and update methods to replace the comma for any attributes with a data type of decimal or float and a value that contains only a single comma and no points. It also updates the attributes values for any associations within the attributes hash.
I18n.locale = :nl
ActiveManageable.current_user = User.first
manager = AlbumManager.new
manager.new(attributes: {length: "6,55", songs_attributes: [{length: "8,3"}]})
manager.object.length.to_s # => "6.55"
manager.object.songs.first.length.to_s # => "8.3"
manager.attributes # => {"length"=>"6.55", "songs_attributes"=>[{"length"=>"8.3"}]}ActiveManageable Attributes
ActiveManageable includes the following attributes:
object - the record from the show, new, create, edit, update and destroy methods
collection - the records retrieved by the index method
current_method - the name of the method being executed as a symbol eg. :show
attributes - an ActiveSupport::HashWithIndifferentAccess representation of the argument from the new, create and update methods (in the case of an ActionController::Parameters the attribute contains only the permitted keys)
options - an ActiveSupport::HashWithIndifferentAccess representation of the argument from the index, show, edit, update and destroy methods
ransack - the Ransack search object used when retrieving records in the index method (when using the Ransack search library)
Adding Bespoke Methods
The manager classes provide standard implementations of the seven core CRUD methods. These can be extended or overwritten to perform custom business logic and the classes can also be extended to include the business logic for additional actions, both making use of the internal ActiveManageable methods and variables.
def complete(id:)
initialize_state
@target = model_class.find(id)
authorize(record: @target, action: :complete?)
@target.update(completed_by: current_user.id, completed_at: Time.zone.now)
endEach method should first call the initialize_state method which has optional attributes and options keyword arguments. This method sets the @target variable to nil, sets the @current_method variable to the name of the method being executed as a symbol (eg. :complete) and sets the @attributes and @options variables after performing attribute values parsing.
The model_class method returns the ActiveRecord class set either automatically or manually when calling manageable.
The @target instance variable makes the model object or ActiveRecord::Relation (in the case of the index method) accessible to the internal ActiveManageable methods. For external access, there are less ambiguous alias methods named object and collection.
The authorize method performs authorization for the current user, record and action using the configuration library. The record argument can be a model class or instance and the action argument is optional with the default being the method name.
Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests.
You can also experiment in the rails console using the dummy app. Within the spec/dummy directory:
- run
bin/rails db:setupto create the database, load the schema, and initialize it with the seed data - run
rails c
Then in the console:
ActiveManageable.current_user = User.first
manager = AlbumManager.new
manager.indexAfter making changes:
- run
rake specto run the tests and check the test coverage - run
open coverage/index.htmlto view the test coverage report - run
bundle exec appraisal installto install the appraisal dependencies orbundle exec appraisal updateto upgrade the appraisal dependencies - run
bundle exec appraisal rspecto run the tests against different versions of activerecord & activesupport - run
bundle exec rubocopto check the style of files
To install this gem onto your local machine, run bundle exec rake install.
To release a new version:
- update the version number in
version.rb - update the change log in
CHANGELOG.md - run
bundleto update theGemfile.lockversion - commit the changes
- run
bundle exec rake releaseto create & push a git tag for the version and push the.gemfile to rubygems.org. - create a new release for the version on GitHub
Contributing
Bug reports and pull requests are welcome on GitHub. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create new Pull Request
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the ActiveManageable project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.