Skip to content

Commit

Permalink
added state machines
Browse files Browse the repository at this point in the history
... and introduced a person model.

Linked scholars, people (person) and users accordingly.
  • Loading branch information
biwek committed Aug 21, 2018
1 parent 3ad8681 commit 86b50bf
Show file tree
Hide file tree
Showing 28 changed files with 339 additions and 33 deletions.
16 changes: 11 additions & 5 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.

AllCops:
TargetRubyVersion: 2.3
DisplayCopNames: true
Exclude:
- 'db/schema.rb'
- 'vendor/**/*'

Bundler/OrderedGems:
Enabled: false

Expand Down Expand Up @@ -41,8 +48,7 @@ Layout/EmptyLines:
# Configuration parameters: EnforcedStyle.
# SupportedStyles: empty_lines, no_empty_lines
Layout/EmptyLinesAroundBlockBody:
Exclude:
- db/schema.rb
Enabled: true

# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
Expand Down Expand Up @@ -157,6 +163,9 @@ Style/ExpandPathArguments:
- 'bin/rake'
- 'test/test_helper.rb'

Style/FrozenStringLiteralComment:
Enabled: false

# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols.
Expand All @@ -179,8 +188,6 @@ Style/MixinUsage:
# Configuration parameters: Strict.
Style/NumericLiterals:
MinDigits: 15
Exclude:
- db/schema.rb

# Offense count: 1
# Cop supports --auto-correct.
Expand Down Expand Up @@ -214,7 +221,6 @@ Style/SymbolArray:
Style/WordArray:
Exclude:
- db/migrate/**/*.rb
- db/schema.rb

# Offense count: 107
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
Expand Down
7 changes: 7 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ gem "ransack"
# scope & engine based paginator
gem "kaminari"

## ActiveModel ##
gem "statesman"

## Assets ##
# Use SCSS for stylesheets
gem 'sass-rails', '~> 5.0'
Expand All @@ -51,6 +54,10 @@ gem 'jbuilder', '~> 2.5'
# rails named routes as javascript helpers
gem "js-routes"

## Caching ##
# Per-request global storage
gem "request_store"

## Form generation ##
gem "simple_form"
gem "cocoon"
Expand Down
5 changes: 5 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ GEM
ffi (>= 0.5.0, < 2)
recaptcha (4.11.1)
json
request_store (1.4.1)
rack (>= 1.4)
responders (2.4.0)
actionpack (>= 4.2.0, < 5.3)
railties (>= 4.2.0, < 5.3)
Expand Down Expand Up @@ -291,6 +293,7 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
statesman (3.4.1)
thor (0.20.0)
thread_safe (0.3.6)
tilt (2.0.8)
Expand Down Expand Up @@ -351,6 +354,7 @@ DEPENDENCIES
rails (~> 5.2.0)
ransack
recaptcha
request_store
rolify
rspec-rails
rubocop
Expand All @@ -359,6 +363,7 @@ DEPENDENCIES
simple_form
spring
spring-watcher-listen (~> 2.0.0)
statesman
turbolinks (~> 5)
tzinfo-data
uglifier (>= 1.3.0)
Expand Down
8 changes: 8 additions & 0 deletions app/assets/javascripts/components/_base.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,11 @@ $(document).on "turbolinks:load", ->
$('[data-toggle="tooltip"]').tooltip()
# default select2 theme
$.fn.select2.defaults.set( "theme", "bootstrap" )

document.addEventListener "turbolinks:before-cache", ->
# clear placeholder before caching
# https://github.com/ambethia/recaptcha/issues/217
$("form .g-recaptcha").empty()
# destroy all select2 objects before caching as
# its not attached when using turbolinks
$("form select.select2-hidden-accessible").select2("destroy")
5 changes: 0 additions & 5 deletions app/assets/javascripts/scholars.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,3 @@ $(document).on "ready page:load remote:load turbolinks:load", ->
afterSelect: (data) ->
id = $("input#suggested_id").val()
window.location.href = Routes.scholars_path({sid: id})

document.addEventListener "turbolinks:before-cache", ->
# clear placeholder before caching
# https://github.com/ambethia/recaptcha/issues/217
$(".g-recaptcha").empty()
14 changes: 14 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,18 @@ class ApplicationController < ActionController::Base

protect_from_forgery with: :exception

helper_method :current_person

before_action :set_current_user

def current_person
current_user&.person
end

private

def set_current_user
User.current = current_user
end

end
16 changes: 14 additions & 2 deletions app/controllers/scholars_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def new

def create
@scholar = Scholar.new scholar_params
if verify_recaptcha(model: @scholar) && @scholar.save
if recaptcha_valid? && @scholar.save
redirect_to scholars_path
else
flash.now[:error] = @scholar.errors[:base].to_sentence
Expand All @@ -29,7 +29,8 @@ def scholar_params
:description,
:discipline_id,
organisation_attributes: [:id, :name, :position, :country_code],
web_urls_attributes: [:id, :title, :url, :code, :_destroy])
web_urls_attributes: [:id, :title, :url, :code, :_destroy],
created_by_attributes: [:email])
end

def set_search
Expand All @@ -52,11 +53,22 @@ def load_scholars

def scholars_scope
@search.result
.approved
.preload(:organisation,
:web_urls,
discipline: :self_and_ancestors)
.order(updated_at: :desc)
.page(params[:page])
end

def transition_to!(state)
ActiveRecord::Base.transaction do
@scholar.transition_to! state
end
end

