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
+
+ <%= collection_check_boxes :user, :role_ids, Role.all, :id, :name do |b| %>
+ -
+ <%= b.check_box %> <%= b.label %>
+
+ <% end %>
+
+
+
<% 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