diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 85e9a0a43..c04301f27 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -132,6 +132,7 @@ Metrics/BlockLength: - 'config/environments/production.rb' - 'config/routes.rb' - 'db/migrate/20180828145628_vanity_migration.rb' + - 'db/seeds.rb' - 'features/support/puffing_billy.rb' - 'lib/tasks/cucumber.rake' - 'spec/controllers/application_controller_spec.rb' @@ -224,6 +225,7 @@ Metrics/MethodLength: - 'db/migrate/20140914202645_create_activities.rb' - 'db/migrate/20161103011445_create_friendly_id_slugs.rb' - 'db/migrate/20180828145628_vanity_migration.rb' + - 'db/migrate/20230309172525_rolify_create_roles.rb' - 'db/migrate/20230314192607_create_active_storage_tables.active_storage.rb' - 'features/step_definitions/basic_steps.rb' - 'features/step_definitions/contained_search_steps.rb' diff --git a/Gemfile b/Gemfile index 6a85dbac3..d8fe9f697 100644 --- a/Gemfile +++ b/Gemfile @@ -72,6 +72,7 @@ gem 'rack-timeout' gem 'rails_autolink' gem 'recaptcha', require: 'recaptcha/rails' gem 'redcarpet' +gem 'rolify' gem 'ruby-gitter' gem 'sass-rails', '>= 5' gem 'seed_dump' diff --git a/Gemfile.lock b/Gemfile.lock index 5cbb5707a..ae5c30160 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -592,6 +592,7 @@ GEM mime-types (>= 1.16, < 4.0) netrc (~> 0.8) rexml (3.2.5) + rolify (6.0.1) rspec (3.12.0) rspec-core (~> 3.12.0) rspec-expectations (~> 3.12.0) @@ -873,6 +874,7 @@ DEPENDENCIES rb-readline recaptcha redcarpet + rolify redis rspec-activemodel-mocks rspec-html-matchers diff --git a/app/assets/stylesheets/global/users.scss b/app/assets/stylesheets/global/users.scss index caf1d3d0d..7066ba8c1 100644 --- a/app/assets/stylesheets/global/users.scss +++ b/app/assets/stylesheets/global/users.scss @@ -43,6 +43,14 @@ @extend .user-skills; } +.user-roles { + @extend .user-skills; + label { + display: inline-block; + padding-left: 5px; + } +} + .user-summary { overflow: hidden; diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb new file mode 100644 index 000000000..3b3ddf962 --- /dev/null +++ b/app/controllers/courses_controller.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class CoursesController < ApplicationController + def create + @course = Course.create(course_params) + end + + private + + def course_params + params.require(:course).permit(:title, :description, :status, :user_id, :slack_channel_name) + end +end diff --git a/app/models/course.rb b/app/models/course.rb new file mode 100644 index 000000000..0ad7e96db --- /dev/null +++ b/app/models/course.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class Course < ApplicationRecord + extend FriendlyId + include Filterable + friendly_id :title, use: %i(slugged history) + + validates :title, :description, :status, presence: true + validates :description, presence: true, length: { minimum: 5 } + + belongs_to :user, optional: true + include UserNullable + + scope :active, -> { where('status ILIKE ?', 'active').order(:title) } + + def slack_channel + "https://agileventures.slack.com/app_redirect?channel=#{slack_channel_name}" + end +end diff --git a/app/models/role.rb b/app/models/role.rb new file mode 100644 index 000000000..1a14d7eeb --- /dev/null +++ b/app/models/role.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class Role < ApplicationRecord + has_many :users, through: :users_roles + + belongs_to :resource, + polymorphic: true, + optional: true + + validates :resource_type, + inclusion: { in: Rolify.resource_types }, + allow_nil: true + + validates :name, presence: true + + scopify +end diff --git a/app/models/user.rb b/app/models/user.rb index f2df1867c..8d7fe067d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,8 +1,12 @@ # frozen_string_literal: true class User < ApplicationRecord + rolify acts_as_paranoid + after_validation :geocode, if: ->(obj) { obj.last_sign_in_ip_changed? } + after_create :assign_default_role + include Filterable extend Forwardable @@ -35,7 +39,6 @@ class User < ApplicationRecord extend FriendlyId friendly_id :display_name, use: :slugged - after_validation :geocode, if: ->(obj) { obj.last_sign_in_ip_changed? } # after_validation -> { KarmaCalculator.new(self).perform } has_many :authentications, dependent: :destroy @@ -45,9 +48,14 @@ class User < ApplicationRecord has_many :event_instances has_many :commit_counts has_many :status + has_many :courses has_many :subscriptions, autosave: true + def assign_default_role + add_role(:student) if roles.blank? + end + # ultimately replacing the field stripe_customer def stripe_customer_id subscription = current_subscription diff --git a/app/views/users/profile/_detail.html.erb b/app/views/users/profile/_detail.html.erb index cfde099d3..ede720b4b 100644 --- a/app/views/users/profile/_detail.html.erb +++ b/app/views/users/profile/_detail.html.erb @@ -9,6 +9,7 @@ <% if presenter.contributed? %>
  • Activity
  • <% end %> +
  • Roles
  • @@ -47,6 +48,18 @@
    <% end %> +
    +
    +

    Roles

    + +
    +
    <% if presenter.contributed? %>
    diff --git a/config/initializers/rolify.rb b/config/initializers/rolify.rb new file mode 100644 index 000000000..ace6ec8d9 --- /dev/null +++ b/config/initializers/rolify.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +Rolify.configure do |config| + # By default ORM adapter is ActiveRecord. uncomment to use mongoid + # config.use_mongoid + + # Dynamic shortcuts for User class (user.is_admin? like methods). Default is: false + # config.use_dynamic_shortcuts + + # Configuration to remove roles from database once the last resource is removed. Default is: true + # config.remove_role_if_empty = false +end diff --git a/db/migrate/20230302160750_create_courses.rb b/db/migrate/20230302160750_create_courses.rb new file mode 100644 index 000000000..aec42d80c --- /dev/null +++ b/db/migrate/20230302160750_create_courses.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class CreateCourses < ActiveRecord::Migration[7.0] + def change + create_table :courses do |t| + t.string :title + t.text :description + t.string :slug, null: false + t.integer :user_id + t.string :status + t.string :slack_channel_name + t.timestamps + end + add_index :courses, :user_id + end +end diff --git a/db/migrate/20230309172525_rolify_create_roles.rb b/db/migrate/20230309172525_rolify_create_roles.rb new file mode 100644 index 000000000..76ecd2516 --- /dev/null +++ b/db/migrate/20230309172525_rolify_create_roles.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class RolifyCreateRoles < ActiveRecord::Migration[7.0] + def change + create_table(:roles) do |t| + t.string :name + t.references :resource, polymorphic: true + t.timestamps + end + + create_table(:users_roles, id: false) do |t| + t.references :user + t.references :role + t.timestamps + end + + add_index(:roles, %i(name resource_type resource_id)) + add_index(:users_roles, %i(user_id role_id)) + end +end diff --git a/db/schema.rb b/db/schema.rb index d2fde5a0c..b376eb852 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -97,6 +97,18 @@ t.index ["user_id"], name: "index_commit_counts_on_user_id" end + create_table "courses", force: :cascade do |t| + t.string "title" + t.text "description" + t.string "slug", null: false + t.integer "user_id" + t.string "status" + t.string "slack_channel_name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_courses_on_user_id" + end + create_table "documents", id: :serial, force: :cascade do |t| t.string "title" t.text "body" @@ -269,6 +281,16 @@ t.index ["slack_channel_id", "project_id"], name: "slack_channel_name", unique: true end + create_table "roles", force: :cascade do |t| + t.string "name" + t.string "resource_type" + t.bigint "resource_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id" + t.index ["resource_type", "resource_id"], name: "index_roles_on_resource" + end + create_table "slack_channels", force: :cascade do |t| t.string "environment" t.string "code" @@ -369,6 +391,16 @@ t.index ["slug"], name: "index_users_on_slug", unique: true end + create_table "users_roles", id: false, force: :cascade do |t| + t.bigint "user_id" + t.bigint "role_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["role_id"], name: "index_users_roles_on_role_id" + t.index ["user_id", "role_id"], name: "index_users_roles_on_user_id_and_role_id" + t.index ["user_id"], name: "index_users_roles_on_user_id" + end + create_table "versions", id: :serial, force: :cascade do |t| t.string "item_type", null: false t.integer "item_id", null: false diff --git a/db/seeds.rb b/db/seeds.rb index 0df425c96..984cc709e 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -klasses = [Project, Document, User, Subscription, Karma, Plan, Article, Event, EventInstance, Karma] +klasses = [Project, Document, User, Subscription, Karma, Plan, Article, Event, EventInstance, Karma, Course] old_counts = klasses.map(&:count) should_prompt = old_counts.min.positive? @@ -77,6 +77,20 @@ def get_country ) puts 'Created default projects' + + u.courses.create!( + title: 'OpenSource', + description: 'How to contribute to open source projects.', + status: 'Active' + ) + + u.courses.create!( + title: 'GitCourse', + description: 'Learn how to use git and github.', + status: 'Active' + ) + + puts 'Created default courses' break elsif %w(n no).include?(response) break @@ -108,6 +122,15 @@ def get_country ) end end + + 3.times do + p = u.courses.create( + title: Faker::Educator.course_name, + description: Faker::TvShows::GameOfThrones.quote, + status: 'active', + created_at: 1.month.ago + ) + end end # Premium Users diff --git a/spec/factories/courses.rb b/spec/factories/courses.rb new file mode 100644 index 000000000..249db68a4 --- /dev/null +++ b/spec/factories/courses.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :course do + sequence(:title) { |n| "Course #{n}" } + sequence(:slug) { |n| "course-#{n}" } + description { 'Warp fields stabilize.' } + status { 'active' } + end +end diff --git a/spec/factories/roles.rb b/spec/factories/roles.rb new file mode 100644 index 000000000..01a58391d --- /dev/null +++ b/spec/factories/roles.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :role do + name { 'MyString' } + end +end diff --git a/spec/models/course_spec.rb b/spec/models/course_spec.rb new file mode 100644 index 000000000..863a40667 --- /dev/null +++ b/spec/models/course_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'course' + +RSpec.describe Course, type: :model do + it { is_expected.to belong_to(:user).optional(true) } + + context '#save' do + subject { build_stubbed(:course) } + + it 'should be a valid with all the correct attributes' do + expect(subject).to be_valid + end + + it 'should be invalid without title' do + subject.title = '' + expect(subject).to_not be_valid + end + + it 'should be invalid without description' do + subject.description = '' + expect(subject).to_not be_valid + end + + it 'should be invalid without status' do + subject.status = '' + expect(subject).to_not be_valid + end + end +end diff --git a/spec/models/role_spec.rb b/spec/models/role_spec.rb new file mode 100644 index 000000000..b0b90a4ec --- /dev/null +++ b/spec/models/role_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Role, type: :model do + context '#save' do + subject { build_stubbed(:role) } + + it 'should be a valid with all the correct attributes' do + expect(subject).to be_valid + end + + it 'should be invalid without name' do + subject.name = '' + expect(subject).to_not be_valid + end + end +end