@@ -16,11 +16,62 @@ defmodule Cadet.Assessments do
1616 @ xp_early_submission_max_bonus 100
1717 @ xp_bonus_assessment_type ~w( mission sidequest) a
1818 @ submit_answer_roles ~w( student) a
19+ @ change_dates_assessment_role ~w( staff admin) a
20+ @ delete_assessment_role ~w( staff admin) a
21+ @ publish_assessment_role ~w( staff admin) a
1922 @ unsubmit_assessment_role ~w( staff admin) a
2023 @ grading_roles ~w( ) a
2124 @ see_all_submissions_roles ~w( staff admin) a
2225 @ open_all_assessment_roles ~w( staff admin) a
2326
27+ def change_dates_assessment ( _user = % User { role: role } , id , close_at , open_at ) do
28+ if role in @ change_dates_assessment_role do
29+ assessment = Repo . get ( Assessment , id )
30+ previous_open_time = assessment . open_at
31+
32+ cond do
33+ Timex . before? ( close_at , open_at ) ->
34+ { :error , { :bad_request , "New end date should occur after new opening date" } }
35+
36+ Timex . before? ( close_at , Timex . now ( ) ) ->
37+ { :error , { :bad_request , "New end date should occur after current time" } }
38+
39+ Timex . equal? ( previous_open_time , open_at ) or Timex . after? ( previous_open_time , Timex . now ( ) ) ->
40+ update_assessment ( id , % { close_at: close_at , open_at: open_at } )
41+
42+ Timex . before? ( open_at , Timex . now ( ) ) ->
43+ { :error , { :bad_request , "New Opening date should occur after current time" } }
44+
45+ true ->
46+ { :error , { :unauthorized , "Assessment is already opened" } }
47+ end
48+ else
49+ { :error , { :forbidden , "User is not permitted to edit" } }
50+ end
51+ end
52+
53+ def toggle_publish_assessment ( _publisher = % User { role: role } , id , toggle_publish_to ) do
54+ if role in @ publish_assessment_role do
55+ update_assessment ( id , % { is_published: toggle_publish_to } )
56+ else
57+ { :error , { :forbidden , "User is not permitted to publish" } }
58+ end
59+ end
60+
61+ def delete_assessment ( _deleter = % User { role: role } , id ) do
62+ if role in @ delete_assessment_role do
63+ assessment = Repo . get ( Assessment , id )
64+
65+ Submission
66+ |> where ( assessment_id: ^ id )
67+ |> Repo . delete_all ( )
68+
69+ Repo . delete ( assessment )
70+ else
71+ { :error , { :forbidden , "User is not permitted to delete" } }
72+ end
73+ end
74+
2475 @ spec user_total_xp ( % User { } ) :: integer ( )
2576 def user_total_xp ( % User { id: user_id } ) when is_ecto_id ( user_id ) do
2677 total_xp_bonus =
@@ -163,11 +214,19 @@ defmodule Cadet.Assessments do
163214
164215 def assessment_with_questions_and_answers ( id , user = % User { } , password )
165216 when is_ecto_id ( id ) do
217+ role = user . role
218+
166219 assessment =
167- Assessment
168- |> where ( id: ^ id )
169- |> where ( is_published: true )
170- |> Repo . one ( )
220+ if role in @ open_all_assessment_roles do
221+ Assessment
222+ |> where ( id: ^ id )
223+ |> Repo . one ( )
224+ else
225+ Assessment
226+ |> where ( id: ^ id )
227+ |> where ( is_published: true )
228+ |> Repo . one ( )
229+ end
171230
172231 if assessment do
173232 assessment_with_questions_and_answers ( assessment , user , password )
@@ -210,7 +269,7 @@ defmodule Cadet.Assessments do
210269 Returns a list of assessments with all fields and an indicator showing whether it has been attempted
211270 by the supplied user
212271 """
213- def all_published_assessments ( user = % User { } ) do
272+ def all_assessments ( user = % User { } ) do
214273 assessments =
215274 Query . all_assessments_with_max_xp_and_grade ( )
216275 |> subquery ( )
@@ -240,7 +299,7 @@ defmodule Cadet.Assessments do
240299 question_count: q_count . count ,
241300 graded_count: a_count . count
242301 } )
243- |> where ( is_published: true )
302+ |> filter_published_assessments ( user )
244303 |> order_by ( :open_at )
245304 |> Repo . all ( )
246305 |> Enum . map ( fn assessment = % Assessment { } ->
@@ -259,6 +318,15 @@ defmodule Cadet.Assessments do
259318 { :ok , assessments }
260319 end
261320
321+ def filter_published_assessments ( assessments , user ) do
322+ role = user . role
323+
324+ case role do
325+ :student -> where ( assessments , is_published: true )
326+ _ -> assessments
327+ end
328+ end
329+
262330 defp build_grading_status ( submission_status , a_type , q_count , g_count ) do
263331 case a_type do
264332 type when type in [ :mission , :sidequest ] ->
@@ -283,33 +351,101 @@ defmodule Cadet.Assessments do
283351 @ doc """
284352 The main function that inserts or updates assessments from the XML Parser
285353 """
286- @ spec insert_or_update_assessments_and_questions ( map ( ) , [ map ( ) ] ) ::
354+ @ spec insert_or_update_assessments_and_questions ( map ( ) , [ map ( ) ] , boolean ( ) ) ::
287355 { :ok , any ( ) }
288356 | { :error , Ecto.Multi . name ( ) , any ( ) , % { optional ( Ecto.Multi . name ( ) ) => any ( ) } }
289- def insert_or_update_assessments_and_questions ( assessment_params , questions_params ) do
357+ def insert_or_update_assessments_and_questions (
358+ assessment_params ,
359+ questions_params ,
360+ force_update
361+ ) do
290362 assessment_multi =
291363 Multi . insert_or_update (
292364 Multi . new ( ) ,
293365 :assessment ,
294- insert_or_update_assessment_changeset ( assessment_params )
366+ insert_or_update_assessment_changeset ( assessment_params , force_update )
295367 )
296368
297- questions_params
298- |> Enum . with_index ( 1 )
299- |> Enum . reduce ( assessment_multi , fn { question_params , index } , multi ->
300- Multi . run ( multi , String . to_atom ( "question#{ index } " ) , fn _repo ,
301- % { assessment: % Assessment { id: id } } ->
302- question_params
303- |> Map . put ( :display_order , index )
304- |> build_question_changeset_for_assessment_id ( id )
305- |> Repo . insert ( )
369+ if force_update and invalid_force_update ( assessment_multi , questions_params ) do
370+ { :error , "Question count is different" }
371+ else
372+ questions_params
373+ |> Enum . with_index ( 1 )
374+ |> Enum . reduce ( assessment_multi , fn { question_params , index } , multi ->
375+ Multi . run ( multi , String . to_atom ( "question#{ index } " ) , fn _repo ,
376+ % { assessment: % Assessment { id: id } } ->
377+ question_exists =
378+ Repo . exists? (
379+ where ( Question , [ q ] , q . assessment_id == ^ id and q . display_order == ^ index )
380+ )
381+
382+ # the !question_exists check allows for force updating of brand new assessments
383+ if ! force_update or ! question_exists do
384+ question_params
385+ |> Map . put ( :display_order , index )
386+ |> build_question_changeset_for_assessment_id ( id )
387+ |> Repo . insert ( )
388+ else
389+ params =
390+ question_params
391+ |> Map . put_new ( :max_xp , 0 )
392+ |> Map . put ( :display_order , index )
393+
394+ % { id: question_id , type: type } =
395+ Question
396+ |> where ( [ q ] , q . display_order == ^ index and q . assessment_id == ^ id )
397+ |> Repo . one ( )
398+
399+ if question_params . type != Atom . to_string ( type ) do
400+ { :error ,
401+ create_invalid_changeset_with_error (
402+ :question ,
403+ "Question types should remain the same"
404+ ) }
405+ else
406+ changeset =
407+ Question . changeset ( % Question { assessment_id: id , id: question_id } , params )
408+
409+ Repo . update ( changeset )
410+ end
411+ end
412+ end )
306413 end )
307- end )
308- |> Repo . transaction ( )
414+ |> Repo . transaction ( )
415+ end
309416 end
310417
311- @ spec insert_or_update_assessment_changeset ( map ( ) ) :: Ecto.Changeset . t ( )
312- defp insert_or_update_assessment_changeset ( params = % { number: number } ) do
418+ # Function that checks if the force update is invalid. The force update is only invalid
419+ # if the new question count is different from the old question count.
420+ defp invalid_force_update ( assessment_multi , questions_params ) do
421+ assessment_id =
422+ ( assessment_multi . operations
423+ |> List . first ( )
424+ |> elem ( 1 )
425+ |> elem ( 1 ) ) . data . id
426+
427+ if assessment_id do
428+ open_date = Repo . get ( Assessment , assessment_id ) . open_at
429+ # check if assessment is already opened
430+ if Timex . after? ( open_date , Timex . now ( ) ) do
431+ false
432+ else
433+ existing_questions_count =
434+ Question
435+ |> where ( [ q ] , q . assessment_id == ^ assessment_id )
436+ |> Repo . all ( )
437+ |> Enum . count ( )
438+
439+ new_questions_count = Enum . count ( questions_params )
440+ existing_questions_count != new_questions_count
441+ end
442+ else
443+ false
444+ end
445+ end
446+
447+ @ spec insert_or_update_assessment_changeset ( map ( ) , boolean ( ) ) :: Ecto.Changeset . t ( )
448+ defp insert_or_update_assessment_changeset ( params = % { number: number } , force_update ) do
313449 Assessment
314450 |> where ( number: ^ number )
315451 |> Repo . one ( )
@@ -318,18 +454,30 @@ defmodule Cadet.Assessments do
318454 Assessment . changeset ( % Assessment { } , params )
319455
320456 assessment ->
321- if Timex . after? ( assessment . open_at , Timex . now ( ) ) do
322- # Delete all existing questions
323- % { id: assessment_id } = assessment
457+ cond do
458+ Timex . after? ( assessment . open_at , Timex . now ( ) ) ->
459+ # Delete all existing questions
460+ % { id: assessment_id } = assessment
324461
325- Question
326- |> where ( assessment_id: ^ assessment_id )
327- |> Repo . delete_all ( )
462+ Question
463+ |> where ( assessment_id: ^ assessment_id )
464+ |> Repo . delete_all ( )
328465
329- Assessment . changeset ( assessment , params )
330- else
331- # if the assessment is already open, don't mess with it
332- create_invalid_changeset_with_error ( :assessment , "is already open" )
466+ Assessment . changeset ( assessment , params )
467+
468+ force_update ->
469+ # Maintain the same open/close date when force updating an assessment
470+ new_params =
471+ params
472+ |> Map . delete ( :open_at )
473+ |> Map . delete ( :close_at )
474+ |> Map . delete ( :is_published )
475+
476+ Assessment . changeset ( assessment , new_params )
477+
478+ true ->
479+ # if the assessment is already open, don't mess with it
480+ create_invalid_changeset_with_error ( :assessment , "is already open" )
333481 end
334482 end
335483 end
0 commit comments