Skip to content

Commit fb289fd

Browse files
authored
Merge pull request doubtfire-lms#468 from b0ink/feat/task-discussed-state
feat: task discussed comment + staff notes
2 parents 384f555 + 531a7cf commit fb289fd

16 files changed

+484
-26
lines changed

app/api/api_root.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class ApiRoot < Grape::API
7575
mount Similarity::TaskSimilarityApi
7676
mount TeachingPeriodsPublicApi
7777
mount TeachingPeriodsAuthenticatedApi
78+
mount StaffNotesApi
7879

7980
mount Tii::TurnItInApi
8081
mount Tii::TurnItInHooksApi
@@ -122,6 +123,7 @@ class ApiRoot < Grape::API
122123
AuthenticationHelpers.add_auth_to TaskCommentsApi
123124
AuthenticationHelpers.add_auth_to TaskDefinitionsApi
124125
AuthenticationHelpers.add_auth_to TeachingPeriodsAuthenticatedApi
126+
AuthenticationHelpers.add_auth_to StaffNotesApi
125127

126128
AuthenticationHelpers.add_auth_to Tii::TurnItInApi
127129
AuthenticationHelpers.add_auth_to Tii::TiiGroupAttachmentApi
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module Entities
2+
class StaffNoteEntity < Grape::Entity
3+
expose :id
4+
5+
expose :note
6+
expose :user_id
7+
8+
expose :created_at
9+
expose :updated_at
10+
11+
expose :reply_to_id
12+
13+
end
14+
end

app/api/staff_notes_api.rb

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
require 'grape'
2+
3+
class StaffNotesApi < Grape::API
4+
helpers AuthenticationHelpers
5+
helpers AuthorisationHelpers
6+
7+
before do
8+
authenticated?
9+
end
10+
11+
desc "Get all the staff notes for a project"
12+
params do
13+
requires :project_id, type: Integer, desc: 'Project to fetch staff notes for'
14+
end
15+
get '/projects/:project_id/staff_notes' do
16+
project = Project.find(params[:project_id])
17+
18+
unless authorise? current_user, project, :get_staff_note
19+
error!({ error: 'You do not have permission to access this project' }, 403)
20+
end
21+
22+
result = project.staff_notes
23+
24+
present result, with: Entities::StaffNoteEntity, user: current_user
25+
end
26+
27+
desc "Create a new staff note for a project"
28+
params do
29+
requires :project_id, type: Integer, desc: 'Project to add the staff note for'
30+
requires :note, type: String, desc: 'The text to add to the staff note'
31+
optional :reply_to_id, type: Integer, desc: 'ID of the staff note this is being replied to'
32+
end
33+
post '/projects/:project_id/staff_notes' do
34+
project = Project.find(params[:project_id])
35+
36+
unless authorise? current_user, project, :create_staff_note
37+
error!({ error: 'You do not have permission to access this project' }, 403)
38+
end
39+
40+
text_note = params[:note]
41+
42+
reply_to_id = params[:reply_to_id]
43+
if reply_to_id.present?
44+
original_staff_note = StaffNote.find(reply_to_id)
45+
error!(error: 'You do not have permission to read the replied staff note') unless authorise?(current_user, original_staff_note.project, :get)
46+
error!(error: 'Original staff note is not in this project.') if project.staff_notes.find(reply_to_id).blank?
47+
end
48+
49+
result = project.add_staff_note(current_user, text_note, reply_to_id)
50+
51+
if result.nil?
52+
error!({ error: 'Duplicate note.' }, 403)
53+
else
54+
present result, with: Entities::StaffNoteEntity, user: current_user
55+
end
56+
end
57+
58+
desc "Delete a staff note for a project"
59+
delete '/projects/:project_id/staff_notes/:id' do
60+
project = Project.find(params[:project_id])
61+
staff_note = StaffNote.find(params[:id])
62+
63+
unless authorise?(current_user, project, :delete_staff_note) || staff_note.user.id == current_user.id
64+
error!({ error: 'You do not have permission to delete this note.' }, 403)
65+
end
66+
67+
error!({ error: 'Note does not belong to this project' }, 404) if staff_note.project_id != project.id
68+
69+
staff_note.destroy
70+
error!({ error: staff_note.errors.full_messages.last }, 403) unless staff_note.destroyed?
71+
72+
present staff_note.destroyed?, with: Grape::Presenters::Presenter
73+
end
74+
75+
desc "Update a staff note for a project"
76+
params do
77+
requires :id, type: Integer, desc: 'The staff note id to update'
78+
requires :note, type: String, desc: 'The text to update the staff note with'
79+
end
80+
put '/projects/:project_id/staff_notes/:id' do
81+
project = Project.find(params[:project_id])
82+
staff_note = StaffNote.find(params[:id])
83+
84+
unless authorise?(current_user, project, :create_staff_note) && staff_note.user.id == current_user.id
85+
error!({ error: 'You do not have permission to edit this note.' }, 403)
86+
end
87+
88+
error!({ error: 'Note does not belong to this project' }, 404) if staff_note.project_id != project.id
89+
90+
staff_note.update!(note: params[:note])
91+
present staff_note, with: Entities::StaffNoteEntity, user: current_user
92+
end
93+
94+
end

