diff --git a/.rubocop.yml b/.rubocop.yml index 2271d0d..928fd6f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -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: diff --git a/app/controllers/admin/custom_group_data_types_controller.rb b/app/controllers/admin/custom_group_data_types_controller.rb new file mode 100644 index 0000000..1c6b6ec --- /dev/null +++ b/app/controllers/admin/custom_group_data_types_controller.rb @@ -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 diff --git a/app/controllers/admin/custom_userdata_types_controller.rb b/app/controllers/admin/custom_userdata_types_controller.rb new file mode 100644 index 0000000..5836481 --- /dev/null +++ b/app/controllers/admin/custom_userdata_types_controller.rb @@ -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 diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index a7ac94a..4eb7e2e 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -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 @@ -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 diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 06cbc6d..31b9cd7 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -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? @@ -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 diff --git a/app/controllers/profile/additional_properties_controller.rb b/app/controllers/profile/additional_properties_controller.rb new file mode 100644 index 0000000..156317c --- /dev/null +++ b/app/controllers/profile/additional_properties_controller.rb @@ -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 diff --git a/app/models/concerns/deserializable.rb b/app/models/concerns/deserializable.rb new file mode 100644 index 0000000..7f7558f --- /dev/null +++ b/app/models/concerns/deserializable.rb @@ -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 diff --git a/app/models/custom_group_data_type.rb b/app/models/custom_group_data_type.rb new file mode 100644 index 0000000..faca96f --- /dev/null +++ b/app/models/custom_group_data_type.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class CustomGroupDataType < ApplicationRecord + has_many :custom_groupdata, dependent: :destroy +end diff --git a/app/models/custom_groupdatum.rb b/app/models/custom_groupdatum.rb new file mode 100644 index 0000000..05a7439 --- /dev/null +++ b/app/models/custom_groupdatum.rb @@ -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 diff --git a/app/models/custom_userdata_type.rb b/app/models/custom_userdata_type.rb new file mode 100644 index 0000000..647dc59 --- /dev/null +++ b/app/models/custom_userdata_type.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class CustomUserdataType < ApplicationRecord + has_many :custom_userdata, dependent: :destroy +end diff --git a/app/models/custom_userdatum.rb b/app/models/custom_userdatum.rb new file mode 100644 index 0000000..7d43efc --- /dev/null +++ b/app/models/custom_userdatum.rb @@ -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 diff --git a/app/models/group.rb b/app/models/group.rb index 77d2984..bdf4dbb 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -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 diff --git a/app/models/setting.rb b/app/models/setting.rb index 0e36366..0e1a897 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -17,6 +17,8 @@ # RailsSettings Model class Setting < ApplicationRecord + + SEPARATOR_REGEXP = /[\n,;]+/.freeze # cache_prefix { "v1" } # Define your fields diff --git a/app/models/user.rb b/app/models/user.rb index 86ea1ef..1612057 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/views/admin/custom_group_data_types/_form.html.erb b/app/views/admin/custom_group_data_types/_form.html.erb new file mode 100644 index 0000000..aa77a37 --- /dev/null +++ b/app/views/admin/custom_group_data_types/_form.html.erb @@ -0,0 +1,39 @@ +<%= form_with(model: [:admin, model], local: true, class: 'form') do |form| %> + <% if model.errors.any? %> +
+

<%= pluralize(model.errors.count, "error") %> prohibited this model from being saved:

+ + +
+ <% end %> + +
+
+
+ <%= form.label :name %> +
+
+ <%= form.text_field :name, class: 'form-control' %> +
+
+
+ +
+
+
+ <%= form.label :custom_type %> +
+
+ <%= form.select :custom_type, [[:boolean, :boolean], [:string, :string], [:array, :array]], class: 'form-control' %> +
+
+
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/app/views/admin/custom_userdata_types/_form.html.erb b/app/views/admin/custom_userdata_types/_form.html.erb new file mode 100644 index 0000000..aa77a37 --- /dev/null +++ b/app/views/admin/custom_userdata_types/_form.html.erb @@ -0,0 +1,39 @@ +<%= form_with(model: [:admin, model], local: true, class: 'form') do |form| %> + <% if model.errors.any? %> +
+

<%= pluralize(model.errors.count, "error") %> prohibited this model from being saved:

+ + +
+ <% end %> + +
+
+
+ <%= form.label :name %> +
+
+ <%= form.text_field :name, class: 'form-control' %> +
+
+
+ +
+
+
+ <%= form.label :custom_type %> +
+
+ <%= form.select :custom_type, [[:boolean, :boolean], [:string, :string], [:array, :array]], class: 'form-control' %> +
+
+
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/app/views/admin/groups/_form.html.erb b/app/views/admin/groups/_form.html.erb new file mode 100644 index 0000000..ba88d73 --- /dev/null +++ b/app/views/admin/groups/_form.html.erb @@ -0,0 +1,95 @@ +<%= form_with(model: [:admin, model], local: true, class: 'form') do |form| %> + <% if model.errors.any? %> +
+

<%= pluralize(model.errors.count, "error") %> prohibited this model from being saved:

+ + +
+ <% end %> + <% attributes = new_model ? new_fields : edit_fields %> + <% attributes.each do |attribute| %> +
+
+
+ <%= form.label attribute %> +
+
+ <%- if [true, false].include? model.send(attribute) %> + <%= form.check_box attribute, class: 'form-check' %> + <%- else %> + <%= form.text_field attribute, class: 'form-control' %> + <%- end %> +
+
+
+ <%- end %> + <%- form_relations.each do |relation_name, info| %> + +
+
+
+ <%= form.label relation_name %> +
+
+ <%= form.send(info[:type], relation_name, info[:finder].call, info[:options] || {}, (info[:html_options] || {}).merge({class: 'form-control'}) ) %> +
+
+
+ <%- end %> + +
+
+
+ <%= form.label :groups %> +
+
+ <%= form.collection_check_boxes :permission_ids, Permission.all, :id, :name do |group| %> +
+ <%= group.check_box %><%= group.label %> +
+ <% end %> +
+
+
+ +
+
+
+ <%= form.label :welcome_email %> +
+
+ <%= form.text_area :welcome_email, class: 'form-control' %> +
+
+ <%- unless new_model %> +
+

Preview refreshes after save.

+ + <%- end %> +
+ + +
+ <%= form.submit _('Update Group'), class: 'btn btn-success' %> +
+<% end %> + +<%- unless new_model %> +
+

Additional Properties

+ <%= link_to "Manage Group Data Types", admin_custom_group_data_types_path, class: 'btn btn-info' %> + <%= form_with(url: admin_group_custom_attributes_path(group_id: @model.id), local: true, class: 'form') do |f| %> + <%- custom_groupdata = @model.custom_groupdata.includes([:custom_group_data_type]).group_by(&:custom_group_data_type_id) %> + <%- CustomGroupDataType.all.each do |data_type| %> + <%- custom_groupdatum = custom_groupdata[data_type.id].try(:first) || CustomGroupdatum.new(group: @model, custom_group_data_type: data_type) %> + <%= render partial: 'custom_datum', locals: { data_type: data_type, custom_datum: custom_groupdatum, disabled: false } %> + <%- end %> +
+ <%= f.submit _('Save Changes'), class: 'btn btn-success' %> + <%- end %> +
+<%- end %> diff --git a/app/views/admin/groups/_show.html.erb b/app/views/admin/groups/_show.html.erb index aeced32..6436215 100644 --- a/app/views/admin/groups/_show.html.erb +++ b/app/views/admin/groups/_show.html.erb @@ -1,2 +1,16 @@ +
+

Additional Properties

+ <%= form_with(url: admin_group_custom_attributes_path(group_id: @model.id), local: true, class: 'form') do |f| %> + <%- custom_groupdata = @model.custom_groupdata.includes([:custom_group_data_type]).group_by(&:custom_group_data_type_id) %> + <%- CustomGroupDataType.all.each do |data_type| %> + <%- custom_groupdatum = custom_groupdata[data_type.id].try(:first) || CustomGroupdatum.new(group: @model, custom_group_data_type: data_type) %> + <%= render partial: 'custom_datum', locals: { data_type: data_type, custom_datum: custom_groupdatum, disabled: true } %> + <%- end %> +
+ <%= f.submit _('Save Changes'), class: 'btn btn-success' %> + <%- end %> +
+ +

Welcome Email Preview

diff --git a/app/views/admin/groups/_sub_form.html.erb b/app/views/admin/groups/_sub_form.html.erb deleted file mode 100644 index cf4bc01..0000000 --- a/app/views/admin/groups/_sub_form.html.erb +++ /dev/null @@ -1,28 +0,0 @@ -
-
-
- <%= form.label :groups %> -
-
- <%= form.collection_check_boxes :permission_ids, Permission.all, :id, :name do |group| %> -
- <%= group.check_box %><%= group.label %> -
- <% end %> -
-
-
- -
-
-
- <%= form.label :welcome_email %> -
-
- <%= form.text_area :welcome_email, class: 'form-control' %> -
-
-
-

Preview refreshes after save.

- -
diff --git a/app/views/admin/show.html.erb b/app/views/admin/show.html.erb index 93d5e34..7bdfb64 100644 --- a/app/views/admin/show.html.erb +++ b/app/views/admin/show.html.erb @@ -1,5 +1,10 @@

<%= @model.class.name %> <%= @model %>

<%= render partial: 'sub_heading' %> +
+ <%= link_to 'Edit', ['edit', :admin, @model] %> | + <%= link_to 'Back', [:admin, @model.class] %> +
+
<%- model_attributes.each do |attribute| %> @@ -28,7 +33,3 @@ <% rescue ActionView::MissingTemplate %> <% end %> -
- <%= link_to 'Edit', ['edit', :admin, @model] %> | - <%= link_to 'Back', [:admin, @model.class] %> -
diff --git a/app/views/admin/users/_form.html.erb b/app/views/admin/users/_form.html.erb index 79dc710..2e16924 100644 --- a/app/views/admin/users/_form.html.erb +++ b/app/views/admin/users/_form.html.erb @@ -119,10 +119,24 @@ <%- end %>
- <%= form.submit %> + <%= form.submit _('Update User'), class: 'btn btn-success' %>
<% end %> +
+

Additional Properties

+ <%= link_to "Manage User Data Types", admin_custom_userdata_types_path, class: 'btn btn-info' %> + <%= form_with(url: admin_user_custom_attributes_path(user_id: @model.id), local: true, class: 'form') do |f| %> + <%- custom_userdata = current_user.custom_userdata.includes([:custom_userdata_type]).group_by(&:custom_userdata_type_id) %> + <%- CustomUserdataType.all.each do |data_type| %> + <%- custom_userdatum = custom_userdata[data_type.id].try(:first) || CustomUserdatum.new(user: current_user, custom_userdata_type: data_type) %> + <%= render partial: 'custom_datum', locals: { data_type: data_type, custom_datum: custom_userdatum, disabled: false } %> + <%- end %> +
+ <%= f.submit _('Save Changes'), class: 'btn btn-success' %> + <%- end %> +
+