diff --git a/Gemfile.lock b/Gemfile.lock index e0b2792..05cab02 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -48,6 +48,7 @@ rake (>= 10.4, < 13.0) annotate_gem (0.0.13) bundler (~> 1.1) + api-pagination (4.8.2) archive-zip (0.12.0) io-like (~> 0.3.0) arel (6.0.4) @@ -281,7 +282,6 @@ omniauth (>= 1.0.0) options (2.3.2) orm_adapter (0.5.0) - pager_api (0.3.1) parallel (1.17.0) parser (2.6.2.1) ast (~> 2.4.0) @@ -483,6 +483,7 @@ activerecord-session_store annotate annotate_gem + api-pagination autoprefixer-rails better_errors binding_of_caller @@ -522,7 +523,6 @@ omniauth omniauth-orcid omniauth-shibboleth - pager_api pg (~> 0.19.0) progress_bar puma diff --git a/app/controllers/org_admin/departments_controller.rb b/app/controllers/org_admin/departments_controller.rb new file mode 100644 index 0000000..fc32fb9 --- /dev/null +++ b/app/controllers/org_admin/departments_controller.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +class OrgAdmin::DepartmentsController < ApplicationController + + after_action :verify_authorized + respond_to :html + + # GET add new department + def new + @department = Department.new + authorize @department + end + + # POST /departments + # POST /departments.json + def create + @department = Department.new(department_params) + authorize @department + @department.org_id = current_user.org_id + + if @department.save + flash.now[:notice] = success_message(@department, _("created")) + # reset value + @department = nil + render :new + else + flash.now[:alert] = failure_message(@department, _("create")) + render :new + end + end + + # GET /departments/1/edit + def edit + @department = Department.find(params[:id]) + authorize @department + end + + # PUT /departments/1 + def update + @department = Department.find(params[:id]) + authorize @department + @department.org_id = current_user.org_id + + if @department.update(department_params) + flash.now[:notice] = success_message(@department, _("saved")) + render :edit + else + flash.now[:alert] = failure_message(@department, _("save")) + render :edit + end + end + + # DELETE /departments/1 + def destroy + @department = Department.find(params[:id]) + authorize @department + url = "#{admin_edit_org_path(current_user.org_id)}\#departments" + if @department.destroy + flash[:notice] = success_message(@department, _("deleted")) + redirect_to url + else + flash[:alert] = failure_message(@department, _("delete")) + redirect_to url + end + end + + + private + + def department_params + params.require(:department).permit(:id, :name, :code, :org_id) + end + +end diff --git a/app/controllers/paginable/departments_controller.rb b/app/controllers/paginable/departments_controller.rb new file mode 100644 index 0000000..9bef30d --- /dev/null +++ b/app/controllers/paginable/departments_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class Paginable::DepartmentsController < ApplicationController + + after_action :verify_authorized + respond_to :html + + include Paginable + + # /paginable/departments/index/:page + def index + authorize Department + paginable_renderise( + partial: "index", + scope: Department.by_org(current_user.org), + query_params: { sort_field: "departments.name", sort_direction: :asc } + ) + end + +end diff --git a/app/controllers/paginable/guidances_controller.rb b/app/controllers/paginable/guidances_controller.rb index efec2eb..5f6fa0b 100644 --- a/app/controllers/paginable/guidances_controller.rb +++ b/app/controllers/paginable/guidances_controller.rb @@ -9,8 +9,7 @@ authorize(Guidance) paginable_renderise( partial: "index", - scope: Guidance.by_org(current_user.org) - .includes(:guidance_group, :themes), + scope: org.admin, query_params: { sort_field: "guidances.text", sort_direction: :asc } ) end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 3defde5..503b2f6 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -190,7 +190,9 @@ else successfully_updated = current_user.update_with_password(password_update) if !successfully_updated - message = _("Save unsuccessful. That email address is already registered. You must enter a unique email address.") + message = _("Save unsuccessful. \ + That email address is already registered. \ + You must enter a unique email address.") end end else @@ -268,14 +270,14 @@ def update_params params.require(:user).permit(:firstname, :org_id, :other_organisation, - :language_id, :surname) + :language_id, :surname, :department_id) end def password_update params.require(:user).permit(:email, :firstname, :current_password, :org_id, :language_id, :password, :password_confirmation, :surname, - :other_organisation) + :other_organisation, :department_id) end end diff --git a/app/models/department.rb b/app/models/department.rb new file mode 100644 index 0000000..16ca731 --- /dev/null +++ b/app/models/department.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: departments +# +# id :integer not null, primary key +# code :string +# name :string +# created_at :datetime not null +# updated_at :datetime not null +# org_id :integer +# +# Indexes +# +# index_departments_on_org_id (org_id) +# + +class Department < ActiveRecord::Base + + include ValidationMessages + + belongs_to :org + + has_many :users + + # =============== + # = Validations = + # =============== + + validates :org, presence: { message: PRESENCE_MESSAGE } + + validates :name, presence: { message: PRESENCE_MESSAGE }, + uniqueness: { message: UNIQUENESS_MESSAGE, + scope: :org_id } + + validates :name, uniqueness: { message: UNIQUENESS_MESSAGE, + scope: :org_id } + + # Retrieves every department associated to an org + scope :by_org, ->(org) { where(org_id: org.id) } + +end diff --git a/app/models/org.rb b/app/models/org.rb index ade85ba..c1970be 100644 --- a/app/models/org.rb +++ b/app/models/org.rb @@ -73,6 +73,7 @@ has_many :identifier_schemes, through: :org_identifiers + has_many :departments # =============== # = Validations = @@ -166,9 +167,9 @@ # Returns nil def get_locale if !self.language.nil? - return self.language.abbreviation + self.language.abbreviation else - return nil + nil end end @@ -188,7 +189,7 @@ ret << "Research Institute" if self.research_institute? ret << "Project" if self.project? ret << "School" if self.school? - return (ret.length > 0 ? ret.join(", ") : "None") + (ret.length > 0 ? ret.join(", ") : "None") end def funder_only? @@ -209,9 +210,9 @@ # Returns String def short_name if abbreviation.nil? then - return name + name else - return abbreviation + abbreviation end end @@ -220,7 +221,7 @@ # # Returns ActiveRecord::Relation def published_templates - return templates.where("published = ?", true) + templates.where("published = ?", true) end def org_admins diff --git a/app/models/user.rb b/app/models/user.rb index 479c74c..9e10a71 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -32,6 +32,7 @@ # surname :string # created_at :datetime not null # updated_at :datetime not null +# department_id :integer # invited_by_id :integer # language_id :integer # org_id :integer @@ -43,6 +44,7 @@ # # Foreign Keys # +# fk_rails_... (department_id => departments.id) # fk_rails_... (language_id => languages.id) # fk_rails_... (org_id => orgs.id) # @@ -78,6 +80,8 @@ belongs_to :org + belongs_to :department, required: false + has_one :pref has_many :answers @@ -190,11 +194,11 @@ # Returns nil def get_locale if !self.language.nil? - return self.language.abbreviation + self.language.abbreviation elsif !self.org.nil? - return self.org.get_locale + self.org.get_locale else - return nil + nil end end @@ -205,10 +209,10 @@ # Returns String def name(use_email = true) if (firstname.blank? && surname.blank?) || use_email then - return email + email else name = "#{firstname} #{surname}" - return name.strip + name.strip end end @@ -226,7 +230,7 @@ # # Returns Boolean def can_super_admin? - return self.can_add_orgs? || self.can_grant_api_to_orgs? || self.can_change_org? + self.can_add_orgs? || self.can_grant_api_to_orgs? || self.can_change_org? end # Checks if the user is an organisation admin if the user has any privlege which @@ -234,9 +238,9 @@ # # Returns Boolean def can_org_admin? - return self.can_grant_permissions? || self.can_modify_guidance? || - self.can_modify_templates? || self.can_modify_org_details? || - self.can_review_plans? + self.can_grant_permissions? || self.can_modify_guidance? || + self.can_modify_templates? || self.can_modify_org_details? || + self.can_review_plans? end # Can the User add new organisations? diff --git a/app/policies/department_policy.rb b/app/policies/department_policy.rb new file mode 100644 index 0000000..bfcf900 --- /dev/null +++ b/app/policies/department_policy.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class DepartmentPolicy < ApplicationPolicy + + attr_reader :user + attr_reader :department + + def initialize(user, department) + raise Pundit::NotAuthorizedError, "must be logged in" unless user + @user = user + @department = department + end + + def new? + @user.can_org_admin? + end + + def create? + @user.can_org_admin? + end + + def edit? + # Only org_admins can edit their own org's departments + @user.can_org_admin? && @user.org.id === @department.org_id + end + + def update? + # Only org_admins can update their own org's departments + @user.can_org_admin? && @user.org.id === @department.org_id + end + + def destroy? + # Only org_admins can delete their own org's departments + @user.can_org_admin? && @user.org.id === @department.org_id + end + +end diff --git a/app/views/devise/registrations/_personal_details.html.erb b/app/views/devise/registrations/_personal_details.html.erb index 29ae105..266e720 100644 --- a/app/views/devise/registrations/_personal_details.html.erb +++ b/app/views/devise/registrations/_personal_details.html.erb @@ -29,6 +29,17 @@ <% if org_admin %> <% end %> + + <% departments = current_user.org.departments %> +
+ <% dept_id = current_user.department.nil? ? -1 : current_user.department.id %> + <%= f.label(:department_id, _('Department or school'), class: 'control-label') %> + <%= select_tag("user[department_id]", + options_from_collection_for_select(departments, "id", "name", dept_id), + include_blank: true, + disabled: departments.count === 0, + class: "form-control") %> +
<% if Language.many? %>
diff --git a/app/views/org_admin/departments/edit.html.erb b/app/views/org_admin/departments/edit.html.erb new file mode 100644 index 0000000..2dbada7 --- /dev/null +++ b/app/views/org_admin/departments/edit.html.erb @@ -0,0 +1,27 @@ +<% departments_url = admin_edit_org_path(current_user.org_id) + '#departments' %> +
+
+

