From 05ff8251e7b2a71c54c6429657c7b25a8d9da955 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Tue, 16 Sep 2025 16:01:10 +1000 Subject: [PATCH] Update for 10.0.x - ignore assessments done during tutorials --- README.md | 38 ++-- assessment_time.rb | 458 +++++++++++++++------------------------- extract_ontrack_data.rb | 15 +- 3 files changed, 200 insertions(+), 311 deletions(-) diff --git a/README.md b/README.md index f93c484..45d500a 100644 --- a/README.md +++ b/README.md @@ -3,22 +3,28 @@ 1: Download the production.log file 2: Run script in `extract_ontrack_data.rb` in rails console on production server - - Download to local folder +- Download to local folder Steps: - - Move to home on server - - scp to local machine - - tasks.csv - - users.csv - - units.csv - - projects.csv - - tutorials.csv + - Move to home on server + - scp to local machine + - tasks.csv + - users.csv + - units.csv + - projects.csv + - tutorials.csv -3: Extract only assessment related activities: - - Identify only relevant lines - beware changes of date location - `sed -nE '/.* assessing task|Started PUT .*projects|Started GET .*portfolio|Started GET "\/api\/units\/.*tasks.inbox|Started GET "\/api\/projects\/.*task_def_id.*submission_details/p' production.log > stage1.log` - - Strip date from before "Started" - `cat stage1.log | gawk 'match($0, /^20.*INFO: (.*)$|^(.*)$/, a) {print a[1]a[2]}' > stage2.log` - - `cat stage2.log | gawk 'match($0, /Started (PUT) .*\/projects\/([0-9]+)["?].* for (.*) at (.*)(\+|-)[0-9].*$|Started (GET) .*\/([0-9]+)\/.*\/(inbox).* for (.*) at (.*)(\+|-)[0-9].*$|Started (GET|PUT) .*\/([0-9]+)\/.*\/([0-9]+)["/?].* for (.*) at (.*)(\+|\-)[0-9].*$|^(.*) (assessing) task ([0-9]+) to (.*)$/, a) {print a[1] a[6] a[12] a[19] "," a[2] a[7] a[13] a[18] "," a[8] a[14] a[20] "," a[3] a[9] a[15] "," a[4] a[10] a[16]}' > ontrack-time-data-3.csv` -4: Run `bundle exec ruby assessment_time.rb` -5: Copy formulas from other xlsx versions +3: Extract only assessment related activities: +- Identify only relevant lines - beware changes of date location + ```bash + sed -nE '/.* assessing task|Started PUT .*projects|Started GET .*portfolio|Started GET "\/api\/units\/.*tasks.inbox|Started GET "\/api\/projects\/.*task_def_id.*submission_details/p' production.log > stage1.log + ``` +- Strip date from before "Started" + ```bash + cat stage1.log | gawk 'match($0, /^20.*INFO: (.*)$|^(.*)$/, a) {print a[1]a[2]}' > stage2.log + ``` +- ```bash + cat stage2.log | gawk 'match($0, /Started (PUT) .*/projects/([0-9]+)["?].* for (.*) at (.*)(+|-)[0-9].*$|Started (GET) ._/([0-9]+)/._/(inbox)._ for (._) at (._)(+|-)[0-9]._$|Started (GET|PUT) .*/([0-9]+)/.*/([0-9]+)["/?].* for (.*) at (.*)(+|-)[0-9].*$|^(._) (assessing) task ([0-9]+) to (._)$/, a) {print a[1] a[6] a[12] a[19] "," a[2] a[7] a[13] a[18] "," a[8] a[14] a[20] "," a[3] a[9] a[15] "," a[4] a[10] a[16]}' > ontrack-time-data-3.csv + ``` +- 4: Run `bundle exec ruby assessment_time.rb` +- 5: Copy formulas from other xlsx versions diff --git a/assessment_time.rb b/assessment_time.rb index 398e7c4..ccd3a07 100644 --- a/assessment_time.rb +++ b/assessment_time.rb @@ -13,49 +13,14 @@ def initialize @id = nil end - def id - @id - end - - def id= value - @id = value - end - - def username - @username - end - - def username= value - @username = value - end - - def name - @name - end - - def name= value - @name = value - end - - def role - @role - end + attr_accessor :id, :username, :name, :role - def role= value - @role = value - end end class TaskDefinition - @id - def id - @id - end + attr_accessor :id - def id= value - @id = value - end end class Task @@ -66,37 +31,16 @@ def initialize @activity = [] end - def task_def - @task_def - end - - def task_def= value - @task_def = value - end - - def id - @id - end - - def id= value - @id = value - end - - def project - @project - end + attr_accessor :task_def, :id + attr_reader :project, :activity - def project= value + def project=(value) @project = value @project.add_task self end - def activity - @activity - end - - def add_activity value + def add_activity(value) @activity << value end @@ -114,25 +58,19 @@ class Tutorial @tutor = nil @code = nil @student_count = 0 + @meeting_time = nil # string, eg. 10:00 + @meeting_day = nil # string, eg. Wednesday - def initialize unit, tutor, code, count + def initialize(unit, tutor, code, count, meeting_time, meeting_day) @unit = unit @tutor = tutor @code = code @student_count = count + @meeting_time = meeting_time + @meeting_day = meeting_day end - def tutor - @tutor - end - - def code - @code - end - - def student_count - @student_count - end + attr_reader :tutor, :code, :student_count, :meeting_time, :meeting_day end class Unit @@ -156,47 +94,22 @@ def initialize @tutorials = [] end - def name - @name - end - - def name= value - @name = value - end - - def id - @id - end - - def id= value - @id = value - end - - def code - @code - end - - def code= value - @code = value - end - - def student_count - @student_count - end + attr_accessor :name, :id, :code + attr_reader :student_count, :tutorials - def student_count= value + def student_count=(value) @student_count = value.to_i end - def add_project project + def add_project(project) @projects << project end - def add_tutorial tutorial + def add_tutorial(tutorial) @tutorials << tutorial end - def task_def_for_id task_id + def task_def_for_id(task_id) return @task_defs[task_id] if @task_defs.has_key? task_id result = TaskDefinition.new result.id = task_id @@ -204,7 +117,7 @@ def task_def_for_id task_id result end - def add_session session + def add_session(session) @sessions << session end @@ -212,39 +125,58 @@ def report_summary(stream) return if @sessions.length == 0 # puts "Sessions #{@name} length: #{@sessions.length}" - total_duration = @sessions.inject(0){|sum,e| sum + e.duration } - total_assessments = @sessions.inject(0){|sum,e| sum + e.number_assessments } + total_duration = @sessions.inject(0) { |sum, e| sum + e.duration } + total_assessments = @sessions.inject(0) { |sum, e| sum + e.number_assessments } - stream.write "#{@id},all,#{@code},\"#{@name}\",#{student_count},#{total_duration},#{total_assessments},#{ (total_duration / 60.0) / student_count}\n" + stream.write "#{@id},all,#{@code},\"#{@name}\",#{student_count},#{total_duration},#{total_assessments},#{(total_duration / 60.0) / student_count}\n" - @sessions.group_by(&:marker).each do |k,v| + @sessions.group_by(&:marker).each do |k, v| # Get tutorials for marker - marker_tutorials = @tutorials.select{|t| t.tutor == k } - total_students = marker_tutorials.inject(0){|sum,e| sum + e.student_count } unless marker_tutorials.length == 0 + marker_tutorials = @tutorials.select { |t| t.tutor == k } + total_students = marker_tutorials.inject(0) { |sum, e| sum + e.student_count } unless marker_tutorials.length == 0 total_students = 1 if total_students.nil? || total_students <= 0 - marker_duration = v.inject(0){|sum,e| sum + e.duration } - marker_assessments = v.inject(0){|sum,e| sum + e.number_assessments } + marker_duration = v.inject(0) { |sum, e| sum + e.duration } + marker_assessments = v.inject(0) { |sum, e| sum + e.number_assessments } tutor_name = k.nil? ? "none" : k.name - stream.write "#{@id},#{tutor_name},#{@code},\"#{@name}\",#{total_students},#{marker_duration},#{marker_assessments},#{ (marker_duration / 60.0) / total_students}\n" + stream.write "#{@id},#{tutor_name},#{@code},\"#{@name}\",#{total_students},#{marker_duration},#{marker_assessments},#{(marker_duration / 60.0) / total_students},#{marker_duration / total_students}\n" end + # + # @sessions.group_by(&:marker).each do |marker, sessions| + # # split sessions into during_tutorial vs not + # sessions.group_by(&:during_tutorial).each do |during, group| + # total_students = @tutorials.select { |t| t.tutor == marker }.inject(0) { |sum, t| sum + t.student_count } + # total_students = 1 if total_students.nil? || total_students <= 0 + + # duration = group.inject(0) { |sum, e| sum + e.duration } + # assessments = group.inject(0) { |sum, e| sum + e.number_assessments } + + # tutor_name = marker.nil? ? "none" : marker.name + # tut_flag = during ? "during_tutorial" : "outside_tutorial" + + # stream.write "#{@id},#{tutor_name},#{@code},\"#{@name}\",#{total_students},#{duration},#{assessments},#{(duration / 60.0) / total_students},#{duration / total_students},#{tut_flag}\n" + # end + # end end - def report_session_details stream + def report_session_details(stream) return if @sessions.length == 0 stream.write("\nDetails for #{@code} #{@name} (#{@id})\n") - @sessions.group_by(&:marker).each do |k,v| + @sessions.group_by(&:marker).each do |k, v| tutor_name = k.nil? ? "none" : k.name stream.write("activity for #{tutor_name} in #{@code} #{@name}\n") + # Was this feedback done during their tutorial? + during_their_tutorial = false + v.each do |session| stream.write(" #{session.start_time} - #{session.end_time} (#{session.duration} minutes)\n") - + session.activity_log.each do |activity| - stream.write(" #{activity.action} #{activity.task_def&.id} #{activity.project&.id} #{activity.time_stamp}\n") + stream.write(" #{activity.action}#{" #{activity.status}" if activity.action.start_with?('assessing')} #{activity.task_def&.id} #{activity.project&.id} #{activity.time_stamp}\n") end end end @@ -259,37 +191,16 @@ class MarkingSession @number_assessments = 0 @duration = nil @activity_log = [] + @during_tutorial = false def initialize @number_assessments = 0 @duration = 0 end - def number_assessments - @number_assessments - end - - def marker - @marker - end - - def duration - @duration - end - - def activity_log - @activity_log - end - - def start_time - @start_time - end - - def end_time - @end_time - end + attr_reader :number_assessments, :marker, :duration, :activity_log, :start_time, :end_time, :during_tutorial - def set_details start_time, marker, unit, end_time, num_assessments, duration, activity_log + def set_details(start_time, marker, unit, end_time, num_assessments, duration, activity_log) @start_time = start_time @marker = marker @unit = unit @@ -297,6 +208,7 @@ def set_details start_time, marker, unit, end_time, num_assessments, duration, a @number_assessments = num_assessments @duration = duration @activity_log = activity_log + # @during_tutorial = during_tutorial @unit.add_session self end @@ -308,17 +220,39 @@ def initialize @activity = [] end - def add_activity value + def add_activity(value) @activity << value end - def record_end_session start_time, marker, unit, end_time, num_assessments, activity_log + def record_end_session(start_time, marker, unit, end_time, num_assessments, activity_log) + # byebug name = marker.name unless marker.nil? duration = ((end_time - start_time) * 24 * 60).to_i return if duration == 0 session = MarkingSession.new + + # during_tutorial = false + + # unless marker.nil? + # user_id = marker.id + # # Tutorials run by the user + # tutorials = unit.tutorials.select { |t| t.tutor.id == user_id } + # tutorials.each do |tut| + # # Compare day of week + # next unless start_time.strftime('%A') == tut.meeting_day + # # Parse tutorial time string into a Time object on the same day + # tut_time_obj = DateTime.parse("#{start_time.strftime('%Y-%m-%d')} #{tut.meeting_time}") + + # # Check if tutorial time falls between start_time and end_time + # if tut_time_obj >= start_time && tut_time_obj <= end_time + # during_tutorial = true + # # do something + # end + # end + # end + session.set_details start_time, marker, unit, end_time, num_assessments, duration, activity_log # puts "#{unit.id},#{unit.code},#{name},#{start_time},#{end_time},#{num_assessments},#{duration}" # if !marker.nil? && marker.id == "13883" @@ -336,14 +270,15 @@ def record_assessment_activity_to_tasks # puts "-----------" @activity.each do |activity| + next if activity.during_tutorial session_start = activity.time_stamp if session_start.nil? # look for end conditions first... # is after inbox.... and either timeout, or assessed by different user, or change of unit if post_inbox && ( - (prev != nil && activity.time_diff(prev) > THRESHOLD) || - (activity.action == "assessing" && (activity.user == activity.project.user || marker != nil && activity.user != marker)) || - (activity.project != nil && activity.project.unit != unit) ) + (!prev.nil? && activity.time_diff(prev) > THRESHOLD) || + (activity.action == "assessing" && (activity.user == activity.project.user || (!marker.nil? && activity.user != marker))) || + (!activity.project.nil? && activity.project.unit != unit)) # end the session with what we have already recorded # puts "NEW SESSION - #{num_assessments}" record_end_session session_start, marker, unit, prev.time_stamp, num_assessments, activity_log @@ -360,27 +295,7 @@ def record_assessment_activity_to_tasks activity_log << activity # Now read this activity... - unless post_inbox # if it is not post inbox - we are looking for a start - # puts activity.action - # Looking for start or marker activity on this IP address - if activity.action == 'inbox' - # inbox request marks start... we wont know who yet - post_inbox = true - session_start = activity.time_stamp - unit = activity.unit - prev = activity - elsif (activity.action == "assessing") && activity.user != activity.project.user - # marker updated task... lets mark start of session here... - # TODO: Look back for start of session? - post_inbox = true - session_start = activity.time_stamp - unit = activity.project.unit - prev = activity - - marker = activity.user - num_assessments = 1 # this was an assessment - end - else # this is part of the session... + if post_inbox # this is part of the session... # within marking timeframe... assume this is part of the sequence prev = activity @@ -397,7 +312,25 @@ def record_assessment_activity_to_tasks num_assessments += 1 # puts num_assessments end - end + elsif activity.action == 'inbox' # if it is not post inbox - we are looking for a start + # puts activity.action + # Looking for start or marker activity on this IP address + post_inbox = true + session_start = activity.time_stamp + unit = activity.unit + prev = activity + # inbox request marks start... we wont know who yet + elsif (activity.action == "assessing") && activity.user != activity.project.user + # marker updated task... lets mark start of session here... + # TODO: Look back for start of session? + post_inbox = true + session_start = activity.time_stamp + unit = activity.project.unit + prev = activity + + marker = activity.user + num_assessments = 1 # this was an assessment + end end # All activities done... do we need to record end of this session? @@ -407,72 +340,26 @@ def record_assessment_activity_to_tasks end end - def activity - @activity - end + attr_reader :activity end class AssessmentActivity def initialize @action = nil + @status = nil @project = nil @unit = nil @user = nil @time_stamp = nil @ip_address = nil @task_def = nil + @during_tutorial = false @ip_self_idx = 0 @project_self_idx = 0 end - def task_def - @task_def - end - - def task_def= value - @task_def = value - end - - def unit - @unit - end - - def unit= value - @unit = value - end - - def project - @project - end - - def project= value - @project = value - end - - def action - @action - end - - def action= value - @action = value - end - - def time_stamp - @time_stamp - end - - def time_stamp= value - @time_stamp = value - end - - def user - @user - end - - def user= value - @user = value - end + attr_accessor :task_def, :unit, :project, :action, :status, :time_stamp, :user, :ip_address, :during_tutorial # def project_self_idx # @project_self_idx @@ -482,17 +369,14 @@ def user= value # @project_self_idx = value # end - def ip_address - @ip_address - end - - def ip_address= value - @ip_address = value - end - - def load csv, projects, tasks, units, ip_addresses, users + def load(csv, projects, tasks, units, ip_addresses, users) puts csv.inspect @action = csv[0] + @status = nil + if @action.start_with?("assessing") + @status = @action.sub(/^assessing\s*/, "") # removes "assessing " from start + @action = 'assessing' + end if @action =~ /GET|PUT/ if csv[2] == 'inbox' @@ -513,7 +397,7 @@ def load csv, projects, tasks, units, ip_addresses, users @user = users[csv[1]] @project = task.project @task_def = task.task_def - + last_put = task.last_put_activity if last_put.nil? puts "no last put - #{csv.inspect}" @@ -523,6 +407,26 @@ def load csv, projects, tasks, units, ip_addresses, users @ip_address = last_put.ip_address @time_stamp = last_put.time_stamp end + + unless @user.nil? + user_id = @user.id + tutorials = @project.unit.tutorials.select { |t| t.tutor.id == user_id } + tutorials.each do |tut| + # Make sure the activity is on the same day as the tutorial + next unless @time_stamp.strftime('%A') == tut.meeting_day + + # Tutorial start as DateTime + tut_start = DateTime.parse("#{@time_stamp.strftime('%Y-%m-%d')} #{tut.meeting_time}") + # Tutorial end 2 hours later + tut_end = tut_start + Rational(2, 24) # 2 hours = 2/24 of a day + + next unless @time_stamp >= tut_start && @time_stamp <= tut_end + @during_tutorial = true + # byebug + break # no need to check other tutorials + end + end + end unless @project.nil? @@ -539,13 +443,13 @@ def load csv, projects, tasks, units, ip_addresses, users @ip_self_idx = ip_addresses[@ip_address].activity.length - 1 end - def time_diff other + def time_diff(other) ((@time_stamp - other.time_stamp) * 24 * 60).to_i end # def elapsed_minutes # return 0 if @project_self_idx == 0 # no time for first activity - + # prev_activity = @project.next_activity self, -1 # tmp = time_diff prev_activity @@ -563,58 +467,33 @@ def initialize @tasks = {} end - def unit - @unit - end + attr_accessor :user, :id, :tasks + attr_reader :unit - def unit=value + def unit=(value) @unit = value @unit.add_project(self) end - def user - @user - end - - def user=value - @user = value - end - - def id - @id - end - - def id=value - @id = value - end - - def tasks - @tasks - end - - def tasks=value - @tasks = value - end - - def add_task value + def add_task(value) @tasks[value.task_def.id] = value end - def task_for_task_def task_def_id + def task_for_task_def(task_def_id) @tasks[task_def_id] end - def add_activity value + def add_activity(value) @activity[value.ip_address] = {} unless @activity.has_key? value.ip_address @activity[value.ip_address][value.task_def.id] = [] unless @activity.has_key? value.task_def.id @activity[value.ip_address][value.task_def.id] << value - @activity[value.ip_address][value.task_def.id].length - 1 # return index of assessment activity + @activity[value.ip_address][value.task_def.id].length - 1 # return index of assessment activity end # Return the activity relative to a given activity (based on offset) - def next_activity value, offset + def next_activity(value, offset) arr = @activity[value.ip_address][value.task_def.id] arr_idx = value.project_self_idx + offset # Check valid index @@ -635,13 +514,13 @@ def initialize def load_users puts 'loading users' - CSV.foreach('users.csv', :headers => true) do |csv| - result = User.new() + CSV.foreach('users.csv', headers: true) do |csv| + result = User.new result.id = csv[0] result.username = csv[1] result.name = csv[2] result.role = csv[3] - + @users_by_id[csv[0]] = result @users_by_username[csv[1]] = result @@ -652,13 +531,13 @@ def load_users def load_units puts 'loading units' - CSV.foreach('units.csv', :headers => true) do |csv| - result = Unit.new() + CSV.foreach('units.csv', headers: true) do |csv| + result = Unit.new result.id = csv[0] result.name = csv[1] result.code = csv[2] result.student_count = csv[3] - + @units[csv[0]] = result # puts "Add unit #{result.id} = #{result.name}" end @@ -667,13 +546,13 @@ def load_units def load_tutorials puts 'loading tutorials' - CSV.foreach('tutorials.csv', :headers => true) do |csv| + CSV.foreach('tutorials.csv', headers: true) do |csv| unit = @units[csv[0]] tutor = @users_by_id[csv[1]] - result = Tutorial.new(unit, tutor, csv[2], csv[3].to_i) + result = Tutorial.new(unit, tutor, csv[2], csv[3].to_i, csv[5], csv[6]) unit.add_tutorial(result) - + # puts "Add tutorial #{result.code} = #{result.tutor.name} #{result.student_count}" end end @@ -681,26 +560,26 @@ def load_tutorials def load_projects puts 'loading projects' - CSV.foreach('projects.csv', :headers => true) do |csv| - result = Project.new() + CSV.foreach('projects.csv', headers: true) do |csv| + result = Project.new result.id = csv[0] result.unit = @units[csv[1]] result.user = @users_by_id[csv[2]] - + @projects[csv[0]] = result # puts csv.inspect # puts result.inspect # puts "Add project #{result.id} to #{result.unit.code} for #{result.user.name}" end end - + def load_tasks puts 'loading tasks' @tasks = {} - CSV.foreach('tasks.csv', :headers => true) do |csv| + CSV.foreach('tasks.csv', headers: true) do |csv| project = @projects[csv[1]] - result = Task.new() + result = Task.new result.id = csv[0] result.task_def = project.unit.task_def_for_id csv[2] result.project = project # must be after loading task def as this links them together @@ -712,7 +591,7 @@ def load_tasks end end - def process file_name + def process(file_name) CSV.foreach(file_name) do |csv| result = AssessmentActivity.new result.load csv, @projects, @tasks, @units, @ip_addresses, @users_by_username @@ -726,14 +605,13 @@ def report_time end # Report unit times - date = DateTime.now puts puts "activity summay" File.open("activity_summary_#{date}.log", "w") do |stream| - stream.write "id,assessor,unit code,unit name,students,minutes,assessments,hours/student\n" + stream.write "id,assessor,unit code,unit name,students,minutes,assessments,hours/student,minutes/student\n" @units.values.each do |unit| print "." unit.report_summary(stream) @@ -755,13 +633,13 @@ def report_time end # byebug -log = LogFile.new() -log.load_users() -log.load_units() -log.load_projects() -log.load_tasks() -log.load_tutorials() +log = LogFile.new +log.load_users +log.load_units +log.load_projects +log.load_tasks +log.load_tutorials log.process('ontrack-time-data-3.csv') -log.report_time() +log.report_time diff --git a/extract_ontrack_data.rb b/extract_ontrack_data.rb index cc4af25..bb7f6b0 100644 --- a/extract_ontrack_data.rb +++ b/extract_ontrack_data.rb @@ -15,11 +15,16 @@ end # Extract unit data +# CSV.open("#{Dir.home}/tutorials.csv", "wb") do |csv| +# csv << ["unit_id", "user_id", "code", "student_count", "stream"] +# Tutorial.all.each {|t| csv << [t.unit_id,(t.tutor.id unless t.tutor.nil?),t.abbreviation,t.num_students, t.tutorial_stream.id unless t.tutorial_stream.nil?]} +# true +# end + CSV.open("#{Dir.home}/tutorials.csv", "wb") do |csv| - csv << ["unit_id", "user_id", "code", "student_count", "stream"] - Tutorial.all.each {|t| csv << [t.unit_id,(t.tutor.id unless t.tutor.nil?),t.abbreviation,t.num_students, t.tutorial_stream.id unless t.tutorial_stream.nil?]} - true -end + csv << ["unit_id", "user_id", "code", "student_count", "stream", "start_time", "day"] + Tutorial.find_each { |t| csv << [t.unit_id, (t.tutor&.id), t.abbreviation, t.num_students, t.tutorial_stream&.id, t.meeting_time, t.meeting_day] } +end; true # Extract Users roles = {} @@ -35,6 +40,6 @@ # Extract Tasks CSV.open("#{Dir.home}/tasks.csv", "wb") do |csv| csv << ["id","project_id","task_def_id","stream"] - Task.all.each {|t| csv << [t.id,t.project_id,t.task_definition_id,t.tutorial_stream_id]} + Task.all.each {|t| csv << [t.id,t.project_id,t.task_definition_id,t.task_definition.tutorial_stream_id]} true end