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
@ -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]
|
||||
|
Reference in New Issue
Block a user