diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb new file mode 100644 --- /dev/null +++ b/app/controllers/groups_controller.rb @@ -0,0 +1,86 @@ +class GroupsController < ApplicationController + before_action :set_group, only: [:show, :edit, :update, :destroy, + :add_user, :remove_user, + :add_problem, :remove_problem, + ] + before_action :authenticate, :admin_authorization + + # GET /groups + def index + @groups = Group.all + end + + # GET /groups/1 + def show + end + + # GET /groups/new + def new + @group = Group.new + end + + # GET /groups/1/edit + def edit + end + + # POST /groups + def create + @group = Group.new(group_params) + + if @group.save + redirect_to @group, notice: 'Group was successfully created.' + else + render :new + end + end + + # PATCH/PUT /groups/1 + def update + if @group.update(group_params) + redirect_to @group, notice: 'Group was successfully updated.' + else + render :edit + end + end + + # DELETE /groups/1 + def destroy + @group.destroy + redirect_to groups_url, notice: 'Group was successfully destroyed.' + end + + def remove_user + user = User.find(params[:user_id]) + @group.users.delete(user) + redirect_to group_path(@group), notice: "User #{user.login} was removed from the group #{@group.name}" + end + + def add_user + user = User.find(params[:user_id]) + @group.users << user + redirect_to group_path(@group), notice: "User #{user.login} was add to the group #{@group.name}" + end + + def remove_problem + problem = Problem.find(params[:problem_id]) + @group.problems.delete(problem) + redirect_to group_path(@group), notice: "Problem #{problem.name} was removed from the group #{@group.name}" + end + + def add_problem + problem = Problem.find(params[:problem_id]) + @group.problems << problem + redirect_to group_path(@group), notice: "Problem #{problem.name} was add to the group #{@group.name}" + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_group + @group = Group.find(params[:id]) + end + + # Only allow a trusted parameter "white list" through. + def group_params + params.require(:group).permit(:name, :description) + end +end diff --git a/app/controllers/problems_controller.rb b/app/controllers/problems_controller.rb --- a/app/controllers/problems_controller.rb +++ b/app/controllers/problems_controller.rb @@ -195,6 +195,11 @@ set_available(true) elsif params.has_key? 'disable_problem' set_available(false) + elsif params.has_key? 'add_group' + group = Group.find(params[:group_id]) + get_problems_from_params.each do |p| + group.problems << p + end end redirect_to :action => 'manage' end diff --git a/app/controllers/user_admin_controller.rb b/app/controllers/user_admin_controller.rb --- a/app/controllers/user_admin_controller.rb +++ b/app/controllers/user_admin_controller.rb @@ -440,10 +440,9 @@ u.save end end - if @action[:add_group] - @uses.each do |u| - - end + if @action[:add_group] and @action[:group_name] + @group = Group.find(@action[:group_name]) + @users.each { |user| @group.users << user } end end end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb new file mode 100644 --- /dev/null +++ b/app/helpers/groups_helper.rb @@ -0,0 +1,2 @@ +module GroupsHelper +end diff --git a/app/models/grader_configuration.rb b/app/models/grader_configuration.rb --- a/app/models/grader_configuration.rb +++ b/app/models/grader_configuration.rb @@ -12,6 +12,7 @@ MULTIPLE_IP_LOGIN_KEY = 'right.multiple_ip_login' VIEW_TESTCASE = 'right.view_testcase' SINGLE_USER_KEY = 'system.single_user_mode' + SYSTEM_USE_PROBLEM_GROUP = 'system.use_problem_group' cattr_accessor :config_cache cattr_accessor :task_grading_info_cache @@ -119,6 +120,10 @@ def self.analysis_mode? return get(SYSTEM_MODE_CONF_KEY) == 'analysis' end + + def self.use_problem_group? + return get(SYSTEM_USE_PROBLEM_GROUP) + end def self.contest_time_limit contest_time_str = GraderConfiguration[CONTEST_TIME_LIMIT_KEY] diff --git a/app/models/group.rb b/app/models/group.rb new file mode 100644 --- /dev/null +++ b/app/models/group.rb @@ -0,0 +1,5 @@ +class Group < ActiveRecord::Base + has_and_belongs_to_many :problems + has_and_belongs_to_many :users +end + diff --git a/app/models/problem.rb b/app/models/problem.rb --- a/app/models/problem.rb +++ b/app/models/problem.rb @@ -2,6 +2,7 @@ belongs_to :description has_and_belongs_to_many :contests, :uniq => true + has_and_belongs_to_many :groups has_many :test_pairs, :dependent => :delete_all has_many :testcases, :dependent => :destroy diff --git a/app/models/user.rb b/app/models/user.rb --- a/app/models/user.rb +++ b/app/models/user.rb @@ -7,6 +7,7 @@ class User < ActiveRecord::Base has_and_belongs_to_many :roles + has_and_belongs_to_many :groups has_many :test_requests, -> {order(submitted_at: DESC)} @@ -290,7 +291,11 @@ def available_problems if not GraderConfiguration.multicontests? - return Problem.available_problems + if GraderConfiguration.use_problem_group? + return available_problems_in_group + else + return Problem.available_problems + end else contest_problems = [] pin = {} @@ -307,6 +312,14 @@ end end + def available_problems_in_group + problem = [] + self.groups.each do |group| + group.problems.where(available: true).each { |p| problem << p } + end + return problem.uniq + end + def can_view_problem?(problem) if not GraderConfiguration.multicontests? return problem.available diff --git a/app/views/groups/_form.html.haml b/app/views/groups/_form.html.haml new file mode 100644 --- /dev/null +++ b/app/views/groups/_form.html.haml @@ -0,0 +1,16 @@ += form_for @group do |f| + - if @group.errors.any? + #error_explanation + %h2= "#{pluralize(@group.errors.count, "error")} prohibited this group from being saved:" + %ul + - @group.errors.full_messages.each do |msg| + %li= msg + + .form-group.field + = f.label :name + = f.text_field :name, class: 'form-control' + .form-group.field + = f.label :description + = f.text_field :description, class: 'form-control' + .form-group.actions + = f.submit 'Save', class: 'btn btn-primary' diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml new file mode 100644 --- /dev/null +++ b/app/views/groups/edit.html.haml @@ -0,0 +1,7 @@ +%h1 Editing group + += render 'form' + += link_to 'Show', @group +\| += link_to 'Back', groups_path diff --git a/app/views/groups/index.html.haml b/app/views/groups/index.html.haml new file mode 100644 --- /dev/null +++ b/app/views/groups/index.html.haml @@ -0,0 +1,24 @@ +%h1 Groups + +%p + = link_to 'New Group', new_group_path, class: 'btn btn-primary' +%table.table.table-hover + %thead + %tr + %th Name + %th Description + %th + %th + %th + + %tbody + - @groups.each do |group| + %tr + %td= group.name + %td= group.description + %td= link_to 'Show', group, class: 'btn btn-default' + %td= link_to 'Edit', edit_group_path(group), class: 'btn btn-default' + %td= link_to 'Destroy', group, :method => :delete, :data => { :confirm => 'Are you sure?' }, class: 'btn btn-danger' + +%br + diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml new file mode 100644 --- /dev/null +++ b/app/views/groups/new.html.haml @@ -0,0 +1,5 @@ +%h1 New group + += render 'form' + += link_to 'Back', groups_path diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml new file mode 100644 --- /dev/null +++ b/app/views/groups/show.html.haml @@ -0,0 +1,67 @@ +%p#notice= notice + +%p + %b Name: + = @group.name +%p + %b Description: + = @group.description + +%br += link_to 'Edit', edit_group_path(@group) +\| += link_to 'Back', groups_path + +.row + .col-md-6 + %h1 Users in this group + + =form_tag add_user_group_path(@group), class: 'form-inline' do + .form-group + =label_tag :user_id, "User" + =select_tag :user_id, options_from_collection_for_select(User.all,'id','full_name'), class: 'select2' + =submit_tag "Add",class: 'btn btn-primary' + + + %table.table.table-hover + %thead + %tr + %th Login + %th Full name + %th Remark + %th + + %tbody + - @group.users.each do |user| + %tr + %td= user.login + %td= user.full_name + %td= user.remark + %td= link_to 'Remove', remove_user_group_path(@group,user), :method => :delete, :data => { :confirm => "Remove #{user.full_name}?" }, class: 'btn btn-danger' + .col-md-6 + %h1 Problems in this group + + =form_tag add_problem_group_path(@group), class: 'form-inline' do + .form-group + =label_tag :problem_id, "Problem" + =select_tag :problem_id, options_from_collection_for_select(Problem.all,'id','full_name'), class: 'select2' + =submit_tag "Add",class: 'btn btn-primary' + + + %table.table.table-hover + %thead + %tr + %th name + %th Full name + %th Full score + %th + + %tbody + - @group.problems.each do |problem| + %tr + %td= problem.name + %td= problem.full_name + %td= problem.full_score + %td= link_to 'Remove', remove_problem_group_path(@group,problem), :method => :delete, :data => { :confirm => "Remove #{problem.full_name}?" }, class: 'btn btn-danger' + + diff --git a/app/views/problems/manage.html.haml b/app/views/problems/manage.html.haml --- a/app/views/problems/manage.html.haml +++ b/app/views/problems/manage.html.haml @@ -39,7 +39,7 @@ %p= link_to '[Back to problem list]', :action => 'list' = form_tag :action=>'do_manage' do - .submitbox + .submitbox.panel What do you want to do to the selected problem? %br/ (You can shift-click to select a range of problems) @@ -59,8 +59,13 @@ Add to = select("contest","id",Contest.all.collect {|c| [c.title, c.id]}) = submit_tag 'Add', :name => 'add_to_contest' + %li + Add problems to group + = select_tag "group_id", options_from_collection_for_select( Group.all, 'id','name',params[:group_name]), id: 'group_name',class: 'select2' + = submit_tag 'Add', name: 'add_group' + - %table + %table.table.table-hover %tr{style: "text-align: left;"} %th= check_box_tag 'select_all' %th Name diff --git a/app/views/user_admin/bulk_manage.html.haml b/app/views/user_admin/bulk_manage.html.haml --- a/app/views/user_admin/bulk_manage.html.haml +++ b/app/views/user_admin/bulk_manage.html.haml @@ -53,7 +53,7 @@ Add users to group %label.col-md-3.control-label.text-right Group name .col-md-5 - = text_field_tag "group_name", params[:group_name], id: 'group_name',class: 'form-control select2' + = select_tag "group_name", options_from_collection_for_select( Group.all, 'id','name',params[:group_name]), id: 'group_name',class: 'form-control select2' .row diff --git a/config/routes.rb b/config/routes.rb --- a/config/routes.rb +++ b/config/routes.rb @@ -29,7 +29,15 @@ get 'import' get 'manage' end + end + resources :groups do + member do + post 'add_user', to: 'groups#add_user', as: 'add_user' + delete 'remove_user/:user_id', to: 'groups#remove_user', as: 'remove_user' + post 'add_problem', to: 'groups#add_problem', as: 'add_problem' + delete 'remove_problem/:problem_id', to: 'groups#remove_problem', as: 'remove_problem' + end end resources :testcases, only: [] do diff --git a/db/migrate/20170911091143_create_groups.rb b/db/migrate/20170911091143_create_groups.rb new file mode 100644 --- /dev/null +++ b/db/migrate/20170911091143_create_groups.rb @@ -0,0 +1,30 @@ +class CreateGroups < ActiveRecord::Migration + + def change + create_table :groups do |t| + t.string :name + t.string :description + end + + create_join_table :groups, :users do |t| + # t.index [:group_id, :user_id] + t.index [:user_id, :group_id] + end + + create_join_table :problems, :groups do |t| + # t.index [:problem_id, :group_id] + t.index [:group_id, :problem_id] + end + + reversible do |change| + change.up do + GraderConfiguration.where(key: 'system.use_problem_group').first_or_create(value_type: 'boolean', value: 'false', + description: 'If true, available problem to the user will be only ones associated with the group of the user'); + end + + change.down do + GraderConfiguration.where(key: 'system.use_problem_group').destroy_all + end + end + end +end diff --git a/db/schema.rb b/db/schema.rb --- 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: 20170427070345) do +ActiveRecord::Schema.define(version: 20170911091143) do create_table "announcements", force: :cascade do |t| t.string "author", limit: 255 @@ -79,6 +79,25 @@ add_index "grader_processes", ["host", "pid"], name: "index_grader_processes_on_ip_and_pid", using: :btree + create_table "groups", force: :cascade do |t| + t.string "name", limit: 255 + t.string "description", limit: 255 + end + + create_table "groups_problems", id: false, force: :cascade do |t| + t.integer "problem_id", limit: 4, null: false + t.integer "group_id", limit: 4, null: false + end + + add_index "groups_problems", ["group_id", "problem_id"], name: "index_groups_problems_on_group_id_and_problem_id", using: :btree + + create_table "groups_users", id: false, force: :cascade do |t| + t.integer "group_id", limit: 4, null: false + t.integer "user_id", limit: 4, null: false + end + + add_index "groups_users", ["user_id", "group_id"], name: "index_groups_users_on_user_id_and_group_id", using: :btree + create_table "heart_beats", force: :cascade do |t| t.integer "user_id", limit: 4 t.string "ip_address", limit: 255 diff --git a/db/seeds.rb b/db/seeds.rb --- a/db/seeds.rb +++ b/db/seeds.rb @@ -163,7 +163,16 @@ :value_type => 'string', :default_value => 'none', :description => "New user will be assigned to this contest automatically, if it exists. Set to 'none' if there is no default contest." - } + }, + + { + :key => 'system.use_problem_group', + :value_type => 'boolean', + :default_value => 'false', + :description => "If true, available problem to the user will be only ones associated with the group of the user." + }, + + ] diff --git a/test/controllers/groups_controller_test.rb b/test/controllers/groups_controller_test.rb new file mode 100644 --- /dev/null +++ b/test/controllers/groups_controller_test.rb @@ -0,0 +1,49 @@ +require 'test_helper' + +class GroupsControllerTest < ActionController::TestCase + setup do + @group = groups(:one) + end + + test "should get index" do + get :index + assert_response :success + assert_not_nil assigns(:groups) + end + + test "should get new" do + get :new + assert_response :success + end + + test "should create group" do + assert_difference('Group.count') do + post :create, group: { description: @group.description, name: @group.name } + end + + assert_redirected_to group_path(assigns(:group)) + end + + test "should show group" do + get :show, id: @group + assert_response :success + end + + test "should get edit" do + get :edit, id: @group + assert_response :success + end + + test "should update group" do + patch :update, id: @group, group: { description: @group.description, name: @group.name } + assert_redirected_to group_path(assigns(:group)) + end + + test "should destroy group" do + assert_difference('Group.count', -1) do + delete :destroy, id: @group + end + + assert_redirected_to groups_path + end +end