Skip to content

Commit a93037a

Browse files
Project components table audit (#418)
closes #415 As per issue #415, but also: - Uses db:prepare which will run only run db:seed if the database doesn't exist. - Makes the classroom_management seeds idempotent, by not proceeding if a school exists. --------- Co-authored-by: create-issue-branch[bot] <53036503+create-issue-branch[bot]@users.noreply.github.com> Co-authored-by: Dan Halson <[email protected]>
1 parent c0fd6b3 commit a93037a

16 files changed

+170
-10
lines changed

Diff for: Gemfile

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ gem 'omniauth-rpi',
2727
github: 'RaspberryPiFoundation/omniauth-rpi',
2828
tag: 'v1.3.1'
2929
gem 'open-uri'
30+
gem 'paper_trail'
3031
gem 'pg', '~> 1.1'
3132
gem 'postmark-rails'
3233
gem 'puma', '~> 6'

Diff for: Gemfile.lock

+6
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,9 @@ GEM
290290
stringio
291291
time
292292
uri
293+
paper_trail (15.1.0)
294+
activerecord (>= 6.1)
295+
request_store (~> 1.4)
293296
parallel (1.22.1)
294297
parser (3.2.1.0)
295298
ast (~> 2.4.1)
@@ -364,6 +367,8 @@ GEM
364367
regexp_parser (2.7.0)
365368
reline (0.5.9)
366369
io-console (~> 0.5)
370+
request_store (1.7.0)
371+
rack (>= 1.4)
367372
rexml (3.3.0)
368373
strscan
369374
roo (2.10.1)
@@ -537,6 +542,7 @@ DEPENDENCIES
537542
omniauth-rails_csrf_protection (~> 1.0.1)
538543
omniauth-rpi!
539544
open-uri
545+
paper_trail
540546
pg (~> 1.1)
541547
postmark-rails
542548
pry-byebug

Diff for: app/controllers/api_controller.rb

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ class ::ParameterError < StandardError; end
1010
rescue_from CanCan::AccessDenied, with: -> { denied }
1111
rescue_from ParameterError, with: -> { unprocessable }
1212

13+
before_action :set_paper_trail_whodunnit
14+
1315
private
1416

1517
def bad_request

Diff for: app/models/component.rb

+8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ class Component < ApplicationRecord
66
validates :extension, presence: true
77
validate :default_component_protected_properties, on: :update
88

9+
has_paper_trail(
10+
if: ->(c) { c.project&.school_id },
11+
meta: {
12+
meta_project_id: ->(c) { c.project&.id },
13+
meta_school_id: ->(c) { c.project&.school_id }
14+
}
15+
)
16+
917
private
1018

1119
def default_component_protected_properties

Diff for: app/models/project.rb

+8
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ class Project < ApplicationRecord
2121

2222
scope :internal_projects, -> { where(user_id: nil) }
2323

24+
has_paper_trail(
25+
if: ->(p) { p&.school_id },
26+
meta: {
27+
meta_remixed_from_id: ->(p) { p&.remixed_from_id },
28+
meta_school_id: ->(p) { p&.school_id }
29+
}
30+
)
31+
2432
def self.users(current_user)
2533
school = School.find_by(id: pluck(:school_id))
2634
SchoolStudent::List.call(school:, token: current_user.token, student_ids: pluck(:user_id))[:school_students] || []

Diff for: bin/docker-debug-entrypoint.sh

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1-
rails db:setup --trace
1+
#!/bin/bash
2+
3+
rails db:prepare
24
rdbg -n -o -c -- bin/rails s -p 3009 -b '0.0.0.0'

Diff for: bin/docker-entrypoint.sh

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1-
rails db:setup --trace
1+
#!/bin/bash
2+
3+
rails db:prepare
24
rails server --port 3009 --binding 0.0.0.0

Diff for: db/migrate/20240828112433_create_versions.rb

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# This migration creates the `versions` table, the only schema PT requires.
2+
# All other migrations PT provides are optional.
3+
class CreateVersions < ActiveRecord::Migration[7.1]
4+
5+
# The largest text column available in all supported RDBMS is
6+
# 1024^3 - 1 bytes, roughly one gibibyte. We specify a size
7+
# so that MySQL will use `longtext` instead of `text`. Otherwise,
8+
# when serializing very large objects, `text` might not be big enough.
9+
TEXT_BYTES = 1_073_741_823
10+
11+
def change
12+
create_table :versions, id: :uuid do |t|
13+
t.string :item_type, null: false
14+
t.string :item_id, null: false
15+
t.string :event, null: false
16+
t.string :whodunnit
17+
# We're not using versioning, so exclude the original state by not having an options field (see docs)
18+
# t.json :object
19+
20+
# Known issue in MySQL: fractional second precision
21+
# -------------------------------------------------
22+
#
23+
# MySQL timestamp columns do not support fractional seconds unless
24+
# defined with "fractional seconds precision". MySQL users should manually
25+
# add fractional seconds precision to this migration, specifically, to
26+
# the `created_at` column.
27+
# (https://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html)
28+
#
29+
# MySQL users should also upgrade to at least rails 4.2, which is the first
30+
# version of ActiveRecord with support for fractional seconds in MySQL.
31+
# (https://github.com/rails/rails/pull/14359)
32+
#
33+
# MySQL users should use the following line for `created_at`
34+
# t.datetime :created_at, limit: 6
35+
t.datetime :created_at
36+
end
37+
add_index :versions, %i[item_type item_id]
38+
end
39+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# This migration adds the optional `object_changes` column, in which PaperTrail
2+
# will store the `changes` diff for each update event. See the readme for
3+
# details.
4+
class AddObjectChangesToVersions < ActiveRecord::Migration[7.1]
5+
# The largest text column available in all supported RDBMS.
6+
# See `create_versions.rb` for details.
7+
TEXT_BYTES = 1_073_741_823
8+
9+
def change
10+
add_column :versions, :object_changes, :json
11+
end
12+
end
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# This migration adds the optional `object_changes` column, in which PaperTrail
2+
# will store the `changes` diff for each update event. See the readme for
3+
# details.
4+
class AddMetaColumnsToVersions < ActiveRecord::Migration[7.1]
5+
def change
6+
add_column :versions, :meta_project_id, :uuid
7+
add_column :versions, :meta_school_id, :uuid
8+
add_column :versions, :meta_remixed_from_id, :uuid
9+
end
10+
end

Diff for: db/schema.rb

+14-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: lib/tasks/classroom_management.rake

+15
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ namespace :classroom_management do
5050

5151
desc 'Create an unverified school'
5252
task seed_an_unverified_school: :environment do
53+
if School.find_by(code: TEST_SCHOOL)
54+
puts "Test school (#{TEST_SCHOOL}) already exists, run the destroy_seed_data task to start over)."
55+
return
56+
end
57+
5358
ActiveRecord::Base.transaction do
5459
Rails.logger.info 'Attempting to seed data...'
5560
creator_id = ENV.fetch('SEEDING_CREATOR_ID', TEST_USERS[:jane_doe])
@@ -64,6 +69,11 @@ namespace :classroom_management do
6469

6570
desc 'Create a verified school'
6671
task seed_a_verified_school: :environment do
72+
if School.find_by(code: TEST_SCHOOL)
73+
puts "Test school (#{TEST_SCHOOL}) already exists, run the destroy_seed_data task to start over)."
74+
return
75+
end
76+
6777
ActiveRecord::Base.transaction do
6878
Rails.logger.info 'Attempting to seed data...'
6979
creator_id = ENV.fetch('SEEDING_CREATOR_ID', TEST_USERS[:jane_doe])
@@ -79,6 +89,11 @@ namespace :classroom_management do
7989

8090
desc 'Create a school with lessons and students'
8191
task seed_a_school_with_lessons_and_students: :environment do
92+
if School.find_by(code: TEST_SCHOOL)
93+
puts "Test school (#{TEST_SCHOOL}) already exists, run the destroy_seed_data task to start over)."
94+
return
95+
end
96+
8297
ActiveRecord::Base.transaction do
8398
Rails.logger.info 'Attempting to seed data...'
8499
creator_id = ENV.fetch('SEEDING_CREATOR_ID', TEST_USERS[:jane_doe])

Diff for: lib/tasks/classroom_management_helper.rb

+11-5
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,26 @@ def create_school(creator_id, school_id = nil)
2323
school.creator_id = creator_id
2424
school.creator_agree_authority = true
2525
school.creator_agree_terms_and_conditions = true
26-
school.id = school_id if school_id
2726
end
2827
end
2928

3029
def verify_school(school)
30+
if school.verified?
31+
Rails.logger.info "School #{school.code} is already verified."
32+
return
33+
end
34+
3135
Rails.logger.info 'Verifying the school...'
32-
school.verify!
36+
37+
School.transaction do
38+
school.verify!
39+
Role.owner.create!(user_id: school.creator_id, school:)
40+
Role.teacher.create!(user_id: school.creator_id, school:)
41+
end
3342

3443
# rubocop:disable Rails/SkipsModelValidations
3544
school.update_column(:code, SCHOOL_CODE) # The code needs to match the one in the profile
3645
# rubocop:enable Rails/SkipsModelValidations
37-
38-
Role.owner.create!(user_id: school.creator_id, school:)
39-
Role.teacher.create!(user_id: school.creator_id, school:)
4046
end
4147

4248
def create_school_class(teacher_id, school)

Diff for: spec/models/component_spec.rb

+19-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
require 'rails_helper'
44

5-
RSpec.describe Component do
5+
RSpec.describe Component, versioning: true do
66
subject { build(:component) }
77

88
it { is_expected.to belong_to(:project) }
@@ -37,5 +37,23 @@
3737
.to include(I18n.t('errors.project.editing.change_default_extension'))
3838
end
3939
end
40+
41+
describe 'auditing' do
42+
let(:school) { create(:school) }
43+
let(:teacher) { create(:teacher, school:) }
44+
let(:student) { create(:student, school:) }
45+
46+
it 'enables auditing for a component that belongs to a project with a school_id' do
47+
project_with_school = create(:project, user_id: student.id, school_id: school.id)
48+
component = create(:component, project: project_with_school)
49+
expect(component.versions.length).to(eq(1))
50+
end
51+
52+
it 'does not enable auditing for a component that belongs to a project without a school_id' do
53+
project_without_school = create(:project, school_id: nil)
54+
component = create(:component, project: project_without_school)
55+
expect(component.versions.length).to(eq(0))
56+
end
57+
end
4058
end
4159
end

Diff for: spec/models/project_spec.rb

+17-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
require 'rails_helper'
44

5-
RSpec.describe Project do
5+
RSpec.describe Project, versioning: true do
66
let(:school) { create(:school) }
77

88
describe 'associations' do
@@ -240,4 +240,20 @@
240240
expect(project.last_edited_at).to eq(latest_component.updated_at)
241241
end
242242
end
243+
244+
describe 'auditing' do
245+
let(:school) { create(:school) }
246+
let(:teacher) { create(:teacher, school:) }
247+
let(:student) { create(:student, school:) }
248+
249+
it 'enables auditing for projects with a school_id' do
250+
project_with_school = create(:project, user_id: student.id, school_id: school.id)
251+
expect(project_with_school.versions.length).to(eq(1))
252+
end
253+
254+
it 'does not enable auditing for projects without a school_id' do
255+
project_without_school = create(:project, school_id: nil)
256+
expect(project_without_school.versions.length).to(eq(0))
257+
end
258+
end
243259
end

Diff for: spec/rails_helper.rb

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
2121

22+
require 'paper_trail/frameworks/rspec'
23+
2224
# Requires supporting ruby files with custom matchers and macros, etc, in
2325
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
2426
# run as spec files by default. This means that files in spec/support that end

0 commit comments

Comments
 (0)