app/api/tasks_api.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ class TasksApi < Grape::API
155155
optional :include_in_portfolio, type: Boolean, desc: 'Indicate if this task should be in the portfolio'
156156
optional :grade, type: Integer, desc: 'Grade value if task is a graded task (required if task definition is a graded task)'
157157
optional :quality_pts, type: Integer, desc: 'Quality points value if task has quality assessment'
158+
optional :discussed, type: Boolean, desc: 'Mark task as discussed'
158159
end
159160
put '/projects/:id/task_def_id/:task_definition_id' do
160161
project = Project.find(params[:id])
@@ -166,6 +167,10 @@ class TasksApi < Grape::API
166167
if authorise? current_user, project, :make_submission
167168
task = project.task_for_task_definition(task_definition)
168169

170+
if !params[:discussed].nil? && authorise?(current_user, project, :assess)
171+
task.add_discussed_comment(current_user)
172+
end
173+
169174
# if trigger supplied...
170175
unless params[:trigger].nil?
171176
# Check if they should be using portfolio_evidence api
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
class TaskDiscussedComment < TaskComment
2+
before_create do
3+
self.content_type = :discussed_in_class
4+
end
5+
6+
after_create do
7+
mark_as_read(self.recipient)
8+
end
9+
10+
def serialize(user)
11+
json = super(user)
12+
json[:recipient_read_time] = nil
13+
json[:date] = self.created_at
14+
json
15+
end
16+
end

app/models/project.rb

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ class Project < ApplicationRecord
2929
has_many :task_engagements, through: :tasks
3030
has_many :comments, through: :tasks
3131

32+
has_many :staff_notes, dependent: :destroy
33+
3234
# Callbacks - methods called are private
3335
before_destroy :can_destroy?
3436

@@ -60,18 +62,24 @@ def self.permissions
6062
:get_submission,
6163
:change,
6264
:assess,
63-
:change_campus
65+
:change_campus,
66+
:get_staff_note,
67+
:create_staff_note
6468
]
69+
6570
# What can admins do with projects?
6671
admin_role_permissions = [
6772
:get,
6873
:get_submission
6974
]
75+
7076
# What can auditors do with projects?
7177
auditor_role_permissions = [
7278
:get,
73-
:get_submission
79+
:get_submission,
80+
:get_staff_note
7481
]
82+
7583
# What can nil users do with projects?
7684
nil_role_permissions = []
7785

@@ -661,6 +669,24 @@ def archive_submissions(out)
661669
FileUtils.rm_f(portfolio_path) if portfolio_available
662670
end
663671

