diff --git a/app/controllers/api/lessons_controller.rb b/app/controllers/api/lessons_controller.rb index 305b2a63..5936cfda 100644 --- a/app/controllers/api/lessons_controller.rb +++ b/app/controllers/api/lessons_controller.rb @@ -3,8 +3,8 @@ module Api class LessonsController < ApiController before_action :authorize_user, except: %i[index show] - before_action :verify_school_class_belongs_to_school, only: :create - load_and_authorize_resource :lesson + before_action :verify_school_class_belongs_to_school, only: %i[create remix] + load_and_authorize_resource :lesson, except: :remix def index archive_scope = params[:include_archived] == 'true' ? Lesson : Lesson.unarchived @@ -41,6 +41,26 @@ def create_copy end end + def remix + remix_origin = request.origin || request.referer + project = Project.find_by(identifier: lesson_params[:project_identifier]) + authorize! :show, project + + lesson = Lesson.new(lesson_params.except(:project_identifier)) + lesson.project = Project.new(remixed_from_id: project.id) if project + authorize! :remix, lesson + + result = Lesson::CreateRemix.call(lesson_params:, remix_origin:) + + if result.success? + @lesson = result[:lesson] + @lesson_with_user = @lesson.with_user + render :show, formats: [:json], status: :created + else + render json: { error: result[:error] }, status: :unprocessable_entity + end + end + def update # TODO: Consider removing user_id from the lesson_params for update so users can update other users' lessons without changing ownership # OR consider dropping user_id on lessons and using teacher id/ids on the class instead @@ -78,6 +98,12 @@ def lesson_params base_params.merge(user_id: current_user.id) end + def remix_lesson_params + lesson_params.merge(params.fetch(:lesson, {}).permit( + { project_attributes: [:identifier] } + )) + end + def base_params params.fetch(:lesson, {}).permit( :school_id, @@ -86,6 +112,7 @@ def base_params :description, :visibility, :due_date, + :project_identifier, { project_attributes: [ :name, diff --git a/app/models/ability.rb b/app/models/ability.rb index a7b65202..f5de1b45 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -83,13 +83,16 @@ def define_school_teacher_abilities(user:, school:) can(%i[create update destroy], Lesson) do |lesson| school_teacher_can_manage_lesson?(user:, school:, lesson:) end + can(%i[remix], Lesson) do |lesson| + school_teacher_can_remix_lesson?(user:, school:, lesson:) + end can(%i[read create_copy], Lesson, school_id: school.id, visibility: %w[teachers students]) can(%i[create], Project) do |project| school_teacher_can_manage_project?(user:, school:, project:) end can(%i[read update show_context], Project, school_id: school.id, lesson: { visibility: %w[teachers students] }) can(%i[read], Project, - remixed_from_id: Project.where(school_id: school.id, remixed_from_id: nil, lesson_id: Lesson.where(school_class_id: ClassTeacher.where(teacher_id: user.id).select(:school_class_id))).pluck(:id)) + remixed_from_id: Project.where(school_id: school.id, lesson_id: Lesson.where(school_class_id: ClassTeacher.where(teacher_id: user.id).select(:school_class_id))).pluck(:id)) end def define_school_student_abilities(user:, school:) @@ -121,4 +124,9 @@ def school_teacher_can_manage_project?(user:, school:, project:) is_my_project && (is_my_lesson || !project.lesson) end + + def school_teacher_can_remix_lesson?(user:, school:, lesson:) + original_project_is_public = Project.find(lesson.project.remixed_from_id).user_id.nil? + school_teacher_can_manage_lesson?(user:, school:, lesson:) && original_project_is_public + end end diff --git a/app/models/lesson.rb b/app/models/lesson.rb index ae0f0dfd..55926fe4 100644 --- a/app/models/lesson.rb +++ b/app/models/lesson.rb @@ -17,6 +17,7 @@ class Lesson < ApplicationRecord validate :user_has_the_school_owner_or_school_teacher_role_for_the_school validate :user_is_the_school_teacher_for_the_school_class + validate :remixed_lesson_projects_come_from_public_projects scope :archived, -> { where.not(archived_at: nil) } scope :unarchived, -> { where(archived_at: nil) } @@ -74,4 +75,13 @@ def user_is_the_school_teacher_for_the_school_class errors.add(:user, "'#{user_id}' is not the 'school-teacher' for school_class '#{school_class.id}'") end + + def remixed_lesson_projects_come_from_public_projects + return if !project || !project.remixed_from_id + + original_project = Project.find_by(id: project.remixed_from_id) + return if original_project&.user_id.nil? + + errors.add(:project, "remixed project '#{original_project.id}' is not public") + end end diff --git a/config/routes.rb b/config/routes.rb index f48edf7f..b57c0593 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -61,6 +61,7 @@ resources :lessons, only: %i[index create show update destroy] do post :copy, on: :member, to: 'lessons#create_copy' + post :remix, on: :collection, to: 'lessons#remix' end resources :teacher_invitations, param: :token, only: :show do diff --git a/lib/concepts/lesson/operations/create_remix.rb b/lib/concepts/lesson/operations/create_remix.rb new file mode 100644 index 00000000..21c81998 --- /dev/null +++ b/lib/concepts/lesson/operations/create_remix.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +class Lesson + class CreateRemix + class << self + def call(lesson_params:, remix_origin:) + ActiveRecord::Base.transaction do + response = OperationResponse.new + response[:lesson] = build_remix(lesson_params, remix_origin) + response[:lesson].save! + response + rescue StandardError => e + Sentry.capture_exception(e) + errors = response[:lesson].errors.full_messages.join(',') + response[:error] = "Error creating remix of lesson: #{errors}" + response + end + end + + private + + def build_remix(lesson_params, remix_origin) + original_project = Project.find_by(identifier: lesson_params[:project_identifier]) + lesson_copy = Lesson.new(name: original_project.name) + filtered_params = lesson_params.except(:project_identifier) + lesson_copy.assign_attributes(filtered_params) + lesson_copy.project = build_project_remix(original_project, lesson_params, remix_origin) + + lesson_copy + end + + def build_project_remix(original_project, lesson_params, remix_origin) + response = Project::CreateRemix.call( + params: { school_id: lesson_params[:school_id] }, + user_id: lesson_params[:user_id], + original_project:, + remix_origin: + ) + response[:project] + end + end + end +end diff --git a/lib/concepts/project/operations/create_remix.rb b/lib/concepts/project/operations/create_remix.rb index ef42defc..bc70315c 100644 --- a/lib/concepts/project/operations/create_remix.rb +++ b/lib/concepts/project/operations/create_remix.rb @@ -11,14 +11,14 @@ def call(params:, user_id:, original_project:, remix_origin:) response rescue StandardError => e Sentry.capture_exception(e) - response[:error] = I18n.t('errors.project.remixing.cannot_save') + response[:error] = "#{I18n.t('errors.project.remixing.cannot_save')}: #{e.message}" response end private def validate_params(response, params, user_id, original_project, remix_origin) - valid = params[:identifier].present? && user_id.present? && original_project.present? && remix_origin.present? + valid = (params[:identifier].present? || original_project.identifier.present?) && user_id.present? && original_project.present? && remix_origin.present? response[:error] = I18n.t('errors.project.remixing.invalid_params') unless valid end @@ -30,7 +30,6 @@ def remix_project(response, params, user_id, original_project, remix_origin) def create_remix(original_project, params, user_id, remix_origin) remix = format_project(original_project, params, user_id, remix_origin) - original_project.images.each do |image| remix.images.attach(image.blob) end @@ -43,7 +42,8 @@ def create_remix(original_project, params, user_id, remix_origin) remix.audio.attach(audio_file.blob) end - params[:components].each do |x| + (params[:components] || original_project.components).each do |x| + pp 'using original project components' if params[:components].nil? remix.components.build(x.slice(:name, :extension, :content)) end @@ -54,11 +54,12 @@ def format_project(original_project, params, user_id, remix_origin) original_project.dup.tap do |proj| proj.identifier = PhraseIdentifier.generate proj.locale = nil - proj.name = params[:name] + proj.name = params[:name] || original_project.name proj.user_id = user_id proj.remixed_from_id = original_project.id proj.remix_origin = remix_origin proj.lesson_id = nil # Only the original can have a lesson id + proj.school_id = params[:school_id] || original_project.school_id end end end diff --git a/spec/concepts/project/create_remix_spec.rb b/spec/concepts/project/create_remix_spec.rb index 035cf65b..9a1e5d6d 100644 --- a/spec/concepts/project/create_remix_spec.rb +++ b/spec/concepts/project/create_remix_spec.rb @@ -180,6 +180,37 @@ expect(create_remix[:error]).to eq(I18n.t('errors.project.remixing.cannot_save')) end end + + context 'when the remix params contain only a school' do + let(:school) { create(:school) } + let(:teacher) { create(:teacher, school:) } + let!(:remix_params) { { school_id: school.id } } + subject!(:create_remix) { described_class.call(params: remix_params, user_id: teacher.id, original_project: original_project, remix_origin: remix_origin) } + + it 'sets the name to the original project name' do + remixed_project = create_remix[:project] + expect(remixed_project.name).to eq(original_project.name) + end + + it 'sets the school_id to the one provided by the remix params' do + remixed_project = create_remix[:project] + expect(remixed_project.school_id).to eq(school.id) + end + + # fit 'creates components from the original project' do + # pp original_project.components.count + # pp remix_params + # # expect { create_remix }.to change(Component, :count).by(original_project.components.count) + # expect { create_remix }.to change(Project, :count).by(1) + # end + + it 'copies the components from the original project' do + remixed_project = create_remix[:project] + expect(remixed_project.components.map { |c| component_props(c) }).to match_array( + original_project.components.map { |c| component_props(c) } + ) + end + end end def component_props(component) diff --git a/spec/models/lesson_spec.rb b/spec/models/lesson_spec.rb index 4eba9d72..9545f3c7 100644 --- a/spec/models/lesson_spec.rb +++ b/spec/models/lesson_spec.rb @@ -110,6 +110,20 @@ lesson.visibility = 'invalid' expect(lesson).to be_invalid end + + it 'requires original project to be public if project is a remix' do + original_project = create(:project, user_id: SecureRandom.uuid) + lesson.project.remixed_from_id = original_project.id + + expect(lesson).to be_invalid + end + + it 'is valid with a remixed project from a public project' do + original_project = create(:project, user_id: nil) + lesson.project.remixed_from_id = original_project.id + + expect(lesson).to be_valid + end end describe '.archived' do