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