diff --git a/Gemfile b/Gemfile
index 667035c..8c9a143 100644
--- a/Gemfile
+++ b/Gemfile
@@ -3,7 +3,7 @@ source 'https://rubygems.org'
gem 'figaro', '~> 1.1.1'
gem 'bcrypt', '~> 3.1.7'
gem 'mysql2', '>= 0.3.18', '< 0.5'
-gem 'rails', '~> 5.0.0'
+gem 'rails', '~> 5.0', '>= 5.0.0.1'
gem 'jbuilder', '~> 2.6'
gem 'jquery-rails'
diff --git a/Gemfile.lock b/Gemfile.lock
index 31a7bcb..c9cce22 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,46 +1,46 @@
GEM
remote: https://rubygems.org/
specs:
- actioncable (5.0.0)
- actionpack (= 5.0.0)
+ actioncable (5.0.0.1)
+ actionpack (= 5.0.0.1)
nio4r (~> 1.2)
websocket-driver (~> 0.6.1)
- actionmailer (5.0.0)
- actionpack (= 5.0.0)
- actionview (= 5.0.0)
- activejob (= 5.0.0)
+ actionmailer (5.0.0.1)
+ actionpack (= 5.0.0.1)
+ actionview (= 5.0.0.1)
+ activejob (= 5.0.0.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
- actionpack (5.0.0)
- actionview (= 5.0.0)
- activesupport (= 5.0.0)
+ actionpack (5.0.0.1)
+ actionview (= 5.0.0.1)
+ activesupport (= 5.0.0.1)
rack (~> 2.0)
rack-test (~> 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
- actionview (5.0.0)
- activesupport (= 5.0.0)
+ actionview (5.0.0.1)
+ activesupport (= 5.0.0.1)
builder (~> 3.1)
erubis (~> 2.7.0)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
- activejob (5.0.0)
- activesupport (= 5.0.0)
+ activejob (5.0.0.1)
+ activesupport (= 5.0.0.1)
globalid (>= 0.3.6)
- activemodel (5.0.0)
- activesupport (= 5.0.0)
- activerecord (5.0.0)
- activemodel (= 5.0.0)
- activesupport (= 5.0.0)
+ activemodel (5.0.0.1)
+ activesupport (= 5.0.0.1)
+ activerecord (5.0.0.1)
+ activemodel (= 5.0.0.1)
+ activesupport (= 5.0.0.1)
arel (~> 7.0)
- activesupport (5.0.0)
+ activesupport (5.0.0.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7)
minitest (~> 5.1)
tzinfo (~> 1.1)
addressable (2.4.0)
ansi (1.5.0)
- arel (7.1.0)
+ arel (7.1.1)
ast (2.3.0)
awesome_print (1.7.0)
bcrypt (3.1.11)
@@ -75,7 +75,7 @@ GEM
thor (~> 0.14)
formatador (0.2.5)
foundation_emails (2.2.0.0)
- globalid (0.3.6)
+ globalid (0.3.7)
activesupport (>= 4.1.0)
guard (2.14.0)
formatador (>= 0.2.4)
@@ -92,7 +92,7 @@ GEM
guard (~> 2.8)
guard-compat (~> 1.0)
multi_json (~> 1.8)
- guard-minitest (2.4.5)
+ guard-minitest (2.4.6)
guard-compat (~> 1.2)
minitest (>= 3.0)
guard-rubocop (1.2.0)
@@ -111,7 +111,7 @@ GEM
jbuilder (2.6.0)
activesupport (>= 3.0.0, < 5.1)
multi_json (~> 1.2)
- jquery-rails (4.1.1)
+ jquery-rails (4.2.1)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
@@ -135,7 +135,7 @@ GEM
mime-types-data (3.2016.0521)
mini_portile2 (2.1.0)
minitest (5.9.0)
- minitest-reporters (1.1.10)
+ minitest-reporters (1.1.11)
ansi
builder
minitest (>= 5.0)
@@ -151,7 +151,7 @@ GEM
nokogiri (1.6.8)
mini_portile2 (~> 2.1.0)
pkg-config (~> 1.1.7)
- notiffany (0.1.0)
+ notiffany (0.1.1)
nenv (~> 0.1)
shellany (~> 0.0)
parser (2.3.1.2)
@@ -179,17 +179,17 @@ GEM
rack
rack-test (0.6.3)
rack (>= 1.0)
- rails (5.0.0)
- actioncable (= 5.0.0)
- actionmailer (= 5.0.0)
- actionpack (= 5.0.0)
- actionview (= 5.0.0)
- activejob (= 5.0.0)
- activemodel (= 5.0.0)
- activerecord (= 5.0.0)
- activesupport (= 5.0.0)
+ rails (5.0.0.1)
+ actioncable (= 5.0.0.1)
+ actionmailer (= 5.0.0.1)
+ actionpack (= 5.0.0.1)
+ actionview (= 5.0.0.1)
+ activejob (= 5.0.0.1)
+ activemodel (= 5.0.0.1)
+ activerecord (= 5.0.0.1)
+ activesupport (= 5.0.0.1)
bundler (>= 1.3.0, < 2.0)
- railties (= 5.0.0)
+ railties (= 5.0.0.1)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.1)
actionpack (~> 5.x)
@@ -198,16 +198,16 @@ GEM
rails-dom-testing (2.0.1)
activesupport (>= 4.2.0, < 6.0)
nokogiri (~> 1.6.0)
- rails-erd (1.4.7)
+ rails-erd (1.5.0)
activerecord (>= 3.2)
activesupport (>= 3.2)
choice (~> 0.2.0)
ruby-graphviz (~> 1.2)
rails-html-sanitizer (1.0.3)
loofah (~> 2.0)
- railties (5.0.0)
- actionpack (= 5.0.0)
- activesupport (= 5.0.0)
+ railties (5.0.0.1)
+ actionpack (= 5.0.0.1)
+ activesupport (= 5.0.0.1)
method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
@@ -228,7 +228,7 @@ GEM
unicode-display_width (~> 1.0, >= 1.0.1)
ruby-graphviz (1.2.2)
ruby-progressbar (1.8.1)
- ruby_dep (1.3.1)
+ ruby_dep (1.4.0)
sass (3.4.22)
sass-rails (5.0.6)
railties (>= 4.0.0, < 6)
@@ -258,12 +258,12 @@ GEM
thor (0.19.1)
thread_safe (0.3.5)
tilt (2.0.5)
- turbolinks (5.0.0)
+ turbolinks (5.0.1)
turbolinks-source (~> 5)
turbolinks-source (5.0.0)
tzinfo (1.2.2)
thread_safe (~> 0.1)
- uglifier (3.0.0)
+ uglifier (3.0.2)
execjs (>= 0.3.0, < 3)
unf (0.1.4)
unf_ext
@@ -308,7 +308,7 @@ DEPENDENCIES
pry-rails
puma (~> 3.0)
rack-livereload
- rails (~> 5.0.0)
+ rails (~> 5.0, >= 5.0.0.1)
rails-controller-testing
rails-erd
rubocop (~> 0.42.0)
diff --git a/Guardfile b/Guardfile
index e686215..0b31b2e 100644
--- a/Guardfile
+++ b/Guardfile
@@ -15,7 +15,21 @@
#
# and, you'll have to watch "config/Guardfile" instead of "Guardfile"
-guard :minitest, spring: true, all_after_pass: true, all_on_start: false do
+guard 'livereload' do
+ watch(%r{app/assets/.+\.(scss|css|js|erb)})
+ watch(%r{app/views/.+\.(erb|haml|slim)$})
+ watch(%r{app/controllers/.+\.rb})
+ watch(%r{app/helpers/.+\.rb})
+ watch(%r{public/.+\.(css|js|html)})
+ watch(%r{config/locales/.+\.yml})
+
+ # Rails Assets Pipeline
+ watch(%r{(app|vendor)(/assets/\w+/(.+\.(scss|css|js|erb|html|png|jpg))).*}) do |m|
+ "/assets/#{m[3]}"
+ end
+end
+
+guard :minitest, spring: true, all_after_pass: true do
watch(%r{^test/test_helper\.rb$}) { 'test' }
watch(%r{^test/(.*)\/?(.*)_test\.rb$})
@@ -32,20 +46,6 @@ guard :minitest, spring: true, all_after_pass: true, all_on_start: false do
watch(%r{^app/views/(.*_mailer/)?([^/]+)\.erb$}) { ["test/mailers", "test/integration"] }
end
-guard 'livereload' do
- watch(%r{app/assets/.+\.(scss|css|js|erb)})
- watch(%r{app/views/.+\.(erb|haml|slim)$})
- watch(%r{app/controllers/.+\.rb})
- watch(%r{app/helpers/.+\.rb})
- watch(%r{public/.+\.(css|js|html)})
- watch(%r{config/locales/.+\.yml})
-
- # Rails Assets Pipeline
- watch(%r{(app|vendor)(/assets/\w+/(.+\.(scss|css|js|erb|html|png|jpg))).*}) do |m|
- "/assets/#{m[3]}"
- end
-end
-
guard :rubocop do
watch(/.+\.rb$/)
watch(/Rakefile/)
diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js
new file mode 100644
index 0000000..7633280
--- /dev/null
+++ b/app/assets/javascripts/admin.js
@@ -0,0 +1,16 @@
+$(function(){
+
+ $("form").on('click', "[data-id=input_option_adder]", function(){
+ var $new_li = $(this).siblings('li').clone();
+ $new_li.attr('style', '');
+ $("[data-id=input_option_list]").append($new_li);
+ $new_li.find('input').focus();
+ });
+
+ $("#question_input_type").on('change', function(){
+ var qid = $(this).attr('data-qid') === undefined ? '' : "/" + $(this).attr('data-qid');
+ // /admin/question(/:question_id)/options/:input_type
+ $("[data-id=input-options-wrapper]").load("/admin/question" + qid + "/options/" + $(this).val());
+ });
+
+});
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index e716f6f..3cfbc1b 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -21,3 +21,6 @@
//= require summary-edit
//= require textarea-limit
//= require live-coder
+
+
+//= require admin
diff --git a/app/controllers/admin/auth_controller.rb b/app/controllers/admin/auth_controller.rb
new file mode 100644
index 0000000..a6db502
--- /dev/null
+++ b/app/controllers/admin/auth_controller.rb
@@ -0,0 +1,25 @@
+module Admin
+ class AuthController < AdminController
+ skip_before_action :authorize_admin
+
+ def login
+ end
+
+ def auth
+ admin = User.find_by(email: auth_params[:email], role: 'admin')
+
+ if admin && admin.authenticate(auth_params[:password])
+ session[:user] = admin.to_i
+ redirect_to admin_path
+ else
+ redirect_to admin_login_path,
+ flash: { error: "Sorry, incorrect email or password. Please try again." }
+ end
+ end
+
+ def logout
+ reset_session
+ redirect_to admin_login_path
+ end
+ end
+end
diff --git a/app/controllers/admin/question_controller.rb b/app/controllers/admin/question_controller.rb
new file mode 100644
index 0000000..c066353
--- /dev/null
+++ b/app/controllers/admin/question_controller.rb
@@ -0,0 +1,69 @@
+module Admin
+ class QuestionController < AdminController
+ def index
+ @questions = Question.includes(:quiz).order("quizzes.name", { active: :desc }, :sort)
+ end
+
+ def new
+ @question = Question.new(active: true)
+ @quizzes = Quiz.all
+ end
+
+ def create
+ @quizzes = Quiz.all
+ @question = Question.create(process_question_params)
+
+ if @question.persisted?
+ redirect_to admin_questions_path, flash: { notice: "Sucessfully created question" }
+ else
+ flash[:error] = "Failed to save question."
+ render :new
+ end
+ end
+
+ def view
+ @question = Question.includes(:quiz).find(params[:question_id])
+ end
+
+ def edit
+ @quizzes = Quiz.all
+ @question = Question.includes(:quiz).find(params[:question_id])
+ end
+
+ def update
+ @quizzes = Quiz.all
+ @question = Question.find(params[:question_id])
+
+ if @question.update_attributes(process_question_params)
+ redirect_to admin_question_path(@question.to_i),
+ flash: { notice: "Sucessfully updated question" }
+ else
+ flash[:error] = "Failed to update question."
+ render :edit
+ end
+ end
+
+ def options
+ @question = params[:question_id].present? ? Question.find(params[:question_id]) : Question.new
+ render layout: false
+ end
+
+ private
+
+ def question_params
+ params.require(:question).permit(
+ :quiz_id, :question, :category, :input_type, :sort, :active, :input_options,
+ multi_choice: [], live_code: [:later, :html, :css, :js, :text]
+ )
+ end
+
+ def process_question_params
+ question = question_params
+ question[:input_options] = question_params[:multi_choice] unless question_params[:multi_choice].nil?
+ question[:input_options] = question_params[:live_coder] unless question_params[:live_coder].nil?
+ question.delete(:multi_choice)
+ question.delete(:live_coder)
+ question
+ end
+ end
+end
diff --git a/app/controllers/admin/quiz_controller.rb b/app/controllers/admin/quiz_controller.rb
new file mode 100644
index 0000000..82c2de4
--- /dev/null
+++ b/app/controllers/admin/quiz_controller.rb
@@ -0,0 +1,48 @@
+module Admin
+ class QuizController < AdminController
+ def index
+ @quizzes = Quiz.all
+ end
+
+ def new
+ @quiz = Quiz.new
+ end
+
+ def create
+ @quiz = Quiz.create(quiz_params)
+
+ if @quiz.persisted?
+ redirect_to admin_quizzes_path, flash: { notice: "Sucessfully created quiz" }
+ else
+ flash[:error] = "Failed to save quiz."
+ render :new
+ end
+ end
+
+ def view
+ @quiz = Quiz.find(params[:quiz_id])
+ end
+
+ def edit
+ @quiz = Quiz.find(params[:quiz_id])
+ end
+
+ def update
+ @quiz = Quiz.find(params[:quiz_id])
+
+ if @quiz.update_attributes(quiz_params)
+ redirect_to admin_quiz_path(@quiz.to_i),
+ flash: { notice: "Sucessfully updated quiz" }
+ else
+ flash[:error] = "Failed to update quiz."
+ render :edit
+ end
+ end
+
+ private
+
+ def quiz_params
+ params.require(:quiz).permit(:name, :dept, :unit)
+ end
+ end
+end
diff --git a/app/controllers/admin/user_controller.rb b/app/controllers/admin/user_controller.rb
new file mode 100644
index 0000000..23c11dd
--- /dev/null
+++ b/app/controllers/admin/user_controller.rb
@@ -0,0 +1,50 @@
+module Admin
+ class UserController < AdminController
+ def index
+ @users = User.order(:name)
+ end
+
+ def new
+ @user = User.new
+ end
+
+ def create
+ default_passwd = SecureRandom.urlsafe_base64(12)
+ @user = User.create({ password: default_passwd }.merge(user_params.to_h))
+
+ if @user.persisted?
+ # TODO: UserMailer.welcome(@user, default_passwd).deliver_now
+ redirect_to admin_users_path, flash: { notice: "Sucessfully created user #{@user.name}" }
+ else
+ flash[:error] = "Failed to save user."
+ render :new
+ end
+ end
+
+ def view
+ @user = User.find(params[:user_id])
+ end
+
+ def edit
+ @user = User.find(params[:user_id])
+ end
+
+ def update
+ @user = User.find(params[:user_id])
+
+ if @user.update_attributes(user_params)
+ redirect_to admin_user_path(@user.to_i),
+ flash: { notice: "Sucessfully updated #{@user.name}" }
+ else
+ flash[:error] = "Failed to update user."
+ render :edit
+ end
+ end
+
+ private
+
+ def user_params
+ params.require(:user).permit(:name, :email, :role, :password)
+ end
+ end
+end
diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb
new file mode 100644
index 0000000..37a79b4
--- /dev/null
+++ b/app/controllers/admin_controller.rb
@@ -0,0 +1,21 @@
+class AdminController < ApplicationController
+ layout 'admin'
+ before_action :authorize_admin
+
+ def dashboard
+ @quizzes = Quiz.includes(:questions).all
+ @users = User.order(:role, :name)
+ end
+
+ def current_admin
+ user_args = { id: session[:user], role: 'admin' }
+ @current_admin ||= User.find_by(user_args) if session[:user]
+ end
+ helper_method :current_admin
+
+ private
+
+ def authorize_admin
+ redirect_to admin_login_path unless current_admin
+ end
+end
diff --git a/app/controllers/quiz_controller.rb b/app/controllers/quiz_controller.rb
index d604fed..016bd76 100644
--- a/app/controllers/quiz_controller.rb
+++ b/app/controllers/quiz_controller.rb
@@ -86,6 +86,7 @@ class QuizController < ApplicationController
end
end
+ # TODO: maybe a better way to do this. See Admin/QuestionController#process_question_params
def process_text
@answer.update(answer: answer_params[:text],
saved: params.key?(:save),
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index feaaee8..876a05c 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -9,4 +9,21 @@ module ApplicationHelper
["15+ Years", "15+"]
], disabled: "-", selected: (val.blank? ? '' : val))
end
+
+ def admin_role_options val
+ options_for_select([
+ %w(Reviewer reviewer),
+ %w(Recruiter recruiter),
+ %w(Admin admin)
+ ], disabled: "-", selected: (val.blank? ? '' : val))
+ end
+
+ def question_type_options val
+ options_for_select([
+ %w(Text text),
+ %w(Radio radio),
+ %w(Checkbox checkbox),
+ %w(Coder live_code)
+ ], selected: (val.blank? ? '' : val))
+ end
end
diff --git a/app/models/question.rb b/app/models/question.rb
index 3abd437..cdcdb3e 100644
--- a/app/models/question.rb
+++ b/app/models/question.rb
@@ -3,4 +3,18 @@ class Question < ApplicationRecord
has_many :answers
belongs_to :quiz
+
+ before_validation :compact_input_options
+
+ validates :quiz_id, presence: true
+ validates :question, presence: true
+ validates :category, presence: true
+ validates :input_type, presence: true
+ validates :input_options, input_options_presence: true
+
+ private
+
+ def compact_input_options
+ self.input_options = input_options.reject(&:blank?)
+ end
end
diff --git a/app/models/quiz.rb b/app/models/quiz.rb
index b92a063..bf967e4 100644
--- a/app/models/quiz.rb
+++ b/app/models/quiz.rb
@@ -1,4 +1,8 @@
class Quiz < ApplicationRecord
has_many :questions, -> { order(:sort) }
has_many :candidates
+
+ validates_presence_of :name
+ validates_presence_of :dept
+ validates_presence_of :unit
end
diff --git a/app/models/user.rb b/app/models/user.rb
index e1b1ee7..1da0679 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,4 +1,8 @@
class User < ApplicationRecord
has_secure_password
has_many :candidates, foreign_key: "recruiter_id"
+
+ validates_presence_of :email
+ validates_presence_of :name
+ validates_presence_of :role
end
diff --git a/app/validators/input_options_presence_validator.rb b/app/validators/input_options_presence_validator.rb
new file mode 100644
index 0000000..0bb6284
--- /dev/null
+++ b/app/validators/input_options_presence_validator.rb
@@ -0,0 +1,11 @@
+class InputOptionsPresenceValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ return true unless record.input_type =~ /radio|check/i
+ return true if value.present? && value.count > 0
+
+ record.errors[attribute] << (options[:message] ||
+ "You must provide answer options for the selected input type.")
+
+ false
+ end
+end
diff --git a/app/views/admin/auth/login.html.erb b/app/views/admin/auth/login.html.erb
new file mode 100644
index 0000000..b54def1
--- /dev/null
+++ b/app/views/admin/auth/login.html.erb
@@ -0,0 +1,21 @@
+Admin Login
+
+ <% if flash[:error].present? %>
+ Quizzes
+ <%= render partial: 'admin/quiz/table_list', locals: { quizzes: @quizzes } %>
+ <%= link_to('New Quiz', admin_new_quiz_path, { class: 'btn' }) %>
+ Users
+ <%= render partial: 'admin/user/table_list', locals: { users: @users } %>
+ <%= link_to('New User', admin_new_user_path, { class: 'btn' }) %>
+
Sort | +Question | +Type | +Category | +Active | ++ |
---|---|---|---|---|---|
<%= question.sort %> | +<%= question.question %> | +<%= question.input_type %> | +<%= question.category %> | +<%= question.active unless question.active? %> | +<%= link_to 'Edit', admin_edit_question_path(question.to_i), { class: 'btn tertiary-btn' } %> | +
Category | +<%= @question.category %> | +
---|---|
Type | +<%= @question.input_type %> | +
Sort | +<%= @question.sort %> | +
+ | + <%= check_box_tag 'question_active', nil, @question.active?, {disabled: true} %> + <%= label_tag 'question_active', 'Active' %> + | +
<%= @question.question %>
+ + <%= fields_for @question do |fields| %> + <%= render partial: "admin/question/#{@question.input_type}", locals: {question: @question, disable: true, fields: fields } %> + <% end %> + + <%= link_to('Edit', admin_edit_question_path(@question.to_i), { class: 'btn' }) %> + + <%= link_to('View Quiz', admin_quiz_path(@question.quiz_id), { class: 'btn' }) %> +Name | +Dept | +Unit | +Questions | ++ |
---|---|---|---|---|
<%= link_to quiz.name, admin_quiz_path(quiz.to_i) %> | +<%= quiz.dept %> | +<%= quiz.unit %> | +<%= quiz.questions.count %> | +<%= link_to 'edit', admin_edit_quiz_path(quiz.to_i), { class: 'btn tertiary-btn' } %> | +
<%= @quiz.name %>
+<%= @quiz.dept %>
+<%= @quiz.unit %>
+ <%= link_to('Edit', admin_edit_quiz_path(@quiz.to_i), { class: 'btn' }) %> +User | +Role | ++ | |
---|---|---|---|
<%= link_to user.name, admin_user_path(user.to_i) %> | +<%= mail_to(user.email) %> | +<%= user.role %> | +<%= link_to 'edit', admin_edit_user_path(user.to_i), { class: 'btn tertiary-btn' } %> | +
<%= @user.name %>
+<%= mail_to(@user.email) %>
+<%= @user.role %>
+ <%= link_to('Edit', admin_edit_user_path(@user.to_i), { class: 'btn' }) %> +