<%= _('Edit the School/Department') %>

+ <%= link_to _('View all departments'), departments_url, class: 'btn btn-default pull-right' %> +
+
+ +
+
+ <%= form_for :department, url: org_admin_department_path, html: { method: :put, id: 'edit_department_form' } do |f| %> +
+ <%= f.label _('Name'), for: :name, class: "control-label" %> + <%= f.text_field :name, as: :string, class: "form-control", 'aria-required': true, 'data-toggle': 'tooltip', title: _('Add the name of a school/department.') %> +
+
+ <%= f.label _('Abbreviated name or code'), for: :code, class: "control-label" %> + <%= f.text_field :code, as: :string, class: "form-control", 'aria-required': false, 'data-toggle': 'tooltip', title: _('Add the abbreviated name or code for the school/department.') %> +
+ +
+ <%= f.submit _('Save'), name: "draft", class: "btn btn-primary" %> + <%= link_to _('Cancel'), departments_url, class: "btn btn-primary", role: 'button' %> +
+ <% end %> +
+
\ No newline at end of file diff --git a/app/views/org_admin/departments/new.html.erb b/app/views/org_admin/departments/new.html.erb new file mode 100644 index 0000000..92f7da2 --- /dev/null +++ b/app/views/org_admin/departments/new.html.erb @@ -0,0 +1,27 @@ +<% departments_url = admin_edit_org_path(current_user.org_id) + '#departments' %> +
+
+

