started implementing teams

This commit is contained in:
Hauke Klement
2015-01-28 12:28:09 +01:00
parent cf346e2271
commit dd624b26c8
25 changed files with 355 additions and 3 deletions

View File

@ -7,6 +7,7 @@ class ExercisesController < ApplicationController
before_action :set_execution_environments, only: [:create, :edit, :new, :update] 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_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :run, :statistics, :submit]
before_action :set_file_types, only: [:create, :edit, :new, :update] before_action :set_file_types, only: [:create, :edit, :new, :update]
before_action :set_teams, only: [:create, :edit, :new, :update]
def authorize! def authorize!
authorize(@exercise || @exercises) authorize(@exercise || @exercises)
@ -49,7 +50,7 @@ class ExercisesController < ApplicationController
end end
def exercise_params 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 end
private :exercise_params private :exercise_params
@ -114,6 +115,11 @@ class ExercisesController < ApplicationController
end end
private :set_file_types private :set_file_types
def set_teams
@teams = Team.all.order(:name)
end
private :set_teams
def show def show
end end

View 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

View File

@ -7,6 +7,7 @@ class Exercise < ActiveRecord::Base
belongs_to :execution_environment belongs_to :execution_environment
has_many :submissions has_many :submissions
belongs_to :team
has_many :users, source_type: ExternalUser, through: :submissions has_many :users, source_type: ExternalUser, through: :submissions
scope :with_submissions, -> { where('id IN (SELECT exercise_id FROM submissions)') } scope :with_submissions, -> { where('id IN (SELECT exercise_id FROM submissions)') }

View File

@ -3,6 +3,8 @@ class InternalUser < ActiveRecord::Base
authenticates_with_sorcery! authenticates_with_sorcery!
has_and_belongs_to_many :teams
validates :email, presence: true, uniqueness: true validates :email, presence: true, uniqueness: true
validates :password, confirmation: true, on: :update, presence: true, unless: :activated? validates :password, confirmation: true, on: :update, presence: true, unless: :activated?
validates :role, inclusion: {in: ROLES} validates :role, inclusion: {in: ROLES}

10
app/models/team.rb Normal file
View 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

View 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

View File

@ -5,7 +5,7 @@
= t('shared.administration') = t('shared.administration')
span.caret span.caret
ul.dropdown-menu role='menu' 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| - models.each do |model|
- if policy(model).index? - if policy(model).index?
li = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path")) li = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path"))

View File

@ -13,6 +13,9 @@
= f.label(:instructions) = f.label(:instructions)
= f.hidden_field(:instructions) = f.hidden_field(:instructions)
.form-control.markdown .form-control.markdown
.form-group
= f.label(:team_id)
= f.collection_select(:team_id, @teams, :id, :name, {include_blank: true}, class: 'form-control')
.checkbox .checkbox
label label
= f.check_box(:public) = f.check_box(:public)

View File

@ -11,6 +11,7 @@ h1
= row(label: 'exercise.description', value: @exercise.description) = row(label: 'exercise.description', value: @exercise.description)
= row(label: 'exercise.execution_environment', value: link_to(@exercise.execution_environment, @exercise.execution_environment)) = 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.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.maximum_score', value: @exercise.maximum_score)
= row(label: 'exercise.public', value: @exercise.public?) = row(label: 'exercise.public', value: @exercise.public?)
= row(label: 'exercise.embedding_parameters') do = row(label: 'exercise.embedding_parameters') do

View 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)

View File

@ -0,0 +1,3 @@
h1 = @hint
= render('form')

View 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)

View File

@ -0,0 +1,3 @@
h1 = t('shared.new_model', model: Team.model_name.human)
= render('form')

View 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)

View File

@ -27,6 +27,8 @@ de:
maximum_score: Erreichbare Punktzahl maximum_score: Erreichbare Punktzahl
public: Öffentlich public: Öffentlich
reference_implementation: Reference Implementation reference_implementation: Reference Implementation
team: Team
team_id: Team
template_code: Template Code template_code: Template Code
template_test_code: Template Test Code template_test_code: Template Test Code
test_code: Test Code test_code: Test Code
@ -77,6 +79,9 @@ de:
files: Dateien files: Dateien
score: Punktzahl score: Punktzahl
user: Autor user: Autor
team:
internal_user_ids: Mitglieder
name: Name
models: models:
consumer: consumer:
one: Konsument one: Konsument
@ -108,6 +113,9 @@ de:
submission: submission:
one: Abgabe one: Abgabe
other: Abgaben other: Abgaben
team:
one: Team
other: Teams
errors: errors:
messages: messages:
together: 'muss zusammen mit %{attribute} definiert werden' together: 'muss zusammen mit %{attribute} definiert werden'

View File

@ -27,6 +27,8 @@ en:
maximum_score: Maximum Score maximum_score: Maximum Score
public: Public public: Public
reference_implementation: Reference Implementation reference_implementation: Reference Implementation
team: Team
team_id: Team
template_code: Template Code template_code: Template Code
template_test_code: Template Test Code template_test_code: Template Test Code
test_code: Test Code test_code: Test Code
@ -77,6 +79,9 @@ en:
files: Files files: Files
score: Score score: Score
user: Author user: Author
team:
internal_user_ids: Members
name: Name
models: models:
consumer: consumer:
one: Consumer one: Consumer
@ -108,6 +113,9 @@ en:
submission: submission:
one: Submission one: Submission
other: Submissions other: Submissions
team:
one: Team
other: Teams
errors: errors:
messages: messages:
together: 'has to be set along with %{attribute}' together: 'has to be set along with %{attribute}'

View File

@ -61,4 +61,6 @@ Rails.application.routes.draw do
get 'test/:filename', as: :test, constraints: {filename: FILENAME_REGEXP}, to: :test get 'test/:filename', as: :test, constraints: {filename: FILENAME_REGEXP}, to: :test
end end
end end
resources :teams
end end

View File

@ -0,0 +1,8 @@
class CreateTeams < ActiveRecord::Migration
def change
create_table :teams do |t|
t.string :name
t.timestamps
end
end
end

View 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

View File

@ -0,0 +1,5 @@
class AddTeamIdToExercises < ActiveRecord::Migration
def change
add_reference :exercises, :team
end
end

View File

@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -57,6 +57,7 @@ ActiveRecord::Schema.define(version: 20141031161603) do
t.boolean "public" t.boolean "public"
t.string "user_type" t.string "user_type"
t.string "token" t.string "token"
t.integer "team_id"
end end
create_table "external_users", force: true do |t| 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", ["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 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| create_table "submissions", force: true do |t|
t.integer "exercise_id" t.integer "exercise_id"
t.float "score" t.float "score"
@ -148,4 +157,10 @@ ActiveRecord::Schema.define(version: 20141031161603) do
t.string "user_type" t.string "user_type"
end end
create_table "teams", force: true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
end end

View 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
View 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
View 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

View 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