-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathproject.rb
More file actions
147 lines (112 loc) · 4.81 KB
/
project.rb
File metadata and controls
147 lines (112 loc) · 4.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# frozen_string_literal: true
class Project < ApplicationRecord
module Types
PYTHON = 'python'
HTML = 'html'
SCRATCH = 'scratch'
end
attr_accessor :skip_identifier_generation
belongs_to :school, optional: true
belongs_to :lesson, optional: true
belongs_to :parent, optional: true, class_name: :Project, foreign_key: :remixed_from_id, inverse_of: :remixes
has_many :remixes, dependent: :nullify, class_name: :Project, foreign_key: :remixed_from_id, inverse_of: :parent
has_many :components, -> { order(default: :desc, name: :asc) }, dependent: :destroy, inverse_of: :project
has_many :project_errors, dependent: :nullify
has_many_attached :images
has_many_attached :videos
has_many_attached :audio
has_one :school_project, dependent: :destroy
accepts_nested_attributes_for :components
before_validation :generate_identifier, on: :create, unless: :skip_identifier_generation
before_validation :create_school_project_if_needed
validates :identifier, presence: true, uniqueness: { scope: :locale }
validate :identifier_cannot_be_taken_by_another_user
validates :locale, presence: true, unless: :user_id
validate :user_has_a_role_within_the_school
validate :user_is_class_teacher_or_student
validate :project_with_instructions_must_belong_to_school
validate :project_with_school_id_has_school_project
validate :school_project_school_matches_project_school
default_scope -> { where.not(project_type: Types::SCRATCH) }
scope :internal_projects, -> { where(user_id: nil) }
scope :only_scratch, lambda { |only_scratch|
only_scratch ? unscoped.where(project_type: Project::Types::SCRATCH) : self
}
has_paper_trail(
if: ->(p) { p&.school_id },
meta: {
meta_remixed_from_id: ->(p) { p&.remixed_from_id },
meta_school_id: ->(p) { p&.school_id }
}
)
def self.users(current_user)
school = School.find_by(id: pluck(:school_id))
SchoolStudent::List.call(school:, token: current_user.token, student_ids: pluck(:user_id).uniq)[:school_students] || []
end
def self.with_users(current_user)
by_id = users(current_user).index_by(&:id)
all.map { |instance| [instance, by_id[instance.user_id]] }
end
def with_user(current_user)
school = School.find_by(id: school_id)
students = SchoolStudent::List.call(school:, token: current_user.token,
student_ids: [user_id])[:school_students] || []
[self, students.first]
end
# Work around a CanCanCan issue with accepts_nested_attributes_for.
# https://github.com/CanCanCommunity/cancancan/issues/774
def components=(array)
super(array.map { |o| o.is_a?(Hash) ? Component.new(o) : o })
end
def last_edited_at
# datetime that the project or one of its components was last updated
[updated_at, components.maximum(:updated_at)].compact.max
end
def media
images + videos + audio
end
private
def generate_identifier
self.identifier ||= PhraseIdentifier.generate
end
def create_school_project_if_needed
return unless school.present? && school_project.nil?
self.school_project = SchoolProject.new(school:)
end
def identifier_cannot_be_taken_by_another_user
return if Project.where(identifier: self.identifier).where.not(user_id:).empty?
errors.add(:identifier, "can't be taken by another user")
end
def user_has_a_role_within_the_school
return unless user_id_changed? && errors.blank? && school
user = User.new(id: user_id)
return if user.school_roles(school).any?
msg = "'#{user_id}' does not have any roles for for organisation '#{school_id}'"
errors.add(:user, msg)
end
def user_is_class_teacher_or_student
# TODO: Revisit the case where the lesson is not associated to a class i.e. when we build a lesson library
no_lesson = !lesson
no_school_class = lesson && !lesson.school_class
return if no_lesson || no_school_class || user_is_class_student || user_is_class_teacher
errors.add(:user, "'#{user_id}' is not a class member or the owner of the lesson '#{lesson_id}'")
end
def user_is_class_student
lesson&.school_class&.students&.exists?(student_id: user_id)
end
def user_is_class_teacher
lesson&.school_class&.teachers&.exists?(teacher_id: user_id)
end
def project_with_instructions_must_belong_to_school
return unless instructions && !school_id
errors.add(:instructions, 'Projects with instructions must belong to a school')
end
def project_with_school_id_has_school_project
return unless school_id && !school_project
errors.add(:school_project, 'Project with school_id must have a school_project')
end
def school_project_school_matches_project_school
return unless school_id && school_project && school_id != school_project.school_id
errors.add(:school_project, 'School project school_id must match project school_id')
end
end