<%= _('Create a School/Department') %>

+ <%= link_to _('View all departments'), departments_url, class: 'btn btn-default pull-right' %> +
+
+ +
+
+ <%= form_for :department, url: org_admin_departments_path, html: { method: :post, id: 'create_department_form' } do |f| %> +
+ <%= f.label _('Name'), for: :name, class: "control-label" %> + <%= f.text_field :name, as: :string, class: "form-control", 'aria-required': true, 'data-toggle': 'tooltip', title: _('Add the name of a school/department.') %> +
+
+ <%= f.label _('Abbreviated name or code'), for: :code, class: "control-label" %> + <%= f.text_field :code, as: :string, class: "form-control", 'aria-required': false, 'data-toggle': 'tooltip', title: _('Add the abbreviated name or code for the school/department.') %> +
+ +
+ <%= f.submit _('Save'), name: "draft", class: "btn btn-primary" %> + <%= link_to _('Cancel'), departments_url, class: "btn btn-primary", role: 'button' %> +
+ <% end %> +
+
\ No newline at end of file diff --git a/app/views/orgs/_departments.html.erb b/app/views/orgs/_departments.html.erb new file mode 100644 index 0000000..5acbbbe --- /dev/null +++ b/app/views/orgs/_departments.html.erb @@ -0,0 +1,16 @@ +

