Skip to content

Commit

Permalink
A user and group can have custom attributes (#170)
Browse files Browse the repository at this point in the history
* A user can have custom attributes

The user can edit their own attributes and an admin can update these
attributes.

* A group can have custom properties

In addition to adding functionality for a group to have custom
properties, this change includes additional admin controllers
and views to manage the newly created models.

Closes #48

* Monkey patch rails_erd to work with Rails 6.1

Until voormedia/rails-erd#358 is resolved,
rails-erd cannot generate ERD data from the models on Rails 6.1.

This patch resolves the above by monkey-patching in a fix that has
been proposed upstream.

This also ended up with some tidying of Rubocop config as
a result of needing to update it for the new concern spec.
  • Loading branch information
ChrisMacNaughton authored Jan 14, 2021
1 parent ba587e0 commit 771e10d
Show file tree
Hide file tree
Showing 44 changed files with 785 additions and 47 deletions.
6 changes: 3 additions & 3 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ Metrics/BlockLength:
- config/initializers/doorkeeper_openid_connect.rb
- lib/tasks/*
- app/admin/*
- spec/models/*.rb
- spec/controllers/*.rb
- spec/controllers/admin/*.rb
- 'Rakefile'
- '**/*.rake'
- 'spec/**/*.rb'

Style/GlobalVars:
AllowedVariables:
Expand Down
19 changes: 19 additions & 0 deletions app/controllers/admin/custom_group_data_types_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

class Admin::CustomGroupDataTypesController < AdminController
private

def model
CustomGroupDataType
end

def model_params
params.require(:custom_group_data_type).permit(
:name, :custom_type
)
end

def whitelist_attributes
%w[name custom_type]
end
end
19 changes: 19 additions & 0 deletions app/controllers/admin/custom_userdata_types_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

class Admin::CustomUserdataTypesController < AdminController
private

def model
CustomUserdataType
end

def model_params
params.require(:custom_userdata_type).permit(
:name, :custom_type
)
end

def whitelist_attributes
%w[name custom_type]
end
end
22 changes: 22 additions & 0 deletions app/controllers/admin/groups_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@ def email
render :email, layout: nil
end

def update_custom_attributes # rubocop:disable Metrics/MethodLength
@model = Group.find(params[:group_id])
custom_groupdata_params.each do |name, value|
custom_type = CustomGroupDataType.where(name: name).first
custom_datum = CustomGroupdatum.where(
group_id: @model.id,
custom_group_data_type: custom_type
).first_or_initialize
begin
custom_datum.value = value
custom_datum.save
rescue RuntimeError
flash[:error] = 'Failed to update group data, invalid value'
end
end
redirect_to [:edit, :admin, @model]
end

private

def whitelist_attributes
Expand Down Expand Up @@ -53,4 +71,8 @@ def model_params
def sort_whitelist
%w[created_at name]
end

def custom_groupdata_params
params.require(:custom_data).permit!
end
end
22 changes: 22 additions & 0 deletions app/controllers/admin/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,24 @@ def reset_password
end
end

def update_custom_attributes # rubocop:disable Metrics/MethodLength
@model = User.find(params[:user_id])
custom_userdata_params.each do |name, value|
custom_type = CustomUserdataType.where(name: name).first
custom_datum = CustomUserdatum.where(
user_id: @model.id,
custom_userdata_type: custom_type
).first_or_initialize
begin
custom_datum.value = value
custom_datum.save
rescue RuntimeError
flash[:error] = 'Failed to update userdata, invalid value'
end
end
redirect_to [:edit, :admin, @model]
end

private

def can_destroy?
Expand Down Expand Up @@ -78,4 +96,8 @@ def filter(rel) # rubocop:disable Metrics/AbcSize
rel
end
end

def custom_userdata_params
params.require(:custom_data).permit!
end
end
28 changes: 28 additions & 0 deletions app/controllers/profile/additional_properties_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

class Profile::AdditionalPropertiesController < ApplicationController
def index; end

def update # rubocop:disable Metrics/MethodLength
custom_userdata_params.each do |name, value|
custom_type = CustomUserdataType.where(name: name).first
custom_datum = CustomUserdatum.where(
user_id: current_user.id,
custom_userdata_type: custom_type
).first_or_initialize
begin
custom_datum.value = value
custom_datum.save
rescue RuntimeError
flash[:error] = 'Failed to update userdata, invalid value'
end
end
redirect_to profile_additional_properties_path
end

protected

def custom_userdata_params
params.require(:custom_data).permit!
end
end
19 changes: 19 additions & 0 deletions app/models/concerns/deserializable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module Deserializable
SEPARATOR_REGEXP = /[\n,;]+/.freeze

# takes a string and returns a typed thing
def deserialize(value, custom_type)
case custom_type
when 'boolean'
['t', 'true', '1', 1, true, :true].include?(value) # rubocop:disable Lint/BooleanSymbol
when 'array'
(value || '').split(SEPARATOR_REGEXP).map(&:strip).reject(&:empty?)
when 'integer'
value.to_i
else
value
end
end
end
5 changes: 5 additions & 0 deletions app/models/custom_group_data_type.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

class CustomGroupDataType < ApplicationRecord
has_many :custom_groupdata, dependent: :destroy
end
38 changes: 38 additions & 0 deletions app/models/custom_groupdatum.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# frozen_string_literal: true

class CustomGroupdatum < ApplicationRecord
belongs_to :group
belongs_to :custom_group_data_type

serialize :value_raw, JSON

delegate :name, to: :custom_group_data_type

include Deserializable

def value
value_raw
end

def type
custom_group_data_type.custom_type
end

def value=(new_value) # rubocop:disable Metrics/MethodLength
new_value = deserialize(new_value, custom_group_data_type.custom_type)
if custom_group_data_type
valid = case custom_group_data_type.custom_type
when 'boolean'
new_value.is_a?(TrueClass) || new_value.is_a?(FalseClass)
when 'array'
new_value.is_a?(Array)
when 'integer'
new_value.is_a?(Integer)
else
true
end
raise "Invalid User Data: #{new_value} isn't an #{custom_group_data_type.custom_type}" unless valid
end
self.value_raw = new_value
end
end
5 changes: 5 additions & 0 deletions app/models/custom_userdata_type.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

class CustomUserdataType < ApplicationRecord
has_many :custom_userdata, dependent: :destroy
end
37 changes: 37 additions & 0 deletions app/models/custom_userdatum.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

class CustomUserdatum < ApplicationRecord
belongs_to :user
belongs_to :custom_userdata_type
serialize :value_raw, JSON

delegate :name, to: :custom_userdata_type

include Deserializable

def value
value_raw
end

def type
custom_userdata_type.custom_type
end

def value=(new_value) # rubocop:disable Metrics/MethodLength
new_value = deserialize(new_value, custom_userdata_type.custom_type)
if custom_userdata_type
valid = case custom_userdata_type.custom_type
when 'boolean'
new_value.is_a?(TrueClass) || new_value.is_a?(FalseClass)
when 'array'
new_value.is_a?(Array)
when 'integer'
new_value.is_a?(Integer)
else
true
end
raise "Invalid User Data: #{new_value} isn't an #{custom_userdata_type.custom_type}" unless valid
end
self.value_raw = new_value
end
end
2 changes: 2 additions & 0 deletions app/models/group.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class Group < ApplicationRecord
has_many :user_groups, dependent: :destroy
has_many :users, through: :user_groups

has_many :custom_groupdata, dependent: :destroy

def to_s
name
end
Expand Down
2 changes: 2 additions & 0 deletions app/models/setting.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

# RailsSettings Model
class Setting < ApplicationRecord

SEPARATOR_REGEXP = /[\n,;]+/.freeze
# cache_prefix { "v1" }

# Define your fields
Expand Down
2 changes: 2 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength

has_many :logins, dependent: :destroy

has_many :custom_userdata, dependent: :destroy

validates :username, presence: true, uniqueness: { case_sensitive: false } # rubocop:disable Rails/UniqueValidationWithoutIndex

validate :validate_username
Expand Down
39 changes: 39 additions & 0 deletions app/views/admin/custom_group_data_types/_form.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<%= form_with(model: [:admin, model], local: true, class: 'form') do |form| %>
<% if model.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(model.errors.count, "error") %> prohibited this model from being saved:</h2>

<ul>
<% model.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>

<div class="form-group">
<div class="row">
<div class="col-sm-2">
<%= form.label :name %>
</div>
<div class="col-sm-10">
<%= form.text_field :name, class: 'form-control' %>
</div>
</div>
</div>

<div class="form-group">
<div class="row">
<div class="col-sm-2">
<%= form.label :custom_type %>
</div>
<div class="col-sm-10">
<%= form.select :custom_type, [[:boolean, :boolean], [:string, :string], [:array, :array]], class: 'form-control' %>
</div>
</div>
</div>

<div class="actions">
<%= form.submit %>
</div>
<% end %>
39 changes: 39 additions & 0 deletions app/views/admin/custom_userdata_types/_form.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<%= form_with(model: [:admin, model], local: true, class: 'form') do |form| %>
<% if model.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(model.errors.count, "error") %> prohibited this model from being saved:</h2>

<ul>
<% model.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>

<div class="form-group">
<div class="row">
<div class="col-sm-2">
<%= form.label :name %>
</div>
<div class="col-sm-10">
<%= form.text_field :name, class: 'form-control' %>
</div>
</div>
</div>

<div class="form-group">
<div class="row">
<div class="col-sm-2">
<%= form.label :custom_type %>
</div>
<div class="col-sm-10">
<%= form.select :custom_type, [[:boolean, :boolean], [:string, :string], [:array, :array]], class: 'form-control' %>
</div>
</div>
</div>

<div class="actions">
<%= form.submit %>
</div>
<% end %>
Loading

0 comments on commit 771e10d

Please sign in to comment.