672+
def add_staff_note(user, text, reply_to_id = nil)
673+
text = text.strip
674+
return nil if user.nil? || text.nil? || text.empty?
675+
676+
ln = staff_notes.last
677+
678+
# don't add if duplicate note
679+
return if ln && ln.user == user && ln.note == text
680+
681+
note = StaffNote.create
682+
note.note = text
683+
note.user = user
684+
note.project = self
685+
note.reply_to_id = reply_to_id
686+
note.save!
687+
note
688+
end
689+
664690
private
665691

666692
def can_destroy?

app/models/staff_note.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class StaffNote < ApplicationRecord
2+
belongs_to :project
3+
belongs_to :user
4+
5+
# belongs_to :reply_to, class_name: 'StaffNote', optional: true, inverse_of: :replies
6+
# has_many :replies, class_name: 'StaffNote', dependent: :restrict_with_exception, inverse_of: :reply_to
7+
end

app/models/task.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,16 @@ def add_status_comment(current_user, status)
750750
comment
751751
end
752752

753+
def add_discussed_comment(current_user)
754+
discussed = TaskDiscussedComment.create
755+
discussed.task = self
756+
discussed.user = current_user
757+
discussed.comment = "Discussed in class"
758+
discussed.recipient = current_user == project.student ? project.tutor_for(task_definition) : project.student
759+
discussed.save!
760+
discussed
761+
end
762+
753763
def add_discussion_comment(user, prompts)
754764
# don't allow if group task.
755765
discussion = DiscussionComment.create

app/models/unit.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,7 @@ def student_query(enrolled)
440440
.joins('LEFT OUTER JOIN tutorial_enrolments ON tutorial_enrolments.project_id = projects.id')
441441
.joins('LEFT OUTER JOIN tutorials ON tutorials.id = tutorial_enrolments.tutorial_id')
442442
.joins('LEFT OUTER JOIN tutorial_streams ON tutorials.tutorial_stream_id = tutorial_streams.id')
443+
.joins('LEFT OUTER JOIN staff_notes ON staff_notes.project_id = projects.id')
443444
.group(
444445
'projects.id',
445446
'projects.target_grade',
@@ -481,7 +482,8 @@ def student_query(enrolled)
481482
# Get tutorial for each stream in unit
482483
*tutorial_streams.map { |s| "MAX(CASE WHEN tutorials.tutorial_stream_id = #{s.id} OR tutorials.tutorial_stream_id IS NULL THEN tutorials.id ELSE NULL END) AS tutorial_#{s.id}" },
483484
# Get tutorial for case when no stream
484-
"MAX(CASE WHEN tutorial_streams.id IS NULL THEN tutorials.id ELSE NULL END) AS tutorial"
485+
"MAX(CASE WHEN tutorial_streams.id IS NULL THEN tutorials.id ELSE NULL END) AS tutorial",
486+
'COUNT(DISTINCT staff_notes.id) as staff_note_count'
485487
)
486488
.order('users.first_name')
487489

@@ -515,6 +517,7 @@ def student_query(enrolled)
515517
similarity_flag: t.task_similarities_max_pct > 0,
516518
has_portfolio: !t.portfolio_production_date.nil?,
517519
stats: map_stats.call(t),
520+
staff_note_count: t.staff_note_count,
518521
tutorial_enrolments: tutorial_streams.map do |s|
519522
{
520523
stream_abbr: s.abbreviation,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
class CreateStaffNotes < ActiveRecord::Migration[8.0]
2+
def change
3+
create_table :staff_notes do |t|
4+
t.text :note
5+
t.references :project, index: true, null: false
6+
t.references :user, index: true, null: false
7+
t.references :staff_notes, :reply_to, index: true, null: true
8+
9+
t.timestamps
10+
end
11+
end
12+
end

0 commit comments

Comments
 (0)