<%=_('Schools/Departments') %>

+
+
+ <%= paginable_renderise( partial: '/paginable/departments/index', + controller: 'paginable/departments', + action: 'index', + scope: departments, + query_params: { sort_field: 'departments.name', sort_direction: 'asc' }) %> +
+
+
+ +
+
\ No newline at end of file diff --git a/app/views/orgs/_profile_form.html.erb b/app/views/orgs/_profile_form.html.erb index 1d80dca..9ccbc96 100644 --- a/app/views/orgs/_profile_form.html.erb +++ b/app/views/orgs/_profile_form.html.erb @@ -46,7 +46,7 @@ <%= hidden_field_tag('org_links', value: org.links) %> - +

<%= _("Administrator contact") %>

diff --git a/app/views/orgs/admin_edit.html.erb b/app/views/orgs/admin_edit.html.erb index bf3e7fa..5ed0835 100644 --- a/app/views/orgs/admin_edit.html.erb +++ b/app/views/orgs/admin_edit.html.erb @@ -18,6 +18,9 @@
  • <%= _('Request feedback') %>
  • +
  • + <%= _('Schools/Departments') %> +
  • <% end %> @@ -43,6 +46,17 @@
    +
    +
    +
    + <% if org.id.present? %> +
    + <%= render partial: 'orgs/departments', locals: local_assigns.merge({ departments: org.departments }) %> +
    + <% end %> +
    +
    +
    diff --git a/app/views/paginable/departments/_index.html.erb b/app/views/paginable/departments/_index.html.erb new file mode 100644 index 0000000..af7f454 --- /dev/null +++ b/app/views/paginable/departments/_index.html.erb @@ -0,0 +1,36 @@ +
    + + + + + + + + + + <% scope.each do |department| %> + + + + + + <% end %> + +
    <%= _('School or Department') %> <%= paginable_sort_link('departments.name') %><%= _('Abbreviated Name or Code') %> <%= paginable_sort_link('departments.code') %><%= _('Actions') %>
    <%= department.name %><%= department.code %> + +
    +
    \ No newline at end of file diff --git a/app/views/paginable/users/_index.html.erb b/app/views/paginable/users/_index.html.erb index e4282fc..eb1eabe 100644 --- a/app/views/paginable/users/_index.html.erb +++ b/app/views/paginable/users/_index.html.erb @@ -7,6 +7,7 @@ <%= _('Name') %> <%= paginable_sort_link('users.firstname') %> <%= _('Email') %> <%= paginable_sort_link('users.email') %> + <%= _('School or Department') %> <%= paginable_sort_link('departments.name') %> <%= _('Organisation') %> <%= paginable_sort_link('orgs.name') %> <%= _('Created date') %> <%= paginable_sort_link('users.created_at') %> <%= _('Last activity') %> <%= paginable_sort_link('users.last_sign_in_at') %> @@ -28,6 +29,8 @@ <% end %> <%= user.email %> + <%= user.department.nil? ? '' : user.department.name %> + <%= user.org.name if user.org.present? %> <% if !user.created_at.nil? %> diff --git a/config/routes.rb b/config/routes.rb index a178546..51211d1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -196,6 +196,10 @@ resources :guidance_groups, only: [] do get 'index/:page', action: :index, on: :collection, as: :index end + # Paginable actions for departments + resources :departments, only: [] do + get 'index/:page', action: :index, on: :collection, as: :index + end end resources :template_options, only: [:index], constraints: { format: /json/ } @@ -247,6 +251,8 @@ end end + resources :departments + get 'download_plans' => 'plans#download_plans' end diff --git a/db/migrate/20190507091025_create_departments.rb b/db/migrate/20190507091025_create_departments.rb new file mode 100644 index 0000000..103d53f --- /dev/null +++ b/db/migrate/20190507091025_create_departments.rb @@ -0,0 +1,15 @@ +class CreateDepartments < ActiveRecord::Migration + def change + create_table :departments do |t| + t.string :name + t.string :code + t.belongs_to :org, index: true + + t.timestamps null: false + end + + add_column :users, :department_id, :integer, index: true + add_foreign_key :users, :departments + + end +end diff --git a/db/schema.rb b/db/schema.rb index 0b0529e..54e92bc 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20181025220743) do +ActiveRecord::Schema.define(version: 20190507091025) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -49,6 +49,16 @@ add_index "answers_question_options", ["answer_id"], name: "index_answers_question_options_on_answer_id", using: :btree + create_table "departments", force: :cascade do |t| + t.string "name" + t.string "code" + t.integer "org_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "departments", ["org_id"], name: "index_departments_on_org_id", using: :btree + create_table "exported_plans", force: :cascade do |t| t.integer "plan_id" t.integer "user_id" @@ -415,6 +425,7 @@ t.integer "language_id" t.string "recovery_email" t.boolean "active", default: true + t.integer "department_id" end add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree @@ -463,6 +474,7 @@ add_foreign_key "themes_in_guidance", "themes" add_foreign_key "user_identifiers", "identifier_schemes" add_foreign_key "user_identifiers", "users" + add_foreign_key "users", "departments" add_foreign_key "users", "languages" add_foreign_key "users", "orgs" add_foreign_key "users_perms", "perms" diff --git a/spec/factories/departments.rb b/spec/factories/departments.rb new file mode 100644 index 0000000..b480804 --- /dev/null +++ b/spec/factories/departments.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: departments +# +# id :integer not null, primary key +# code :string +# name :string +# created_at :datetime not null +# updated_at :datetime not null +# org_id :integer +# +# Indexes +# +# index_departments_on_org_id (org_id) +# + +FactoryBot.define do + factory :department do + name { Faker::Commerce.department } + code { SecureRandom.hex(5) } + org_id { Faker::Number.number(5) } + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index e139eb4..9233293 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # == Schema Information # # Table name: users @@ -30,6 +32,7 @@ # surname :string # created_at :datetime not null # updated_at :datetime not null +# department_id :integer # invited_by_id :integer # language_id :integer # org_id :integer @@ -41,6 +44,7 @@ # # Foreign Keys # +# fk_rails_... (department_id => departments.id) # fk_rails_... (language_id => languages.id) # fk_rails_... (org_id => orgs.id) # @@ -56,7 +60,9 @@ trait :org_admin do after(:create) do |user, evaluator| - %w[modify_templates modify_guidance change_org_details grant_permissions].each do |perm_name| + %w[modify_templates modify_guidance + change_org_details + grant_permissions].each do |perm_name| user.perms << Perm.find_or_create_by(name: perm_name) end end diff --git a/spec/models/department_spec.rb b/spec/models/department_spec.rb new file mode 100644 index 0000000..014859d --- /dev/null +++ b/spec/models/department_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Department, type: :model do + + context "validations" do + + it { is_expected.to validate_presence_of(:org) } + + it { is_expected.to validate_presence_of(:name) } + + it { is_expected.to allow_value(nil).for(:code) } + + it "validates uniqueness of name" do + org = create(:org) + subject = create(:department, org_id: org.id) + expect(subject).to validate_uniqueness_of(:name) + .scoped_to(:org_id) + .with_message("must be unique") + end + + end + + context "associations" do + + it { is_expected.to belong_to :org } + + it { is_expected.to have_many :users } + + end +end