started implementing teams
This commit is contained in:
@ -7,6 +7,7 @@ class ExercisesController < ApplicationController
|
||||
before_action :set_execution_environments, only: [:create, :edit, :new, :update]
|
||||
before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :run, :statistics, :submit]
|
||||
before_action :set_file_types, only: [:create, :edit, :new, :update]
|
||||
before_action :set_teams, only: [:create, :edit, :new, :update]
|
||||
|
||||
def authorize!
|
||||
authorize(@exercise || @exercises)
|
||||
@ -49,7 +50,7 @@ class ExercisesController < ApplicationController
|
||||
end
|
||||
|
||||
def exercise_params
|
||||
params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :title, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name)
|
||||
params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :team_id, :title, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name)
|
||||
end
|
||||
private :exercise_params
|
||||
|
||||
@ -114,6 +115,11 @@ class ExercisesController < ApplicationController
|
||||
end
|
||||
private :set_file_types
|
||||
|
||||
def set_teams
|
||||
@teams = Team.all.order(:name)
|
||||
end
|
||||
private :set_teams
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
|
69
app/controllers/teams_controller.rb
Normal file
69
app/controllers/teams_controller.rb
Normal file
@ -0,0 +1,69 @@
|
||||
class TeamsController < ApplicationController
|
||||
before_action :set_team, only: MEMBER_ACTIONS
|
||||
|
||||
def authorize!
|
||||
authorize(@team || @teams)
|
||||
end
|
||||
private :authorize!
|
||||
|
||||
def create
|
||||
@team = Team.new(team_params)
|
||||
authorize!
|
||||
respond_to do |format|
|
||||
if @team.save
|
||||
format.html { redirect_to(team_path(@team.id), notice: t('shared.object_created', model: Team.model_name.human)) }
|
||||
format.json { render(:show, location: @team, status: :created) }
|
||||
else
|
||||
format.html { render(:new) }
|
||||
format.json { render(json: @team.errors, status: :unprocessable_entity) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@team.destroy
|
||||
respond_to do |format|
|
||||
format.html { redirect_to(teams_path, notice: t('shared.object_destroyed', model: Team.model_name.human)) }
|
||||
format.json { head(:no_content) }
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def index
|
||||
@teams = Team.all.order(:name)
|
||||
authorize!
|
||||
end
|
||||
|
||||
def new
|
||||
@team = Team.new
|
||||
authorize!
|
||||
end
|
||||
|
||||
def set_team
|
||||
@team = Team.find(params[:id])
|
||||
authorize!
|
||||
end
|
||||
private :set_team
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def team_params
|
||||
params[:team].permit(:name, internal_user_ids: [])
|
||||
end
|
||||
private :team_params
|
||||
|
||||
def update
|
||||
respond_to do |format|
|
||||
if @team.update(team_params)
|
||||
format.html { redirect_to(team_path(@team.id), notice: t('shared.object_updated', model: Team.model_name.human)) }
|
||||
format.json { render(:show, location: @team, status: :ok) }
|
||||
else
|
||||
format.html { render(:edit) }
|
||||
format.json { render(json: @team.errors, status: :unprocessable_entity) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -7,6 +7,7 @@ class Exercise < ActiveRecord::Base
|
||||
|
||||
belongs_to :execution_environment
|
||||
has_many :submissions
|
||||
belongs_to :team
|
||||
has_many :users, source_type: ExternalUser, through: :submissions
|
||||
|
||||
scope :with_submissions, -> { where('id IN (SELECT exercise_id FROM submissions)') }
|
||||
|
@ -3,6 +3,8 @@ class InternalUser < ActiveRecord::Base
|
||||
|
||||
authenticates_with_sorcery!
|
||||
|
||||
has_and_belongs_to_many :teams
|
||||
|
||||
validates :email, presence: true, uniqueness: true
|
||||
validates :password, confirmation: true, on: :update, presence: true, unless: :activated?
|
||||
validates :role, inclusion: {in: ROLES}
|
||||
|
10
app/models/team.rb
Normal file
10
app/models/team.rb
Normal file
@ -0,0 +1,10 @@
|
||||
class Team < ActiveRecord::Base
|
||||
has_and_belongs_to_many :internal_users
|
||||
alias_method :members, :internal_users
|
||||
|
||||
validates :name, presence: true
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
end
|
14
app/policies/team_policy.rb
Normal file
14
app/policies/team_policy.rb
Normal file
@ -0,0 +1,14 @@
|
||||
class TeamPolicy < ApplicationPolicy
|
||||
[:create?, :index?, :new?].each do |action|
|
||||
define_method(action) { @user.internal? }
|
||||
end
|
||||
|
||||
[:destroy?, :edit?, :show?, :update?].each do |action|
|
||||
define_method(action) { admin? || member? }
|
||||
end
|
||||
|
||||
def member?
|
||||
@record.members.include?(@user)
|
||||
end
|
||||
private :member?
|
||||
end
|
@ -5,7 +5,7 @@
|
||||
= t('shared.administration')
|
||||
span.caret
|
||||
ul.dropdown-menu role='menu'
|
||||
- models = [ExecutionEnvironment, Exercise, Consumer, ExternalUser, FileType, InternalUser, Submission].sort_by { |model| model.model_name.human(count: 2) }
|
||||
- models = [ExecutionEnvironment, Exercise, Consumer, ExternalUser, FileType, InternalUser, Submission, Team].sort_by { |model| model.model_name.human(count: 2) }
|
||||
- models.each do |model|
|
||||
- if policy(model).index?
|
||||
li = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path"))
|
||||
|
@ -13,6 +13,9 @@
|
||||
= f.label(:instructions)
|
||||
= f.hidden_field(:instructions)
|
||||
.form-control.markdown
|
||||
.form-group
|
||||
= f.label(:team_id)
|
||||
= f.collection_select(:team_id, @teams, :id, :name, {include_blank: true}, class: 'form-control')
|
||||
.checkbox
|
||||
label
|
||||
= f.check_box(:public)
|
||||
|
@ -11,6 +11,7 @@ h1
|
||||
= row(label: 'exercise.description', value: @exercise.description)
|
||||
= row(label: 'exercise.execution_environment', value: link_to(@exercise.execution_environment, @exercise.execution_environment))
|
||||
= row(label: 'exercise.instructions', value: render_markdown(@exercise.instructions))
|
||||
= row(label: 'exercise.team', value: @exercise.team ? link_to(@exercise.team, @exercise.team) : nil)
|
||||
= row(label: 'exercise.maximum_score', value: @exercise.maximum_score)
|
||||
= row(label: 'exercise.public', value: @exercise.public?)
|
||||
= row(label: 'exercise.embedding_parameters') do
|
||||
|
9
app/views/teams/_form.html.slim
Normal file
9
app/views/teams/_form.html.slim
Normal file
@ -0,0 +1,9 @@
|
||||
= form_for(@team) do |f|
|
||||
= render('shared/form_errors', object: @team)
|
||||
.form-group
|
||||
= f.label(:name)
|
||||
= f.text_field(:name, class: 'form-control', required: true)
|
||||
.form-group
|
||||
= f.label(:internal_user_ids)
|
||||
= f.collection_select(:internal_user_ids, InternalUser.all.order(:name), :id, :name, {}, {class: 'form-control', multiple: true})
|
||||
.actions = render('shared/submit_button', f: f, object: @team)
|
3
app/views/teams/edit.html.slim
Normal file
3
app/views/teams/edit.html.slim
Normal file
@ -0,0 +1,3 @@
|
||||
h1 = @hint
|
||||
|
||||
= render('form')
|
19
app/views/teams/index.html.slim
Normal file
19
app/views/teams/index.html.slim
Normal file
@ -0,0 +1,19 @@
|
||||
h1 = Team.model_name.human(count: 2)
|
||||
|
||||
.table-responsive
|
||||
table.table
|
||||
thead
|
||||
tr
|
||||
th = t('activerecord.attributes.team.name')
|
||||
th = t('activerecord.attributes.team.internal_user_ids')
|
||||
th colspan=3 = t('shared.actions')
|
||||
tbody
|
||||
- @teams.each do |team|
|
||||
tr
|
||||
td = team.name
|
||||
td = team.members.count
|
||||
td = link_to(t('shared.show'), team_path(team.id))
|
||||
td = link_to(t('shared.edit'), edit_team_path(team.id))
|
||||
td = link_to(t('shared.destroy'), team_path(team.id), data: {confirm: t('shared.confirm_destroy')}, method: :delete)
|
||||
|
||||
p = render('shared/new_button', model: Team, path: new_team_path)
|
3
app/views/teams/new.html.slim
Normal file
3
app/views/teams/new.html.slim
Normal file
@ -0,0 +1,3 @@
|
||||
h1 = t('shared.new_model', model: Team.model_name.human)
|
||||
|
||||
= render('form')
|
9
app/views/teams/show.html.slim
Normal file
9
app/views/teams/show.html.slim
Normal file
@ -0,0 +1,9 @@
|
||||
h1
|
||||
= @team
|
||||
= render('shared/edit_button', object: @team, path: edit_team_path(@team.id))
|
||||
|
||||
= row(label: 'team.name', value: @team.name)
|
||||
= row(label: 'team.internal_user_ids') do
|
||||
ul.list-unstyled
|
||||
- @team.members.order(:name).each do |internal_user|
|
||||
li = link_to(internal_user, internal_user)
|
@ -27,6 +27,8 @@ de:
|
||||
maximum_score: Erreichbare Punktzahl
|
||||
public: Öffentlich
|
||||
reference_implementation: Reference Implementation
|
||||
team: Team
|
||||
team_id: Team
|
||||
template_code: Template Code
|
||||
template_test_code: Template Test Code
|
||||
test_code: Test Code
|
||||
@ -77,6 +79,9 @@ de:
|
||||
files: Dateien
|
||||
score: Punktzahl
|
||||
user: Autor
|
||||
team:
|
||||
internal_user_ids: Mitglieder
|
||||
name: Name
|
||||
models:
|
||||
consumer:
|
||||
one: Konsument
|
||||
@ -108,6 +113,9 @@ de:
|
||||
submission:
|
||||
one: Abgabe
|
||||
other: Abgaben
|
||||
team:
|
||||
one: Team
|
||||
other: Teams
|
||||
errors:
|
||||
messages:
|
||||
together: 'muss zusammen mit %{attribute} definiert werden'
|
||||
|
@ -27,6 +27,8 @@ en:
|
||||
maximum_score: Maximum Score
|
||||
public: Public
|
||||
reference_implementation: Reference Implementation
|
||||
team: Team
|
||||
team_id: Team
|
||||
template_code: Template Code
|
||||
template_test_code: Template Test Code
|
||||
test_code: Test Code
|
||||
@ -77,6 +79,9 @@ en:
|
||||
files: Files
|
||||
score: Score
|
||||
user: Author
|
||||
team:
|
||||
internal_user_ids: Members
|
||||
name: Name
|
||||
models:
|
||||
consumer:
|
||||
one: Consumer
|
||||
@ -108,6 +113,9 @@ en:
|
||||
submission:
|
||||
one: Submission
|
||||
other: Submissions
|
||||
team:
|
||||
one: Team
|
||||
other: Teams
|
||||
errors:
|
||||
messages:
|
||||
together: 'has to be set along with %{attribute}'
|
||||
|
@ -61,4 +61,6 @@ Rails.application.routes.draw do
|
||||
get 'test/:filename', as: :test, constraints: {filename: FILENAME_REGEXP}, to: :test
|
||||
end
|
||||
end
|
||||
|
||||
resources :teams
|
||||
end
|
||||
|
8
db/migrate/20150128083123_create_teams.rb
Normal file
8
db/migrate/20150128083123_create_teams.rb
Normal file
@ -0,0 +1,8 @@
|
||||
class CreateTeams < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :teams do |t|
|
||||
t.string :name
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
8
db/migrate/20150128084834_create_internal_users_teams.rb
Normal file
8
db/migrate/20150128084834_create_internal_users_teams.rb
Normal file
@ -0,0 +1,8 @@
|
||||
class CreateInternalUsersTeams < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :internal_users_teams do |t|
|
||||
t.belongs_to :internal_user, index: true
|
||||
t.belongs_to :team, index: true
|
||||
end
|
||||
end
|
||||
end
|
5
db/migrate/20150128093003_add_team_id_to_exercises.rb
Normal file
5
db/migrate/20150128093003_add_team_id_to_exercises.rb
Normal file
@ -0,0 +1,5 @@
|
||||
class AddTeamIdToExercises < ActiveRecord::Migration
|
||||
def change
|
||||
add_reference :exercises, :team
|
||||
end
|
||||
end
|
17
db/schema.rb
17
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: 20141031161603) do
|
||||
ActiveRecord::Schema.define(version: 20150128093003) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
@ -57,6 +57,7 @@ ActiveRecord::Schema.define(version: 20141031161603) do
|
||||
t.boolean "public"
|
||||
t.string "user_type"
|
||||
t.string "token"
|
||||
t.integer "team_id"
|
||||
end
|
||||
|
||||
create_table "external_users", force: true do |t|
|
||||
@ -138,6 +139,14 @@ ActiveRecord::Schema.define(version: 20141031161603) do
|
||||
add_index "internal_users", ["remember_me_token"], name: "index_internal_users_on_remember_me_token", using: :btree
|
||||
add_index "internal_users", ["reset_password_token"], name: "index_internal_users_on_reset_password_token", using: :btree
|
||||
|
||||
create_table "internal_users_teams", force: true do |t|
|
||||
t.integer "internal_user_id"
|
||||
t.integer "team_id"
|
||||
end
|
||||
|
||||
add_index "internal_users_teams", ["internal_user_id"], name: "index_internal_users_teams_on_internal_user_id", using: :btree
|
||||
add_index "internal_users_teams", ["team_id"], name: "index_internal_users_teams_on_team_id", using: :btree
|
||||
|
||||
create_table "submissions", force: true do |t|
|
||||
t.integer "exercise_id"
|
||||
t.float "score"
|
||||
@ -148,4 +157,10 @@ ActiveRecord::Schema.define(version: 20141031161603) do
|
||||
t.string "user_type"
|
||||
end
|
||||
|
||||
create_table "teams", force: true do |t|
|
||||
t.string "name"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
end
|
||||
|
||||
end
|
||||
|
93
spec/controllers/teams_controller_spec.rb
Normal file
93
spec/controllers/teams_controller_spec.rb
Normal file
@ -0,0 +1,93 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe TeamsController do
|
||||
let(:team) { FactoryGirl.create(:team) }
|
||||
let(:user) { FactoryGirl.create(:admin) }
|
||||
before(:each) { allow(controller).to receive(:current_user).and_return(user) }
|
||||
|
||||
describe 'POST #create' do
|
||||
context 'with a valid team' do
|
||||
let(:request) { Proc.new { post :create, team: FactoryGirl.attributes_for(:team) } }
|
||||
before(:each) { request.call }
|
||||
|
||||
expect_assigns(team: Team)
|
||||
|
||||
it 'creates the team' do
|
||||
expect { request.call }.to change(Team, :count).by(1)
|
||||
end
|
||||
|
||||
expect_redirect
|
||||
end
|
||||
|
||||
context 'with an invalid team' do
|
||||
before(:each) { post :create, team: {} }
|
||||
|
||||
expect_assigns(team: Team)
|
||||
expect_status(200)
|
||||
expect_template(:new)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
before(:each) { delete :destroy, id: team.id }
|
||||
|
||||
expect_assigns(team: Team)
|
||||
|
||||
it 'destroys the team' do
|
||||
team = FactoryGirl.create(:team)
|
||||
expect { delete :destroy, id: team.id }.to change(Team, :count).by(-1)
|
||||
end
|
||||
|
||||
expect_redirect(:teams)
|
||||
end
|
||||
|
||||
describe 'GET #edit' do
|
||||
before(:each) { get :edit, id: team.id }
|
||||
|
||||
expect_assigns(team: Team)
|
||||
expect_status(200)
|
||||
expect_template(:edit)
|
||||
end
|
||||
|
||||
describe 'GET #index' do
|
||||
let!(:teams) { FactoryGirl.create_pair(:team) }
|
||||
before(:each) { get :index }
|
||||
|
||||
expect_assigns(teams: Team.all)
|
||||
expect_status(200)
|
||||
expect_template(:index)
|
||||
end
|
||||
|
||||
describe 'GET #new' do
|
||||
before(:each) { get :new }
|
||||
|
||||
expect_assigns(team: Team)
|
||||
expect_status(200)
|
||||
expect_template(:new)
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
before(:each) { get :show, id: team.id }
|
||||
|
||||
expect_assigns(team: :team)
|
||||
expect_status(200)
|
||||
expect_template(:show)
|
||||
end
|
||||
|
||||
describe 'PUT #update' do
|
||||
context 'with a valid team' do
|
||||
before(:each) { put :update, team: FactoryGirl.attributes_for(:team), id: team.id }
|
||||
|
||||
expect_assigns(team: Team)
|
||||
expect_redirect
|
||||
end
|
||||
|
||||
context 'with an invalid team' do
|
||||
before(:each) { put :update, team: {name: ''}, id: team.id }
|
||||
|
||||
expect_assigns(team: Team)
|
||||
expect_status(200)
|
||||
expect_template(:edit)
|
||||
end
|
||||
end
|
||||
end
|
6
spec/factories/team.rb
Normal file
6
spec/factories/team.rb
Normal file
@ -0,0 +1,6 @@
|
||||
FactoryGirl.define do
|
||||
factory :team do
|
||||
internal_users { build_list :teacher, 10 }
|
||||
name 'A-Team'
|
||||
end
|
||||
end
|
9
spec/models/team_spec.rb
Normal file
9
spec/models/team_spec.rb
Normal file
@ -0,0 +1,9 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Team do
|
||||
let(:team) { Team.create }
|
||||
|
||||
it 'validates the presence of a name' do
|
||||
expect(team.errors[:name]).to be_present
|
||||
end
|
||||
end
|
41
spec/policies/team_policy_spec.rb
Normal file
41
spec/policies/team_policy_spec.rb
Normal file
@ -0,0 +1,41 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe TeamPolicy do
|
||||
subject { TeamPolicy }
|
||||
|
||||
let(:team) { FactoryGirl.build(:team) }
|
||||
|
||||
[:create?, :index?, :new?].each do |action|
|
||||
permissions(action) do
|
||||
it 'grants access to admins' do
|
||||
expect(subject).to permit(FactoryGirl.build(:admin), team)
|
||||
end
|
||||
|
||||
it 'grants access to teachers' do
|
||||
expect(subject).to permit(FactoryGirl.build(:teacher), team)
|
||||
end
|
||||
|
||||
it 'does not grant access to external users' do
|
||||
expect(subject).not_to permit(FactoryGirl.build(:external_user), team)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
[:destroy?, :edit?, :show?, :update?].each do |action|
|
||||
permissions(action) do
|
||||
it 'grants access to admins' do
|
||||
expect(subject).to permit(FactoryGirl.build(:admin), team)
|
||||
end
|
||||
|
||||
it 'grants access to members' do
|
||||
expect(subject).to permit(team.members.last, team)
|
||||
end
|
||||
|
||||
it 'does not grant access to all other users' do
|
||||
[:external_user, :teacher].each do |factory_name|
|
||||
expect(subject).not_to permit(FactoryGirl.build(factory_name), team)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Reference in New Issue
Block a user