def recaptcha_valid?
current_user.present? || verify_recaptcha(model: @scholar)
end

end
1 change: 1 addition & 0 deletions app/helpers/scholars_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ def setup_scholar(scholar)
s.build_publication_urls
s.build_personal_urls
s.build_organisation if s.organisation.blank?
s.build_created_by if s.created_by.blank?
end
end

Expand Down
15 changes: 15 additions & 0 deletions app/models/concerns/name_finder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module NameFinder

extend ActiveSupport::Concern

included do
ransacker :name do |parent|
Arel::Nodes::NamedFunction.new "concat", [parent.table[:first_name], Arel::Nodes.build_quoted(" "), parent.table[:last_name]]
end
end

def name
[first_name, last_name].reject(&:blank?).map(&:strip).join(" ")
end

end
10 changes: 10 additions & 0 deletions app/models/person.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class Person < ApplicationRecord

include NameFinder

has_one :user, inverse_of: :person

validates :email, presence: true, uniqueness: true
validates_format_of :email, with: Devise.email_regexp

end
42 changes: 35 additions & 7 deletions app/models/scholar.rb
Original file line number Diff line number Diff line change
@@ -1,36 +1,64 @@
class Scholar < ApplicationRecord

extend Enumerize
include Statesman::Adapters::ActiveRecordQueries
include NameFinder

class << self

def transition_class
ScholarTransition
end

private

def initial_state
:in_review
end

end

paginates_per 45

belongs_to :discipline
belongs_to :institute, optional: true
belongs_to :created_by, class_name: "Person"
has_one :organisation
has_many :web_urls, as: :linkable
has_many :scholar_transitions, inverse_of: :scholar, dependent: :destroy

scope :approved, -> { with_state :approved }

accepts_nested_attributes_for :organisation, allow_destroy: true
accepts_nested_attributes_for :web_urls, reject_if: :reject_web_urls?, allow_destroy: true
accepts_nested_attributes_for :created_by

ransacker :name do |parent|
Arel::Nodes::NamedFunction.new "concat", [parent.table[:first_name], Arel::Nodes.build_quoted(" "), parent.table[:last_name]]
end
before_validation :set_created_by, on: :create

validates :first_name, :last_name, :discipline, presence: true

def name
[first_name, last_name].reject(&:blank?).map(&:strip).join(" ")
end
enumerize :state, in: ScholarStateMachine.states, default: :in_review, predicates: true, scope: true

delegate :can_transition_to?, :transition_to!, :transition_to, :current_state, to: :state_machine

%i[publication personal].each do |code|
define_method("build_#{code}_urls") do
web_urls.build(code: code) if web_urls.none?(&:"#{code}?")
end
end

def state_machine
@state_machine ||= ScholarStateMachine.new(self, transition_class: ScholarTransition)
end

private

def reject_web_urls?(attributes)
attributes["code"] == "personal" &&
(attributes["title"].blank? || attributes["url"].blank?)
end

def set_created_by
self.created_by = User.current&.person || Person.find_by(email: created_by&.email) || created_by
end

end
28 changes: 28 additions & 0 deletions app/models/scholar_state_machine.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
class ScholarStateMachine

include Statesman::Machine

state :in_review, initial: true
state :approved
state :declined

transition from: :in_review, to: [:approved, :declined]
transition from: :declined, to: :in_review
# TODO: approved scholars can be edited.
# add appropriate transitions for such scenarios.

guard_transition(to: :declined) do |scholar|
# TODO: validate feedback/reason supplied
end

after_transition(to: :declined) do |scholar, transition|
# TODO: trigger mail to the person who added the scholar
# with feedback/reason on why it was declined
end

after_transition do |scholar, transition|
# also store current state on model directly
scholar.update_columns state: transition.to_state
end

end
28 changes: 28 additions & 0 deletions app/models/scholar_transition.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
class ScholarTransition < ApplicationRecord

extend Enumerize

belongs_to :scholar, inverse_of: :scholar_transitions
belongs_to :created_by, class_name: "Person"

validates :scholar, :created_by, presence: true
validates :to_state, inclusion: {in: ScholarStateMachine.states}

before_validation :set_created_by
after_destroy :update_most_recent, if: :most_recent?

enumerize :to_state, in: ScholarStateMachine.states, predicates: true

private

def update_most_recent
last_transition = scholar.scholar_transitions.order(:sort_key).last
return unless last_transition.present?
last_transition.update_column(:most_recent, true)
end

def set_created_by
self.created_by_id ||= User.current&.person_id
end

end
24 changes: 24 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
class User < ApplicationRecord

class << self

def current=(user)
RequestStore.store[:current_user] = user
end

def current
RequestStore.store[:current_user]
end

end

rolify strict: true

# Include default devise modules. Others available are:
Expand All @@ -8,4 +20,16 @@ class User < ApplicationRecord
:recoverable, :rememberable, :trackable, :validatable
# :registerable # disable sign_up

belongs_to :person, inverse_of: :user

before_validation :link_person, on: :create

delegate :name, to: :person

private

def link_person
self.person = Person.find_by(email: email)
end

end
2 changes: 1 addition & 1 deletion app/views/navigations/_navbar.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<ul class="navbar-nav my-2 my-lg-0">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Admin
<%= current_person.name %>
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<%= link_to "Settings", "#", class: "dropdown-item small" %>
Expand Down
Loading

0 comments on commit 86b50bf

Please sign in to comment.