diff --git a/app/assets/javascripts/channels/la_exercises.js b/app/assets/javascripts/channels/la_exercises.js
deleted file mode 100644
index ceef0135e..000000000
--- a/app/assets/javascripts/channels/la_exercises.js
+++ /dev/null
@@ -1,265 +0,0 @@
-$(document).on('turbo-migration:load', function() {
- if ($.isController('exercises') && $('.teacher_dashboard').isPresent()) {
-
- const exercise_id = $('.teacher_dashboard').data().exerciseId;
- const study_group_id = $('.teacher_dashboard').data().studyGroupId;
-
- $("tbody#posted_rfcs").children().each(function() {
- let $row = $(this);
- addClickEventToRfCEntry($row);
- });
-
- function addClickEventToRfCEntry($row) {
- $row.click(function () {
- Turbo.visit($(this).data("href"));
- });
- }
-
- const specific_channel = { channel: "LaExercisesChannel", exercise_id: exercise_id, study_group_id: study_group_id };
-
-
- App.la_exercise = App.cable.subscriptions.create(specific_channel, {
- connected: function () {
- // Called when the subscription is ready for use on the server
- },
-
- disconnected: function () {
- // Called when the subscription has been terminated by the server
- },
-
- received: function (data) {
- // Called when there's incoming data on the websocket for this channel
- if (data.type === 'rfc') {
- handleNewRfCdata(data);
- } else if (data.type === 'working_times') {
- handleWorkingTimeUpdate(data.working_time_data)
- }
- }
- });
-
- function handleNewRfCdata(data) {
- let $row = $('tr[data-id="' + data.id + '"]');
- if ($row.length === 0) {
- $row = $($('#posted_rfcs')[0].insertRow(0));
- }
- const $html = $(data.html);
- $row.replaceWith($html);
- $row = $html;
- $row.find('time').timeago();
- addClickEventToRfCEntry($row);
- }
-
- function handleWorkingTimeUpdate(data) {
- const user_progress = data['user_progress'];
- const additional_user_data = data['additional_user_data'];
-
- const user = additional_user_data[additional_user_data.length - 1][0];
- const position = userPosition[user.type + user.id]; // TODO validate: will result in undef. if not existent.
- // TODO: Do update
- }
-
- const graph_data = $('#initial_graph_data').data('graph_data');
- let userPosition = {};
-
- drawGraph(graph_data);
-
- function drawGraph(graph_data) {
- const user_progress = graph_data['user_progress'];
- const additional_user_data = graph_data['additional_user_data'];
- const user_info = additional_user_data.length - 1;
- const learners = additional_user_data[user_info]
-
- function get_minutes (time_stamp) {
- try {
- hours = time_stamp.split(":")[0];
- minutes = time_stamp.split(":")[1];
- seconds = time_stamp.split(":")[2];
- seconds /= 60;
- minutes = parseFloat(hours * 60) + parseInt(minutes) + seconds;
- if (minutes > 0){
- return minutes;
- } else{
- return parseFloat(seconds/60);
- }
- } catch (err) {
- return 0;
- }
- }
-
- function learners_name(index) {
- return additional_user_data[user_info][index]["name"] + ", ID: " + additional_user_data[user_info][index]["id"];
- }
-
- function learners_time(group, index) {
- if (user_progress[group] !== null && user_progress[group] !== undefined && user_progress[group][index] !== null) {
- return user_progress[group][index]
- } else {
- return 0;
- }
- }
-
- if (user_progress.length === 0) {
- // No data available
- $('#no_chart_data').removeClass("d-none");
- return;
- }
-
- const margin = ({top: 20, right: 20, bottom: 150, left: 80});
- const width = $('#chart_stacked').width();
- const height = 500;
- const users = user_progress[0].length; // # of users
- const n = user_progress.length; // # of different sub bars, called buckets
-
- let working_times_in_minutes = d3.range(n).map((index) => {
- if (user_progress[index] !== null) {
- return user_progress[index].map((time) => get_minutes(time))
- } else return new Array(users).fill(0);
- });
-
- let xAxis = svg => svg.append("g")
- .attr("transform", `translate(0,${height - margin.bottom})`)
- .call(d3.axisBottom(x).tickSizeOuter(0).tickFormat((index) => learners_name(index)));
-
- let yAxis = svg => svg.append("g")
- .attr("transform", `translate(${margin.left}, 0)`)
- .call(d3.axisLeft(y).tickSizeOuter(0).tickFormat((index) => index));
-
- let color = d3.scaleSequential(d3.interpolateRdYlGn)
- .domain([-0.5 * n, 1.5 * n]);
-
- let userAxis = d3.range(users); // the x-values shared by all series
-
- // Calculate the corresponding start and end value of each value;
- const yBarValuesGrouped = d3.stack()
- .keys(d3.range(n))
- (d3.transpose(working_times_in_minutes)) // stacked working_times_in_minutes
- .map((data, i) => data.map(([y0, y1]) => [y0, y1, i]));
-
- const maxYSingleBar = d3.max(working_times_in_minutes, y => d3.max(y));
-
- const maxYBarStacked = d3.max(yBarValuesGrouped, y => d3.max(y, d => d[1]));
-
- let x = d3.scaleBand()
- .domain(userAxis)
- .rangeRound([margin.left, width - margin.right])
- .padding(0.08);
-
- let y = d3.scaleLinear()
- .domain([0, maxYBarStacked])
- .range([height - margin.bottom, margin.top]);
-
- const svg = d3.select("#chart_stacked")
- .append("svg")
- .attr("width", '100%')
- .attr("height", '100%')
- .attr("viewBox", `0 0 ${width} ${height}`)
- .attr("preserveAspectRatio","xMinYMin meet");
-
- const rect = svg.selectAll("g")
- .data(yBarValuesGrouped)
- .enter().append("g")
- .attr("fill", (d, i) => color(i))
- .selectAll("rect")
- .data(d => d)
- .join("rect")
- .attr("x", (d, i) => x(i))
- .attr("y", height - margin.bottom)
- .attr("width", x.bandwidth())
- .attr("height", 0)
- .attr("class", (d) => "bar-stacked-"+d[2]);
-
- svg.append("g")
- .attr("class", "x axis")
- .call(xAxis)
- .selectAll("text")
- .style("text-anchor", "end")
- .attr("dx", "-.8em")
- .attr("dy", ".15em")
- .attr("transform", function(d) {
- return "rotate(-45)"
- });
-
- svg.append("g")
- .attr("class", "y axis")
- .call(yAxis);
-
- // Y Axis Label
- svg.append("text")
- .attr("transform", "rotate(-90)")
- .attr("x", (-height - margin.top + margin.bottom) / 2)
- .attr("dy", "+2em")
- .style("text-anchor", "middle")
- .text(I18n.t('exercises.study_group_dashboard.time_spent_in_minutes'))
- .style('font-size', 14);
-
- // X Axis Label
- svg.append("text")
- .attr("class", "x axis")
- .attr("text-anchor", "middle")
- .attr("x", (width + margin.left - margin.right) / 2)
- .attr("y", height)
- .attr("dy", '-1em')
- .text(I18n.t('exercises.study_group_dashboard.learner'))
- .style('font-size', 14);
-
- let tip = d3.tip()
- .attr('class', 'd3-tip')
- .offset([-10, 0])
- .html(function(_event, _d) {
- const e = rect.nodes();
- const i = e.indexOf(this) % learners.length;
- return "Student: " + learners_name(i) + "
" +
- "0: " + learners_time(0, i) + "
" +
- "1: " + learners_time(1, i) + "
" +
- "2: " + learners_time(2, i) + "
" +
- "3: " + learners_time(3, i) + "
" +
- "4: " + learners_time(4, i);
- });
-
- svg.call(tip);
-
- rect.on('mouseenter', tip.show)
- .on('mouseout', tip.hide);
-
- function transitionGrouped() {
- // Show all sub-bars next to each other
- y.domain([0, maxYSingleBar]);
-
- rect.transition()
- .duration(500)
- .delay((d, i) => i * 20)
- .attr("x", (d, i) => x(i) + x.bandwidth() / n * d[2])
- .attr("width", x.bandwidth() / n)
- .transition()
- .attr("y", d => y(d[1] - d[0]))
- .attr("height", d => y(0) - y(d[1] - d[0]));
- }
-
- function transitionStacked() {
- // Show all sub-bars on top of each other
- y.domain([0, maxYBarStacked]);
-
- rect.transition()
- .duration(500)
- .delay((d, i) => i * 20)
- .attr("y", d => y(d[1]))
- .attr("height", d => y(d[0]) - y(d[1]))
- .transition()
- .attr("x", (d, i) => x(i))
- .attr("width", x.bandwidth());
- }
-
- $('#no_chart_data').addClass("d-none");
- transitionStacked();
- // ToDo: Add button to switch using transitionGrouped();
-
- buildDictionary(additional_user_data);
- }
-
- function buildDictionary(users) {
- users[users.length - 1].forEach(function(user, index) {
- userPosition[user.type + user.id] = index;
- });
- }
- }
-});
diff --git a/app/channels/la_exercises_channel.rb b/app/channels/la_exercises_channel.rb
deleted file mode 100644
index 163e9f117..000000000
--- a/app/channels/la_exercises_channel.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-class LaExercisesChannel < ApplicationCable::Channel
- def subscribed
- set_and_authorize_exercise
- set_and_authorize_study_group
-
- stream_from specific_channel unless subscription_rejected?
- end
-
- def unsubscribed
- stop_all_streams
- end
-
- private
-
- def specific_channel
- "la_exercises_#{@exercise.id}_channel_study_group_#{@study_group.id}"
- end
-
- def set_and_authorize_exercise
- @exercise = Exercise.find(params[:exercise_id])
- reject unless ExercisePolicy.new(current_user, @exercise).implement?
- rescue ActiveRecord::RecordNotFound
- reject
- end
-
- def set_and_authorize_study_group
- @study_group = @exercise.study_groups.find(params[:study_group_id])
- reject unless StudyGroupPolicy.new(current_user, @study_group).stream_la?
- rescue ActiveRecord::RecordNotFound
- reject
- end
-end
diff --git a/app/helpers/action_cable_helper.rb b/app/helpers/action_cable_helper.rb
deleted file mode 100644
index 254ebf8cd..000000000
--- a/app/helpers/action_cable_helper.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-module ActionCableHelper
- def trigger_rfc_action_cable
- Thread.new do
- # Context: RfC
- if submission.study_group_id.present?
- ActionCable.server.broadcast(
- "la_exercises_#{exercise_id}_channel_study_group_#{submission.study_group_id}",
- type: :rfc,
- id:,
- html: ApplicationController.render(partial: 'request_for_comments/list_entry',
- locals: {request_for_comment: self})
- )
- end
- rescue StandardError => e
- Sentry.capture_exception(e)
- ensure
- ActiveRecord::Base.connection_pool.release_connection
- end
- end
-
- def trigger_rfc_action_cable_from_comment
- # Context: Comment
- request_for_comment.trigger_rfc_action_cable
- end
-
- def trigger_working_times_action_cable
- Thread.new do
- # Context: Submission
- if study_group_id.present?
- ActionCable.server.broadcast(
- "la_exercises_#{exercise_id}_channel_study_group_#{study_group_id}",
- type: :working_times,
- working_time_data: exercise.get_working_times_for_study_group(study_group_id, user)
- )
- end
- rescue StandardError => e
- Sentry.capture_exception(e)
- ensure
- ActiveRecord::Base.connection_pool.release_connection
- end
- end
-end
-
-# TODO: Check if any user is connected and prevent preparing the data otherwise
diff --git a/app/models/comment.rb b/app/models/comment.rb
index 4674aec73..ed42a1e80 100644
--- a/app/models/comment.rb
+++ b/app/models/comment.rb
@@ -3,14 +3,12 @@
class Comment < ApplicationRecord
# inherit the creation module: encapsulates that this is a polymorphic user, offers some aliases and makes sure that all necessary attributes are set.
include Creation
- include ActionCableHelper
attr_accessor :username, :date, :updated, :editable
belongs_to :file, class_name: 'CodeOcean::File'
has_one :submission, through: :file, source: :context, source_type: 'Submission'
has_one :request_for_comment, through: :submission
- # after_save :trigger_rfc_action_cable_from_comment
def only_comment_for_rfc?
request_for_comment.comments.one?
diff --git a/app/models/request_for_comment.rb b/app/models/request_for_comment.rb
index 09488041a..e90079367 100644
--- a/app/models/request_for_comment.rb
+++ b/app/models/request_for_comment.rb
@@ -2,7 +2,6 @@
class RequestForComment < ApplicationRecord
include Creation
- include ActionCableHelper
# SOLVED: The author explicitly marked the RfC as solved.
# SOFT_SOLVED: The author did not mark the RfC as solved but reached the maximum score in the corresponding exercise at any time.
@@ -21,8 +20,6 @@ class RequestForComment < ApplicationRecord
scope :in_range, ->(from, to) { from == DateTime.new(0) && to > 5.seconds.ago ? all : where(created_at: from..to) }
scope :with_comments, -> { select {|rfc| rfc.comments.any? } }
- # after_save :trigger_rfc_action_cable
-
def commenters
comments.map(&:user).uniq
end
diff --git a/app/models/submission.rb b/app/models/submission.rb
index 83b2da2be..36245e9f6 100644
--- a/app/models/submission.rb
+++ b/app/models/submission.rb
@@ -3,7 +3,6 @@
class Submission < ApplicationRecord
include Context
include ContributorCreation
- include ActionCableHelper
CAUSES = %w[assess download file render run save submit test autosave requestComments remoteAssess
remoteSubmit].freeze
@@ -48,8 +47,6 @@ class Submission < ApplicationRecord
validates :cause, inclusion: {in: CAUSES}
- # after_save :trigger_working_times_action_cable
-
def collect_files
@collect_files ||= begin
ancestors = build_files_hash(exercise&.files&.includes(:file_type), :id)
diff --git a/app/policies/study_group_policy.rb b/app/policies/study_group_policy.rb
index feeacc037..16eadc8d8 100644
--- a/app/policies/study_group_policy.rb
+++ b/app/policies/study_group_policy.rb
@@ -5,7 +5,7 @@ def index?
admin? || teacher?
end
- %i[show? edit? update? stream_la? set_as_current?].each do |action|
+ %i[show? edit? update? set_as_current?].each do |action|
define_method(action) { admin? || teacher_in_study_group? }
end
diff --git a/config/locales/de/exercise.yml b/config/locales/de/exercise.yml
index 98a5a7b71..63da0ffd9 100644
--- a/config/locales/de/exercise.yml
+++ b/config/locales/de/exercise.yml
@@ -250,9 +250,7 @@ de:
users_and_programming_groups: "%{count} verschiedene Teilnehmende und Programmiergruppen"
worktime: Arbeitszeit
study_group_dashboard:
- learner: Lerner
live_dashboard: Live Dashboard
no_data_yet: Bisher sind keine Daten verfügbar
related_requests_for_comments: Zugehörige Kommentaranfragen
- time_spent_in_minutes: benötigte Zeit in Minuten
time_spent_per_learner: Verwendete Zeit pro Lerner
diff --git a/config/locales/en/exercise.yml b/config/locales/en/exercise.yml
index 2397a84f2..5594d30d3 100644
--- a/config/locales/en/exercise.yml
+++ b/config/locales/en/exercise.yml
@@ -250,9 +250,7 @@ en:
users_and_programming_groups: "%{count} distinct users and programming groups"
worktime: Working Time
study_group_dashboard:
- learner: Learner
live_dashboard: Live Dashboard
no_data_yet: No data available yet
related_requests_for_comments: Related Requests for Comments
- time_spent_in_minutes: Time spent in Minutes
time_spent_per_learner: Time spent per Learner