Add ProgrammingGroup & ProgrammingGroupMembership
* User can create programming group with other users for exercise * Submission is shared in a group * Also adjust specs
This commit is contained in:

committed by
Sebastian Serth

parent
0234414bae
commit
319c3ab3b4
@ -23,6 +23,15 @@ class ApplicationController < ActionController::Base
|
||||
@current_user ||= find_or_login_current_user&.store_current_study_group_id(session[:study_group_id])
|
||||
end
|
||||
|
||||
def current_contributor
|
||||
@current_contributor ||= if session[:pg_id]
|
||||
current_user.programming_groups.find(session[:pg_id])
|
||||
else
|
||||
current_user
|
||||
end
|
||||
end
|
||||
helper_method :current_contributor
|
||||
|
||||
def find_or_login_current_user
|
||||
login_from_authentication_token ||
|
||||
login_from_lti_session ||
|
||||
|
@ -100,6 +100,6 @@ class CommunitySolutionsController < ApplicationController
|
||||
|
||||
def set_exercise_and_submission
|
||||
@exercise = @community_solution.exercise
|
||||
@submission = current_user.submissions.final.where(exercise_id: @community_solution.exercise.id).order('created_at DESC').first
|
||||
@submission = current_contributor.submissions.final.where(exercise: @community_solution.exercise).order(created_at: :desc).first
|
||||
end
|
||||
end
|
||||
|
@ -17,7 +17,7 @@ module FileParameters
|
||||
# avoid that public files from other contexts can be created
|
||||
# `next` is similar to an early return and will proceed with the next iteration of the loop
|
||||
next true if file.context_type == 'Exercise' && file.context_id != exercise.id
|
||||
next true if file.context_type == 'Submission' && (file.context.contributor_id != current_user.id || file.context.contributor_type != current_user.class.name)
|
||||
next true if file.context_type == 'Submission' && (file.context.contributor_id != current_contributor.id || file.context.contributor_type != current_contributor.class.name)
|
||||
next true if file.context_type == 'CommunitySolution' && controller_name != 'community_solutions'
|
||||
|
||||
# Optimization: We already queried the ancestor file, let's reuse the object.
|
||||
|
@ -21,18 +21,17 @@ module Lti
|
||||
# exercise_id.nil? ==> the user has logged out. All session data is to be destroyed
|
||||
# exercise_id.exists? ==> the user has submitted the results of an exercise to the consumer.
|
||||
# Only the lti_parameters are deleted.
|
||||
def clear_lti_session_data(exercise_id = nil, _user_id = nil)
|
||||
def clear_lti_session_data(exercise_id = nil)
|
||||
if exercise_id.nil?
|
||||
session.delete(:external_user_id)
|
||||
session.delete(:study_group_id)
|
||||
session.delete(:embed_options)
|
||||
session.delete(:lti_exercise_id)
|
||||
session.delete(:lti_parameters_id)
|
||||
end
|
||||
session.delete(:pg_id)
|
||||
|
||||
# March 2022: We temporarily allow reusing the LTI credentials and don't remove them on purpose.
|
||||
# We allow reusing the LTI credentials and don't remove them on purpose.
|
||||
# This allows users to jump between remote and web evaluation with the same behavior.
|
||||
# LtiParameter.where(external_users_id: user_id, exercises_id: exercise_id).destroy_all
|
||||
# Also it prevents the user from deleting the lti_parameters for their programming team members.
|
||||
end
|
||||
|
||||
private :clear_lti_session_data
|
||||
@ -159,20 +158,20 @@ module Lti
|
||||
session: session.to_hash,
|
||||
exercise_id: submission.exercise_id,
|
||||
})
|
||||
normalized_lit_score = submission.normalized_score
|
||||
normalized_lti_score = submission.normalized_score
|
||||
if submission.before_deadline?
|
||||
# Keep the full score
|
||||
elsif submission.within_grace_period?
|
||||
# Reduce score by 20%
|
||||
normalized_lit_score *= 0.8
|
||||
normalized_lti_score *= 0.8
|
||||
elsif submission.after_late_deadline?
|
||||
# Reduce score by 100%
|
||||
normalized_lit_score *= 0.0
|
||||
normalized_lti_score *= 0.0
|
||||
end
|
||||
|
||||
begin
|
||||
response = provider.post_replace_result!(normalized_lit_score)
|
||||
{code: response.response_code, message: response.post_response.body, status: response.code_major, score_sent: normalized_lit_score}
|
||||
response = provider.post_replace_result!(normalized_lti_score)
|
||||
{code: response.response_code, message: response.post_response.body, status: response.code_major, score_sent: normalized_lti_score}
|
||||
rescue IMS::LTI::XMLParseError, Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNRESET, SocketError
|
||||
# A parsing error might happen if the LTI provider is down and doesn't return a valid XML response
|
||||
{status: 'error'}
|
||||
|
@ -22,8 +22,7 @@ module SubmissionParameters
|
||||
def merge_user(params)
|
||||
# The study_group_id might not be present in the session (e.g. for internal users), resulting in session[:study_group_id] = nil which is intended.
|
||||
params.merge(
|
||||
contributor_id: current_user.id,
|
||||
contributor_type: current_user.class.name,
|
||||
contributor: current_contributor,
|
||||
study_group_id: current_user.current_study_group_id
|
||||
)
|
||||
end
|
||||
|
@ -92,7 +92,7 @@ class ExercisesController < ApplicationController
|
||||
authorize!
|
||||
@feedbacks = @exercise.user_exercise_feedbacks.paginate(page: params[:page], per_page: per_page_param)
|
||||
@submissions = @feedbacks.map do |feedback|
|
||||
feedback.exercise.final_submission(feedback.user)
|
||||
feedback.exercise.final_submission(feedback.user.programming_groups.where(exercise: @exercise).presence || feedback.user)
|
||||
end
|
||||
end
|
||||
|
||||
@ -298,7 +298,7 @@ class ExercisesController < ApplicationController
|
||||
private :update_exercise_tips
|
||||
|
||||
def implement
|
||||
user_solved_exercise = @exercise.solved_by?(current_user)
|
||||
user_solved_exercise = @exercise.solved_by?(current_contributor)
|
||||
count_interventions_today = UserExerciseIntervention.where(user: current_user).where('created_at >= ?',
|
||||
Time.zone.now.beginning_of_day).count
|
||||
user_got_intervention_in_exercise = UserExerciseIntervention.where(user: current_user,
|
||||
@ -330,7 +330,7 @@ class ExercisesController < ApplicationController
|
||||
|
||||
@hide_rfc_button = @embed_options[:disable_rfc]
|
||||
|
||||
@submission = current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first
|
||||
@submission = current_contributor.submissions.order(created_at: :desc).find_by(exercise: @exercise)
|
||||
@files = (@submission ? @submission.collect_files : @exercise.files).select(&:visible).sort_by(&:filepath)
|
||||
@paths = collect_paths(@files)
|
||||
end
|
||||
@ -363,7 +363,7 @@ class ExercisesController < ApplicationController
|
||||
private :set_available_tips
|
||||
|
||||
def working_times
|
||||
working_time_accumulated = @exercise.accumulated_working_time_for_only(current_user)
|
||||
working_time_accumulated = @exercise.accumulated_working_time_for_only(current_contributor)
|
||||
working_time_75_percentile = @exercise.get_quantiles([0.75]).first
|
||||
render(json: {working_time_75_percentile:,
|
||||
working_time_accumulated:})
|
||||
@ -375,8 +375,8 @@ class ExercisesController < ApplicationController
|
||||
render(json: {success: 'false', error: "undefined intervention #{params[:intervention_type]}"})
|
||||
else
|
||||
uei = UserExerciseIntervention.new(
|
||||
user: current_user, exercise: @exercise, intervention:,
|
||||
accumulated_worktime_s: @exercise.accumulated_working_time_for_only(current_user)
|
||||
user: current_contributor, exercise: @exercise, intervention:,
|
||||
accumulated_worktime_s: @exercise.accumulated_working_time_for_only(current_contributor)
|
||||
)
|
||||
uei.save
|
||||
render(json: {success: 'true'})
|
||||
@ -465,7 +465,7 @@ class ExercisesController < ApplicationController
|
||||
|
||||
def statistics
|
||||
# Show general statistic page for specific exercise
|
||||
user_statistics = {'InternalUser' => {}, 'ExternalUser' => {}}
|
||||
contributor_statistics = {'InternalUser' => {}, 'ExternalUser' => {}, 'ProgrammingGroup' => {}}
|
||||
|
||||
query = Submission.select('contributor_id, contributor_type, MAX(score) AS maximum_score, COUNT(id) AS runs')
|
||||
.where(exercise_id: @exercise.id)
|
||||
@ -481,11 +481,11 @@ class ExercisesController < ApplicationController
|
||||
end
|
||||
|
||||
query.each do |tuple|
|
||||
user_statistics[tuple['contributor_type']][tuple['contributor_id'].to_i] = tuple
|
||||
contributor_statistics[tuple['contributor_type']][tuple['contributor_id'].to_i] = tuple
|
||||
end
|
||||
|
||||
render locals: {
|
||||
user_statistics:,
|
||||
contributor_statistics:,
|
||||
}
|
||||
end
|
||||
|
||||
@ -495,7 +495,7 @@ class ExercisesController < ApplicationController
|
||||
if policy(@exercise).detailed_statistics?
|
||||
submissions = Submission.where(contributor: @external_user, exercise: @exercise)
|
||||
.in_study_group_of(current_user)
|
||||
.order('created_at')
|
||||
.order(:created_at)
|
||||
@show_autosaves = params[:show_autosaves] == 'true' || submissions.none? {|s| s.cause != 'autosave' }
|
||||
submissions = submissions.where.not(cause: 'autosave') unless @show_autosaves
|
||||
interventions = UserExerciseIntervention.where('user_id = ? AND exercise_id = ?', @external_user.id,
|
||||
@ -526,7 +526,8 @@ class ExercisesController < ApplicationController
|
||||
def submit
|
||||
@submission = Submission.create(submission_params)
|
||||
@submission.calculate_score
|
||||
if @submission.user.external_user? && lti_outcome_service?(@submission.exercise_id, @submission.user.id)
|
||||
|
||||
if @submission.users.map {|user| user.external_user? && lti_outcome_service?(@submission.exercise_id, user.id) }.any?
|
||||
transmit_lti_score
|
||||
else
|
||||
redirect_after_submit
|
||||
|
@ -5,7 +5,7 @@ class FlowrController < ApplicationController
|
||||
require_user!
|
||||
# get the latest submission for this user that also has a test run (i.e. structured_errors if applicable)
|
||||
submission = Submission.joins(:testruns)
|
||||
.where(submissions: {contributor: current_user})
|
||||
.where(submissions: {contributor: current_contributor})
|
||||
.includes(structured_errors: [structured_error_attributes: [:error_template_attribute]])
|
||||
.merge(Testrun.order(created_at: :desc)).first
|
||||
|
||||
|
54
app/controllers/programming_groups_controller.rb
Normal file
54
app/controllers/programming_groups_controller.rb
Normal file
@ -0,0 +1,54 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ProgrammingGroupsController < ApplicationController
|
||||
include CommonBehavior
|
||||
include LtiHelper
|
||||
|
||||
before_action :set_exercise_and_authorize
|
||||
|
||||
def new
|
||||
@programming_group = ProgrammingGroup.new(exercise: @exercise)
|
||||
authorize!
|
||||
existing_programming_group = current_user.programming_groups.find_by(exercise: @exercise)
|
||||
if existing_programming_group
|
||||
session[:pg_id] = existing_programming_group.id
|
||||
redirect_to(implement_exercise_path(@exercise),
|
||||
notice: t("sessions.create_through_lti.session_#{lti_outcome_service?(@exercise.id, current_user.id) ? 'with' : 'without'}_outcome", consumer: @consumer))
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
programming_partner_ids = programming_group_params[:programming_partner_ids].split(',').map(&:strip).uniq
|
||||
users = programming_partner_ids.map do |partner_id|
|
||||
User.find_by_id_with_type(partner_id)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
partner_id
|
||||
end
|
||||
@programming_group = ProgrammingGroup.new(exercise: @exercise, users:)
|
||||
authorize!
|
||||
|
||||
unless programming_partner_ids.include? current_user.id_with_type
|
||||
@programming_group.add(current_user)
|
||||
end
|
||||
|
||||
create_and_respond(object: @programming_group, path: proc { implement_exercise_path(@exercise) }) do
|
||||
session[:pg_id] = @programming_group.id
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorize!
|
||||
authorize(@programming_group || @programming_groups)
|
||||
end
|
||||
|
||||
def programming_group_params
|
||||
params.require(:programming_group).permit(:programming_partner_ids)
|
||||
end
|
||||
|
||||
def set_exercise_and_authorize
|
||||
@exercise = Exercise.find(params[:exercise_id])
|
||||
authorize(@exercise, :implement?)
|
||||
end
|
||||
end
|
@ -34,6 +34,7 @@ class RemoteEvaluationController < ApplicationController
|
||||
end
|
||||
|
||||
def try_lti
|
||||
# TODO: Need to consider and support programming groups
|
||||
if !@submission.user.nil? && lti_outcome_service?(@submission.exercise_id, @submission.user.id)
|
||||
lti_response = send_score(@submission)
|
||||
process_lti_response(lti_response)
|
||||
|
@ -41,9 +41,9 @@ class UserExerciseFeedbacksController < ApplicationController
|
||||
Sentry.set_extras(params: uef_params)
|
||||
|
||||
@exercise = Exercise.find(uef_params[:exercise_id])
|
||||
rfc = RequestForComment.unsolved.where(exercise_id: @exercise.id, user_id: current_user.id).first
|
||||
rfc = RequestForComment.unsolved.where(exercise_id: @exercise.id, user: current_user).first
|
||||
submission = begin
|
||||
current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first
|
||||
current_contributor.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
@ -69,11 +69,11 @@ class UserExerciseFeedbacksController < ApplicationController
|
||||
|
||||
def update
|
||||
submission = begin
|
||||
current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').final.first
|
||||
current_contributor.submissions.where(exercise: @exercise).order(created_at: :desc).final.first
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
rfc = RequestForComment.unsolved.where(exercise_id: @exercise.id, user_id: current_user.id).first
|
||||
rfc = RequestForComment.unsolved.where(exercise: @exercise, user: current_user).first
|
||||
authorize!
|
||||
if @exercise && validate_inputs(uef_params)
|
||||
path =
|
||||
@ -123,18 +123,15 @@ class UserExerciseFeedbacksController < ApplicationController
|
||||
params[:user_exercise_feedback][:exercise_id]
|
||||
end
|
||||
|
||||
user_id = current_user.id
|
||||
user_type = current_user.class.name
|
||||
latest_submission = Submission
|
||||
.where(contributor_id: user_id, contributor_type: user_type, exercise_id:)
|
||||
.where(contributor: current_contributor, exercise_id:)
|
||||
.order(created_at: :desc).final.first
|
||||
|
||||
authorize(latest_submission, :show?)
|
||||
|
||||
params[:user_exercise_feedback]
|
||||
.permit(:feedback_text, :difficulty, :exercise_id, :user_estimated_worktime)
|
||||
.merge(user_id:,
|
||||
user_type:,
|
||||
.merge(user: current_user,
|
||||
submission: latest_submission,
|
||||
normalized_score: latest_submission&.normalized_score)
|
||||
end
|
||||
|
@ -9,9 +9,9 @@ module StatisticsHelper
|
||||
def statistics_data
|
||||
[
|
||||
{
|
||||
key: 'users',
|
||||
name: t('statistics.sections.users'),
|
||||
entries: user_statistics,
|
||||
key: 'contributors',
|
||||
name: t('statistics.sections.contributors'),
|
||||
entries: contributor_statistics,
|
||||
},
|
||||
{
|
||||
key: 'exercises',
|
||||
@ -26,7 +26,7 @@ module StatisticsHelper
|
||||
]
|
||||
end
|
||||
|
||||
def user_statistics
|
||||
def contributor_statistics
|
||||
[
|
||||
{
|
||||
key: 'internal_users',
|
||||
@ -40,10 +40,15 @@ module StatisticsHelper
|
||||
data: ExternalUser.count,
|
||||
url: external_users_path,
|
||||
},
|
||||
{
|
||||
key: 'programming_groups',
|
||||
name: t('activerecord.models.programming_group.other'),
|
||||
data: ProgrammingGroup.count,
|
||||
},
|
||||
{
|
||||
key: 'currently_active',
|
||||
name: t('statistics.entries.users.currently_active'),
|
||||
data: Submission.where(created_at: 5.minutes.ago.., contributor_type: ExternalUser.name).distinct.count(:contributor_id),
|
||||
data: Submission.from(Submission.where(created_at: 5.minutes.ago..).distinct.select(:contributor_id, :contributor_type)).count,
|
||||
url: statistics_graphs_path,
|
||||
},
|
||||
]
|
||||
@ -108,6 +113,7 @@ module StatisticsHelper
|
||||
]
|
||||
end
|
||||
|
||||
# TODO: Need to consider and support programming groups
|
||||
def user_activity_live_data
|
||||
[
|
||||
{
|
||||
@ -209,6 +215,7 @@ module StatisticsHelper
|
||||
]
|
||||
end
|
||||
|
||||
# TODO: Need to consider and support programming groups
|
||||
def ranged_user_data(interval = 'year', from = DateTime.new(0), to = DateTime.now)
|
||||
[
|
||||
{
|
||||
|
@ -29,7 +29,7 @@ class Exercise < ApplicationRecord
|
||||
|
||||
has_many :external_users, source: :contributor, source_type: 'ExternalUser', through: :submissions
|
||||
has_many :internal_users, source: :contributor, source_type: 'InternalUser', through: :submissions
|
||||
alias users external_users
|
||||
has_many :programming_groups
|
||||
|
||||
scope :with_submissions, -> { where('id IN (SELECT exercise_id FROM submissions)') }
|
||||
|
||||
@ -57,10 +57,10 @@ class Exercise < ApplicationRecord
|
||||
end
|
||||
|
||||
def finishers_percentage
|
||||
if users.distinct.count.zero?
|
||||
if contributors.empty?
|
||||
0
|
||||
else
|
||||
(100.0 / users.distinct.count * finishers.count).round(2)
|
||||
(100.0 / contributors.size * finishers_count).round(2)
|
||||
end
|
||||
end
|
||||
|
||||
@ -72,8 +72,11 @@ class Exercise < ApplicationRecord
|
||||
end
|
||||
|
||||
def average_number_of_submissions
|
||||
user_count = internal_users.distinct.count + external_users.distinct.count
|
||||
user_count.zero? ? 0 : submissions.count / user_count.to_f
|
||||
contributors.empty? ? 0 : submissions.count / contributors.size.to_f
|
||||
end
|
||||
|
||||
def contributors
|
||||
@contributors ||= internal_users.distinct + external_users.distinct + programming_groups.distinct
|
||||
end
|
||||
|
||||
def time_maximum_score(contributor)
|
||||
@ -201,6 +204,17 @@ class Exercise < ApplicationRecord
|
||||
total_working_time
|
||||
FROM working_times_with_index
|
||||
JOIN internal_users ON contributor_type = 'InternalUser' AND contributor_id = internal_users.id
|
||||
UNION ALL
|
||||
SELECT index,
|
||||
contributor_id,
|
||||
contributor_type,
|
||||
concat('PG ', programming_groups.id::varchar) AS name,
|
||||
score,
|
||||
start_time,
|
||||
working_time_per_score,
|
||||
total_working_time
|
||||
FROM working_times_with_index
|
||||
JOIN programming_groups ON contributor_type = 'ProgrammingGroup' AND contributor_id = programming_groups.id
|
||||
ORDER BY index, score ASC;
|
||||
"
|
||||
end
|
||||
@ -262,7 +276,7 @@ class Exercise < ApplicationRecord
|
||||
(created_at - Lag(created_at) OVER (partition BY contributor_id, exercise_id ORDER BY created_at)) AS working_time
|
||||
FROM submissions
|
||||
WHERE #{self.class.sanitize_sql(['exercise_id = ?', id])}
|
||||
AND contributor_type = 'ExternalUser'
|
||||
AND contributor_type IN ('ExternalUser', 'ProgrammingGroup')
|
||||
GROUP BY contributor_id,
|
||||
id,
|
||||
exercise_id), max_points AS
|
||||
@ -367,7 +381,7 @@ class Exercise < ApplicationRecord
|
||||
end
|
||||
|
||||
def retrieve_working_time_statistics
|
||||
@working_time_statistics = {'InternalUser' => {}, 'ExternalUser' => {}}
|
||||
@working_time_statistics = {'InternalUser' => {}, 'ExternalUser' => {}, 'ProgrammingGroup' => {}}
|
||||
self.class.connection.exec_query(user_working_time_query).each do |tuple|
|
||||
tuple = tuple.merge('working_time' => format_time_difference(tuple['working_time']))
|
||||
@working_time_statistics[tuple['contributor_type']][tuple['contributor_id'].to_i] = tuple
|
||||
@ -532,9 +546,8 @@ class Exercise < ApplicationRecord
|
||||
maximum_score(contributor).to_i == maximum_score.to_i
|
||||
end
|
||||
|
||||
def finishers
|
||||
ExternalUser.joins(:submissions).where(submissions: {exercise_id: id, score: maximum_score,
|
||||
cause: %w[submit assess remoteSubmit remoteAssess]}).distinct
|
||||
def finishers_count
|
||||
Submission.from(submissions.where(score: maximum_score, cause: %w[submit assess remoteSubmit remoteAssess]).group(:contributor_id, :contributor_type).select(:contributor_id, :contributor_type), 'submissions').count
|
||||
end
|
||||
|
||||
def set_default_values
|
||||
|
82
app/models/programming_group.rb
Normal file
82
app/models/programming_group.rb
Normal file
@ -0,0 +1,82 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ProgrammingGroup < ApplicationRecord
|
||||
include Contributor
|
||||
|
||||
has_many :programming_group_memberships, dependent: :destroy
|
||||
has_many :external_users, through: :programming_group_memberships, source_type: 'ExternalUser', source: :user
|
||||
has_many :internal_users, through: :programming_group_memberships, source_type: 'InternalUser', source: :user
|
||||
belongs_to :exercise
|
||||
|
||||
validate :group_size
|
||||
validate :no_erroneous_users
|
||||
accepts_nested_attributes_for :programming_group_memberships
|
||||
|
||||
def initialize(attributes = nil)
|
||||
@erroneous_users = []
|
||||
super
|
||||
end
|
||||
|
||||
def external_user?
|
||||
false
|
||||
end
|
||||
|
||||
def internal_user?
|
||||
false
|
||||
end
|
||||
|
||||
def self.nested_resource?
|
||||
true
|
||||
end
|
||||
|
||||
def programming_group?
|
||||
true
|
||||
end
|
||||
|
||||
def add(user)
|
||||
# Accessing the `users` method here will preload all users, which is otherwise done during validation.
|
||||
internal_users << user if user.internal_user? && users.exclude?(user)
|
||||
external_users << user if user.external_user? && users.exclude?(user)
|
||||
user
|
||||
end
|
||||
|
||||
def to_s
|
||||
displayname
|
||||
end
|
||||
|
||||
def displayname
|
||||
"Programming Group #{id}"
|
||||
end
|
||||
|
||||
def programming_partner_ids
|
||||
users.map(&:id_with_type)
|
||||
end
|
||||
|
||||
def users
|
||||
internal_users + external_users
|
||||
end
|
||||
|
||||
def users=(users)
|
||||
self.internal_users = []
|
||||
self.external_users = []
|
||||
users.each do |user|
|
||||
next @erroneous_users << user unless user.is_a?(User)
|
||||
|
||||
add(user)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def group_size
|
||||
if users.size < 2
|
||||
errors.add(:base, :size_too_small)
|
||||
end
|
||||
end
|
||||
|
||||
def no_erroneous_users
|
||||
@erroneous_users.each do |partner_id|
|
||||
errors.add(:base, :invalid_partner_id, partner_id:)
|
||||
end
|
||||
end
|
||||
end
|
15
app/models/programming_group_membership.rb
Normal file
15
app/models/programming_group_membership.rb
Normal file
@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ProgrammingGroupMembership < ApplicationRecord
|
||||
belongs_to :user, polymorphic: true
|
||||
belongs_to :programming_group
|
||||
|
||||
validate :unique_membership_for_exercise
|
||||
validates :user_id, uniqueness: {scope: %i[programming_group_id user_type]}
|
||||
|
||||
def unique_membership_for_exercise
|
||||
if user.programming_groups.where(exercise: programming_group.exercise).any?
|
||||
errors.add(:base, :already_exists, id_with_type: user.id_with_type)
|
||||
end
|
||||
end
|
||||
end
|
@ -24,6 +24,9 @@ class Submission < ApplicationRecord
|
||||
belongs_to :internal_users, lambda {
|
||||
where(submissions: {contributor_type: 'InternalUser'}).includes(:submissions)
|
||||
}, foreign_key: :contributor_id, class_name: 'InternalUser', optional: true
|
||||
belongs_to :programming_groups, lambda {
|
||||
where(submissions: {contributor_type: 'ProgrammingGroup'}).includes(:submissions)
|
||||
}, foreign_key: :contributor_id, class_name: 'ProgrammingGroup', optional: true
|
||||
delegate :execution_environment, to: :exercise
|
||||
|
||||
scope :final, -> { where(cause: %w[submit remoteSubmit]) }
|
||||
@ -49,12 +52,6 @@ class Submission < ApplicationRecord
|
||||
|
||||
# after_save :trigger_working_times_action_cable
|
||||
|
||||
def build_files_hash(files, attribute)
|
||||
files.map(&attribute.to_proc).zip(files).to_h
|
||||
end
|
||||
|
||||
private :build_files_hash
|
||||
|
||||
def collect_files
|
||||
@collect_files ||= begin
|
||||
ancestors = build_files_hash(exercise.files.includes(:file_type), :id)
|
||||
@ -202,8 +199,16 @@ class Submission < ApplicationRecord
|
||||
%w[study_group_id exercise_id cause]
|
||||
end
|
||||
|
||||
def users
|
||||
contributor.try(:users) || [contributor]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_files_hash(files, attribute)
|
||||
files.map(&attribute.to_proc).zip(files).to_h
|
||||
end
|
||||
|
||||
def prepared_runner
|
||||
request_time = Time.zone.now
|
||||
begin
|
||||
|
@ -9,6 +9,8 @@ class User < ApplicationRecord
|
||||
has_many :authentication_token, dependent: :destroy
|
||||
has_many :study_group_memberships, as: :user
|
||||
has_many :study_groups, through: :study_group_memberships, as: :user
|
||||
has_many :programming_group_memberships, as: :user
|
||||
has_many :programming_groups, through: :programming_group_memberships, as: :user
|
||||
has_many :exercises, as: :user
|
||||
has_many :file_types, as: :user
|
||||
has_many :submissions, as: :contributor
|
||||
@ -43,6 +45,10 @@ class User < ApplicationRecord
|
||||
is_a?(ExternalUser)
|
||||
end
|
||||
|
||||
def programming_group?
|
||||
false
|
||||
end
|
||||
|
||||
def learner?
|
||||
return true if current_study_group_id.nil?
|
||||
|
||||
@ -57,6 +63,10 @@ class User < ApplicationRecord
|
||||
@admin ||= platform_admin?
|
||||
end
|
||||
|
||||
def id_with_type
|
||||
self.class.name.downcase.first + id.to_s
|
||||
end
|
||||
|
||||
def store_current_study_group_id(study_group_id)
|
||||
@current_study_group_id = study_group_id
|
||||
self
|
||||
@ -79,6 +89,16 @@ class User < ApplicationRecord
|
||||
}
|
||||
end
|
||||
|
||||
def self.find_by_id_with_type(id_with_type)
|
||||
if id_with_type[0].casecmp('e').zero?
|
||||
ExternalUser.find(id_with_type[1..])
|
||||
elsif id_with_type[0].casecmp('i').zero?
|
||||
InternalUser.find(id_with_type[1..])
|
||||
else
|
||||
raise ActiveRecord::RecordNotFound
|
||||
end
|
||||
end
|
||||
|
||||
def self.ransackable_attributes(auth_object)
|
||||
if auth_object.present? && auth_object.admin?
|
||||
%w[name email external_id consumer_id platform_admin]
|
||||
|
@ -49,6 +49,13 @@ class ApplicationPolicy
|
||||
end
|
||||
private :teacher_in_study_group?
|
||||
|
||||
def author_in_programming_group?
|
||||
return false unless @record.contributor.programming_group?
|
||||
|
||||
@record.contributor.users.include?(@user)
|
||||
end
|
||||
private :author_in_programming_group?
|
||||
|
||||
def initialize(user, record)
|
||||
@user = user
|
||||
@record = record
|
||||
|
11
app/policies/programming_group_policy.rb
Normal file
11
app/policies/programming_group_policy.rb
Normal file
@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ProgrammingGroupPolicy < ApplicationPolicy
|
||||
def new?
|
||||
everyone
|
||||
end
|
||||
|
||||
def create?
|
||||
everyone
|
||||
end
|
||||
end
|
@ -9,7 +9,7 @@ class SubmissionPolicy < ApplicationPolicy
|
||||
# download_submission_file? is used in the live_streams_controller.rb
|
||||
%i[download? download_file? download_submission_file? run? score? show? statistics? stop? test?
|
||||
insights?].each do |action|
|
||||
define_method(action) { admin? || author? }
|
||||
define_method(action) { admin? || author? || author_in_programming_group? }
|
||||
end
|
||||
|
||||
def render_file?
|
||||
|
@ -4,7 +4,7 @@ h1 = @execution_environment
|
||||
table.table.table-striped class="#{@execution_environment.present? ? 'sortable' : ''}"
|
||||
thead
|
||||
tr
|
||||
- ['.exercise', '.users', '.score', '.maximum_score', '.stddev_score', '.percentage_correct', '.runs', '.worktime', '.stddev_worktime'].each do |title|
|
||||
- ['.exercise', '.users_and_programming_groups', '.score', '.maximum_score', '.stddev_score', '.percentage_correct', '.runs', '.worktime', '.stddev_worktime'].each do |title|
|
||||
th.header = t(title)
|
||||
tbody
|
||||
- @execution_environment.exercises.each do |exercise|
|
||||
|
@ -33,7 +33,7 @@ h4.mt-4 = t('activerecord.attributes.exercise_collections.exercises')
|
||||
tr
|
||||
th = '#'
|
||||
th = t('activerecord.attributes.exercise.title')
|
||||
th = t('activerecord.attributes.exercise.number_of_users')
|
||||
th = t('activerecord.attributes.exercise.number_of_users_and_programming_groups')
|
||||
th = t('activerecord.attributes.exercise.distinct_final_submissions')
|
||||
th = t('activerecord.attributes.exercise.finishing_rate')
|
||||
th = t('activerecord.attributes.exercise.average_score_percentage')
|
||||
@ -44,8 +44,8 @@ h4.mt-4 = t('activerecord.attributes.exercise_collections.exercises')
|
||||
tr
|
||||
td = exercise_collection_item.position
|
||||
td = link_to_if(policy(exercise).show?, exercise.title, exercise)
|
||||
td = exercise.users.distinct.count
|
||||
td = exercise.submissions.send(:final).distinct.count(:user_id)
|
||||
td = exercise.contributors.size
|
||||
td = exercise.submissions.send(:final).distinct.count(:contributor_id)
|
||||
td = exercise.finishers_percentage
|
||||
td = exercise.average_percentage
|
||||
td = link_to(t('shared.statistics'), statistics_exercise_path(exercise), 'data-turbolinks' => "false") if policy(exercise).statistics?
|
||||
|
@ -52,7 +52,7 @@ div.d-grid id='output_sidebar_uncollapsed' class='d-none col-sm-12 enforce-botto
|
||||
| 0
|
||||
= row(label: 'exercises.implement.feedback')
|
||||
= row(label: 'exercises.implement.messages')
|
||||
#score data-maximum-score=@exercise.maximum_score data-score=@exercise.final_submission(current_user).try(:score)
|
||||
#score data-maximum-score=@exercise.maximum_score data-score=@exercise.final_submission(current_contributor).try(:score)
|
||||
h4
|
||||
span == "#{t('activerecord.attributes.submission.score')}: "
|
||||
span.score
|
||||
|
@ -5,25 +5,26 @@
|
||||
- append_javascript_pack_tag('d3-tip')
|
||||
h1 = @exercise
|
||||
|
||||
= row(label: '.participants', value: @exercise.users.distinct.count)
|
||||
= row(label: '.participants', value: @exercise.contributors.size)
|
||||
|
||||
- [:intermediate, :final].each do |scope|
|
||||
= row(label: ".#{scope}_submissions") do
|
||||
= "#{@exercise.submissions.send(scope).count} (#{t('.users', count: @exercise.submissions.send(scope).distinct.count(:contributor_id))})"
|
||||
/TODO: Refactor next line
|
||||
= "#{@exercise.submissions.send(scope).count} (#{t('.users_and_programming_groups', count: Submission.from(@exercise.submissions.send(scope).group(:contributor_id, :contributor_type).select(:contributor_id, :contributor_type)).count)})"
|
||||
|
||||
= row(label: '.finishing_rate') do
|
||||
p
|
||||
- if @exercise.finishers.count
|
||||
- if @exercise.finishers_count
|
||||
span.number
|
||||
= @exercise.finishers.count
|
||||
= @exercise.finishers_count
|
||||
=<> t('shared.out_of')
|
||||
span.number
|
||||
= @exercise.users.distinct.count
|
||||
= @exercise.contributors.size
|
||||
=< t('exercises.statistics.external_users')
|
||||
- else
|
||||
= empty
|
||||
- finishers_count = @exercise.users.distinct.count
|
||||
- finishers_percentage = finishers_count == 0 ? 0 : (100.0 / finishers_count * @exercise.finishers.count).round(2)
|
||||
- finishers_count = @exercise.contributors.size
|
||||
- finishers_percentage = finishers_count == 0 ? 0 : (100.0 / finishers_count * @exercise.finishers_count).round(2)
|
||||
p = progress_bar(finishers_percentage)
|
||||
|
||||
= row(label: '.average_score') do
|
||||
@ -42,7 +43,7 @@ h1 = @exercise
|
||||
= row(label: '.average_worktime') do
|
||||
p = @exercise.average_working_time
|
||||
|
||||
- Hash[:internal_users => t('.internal_users'), :external_users => t('.external_users')].each_pair do |symbol, label|
|
||||
- Hash[:internal_users => t('.internal_users'), :external_users => t('.external_users'), :programming_groups => t('.programming_groups')].each_pair do |symbol, label|
|
||||
- submissions = Submission.where(contributor: @exercise.send(symbol), exercise: @exercise).in_study_group_of(current_user)
|
||||
- if !policy(@exercise).detailed_statistics?
|
||||
- submissions = submissions.final
|
||||
|
7
app/views/programming_groups/_form.html.slim
Normal file
7
app/views/programming_groups/_form.html.slim
Normal file
@ -0,0 +1,7 @@
|
||||
= form_for(@programming_group, url: exercise_programming_groups_path) do |f|
|
||||
= render('shared/form_errors', object: @programming_group)
|
||||
.mb-3
|
||||
= f.label(:programming_partner_ids, class: 'form-label')
|
||||
= f.text_field(:programming_partner_ids, class: 'form-control', required: true, value: (@programming_group.programming_partner_ids - [current_user.id_with_type]).join(', '))
|
||||
.help-block.form-text = t('.hints.programming_partner_ids')
|
||||
.actions.mb-0 = render('shared/submit_button', f: f, object: @programming_group)
|
14
app/views/programming_groups/new.html.slim
Normal file
14
app/views/programming_groups/new.html.slim
Normal file
@ -0,0 +1,14 @@
|
||||
h1 = t('shared.new_model', model: ProgrammingGroup.model_name.human)
|
||||
p
|
||||
=> t('programming_groups.new.own_user_id')
|
||||
b
|
||||
= current_user.id_with_type
|
||||
p
|
||||
= t('programming_groups.new.enter_partner_id', exercise_title: @exercise.title)
|
||||
= render('form')
|
||||
|
||||
div.mt-4
|
||||
a.btn.btn-success href=new_exercise_programming_group_path(@exercise) == t('programming_groups.new.check_invitation')
|
||||
|
||||
p.mt-4
|
||||
== t('programming_groups.new.work_alone', path: implement_exercise_path(@exercise))
|
@ -22,7 +22,7 @@ h1 = Submission.model_name.human(count: 2)
|
||||
- @submissions.each do |submission|
|
||||
tr
|
||||
td = link_to_if(submission.exercise && policy(submission.exercise).show?, submission.exercise, submission.exercise)
|
||||
td = link_to_if(policy(submission.contributor).show?, submission.contributor, submission.contributor)
|
||||
td = link_to_if(submission.contributor.is_a?(User) && policy(submission.contributor).show?, submission.contributor, submission.contributor)
|
||||
td = t("submissions.causes.#{submission.cause}")
|
||||
td = submission.score
|
||||
td = l(submission.created_at, format: :short)
|
||||
|
@ -8,7 +8,7 @@
|
||||
h1 = @submission
|
||||
|
||||
= row(label: 'submission.exercise', value: link_to_if(policy(@submission.exercise).show?, @submission.exercise, @submission.exercise))
|
||||
= row(label: 'submission.contributor', value: link_to_if(policy(@submission.contributor).show?, @submission.contributor, @submission.contributor))
|
||||
= row(label: 'submission.contributor', value: link_to_if(@submission.contributor.is_a?(User) && policy(@submission.contributor).show?, @submission.contributor, @submission.contributor))
|
||||
= row(label: 'submission.study_group', value: link_to_if(@submission.study_group.present? && policy(@submission.study_group).show?, @submission.study_group, @submission.study_group))
|
||||
= row(label: 'submission.cause', value: t("submissions.causes.#{@submission.cause}"))
|
||||
= row(label: 'submission.score', value: @submission.score)
|
||||
|
Reference in New Issue
Block a user