diff --git a/Gemfile b/Gemfile index 6ab930c76..607ed5a2b 100644 --- a/Gemfile +++ b/Gemfile @@ -11,9 +11,10 @@ ruby_versions = { ruby ruby_versions[(ENV['RAILS_ENV'] || 'development').to_sym] # The venerable, almighty Rails -gem 'rails', '~>6.1.0' +gem 'rails', '~>7.0.0' group :development, :test do + gem "sprockets-rails" gem 'better_errors' gem 'byebug' gem 'database_cleaner' @@ -30,9 +31,8 @@ group :development, :test, :staging do gem 'factory_bot' gem 'factory_bot_rails' gem 'faker' - gem 'minitest', '~>5.14' + gem 'minitest' gem 'minitest-around' - gem 'minitest-rails' gem 'webmock' end @@ -48,7 +48,7 @@ gem 'bootsnap', '>= 1.4.4', require: false gem 'hirb' # Authentication -gem 'devise', '~> 4.7.1' +gem 'devise' gem 'devise_ldap_authenticatable' gem 'json-jwt', '1.7.0' gem 'ruby-saml', '~> 1.13.0' @@ -66,15 +66,12 @@ gem 'moss_ruby', '>= 1.1.2' gem 'rails-latex', '>2.3' # API -gem 'active_model_serializers', '~> 0.10.0' gem 'grape' -gem 'grape-active_model_serializers', '~> 1.3.2' gem 'grape-entity' gem 'grape-swagger' gem 'grape-swagger-rails' # Miscellaneous -gem 'attr_encrypted', '~> 3.1.0' gem 'ci_reporter' gem 'dotenv-rails' gem 'rack-cors', require: 'rack/cors' diff --git a/Gemfile.lock b/Gemfile.lock index 001c86638..89dd0fbce 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,76 +1,75 @@ GEM remote: https://rubygems.org/ specs: - actioncable (6.1.4.4) - actionpack (= 6.1.4.4) - activesupport (= 6.1.4.4) + actioncable (7.0.1) + actionpack (= 7.0.1) + activesupport (= 7.0.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.4.4) - actionpack (= 6.1.4.4) - activejob (= 6.1.4.4) - activerecord (= 6.1.4.4) - activestorage (= 6.1.4.4) - activesupport (= 6.1.4.4) + actionmailbox (7.0.1) + actionpack (= 7.0.1) + activejob (= 7.0.1) + activerecord (= 7.0.1) + activestorage (= 7.0.1) + activesupport (= 7.0.1) mail (>= 2.7.1) - actionmailer (6.1.4.4) - actionpack (= 6.1.4.4) - actionview (= 6.1.4.4) - activejob (= 6.1.4.4) - activesupport (= 6.1.4.4) + net-imap + net-pop + net-smtp + actionmailer (7.0.1) + actionpack (= 7.0.1) + actionview (= 7.0.1) + activejob (= 7.0.1) + activesupport (= 7.0.1) mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp rails-dom-testing (~> 2.0) - actionpack (6.1.4.4) - actionview (= 6.1.4.4) - activesupport (= 6.1.4.4) - rack (~> 2.0, >= 2.0.9) + actionpack (7.0.1) + actionview (= 7.0.1) + activesupport (= 7.0.1) + rack (~> 2.0, >= 2.2.0) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.4.4) - actionpack (= 6.1.4.4) - activerecord (= 6.1.4.4) - activestorage (= 6.1.4.4) - activesupport (= 6.1.4.4) + actiontext (7.0.1) + actionpack (= 7.0.1) + activerecord (= 7.0.1) + activestorage (= 7.0.1) + activesupport (= 7.0.1) + globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (6.1.4.4) - activesupport (= 6.1.4.4) + actionview (7.0.1) + activesupport (= 7.0.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - active_model_serializers (0.10.12) - actionpack (>= 4.1, < 6.2) - activemodel (>= 4.1, < 6.2) - case_transform (>= 0.2) - jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (6.1.4.4) - activesupport (= 6.1.4.4) + activejob (7.0.1) + activesupport (= 7.0.1) globalid (>= 0.3.6) - activemodel (6.1.4.4) - activesupport (= 6.1.4.4) - activerecord (6.1.4.4) - activemodel (= 6.1.4.4) - activesupport (= 6.1.4.4) - activestorage (6.1.4.4) - actionpack (= 6.1.4.4) - activejob (= 6.1.4.4) - activerecord (= 6.1.4.4) - activesupport (= 6.1.4.4) - marcel (~> 1.0.0) + activemodel (7.0.1) + activesupport (= 7.0.1) + activerecord (7.0.1) + activemodel (= 7.0.1) + activesupport (= 7.0.1) + activestorage (7.0.1) + actionpack (= 7.0.1) + activejob (= 7.0.1) + activerecord (= 7.0.1) + activesupport (= 7.0.1) + marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.4.4) + activesupport (7.0.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - zeitwerk (~> 2.3) addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) amq-protocol (2.3.2) ast (2.4.2) - attr_encrypted (3.1.0) - encryptor (~> 3.0.0) bcrypt (3.1.16) better_errors (2.9.1) coderay (>= 1.0.0) @@ -86,8 +85,6 @@ GEM bunny-pub-sub (0.5.2) bunny (~> 2.14) byebug (11.1.3) - case_transform (0.2) - activesupport ci_reporter (2.0.0) builder (>= 2.1.2) code_analyzer (0.5.2) @@ -103,7 +100,7 @@ GEM activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - devise (4.7.3) + devise (4.8.1) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) @@ -138,7 +135,6 @@ GEM dry-core (~> 0.5, >= 0.5) dry-inflector (~> 0.1, >= 0.1.2) dry-logic (~> 1.0, >= 1.0.2) - encryptor (3.0.0) erubi (1.10.0) erubis (2.7.0) factory_bot (6.2.0) @@ -158,9 +154,6 @@ GEM mustermann-grape (~> 1.0.0) rack (>= 1.3.0) rack-accept - grape-active_model_serializers (1.3.2) - active_model_serializers (>= 0.9.0) - grape grape-entity (0.10.1) activesupport (>= 3.0.0) multi_json (>= 1.3.2) @@ -186,7 +179,6 @@ GEM multi_json (>= 1.3) securecompare url_safe_base64 - jsonapi-renderer (0.2.2) listen (3.7.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) @@ -204,9 +196,6 @@ GEM minitest (5.15.0) minitest-around (0.5.0) minitest (~> 5.0) - minitest-rails (6.1.0) - minitest (~> 5.10) - railties (~> 6.1.0) moss_ruby (1.1.3) msgpack (1.4.2) multi_json (1.15.0) @@ -215,7 +204,15 @@ GEM mustermann-grape (1.0.1) mustermann (>= 1.0.0) mysql2 (0.5.3) + net-imap (0.2.3) + digest + net-protocol + strscan net-ldap (0.17.0) + net-pop (0.1.1) + digest + net-protocol + timeout net-protocol (0.1.2) io-wait timeout @@ -242,21 +239,20 @@ GEM rack (>= 2.0.0) rack-test (1.1.0) rack (>= 1.0, < 3) - rails (6.1.4.4) - actioncable (= 6.1.4.4) - actionmailbox (= 6.1.4.4) - actionmailer (= 6.1.4.4) - actionpack (= 6.1.4.4) - actiontext (= 6.1.4.4) - actionview (= 6.1.4.4) - activejob (= 6.1.4.4) - activemodel (= 6.1.4.4) - activerecord (= 6.1.4.4) - activestorage (= 6.1.4.4) - activesupport (= 6.1.4.4) + rails (7.0.1) + actioncable (= 7.0.1) + actionmailbox (= 7.0.1) + actionmailer (= 7.0.1) + actionpack (= 7.0.1) + actiontext (= 7.0.1) + actionview (= 7.0.1) + activejob (= 7.0.1) + activemodel (= 7.0.1) + activerecord (= 7.0.1) + activestorage (= 7.0.1) + activesupport (= 7.0.1) bundler (>= 1.15.0) - railties (= 6.1.4.4) - sprockets-rails (>= 2.0.0) + railties (= 7.0.1) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) @@ -272,12 +268,13 @@ GEM json require_all (~> 3.0) ruby-progressbar - railties (6.1.4.4) - actionpack (= 6.1.4.4) - activesupport (= 6.1.4.4) + railties (7.0.1) + actionpack (= 7.0.1) + activesupport (= 7.0.1) method_source - rake (>= 0.13) + rake (>= 12.2) thor (~> 1.0) + zeitwerk (~> 2.5) rainbow (3.0.0) rake (13.0.6) rb-fsevent (0.11.0) @@ -317,11 +314,11 @@ GEM rubocop-faker (1.1.0) faker (>= 2.12.0) rubocop (>= 0.82.0) - rubocop-rails (2.13.0) + rubocop-rails (2.13.1) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.7.0, < 2.0) - ruby-filemagic (0.7.2) + ruby-filemagic (0.7.3) ruby-ole (1.2.12.2) ruby-progressbar (1.11.0) ruby-saml (1.13.0) @@ -350,6 +347,7 @@ GEM actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) + strscan (3.0.1) thor (1.2.1) timeout (0.2.0) tzinfo (2.0.4) @@ -374,8 +372,6 @@ PLATFORMS x86_64-linux DEPENDENCIES - active_model_serializers (~> 0.10.0) - attr_encrypted (~> 3.1.0) better_errors bootsnap (>= 1.4.4) bunny-pub-sub (= 0.5.2) @@ -383,14 +379,13 @@ DEPENDENCIES ci_reporter coderay database_cleaner - devise (~> 4.7.1) + devise devise_ldap_authenticatable dotenv-rails factory_bot factory_bot_rails faker grape - grape-active_model_serializers (~> 1.3.2) grape-entity grape-swagger grape-swagger-rails @@ -398,15 +393,14 @@ DEPENDENCIES icalendar (~> 2.5, >= 2.5.3) json-jwt (= 1.7.0) listen - minitest (~> 5.14) + minitest minitest-around - minitest-rails moss_ruby (>= 1.1.2) mysql2 (~> 0.5.0) net-smtp puma (~> 5.5) rack-cors - rails (~> 6.1.0) + rails (~> 7.0.0) rails-latex (> 2.3) rails_best_practices require_all (>= 1.3.3) @@ -421,6 +415,7 @@ DEPENDENCIES ruby-saml (~> 1.13.0) rubyzip simplecov + sprockets-rails webmock RUBY VERSION diff --git a/app/api/activity_types_authenticated_api.rb b/app/api/activity_types_authenticated_api.rb index 3c483c9f9..bb8b47f0c 100644 --- a/app/api/activity_types_authenticated_api.rb +++ b/app/api/activity_types_authenticated_api.rb @@ -1,70 +1,68 @@ require 'grape' -module Api - class ActivityTypesAuthenticatedApi < Grape::API - helpers AuthenticationHelpers - helpers AuthorisationHelpers +class ActivityTypesAuthenticatedApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers - before do - authenticated? - end + before do + authenticated? + end - desc 'Add an activity type' - params do - requires :activity_type, type: Hash do - requires :name, type: String, desc: 'The name of the activity type' - requires :abbreviation, type: String, desc: 'The abbreviation for the activity type' - end + desc 'Add an activity type' + params do + requires :activity_type, type: Hash do + requires :name, type: String, desc: 'The name of the activity type' + requires :abbreviation, type: String, desc: 'The abbreviation for the activity type' end - post '/activity_types' do - unless authorise? current_user, User, :handle_activity_types - error!({ error: 'Not authorised to create an activity type' }, 403) - end - activity_type_parameters = ActionController::Parameters.new(params) - .require(:activity_type) - .permit(:name, - :abbreviation) + end + post '/activity_types' do + unless authorise? current_user, User, :handle_activity_types + error!({ error: 'Not authorised to create an activity type' }, 403) + end + activity_type_parameters = ActionController::Parameters.new(params) + .require(:activity_type) + .permit(:name, + :abbreviation) - result = ActivityType.create!(activity_type_parameters) + result = ActivityType.create!(activity_type_parameters) - if result.nil? - error!({ error: 'No activity type added' }, 403) - else - present result, with: Api::Entities::ActivityTypeEntity - end + if result.nil? + error!({ error: 'No activity type added' }, 403) + else + present result, with: Entities::ActivityTypeEntity end + end - desc 'Update an activity type' - params do - requires :activity_type, type: Hash do - optional :name, type: String, desc: 'The name of the activity type' - optional :abbreviation, type: String, desc: 'The abbreviation for the activity type' - end + desc 'Update an activity type' + params do + requires :activity_type, type: Hash do + optional :name, type: String, desc: 'The name of the activity type' + optional :abbreviation, type: String, desc: 'The abbreviation for the activity type' end - put '/activity_types/:id' do - activity_type = ActivityType.find(params[:id]) - unless authorise? current_user, User, :handle_activity_types - error!({ error: 'Not authorised to update an activity type' }, 403) - end - activity_type_parameters = ActionController::Parameters.new(params) - .require(:activity_type) - .permit(:name, - :abbreviation) - - activity_type.update!(activity_type_parameters) - present activity_type, with: Api::Entities::ActivityTypeEntity + end + put '/activity_types/:id' do + activity_type = ActivityType.find(params[:id]) + unless authorise? current_user, User, :handle_activity_types + error!({ error: 'Not authorised to update an activity type' }, 403) end + activity_type_parameters = ActionController::Parameters.new(params) + .require(:activity_type) + .permit(:name, + :abbreviation) - desc 'Delete an activity type' - delete '/activity_types/:id' do - unless authorise? current_user, User, :handle_activity_types - error!({ error: 'Not authorised to delete an activity type' }, 403) - end + activity_type.update!(activity_type_parameters) + present activity_type, with: Entities::ActivityTypeEntity + end - activity_type = ActivityType.find(params[:id]) - activity_type.destroy - error!({ error: activity_type.errors.full_messages.last }, 403) unless activity_type.destroyed? - present activity_type.destroyed?, with: Grape::Presenters::Presenter + desc 'Delete an activity type' + delete '/activity_types/:id' do + unless authorise? current_user, User, :handle_activity_types + error!({ error: 'Not authorised to delete an activity type' }, 403) end + + activity_type = ActivityType.find(params[:id]) + activity_type.destroy + error!({ error: activity_type.errors.full_messages.last }, 403) unless activity_type.destroyed? + present activity_type.destroyed?, with: Grape::Presenters::Presenter end end diff --git a/app/api/activity_types_public_api.rb b/app/api/activity_types_public_api.rb index fc26255c3..439497ac0 100644 --- a/app/api/activity_types_public_api.rb +++ b/app/api/activity_types_public_api.rb @@ -1,16 +1,14 @@ require 'grape' -module Api - class ActivityTypesPublicApi < Grape::API +class ActivityTypesPublicApi < Grape::API - desc "Get an activity type details" - get '/activity_types/:id' do - present ActivityType.find(params[:id]), with: Api::Entities::ActivityTypeEntity - end + desc "Get an activity type details" + get '/activity_types/:id' do + present ActivityType.find(params[:id]), with: Entities::ActivityTypeEntity + end - desc 'Get all the activity types' - get '/activity_types' do - present ActivityType.all, with: Api::Entities::ActivityTypeEntity - end + desc 'Get all the activity types' + get '/activity_types' do + present ActivityType.all, with: Entities::ActivityTypeEntity end end diff --git a/app/api/admin/overseer_admin_api.rb b/app/api/admin/overseer_admin_api.rb index 7f3e754b7..d84408f0c 100644 --- a/app/api/admin/overseer_admin_api.rb +++ b/app/api/admin/overseer_admin_api.rb @@ -1,93 +1,92 @@ require 'grape' -module Api - module Admin +module Admin - class OverseerAdminApi < Grape::API - helpers AuthenticationHelpers - helpers AuthorisationHelpers + class OverseerAdminApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers - before do - authenticated? - end + before do + authenticated? + end - desc 'Add an overseer image' - params do - requires :overseer_image, type: Hash do - requires :name, type: String, desc: 'The name to display for this image' - requires :tag, type: String, desc: 'The tag used to receive from container repo' - end + desc 'Add an overseer image' + params do + requires :overseer_image, type: Hash do + requires :name, type: String, desc: 'The name to display for this image' + requires :tag, type: String, desc: 'The tag used to receive from container repo' + end + end + post '/admin/overseer_images' do + unless authorise? current_user, User, :admin_overseer + error!({ error: 'Not authorised to create overseer images' }, 403) end - post '/admin/overseer_images' do - unless authorise? current_user, User, :admin_overseer - error!({ error: 'Not authorised to create overseer images' }, 403) - end - unless Doubtfire::Application.config.overseer_enabled - error!({ error: 'Overseer is not enabled. Enable Overseer before updating settings.' }, 403) - end - overseer_image_params = ActionController::Parameters.new(params) - .require(:overseer_image) - .permit(:name, - :tag) + unless Doubtfire::Application.config.overseer_enabled + error!({ error: 'Overseer is not enabled. Enable Overseer before updating settings.' }, 403) + end + overseer_image_params = ActionController::Parameters.new(params) + .require(:overseer_image) + .permit(:name, + :tag) - result = OverseerImage.create!(overseer_image_params) + result = OverseerImage.create!(overseer_image_params) - if result.nil? - error!({ error: 'No overseer image added' }, 403) - else - result - end + if result.nil? + error!({ error: 'No overseer image added' }, 403) + else + present result, with: Entities::OverseerImageEntity end + end - desc 'Update an overseer image' - params do - requires :overseer_image, type: Hash do - optional :name, type: String, desc: 'The name of the overseer image' - optional :tag, type: String, desc: 'The tag used to receive from container repo' - end + desc 'Update an overseer image' + params do + requires :overseer_image, type: Hash do + optional :name, type: String, desc: 'The name of the overseer image' + optional :tag, type: String, desc: 'The tag used to receive from container repo' end - put '/admin/overseer_images/:id' do - unless authorise? current_user, User, :admin_overseer - error!({ error: 'Not authorised to update an overseer image' }, 403) - end - unless Doubtfire::Application.config.overseer_enabled - error!({ error: 'Overseer is not enabled. Enable Overseer before updating settings.' }, 403) - end + end + put '/admin/overseer_images/:id' do + unless authorise? current_user, User, :admin_overseer + error!({ error: 'Not authorised to update an overseer image' }, 403) + end + unless Doubtfire::Application.config.overseer_enabled + error!({ error: 'Overseer is not enabled. Enable Overseer before updating settings.' }, 403) + end + + overseer_image = OverseerImage.find(params[:id]) - overseer_image = OverseerImage.find(params[:id]) + overseer_image_params = ActionController::Parameters.new(params) + .require(:overseer_image) + .permit(:name, + :tag) - overseer_image_params = ActionController::Parameters.new(params) - .require(:overseer_image) - .permit(:name, - :tag) + overseer_image.update!(overseer_image_params) + present overseer_image, with: Entities::OverseerImageEntity + end - overseer_image.update!(overseer_image_params) - overseer_image + desc 'Delete an overseer image' + delete '/admin/overseer_images/:id' do + unless authorise? current_user, User, :admin_overseer + error!({ error: 'Not authorised to delete an overseer image' }, 403) end - desc 'Delete an overseer image' - delete '/admin/overseer_images/:id' do - unless authorise? current_user, User, :admin_overseer - error!({ error: 'Not authorised to delete an overseer image' }, 403) - end + overseer_image = OverseerImage.find(params[:id]) + overseer_image.destroy + error!({ error: overseer_image.errors.full_messages.last }, 403) unless overseer_image.destroyed? + + present overseer_image.destroyed?, with: Grape::Presenters::Presenter + end - overseer_image = OverseerImage.find(params[:id]) - overseer_image.destroy - error!({ error: overseer_image.errors.full_messages.last }, 403) unless overseer_image.destroyed? - overseer_image.destroyed? + desc 'Get all overseer images' + get '/admin/overseer_images' do + unless authorise? current_user, User, :use_overseer + error!({ error: 'Not authorised to get overseer images' }, 403) end - desc 'Get all overseer images' - get '/admin/overseer_images' do - unless authorise? current_user, User, :use_overseer - error!({ error: 'Not authorised to get overseer images' }, 403) - end - - if Doubtfire::Application.config.overseer_enabled - OverseerImage.all - else - [] - end + if Doubtfire::Application.config.overseer_enabled + present OverseerImage.all, with: Entities::OverseerImageEntity + else + present [], with: Grape::Presenters::Presenter end end end diff --git a/app/api/api.rb b/app/api/api.rb deleted file mode 100644 index 3fc5b79b9..000000000 --- a/app/api/api.rb +++ /dev/null @@ -1,131 +0,0 @@ -require 'grape' -require 'grape-swagger' - -module Api - class Root < Grape::API - helpers AuthorisationHelpers - helpers LogHelper - helpers AuthenticationHelpers - - prefix 'api' - format :json - - before do - header['Access-Control-Allow-Origin'] = '*' - header['Access-Control-Request-Method'] = '*' - end - - rescue_from :all do |e| - case e - when ActiveRecord::RecordInvalid, Grape::Exceptions::ValidationErrors - message = e.message - status = 400 - when ActiveRecord::InvalidForeignKey - message = "This operation has been rejected as it would break data integrity. Ensure that related values are deleted or updated before trying again." - status = 400 - when Grape::Exceptions::MethodNotAllowed - message = e.message - status = 405 - when ActiveRecord::RecordNotDestroyed - message = e.message - status = 400 - when ActiveRecord::RecordNotFound - message = "Unable to find requested #{e.message[/(Couldn't find )(.*)( with)/,2]}" - status = 404 - else - logger.error "Unhandled exception: #{e.class}" - logger.error e.inspect - logger.error e.backtrace.join("\n") - message = "Sorry... something went wrong with your request." - status = 500 - end - Rack::Response.new( {error: message}.to_json, status, { 'Content-type' => 'text/error' } ) - end - - # - # Mount the api modules - # - mount Api::Admin::OverseerAdminApi - mount Api::ActivityTypesAuthenticatedApi - mount Api::ActivityTypesPublicApi - mount Api::AuthenticationApi - mount Api::BreaksApi - mount Api::DiscussionCommentApi - mount Api::ExtensionCommentsApi - mount Api::GroupSetsApi - mount Api::LearningOutcomesApi - mount Api::LearningAlignmentApi - mount Api::ProjectsApi - mount Api::SettingsApi - mount Api::StudentsApi - mount Api::Submission::PortfolioApi - mount Api::Submission::PortfolioEvidenceApi - mount Api::Submission::BatchTaskApi - mount Api::TaskCommentsApi - mount Api::TaskDefinitionsApi - mount Api::TasksApi - mount Api::TeachingPeriodsPublicApi - mount Api::TeachingPeriodsAuthenticatedApi - mount Api::CampusesPublicApi - mount Api::CampusesAuthenticatedApi - mount Api::TutorialsApi - mount Api::TutorialStreamsApi - mount Api::TutorialEnrolmentsApi - mount Api::UnitRolesApi - mount Api::UnitsApi - mount Api::UsersApi - mount Api::WebcalApi - mount Api::WebcalPublicApi - - # - # Add auth details to all end points - # - AuthenticationHelpers.add_auth_to Api::Admin::OverseerAdminApi - - AuthenticationHelpers.add_auth_to Api::ActivityTypesAuthenticatedApi - AuthenticationHelpers.add_auth_to Api::BreaksApi - AuthenticationHelpers.add_auth_to Api::DiscussionCommentApi - AuthenticationHelpers.add_auth_to Api::ExtensionCommentsApi - AuthenticationHelpers.add_auth_to Api::GroupSetsApi - AuthenticationHelpers.add_auth_to Api::LearningOutcomesApi - AuthenticationHelpers.add_auth_to Api::LearningAlignmentApi - AuthenticationHelpers.add_auth_to Api::ProjectsApi - AuthenticationHelpers.add_auth_to Api::StudentsApi - AuthenticationHelpers.add_auth_to Api::Submission::PortfolioApi - AuthenticationHelpers.add_auth_to Api::Submission::PortfolioEvidenceApi - AuthenticationHelpers.add_auth_to Api::Submission::BatchTaskApi - AuthenticationHelpers.add_auth_to Api::TasksApi - AuthenticationHelpers.add_auth_to Api::TaskCommentsApi - AuthenticationHelpers.add_auth_to Api::TaskDefinitionsApi - AuthenticationHelpers.add_auth_to Api::TeachingPeriodsAuthenticatedApi - AuthenticationHelpers.add_auth_to Api::CampusesAuthenticatedApi - AuthenticationHelpers.add_auth_to Api::TutorialsApi - AuthenticationHelpers.add_auth_to Api::TutorialStreamsApi - AuthenticationHelpers.add_auth_to Api::TutorialEnrolmentsApi - AuthenticationHelpers.add_auth_to Api::UsersApi - AuthenticationHelpers.add_auth_to Api::UnitRolesApi - AuthenticationHelpers.add_auth_to Api::UnitsApi - AuthenticationHelpers.add_auth_to Api::WebcalApi - - # add_swagger_documentation format: :json, - # hide_documentation_path: false, - # api_version: 'v1', - # info: { - # title: "Horses and Hussars", - # description: "Demo app for dev of grape swagger 2.0" - # }, - # mount_path: 'swagger_doc' - - add_swagger_documentation \ - base_path: nil, - api_version: 'v1', - hide_documentation_path: true, - info: { - title: 'Doubtfire API Documentaion', - description: 'Doubtfire is a modern, lightweight learning management system.', - license: 'AGPL v3.0', - license_url: 'https://github.com/doubtfire-lms/doubtfire-api/blob/master/LICENSE' - }, - mount_path: 'swagger_doc' - end -end diff --git a/app/api/api_root.rb b/app/api/api_root.rb new file mode 100644 index 000000000..c66531b8b --- /dev/null +++ b/app/api/api_root.rb @@ -0,0 +1,122 @@ +require 'grape' +require 'grape-swagger' + +class ApiRoot < Grape::API + helpers AuthorisationHelpers + helpers LogHelper + helpers AuthenticationHelpers + + prefix 'api' + format :json + + before do + header['Access-Control-Allow-Origin'] = '*' + header['Access-Control-Request-Method'] = '*' + + Thread.current.thread_variable_set(:ip, request.ip) + end + + rescue_from :all do |e| + case e + when ActiveRecord::RecordInvalid, Grape::Exceptions::ValidationErrors + message = e.message + status = 400 + when ActiveRecord::InvalidForeignKey + message = "This operation has been rejected as it would break data integrity. Ensure that related values are deleted or updated before trying again." + status = 400 + when Grape::Exceptions::MethodNotAllowed + message = e.message + status = 405 + when ActiveRecord::RecordNotDestroyed + message = e.message + status = 400 + when ActiveRecord::RecordNotFound + message = "Unable to find requested #{e.message[/(Couldn't find )(.*)( with)/,2]}" + status = 404 + else + logger.error "Unhandled exception: #{e.class}" + logger.error e.inspect + logger.error e.backtrace.join("\n") + message = "Sorry... something went wrong with your request." + status = 500 + end + Rack::Response.new( {error: message}.to_json, status, { 'Content-type' => 'text/error' } ) + end + + # + # Mount the api modules + # + mount Admin::OverseerAdminApi + mount ActivityTypesAuthenticatedApi + mount ActivityTypesPublicApi + mount AuthenticationApi + mount BreaksApi + mount DiscussionCommentApi + mount ExtensionCommentsApi + mount GroupSetsApi + mount LearningOutcomesApi + mount LearningAlignmentApi + mount ProjectsApi + mount SettingsApi + mount StudentsApi + mount Submission::PortfolioApi + mount Submission::PortfolioEvidenceApi + mount Submission::BatchTaskApi + mount TaskCommentsApi + mount TaskDefinitionsApi + mount TasksApi + mount TeachingPeriodsPublicApi + mount TeachingPeriodsAuthenticatedApi + mount CampusesPublicApi + mount CampusesAuthenticatedApi + mount TutorialsApi + mount TutorialStreamsApi + mount TutorialEnrolmentsApi + mount UnitRolesApi + mount UnitsApi + mount UsersApi + mount WebcalApi + mount WebcalPublicApi + + # + # Add auth details to all end points + # + AuthenticationHelpers.add_auth_to Admin::OverseerAdminApi + + AuthenticationHelpers.add_auth_to ActivityTypesAuthenticatedApi + AuthenticationHelpers.add_auth_to BreaksApi + AuthenticationHelpers.add_auth_to DiscussionCommentApi + AuthenticationHelpers.add_auth_to ExtensionCommentsApi + AuthenticationHelpers.add_auth_to GroupSetsApi + AuthenticationHelpers.add_auth_to LearningOutcomesApi + AuthenticationHelpers.add_auth_to LearningAlignmentApi + AuthenticationHelpers.add_auth_to ProjectsApi + AuthenticationHelpers.add_auth_to StudentsApi + AuthenticationHelpers.add_auth_to Submission::PortfolioApi + AuthenticationHelpers.add_auth_to Submission::PortfolioEvidenceApi + AuthenticationHelpers.add_auth_to Submission::BatchTaskApi + AuthenticationHelpers.add_auth_to TasksApi + AuthenticationHelpers.add_auth_to TaskCommentsApi + AuthenticationHelpers.add_auth_to TaskDefinitionsApi + AuthenticationHelpers.add_auth_to TeachingPeriodsAuthenticatedApi + AuthenticationHelpers.add_auth_to CampusesAuthenticatedApi + AuthenticationHelpers.add_auth_to TutorialsApi + AuthenticationHelpers.add_auth_to TutorialStreamsApi + AuthenticationHelpers.add_auth_to TutorialEnrolmentsApi + AuthenticationHelpers.add_auth_to UsersApi + AuthenticationHelpers.add_auth_to UnitRolesApi + AuthenticationHelpers.add_auth_to UnitsApi + AuthenticationHelpers.add_auth_to WebcalApi + + add_swagger_documentation \ + base_path: nil, + api_version: 'v1', + hide_documentation_path: true, + info: { + title: 'Doubtfire API Documentaion', + description: 'Doubtfire is a modern, lightweight learning management system.', + license: 'AGPL v3.0', + license_url: 'https://github.com/doubtfire-lms/doubtfire-api/blob/master/LICENSE' + }, + mount_path: 'swagger_doc' +end diff --git a/app/api/authentication_api.rb b/app/api/authentication_api.rb index bd1c7935d..0f501dc3f 100644 --- a/app/api/authentication_api.rb +++ b/app/api/authentication_api.rb @@ -3,367 +3,364 @@ require 'onelogin/ruby-saml' require 'entities/user_entity' -module Api +# +# Provides the authentication API for Doubtfire. +# Users can sign in via email and password and receive an auth token +# that can be used with other API calls. +# +class AuthenticationApi < Grape::API + helpers LogHelper + helpers AuthenticationHelpers + # - # Provides the authentication API for Doubtfire. - # Users can sign in via email and password and receive an auth token - # that can be used with other API calls. + # Sign in - only mounted if AAF auth is NOT used # - class AuthenticationApi < Grape::API - helpers LogHelper - helpers AuthenticationHelpers - - # - # Sign in - only mounted if AAF auth is NOT used - # - if !AuthenticationHelpers.aaf_auth? && !AuthenticationHelpers.saml_auth? - desc 'Sign in' - params do - requires :username, type: String, desc: 'User username' - requires :password, type: String, desc: 'User\'s password' - optional :remember, type: Boolean, desc: 'User has requested to remember login', default: false + if !AuthenticationHelpers.aaf_auth? && !AuthenticationHelpers.saml_auth? + desc 'Sign in' + params do + requires :username, type: String, desc: 'User username' + requires :password, type: String, desc: 'User\'s password' + optional :remember, type: Boolean, desc: 'User has requested to remember login', default: false + end + post '/auth' do + username = params[:username] + password = params[:password] + remember = params[:remember] + logger.info "Authenticate #{username} from #{request.ip}" + + # Truncate the 's' from sXXX for Swinburne auth + truncate_s_match = (username =~ /^[Ss]\d{6,10}([Xx]|\d)$/) + username[0] = '' if !truncate_s_match.nil? && truncate_s_match.zero? + + # No provided credentials + if username.nil? || password.nil? + error!({ error: 'The request must contain the user username and password.' }, 400) end - post '/auth' do - username = params[:username] - password = params[:password] - remember = params[:remember] - logger.info "Authenticate #{username} from #{request.ip}" - - # Truncate the 's' from sXXX for Swinburne auth - truncate_s_match = (username =~ /^[Ss]\d{6,10}([Xx]|\d)$/) - username[0] = '' if !truncate_s_match.nil? && truncate_s_match.zero? - - # No provided credentials - if username.nil? || password.nil? - error!({ error: 'The request must contain the user username and password.' }, 400) - return - end - # User lookup - username = username.downcase - institution_email_domain = Doubtfire::Application.config.institution[:email_domain] - user = User.find_or_create_by(username: username) do |new_user| - new_user.first_name = 'First Name' - new_user.last_name = 'Surname' - new_user.email = "#{username}@#{institution_email_domain}" - new_user.nickname = 'Nickname' - new_user.role_id = Role.student.id - new_user.login_id = username - end + # User lookup + username = username.downcase + institution_email_domain = Doubtfire::Application.config.institution[:email_domain] + user = User.find_or_create_by(username: username) do |new_user| + new_user.first_name = 'First Name' + new_user.last_name = 'Surname' + new_user.email = "#{username}@#{institution_email_domain}" + new_user.nickname = 'Nickname' + new_user.role_id = Role.student.id + new_user.login_id = username + end - # Try to authenticate - unless user.authenticate?(password) - error!({ error: 'Invalid email or password.' }, 401) - return - end + # Try to authenticate + unless user.authenticate?(password) + error!({ error: 'Invalid email or password.' }, 401) + return + end - # Create user if they are a new record - if user.new_record? - user.encrypted_password = BCrypt::Password.create('password') + # Create user if they are a new record + if user.new_record? + user.encrypted_password = BCrypt::Password.create('password') - unless user.valid? - error!(error: 'There was an error creating your account in Doubtfire. ' \ - 'Please get in contact with your unit convenor or the ' \ - 'Doubtfire administrators.') - end - user.save + unless user.valid? + error!(error: 'There was an error creating your account in Doubtfire. ' \ + 'Please get in contact with your unit convenor or the ' \ + 'Doubtfire administrators.') end + user.save + end - logger.info "Login #{username} from #{request.ip}" + logger.info "Login #{username} from #{request.ip}" - # Return user details - present :user, user, with: Api::Entities::UserEntity - present :auth_token, user.generate_authentication_token!(remember).authentication_token - end + # Return user details + present :user, user, with: Entities::UserEntity + present :auth_token, user.generate_authentication_token!(remember).authentication_token end + end - # - # AAF JWT callback - only mounted if AAF SAML is used - # This isn't really a JWT, we will treat it as if it's a SAML response - # - if AuthenticationHelpers.saml_auth? - desc 'SAML2.0 auth' - params do - requires :SAMLResponse, type: String, desc: 'Data provided for further processing.' - end - post '/auth/jwt' do - response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], allowed_clock_drift: 1.second, - settings: AuthenticationHelpers.saml_settings) - - # We validate the SAML Response and check if the user already exists in the system - return error!({ error: 'Invalid SAML response.' }, 401) unless response.is_valid? - - attributes = response.attributes - - login_id = response.name_id || response.nameid - email = login_id - - logger.info "Authenticate #{email} from #{request.ip}" - - # Lookup using login_id if it exists - # Lookup using email otherwise and set login_id - # Otherwise create new - user = User.find_by(login_id: login_id) || - User.find_by_username(email[/(.*)@/, 1]) || - User.find_by(email: email) || - User.find_or_create_by(login_id: login_id) do |new_user| - role = attributes.fetch(/role/) || Role.student.id - first_name = (attributes.fetch(/givenname/) || attributes.fetch(/cn/)).capitalize - last_name = attributes.fetch(/surname/).capitalize - username = email.split('@').first - # Some institutions may provide givenname and surname, others - # may only provide common name which we will use as first name - new_user.first_name = first_name - new_user.last_name = last_name - new_user.email = email - new_user.username = username - new_user.nickname = first_name - new_user.role_id = role - end - - # Set login id + username if not yet specified - user.login_id = login_id if user.login_id.nil? - user.username = username if user.username.nil? - - # Try and save the user once authenticated if new - if user.new_record? - user.encrypted_password = BCrypt::Password.create(SecureRandom.hex(32)) - unless user.valid? - error!(error: 'There was an error creating your account in Doubtfire. ' \ - 'Please get in contact with your unit convenor or the ' \ - 'Doubtfire administrators.') - end - user.save + # + # AAF JWT callback - only mounted if AAF SAML is used + # This isn't really a JWT, we will treat it as if it's a SAML response + # + if AuthenticationHelpers.saml_auth? + desc 'SAML2.0 auth' + params do + requires :SAMLResponse, type: String, desc: 'Data provided for further processing.' + end + post '/auth/jwt' do + response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], allowed_clock_drift: 1.second, + settings: AuthenticationHelpers.saml_settings) + + # We validate the SAML Response and check if the user already exists in the system + return error!({ error: 'Invalid SAML response.' }, 401) unless response.is_valid? + + attributes = response.attributes + + login_id = response.name_id || response.nameid + email = login_id + + logger.info "Authenticate #{email} from #{request.ip}" + + # Lookup using login_id if it exists + # Lookup using email otherwise and set login_id + # Otherwise create new + user = User.find_by(login_id: login_id) || + User.find_by_username(email[/(.*)@/, 1]) || + User.find_by(email: email) || + User.find_or_create_by(login_id: login_id) do |new_user| + role = attributes.fetch(/role/) || Role.student.id + first_name = (attributes.fetch(/givenname/) || attributes.fetch(/cn/)).capitalize + last_name = attributes.fetch(/surname/).capitalize + username = email.split('@').first + # Some institutions may provide givenname and surname, others + # may only provide common name which we will use as first name + new_user.first_name = first_name + new_user.last_name = last_name + new_user.email = email + new_user.username = username + new_user.nickname = first_name + new_user.role_id = role + end + + # Set login id + username if not yet specified + user.login_id = login_id if user.login_id.nil? + user.username = username if user.username.nil? + + # Try and save the user once authenticated if new + if user.new_record? + user.encrypted_password = BCrypt::Password.create(SecureRandom.hex(32)) + unless user.valid? + error!(error: 'There was an error creating your account in Doubtfire. ' \ + 'Please get in contact with your unit convenor or the ' \ + 'Doubtfire administrators.') end + user.save + end - # Generate a temporary auth_token for future requests - onetime_token = user.generate_temporary_authentication_token! + # Generate a temporary auth_token for future requests + onetime_token = user.generate_temporary_authentication_token! - logger.info "Redirecting #{user.username} from #{request.ip}" + logger.info "Redirecting #{user.username} from #{request.ip}" - # Must redirect to the front-end after sign in - protocol = Rails.env.development? ? 'http' : 'https' - host = Rails.env.development? ? "#{protocol}://localhost:3000" : Doubtfire::Application.config.institution[:host] - host = "#{protocol}://#{host}" unless host.starts_with?('http') - redirect "#{host}/#sign_in?authToken=#{onetime_token.authentication_token}&username=#{user.username}" - end + # Must redirect to the front-end after sign in + protocol = Rails.env.development? ? 'http' : 'https' + host = Rails.env.development? ? "#{protocol}://localhost:3000" : Doubtfire::Application.config.institution[:host] + host = "#{protocol}://#{host}" unless host.starts_with?('http') + redirect "#{host}/#sign_in?authToken=#{onetime_token.authentication_token}&username=#{user.username}" end + end - # - # AAF JWT callback - only mounted if AAF auth is used - # - if AuthenticationHelpers.aaf_auth? - desc 'AAF Rapid Connect JWT callback' - params do - requires :assertion, type: String, desc: 'Data provided for further processing.' - end - post '/auth/jwt' do - jws = params[:assertion] - error!({ error: 'JWS was not found in request.' }, 500) unless jws - - # Decode JWS - jwt = User.decode_jws(jws) - error!({ error: 'Invalid JWS.' }, 500) unless jwt - - # User lookup via unique login id - attrs = jwt['https://aaf.edu.au/attributes'] - login_id = jwt[:sub] - email = attrs[:mail] - - logger.info "Authenticate #{email} from #{request.ip}" - - # Lookup using login_id if it exists - # Lookup using email otherwise and set login_id - # Otherwise create new - user = User.find_by(login_id: login_id) || - User.find_by_username(email[/(.*)@/, 1]) || - User.find_by(email: email) || - User.find_or_create_by(login_id: login_id) do |new_user| - role = Role.aaf_affiliation_to_role_id(attrs[:edupersonscopedaffiliation]) - first_name = (attrs[:givenname] || attrs[:cn]).capitalize - last_name = attrs[:surname].capitalize - username = email.split('@').first - # Some institutions may provide givenname and surname, others - # may only provide common name which we will use as first name - new_user.first_name = first_name - new_user.last_name = last_name - new_user.email = email - new_user.username = username - new_user.nickname = first_name - new_user.role_id = role - end - - # Set login id + username if not yet specified - user.login_id = login_id if user.login_id.nil? - user.username = username if user.username.nil? - - # Try to authenticate - return error!({ error: 'Invalid JSON web token.' }, 401) unless user.authenticate?(jws) - - # Try and save the user once authenticated if new - if user.new_record? - user.encrypted_password = BCrypt::Password.create(SecureRandom.hex(32)) - unless user.valid? - error!(error: 'There was an error creating your account in Doubtfire. ' \ - 'Please get in contact with your unit convenor or the ' \ - 'Doubtfire administrators.') - end - user.save + # + # AAF JWT callback - only mounted if AAF auth is used + # + if AuthenticationHelpers.aaf_auth? + desc 'AAF Rapid Connect JWT callback' + params do + requires :assertion, type: String, desc: 'Data provided for further processing.' + end + post '/auth/jwt' do + jws = params[:assertion] + error!({ error: 'JWS was not found in request.' }, 500) unless jws + + # Decode JWS + jwt = User.decode_jws(jws) + error!({ error: 'Invalid JWS.' }, 500) unless jwt + + # User lookup via unique login id + attrs = jwt['https://aaf.edu.au/attributes'] + login_id = jwt[:sub] + email = attrs[:mail] + + logger.info "Authenticate #{email} from #{request.ip}" + + # Lookup using login_id if it exists + # Lookup using email otherwise and set login_id + # Otherwise create new + user = User.find_by(login_id: login_id) || + User.find_by_username(email[/(.*)@/, 1]) || + User.find_by(email: email) || + User.find_or_create_by(login_id: login_id) do |new_user| + role = Role.aaf_affiliation_to_role_id(attrs[:edupersonscopedaffiliation]) + first_name = (attrs[:givenname] || attrs[:cn]).capitalize + last_name = attrs[:surname].capitalize + username = email.split('@').first + # Some institutions may provide givenname and surname, others + # may only provide common name which we will use as first name + new_user.first_name = first_name + new_user.last_name = last_name + new_user.email = email + new_user.username = username + new_user.nickname = first_name + new_user.role_id = role + end + + # Set login id + username if not yet specified + user.login_id = login_id if user.login_id.nil? + user.username = username if user.username.nil? + + # Try to authenticate + return error!({ error: 'Invalid JSON web token.' }, 401) unless user.authenticate?(jws) + + # Try and save the user once authenticated if new + if user.new_record? + user.encrypted_password = BCrypt::Password.create(SecureRandom.hex(32)) + unless user.valid? + error!(error: 'There was an error creating your account in Doubtfire. ' \ + 'Please get in contact with your unit convenor or the ' \ + 'Doubtfire administrators.') end + user.save + end - # Generate a temporary auth_token for future requests - onetime_token = user.generate_temporary_authentication_token! + # Generate a temporary auth_token for future requests + onetime_token = user.generate_temporary_authentication_token! - logger.info "Redirecting #{user.username} from #{request.ip}" + logger.info "Redirecting #{user.username} from #{request.ip}" - # Must redirect to the front-end after sign in - protocol = Rails.env.development? ? 'http' : 'https' - host = Rails.env.development? ? "#{protocol}://localhost:3000" : Doubtfire::Application.config.institution[:host] - host = "#{protocol}://#{host}" unless host.starts_with?('http') - redirect "#{host}/#sign_in?authToken=#{onetime_token.authentication_token}&username=#{user.username}" - end + # Must redirect to the front-end after sign in + protocol = Rails.env.development? ? 'http' : 'https' + host = Rails.env.development? ? "#{protocol}://localhost:3000" : Doubtfire::Application.config.institution[:host] + host = "#{protocol}://#{host}" unless host.starts_with?('http') + redirect "#{host}/#sign_in?authToken=#{onetime_token.authentication_token}&username=#{user.username}" end + end - if AuthenticationHelpers.saml_auth? || AuthenticationHelpers.aaf_auth? - # - # Respond user details provided a temporary login token - # - desc 'Get user details from an authentication token' - params do - requires :username, type: String, desc: 'The user\'s username' - requires :auth_token, type: String, desc: 'The user\'s temporary auth token' - end - post '/auth' do - error!({ error: 'Invalid token.' }, 404) if params[:auth_token].nil? - logger.info "Get user via auth_token from #{request.ip}" + if AuthenticationHelpers.saml_auth? || AuthenticationHelpers.aaf_auth? + # + # Respond user details provided a temporary login token + # + desc 'Get user details from an authentication token' + params do + requires :username, type: String, desc: 'The user\'s username' + requires :auth_token, type: String, desc: 'The user\'s temporary auth token' + end + post '/auth' do + error!({ error: 'Invalid token.' }, 404) if params[:auth_token].nil? + logger.info "Get user via auth_token from #{request.ip}" - # Authenticate that the token is okay - if authenticated? - user = User.find_by_username(params[:username]) - token = user.token_for_text?(params[:auth_token]) unless user.nil? - error!({ error: 'Invalid token.' }, 404) if token.nil? + # Authenticate that the token is okay + if authenticated? + user = User.find_by_username(params[:username]) + token = user.token_for_text?(params[:auth_token]) unless user.nil? + error!({ error: 'Invalid token.' }, 404) if token.nil? - # Invalidate the token and regenrate a new one - token.destroy! - token = user.generate_authentication_token! true + # Invalidate the token and regenrate a new one + token.destroy! + token = user.generate_authentication_token! true - logger.info "Login #{params[:username]} from #{request.ip}" + logger.info "Login #{params[:username]} from #{request.ip}" - # Respond user details with new auth token - present :user, user, with: Api::Entities::UserEntity - present :auth_token, token.authentication_token - end + # Respond user details with new auth token + present :user, user, with: Entities::UserEntity + present :auth_token, token.authentication_token end end + end - # - # Returns the current auth method - # - desc 'Authentication method configuration' - get '/auth/method' do - response = { - method: Doubtfire::Application.config.auth_method - } - response[:redirect_to] = - if aaf_auth? - Doubtfire::Application.config.aaf[:redirect_url] - elsif saml_auth? - request = OneLogin::RubySaml::Authrequest.new - request.create(AuthenticationHelpers.saml_settings) - end - present response, with: Grape::Presenters::Presenter - end + # + # Returns the current auth method + # + desc 'Authentication method configuration' + get '/auth/method' do + response = { + method: Doubtfire::Application.config.auth_method + } + response[:redirect_to] = + if aaf_auth? + Doubtfire::Application.config.aaf[:redirect_url] + elsif saml_auth? + request = OneLogin::RubySaml::Authrequest.new + request.create(AuthenticationHelpers.saml_settings) + end + present response, with: Grape::Presenters::Presenter + end - # - # Returns the current auth signout URL - # - desc 'Authentication signout URL' - get '/auth/signout_url' do - response = {} - response[:auth_signout_url] = - if aaf_auth? && Doubtfire::Application.config.aaf[:auth_signout_url].present? - Doubtfire::Application.config.aaf[:auth_signout_url] - elsif saml_auth? && Doubtfire::Application.config.saml[:idp_sso_target_url].present? - Doubtfire::Application.config.saml[:idp_sso_target_url] - end - present response, with: Grape::Presenters::Presenter - end + # + # Returns the current auth signout URL + # + desc 'Authentication signout URL' + get '/auth/signout_url' do + response = {} + response[:auth_signout_url] = + if aaf_auth? && Doubtfire::Application.config.aaf[:auth_signout_url].present? + Doubtfire::Application.config.aaf[:auth_signout_url] + elsif saml_auth? && Doubtfire::Application.config.saml[:idp_sso_target_url].present? + Doubtfire::Application.config.saml[:idp_sso_target_url] + end + present response, with: Grape::Presenters::Presenter + end - # - # Update the expiry of an existing authentication token - # - desc 'Allow tokens to be updated', + # + # Update the expiry of an existing authentication token + # + desc 'Allow tokens to be updated', + { + headers: { - headers: + "username" => { - "username" => - { - description: "User username", - required: true - }, - "auth_token" => - { - description: "The user\'s temporary auth token", - required: true - } + description: "User username", + required: true + }, + "auth_token" => + { + description: "The user\'s temporary auth token", + required: true } } - params do - optional :remember, type: Boolean, desc: 'User has requested to remember login', default: false - end - put '/auth' do - token_param = headers['Auth-Token'] || params['Auth-Token'] - user_param = headers['Username'] || params['Username'] + } + params do + optional :remember, type: Boolean, desc: 'User has requested to remember login', default: false + end + put '/auth' do + token_param = headers['Auth-Token'] || params['Auth-Token'] + user_param = headers['Username'] || params['Username'] - error!({ error: 'Invalid token/username.' }, 404) if token_param.nil? || user_param.nil? + error!({ error: 'Invalid token/username.' }, 404) if token_param.nil? || user_param.nil? - logger.info "Update token #{token_param} from #{request.ip} for #{user_param}" + logger.info "Update token #{token_param} from #{request.ip} for #{user_param}" - # Find user - user = User.find_by_username(user_param) - token = user.token_for_text?(token_param) unless user.nil? - remember = params[:remember] || false + # Find user + user = User.find_by_username(user_param) + token = user.token_for_text?(token_param) unless user.nil? + remember = params[:remember] || false - # Token does not match user - if token.nil? || user.nil? || user.username != user_param - error!({ error: 'Invalid token.' }, 404) - else - token.extend_token remember if token.auth_token_expiry > Time.zone.now + # Token does not match user + if token.nil? || user.nil? || user.username != user_param + error!({ error: 'Invalid token.' }, 404) + else + token.extend_token remember if token.auth_token_expiry > Time.zone.now - # Return extended auth token - present :auth_token, token.authentication_token - end + # Return extended auth token + present :auth_token, token.authentication_token end + end - # - # Sign out - # - desc 'Sign out', + # + # Sign out + # + desc 'Sign out', + { + headers: { - headers: + "username" => + { + description: "User username", + required: true + }, + "auth_token" => { - "username" => - { - description: "User username", - required: true - }, - "auth_token" => - { - description: "The user\'s temporary auth token", - required: true - } + description: "The user\'s temporary auth token", + required: true } } - delete '/auth' do - user = User.find_by_username(headers['Username']) - token = user.token_for_text?(headers['Auth-Token']) unless user.nil? - - if token.present? - logger.info "Sign out #{user.username} from #{request.ip}" - token.destroy! - end - - present nil + } + delete '/auth' do + user = User.find_by_username(headers['Username']) + token = user.token_for_text?(headers['Auth-Token']) unless user.nil? + + if token.present? + logger.info "Sign out #{user.username} from #{request.ip}" + token.destroy! end + + present nil end end diff --git a/app/api/breaks_api.rb b/app/api/breaks_api.rb index d00369d88..2bcb24601 100644 --- a/app/api/breaks_api.rb +++ b/app/api/breaks_api.rb @@ -1,79 +1,77 @@ require 'grape' -module Api - class BreaksApi < Grape::API - helpers AuthenticationHelpers - helpers AuthorisationHelpers +class BreaksApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers - before do - authenticated? - end + before do + authenticated? + end - desc 'Add a new break to the teaching period' - params do - requires :start_date, type: Date, desc: 'The start date of the break' - requires :number_of_weeks, type: Integer, desc: 'Break duration' + desc 'Add a new break to the teaching period' + params do + requires :start_date, type: Date, desc: 'The start date of the break' + requires :number_of_weeks, type: Integer, desc: 'Break duration' + end + post '/teaching_periods/:teaching_period_id/breaks' do + unless authorise? current_user, User, :handle_teaching_period + error!({ error: 'Not authorised to add a break' }, 403) end - post '/teaching_periods/:teaching_period_id/breaks' do - unless authorise? current_user, User, :handle_teaching_period - error!({ error: 'Not authorised to add a break' }, 403) - end - # Find the Teaching Period to add break - teaching_period = TeachingPeriod.find(params[:teaching_period_id]) + # Find the Teaching Period to add break + teaching_period = TeachingPeriod.find(params[:teaching_period_id]) - start_date = params[:start_date] - number_of_weeks = params[:number_of_weeks] + start_date = params[:start_date] + number_of_weeks = params[:number_of_weeks] - result = teaching_period.add_break(start_date, number_of_weeks) - present result, with: Api::Entities::BreakEntity - end + result = teaching_period.add_break(start_date, number_of_weeks) + present result, with: Entities::BreakEntity + end - desc 'Update a break in the teaching period' - params do - optional :start_date, type: Date, desc: 'The start date of the break' - optional :number_of_weeks, type: Integer, desc: 'Break duration' + desc 'Update a break in the teaching period' + params do + optional :start_date, type: Date, desc: 'The start date of the break' + optional :number_of_weeks, type: Integer, desc: 'Break duration' + end + put '/teaching_periods/:teaching_period_id/breaks/:id' do + unless authorise? current_user, User, :handle_teaching_period + error!({ error: 'Not authorised to update a break' }, 403) end - put '/teaching_periods/:teaching_period_id/breaks/:id' do - unless authorise? current_user, User, :handle_teaching_period - error!({ error: 'Not authorised to update a break' }, 403) - end - # Find the Teaching Period to update break - teaching_period = TeachingPeriod.find(params[:teaching_period_id]) + # Find the Teaching Period to update break + teaching_period = TeachingPeriod.find(params[:teaching_period_id]) - id = params[:id] - start_date = params[:start_date] - number_of_weeks = params[:number_of_weeks] + id = params[:id] + start_date = params[:start_date] + number_of_weeks = params[:number_of_weeks] - result = teaching_period.update_break(id, start_date, number_of_weeks) - present result, with: Api::Entities::BreakEntity + result = teaching_period.update_break(id, start_date, number_of_weeks) + present result, with: Entities::BreakEntity + end + + desc 'Get all the breaks in the Teaching Period' + get '/teaching_periods/:teaching_period_id/breaks' do + unless authorise? current_user, User, :get_teaching_periods + error!({ error: 'Not authorised to get breaks' }, 403) end - desc 'Get all the breaks in the Teaching Period' - get '/teaching_periods/:teaching_period_id/breaks' do - unless authorise? current_user, User, :get_teaching_periods - error!({ error: 'Not authorised to get breaks' }, 403) - end + teaching_period = TeachingPeriod.find(params[:teaching_period_id]) + present teaching_period.breaks, with: Entities::BreakEntity + end - teaching_period = TeachingPeriod.find(params[:teaching_period_id]) - present teaching_period.breaks, with: Api::Entities::BreakEntity + desc 'Remove a break from a teaching period' + delete '/teaching_periods/:teaching_period_id/breaks/:id' do + unless authorise? current_user, User, :handle_teaching_period + error!({ error: 'Not authorised to delete a break' }, 403) end - desc 'Remove a break from a teaching period' - delete '/teaching_periods/:teaching_period_id/breaks/:id' do - unless authorise? current_user, User, :handle_teaching_period - error!({ error: 'Not authorised to delete a break' }, 403) - end + # Find the Teaching Period to update break + teaching_period = TeachingPeriod.find(params[:teaching_period_id]) - # Find the Teaching Period to update break - teaching_period = TeachingPeriod.find(params[:teaching_period_id]) + id = params[:id] + the_break = teaching_period.breaks.find(id) - id = params[:id] - the_break = teaching_period.breaks.find(id) - - the_break.destroy - present the_break.destroyed?, with: Grape::Presenters::Presenter - end + the_break.destroy + present the_break.destroyed?, with: Grape::Presenters::Presenter end end diff --git a/app/api/campuses_authenticated_api.rb b/app/api/campuses_authenticated_api.rb index 92cd68d13..28fd53c08 100644 --- a/app/api/campuses_authenticated_api.rb +++ b/app/api/campuses_authenticated_api.rb @@ -1,78 +1,76 @@ require 'grape' -module Api - class CampusesAuthenticatedApi < Grape::API - helpers AuthenticationHelpers - helpers AuthorisationHelpers +class CampusesAuthenticatedApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers - before do - authenticated? - end + before do + authenticated? + end - desc 'Add a Campus' - params do - requires :campus, type: Hash do - requires :name, type: String, desc: 'The name of the campus' - requires :mode, type: String, desc: 'This will determine the campus mode', values: ['timetable', 'automatic', 'manual'] - requires :abbreviation, type: String, desc: 'The abbreviation for the campus' - requires :active, type: Boolean, desc: 'Determines whether campus is active' - end + desc 'Add a Campus' + params do + requires :campus, type: Hash do + requires :name, type: String, desc: 'The name of the campus' + requires :mode, type: String, desc: 'This will determine the campus mode', values: ['timetable', 'automatic', 'manual'] + requires :abbreviation, type: String, desc: 'The abbreviation for the campus' + requires :active, type: Boolean, desc: 'Determines whether campus is active' end - post '/campuses' do - unless authorise? current_user, User, :handle_campuses - error!({ error: 'Not authorised to create a campus' }, 403) - end - campus_parameters = ActionController::Parameters.new(params) - .require(:campus) - .permit(:name, - :mode, - :abbreviation, - :active) + end + post '/campuses' do + unless authorise? current_user, User, :handle_campuses + error!({ error: 'Not authorised to create a campus' }, 403) + end + campus_parameters = ActionController::Parameters.new(params) + .require(:campus) + .permit(:name, + :mode, + :abbreviation, + :active) - result = Campus.create!(campus_parameters) + result = Campus.create!(campus_parameters) - if result.nil? - error!({ error: 'No campus added.' }, 403) - else - present result, with: Api::Entities::CampusEntity - end + if result.nil? + error!({ error: 'No campus added.' }, 403) + else + present result, with: Entities::CampusEntity end + end - desc 'Update Campus' - params do - requires :campus, type: Hash do - optional :name, type: String, desc: 'The name of the campus' - optional :mode, type: String, desc: 'This will determine the campus mode', values: ['timetable', 'automatic', 'manual'] - optional :abbreviation, type: String, desc: 'The abbreviation for the campus' - optional :active, type: Boolean, desc: 'Determines whether campus is active' - end + desc 'Update Campus' + params do + requires :campus, type: Hash do + optional :name, type: String, desc: 'The name of the campus' + optional :mode, type: String, desc: 'This will determine the campus mode', values: ['timetable', 'automatic', 'manual'] + optional :abbreviation, type: String, desc: 'The abbreviation for the campus' + optional :active, type: Boolean, desc: 'Determines whether campus is active' end - put '/campuses/:id' do - campus = Campus.find(params[:id]) - unless authorise? current_user, User, :handle_campuses - error!({ error: 'Not authorised to update a campus' }, 403) - end - campus_parameters = ActionController::Parameters.new(params) - .require(:campus) - .permit(:name, - :mode, - :abbreviation, - :active) - - campus.update!(campus_parameters) - present campus, with: Api::Entities::CampusEntity + end + put '/campuses/:id' do + campus = Campus.find(params[:id]) + unless authorise? current_user, User, :handle_campuses + error!({ error: 'Not authorised to update a campus' }, 403) end + campus_parameters = ActionController::Parameters.new(params) + .require(:campus) + .permit(:name, + :mode, + :abbreviation, + :active) - desc 'Delete a campus' - delete '/campuses/:id' do - unless authorise? current_user, User, :handle_campuses - error!({ error: 'Not authorised to delete a campus' }, 403) - end + campus.update!(campus_parameters) + present campus, with: Entities::CampusEntity + end - campus = Campus.find(params[:id]) - campus.destroy - error!({ error: campus.errors.full_messages.last }, 403) unless campus.destroyed? - present campus.destroyed?, with: Grape::Presenters::Presenter + desc 'Delete a campus' + delete '/campuses/:id' do + unless authorise? current_user, User, :handle_campuses + error!({ error: 'Not authorised to delete a campus' }, 403) end + + campus = Campus.find(params[:id]) + campus.destroy + error!({ error: campus.errors.full_messages.last }, 403) unless campus.destroyed? + present campus.destroyed?, with: Grape::Presenters::Presenter end end diff --git a/app/api/campuses_public_api.rb b/app/api/campuses_public_api.rb index b95db9830..9d4eec219 100644 --- a/app/api/campuses_public_api.rb +++ b/app/api/campuses_public_api.rb @@ -1,17 +1,15 @@ require 'grape' -module Api - class CampusesPublicApi < Grape::API +class CampusesPublicApi < Grape::API - desc "Get a campus details" - get '/campuses/:id' do - campus = Campus.find(params[:id]) - present campus, with: Api::Entities::CampusEntity - end + desc "Get a campus details" + get '/campuses/:id' do + campus = Campus.find(params[:id]) + present campus, with: Entities::CampusEntity + end - desc 'Get all the Campuses' - get '/campuses' do - present Campus.all, with: Api::Entities::CampusEntity - end + desc 'Get all the Campuses' + get '/campuses' do + present Campus.all, with: Entities::CampusEntity end end diff --git a/app/api/discussion_comment_api.rb b/app/api/discussion_comment_api.rb index 4df749753..e5ca2c172 100644 --- a/app/api/discussion_comment_api.rb +++ b/app/api/discussion_comment_api.rb @@ -1,212 +1,211 @@ # frozen_string_literal: true require 'grape' -module Api - class DiscussionCommentApi < Grape::API - helpers AuthenticationHelpers - helpers AuthorisationHelpers +class DiscussionCommentApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers - before do - authenticated? - end + before do + authenticated? + end - desc 'Add a new discussion comment to a task' - params do - requires :attachments, type: Array do - requires type: File, desc: 'audio prompts.' - end + desc 'Add a new discussion comment to a task' + params do + requires :attachments, type: Array do + requires type: File, desc: 'audio prompts.' end - post '/projects/:project_id/task_def_id/:task_definition_id/discussion_comments' do - project = Project.find(params[:project_id]) - task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - task = project.task_for_task_definition(task_definition) + end + post '/projects/:project_id/task_def_id/:task_definition_id/discussion_comments' do + project = Project.find(params[:project_id]) + task_definition = project.unit.task_definitions.find(params[:task_definition_id]) + task = project.task_for_task_definition(task_definition) - unless authorise? current_user, task, :create_discussion - error!({ error: 'Not authorised to create a discussion comment for this task' }, 403) - end + unless authorise? current_user, task, :create_discussion + error!({ error: 'Not authorised to create a discussion comment for this task' }, 403) + end - attached_files = params[:attachments] + attached_files = params[:attachments] - for attached_file in attached_files do - if attached_file.present? - error!(error: 'Attachment is empty.') unless File.size?(attached_file["tempfile"].path).present? - error!(error: 'Attachment exceeds the maximum attachment size of 30MB.') unless File.size?(attached_file["tempfile"].path) < 30_000_000 - end + for attached_file in attached_files do + if attached_file.present? + error!(error: 'Attachment is empty.') unless File.size?(attached_file["tempfile"].path).present? + error!(error: 'Attachment exceeds the maximum attachment size of 30MB.') unless File.size?(attached_file["tempfile"].path) < 30_000_000 end + end - type_string = content_type.to_s - - logger.info("#{current_user.username} - added discussion comment for task #{task.id} (#{task_definition.abbreviation})") + type_string = content_type.to_s - if attached_files.nil? || attached_files.empty? - error!({ error: 'Audio prompts are empty, unable to add new discussion comment' }, 403) - end + logger.info("#{current_user.username} - added discussion comment for task #{task.id} (#{task_definition.abbreviation})") - result = task.add_discussion_comment(current_user, attached_files) - result.serialize(current_user) + if attached_files.nil? || attached_files.empty? + error!({ error: 'Audio prompts are empty, unable to add new discussion comment' }, 403) end - desc 'Get a discussion comment prompt' - params do - optional :as_attachment, type: Boolean, desc: 'Whether or not to download file as attachment. Default is false.' - end - get '/projects/:project_id/task_def_id/:task_definition_id/comments/:task_comment_id/discussion_comment/prompt_number/:prompt_number' do - project = Project.find(params[:project_id]) - task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - prompt_number = params[:prompt_number] + result = task.add_discussion_comment(current_user, attached_files) - task = project.task_for_task_definition(task_definition) + present result.serialize(current_user), Grape::Presenters::Presenter + end - unless authorise? current_user, task, :get_discussion - error!({ error: 'You cannot get this discussion prompt' }, 403) - end + desc 'Get a discussion comment prompt' + params do + optional :as_attachment, type: Boolean, desc: 'Whether or not to download file as attachment. Default is false.' + end + get '/projects/:project_id/task_def_id/:task_definition_id/comments/:task_comment_id/discussion_comment/prompt_number/:prompt_number' do + project = Project.find(params[:project_id]) + task_definition = project.unit.task_definitions.find(params[:task_definition_id]) + prompt_number = params[:prompt_number] - if project.has_task_for_task_definition? task_definition - task = project.task_for_task_definition(task_definition) - discussion_comment = task.all_comments.find(params[:task_comment_id]).becomes(DiscussionComment) - discussion_comment.mark_discussion_started + task = project.task_for_task_definition(task_definition) - prompt_path = discussion_comment.attachment_path(prompt_number) + unless authorise? current_user, task, :get_discussion + error!({ error: 'You cannot get this discussion prompt' }, 403) + end - error!({ error: 'File missing' }, 404) unless File.exist? prompt_path - logger.info("#{current_user.username} - get discussion comment for task #{task.id} (#{task_definition.abbreviation})") + if project.has_task_for_task_definition? task_definition + task = project.task_for_task_definition(task_definition) + discussion_comment = task.all_comments.find(params[:task_comment_id]).becomes(DiscussionComment) + discussion_comment.mark_discussion_started - content_type('audio/wav; charset:binary') - env['api.format'] = :binary + prompt_path = discussion_comment.attachment_path(prompt_number) - # mark as attachment - if params[:as_attachment] - header['Content-Disposition'] = "attachment; filename=#{prompt_path}" - end + error!({ error: 'File missing' }, 404) unless File.exist? prompt_path + logger.info("#{current_user.username} - get discussion comment for task #{task.id} (#{task_definition.abbreviation})") + + content_type('audio/wav; charset:binary') + env['api.format'] = :binary - # Work out what part to return - file_size = File.size(prompt_path) - begin_point = 0 - end_point = file_size - 1 + # mark as attachment + if params[:as_attachment] + header['Content-Disposition'] = "attachment; filename=#{prompt_path}" + end - # Was it asked for just a part of the file? - if request.headers['Range'] - # indicate partial content - status 206 + # Work out what part to return + file_size = File.size(prompt_path) + begin_point = 0 + end_point = file_size - 1 - # extract part desired from the content - if request.headers['Range'] =~ /bytes\=(\d+)\-(\d*)/ - begin_point = Regexp.last_match(1).to_i - end_point = Regexp.last_match(2).to_i if Regexp.last_match(2).present? - end + # Was it asked for just a part of the file? + if request.headers['Range'] + # indicate partial content + status 206 - end_point = file_size - 1 unless end_point < file_size - 1 + # extract part desired from the content + if request.headers['Range'] =~ /bytes\=(\d+)\-(\d*)/ + begin_point = Regexp.last_match(1).to_i + end_point = Regexp.last_match(2).to_i if Regexp.last_match(2).present? end - # Return the requested content - content_length = end_point - begin_point + 1 - header['Content-Range'] = "bytes #{begin_point}-#{end_point}/#{file_size}" - header['Content-Length'] = content_length.to_s - header['Accept-Ranges'] = 'bytes' - - # Read the binary data and return - result = IO.binread(prompt_path, content_length, begin_point) - result + end_point = file_size - 1 unless end_point < file_size - 1 end - end - desc 'Get a discussion comment student response' - params do - optional :as_attachment, type: Boolean, desc: 'Whether or not to download file as attachment. Default is false.' + # Return the requested content + content_length = end_point - begin_point + 1 + header['Content-Range'] = "bytes #{begin_point}-#{end_point}/#{file_size}" + header['Content-Length'] = content_length.to_s + header['Accept-Ranges'] = 'bytes' + + # Read the binary data and return + result = IO.binread(prompt_path, content_length, begin_point) + result end - get '/projects/:project_id/task_def_id/:task_definition_id/comments/:task_comment_id/discussion_comment/response' do - project = Project.find(params[:project_id]) - task_definition = project.unit.task_definitions.find(params[:task_definition_id]) + end - task = project.task_for_task_definition(task_definition) + desc 'Get a discussion comment student response' + params do + optional :as_attachment, type: Boolean, desc: 'Whether or not to download file as attachment. Default is false.' + end + get '/projects/:project_id/task_def_id/:task_definition_id/comments/:task_comment_id/discussion_comment/response' do + project = Project.find(params[:project_id]) + task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - unless authorise? current_user, task, :get_discussion - error!({ error: 'You cannot get this discussion prompt' }, 403) - end + task = project.task_for_task_definition(task_definition) - if project.has_task_for_task_definition? task_definition - task = project.task_for_task_definition(task_definition) - discussion_comment = task.all_comments.find(params[:task_comment_id]).becomes(DiscussionComment) + unless authorise? current_user, task, :get_discussion + error!({ error: 'You cannot get this discussion prompt' }, 403) + end - response_path = discussion_comment.reply_attachment_path + if project.has_task_for_task_definition? task_definition + task = project.task_for_task_definition(task_definition) + discussion_comment = task.all_comments.find(params[:task_comment_id]).becomes(DiscussionComment) - error!({ error: 'File missing' }, 404) unless File.exist? response_path - logger.info("#{current_user.username} - get discussion comment for task #{task.id} (#{task_definition.abbreviation})") + response_path = discussion_comment.reply_attachment_path - content_type('audio/wav; charset:binary') - env['api.format'] = :binary + error!({ error: 'File missing' }, 404) unless File.exist? response_path + logger.info("#{current_user.username} - get discussion comment for task #{task.id} (#{task_definition.abbreviation})") - # mark as attachment - if params[:as_attachment] - header['Content-Disposition'] = "attachment; filename=#{response_path}" - end + content_type('audio/wav; charset:binary') + env['api.format'] = :binary - # Work out what part to return - file_size = File.size(response_path) - begin_point = 0 - end_point = file_size - 1 + # mark as attachment + if params[:as_attachment] + header['Content-Disposition'] = "attachment; filename=#{response_path}" + end - # Was it asked for just a part of the file? - if request.headers['Range'] - # indicate partial content - status 206 + # Work out what part to return + file_size = File.size(response_path) + begin_point = 0 + end_point = file_size - 1 - # extract part desired from the content - if request.headers['Range'] =~ /bytes\=(\d+)\-(\d*)/ - begin_point = Regexp.last_match(1).to_i - end_point = Regexp.last_match(2).to_i if Regexp.last_match(2).present? - end + # Was it asked for just a part of the file? + if request.headers['Range'] + # indicate partial content + status 206 - end_point = file_size - 1 unless end_point < file_size - 1 + # extract part desired from the content + if request.headers['Range'] =~ /bytes\=(\d+)\-(\d*)/ + begin_point = Regexp.last_match(1).to_i + end_point = Regexp.last_match(2).to_i if Regexp.last_match(2).present? end - # Return the requested content - content_length = end_point - begin_point + 1 - header['Content-Range'] = "bytes #{begin_point}-#{end_point}/#{file_size}" - header['Content-Length'] = content_length.to_s - header['Accept-Ranges'] = 'bytes' - - # Read the binary data and return - result = IO.binread(response_path, content_length, begin_point) - result + end_point = file_size - 1 unless end_point < file_size - 1 end - end - desc 'Reply to a discussion comment of a task' - params do - requires :attachment, type: File, desc: 'discussion reply.' + # Return the requested content + content_length = end_point - begin_point + 1 + header['Content-Range'] = "bytes #{begin_point}-#{end_point}/#{file_size}" + header['Content-Length'] = content_length.to_s + header['Accept-Ranges'] = 'bytes' + + # Read the binary data and return + result = IO.binread(response_path, content_length, begin_point) + result end - post '/projects/:project_id/task_def_id/:task_definition_id/comments/:task_comment_id/discussion_comment/reply' do - project = Project.find(params[:project_id]) - task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - task = project.task_for_task_definition(task_definition) + end - unless authorise? current_user, task, :make_discussion_reply - error!({ error: 'Not authorised to reply to this discussion comment' }, 403) - end + desc 'Reply to a discussion comment of a task' + params do + requires :attachment, type: File, desc: 'discussion reply.' + end + post '/projects/:project_id/task_def_id/:task_definition_id/comments/:task_comment_id/discussion_comment/reply' do + project = Project.find(params[:project_id]) + task_definition = project.unit.task_definitions.find(params[:task_definition_id]) + task = project.task_for_task_definition(task_definition) - attached_file = params[:attachment] + unless authorise? current_user, task, :make_discussion_reply + error!({ error: 'Not authorised to reply to this discussion comment' }, 403) + end - if attached_file.present? - error!(error: 'Attachment is empty.') unless File.size?(attached_file["tempfile"].path).present? - error!(error: 'Attachment exceeds the maximum attachment size of 30MB.') unless File.size?(attached_file["tempfile"].path) < 30_000_000 - end + attached_file = params[:attachment] - logger.info("#{current_user.username} - added a reply to the discussion comment #{params[:discussion_comment_id]} for task #{task.id} (#{task_definition.abbreviation})") + if attached_file.present? + error!(error: 'Attachment is empty.') unless File.size?(attached_file["tempfile"].path).present? + error!(error: 'Attachment exceeds the maximum attachment size of 30MB.') unless File.size?(attached_file["tempfile"].path) < 30_000_000 + end - if attached_file.nil? || attached_file.empty? - error!({ error: 'Discussion reply is empty, unable to add new reply to discussion comment' }, 403) - end + logger.info("#{current_user.username} - added a reply to the discussion comment #{params[:discussion_comment_id]} for task #{task.id} (#{task_definition.abbreviation})") - discussion_comment = task.all_comments.find(params[:task_comment_id]) - # discussion_comment.mark_discussion_completed - # mark comment read for student - discussion_comment.mark_as_read(current_user, project.unit) + if attached_file.nil? || attached_file.empty? + error!({ error: 'Discussion reply is empty, unable to add new reply to discussion comment' }, 403) + end - error!({ error: 'No discussion comment found for the given task' }, 403) if discussion_comment.nil? + discussion_comment = task.all_comments.find(params[:task_comment_id]) + # discussion_comment.mark_discussion_completed + # mark comment read for student + discussion_comment.mark_as_read(current_user, project.unit) - result = discussion_comment.add_reply(attached_file) - nil - end + error!({ error: 'No discussion comment found for the given task' }, 403) if discussion_comment.nil? + + result = discussion_comment.add_reply(attached_file) + nil end end diff --git a/app/api/entities/activity_type_entity.rb b/app/api/entities/activity_type_entity.rb index 3dba441be..d878097c9 100644 --- a/app/api/entities/activity_type_entity.rb +++ b/app/api/entities/activity_type_entity.rb @@ -1,9 +1,7 @@ -module Api - module Entities - class ActivityTypeEntity < Grape::Entity - expose :id - expose :name - expose :abbreviation - end +module Entities + class ActivityTypeEntity < Grape::Entity + expose :id + expose :name + expose :abbreviation end end diff --git a/app/api/entities/break_entity.rb b/app/api/entities/break_entity.rb index 2ae2f0b64..f21c06e57 100644 --- a/app/api/entities/break_entity.rb +++ b/app/api/entities/break_entity.rb @@ -1,17 +1,15 @@ -module Api - module Entities - class BreakEntity < Grape::Entity - format_with(:date_only) do |date| - date.strftime('%Y-%m-%d') - end - - expose :id +module Entities + class BreakEntity < Grape::Entity + format_with(:date_only) do |date| + date.strftime('%Y-%m-%d') + end - with_options(format_with: :date_only) do - expose :start_date - end + expose :id - expose :number_of_weeks + with_options(format_with: :date_only) do + expose :start_date end + + expose :number_of_weeks end end diff --git a/app/api/entities/campus_entity.rb b/app/api/entities/campus_entity.rb index 88978eb25..ccb7d1e35 100644 --- a/app/api/entities/campus_entity.rb +++ b/app/api/entities/campus_entity.rb @@ -1,11 +1,9 @@ -module Api - module Entities - class CampusEntity < Grape::Entity - expose :id - expose :name - expose :mode - expose :abbreviation - expose :active - end +module Entities + class CampusEntity < Grape::Entity + expose :id + expose :name + expose :mode + expose :abbreviation + expose :active end end diff --git a/app/api/entities/comment_entity.rb b/app/api/entities/comment_entity.rb index 482639e86..014ddd8be 100644 --- a/app/api/entities/comment_entity.rb +++ b/app/api/entities/comment_entity.rb @@ -1,53 +1,51 @@ -module Api - module Entities - class CommentEntity < Grape::Entity - expose :id - expose :comment - expose :has_attachment do |data, options| - ["audio", "image", "pdf"].include?(data.content_type) - end - expose :type do |data, options| - data.content_type || "text" - end - expose :is_new do |data, options| - if !data.is_new.nil? - data.is_new != 0 - else - data.new_for?(options[:current_user]) - end +module Entities + class CommentEntity < Grape::Entity + expose :id + expose :comment + expose :has_attachment do |data, options| + ["audio", "image", "pdf"].include?(data.content_type) + end + expose :type do |data, options| + data.content_type || "text" + end + expose :is_new do |data, options| + if data.has_attribute?(:is_new) && data.is_new.present? + data.is_new != 0 + else + data.new_for?(options[:current_user]) end - expose :reply_to_id - expose :created_at - expose :recipient_read_time - expose :author do |data, options| - if data.author_id.present? - { - id: data.author_id, - name: "#{data.author_first_name} #{data.author_last_name}", - email: data.author_email - } - else - { - id: data.user_id, - name: data.user.name, - email: data.user.email - } - end + end + expose :reply_to_id + expose :created_at + expose :recipient_read_time, safe: true + expose :author do |data, options| + if data.has_attribute? :author_id + { + id: data.author_id, + name: "#{data.author_first_name} #{data.author_last_name}", + email: data.author_email + } + else + { + id: data.user_id, + name: data.user.name, + email: data.user.email + } end - expose :recipient do |data, options| - if data.recipient_id.present? - { - id: data.recipient_id, - name: "#{data.recipient_first_name} #{data.recipient_last_name}", - email: data.recipient_email - } - else - { - id: data.recipient_id, - name: data.recipient.name, - email: data.recipient.email - } - end + end + expose :recipient do |data, options| + if data.has_attribute? :recipient_first_name + { + id: data.recipient_id, + name: "#{data.recipient_first_name} #{data.recipient_last_name}", + email: data.recipient_email + } + else + { + id: data.recipient_id, + name: data.recipient.name, + email: data.recipient.email + } end end end diff --git a/app/api/entities/group_entity.rb b/app/api/entities/group_entity.rb index a5d7000de..1fc44a73a 100644 --- a/app/api/entities/group_entity.rb +++ b/app/api/entities/group_entity.rb @@ -1,13 +1,11 @@ -module Api - module Entities - class GroupEntity < Grape::Entity - expose :id - expose :name - expose :tutorial_id - expose :group_set_id - expose :student_count - expose :capacity_adjustment - expose :locked - end +module Entities + class GroupEntity < Grape::Entity + expose :id + expose :name + expose :tutorial_id + expose :group_set_id + expose :student_count #TODO: remove this and request it dynamically when needed + expose :capacity_adjustment + expose :locked end end diff --git a/app/api/entities/group_membership_entity.rb b/app/api/entities/group_membership_entity.rb index 0654d77cd..8af3eb4bb 100644 --- a/app/api/entities/group_membership_entity.rb +++ b/app/api/entities/group_membership_entity.rb @@ -1,8 +1,6 @@ -module Api - module Entities - class GroupMembershipEntity < Grape::Entity - expose :group_id - expose :project_id - end +module Entities + class GroupMembershipEntity < Grape::Entity + expose :group_id + expose :project_id end end diff --git a/app/api/entities/group_set_entity.rb b/app/api/entities/group_set_entity.rb index 20688d99f..4c4397348 100644 --- a/app/api/entities/group_set_entity.rb +++ b/app/api/entities/group_set_entity.rb @@ -1,13 +1,11 @@ -module Api - module Entities - class GroupSetEntity < Grape::Entity - expose :id - expose :name - expose :allow_students_to_create_groups - expose :allow_students_to_manage_groups - expose :keep_groups_in_same_class - expose :capacity - expose :locked - end +module Entities + class GroupSetEntity < Grape::Entity + expose :id + expose :name + expose :allow_students_to_create_groups + expose :allow_students_to_manage_groups + expose :keep_groups_in_same_class + expose :capacity + expose :locked end end diff --git a/app/api/entities/learning_outcome_entity.rb b/app/api/entities/learning_outcome_entity.rb index a3948baec..2467bd742 100644 --- a/app/api/entities/learning_outcome_entity.rb +++ b/app/api/entities/learning_outcome_entity.rb @@ -1,15 +1,9 @@ -# class LearningOutcomeSerializer < DoubtfireSerializer -# attributes :id, :ilo_number, :abbreviation, :name, :description -# end - -module Api - module Entities - class LearningOutcomeEntity < Grape::Entity - expose :id - expose :ilo_number - expose :abbreviation - expose :name - expose :description - end +module Entities + class LearningOutcomeEntity < Grape::Entity + expose :id + expose :ilo_number + expose :abbreviation + expose :name + expose :description end end diff --git a/app/api/entities/project_entity.rb b/app/api/entities/project_entity.rb index 6a001fa5e..6a917ed29 100644 --- a/app/api/entities/project_entity.rb +++ b/app/api/entities/project_entity.rb @@ -1,32 +1,33 @@ -module Api - module Entities - class ProjectEntity < Grape::Entity - expose :unit_id - expose :id, as: :project_id - expose :student_id do |project, options| - project.student.username - end - expose :campus_id - expose :student_name do |project, options| - "#{project.student.name}#{project.student.nickname.nil? ? '' : ' (' << project.student.nickname << ')'}" - end - expose :enrolled - expose :target_grade - expose :submitted_grade - expose :portfolio_files - expose :compile_portfolio - expose :portfolio_available - expose :uses_draft_learning_summary +module Entities + class ProjectEntity < Grape::Entity + expose :unit_id + expose :id, as: :project_id + expose :student_id do |project, options| + project.student.username + end + expose :campus_id + expose :student_name do |project, options| + "#{project.student.name}#{project.student.nickname.nil? ? '' : ' (' << project.student.nickname << ')'}" + end + expose :enrolled + expose :target_grade + expose :submitted_grade + expose :portfolio_files + expose :compile_portfolio + expose :portfolio_available + expose :uses_draft_learning_summary - expose :task_stats, as: :stats - expose :burndown_chart_data + expose :task_stats, as: :stats + expose :burndown_chart_data - expose :tasks do | project, options | - project.task_details_for_shallow_serializer(options[:user]) - end - expose :tutorial_enrolments, using: TutorialEnrolmentEntity - expose :groups, using: GroupEntity - expose :task_outcome_alignments, using: TaskOutcomeAlignmentEntity + expose :tasks do | project, options | + project.task_details_for_shallow_serializer(options[:user]) end + expose :tutorial_enrolments, using: TutorialEnrolmentEntity + expose :groups, using: GroupEntity + expose :task_outcome_alignments, using: TaskOutcomeAlignmentEntity + + expose :grade, if: :for_staff + expose :grade_rationale, if: :for_staff end end diff --git a/app/api/entities/task_definition_entity.rb b/app/api/entities/task_definition_entity.rb index f44a7b28d..4bfc5f8ed 100644 --- a/app/api/entities/task_definition_entity.rb +++ b/app/api/entities/task_definition_entity.rb @@ -1,58 +1,37 @@ -# class TaskDefinitionSerializer < DoubtfireSerializer -# attributes :id, :abbreviation, :name, :description, -# :weight, :target_grade, :target_date, -# :upload_requirements, -# :tutorial_stream, -# :plagiarism_checks, :plagiarism_report_url, :plagiarism_warn_pct, -# :restrict_status_updates, -# :group_set_id, :has_task_sheet?, :has_task_resources?, -# :due_date, :start_date, :is_graded, :max_quality_pts - -# def weight -# object.weighting -# end - -# def tutorial_stream -# object.tutorial_stream.abbreviation unless object.tutorial_stream.nil? -# end -# end - -module Api - module Entities - class TaskDefinitionEntity < Grape::Entity - format_with(:date_only) do |date| - date.strftime('%Y-%m-%d') - end +module Entities + class TaskDefinitionEntity < Grape::Entity + format_with(:date_only) do |date| + date.strftime('%Y-%m-%d') + end - expose :id - expose :abbreviation - expose :name - expose :description - expose :weighting, as: :weight - expose :target_grade + expose :id + expose :abbreviation + expose :name + expose :description + expose :weighting, as: :weight + expose :target_grade - with_options(format_with: :date_only) do - expose :target_date - expose :due_date - expose :start_date - end + with_options(format_with: :date_only) do + expose :target_date + expose :due_date + expose :start_date + end - expose :upload_requirements - expose :tutorial_stream do |tutorial, options| - tutorial.tutorial_stream.abbreviation unless tutorial.tutorial_stream.nil? - end - expose :plagiarism_checks - expose :plagiarism_report_url - expose :plagiarism_warn_pct - expose :restrict_status_updates - expose :group_set_id - expose :has_task_sheet?, as: :has_task_sheet - expose :has_task_resources?, as: :has_task_resources - expose :has_task_assessment_resources?, as: :has_task_assessment_resources - expose :is_graded - expose :max_quality_pts - expose :overseer_image_id - expose :assessment_enabled + expose :upload_requirements + expose :tutorial_stream do |tutorial, options| + tutorial.tutorial_stream.abbreviation unless tutorial.tutorial_stream.nil? end + expose :plagiarism_checks + expose :plagiarism_report_url + expose :plagiarism_warn_pct + expose :restrict_status_updates + expose :group_set_id + expose :has_task_sheet?, as: :has_task_sheet + expose :has_task_resources?, as: :has_task_resources + expose :has_task_assessment_resources?, as: :has_task_assessment_resources + expose :is_graded + expose :max_quality_pts + expose :overseer_image_id + expose :assessment_enabled end end diff --git a/app/api/entities/task_entity.rb b/app/api/entities/task_entity.rb index 00bc4df07..3b0271cd5 100644 --- a/app/api/entities/task_entity.rb +++ b/app/api/entities/task_entity.rb @@ -1,35 +1,43 @@ -module Api - module Entities - class TaskEntity < Grape::Entity - expose :id - expose :project_id - expose :task_definition_id +module Entities + class TaskEntity < Grape::Entity + expose :id + expose :project_id + expose :task_definition_id - expose :status + expose :status - expose :due_date - expose :extensions + expose :due_date + expose :extensions - expose :submission_date - expose :completion_date + expose :submission_date + expose :completion_date - expose :times_assessed - expose :grade - expose :quality_pts + expose :times_assessed + expose :grade + expose :quality_pts - expose :include_in_portfolio + expose :include_in_portfolio - expose :pct_similar, unless: :update_only - expose :similar_to_count, unless: :update_only - expose :similar_to_dismissed_count, unless: :update_only + # Attributes excluded from update only - expose :num_new_comments, unless: :update_only + expose :pct_similar, unless: :update_only + expose :similar_to_count, unless: :update_only + expose :similar_to_dismissed_count, unless: :update_only - expose :other_projects, if: :include_other_projects do |task, options| - if task.group_task? && !task.group.nil? - grp = task.group - grp.projects.select { |p| p.id != task.project_id }.map { |p| { id: p.id, new_stats: p.task_stats } } - end + expose :num_new_comments, unless: :update_only + + # Attributes only included in "update only" + + expose :new_stats, if: :update_only do |task, options| + task.project.task_stats + end + + # Attributes only included if include other projects + + expose :other_projects, if: :include_other_projects do |task, options| + if task.group_task? && !task.group.nil? + grp = task.group + grp.projects.select { |p| p.id != task.project_id }.map { |p| { id: p.id, new_stats: p.task_stats } } end end end diff --git a/app/api/entities/task_outcome_alignment_entity.rb b/app/api/entities/task_outcome_alignment_entity.rb index 781c76368..3e7cd9a84 100644 --- a/app/api/entities/task_outcome_alignment_entity.rb +++ b/app/api/entities/task_outcome_alignment_entity.rb @@ -1,12 +1,10 @@ -module Api - module Entities - class TaskOutcomeAlignmentEntity < Grape::Entity - expose :id - expose :description - expose :rating - expose :learning_outcome_id - expose :task_definition_id - expose :task_id - end +module Entities + class TaskOutcomeAlignmentEntity < Grape::Entity + expose :id + expose :description + expose :rating + expose :learning_outcome_id + expose :task_definition_id + expose :task_id end end diff --git a/app/api/entities/teaching_period_entity.rb b/app/api/entities/teaching_period_entity.rb new file mode 100644 index 000000000..8d6067b06 --- /dev/null +++ b/app/api/entities/teaching_period_entity.rb @@ -0,0 +1,17 @@ +module Entities + class TeachingPeriodEntity < Grape::Entity + expose :id + expose :period + expose :year + expose :start_date + expose :end_date + expose :active_until + expose :active do |teaching_period, options| + object.active_until > DateTime.now + end + expose :breaks, if: :full_details, using: Entities::BreakEntity + expose :units, if: :full_details do |teaching_period, options| + Entities::UnitEntity.represent teaching_period.units, only: [:id, :name, :code, :active] + end + end +end diff --git a/app/api/entities/tutorial_enrolment_entity.rb b/app/api/entities/tutorial_enrolment_entity.rb index 6e62cd4b5..6c1f1dc39 100644 --- a/app/api/entities/tutorial_enrolment_entity.rb +++ b/app/api/entities/tutorial_enrolment_entity.rb @@ -1,9 +1,7 @@ -module Api - module Entities - class TutorialEnrolmentEntity < Grape::Entity - expose :id - expose :project_id - expose :tutorial_id - end +module Entities + class TutorialEnrolmentEntity < Grape::Entity + expose :id + expose :project_id + expose :tutorial_id end end diff --git a/app/api/entities/tutorial_entity.rb b/app/api/entities/tutorial_entity.rb index e77790e49..624f57ff0 100644 --- a/app/api/entities/tutorial_entity.rb +++ b/app/api/entities/tutorial_entity.rb @@ -1,56 +1,18 @@ -# class TutorialSerializer < DoubtfireSerializer -# attributes :id, :meeting_day, :meeting_time, :meeting_location, :abbreviation, :campus_id, :capacity, :num_students, -# :tutorial_stream - -# def tutorial_stream -# object.tutorial_stream.abbreviation unless object.tutorial_stream.nil? -# end - -# def meeting_time -# object.meeting_time.to_time -# # DateTime.parse("#{object.meeting_time}") -# end - -# has_one :tutor, serializer: ShallowUserSerializer - -# def include_tutor? -# if Thread.current[:user] -# my_role = object.unit.role_for(Thread.current[:user]) -# [ Role.convenor, Role.admin ].include? my_role -# end -# end - -# def include_num_students? -# if Thread.current[:user] -# my_role = object.unit.role_for(Thread.current[:user]) -# [ Role.convenor, Role.tutor, Role.admin ].include? my_role -# end -# end - -# def filter(keys) -# keys.delete :num_students unless include_num_students? -# keys -# end -# end - - -module Api - module Entities - class TutorialEntity < Grape::Entity - expose :id - expose :meeting_day - expose :meeting_time # ?? should we use: tutorial.meeting_time.to_time - expose :meeting_location - expose :abbreviation - expose :campus_id - expose :capacity - expose :tutorial_stream do |tutorial, options| - tutorial.tutorial_stream.abbreviation unless tutorial.tutorial_stream.nil? - end - expose :num_students - expose :tutor do |tutorial, options| - Api::Entities::UserEntity.represent tutorial.tutor, only: [:id, :name] - end +module Entities + class TutorialEntity < Grape::Entity + expose :id + expose :meeting_day + expose :meeting_time # ?? should we use: tutorial.meeting_time.to_time + expose :meeting_location + expose :abbreviation + expose :campus_id + expose :capacity + expose :tutorial_stream do |tutorial, options| + tutorial.tutorial_stream.abbreviation unless tutorial.tutorial_stream.nil? + end + expose :num_students #TODO: remove this and request it dynamically when needed + expose :tutor do |tutorial, options| + Entities::UserEntity.represent tutorial.tutor, only: [:id, :name] end end end diff --git a/app/api/entities/tutorial_stream_entity.rb b/app/api/entities/tutorial_stream_entity.rb index 9c6f9c7ba..586b00d2b 100644 --- a/app/api/entities/tutorial_stream_entity.rb +++ b/app/api/entities/tutorial_stream_entity.rb @@ -1,20 +1,10 @@ -# class TutorialStreamSerializer < DoubtfireSerializer -# attributes :id, :name, :abbreviation, :activity_type - -# def activity_type -# object.activity_type.abbreviation -# end -# end - -module Api - module Entities - class TutorialStreamEntity < Grape::Entity - expose :id - expose :name - expose :abbreviation - expose :activity_type do |stream, options| - stream.activity_type.abbreviation - end +module Entities + class TutorialStreamEntity < Grape::Entity + expose :id + expose :name + expose :abbreviation + expose :activity_type do |stream, options| + stream.activity_type.abbreviation #TODO: cache all activities in the client and just send the code end end end diff --git a/app/api/entities/unit_entity.rb b/app/api/entities/unit_entity.rb index 0d3fea0f7..434e26be3 100644 --- a/app/api/entities/unit_entity.rb +++ b/app/api/entities/unit_entity.rb @@ -1,135 +1,55 @@ -# class ShallowUnitSerializer < DoubtfireSerializer -# attributes :code, :id, :name, :teaching_period_id, :start_date, :end_date, :active -# end - -# class UnitSerializer < DoubtfireSerializer -# attributes :code, :id, :name, :my_role, :main_convenor_id, :description, :teaching_period_id, :start_date, :end_date, :active, :convenors, :ilos, :auto_apply_extension_before_deadline, :send_notifications, :enable_sync_enrolments, :enable_sync_timetable, :group_memberships, :draft_task_definition_id, :allow_student_extension_requests, :extension_weeks_on_resubmit_request, :allow_student_change_tutorial - -# def start_date -# object.start_date.to_date -# end - -# def end_date -# object.end_date.to_date -# end - -# def my_role_obj -# object.role_for(Thread.current[:user]) if Thread.current[:user] -# end - -# def my_user_role -# Thread.current[:user].role if Thread.current[:user] -# end - -# def role -# role = my_role_obj -# role.name unless role.nil? -# end - -# def my_role -# role -# end - -# def ilos -# object.learning_outcomes -# end - -# def main_convenor_id -# object.main_convenor.id -# end - -# has_many :tutorial_streams -# has_many :tutorials -# has_many :tutorial_enrolments -# has_many :task_definitions -# has_many :convenors, serializer: UserUnitRoleSerializer -# has_many :staff, serializer: UserUnitRoleSerializer -# has_many :group_sets, serializer: GroupSetSerializer -# has_many :ilos, serializer: LearningOutcomeSerializer -# has_many :task_outcome_alignments, serializer: LearningOutcomeTaskLinkSerializer -# has_many :groups, serializer: GroupSerializer - -# def group_memberships -# ActiveModel::ArraySerializer.new(object.group_memberships.where(active: true), each_serializer: GroupMembershipSerializer) -# end - -# def include_convenors? -# ([ Role.convenor, :convenor ].include? my_role_obj) || (my_user_role == Role.admin) -# end - -# def include_staff? -# ([ Role.convenor, :convenor, Role.tutor, :tutor ].include? my_role_obj) || (my_user_role == Role.admin) -# end - -# def include_groups? -# ([ Role.convenor, :convenor, Role.tutor, :tutor ].include? my_role_obj) || (my_user_role == Role.admin) -# end - -# def include_enrolments? -# ([ Role.convenor, :convenor, Role.tutor, :tutor ].include? my_role_obj) || (my_user_role == Role.admin) -# end - -# def filter(keys) -# keys.delete :groups unless include_groups? -# keys.delete :convenors unless include_convenors? -# keys.delete :staff unless include_staff? -# keys.delete :tutorial_enrolments unless include_enrolments? -# keys -# end -# end - - -module Api - module Entities - class UnitEntity < Grape::Entity - format_with(:date_only) do |date| - date.strftime('%Y-%m-%d') - end - - expose :code - expose :id - expose :name - expose :my_role do |unit, options| - role = unit.role_for(options[:user]) - role.name unless role.nil? - end - expose :main_convenor_id - expose :description - expose :teaching_period_id - - with_options(format_with: :date_only) do - expose :start_date - expose :end_date - end +module Entities + class UnitEntity < Grape::Entity + format_with(:date_only) do |date| + date.strftime('%Y-%m-%d') + end - expose :active + def is_staff?(user, unit) + [ Role.convenor_id, Role.tutor_id, Role.admin_id ].include? unit.role_for(user).id + end - expose :overseer_image_id - expose :assessment_enabled + expose :code + expose :id + expose :name + expose :my_role do |unit, options| + role = unit.role_for(options[:user]) + role.name unless role.nil? + end + expose :main_convenor_id + expose :description + expose :teaching_period_id, expose_nil: false - expose :auto_apply_extension_before_deadline - expose :send_notifications - expose :enable_sync_enrolments - expose :enable_sync_timetable - expose :draft_task_definition_id - expose :allow_student_extension_requests - expose :extension_weeks_on_resubmit_request - expose :allow_student_change_tutorial + with_options(format_with: :date_only) do + expose :start_date + expose :end_date + end - expose :learning_outcomes, using: LearningOutcomeEntity, as: :ilos - expose :tutorial_streams, using: TutorialStreamEntity - expose :tutorials, using: TutorialEntity - expose :tutorial_enrolments, using: TutorialEnrolmentEntity, if: lambda { |unit, options| - ([ Role.convenor, :convenor, Role.tutor, :tutor ].include? unit.role_for(options[:user])) || (options[:user].role_id == Role.admin_id) - } - expose :task_definitions, using: TaskDefinitionEntity - expose :task_outcome_alignments, using: TaskOutcomeAlignmentEntity - expose :staff, using: UnitRoleEntity - expose :group_sets, using: GroupSetEntity - expose :groups, using: GroupEntity - expose :group_memberships, using: GroupMembershipEntity do |unit, options| - unit.group_memberships.where(active: true) - end + expose :active + + expose :overseer_image_id, unless: :summary_only + expose :assessment_enabled, unless: :summary_only + + expose :auto_apply_extension_before_deadline, unless: :summary_only, if: lambda { |unit, options| is_staff?(options[:user], unit) } + expose :send_notifications, unless: :summary_only, if: lambda { |unit, options| is_staff?(options[:user], unit) } + expose :enable_sync_enrolments, unless: :summary_only, if: lambda { |unit, options| is_staff?(options[:user], unit) } + expose :enable_sync_timetable, unless: :summary_only, if: lambda { |unit, options| is_staff?(options[:user], unit) } + expose :draft_task_definition_id, unless: :summary_only, if: lambda { |unit, options| is_staff?(options[:user], unit) } + expose :allow_student_extension_requests, unless: :summary_only + expose :extension_weeks_on_resubmit_request, unless: :summary_only, if: lambda { |unit, options| is_staff?(options[:user], unit) } + expose :allow_student_change_tutorial, unless: :summary_only + + expose :learning_outcomes, using: LearningOutcomeEntity, as: :ilos, unless: :summary_only + expose :tutorial_streams, using: TutorialStreamEntity, unless: :summary_only + expose :tutorials, using: TutorialEntity, unless: :summary_only + expose :tutorial_enrolments, using: TutorialEnrolmentEntity, unless: :summary_only, if: lambda { |unit, options| is_staff?(options[:user], unit) } + + expose :task_definitions, using: TaskDefinitionEntity, unless: :summary_only + expose :task_outcome_alignments, using: TaskOutcomeAlignmentEntity, unless: :summary_only + expose :staff, using: UnitRoleEntity, unless: :summary_only + expose :group_sets, using: GroupSetEntity, unless: :summary_only + expose :groups, using: GroupEntity, unless: :summary_only + expose :group_memberships, using: GroupMembershipEntity, unless: :summary_only do |unit, options| + unit.group_memberships.where(active: true) end end end diff --git a/app/api/entities/unit_role_entity.rb b/app/api/entities/unit_role_entity.rb index 99a99cda7..52a857c24 100644 --- a/app/api/entities/unit_role_entity.rb +++ b/app/api/entities/unit_role_entity.rb @@ -1,11 +1,19 @@ -module Api - module Entities - class UnitRoleEntity < Grape::Entity - expose :id - expose :role do |unit_role, options| unit_role.role.name end - expose :user_id - expose :name do |unit_role, options| unit_role.user.name end - expose :email do |unit_role, options| unit_role.user.email end - end +module Entities + class UnitRoleEntity < Grape::Entity + expose :id + expose :role do |unit_role, options| unit_role.role.name end + expose :user_id + expose :name do |unit_role, options| unit_role.user.name end + expose :email do |unit_role, options| unit_role.user.email end + expose :unit_id, unless: :in_unit + end + + class UnitRoleWithUnitEntity < UnitRoleEntity + expose :unit_code do |unit_role, options| unit_role.unit.code end + expose :unit_name do |unit_role, options| unit_role.unit.name end + expose :start_date do |unit_role, options| unit_role.unit.start_date end + expose :end_date do |unit_role, options| unit_role.unit.end_date end + expose :teaching_period_id do |unit_role, options| unit_role.unit.teaching_period_id end + expose :active do |unit_role, options| unit_role.unit.active end end end diff --git a/app/api/entities/user_entity.rb b/app/api/entities/user_entity.rb index 7f275b155..a8791038a 100644 --- a/app/api/entities/user_entity.rb +++ b/app/api/entities/user_entity.rb @@ -1,42 +1,22 @@ -# class UserSerializer < ActiveModel::Serializer -# attributes :id, :student_id, :email, :name, :first_name, :last_name, :username, :nickname, :system_role, :receive_task_notifications, :receive_portfolio_notifications, :receive_feedback_notifications, :opt_in_to_research, :has_run_first_time_setup - -# def system_role -# object.object.role.name if object.object.role -# end -# end - -# class ShallowUserSerializer < ActiveModel::Serializer -# attributes :id, :name, :email, :student_id -# end - -# class ShallowTutorSerializer < ActiveModel::Serializer -# attributes :id, :name, :email -# end - - -module Api - module Entities - class UserEntity < Grape::Entity - expose :id - expose :student_id - expose :email - expose :name - expose :first_name - expose :last_name - expose :username - expose :nickname - expose :receive_task_notifications - expose :receive_portfolio_notifications - expose :receive_feedback_notifications - expose :opt_in_to_research - expose :has_run_first_time_setup - - expose :system_role do |user, options| - user.role.name if user.role.present? - end +module Entities + class UserEntity < Grape::Entity + expose :id + expose :student_id + expose :email + expose :name + expose :first_name + expose :last_name + expose :username + expose :nickname + expose :receive_task_notifications + expose :receive_portfolio_notifications + expose :receive_feedback_notifications + expose :opt_in_to_research + expose :has_run_first_time_setup + + expose :system_role do |user, options| + user.role.name if user.role.present? end end end - diff --git a/app/api/entities/webcal_entity.rb b/app/api/entities/webcal_entity.rb new file mode 100644 index 000000000..d316324d2 --- /dev/null +++ b/app/api/entities/webcal_entity.rb @@ -0,0 +1,27 @@ + +module Entities + class WebcalEntity < Grape::Entity + expose :id, expose_nil: false + expose :guid, expose_nil: false + expose :include_start_dates, expose_nil: false + + expose :enabled do |webcal, options| + webcal.present? + end + + expose :reminder, expose_nil: false do |webcal, options| + if webcal.nil? || webcal.reminder_time.nil? || webcal.reminder_unit.nil? + nil + else + { + time: webcal.reminder_time, + unit: webcal.reminder_unit + } + end + end + + expose :unit_exclusions do |webcal, options| + webcal.webcal_unit_exclusions.map(&:unit_id) + end + end +end diff --git a/app/api/extension_comments_api.rb b/app/api/extension_comments_api.rb index 285b4826c..7617b3d3d 100644 --- a/app/api/extension_comments_api.rb +++ b/app/api/extension_comments_api.rb @@ -1,61 +1,58 @@ require 'grape' -module Api - class ExtensionCommentsApi < Grape::API - helpers AuthenticationHelpers - helpers AuthorisationHelpers - - desc 'Request an extension for a task' - params do - requires :comment, type: String, desc: 'The details of the request' - requires :weeks_requested, type: Integer, desc: 'The details of the request' +class ExtensionCommentsApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers + + desc 'Request an extension for a task' + params do + requires :comment, type: String, desc: 'The details of the request' + requires :weeks_requested, type: Integer, desc: 'The details of the request' + end + post '/projects/:project_id/task_def_id/:task_definition_id/request_extension' do + project = Project.find(params[:project_id]) + task_definition = project.unit.task_definitions.find(params[:task_definition_id]) + task = project.task_for_task_definition(task_definition) + + # check permissions using specific permission has with addition of request extension if allowed in unit + unless authorise? current_user, task, :request_extension, ->(role, perm_hash, other) { task.specific_permission_hash(role, perm_hash, other) } + error!({ error: 'Not authorised to request an extension for this task' }, 403) end - post '/projects/:project_id/task_def_id/:task_definition_id/request_extension' do - project = Project.find(params[:project_id]) - task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - task = project.task_for_task_definition(task_definition) - - # check permissions using specific permission has with addition of request extension if allowed in unit - unless authorise? current_user, task, :request_extension, ->(role, perm_hash, other) { task.specific_permission_hash(role, perm_hash, other) } - error!({ error: 'Not authorised to request an extension for this task' }, 403) - end - error!({error:'Extension weeks can not be 0.'}, 403) if params[:weeks_requested] == 0 + error!({error:'Extension weeks can not be 0.'}, 403) if params[:weeks_requested] == 0 - max_duration = task.weeks_can_extend - duration = params[:weeks_requested] - duration = max_duration unless params[:weeks_requested] <= max_duration + max_duration = task.weeks_can_extend + duration = params[:weeks_requested] + duration = max_duration unless params[:weeks_requested] <= max_duration - error!({error:'Extensions cannot be granted beyond task deadline.'}, 403) if duration <= 0 + error!({error:'Extensions cannot be granted beyond task deadline.'}, 403) if duration <= 0 - result = task.apply_for_extension(current_user, params[:comment], duration) - result.serialize(current_user) - end + result = task.apply_for_extension(current_user, params[:comment], duration) + present result.serialize(current_user), Grape::Presenters::Presenter + end - desc 'Assess an extension for a task' - params do - requires :granted, type: Boolean, desc: 'Assess an extension' - end - put '/projects/:project_id/task_def_id/:task_definition_id/assess_extension/:task_comment_id' do - project = Project.find(params[:project_id]) - task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - task = project.task_for_task_definition(task_definition) + desc 'Assess an extension for a task' + params do + requires :granted, type: Boolean, desc: 'Assess an extension' + end + put '/projects/:project_id/task_def_id/:task_definition_id/assess_extension/:task_comment_id' do + project = Project.find(params[:project_id]) + task_definition = project.unit.task_definitions.find(params[:task_definition_id]) + task = project.task_for_task_definition(task_definition) - unless authorise? current_user, task, :assess_extension - error!({ error: 'Not authorised to assess an extension for this task' }, 403) - end + unless authorise? current_user, task, :assess_extension + error!({ error: 'Not authorised to assess an extension for this task' }, 403) + end - task_comment = task.all_comments.find(params[:task_comment_id]).becomes(ExtensionComment) + task_comment = task.all_comments.find(params[:task_comment_id]).becomes(ExtensionComment) - unless task_comment.assess_extension(current_user, params[:granted]) - if task_comment.errors.count >= 1 - error!({error: task_comment.errors.full_messages.first}, 403) - else - error!({error: 'Error saving extension'}, 403) - end + unless task_comment.assess_extension(current_user, params[:granted]) + if task_comment.errors.count >= 1 + error!({error: task_comment.errors.full_messages.first}, 403) + else + error!({error: 'Error saving extension'}, 403) end - task_comment.serialize(current_user) end - + present task_comment.serialize(current_user), Grape::Presenters::Presenter end end diff --git a/app/api/group_sets_api.rb b/app/api/group_sets_api.rb index 7bdb85033..d37d5bc97 100644 --- a/app/api/group_sets_api.rb +++ b/app/api/group_sets_api.rb @@ -1,460 +1,456 @@ require 'grape' -require 'mime-check-helpers' -require 'group_serializer' - -module Api - # - # Allow GroupSets to be managed via the API - # - class GroupSetsApi < Grape::API - helpers AuthenticationHelpers - helpers AuthorisationHelpers - helpers MimeCheckHelpers - helpers LogHelper - - before do - authenticated? - end - - # ------------------------------------------------------------------------ - # Group Sets - # ------------------------------------------------------------------------ - - desc 'Add a new group set to the given unit' - params do - requires :unit_id, type: Integer, desc: 'The unit for the new group set' - requires :group_set, type: Hash do - requires :name, type: String, desc: 'The name of this group set' - optional :allow_students_to_create_groups, type: Boolean, desc: 'Are students allowed to create groups' - optional :allow_students_to_manage_groups, type: Boolean, desc: 'Are students allowed to manage their group memberships' - optional :keep_groups_in_same_class, type: Boolean, desc: 'Must groups be kept in the one class' - optional :capacity, type: Integer, desc: 'Capacity for each group' - end - end - post '/units/:unit_id/group_sets' do - unit = Unit.find(params[:unit_id]) - unless authorise? current_user, unit, :update - error!({ error: 'Not authorised to create a group set for this unit' }, 403) - end - logger.info "Create group set: #{current_user.username} in #{unit.code} from #{request.ip}" - - group_params = ActionController::Parameters.new(params) - .require(:group_set) - .permit( - :name, - :allow_students_to_create_groups, - :allow_students_to_manage_groups, - :keep_groups_in_same_class, - :capacity - ) - - group_set = GroupSet.create!(group_params) - group_set.unit = unit - group_set.save! - present group_set, with: Api::Entities::GroupSetEntity - end - - desc 'Edits the given group set' - params do - requires :id, type: Integer, desc: 'The group set id to edit' - requires :group_set, type: Hash do - optional :name, type: String, desc: 'The name of this group set' - optional :allow_students_to_create_groups, type: Boolean, desc: 'Are students allowed to create groups' - optional :allow_students_to_manage_groups, type: Boolean, desc: 'Are students allowed to manage their group memberships' - optional :keep_groups_in_same_class, type: Boolean, desc: 'Must groups be kept in the one class' - optional :capacity, type: Integer, desc: 'Capacity for each group' - optional :locked, type: Boolean, desc: 'Is this group set locked' - end - end - put '/units/:unit_id/group_sets/:id' do - group_set = GroupSet.find(params[:id]) - unit = Unit.find(params[:unit_id]) +# +# Allow GroupSets to be managed via the API +# +class GroupSetsApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers + helpers MimeCheckHelpers + helpers LogHelper + + before do + authenticated? + end - logger.info "Edit group set: #{current_user.username} in #{unit.code} from #{request.ip}" + # ------------------------------------------------------------------------ + # Group Sets + # ------------------------------------------------------------------------ + + desc 'Add a new group set to the given unit' + params do + requires :unit_id, type: Integer, desc: 'The unit for the new group set' + requires :group_set, type: Hash do + requires :name, type: String, desc: 'The name of this group set' + optional :allow_students_to_create_groups, type: Boolean, desc: 'Are students allowed to create groups' + optional :allow_students_to_manage_groups, type: Boolean, desc: 'Are students allowed to manage their group memberships' + optional :keep_groups_in_same_class, type: Boolean, desc: 'Must groups be kept in the one class' + optional :capacity, type: Integer, desc: 'Capacity for each group' + end + end + post '/units/:unit_id/group_sets' do + unit = Unit.find(params[:unit_id]) + unless authorise? current_user, unit, :update + error!({ error: 'Not authorised to create a group set for this unit' }, 403) + end - if group_set.unit != unit - error!({ error: 'Unable to locate group set for unit' }, 404) - end + logger.info "Create group set: #{current_user.username} in #{unit.code} from #{request.ip}" + + group_params = ActionController::Parameters.new(params) + .require(:group_set) + .permit( + :name, + :allow_students_to_create_groups, + :allow_students_to_manage_groups, + :keep_groups_in_same_class, + :capacity + ) + + group_set = GroupSet.create(group_params) + group_set.unit = unit + group_set.save! + present group_set, with: Entities::GroupSetEntity + end - unless authorise? current_user, unit, :update - error!({ error: 'Not authorised to update group set for this unit' }, 403) - end + desc 'Edits the given group set' + params do + requires :id, type: Integer, desc: 'The group set id to edit' + requires :group_set, type: Hash do + optional :name, type: String, desc: 'The name of this group set' + optional :allow_students_to_create_groups, type: Boolean, desc: 'Are students allowed to create groups' + optional :allow_students_to_manage_groups, type: Boolean, desc: 'Are students allowed to manage their group memberships' + optional :keep_groups_in_same_class, type: Boolean, desc: 'Must groups be kept in the one class' + optional :capacity, type: Integer, desc: 'Capacity for each group' + optional :locked, type: Boolean, desc: 'Is this group set locked' + end + end + put '/units/:unit_id/group_sets/:id' do + group_set = GroupSet.find(params[:id]) + unit = Unit.find(params[:unit_id]) - group_params = ActionController::Parameters.new(params) - .require(:group_set) - .permit( - :name, - :allow_students_to_create_groups, - :allow_students_to_manage_groups, - :keep_groups_in_same_class, - :capacity, - :locked, - ) + logger.info "Edit group set: #{current_user.username} in #{unit.code} from #{request.ip}" - group_set.update!(group_params) - present group_set, with: Api::Entities::GroupSetEntity + if group_set.unit != unit + error!({ error: 'Unable to locate group set for unit' }, 404) end - desc 'Delete a group set' - delete '/units/:unit_id/group_sets/:id' do - group_set = GroupSet.find(params[:id]) - unit = Unit.find(params[:unit_id]) + unless authorise? current_user, unit, :update + error!({ error: 'Not authorised to update group set for this unit' }, 403) + end - logger.info "Delete group set: #{current_user.username} in #{unit.code} from #{request.ip}" + group_params = ActionController::Parameters.new(params) + .require(:group_set) + .permit( + :name, + :allow_students_to_create_groups, + :allow_students_to_manage_groups, + :keep_groups_in_same_class, + :capacity, + :locked, + ) + + group_set.update!(group_params) + present group_set, with: Entities::GroupSetEntity + end - if group_set.unit != unit - error!({ error: 'Unable to locate group set for unit' }, 404) - end + desc 'Delete a group set' + delete '/units/:unit_id/group_sets/:id' do + group_set = GroupSet.find(params[:id]) + unit = Unit.find(params[:unit_id]) - unless authorise? current_user, unit, :update - error!({ error: 'Not authorised to delete group set for this unit' }, 403) - end + logger.info "Delete group set: #{current_user.username} in #{unit.code} from #{request.ip}" - error!(error: group_set.errors[:base].last) unless group_set.destroy - present true, with: Grape::Presenters::Presenter + if group_set.unit != unit + error!({ error: 'Unable to locate group set for unit' }, 404) end - # ------------------------------------------------------------------------ - # Groups - # ------------------------------------------------------------------------ + unless authorise? current_user, unit, :update + error!({ error: 'Not authorised to delete group set for this unit' }, 403) + end - desc 'Get the groups in a group set' - get '/units/:unit_id/group_sets/:id/groups' do - unit = Unit.find(params[:unit_id]) - group_set = unit.group_sets.find(params[:id]) + error!(error: group_set.errors[:base].last) unless group_set.destroy + present true, with: Grape::Presenters::Presenter + end - unless authorise? current_user, group_set, :get_groups, ->(role, perm_hash, other) { group_set.specific_permission_hash(role, perm_hash, other) } - error!({ error: 'Not authorised to get groups for this unit' }, 403) - end + # ------------------------------------------------------------------------ + # Groups + # ------------------------------------------------------------------------ - result = group_set. - groups. - joins('LEFT OUTER JOIN group_memberships ON group_memberships.group_id = groups.id AND group_memberships.active = TRUE'). - group( - 'groups.id', - 'groups.name', - 'groups.tutorial_id', - 'groups.group_set_id', - 'groups.capacity_adjustment', - 'groups.locked', - ). - select( - 'groups.id as id', - 'groups.name as name', - 'groups.tutorial_id as tutorial_id', - 'groups.group_set_id as group_set_id', - 'groups.capacity_adjustment as capacity_adjustment', - 'groups.locked as locked', - 'COUNT(group_memberships.id) as student_count' - ) - present result, with: Grape::Presenters::Presenter - end - - desc 'Download a CSV of groups and their students in a group set' - get '/units/:unit_id/group_sets/:group_set_id/groups/student_csv' do - unit = Unit.find(params[:unit_id]) - group_set = unit.group_sets.find(params[:group_set_id]) - - unless authorise? current_user, unit, :update - error!({ error: 'Not authorised to download csv of groups for this unit' }, 403) - end + desc 'Get the groups in a group set' + get '/units/:unit_id/group_sets/:id/groups' do + unit = Unit.find(params[:unit_id]) + group_set = unit.group_sets.find(params[:id]) - content_type 'application/octet-stream' - header['Content-Disposition'] = "attachment; filename=#{unit.code}-#{group_set.name}-student-groups.csv " - env['api.format'] = :binary - unit.export_student_groups_to_csv(group_set) + unless authorise? current_user, group_set, :get_groups, ->(role, perm_hash, other) { group_set.specific_permission_hash(role, perm_hash, other) } + error!({ error: 'Not authorised to get groups for this unit' }, 403) end - desc 'Download a CSV of groups in a group set' - get '/units/:unit_id/group_sets/:group_set_id/groups/csv' do - unit = Unit.find(params[:unit_id]) - group_set = unit.group_sets.find(params[:group_set_id]) + result = group_set. + groups. + joins('LEFT OUTER JOIN group_memberships ON group_memberships.group_id = groups.id AND group_memberships.active = TRUE'). + group( + 'groups.id', + 'groups.name', + 'groups.tutorial_id', + 'groups.group_set_id', + 'groups.capacity_adjustment', + 'groups.locked', + ). + select( + 'groups.id as id', + 'groups.name as name', + 'groups.tutorial_id as tutorial_id', + 'groups.group_set_id as group_set_id', + 'groups.capacity_adjustment as capacity_adjustment', + 'groups.locked as locked', + 'COUNT(group_memberships.id) as student_count' + ) + present result, with: Grape::Presenters::Presenter + end - unless authorise? current_user, unit, :update - error!({ error: 'Not authorised to download csv of groups for this unit' }, 403) - end + desc 'Download a CSV of groups and their students in a group set' + get '/units/:unit_id/group_sets/:group_set_id/groups/student_csv' do + unit = Unit.find(params[:unit_id]) + group_set = unit.group_sets.find(params[:group_set_id]) - content_type 'application/octet-stream' - header['Content-Disposition'] = "attachment; filename=#{unit.code}-#{group_set.name}-groups.csv " - env['api.format'] = :binary - unit.export_groups_to_csv(group_set) + unless authorise? current_user, unit, :update + error!({ error: 'Not authorised to download csv of groups for this unit' }, 403) end - desc "Add a new group to the given unit's group_set" - params do - requires :unit_id, type: Integer, desc: 'The unit for the new group' - requires :group_set_id, type: Integer, desc: 'The id of the group set' - requires :group, type: Hash do - optional :name, type: String, desc: 'The name of this group' - requires :tutorial_id, type: Integer, desc: 'The id of the tutorial for the group' - optional :capacity_adjustment, type: Integer, desc: 'How capacity for group is adjusted', default: 0 - end + content_type 'application/octet-stream' + header['Content-Disposition'] = "attachment; filename=#{unit.code}-#{group_set.name}-student-groups.csv " + env['api.format'] = :binary + unit.export_student_groups_to_csv(group_set) + end + + desc 'Download a CSV of groups in a group set' + get '/units/:unit_id/group_sets/:group_set_id/groups/csv' do + unit = Unit.find(params[:unit_id]) + group_set = unit.group_sets.find(params[:group_set_id]) + + unless authorise? current_user, unit, :update + error!({ error: 'Not authorised to download csv of groups for this unit' }, 403) end - post '/units/:unit_id/group_sets/:group_set_id/groups' do - unit = Unit.find(params[:unit_id]) - group_set = unit.group_sets.find(params[:group_set_id]) - tutorial = unit.tutorials.find(params[:group][:tutorial_id]) - unless authorise? current_user, group_set, :create_group, ->(role, perm_hash, other) { group_set.specific_permission_hash(role, perm_hash, other) } - error!({ error: 'Not authorised to create a group set for this unit' }, 403) - end + content_type 'application/octet-stream' + header['Content-Disposition'] = "attachment; filename=#{unit.code}-#{group_set.name}-groups.csv " + env['api.format'] = :binary + unit.export_groups_to_csv(group_set) + end - group_params = ActionController::Parameters.new(params) - .require(:group) - .permit( - :name, - :capacity_adjustment - ) + desc "Add a new group to the given unit's group_set" + params do + requires :unit_id, type: Integer, desc: 'The unit for the new group' + requires :group_set_id, type: Integer, desc: 'The id of the group set' + requires :group, type: Hash do + optional :name, type: String, desc: 'The name of this group' + requires :tutorial_id, type: Integer, desc: 'The id of the tutorial for the group' + optional :capacity_adjustment, type: Integer, desc: 'How capacity for group is adjusted', default: 0 + end + end + post '/units/:unit_id/group_sets/:group_set_id/groups' do + unit = Unit.find(params[:unit_id]) + group_set = unit.group_sets.find(params[:group_set_id]) + tutorial = unit.tutorials.find(params[:group][:tutorial_id]) - # Group with the same name - unless group_set.groups.where(name: group_params[:name]).empty? - error!({ error: "This group name is not unique to the #{group_set.name} group set." }, 403) - end + unless authorise? current_user, group_set, :create_group, ->(role, perm_hash, other) { group_set.specific_permission_hash(role, perm_hash, other) } + error!({ error: 'Not authorised to create a group set for this unit' }, 403) + end - # Now check if they are a student... - project = nil - if unit.role_for(current_user) == Role.student - project = unit.active_projects.find_by(user_id: current_user.id) - # They cannot already be in a group for this group set - error!({error: "You are already in a group for #{group_set.name}"}, 403) unless project.group_for_groupset(group_set).nil? - end + group_params = ActionController::Parameters.new(params) + .require(:group) + .permit( + :name, + :capacity_adjustment + ) - num = group_set.groups.count + 1 - while group_params[:name].nil? || group_params[:name].empty? || group_set.groups.where(name: group_params[:name]).count > 0 - group_params[:name] = "Group #{num}" - num += 1 - end - grp = Group.create(name: group_params[:name], group_set: group_set, tutorial: tutorial) - grp.save! + # Group with the same name + unless group_set.groups.where(name: group_params[:name]).empty? + error!({ error: "This group name is not unique to the #{group_set.name} group set." }, 403) + end - # If they are a student, then add them to the group they created - if project.present? - grp.add_member(project) - end + # Now check if they are a student... + project = nil + if unit.role_for(current_user) == Role.student + project = unit.active_projects.find_by(user_id: current_user.id) + # They cannot already be in a group for this group set + error!({error: "You are already in a group for #{group_set.name}"}, 403) unless project.group_for_groupset(group_set).nil? + end - present grp, with: Api::Entities::GroupEntity + num = group_set.groups.count + 1 + while group_params[:name].nil? || group_params[:name].empty? || group_set.groups.where(name: group_params[:name]).count > 0 + group_params[:name] = "Group #{num}" + num += 1 end + grp = Group.create(name: group_params[:name], group_set: group_set, tutorial: tutorial) + grp.save! - desc 'Upload a CSV for groups in a group set' - params do - requires :unit_id, type: Integer, desc: 'The unit for the new group' - requires :group_set_id, type: Integer, desc: 'The id of the group set' - requires :file, type: File, desc: 'CSV upload file.' + # If they are a student, then add them to the group they created + if project.present? + grp.add_member(project) end - post '/units/:unit_id/group_sets/:group_set_id/groups/csv' do - # check mime is correct before uploading - ensure_csv!(params[:file][:tempfile]) - unit = Unit.find(params[:unit_id]) - group_set = unit.group_sets.find(params[:group_set_id]) + present grp, with: Entities::GroupEntity + end - unless authorise? current_user, unit, :update - error!({ error: 'Not authorised to upload csv of groups for this unit' }, 403) - end + desc 'Upload a CSV for groups in a group set' + params do + requires :unit_id, type: Integer, desc: 'The unit for the new group' + requires :group_set_id, type: Integer, desc: 'The id of the group set' + requires :file, type: File, desc: 'CSV upload file.' + end + post '/units/:unit_id/group_sets/:group_set_id/groups/csv' do + # check mime is correct before uploading + ensure_csv!(params[:file][:tempfile]) - present unit.import_groups_from_csv(group_set, params[:file][:tempfile]), with: Grape.Presenters.Presenter - end + unit = Unit.find(params[:unit_id]) + group_set = unit.group_sets.find(params[:group_set_id]) - desc 'Upload a CSV for students in groups in a group set' - params do - requires :unit_id, type: Integer, desc: 'The unit for the new group' - requires :group_set_id, type: Integer, desc: 'The id of the group set' - requires :file, type: File, desc: 'CSV upload file.' + unless authorise? current_user, unit, :update + error!({ error: 'Not authorised to upload csv of groups for this unit' }, 403) end - post '/units/:unit_id/group_sets/:group_set_id/groups/student_csv' do - # check mime is correct before uploading - ensure_csv!(params[:file][:tempfile]) - unit = Unit.find(params[:unit_id]) - group_set = unit.group_sets.find(params[:group_set_id]) + present unit.import_groups_from_csv(group_set, params[:file][:tempfile]), with: Grape.Presenters.Presenter + end - unless authorise? current_user, unit, :update - error!({ error: 'Not authorised to upload csv of groups for this unit' }, 403) - end + desc 'Upload a CSV for students in groups in a group set' + params do + requires :unit_id, type: Integer, desc: 'The unit for the new group' + requires :group_set_id, type: Integer, desc: 'The id of the group set' + requires :file, type: File, desc: 'CSV upload file.' + end + post '/units/:unit_id/group_sets/:group_set_id/groups/student_csv' do + # check mime is correct before uploading + ensure_csv!(params[:file][:tempfile]) + + unit = Unit.find(params[:unit_id]) + group_set = unit.group_sets.find(params[:group_set_id]) - present unit.import_student_groups_from_csv(group_set, params[:file][:tempfile]), with: Grape.Presenters.Presenter + unless authorise? current_user, unit, :update + error!({ error: 'Not authorised to upload csv of groups for this unit' }, 403) end - desc 'Edits the given group' - params do - requires :unit_id, type: Integer, desc: 'The unit for the new group' - requires :group_set_id, type: Integer, desc: 'The id of the group set' - requires :group_id, type: Integer, desc: 'The id of the group' - requires :group, type: Hash do - optional :name, type: String, desc: 'The name of this group set' - optional :tutorial_id, type: Integer, desc: 'Tutorial of the group' - optional :capacity_adjustment, type: Integer, desc: 'How capacity for group is adjusted' - optional :locked, type: Boolean, desc: 'Is the group locked' - end + present unit.import_student_groups_from_csv(group_set, params[:file][:tempfile]), with: Grape.Presenters.Presenter + end + + desc 'Edits the given group' + params do + requires :unit_id, type: Integer, desc: 'The unit for the new group' + requires :group_set_id, type: Integer, desc: 'The id of the group set' + requires :group_id, type: Integer, desc: 'The id of the group' + requires :group, type: Hash do + optional :name, type: String, desc: 'The name of this group set' + optional :tutorial_id, type: Integer, desc: 'Tutorial of the group' + optional :capacity_adjustment, type: Integer, desc: 'How capacity for group is adjusted' + optional :locked, type: Boolean, desc: 'Is the group locked' end - put '/units/:unit_id/group_sets/:group_set_id/groups/:group_id' do - unit = Unit.find(params[:unit_id]) - gs = unit.group_sets.find(params[:group_set_id]) - grp = gs.groups.find(params[:group_id]) + end + put '/units/:unit_id/group_sets/:group_set_id/groups/:group_id' do + unit = Unit.find(params[:unit_id]) + gs = unit.group_sets.find(params[:group_set_id]) + grp = gs.groups.find(params[:group_id]) - unless authorise? current_user, grp, :manage_group, ->(role, perm_hash, other) { grp.specific_permission_hash(role, perm_hash, other) } - error!({ error: 'Not authorised to update this group' }, 403) - end + unless authorise? current_user, grp, :manage_group, ->(role, perm_hash, other) { grp.specific_permission_hash(role, perm_hash, other) } + error!({ error: 'Not authorised to update this group' }, 403) + end - group_params = ActionController::Parameters.new(params) - .require(:group) - .permit( - :name, - :tutorial_id, - :capacity_adjustment, - :locked, - ) - - # Allow locking only if the current user has permission to do so - if params[:group][:locked].present? && params[:group][:locked] != grp.locked - unless authorise? current_user, grp, :lock_group - error!({ error: "Not authorised to #{grp.locked ? 'unlock' : 'lock'} this group" }, 403) - end - end + group_params = ActionController::Parameters.new(params) + .require(:group) + .permit( + :name, + :tutorial_id, + :capacity_adjustment, + :locked, + ) - # Switching tutorials will violate any existing group members - if params[:group][:tutorial_id].present? && params[:group][:tutorial_id] != grp.tutorial_id - if authorise? current_user, grp, :move_tutorial - tutorial = unit.tutorials.find_by(id: params[:group][:tutorial_id]) - begin - grp.switch_to_tutorial tutorial - rescue StandardError => e - error!({ error: e.message }, 403) - end - else - error!({ error: 'You are not authorised to change the tutorial of this group' }, 403) - end + # Allow locking only if the current user has permission to do so + if params[:group][:locked].present? && params[:group][:locked] != grp.locked + unless authorise? current_user, grp, :lock_group + error!({ error: "Not authorised to #{grp.locked ? 'unlock' : 'lock'} this group" }, 403) end + end - if params[:group][:capacity_adjustment].present? && params[:group][:capacity_adjustment] != grp.capacity_adjustment - if authorise? current_user, grp, :move_tutorial - group_params[:capacity_adjustment] = params[:group][:capacity_adjustment] - else - error!({ error: 'You are not authorised to change the capacity of this group' }, 403) + # Switching tutorials will violate any existing group members + if params[:group][:tutorial_id].present? && params[:group][:tutorial_id] != grp.tutorial_id + if authorise? current_user, grp, :move_tutorial + tutorial = unit.tutorials.find_by(id: params[:group][:tutorial_id]) + begin + grp.switch_to_tutorial tutorial + rescue StandardError => e + error!({ error: e.message }, 403) end + else + error!({ error: 'You are not authorised to change the tutorial of this group' }, 403) end - - grp.update!(group_params) - present grp, with: Api::Entities::GroupEntity end - desc 'Delete a group' - params do - requires :unit_id, type: Integer, desc: 'The unit for the new group' - requires :group_set_id, type: Integer, desc: 'The id of the group set' - requires :group_id, type: Integer, desc: 'The id of the group' + if params[:group][:capacity_adjustment].present? && params[:group][:capacity_adjustment] != grp.capacity_adjustment + if authorise? current_user, grp, :move_tutorial + group_params[:capacity_adjustment] = params[:group][:capacity_adjustment] + else + error!({ error: 'You are not authorised to change the capacity of this group' }, 403) + end end - delete '/units/:unit_id/group_sets/:group_set_id/groups/:group_id' do - unit = Unit.find(params[:unit_id]) - gs = unit.group_sets.find(params[:group_set_id]) - grp = gs.groups.find(params[:group_id]) - unless authorise? current_user, grp, :manage_group, ->(role, perm_hash, other) { grp.specific_permission_hash(role, perm_hash, other) } - error!({ error: 'Not authorised to delete group set for this unit' }, 403) - end + grp.update!(group_params) + present grp, with: Entities::GroupEntity + end - unless unit.tutors.include? current_user - # check that they are the only member of the group, or the group is empty - error!({ error: 'You cannot delete a group with members' }, 403) unless grp.projects.count <= 1 - error!({ error: 'You cannot delete this group' }, 403) unless grp.projects.count.zero? || grp.projects.first.student == current_user - end + desc 'Delete a group' + params do + requires :unit_id, type: Integer, desc: 'The unit for the new group' + requires :group_set_id, type: Integer, desc: 'The id of the group set' + requires :group_id, type: Integer, desc: 'The id of the group' + end + delete '/units/:unit_id/group_sets/:group_set_id/groups/:group_id' do + unit = Unit.find(params[:unit_id]) + gs = unit.group_sets.find(params[:group_set_id]) + grp = gs.groups.find(params[:group_id]) - error!(error: grp.errors[:base].last) unless grp.destroy - true + unless authorise? current_user, grp, :manage_group, ->(role, perm_hash, other) { grp.specific_permission_hash(role, perm_hash, other) } + error!({ error: 'Not authorised to delete group set for this unit' }, 403) end - desc 'Get the members of a group' - get '/units/:unit_id/group_sets/:group_set_id/groups/:group_id/members' do - unit = Unit.find(params[:unit_id]) - group_set = unit.group_sets.find(params[:group_set_id]) - grp = group_set.groups.find(params[:group_id]) + unless unit.tutors.include? current_user + # check that they are the only member of the group, or the group is empty + error!({ error: 'You cannot delete a group with members' }, 403) unless grp.projects.count <= 1 + error!({ error: 'You cannot delete this group' }, 403) unless grp.projects.count.zero? || grp.projects.first.student == current_user + end - unless authorise? current_user, grp, :get_members, ->(role, perm_hash, other) { grp.specific_permission_hash(role, perm_hash, other) } - error!({ error: 'Not authorised to get groups for this unit' }, 403) - end + error!(error: grp.errors[:base].last) unless grp.destroy + true + end - present grp.projects, with: Api::Entities::ProjectEntity, only: [:student_id, :project_id, :student_name, :target_grade], user: current_user - end + desc 'Get the members of a group' + get '/units/:unit_id/group_sets/:group_set_id/groups/:group_id/members' do + unit = Unit.find(params[:unit_id]) + group_set = unit.group_sets.find(params[:group_set_id]) + grp = group_set.groups.find(params[:group_id]) - desc 'Add a group member' - params do - requires :unit_id, type: Integer, desc: 'The unit for the new group' - requires :group_set_id, type: Integer, desc: 'The id of the group set' - requires :group_id, type: Integer, desc: 'The id of the group' - requires :project_id, type: Integer, desc: 'The project id of the member' + unless authorise? current_user, grp, :get_members, ->(role, perm_hash, other) { grp.specific_permission_hash(role, perm_hash, other) } + error!({ error: 'Not authorised to get groups for this unit' }, 403) end - post '/units/:unit_id/group_sets/:group_set_id/groups/:group_id/members' do - unit = Unit.find(params[:unit_id]) - gs = unit.group_sets.find(params[:group_set_id]) - grp = gs.groups.find(params[:group_id]) - prj = unit.projects.find(params[:project_id]) + present grp.projects, with: Entities::ProjectEntity, only: [:student_id, :project_id, :student_name, :target_grade], user: current_user + end - unless authorise? current_user, gs, :join_group, ->(role, perm_hash, other) { gs.specific_permission_hash(role, perm_hash, other) } - if gs.locked - error!({ error: 'All of these groups are now locked' }, 403) - else - error!({ error: 'Not authorised to manage this group' }, 403) - end - end + desc 'Add a group member' + params do + requires :unit_id, type: Integer, desc: 'The unit for the new group' + requires :group_set_id, type: Integer, desc: 'The id of the group set' + requires :group_id, type: Integer, desc: 'The id of the group' + requires :project_id, type: Integer, desc: 'The project id of the member' + end + post '/units/:unit_id/group_sets/:group_set_id/groups/:group_id/members' do + unit = Unit.find(params[:unit_id]) + gs = unit.group_sets.find(params[:group_set_id]) + grp = gs.groups.find(params[:group_id]) - unless authorise? current_user, prj, :get - error!({ error: 'Not authorised to manage this student' }, 403) - end + prj = unit.projects.find(params[:project_id]) - if gs.keep_groups_in_same_class && !prj.enrolled_in?(grp.tutorial) - error!({ error: "Students from the tutorial '#{grp.tutorial.abbreviation}' can only be added to this group." }, 403) - end - - if grp.active_group_members.find_by(project: prj, active: true) - error!({ error: "#{prj.student.name} is already a member of this group" }, 403) + unless authorise? current_user, gs, :join_group, ->(role, perm_hash, other) { gs.specific_permission_hash(role, perm_hash, other) } + if gs.locked + error!({ error: 'All of these groups are now locked' }, 403) + else + error!({ error: 'Not authorised to manage this group' }, 403) end + end - if grp.locked - error!({ error: 'Group is locked, no additional members can be added'}, 403) - end + unless authorise? current_user, prj, :get + error!({ error: 'Not authorised to manage this student' }, 403) + end - if grp.at_capacity? && ! authorise?(current_user, grp, :can_exceed_capacity) - error!({ error: 'Group is at capacity, no additional members can be added'}, 403) - end + if gs.keep_groups_in_same_class && !prj.enrolled_in?(grp.tutorial) + error!({ error: "Students from the tutorial '#{grp.tutorial.abbreviation}' can only be added to this group." }, 403) + end - gm = grp.add_member(prj) + if grp.active_group_members.find_by(project: prj, active: true) + error!({ error: "#{prj.student.name} is already a member of this group" }, 403) + end - present prj, with: Api::Entities::ProjectEntity, only: [:student_id, :project_id, :student_name, :target_grade], user: current_user + if grp.locked + error!({ error: 'Group is locked, no additional members can be added'}, 403) end - desc 'Remove a group member' - params do - requires :unit_id, type: Integer, desc: 'The unit for the new group' - requires :group_set_id, type: Integer, desc: 'The id of the group set' - requires :group_id, type: Integer, desc: 'The id of the group' - requires :id, type: Integer, desc: 'The project id of the member' + if grp.at_capacity? && ! authorise?(current_user, grp, :can_exceed_capacity) + error!({ error: 'Group is at capacity, no additional members can be added'}, 403) end - delete '/units/:unit_id/group_sets/:group_set_id/groups/:group_id/members/:id' do - unit = Unit.find(params[:unit_id]) - gs = unit.group_sets.find(params[:group_set_id]) - grp = gs.groups.find(params[:group_id]) - prj = grp.projects.find(params[:id]) - unless authorise? current_user, grp, :manage_group, ->(role, perm_hash, other) { grp.specific_permission_hash(role, perm_hash, other) } - if grp.locked || gs.locked - error!({ error: 'This group is locked' }, 403) - else - error!({ error: 'Not authorised to manage this group' }, 403) - end - end + gm = grp.add_member(prj) - unless authorise? current_user, prj, :get - error!({ error: 'Not authorised to manage this student' }, 403) - end + present prj, with: Entities::ProjectEntity, only: [:student_id, :project_id, :student_name, :target_grade], user: current_user + end - if grp.active_group_members.find_by(project: prj).nil? - error!({ error: "#{prj.student.name} is not a member of this group" }, 403) + desc 'Remove a group member' + params do + requires :unit_id, type: Integer, desc: 'The unit for the new group' + requires :group_set_id, type: Integer, desc: 'The id of the group set' + requires :group_id, type: Integer, desc: 'The id of the group' + requires :id, type: Integer, desc: 'The project id of the member' + end + delete '/units/:unit_id/group_sets/:group_set_id/groups/:group_id/members/:id' do + unit = Unit.find(params[:unit_id]) + gs = unit.group_sets.find(params[:group_set_id]) + grp = gs.groups.find(params[:group_id]) + prj = grp.projects.find(params[:id]) + + unless authorise? current_user, grp, :manage_group, ->(role, perm_hash, other) { grp.specific_permission_hash(role, perm_hash, other) } + if grp.locked || gs.locked + error!({ error: 'This group is locked' }, 403) + else + error!({ error: 'Not authorised to manage this group' }, 403) end + end - grp.remove_member(prj) - true + unless authorise? current_user, prj, :get + error!({ error: 'Not authorised to manage this student' }, 403) end + + if grp.active_group_members.find_by(project: prj).nil? + error!({ error: "#{prj.student.name} is not a member of this group" }, 403) + end + + grp.remove_member(prj) + true end end diff --git a/app/api/learning_alignment_api.rb b/app/api/learning_alignment_api.rb index 89202354f..c83ddd5fc 100644 --- a/app/api/learning_alignment_api.rb +++ b/app/api/learning_alignment_api.rb @@ -1,236 +1,236 @@ require 'grape' -module Api - class LearningAlignmentApi < Grape::API - helpers AuthenticationHelpers - helpers AuthorisationHelpers - helpers MimeCheckHelpers - - before do - authenticated? - end +class LearningAlignmentApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers + helpers MimeCheckHelpers - desc 'Get the task/outcome alignment details for a unit or a project' - params do - requires :unit_id, type: Integer, desc: 'The id of the unit' - optional :project_id, type: Integer, desc: 'The id of the student project to get the alignment from' - end - get '/units/:unit_id/learning_alignments' do - unit = Unit.find(params[:unit_id]) + before do + authenticated? + end - unless authorise?(current_user, unit, :get_unit) - error!({ error: 'You are not authorised to access this unit.' }, 403) - end + desc 'Get the task/outcome alignment details for a unit or a project' + params do + requires :unit_id, type: Integer, desc: 'The id of the unit' + optional :project_id, type: Integer, desc: 'The id of the student project to get the alignment from' + end + get '/units/:unit_id/learning_alignments' do + unit = Unit.find(params[:unit_id]) - if params[:project_id].nil? - return unit.task_outcome_alignments - else - proj = unit.projects.find(params[:project_id]) - unless authorise?(current_user, proj, :get) - error!({ error: 'You are not authorised to access this project.' }, 403) - end - return proj.task_outcome_alignments + unless authorise?(current_user, unit, :get_unit) + error!({ error: 'You are not authorised to access this unit.' }, 403) + end + + if params[:project_id].nil? + present unit.task_outcome_alignments, with: Entities::TaskOutcomeAlignmentEntity + else + proj = unit.projects.find(params[:project_id]) + unless authorise?(current_user, proj, :get) + error!({ error: 'You are not authorised to access this project.' }, 403) end + present proj.task_outcome_alignments, with: Entities::TaskOutcomeAlignmentEntity end + end + + desc 'Download CSV of task alignments in this unit' + params do + requires :unit_id, type: Integer, desc: 'The id of the unit' + optional :project_id, type: Integer, desc: 'The id of the student project to get the alignment from' + end + get '/units/:unit_id/learning_alignments/csv' do + unit = Unit.find(params[:unit_id]) - desc 'Download CSV of task alignments in this unit' - params do - requires :unit_id, type: Integer, desc: 'The id of the unit' - optional :project_id, type: Integer, desc: 'The id of the student project to get the alignment from' + unless authorise?(current_user, unit, :get_unit) + error!({ error: 'You are not authorised to access this unit.' }, 403) end - get '/units/:unit_id/learning_alignments/csv' do - unit = Unit.find(params[:unit_id]) - unless authorise?(current_user, unit, :get_unit) - error!({ error: 'You are not authorised to access this unit.' }, 403) + if params[:project_id].nil? + unless authorise? current_user, unit, :download_unit_csv + error!({ error: "Not authorised to download CSV of task alignment in #{unit.code}" }, 403) end - if params[:project_id].nil? - unless authorise? current_user, unit, :download_unit_csv - error!({ error: "Not authorised to download CSV of task alignment in #{unit.code}" }, 403) - end - - content_type 'application/octet-stream' - header['Content-Disposition'] = "attachment; filename=#{unit.code}-Alignment.csv " - env['api.format'] = :binary - unit.export_task_alignment_to_csv - else - proj = unit.projects.find(params[:project_id]) - unless authorise?(current_user, proj, :get) - error!({ error: 'You are not authorised to access this project.' }, 403) - end - - content_type 'application/octet-stream' - header['Content-Disposition'] = "attachment; filename=#{unit.code}-#{proj.student.name}-Task-Alignment.csv " - env['api.format'] = :binary - - proj.export_task_alignment_to_csv + content_type 'application/octet-stream' + header['Content-Disposition'] = "attachment; filename=#{unit.code}-Alignment.csv " + env['api.format'] = :binary + unit.export_task_alignment_to_csv + else + proj = unit.projects.find(params[:project_id]) + unless authorise?(current_user, proj, :get) + error!({ error: 'You are not authorised to access this project.' }, 403) end - end - desc 'Upload CSV of task to outcome alignments' - params do - requires :file, type: File, desc: 'CSV upload file.' - optional :project_id, type: Integer, desc: 'The id of the student project to upload the alignment to' + content_type 'application/octet-stream' + header['Content-Disposition'] = "attachment; filename=#{unit.code}-#{proj.student.name}-Task-Alignment.csv " + env['api.format'] = :binary + + proj.export_task_alignment_to_csv end - post '/units/:unit_id/learning_alignments/csv' do - ensure_csv!(params[:file][:tempfile]) + end - unit = Unit.find(params[:unit_id]) + desc 'Upload CSV of task to outcome alignments' + params do + requires :file, type: File, desc: 'CSV upload file.' + optional :project_id, type: Integer, desc: 'The id of the student project to upload the alignment to' + end + post '/units/:unit_id/learning_alignments/csv' do + ensure_csv!(params[:file][:tempfile]) - unless authorise?(current_user, unit, :get_unit) - error!({ error: 'You are not authorised to access this unit.' }, 403) - end + unit = Unit.find(params[:unit_id]) - if params[:project_id].nil? - unless authorise? current_user, unit, :upload_csv - error!({ error: "Not authorised to upload CSV of task alignment to #{unit.code}" }, 403) - end + unless authorise?(current_user, unit, :get_unit) + error!({ error: 'You are not authorised to access this unit.' }, 403) + end - # Actually import... - unit.import_task_alignment_from_csv(params[:file][:tempfile], nil) - else - proj = unit.projects.find(params[:project_id]) - unless authorise?(current_user, proj, :make_submission) - error!({ error: 'You are not authorised to access this project.' }, 403) - end + if params[:project_id].nil? + unless authorise? current_user, unit, :upload_csv + error!({ error: "Not authorised to upload CSV of task alignment to #{unit.code}" }, 403) + end - unit.import_task_alignment_from_csv(params[:file][:tempfile], proj) + # Actually import... + unit.import_task_alignment_from_csv(params[:file][:tempfile], nil) + else + proj = unit.projects.find(params[:project_id]) + unless authorise?(current_user, proj, :make_submission) + error!({ error: 'You are not authorised to access this project.' }, 403) end - end - desc "Add an outcome to a unit's task definition" - params do - requires :unit_id, type: Integer, desc: 'The id of the unit' - requires :learning_outcome_id, type: Integer, desc: 'The id of the learning outcome' - requires :task_definition_id, type: Integer, desc: 'The id of the task definition' - optional :project_id, type: Integer, desc: 'The id of the project if this is a self reflection' - requires :description, type: String, desc: 'The ILO''s description' - requires :rating, type: Integer, desc: 'The rating for this link, indicating the strength of this alignment' + unit.import_task_alignment_from_csv(params[:file][:tempfile], proj) end - post '/units/:unit_id/learning_alignments' do - unit = Unit.find(params[:unit_id]) + end - # if there is no project -- then this is a unit LO link - # so need to check the user is authorised to update the unit... - if params[:project_id].nil? && !authorise?(current_user, unit, :update) - error!({ error: 'You are not authorised to create task alignments in this unit.' }, 403) - end + desc "Add an outcome to a unit's task definition" + params do + requires :unit_id, type: Integer, desc: 'The id of the unit' + requires :learning_outcome_id, type: Integer, desc: 'The id of the learning outcome' + requires :task_definition_id, type: Integer, desc: 'The id of the task definition' + optional :project_id, type: Integer, desc: 'The id of the project if this is a self reflection' + requires :description, type: String, desc: 'The ILO''s description' + requires :rating, type: Integer, desc: 'The rating for this link, indicating the strength of this alignment' + end + post '/units/:unit_id/learning_alignments' do + unit = Unit.find(params[:unit_id]) - unit.learning_outcomes.find(params[:learning_outcome_id]) + # if there is no project -- then this is a unit LO link + # so need to check the user is authorised to update the unit... + if params[:project_id].nil? && !authorise?(current_user, unit, :update) + error!({ error: 'You are not authorised to create task alignments in this unit.' }, 403) + end - task_def = unit.task_definitions.find(params[:task_definition_id]) + unit.learning_outcomes.find(params[:learning_outcome_id]) - link_parameters = ActionController::Parameters.new(params) - .permit( - :task_definition_id, - :learning_outcome_id, - :description, - :rating - ) + task_def = unit.task_definitions.find(params[:task_definition_id]) - unless params[:project_id].nil? - project = unit.projects.find(params[:project_id]) - task = project.task_for_task_definition(task_def) + link_parameters = ActionController::Parameters.new(params) + .permit( + :task_definition_id, + :learning_outcome_id, + :description, + :rating + ) - unless authorise?(current_user, task, :make_submission) - error!({ error: 'You are not authorised to create outcome alignments for this task.' }, 403) - end + unless params[:project_id].nil? + project = unit.projects.find(params[:project_id]) + task = project.task_for_task_definition(task_def) - link_parameters[:task_id] = task.id + unless authorise?(current_user, task, :make_submission) + error!({ error: 'You are not authorised to create outcome alignments for this task.' }, 403) end - LearningOutcomeTaskLink.create! link_parameters + link_parameters[:task_id] = task.id end - desc 'Update the alignment between a task and unit outcome' - params do - requires :id, type: Integer, desc: 'The id of the task alignment' - requires :unit_id, type: Integer, desc: 'The id of the unit' - optional :description, type: String, desc: 'The description of the alignment' - optional :rating, type: Integer, desc: 'The rating for this link, indicating the strength of this alignment' - end - put '/units/:unit_id/learning_alignments/:id' do - unit = Unit.find(params[:unit_id]) + result = LearningOutcomeTaskLink.create! link_parameters + present result, with: Entities::TaskOutcomeAlignmentEntity + end - align = unit.learning_outcome_task_links.find(params[:id]) + desc 'Update the alignment between a task and unit outcome' + params do + requires :id, type: Integer, desc: 'The id of the task alignment' + requires :unit_id, type: Integer, desc: 'The id of the unit' + optional :description, type: String, desc: 'The description of the alignment' + optional :rating, type: Integer, desc: 'The rating for this link, indicating the strength of this alignment' + end + put '/units/:unit_id/learning_alignments/:id' do + unit = Unit.find(params[:unit_id]) - link_parameters = ActionController::Parameters.new(params) - .permit( - :description, - :rating - ) + align = unit.learning_outcome_task_links.find(params[:id]) - # is this a project task alignment update? - if !align.task_id.nil? - task = align.task + link_parameters = ActionController::Parameters.new(params) + .permit( + :description, + :rating + ) - unless authorise?(current_user, task, :make_submission) - error!({ error: 'You are not authorised to update outcome alignments for this task.' }, 403) - end + # is this a project task alignment update? + if !align.task_id.nil? + task = align.task - # else, this is a unit alignment update! - elsif !authorise?(current_user, unit, :update) - error!({ error: 'You are not authorised to update the task alignments in this unit.' }, 403) + unless authorise?(current_user, task, :make_submission) + error!({ error: 'You are not authorised to update outcome alignments for this task.' }, 403) end - align.update(link_parameters) - align.save! + # else, this is a unit alignment update! + elsif !authorise?(current_user, unit, :update) + error!({ error: 'You are not authorised to update the task alignments in this unit.' }, 403) end - desc 'Delete the alignment between a task and unit outcome' - params do - requires :id, type: Integer, desc: 'The id of the task alignment' - requires :unit_id, type: Integer, desc: 'The id of the unit' - end - delete '/units/:unit_id/learning_alignments/:id' do - unit = Unit.find(params[:unit_id]) + align.update(link_parameters) + align.save! + present align, with: Entities::TaskOutcomeAlignmentEntity + end - align = unit.learning_outcome_task_links.find(params[:id]) + desc 'Delete the alignment between a task and unit outcome' + params do + requires :id, type: Integer, desc: 'The id of the task alignment' + requires :unit_id, type: Integer, desc: 'The id of the unit' + end + delete '/units/:unit_id/learning_alignments/:id' do + unit = Unit.find(params[:unit_id]) - # is this a project task alignment update? - if !align.task_id.nil? - task = align.task + align = unit.learning_outcome_task_links.find(params[:id]) - unless authorise?(current_user, task, :make_submission) - error!({ error: 'You are not authorised to update outcome alignments for this task.' }, 403) - end + # is this a project task alignment update? + if !align.task_id.nil? + task = align.task - # else, this is a unit alignment update! - elsif !authorise?(current_user, unit, :update) - error!({ error: 'You are not authorised to update the task alignments in this unit.' }, 403) + unless authorise?(current_user, task, :make_submission) + error!({ error: 'You are not authorised to update outcome alignments for this task.' }, 403) end - align.destroy! - nil + # else, this is a unit alignment update! + elsif !authorise?(current_user, unit, :update) + error!({ error: 'You are not authorised to update the task alignments in this unit.' }, 403) end - desc 'Return unit learning alignment median values' - params do - requires :unit_id, type: Integer, desc: 'The id of the unit' - end - get '/units/:unit_id/learning_alignments/class_stats' do - unit = Unit.find(params[:unit_id]) + align.destroy! + nil + end - unless authorise?(current_user, unit, :get_unit) - error!({ error: 'You are not authorised to access these task alignments.' }, 403) - end + desc 'Return unit learning alignment median values' + params do + requires :unit_id, type: Integer, desc: 'The id of the unit' + end + get '/units/:unit_id/learning_alignments/class_stats' do + unit = Unit.find(params[:unit_id]) - unit.ilo_progress_class_stats + unless authorise?(current_user, unit, :get_unit) + error!({ error: 'You are not authorised to access these task alignments.' }, 403) end - desc 'Return unit learning alignment values with median stats for each tutorial' - params do - requires :unit_id, type: Integer, desc: 'The id of the unit' - end - get '/units/:unit_id/learning_alignments/class_details' do - unit = Unit.find(params[:unit_id]) + present unit.ilo_progress_class_stats + end - unless authorise?(current_user, unit, :provide_feedback) - error!({ error: 'You are not authorised to access these task alignments.' }, 403) - end + desc 'Return unit learning alignment values with median stats for each tutorial' + params do + requires :unit_id, type: Integer, desc: 'The id of the unit' + end + get '/units/:unit_id/learning_alignments/class_details' do + unit = Unit.find(params[:unit_id]) - unit.ilo_progress_class_details + unless authorise?(current_user, unit, :provide_feedback) + error!({ error: 'You are not authorised to access these task alignments.' }, 403) end + + present unit.ilo_progress_class_details end end diff --git a/app/api/learning_outcomes_api.rb b/app/api/learning_outcomes_api.rb index 8a59d970e..bf123b984 100644 --- a/app/api/learning_outcomes_api.rb +++ b/app/api/learning_outcomes_api.rb @@ -1,115 +1,113 @@ require 'grape' -module Api - class LearningOutcomesApi < Grape::API - helpers AuthenticationHelpers - helpers AuthorisationHelpers - helpers MimeCheckHelpers - - before do - authenticated? - end +class LearningOutcomesApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers + helpers MimeCheckHelpers - desc 'Add an outcome to a unit' - params do - requires :unit_id, type: Integer, desc: 'The unit ID for which the ILO belongs to' - requires :name, type: String, desc: 'The ILO''s name' - requires :description, type: String, desc: 'The ILO''s description' - optional :abbreviation, type: String, desc: 'The ILO''s new abbreviation' - end - post '/units/:unit_id/outcomes' do - unit = Unit.find(params[:unit_id]) + before do + authenticated? + end - unless authorise? current_user, unit, :update - error!({ error: 'You are not authorised to create outcomes in this unit.' }, 403) - end + desc 'Add an outcome to a unit' + params do + requires :unit_id, type: Integer, desc: 'The unit ID for which the ILO belongs to' + requires :name, type: String, desc: 'The ILO''s name' + requires :description, type: String, desc: 'The ILO''s description' + optional :abbreviation, type: String, desc: 'The ILO''s new abbreviation' + end + post '/units/:unit_id/outcomes' do + unit = Unit.find(params[:unit_id]) - ilo = unit.add_ilo(params[:name], params[:description], params[:abbreviation]) - ilo + unless authorise? current_user, unit, :update + error!({ error: 'You are not authorised to create outcomes in this unit.' }, 403) end - desc 'Update ILO' - params do - requires :unit_id, type: Integer, desc: 'The unit ID for which the ILO belongs to' - optional :name, type: String, desc: 'The ILO''s new name' - optional :description, type: String, desc: 'The ILO''s new description' - optional :abbreviation, type: String, desc: 'The ILO''s new abbreviation' - optional :ilo_number, type: Integer, desc: 'The ILO''s new sequence number' - end - put '/units/:unit_id/outcomes/:id' do - unit = Unit.find(params[:unit_id]) - error!({ error: 'Unable to locate requested unit.' }, 405) if unit.nil? - - unless authorise? current_user, unit, :update - error!({ error: 'You are not authorised to update outcomes in this unit.' }, 403) - end - - ilo = unit.learning_outcomes.find(params[:id]) - error!({ error: 'Unable to locate outcome requested.' }, 405) if ilo.nil? - - ilo_parameters = ActionController::Parameters.new(params) - .permit( - :name, - :description, - :abbreviation - ) - unit.move_ilo(ilo, params[:ilo_number]) if params[:ilo_number] - ilo.update!(ilo_parameters) - ilo - end + ilo = unit.add_ilo(params[:name], params[:description], params[:abbreviation]) + present ilo, with: Entities::LearningOutcomeEntity + end + + desc 'Update ILO' + params do + requires :unit_id, type: Integer, desc: 'The unit ID for which the ILO belongs to' + optional :name, type: String, desc: 'The ILO''s new name' + optional :description, type: String, desc: 'The ILO''s new description' + optional :abbreviation, type: String, desc: 'The ILO''s new abbreviation' + optional :ilo_number, type: Integer, desc: 'The ILO''s new sequence number' + end + put '/units/:unit_id/outcomes/:id' do + unit = Unit.find(params[:unit_id]) + error!({ error: 'Unable to locate requested unit.' }, 405) if unit.nil? - desc 'Delete an outcome from a unit' - params do - requires :unit_id, type: Integer, desc: 'The id for the unit' - requires :id, type: Integer, desc: 'The id for the outcome you wish to delete' + unless authorise? current_user, unit, :update + error!({ error: 'You are not authorised to update outcomes in this unit.' }, 403) end - delete '/units/:unit_id/outcomes/:id' do - unit = Unit.find(params[:unit_id]) - error!({ error: 'Unable to locate requested unit.' }, 405) if unit.nil? - unless authorise? current_user, unit, :update - error!({ error: 'You are not authorised to delete outcomes in this unit.' }, 403) - end + ilo = unit.learning_outcomes.find(params[:id]) + error!({ error: 'Unable to locate outcome requested.' }, 405) if ilo.nil? + + ilo_parameters = ActionController::Parameters.new(params) + .permit( + :name, + :description, + :abbreviation + ) + unit.move_ilo(ilo, params[:ilo_number]) if params[:ilo_number] + ilo.update!(ilo_parameters) + present ilo, with: Entities::LearningOutcomeEntity + end - ilo = unit.learning_outcomes.find(params[:id]) - error!({ error: 'Unable to locate outcome requested.' }, 405) if ilo.nil? + desc 'Delete an outcome from a unit' + params do + requires :unit_id, type: Integer, desc: 'The id for the unit' + requires :id, type: Integer, desc: 'The id for the outcome you wish to delete' + end + delete '/units/:unit_id/outcomes/:id' do + unit = Unit.find(params[:unit_id]) + error!({ error: 'Unable to locate requested unit.' }, 405) if unit.nil? - ilo.destroy - nil + unless authorise? current_user, unit, :update + error!({ error: 'You are not authorised to delete outcomes in this unit.' }, 403) end - desc 'Download the outcomes for a unit to a csv' - get '/units/:unit_id/outcomes/csv' do - unit = Unit.find(params[:unit_id]) - error!({ error: 'Unable to locate requested unit.' }, 405) if unit.nil? + ilo = unit.learning_outcomes.find(params[:id]) + error!({ error: 'Unable to locate outcome requested.' }, 405) if ilo.nil? - unless authorise? current_user, unit, :update - error!({ error: 'You are not authorised to download outcomes for this unit.' }, 403) - end + ilo.destroy + nil + end - content_type 'application/octet-stream' - header['Content-Disposition'] = "attachment; filename=#{unit.code}-LearningOutcomes.csv " - env['api.format'] = :binary - unit.export_learning_outcome_to_csv - end + desc 'Download the outcomes for a unit to a csv' + get '/units/:unit_id/outcomes/csv' do + unit = Unit.find(params[:unit_id]) + error!({ error: 'Unable to locate requested unit.' }, 405) if unit.nil? - desc 'Upload the outcomes for a unit from a csv' - params do - requires :file, type: File, desc: 'CSV upload file.' - requires :unit_id, type: Integer, desc: 'The unit to upload tasks to' + unless authorise? current_user, unit, :update + error!({ error: 'You are not authorised to download outcomes for this unit.' }, 403) end - post '/units/:unit_id/outcomes/csv' do - # check mime is correct before uploading - ensure_csv!(params[:file][:tempfile]) - unit = Unit.find(params[:unit_id]) + content_type 'application/octet-stream' + header['Content-Disposition'] = "attachment; filename=#{unit.code}-LearningOutcomes.csv " + env['api.format'] = :binary + unit.export_learning_outcome_to_csv + end + + desc 'Upload the outcomes for a unit from a csv' + params do + requires :file, type: File, desc: 'CSV upload file.' + requires :unit_id, type: Integer, desc: 'The unit to upload tasks to' + end + post '/units/:unit_id/outcomes/csv' do + # check mime is correct before uploading + ensure_csv!(params[:file][:tempfile]) - unless authorise? current_user, unit, :upload_csv - error!({ error: 'Not authorised to upload CSV of outcomes' }, 403) - end + unit = Unit.find(params[:unit_id]) - # Actually import... - unit.import_outcomes_from_csv(params[:file][:tempfile]) + unless authorise? current_user, unit, :upload_csv + error!({ error: 'Not authorised to upload CSV of outcomes' }, 403) end + + # Actually import... + unit.import_outcomes_from_csv(params[:file][:tempfile]) end end diff --git a/app/api/projects_api.rb b/app/api/projects_api.rb index bef44feb7..59bb03a29 100644 --- a/app/api/projects_api.rb +++ b/app/api/projects_api.rb @@ -1,203 +1,203 @@ require 'grape' -require 'project_serializer' -module Api - class ProjectsApi < Grape::API - helpers AuthenticationHelpers - helpers AuthorisationHelpers - helpers DbHelpers +class ProjectsApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers + helpers DbHelpers - before do - authenticated? - end + before do + authenticated? + end - desc "Fetches all of the current user's projects" - params do - optional :include_inactive, type: Boolean, desc: 'Include projects for units that are no longer active?' + desc "Fetches all of the current user's projects" + params do + optional :include_inactive, type: Boolean, desc: 'Include projects for units that are no longer active?' + end + get '/projects' do + include_inactive = params[:include_inactive] || false + + projects = Project.for_user current_user, include_inactive + + student_name = db_concat('users.first_name', "' '", 'users.last_name') + + # join in other tables to fetch data + data = projects. + joins(:unit). + joins(:user). + select( 'projects.*', + 'units.name AS unit_name', 'units.id AS unit_id', 'units.code AS unit_code', 'units.start_date AS start_date', 'units.end_date AS end_date', 'units.teaching_period_id AS teaching_period_id', 'units.active AS active', + "#{student_name} AS student_name" + ) + + # Now map the data to structure for json to return + result = data.map do |row| + { + unit_id: row['unit_id'], + unit_code: row['unit_code'], + unit_name: row['unit_name'], + project_id: row['id'], + campus_id: row['campus_id'], + target_grade: row['target_grade'], + has_portfolio: !row['portfolio_production_date'].nil?, + start_date: row['start_date'].strftime('%Y-%m-%d'), + end_date: row['end_date'].strftime('%Y-%m-%d'), + teaching_period_id: row['teaching_period_id'], + active: row['active'].is_a?(Numeric) ? row['active'] != 0 : row['active'] + } end - get '/projects' do - include_inactive = params[:include_inactive] || false - - projects = Project.for_user current_user, include_inactive - - student_name = db_concat('users.first_name', "' '", 'users.last_name') - - # join in other tables to fetch data - data = projects. - joins(:unit). - joins(:user). - select( 'projects.*', - 'units.name AS unit_name', 'units.id AS unit_id', 'units.code AS unit_code', 'units.start_date AS start_date', 'units.end_date AS end_date', 'units.teaching_period_id AS teaching_period_id', 'units.active AS active', - "#{student_name} AS student_name" - ) - - # Now map the data to structure for json to return - result = data.map do |row| - { - unit_id: row['unit_id'], - unit_code: row['unit_code'], - unit_name: row['unit_name'], - project_id: row['id'], - campus_id: row['campus_id'], - target_grade: row['target_grade'], - has_portfolio: !row['portfolio_production_date'].nil?, - start_date: row['start_date'].strftime('%Y-%m-%d'), - end_date: row['end_date'].strftime('%Y-%m-%d'), - teaching_period_id: row['teaching_period_id'], - active: row['active'].is_a?(Numeric) ? row['active'] != 0 : row['active'] - } - end - present result, with: Grape::Presenters::Presenter - end + present result, with: Grape::Presenters::Presenter + end + + desc 'Get project' + params do + requires :id, type: Integer, desc: 'The id of the project to get' + end + get '/projects/:id' do + project = Project.find(params[:id]) - desc 'Get project' - params do - requires :id, type: Integer, desc: 'The id of the project to get' + if authorise? current_user, project, :get + project + else + error!({ error: "Couldn't find Project with id=#{params[:id]}" }, 403) end - get '/projects/:id' do - project = Project.find(params[:id]) - if authorise? current_user, project, :get - project - else - error!({ error: "Couldn't find Project with id=#{params[:id]}" }, 403) - end + present project, with: Entities::ProjectEntity, user: current_user + end - present project, with: Api::Entities::ProjectEntity, user: current_user - end + desc 'Update a project' + params do + optional :trigger, type: String, desc: 'The update trigger' + optional :campus_id, type: Integer, desc: 'Campus this project is part of, or -1 for no campus' + optional :enrolled, type: Boolean, desc: 'Enrol or withdraw this project' + optional :target_grade, type: Integer, desc: 'New target grade' + optional :submitted_grade, type: Integer, desc: 'New submitted grade' + optional :compile_portfolio, type: Boolean, desc: 'Schedule a construction of the portfolio' + optional :grade, type: Integer, desc: 'New grade' + optional :old_grade, type: Integer, desc: 'Old grade to check it has not changed...' + optional :grade_rationale, type: String, desc: 'New grade rationale' + end + put '/projects/:id' do + project = Project.find(params[:id]) - desc 'Update a project' - params do - optional :trigger, type: String, desc: 'The update trigger' - optional :campus_id, type: Integer, desc: 'Campus this project is part of, or -1 for no campus' - optional :enrolled, type: Boolean, desc: 'Enrol or withdraw this project' - optional :target_grade, type: Integer, desc: 'New target grade' - optional :submitted_grade, type: Integer, desc: 'New submitted grade' - optional :compile_portfolio, type: Boolean, desc: 'Schedule a construction of the portfolio' - optional :grade, type: Integer, desc: 'New grade' - optional :old_grade, type: Integer, desc: 'Old grade to check it has not changed...' - optional :grade_rationale, type: String, desc: 'New grade rationale' - end - put '/projects/:id' do - project = Project.find(params[:id]) - - if params[:trigger].nil? == false - if params[:trigger] == 'trigger_week_end' - if authorise? current_user, project, :trigger_week_end - project.trigger_week_end(current_user) - else - error!({ error: "You are not authorised to perform this action for Project with id=#{params[:id]}" }, 403) - end + if params[:trigger].nil? == false + if params[:trigger] == 'trigger_week_end' + if authorise? current_user, project, :trigger_week_end + project.trigger_week_end(current_user) else - error!({ error: "Invalid trigger - #{params[:trigger]} unknown" }, 403) - end - # If we are only updating the campus - elsif params[:campus_id].present? - unless authorise? current_user, project, :change_campus - error!({ error: "You cannot change the campus for project #{params[:id]}" }, 403) - end - project.campus_id = params[:campus_id] == -1 ? nil : params[:campus_id] - project.save! - elsif !params[:enrolled].nil? - unless authorise? current_user, project.unit, :change_project_enrolment - error!({ error: "You cannot change the enrolment for project #{params[:id]}" }, 403) - end - project.enrolled = params[:enrolled] - project.save - elsif !params[:target_grade].nil? - unless authorise? current_user, project, :change - error!({ error: "You do not have permissions to change Project with id=#{params[:id]}" }, 403) + error!({ error: "You are not authorised to perform this action for Project with id=#{params[:id]}" }, 403) end + else + error!({ error: "Invalid trigger - #{params[:trigger]} unknown" }, 403) + end + # If we are only updating the campus + elsif params[:campus_id].present? + unless authorise? current_user, project, :change_campus + error!({ error: "You cannot change the campus for project #{params[:id]}" }, 403) + end + project.campus_id = params[:campus_id] == -1 ? nil : params[:campus_id] + project.save! + elsif !params[:enrolled].nil? + unless authorise? current_user, project.unit, :change_project_enrolment + error!({ error: "You cannot change the enrolment for project #{params[:id]}" }, 403) + end + project.enrolled = params[:enrolled] + project.save + elsif !params[:target_grade].nil? + unless authorise? current_user, project, :change + error!({ error: "You do not have permissions to change Project with id=#{params[:id]}" }, 403) + end - project.target_grade = params[:target_grade] - project.save - elsif !params[:submitted_grade].nil? - unless authorise? current_user, project, :change - error!({ error: "You do not have permissions to change Project with id=#{params[:id]}" }, 403) - end - if project.has_portfolio - error!({ error: "You cannot change your submitted grade after portfolio submission"}, 403) - end + project.target_grade = params[:target_grade] + project.save + elsif !params[:submitted_grade].nil? + unless authorise? current_user, project, :change + error!({ error: "You do not have permissions to change Project with id=#{params[:id]}" }, 403) + end + if project.has_portfolio + error!({ error: "You cannot change your submitted grade after portfolio submission"}, 403) + end - project.submitted_grade = params[:submitted_grade] - project.save - elsif !params[:grade].nil? - unless authorise? current_user, project, :assess - error!({ error: "You do not have permissions to assess Project with id=#{params[:id]}" }, 403) - end + project.submitted_grade = params[:submitted_grade] + project.save + elsif !params[:grade].nil? + unless authorise? current_user, project, :assess + error!({ error: "You do not have permissions to assess Project with id=#{params[:id]}" }, 403) + end - if params[:grade_rationale].nil? - error!({ error: 'Grade rationale required to perform assessment.' }, 403) - end + if params[:grade_rationale].nil? + error!({ error: 'Grade rationale required to perform assessment.' }, 403) + end - if params[:old_grade].nil? - error!({ error: 'Existing project grade is required to perform assessment.' }, 403) - end + if params[:old_grade].nil? + error!({ error: 'Existing project grade is required to perform assessment.' }, 403) + end - if params[:old_grade] != project.grade - error!({ error: 'Existing project grade does not match current grade. Refresh project and try again.' }, 403) - end + if params[:old_grade] != project.grade + error!({ error: 'Existing project grade does not match current grade. Refresh project and try again.' }, 403) + end - project.grade = params[:grade] - project.grade_rationale = params[:grade_rationale] - project.save! - elsif !params[:compile_portfolio].nil? - unless authorise? current_user, project, :change - error!({ error: "You do not have permissions to change Project with id=#{params[:id]}" }, 403) - end + project.grade = params[:grade] + project.grade_rationale = params[:grade_rationale] + project.save! - project.compile_portfolio = params[:compile_portfolio] - project.save + present project, Entities::ProjectEntity, for_staff: true + return + elsif !params[:compile_portfolio].nil? + unless authorise? current_user, project, :change + error!({ error: "You do not have permissions to change Project with id=#{params[:id]}" }, 403) end - Api::Entities::ProjectEntity.represent(project, only: [ :campus_id, :enrolled, :target_grade, :submitted_grade, :compile_portfolio, :portfolio_available, :uses_draft_learning_summary, :stats, :burndown_chart_data ]) - end # put + project.compile_portfolio = params[:compile_portfolio] + project.save + end + + Entities::ProjectEntity.represent(project, only: [ :campus_id, :enrolled, :target_grade, :submitted_grade, :compile_portfolio, :portfolio_available, :uses_draft_learning_summary, :stats, :burndown_chart_data ]) + end # put - desc 'Enrol a student in a unit, creating them a project' - params do - requires :unit_id, type: Integer, desc: 'Unit Id' - requires :student_num, type: String, desc: 'Student Number 7 digit code' - requires :campus_id, type: Integer, desc: 'Campus this project is part of' + desc 'Enrol a student in a unit, creating them a project' + params do + requires :unit_id, type: Integer, desc: 'Unit Id' + requires :student_num, type: String, desc: 'Student Number 7 digit code' + requires :campus_id, type: Integer, desc: 'Campus this project is part of' + end + post '/projects' do + unit = Unit.find(params[:unit_id]) + student = User.find_by(username: params[:student_num]) + student = User.find_by(student_id: params[:student_num]) if student.nil? + student = User.find_by(email: params[:student_num]) if student.nil? + + if student.nil? + error!({ error: "Couldn't find Student with username=#{params[:student_num]}" }, 403) end - post '/projects' do - unit = Unit.find(params[:unit_id]) - student = User.find_by(username: params[:student_num]) - student = User.find_by(student_id: params[:student_num]) if student.nil? - student = User.find_by(email: params[:student_num]) if student.nil? - - if student.nil? - error!({ error: "Couldn't find Student with username=#{params[:student_num]}" }, 403) - end - campus = Campus.find(params[:campus_id]) + campus = Campus.find(params[:campus_id]) - if authorise? current_user, unit, :enrol_student - proj = unit.enrol_student(student, campus) - if proj.nil? - error!({ error: 'Error adding student to unit' }, 403) - else - result = { - project_id: proj.id, - enrolled: proj.enrolled, - first_name: proj.student.first_name, - last_name: proj.student.last_name, - student_id: proj.student.username, - student_email: proj.student.email, - target_grade: proj.target_grade, - campus_id: proj.campus_id, - compile_portfolio: false, - grade: proj.grade, - grade_rationale: proj.grade_rationale, - max_pct_copy: 0, - has_portfolio: false, - stats: '0|1|0|0|0|0|0|0|0|0|0' - } - present result, with: Grape::Presenters::Presenter - end + if authorise? current_user, unit, :enrol_student + proj = unit.enrol_student(student, campus) + if proj.nil? + error!({ error: 'Error adding student to unit' }, 403) else - error!({ error: "Couldn't find Unit with id=#{params[:unit_id]}" }, 403) + result = { + project_id: proj.id, + enrolled: proj.enrolled, + first_name: proj.student.first_name, + last_name: proj.student.last_name, + student_id: proj.student.username, + student_email: proj.student.email, + target_grade: proj.target_grade, + campus_id: proj.campus_id, + compile_portfolio: false, + grade: proj.grade, + grade_rationale: proj.grade_rationale, + max_pct_copy: 0, + has_portfolio: false, + stats: '0|1|0|0|0|0|0|0|0|0|0' + } + present result, with: Grape::Presenters::Presenter end + else + error!({ error: "Couldn't find Unit with id=#{params[:unit_id]}" }, 403) end end end diff --git a/app/api/settings_api.rb b/app/api/settings_api.rb index 20f065dd0..2c5ff0a1b 100644 --- a/app/api/settings_api.rb +++ b/app/api/settings_api.rb @@ -1,26 +1,27 @@ require 'grape' -require 'project_serializer' -module Api - class SettingsApi < Grape::API +class SettingsApi < Grape::API - # - # Returns the current auth method - # - desc 'Return configurable details for the Doubtfire front end' - get '/settings' do - { - externalName: Doubtfire::Application.config.institution[:product_name], - overseer_enabled: Doubtfire::Application.config.overseer_enabled - } - end + # + # Returns the current auth method + # + desc 'Return configurable details for the Doubtfire front end' + get '/settings' do + response = { + externalName: Doubtfire::Application.config.institution[:product_name], + overseer_enabled: Doubtfire::Application.config.overseer_enabled + } - desc 'Return privacy policy details' - get '/settings/privacy' do - { - privacy: Doubtfire::Application.config.institution[:privacy], - plagiarism: Doubtfire::Application.config.institution[:plagiarism] - } - end + present response, with: Grape::Presenters::Presenter + end + + desc 'Return privacy policy details' + get '/settings/privacy' do + response = { + privacy: Doubtfire::Application.config.institution[:privacy], + plagiarism: Doubtfire::Application.config.institution[:plagiarism] + } + + present response, with: Grape::Presenters::Presenter end end diff --git a/app/api/students_api.rb b/app/api/students_api.rb index bcb0b1d07..1c85f84b3 100644 --- a/app/api/students_api.rb +++ b/app/api/students_api.rb @@ -1,32 +1,30 @@ require 'grape' -require 'project_serializer' -module Api - class StudentsApi < Grape::API - helpers AuthenticationHelpers - helpers AuthorisationHelpers +class StudentsApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers - before do - authenticated? - end + before do + authenticated? + end - desc 'Get users' - params do - requires :unit_id, type: Integer, desc: 'The unit to get the students for' - optional :all, type: Boolean, desc: 'Show all students or just current students' - end - get '/students' do - unit = Unit.find(params[:unit_id]) + desc 'Get users' + params do + requires :unit_id, type: Integer, desc: 'The unit to get the students for' + optional :all, type: Boolean, desc: 'Show all students or just current students' + end + get '/students' do + unit = Unit.find(params[:unit_id]) - if (authorise? current_user, unit, :get_students) || (authorise? current_user, User, :admin_units) - result = if params[:all].nil? || (!params[:all].nil? && !params[:all]) - unit.student_query(true) - else - unit.student_query(false) - end - else - error!({ error: "Couldn't find Unit with id=#{params[:unit_id]}" }, 403) - end + if (authorise? current_user, unit, :get_students) || (authorise? current_user, User, :admin_units) + result = if params[:all].nil? || (!params[:all].nil? && !params[:all]) + unit.student_query(true) + else + unit.student_query(false) + end + present result, with: Grape::Presenters::Presenter + else + error!({ error: "Couldn't find Unit with id=#{params[:unit_id]}" }, 403) end end end diff --git a/app/api/submission/batch_task_api.rb b/app/api/submission/batch_task_api.rb index bea5a6a0f..7458e894b 100644 --- a/app/api/submission/batch_task_api.rb +++ b/app/api/submission/batch_task_api.rb @@ -1,68 +1,65 @@ require 'grape' -require 'project_serializer' -module Api - module Submission - class BatchTaskApi < Grape::API - helpers GenerateHelpers - helpers AuthenticationHelpers - helpers AuthorisationHelpers +module Submission + class BatchTaskApi < Grape::API + helpers GenerateHelpers + helpers AuthenticationHelpers + helpers AuthorisationHelpers - before do - authenticated? - end + before do + authenticated? + end + + desc "Retrieve all submission documents ready to mark for the provided user's tutorials for the given unit id" + params do + requires :unit_id, type: Integer, desc: 'Unit ID to retrieve submissions for.' + optional :user_id, type: Integer, desc: 'User ID to retrieve submissions for (optional; will use current_user otherwise).' + end + get '/submission/assess/' do + user = params[:user_id].nil? ? current_user : User.find(params[:user_id]) + unit = Unit.find(params[:unit_id]) - desc "Retrieve all submission documents ready to mark for the provided user's tutorials for the given unit id" - params do - requires :unit_id, type: Integer, desc: 'Unit ID to retrieve submissions for.' - optional :user_id, type: Integer, desc: 'User ID to retrieve submissions for (optional; will use current_user otherwise).' + unless authorise? user, unit, :provide_feedback + error!({ error: 'Not authorised to batch download ready to mark submissions' }, 401) end - get '/submission/assess/' do - user = params[:user_id].nil? ? current_user : User.find(params[:user_id]) - unit = Unit.find(params[:unit_id]) - unless authorise? user, unit, :provide_feedback - error!({ error: 'Not authorised to batch download ready to mark submissions' }, 401) - end + unless authorise? current_user, unit, :provide_feedback + error!({ error: 'Not authorised to batch download ready to mark submissions' }, 401) + end - unless authorise? current_user, unit, :provide_feedback - error!({ error: 'Not authorised to batch download ready to mark submissions' }, 401) - end + # Array of tasks that need marking for the given unit id + tasks_to_download = UnitRole.tasks_to_review(user) - # Array of tasks that need marking for the given unit id - tasks_to_download = UnitRole.tasks_to_review(user) + output_zip = unit.generate_batch_task_zip(current_user, tasks_to_download) - output_zip = unit.generate_batch_task_zip(current_user, tasks_to_download) + error!({ error: 'No files to download' }, 401) if output_zip.nil? - error!({ error: 'No files to download' }, 401) if output_zip.nil? + # Set download headers... + content_type 'application/octet-stream' + download_id = "#{Time.new.strftime('%Y-%m-%d')}-#{unit.code}-#{current_user.username}" + header['Content-Disposition'] = "attachment; filename=#{download_id}.zip" + env['api.format'] = :binary - # Set download headers... - content_type 'application/octet-stream' - download_id = "#{Time.new.strftime('%Y-%m-%d')}-#{unit.code}-#{current_user.username}" - header['Content-Disposition'] = "attachment; filename=#{download_id}.zip" - env['api.format'] = :binary + out = File.read(output_zip) + File.unlink(output_zip) + out + end # get - out = File.read(output_zip) - File.unlink(output_zip) - out - end # get + desc 'Upload submission documents for the given unit and user id' + params do + requires :file, type: File, desc: 'batch file upload' + requires :unit_id, type: Integer, desc: 'Unit ID to upload marked submissions to.' + optional :user_id, type: Integer, desc: 'User ID to upload marked submissions to (optional; will use current_user otherwise).' + end + post '/submission/assess/' do + user = params[:user_id].nil? ? current_user : User.find(params[:user_id]) + unit = Unit.find(params[:unit_id]) - desc 'Upload submission documents for the given unit and user id' - params do - requires :file, type: File, desc: 'batch file upload' - requires :unit_id, type: Integer, desc: 'Unit ID to upload marked submissions to.' - optional :user_id, type: Integer, desc: 'User ID to upload marked submissions to (optional; will use current_user otherwise).' + unless authorise? user, unit, :provide_feedback + error!({ error: 'Not authorised to batch upload marks' }, 401) end - post '/submission/assess/' do - user = params[:user_id].nil? ? current_user : User.find(params[:user_id]) - unit = Unit.find(params[:unit_id]) - unless authorise? user, unit, :provide_feedback - error!({ error: 'Not authorised to batch upload marks' }, 401) - end - - unit.upload_batch_task_zip_or_csv(current_user, params[:file]) - end # post - end + present unit.upload_batch_task_zip_or_csv(current_user, params[:file]), with: Grape::Presenters::Presenter + end # post end end diff --git a/app/api/submission/generate_helpers.rb b/app/api/submission/generate_helpers.rb index bbf3474d2..d05adc4d0 100644 --- a/app/api/submission/generate_helpers.rb +++ b/app/api/submission/generate_helpers.rb @@ -1,7 +1,7 @@ # zipping files require 'zip' -module Api::Submission::GenerateHelpers +module Submission::GenerateHelpers # # Scoops out a files array from the params provided # diff --git a/app/api/submission/portfolio_api.rb b/app/api/submission/portfolio_api.rb index fff9b8330..46bb4e5e2 100644 --- a/app/api/submission/portfolio_api.rb +++ b/app/api/submission/portfolio_api.rb @@ -1,101 +1,101 @@ require 'grape' -require 'project_serializer' -module Api - module Submission - class PortfolioApi < Grape::API - helpers GenerateHelpers - helpers AuthenticationHelpers - helpers AuthorisationHelpers +module Submission + class PortfolioApi < Grape::API + helpers GenerateHelpers + helpers AuthenticationHelpers + helpers AuthorisationHelpers - before do - authenticated? + before do + authenticated? + end + + desc "Upload documents for inclusion in a project's portfolio" + params do + requires :name, type: String, desc: 'Name of the part being uploaded' + requires :kind, type: String, desc: 'The kind of file being uploaded: document, code, or image' + requires :file0, type: File, desc: 'file 0.' + end + post '/submission/project/:id/portfolio' do + project = Project.find(params[:id]) + + unless authorise? current_user, project, :make_submission + error!({ error: "Not authorised to submit portfolio for project '#{params[:id]}'" }, 401) end - desc "Upload documents for inclusion in a project's portfolio" - params do - requires :name, type: String, desc: 'Name of the part being uploaded' - requires :kind, type: String, desc: 'The kind of file being uploaded: document, code, or image' - requires :file0, type: File, desc: 'file 0.' + file = params[:file0] + name = params[:name] + kind = params[:kind] + + # Check that the file is OK to accept + unless FileHelper.accept_file(file, name, kind) + error!({ error: "'#{file[:filename]}' is not a valid #{kind} file" }, 403) end - post '/submission/project/:id/portfolio' do - project = Project.find(params[:id]) - unless authorise? current_user, project, :make_submission - error!({ error: "Not authorised to submit portfolio for project '#{params[:id]}'" }, 401) - end + # Move file into place + result = project.move_to_portfolio(file, name, kind) # returns details of file + + present result, Grape::Presenters::Presenter + end # post + + desc 'Remove a file from the portfolio files for a unit' + params do + optional :idx, type: Integer, desc: 'The index of the file' + optional :kind, type: String, desc: 'The kind of file being removed: document, code, or image' + optional :name, type: String, desc: 'Name of file to remove' + end + delete '/submission/project/:id/portfolio' do + project = Project.find(params[:id]) + + unless authorise? current_user, project, :make_submission + error!({ error: "Not authorised to alter portfolio for project '#{params[:id]}'" }, 401) + end - file = params[:file0] + # Remove file or portfolio? + if params[:idx].nil? && params[:name].nil? && params[:kind].nil? + project.remove_portfolio # returns details of file + elsif !(params[:idx].nil? || params[:name].nil? || params[:kind].nil?) + idx = params[:idx] name = params[:name] kind = params[:kind] - # Check that the file is OK to accept - unless FileHelper.accept_file(file, name, kind) - error!({ error: "'#{file[:filename]}' is not a valid #{kind} file" }, 403) - end + project.remove_portfolio_file(idx, kind, name) # returns details of file + end - # Move file into place - project.move_to_portfolio(file, name, kind) # returns details of file - end # post + nil + end - desc 'Remove a file from the portfolio files for a unit' - params do - optional :idx, type: Integer, desc: 'The index of the file' - optional :kind, type: String, desc: 'The kind of file being removed: document, code, or image' - optional :name, type: String, desc: 'Name of file to remove' - end - delete '/submission/project/:id/portfolio' do - project = Project.find(params[:id]) - - unless authorise? current_user, project, :make_submission - error!({ error: "Not authorised to alter portfolio for project '#{params[:id]}'" }, 401) - end - - # Remove file or portfolio? - if params[:idx].nil? && params[:name].nil? && params[:kind].nil? - project.remove_portfolio # returns details of file - elsif !(params[:idx].nil? || params[:name].nil? || params[:kind].nil?) - idx = params[:idx] - name = params[:name] - kind = params[:kind] - - project.remove_portfolio_file(idx, kind, name) # returns details of file - end - nil - end + desc 'Retrieve portfolio for project with the given id' + params do + optional :as_attachment, type: Boolean, desc: 'Whether or not to download file as attachment. Default is false.' + end + get '/submission/project/:id/portfolio' do + project = Project.find(params[:id]) - desc 'Retrieve portfolio for project with the given id' - params do - optional :as_attachment, type: Boolean, desc: 'Whether or not to download file as attachment. Default is false.' + unless authorise? current_user, project, :get_submission + error!({ error: "Not authorised to download portfolio for project '#{params[:id]}'" }, 401) end - get '/submission/project/:id/portfolio' do - project = Project.find(params[:id]) - unless authorise? current_user, project, :get_submission - error!({ error: "Not authorised to download portfolio for project '#{params[:id]}'" }, 401) - end + evidence_loc = project.portfolio_path - evidence_loc = project.portfolio_path - - if evidence_loc.nil? || File.exist?(evidence_loc) == false - evidence_loc = Rails.root.join('public', 'resources', 'FileNotFound.pdf') - filename = "FileNotFound.pdf" - else - filename = "#{project.unit.code}-#{project.student.username}-portfolio.pdf" - end + if evidence_loc.nil? || File.exist?(evidence_loc) == false + evidence_loc = Rails.root.join('public', 'resources', 'FileNotFound.pdf') + filename = "FileNotFound.pdf" + else + filename = "#{project.unit.code}-#{project.student.username}-portfolio.pdf" + end - if params[:as_attachment] - header['Content-Disposition'] = "attachment; filename=#{filename}" - end + if params[:as_attachment] + header['Content-Disposition'] = "attachment; filename=#{filename}" + end - # Set download headers... - content_type 'application/pdf' - env['api.format'] = :binary + # Set download headers... + content_type 'application/pdf' + env['api.format'] = :binary - File.read(evidence_loc) - end # get + File.read(evidence_loc) + end # get - # "Retrieve portfolios for a unit" done using controller - end + # "Retrieve portfolios for a unit" done using controller end end diff --git a/app/api/submission/portfolio_evidence_api.rb b/app/api/submission/portfolio_evidence_api.rb index c25026749..314b87c6f 100644 --- a/app/api/submission/portfolio_evidence_api.rb +++ b/app/api/submission/portfolio_evidence_api.rb @@ -1,280 +1,281 @@ require 'grape' -require 'project_serializer' - -module Api - module Submission - class PortfolioEvidenceApi < Grape::API - helpers GenerateHelpers - helpers AuthenticationHelpers - helpers AuthorisationHelpers - include LogHelper - - def self.logger - LogHelper.logger - end - before do - authenticated? - end +module Submission + class PortfolioEvidenceApi < Grape::API + helpers GenerateHelpers + helpers AuthenticationHelpers + helpers AuthorisationHelpers + include LogHelper - desc 'Upload and generate doubtfire-task-specific submission document' - params do - optional :file0, type: File, desc: 'file 0.' - optional :file1, type: File, desc: 'file 1.' - optional :contributions, type: JSON, desc: "Contribution details JSON, eg: [ { project_id: 1, pct:'0.44', pts: 4 }, ... ]" - optional :alignment_data, type: JSON, desc: "Data for task alignment, eg: [ { ilo_id: 1, rating: 5, rationale: 'Hello' }, ... ]" - optional :trigger, type: String, desc: 'Can be need_help to indicate upload is not a ready to mark submission' - end - post '/projects/:id/task_def_id/:task_definition_id/submission' do + def self.logger + LogHelper.logger + end - project = Project.find(params[:id]) - task_definition = project.unit.task_definitions.find(params[:task_definition_id]) + before do + authenticated? + end - # check the user can put this task - unless authorise? current_user, project, :make_submission - error!({ error: "Not authorised to submit task '#{task_definition.name}'" }, 401) - end + desc 'Upload and generate doubtfire-task-specific submission document' + params do + optional :file0, type: File, desc: 'file 0.' + optional :file1, type: File, desc: 'file 1.' + optional :contributions, type: JSON, desc: "Contribution details JSON, eg: [ { project_id: 1, pct:'0.44', pts: 4 }, ... ]" + optional :alignment_data, type: JSON, desc: "Data for task alignment, eg: [ { ilo_id: 1, rating: 5, rationale: 'Hello' }, ... ]" + optional :trigger, type: String, desc: 'Can be need_help to indicate upload is not a ready to mark submission' + end + post '/projects/:id/task_def_id/:task_definition_id/submission' do - task = project.task_for_task_definition(task_definition) + project = Project.find(params[:id]) + task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - if task.group_task? && !task.group - error!({ error: "This task requires a group submission. Ensure you are in a group for the unit's #{task_definition.group_set.name}" }, 403) - end + # check the user can put this task + unless authorise? current_user, project, :make_submission + error!({ error: "Not authorised to submit task '#{task_definition.name}'" }, 401) + end - trigger = if params[:trigger] && params[:trigger].tr('"\'', '') == 'need_help' - 'need_help' - else - 'ready_for_feedback' - end + task = project.task_for_task_definition(task_definition) - alignments = params[:alignment_data] - upload_reqs = task.upload_requirements - student = task.project.student - unit = task.project.unit + if task.group_task? && !task.group + error!({ error: "This task requires a group submission. Ensure you are in a group for the unit's #{task_definition.group_set.name}" }, 403) + end - # Copy files to be PDFed - task.accept_submission(current_user, scoop_files(params, upload_reqs), student, self, params[:contributions], trigger, alignments) + trigger = if params[:trigger] && params[:trigger].tr('"\'', '') == 'need_help' + 'need_help' + else + 'ready_for_feedback' + end + + alignments = params[:alignment_data] + upload_reqs = task.upload_requirements + student = task.project.student + unit = task.project.unit + + # Copy files to be PDFed + task.accept_submission(current_user, scoop_files(params, upload_reqs), student, self, params[:contributions], trigger, alignments) + + overseer_assessment = OverseerAssessment.create_for(task) + if overseer_assessment.present? + logger.info "Overseer assessment for task_def_id: #{task_definition.id} task_id: #{task.id} was performed" + comment = overseer_assessment.send_to_overseer + return { updated_task: TaskUpdateSerializer.new(task), comment: comment } + end - overseer_assessment = OverseerAssessment.create_for(task) - if overseer_assessment.present? - logger.info "Overseer assessment for task_def_id: #{task_definition.id} task_id: #{task.id} was performed" - comment = overseer_assessment.send_to_overseer - return { updated_task: TaskUpdateSerializer.new(task), comment: comment } - end + logger.info "Overseer assessment for task_def_id: #{task_definition.id} task_id: #{task.id} was not performed" - logger.info "Overseer assessment for task_def_id: #{task_definition.id} task_id: #{task.id} was not performed" + present task, with: Entities::TaskEntity, update_only: true + end # post - present task, with: Api::Entities::TaskEntity, update_only: true - end # post + desc 'Retrieve submission document included for the task id' + params do + optional :as_attachment, type: Boolean, desc: 'Whether or not to download file as attachment. Default is false.' + end + get '/projects/:id/task_def_id/:task_definition_id/submission' do + project = Project.find(params[:id]) + task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - desc 'Retrieve submission document included for the task id' - params do - optional :as_attachment, type: Boolean, desc: 'Whether or not to download file as attachment. Default is false.' + # check the user can put this task + unless authorise? current_user, project, :get_submission + error!({ error: "Not authorised to get task '#{task_definition.name}'" }, 401) end - get '/projects/:id/task_def_id/:task_definition_id/submission' do - project = Project.find(params[:id]) - task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - # check the user can put this task - unless authorise? current_user, project, :get_submission - error!({ error: "Not authorised to get task '#{task_definition.name}'" }, 401) - end + task = project.task_for_task_definition(task_definition) - task = project.task_for_task_definition(task_definition) + evidence_loc = task.portfolio_evidence_path + student = task.project.student + unit = task.project.unit - evidence_loc = task.portfolio_evidence_path - student = task.project.student - unit = task.project.unit + if task.processing_pdf? + evidence_loc = Rails.root.join('public', 'resources', 'AwaitingProcessing.pdf') + filename='AwaitingProcessing.pdf' + elsif evidence_loc.nil? + evidence_loc = Rails.root.join('public', 'resources', 'FileNotFound.pdf') + filename='FileNotFound.pdf' + else + filename="#{task.task_definition.abbreviation}.pdf" + end - if task.processing_pdf? - evidence_loc = Rails.root.join('public', 'resources', 'AwaitingProcessing.pdf') - filename='AwaitingProcessing.pdf' - elsif evidence_loc.nil? - evidence_loc = Rails.root.join('public', 'resources', 'FileNotFound.pdf') - filename='FileNotFound.pdf' - else - filename="#{task.task_definition.abbreviation}.pdf" - end + if params[:as_attachment] + header['Content-Disposition'] = "attachment; filename=#{filename}" + header['Access-Control-Expose-Headers'] = 'Content-Disposition' + end - if params[:as_attachment] - header['Content-Disposition'] = "attachment; filename=#{filename}" - header['Access-Control-Expose-Headers'] = 'Content-Disposition' - end + # Set download headers... + content_type 'application/pdf' + env['api.format'] = :binary - # Set download headers... - content_type 'application/pdf' - env['api.format'] = :binary + File.read(evidence_loc) + end # get - File.read(evidence_loc) - end # get + desc "Request for a task's documents to be re-processed tp recreate the task's PDF" + put '/projects/:id/task_def_id/:task_definition_id/submission' do + project = Project.find(params[:id]) + task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - desc "Request for a task's documents to be re-processed tp recreate the task's PDF" - put '/projects/:id/task_def_id/:task_definition_id/submission' do - project = Project.find(params[:id]) - task_definition = project.unit.task_definitions.find(params[:task_definition_id]) + unless authorise? current_user, project, :get_submission + error!({ error: "Not authorised to get task '#{task_definition.name}'" }, 401) + end - unless authorise? current_user, project, :get_submission - error!({ error: "Not authorised to get task '#{task_definition.name}'" }, 401) - end + task = project.task_for_task_definition(task_definition) - task = project.task_for_task_definition(task_definition) + if task && PortfolioEvidence.recreate_task_pdf(task) + result = 'done' + else + result = 'false' + end - if task && PortfolioEvidence.recreate_task_pdf(task) - result = 'done' - else - result = 'false' - end + present :result, result, with: Grape::Presenters::Presenter + end # put - present :result, result, with: Grape::Presenters::Presenter - end # put + desc 'Get the timestamps of the last 10 submissions of a task' + get '/projects/:id/task_def_id/:task_definition_id/submissions/timestamps' do + project = Project.find(params[:id]) + task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - desc 'Get the timestamps of the last 10 submissions of a task' - get '/projects/:id/task_def_id/:task_definition_id/submissions/timestamps' do - project = Project.find(params[:id]) - task_definition = project.unit.task_definitions.find(params[:task_definition_id]) + unless authorise? current_user, project, :get_submission + error!({ error: "Not authorised to get task '#{task_definition.name}'" }, 401) + end - unless authorise? current_user, project, :get_submission - error!({ error: "Not authorised to get task '#{task_definition.name}'" }, 401) - end + task = project.task_for_task_definition(task_definition) - task = project.task_for_task_definition(task_definition) + unless task + error!({ error: "A submission for this task definition have never been created" }, 401) + end + + result = OverseerAssessment.where(task_id: task.id).order(submission_timestamp: :desc).limit(10) + present result, with: Entities::OverseerAssessmentEntity + end - unless task - error!({ error: "A submission for this task definition have never been created" }, 401) - end + desc 'Trigger an overseer assessment to run again' + put '/projects/:id/task_def_id/:task_definition_id/overseer_assessment/:oa_id/trigger' do + project = Project.find(params[:id]) + task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - OverseerAssessment.where(task_id: task.id).order(submission_timestamp: :desc).limit(10) + unless authorise? current_user, project, :get_submission + error!({ error: "Not authorised to get task '#{task_definition.name}'" }, 401) end - desc 'Trigger an overseer assessment to run again' - put '/projects/:id/task_def_id/:task_definition_id/overseer_assessment/:oa_id/trigger' do - project = Project.find(params[:id]) - task_definition = project.unit.task_definitions.find(params[:task_definition_id]) + task = project.task_for_task_definition(task_definition) - unless authorise? current_user, project, :get_submission - error!({ error: "Not authorised to get task '#{task_definition.name}'" }, 401) - end + unless task + error!({ error: "A submission for this task definition have never been created" }, 401) + end - task = project.task_for_task_definition(task_definition) + oa_id = timestamp = params[:oa_id] - unless task - error!({ error: "A submission for this task definition have never been created" }, 401) - end + oa = task.overseer_assessments.find(oa_id) + result = oa.send_to_overseer - oa_id = timestamp = params[:oa_id] + present result, with: Entities::CommentEntity + end - oa = task.overseer_assessments.find(oa_id) - result = oa.send_to_overseer + desc 'Get the result of the submission of a task made at the given timestamp' + get '/projects/:id/task_def_id/:task_definition_id/submissions/timestamps/:timestamp' do + project = Project.find(params[:id]) + task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - result + unless authorise? current_user, project, :get_submission + error!({ error: "Not authorised to get task '#{task_definition.name}'" }, 401) end - desc 'Get the result of the submission of a task made at the given timestamp' - get '/projects/:id/task_def_id/:task_definition_id/submissions/timestamps/:timestamp' do - project = Project.find(params[:id]) - task_definition = project.unit.task_definitions.find(params[:task_definition_id]) + task = project.task_for_task_definition(task_definition) - unless authorise? current_user, project, :get_submission - error!({ error: "Not authorised to get task '#{task_definition.name}'" }, 401) - end + unless task + error!({ error: "A submission for this task definition have never been created" }, 401) + end - task = project.task_for_task_definition(task_definition) + timestamp = params[:timestamp] - unless task - error!({ error: "A submission for this task definition have never been created" }, 401) - end + path = FileHelper.task_submission_identifier_path_with_timestamp(:done, task, timestamp) + unless File.exist? path + error!({ error: "No submissions found for project: '#{params[:id]}' task: '#{params[:task_def_id]}' and timestamp: '#{timestamp}'" }, 401) + end - timestamp = params[:timestamp] + unless File.exist? "#{path}/output.txt" + error!({ error: "There is no output for this assessment. Either the output wasn't generated, or processing failed. Please review your submission, and discuss with the teaching team if issues persist." }, 401) + end - path = FileHelper.task_submission_identifier_path_with_timestamp(:done, task, timestamp) - unless File.exist? path - error!({ error: "No submissions found for project: '#{params[:id]}' task: '#{params[:task_def_id]}' and timestamp: '#{timestamp}'" }, 401) - end + result = [] + result << { label: 'output', result: File.read("#{path}/output.txt") } - unless File.exist? "#{path}/output.txt" - error!({ error: "There is no output for this assessment. Either the output wasn't generated, or processing failed. Please review your submission, and discuss with the teaching team if issues persist." }, 401) - end + if project.role_for(current_user) == :student + return result + end - result = [] - result << { label: 'output', result: File.read("#{path}/output.txt") } + if File.exist? "#{path}/build-diff.txt" + result << { label: 'build-diff', result: File.read("#{path}/build-diff.txt") } + end - if project.role_for(current_user) == :student - return result - end + if File.exist? "#{path}/run-diff.txt" + result << { label: 'run-diff', result: File.read("#{path}/run-diff.txt") } + end - if File.exist? "#{path}/build-diff.txt" - result << { label: 'build-diff', result: File.read("#{path}/build-diff.txt") } - end + present result, with: Grape::Presenters::Presenter + end - if File.exist? "#{path}/run-diff.txt" - result << { label: 'run-diff', result: File.read("#{path}/run-diff.txt") } - end + desc 'Get the result of the submission of a task made last' + get '/projects/:id/task_def_id/:task_definition_id/submissions/latest' do + project = Project.find(params[:id]) + task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - result + unless authorise? current_user, project, :get_submission + error!({ error: "Not authorised to get task '#{task_definition.name}'" }, 401) end - desc 'Get the result of the submission of a task made last' - get '/projects/:id/task_def_id/:task_definition_id/submissions/latest' do - project = Project.find(params[:id]) - task_definition = project.unit.task_definitions.find(params[:task_definition_id]) + task = project.task_for_task_definition(task_definition) - unless authorise? current_user, project, :get_submission - error!({ error: "Not authorised to get task '#{task_definition.name}'" }, 401) - end - - task = project.task_for_task_definition(task_definition) + unless task + error!({ error: "A submission for this task definition have never been created" }, 401) + end - unless task - error!({ error: "A submission for this task definition have never been created" }, 401) - end + path = FileHelper.task_submission_identifier_path(:done, task) + unless File.exist? path + error!({ error: "No submissions found for project: '#{params[:id]}' task: '#{params[:task_def_id]}'" }, 401) + end - path = FileHelper.task_submission_identifier_path(:done, task) - unless File.exist? path - error!({ error: "No submissions found for project: '#{params[:id]}' task: '#{params[:task_def_id]}'" }, 401) - end + path = "#{path}/#{FileHelper.latest_submission_timestamp_entry_in_dir(path)}" - path = "#{path}/#{FileHelper.latest_submission_timestamp_entry_in_dir(path)}" + unless File.exist? "#{path}/output.txt" + error!({ error: "There is no output for this assessment. Either the output wasn't generated, or processing failed. Please review your submission, and discuss with the teaching team if issues persist." }, 401) + end - unless File.exist? "#{path}/output.txt" - error!({ error: "There is no output for this assessment. Either the output wasn't generated, or processing failed. Please review your submission, and discuss with the teaching team if issues persist." }, 401) - end + result = [] + result << { label: 'output', result: File.read("#{path}/output.txt") } - result = [] - result << { label: 'output', result: File.read("#{path}/output.txt") } + if project.role_for(current_user) == :student + present result, with: Grape::Presenters::Presenter + return + end - if project.role_for(current_user) == :student - return result - end + if File.exist? "#{path}/build-diff.txt" + result << { label: 'build-diff', result: File.read("#{path}/build-diff.txt") } + end - if File.exist? "#{path}/build-diff.txt" - result << { label: 'build-diff', result: File.read("#{path}/build-diff.txt") } - end + if File.exist? "#{path}/run-diff.txt" + result << { label: 'run-diff', result: File.read("#{path}/run-diff.txt") } + end - if File.exist? "#{path}/run-diff.txt" - result << { label: 'run-diff', result: File.read("#{path}/run-diff.txt") } - end + present result, with: Grape::Presenters::Presenter + end - result + # TODO: Remove the dependency on units - figure out how to authorise + desc 'Get the list of supported overseer images' + get '/units/:unit_id/overseer/docker/images' do + unless Doubtfire::Application.config.overseer_enabled + error!({ error: 'Overseer is not enabled' }, 403) end - # TODO: Remove the dependency on units - figure out how to authorise - desc 'Get the list of supported overseer images' - get '/units/:unit_id/overseer/docker/images' do - unless Doubtfire::Application.config.overseer_enabled - error!({ error: 'Overseer is not enabled' }, 403) - return - end - - unit = Unit.find(params[:unit_id]) - - unless authorise? current_user, unit, :add_task_def - error!({ error: 'Not authorised to download task details of unit' }, 403) - end - { - result: Doubtfire::Application.config.overseer_images - } + unit = Unit.find(params[:unit_id]) + + unless authorise? current_user, unit, :add_task_def + error!({ error: 'Not authorised to download task details of unit' }, 403) end + + result = { + result: Doubtfire::Application.config.overseer_images + } + + present result, with: Grape::Presenters::Presenter end end end diff --git a/app/api/task_comments_api.rb b/app/api/task_comments_api.rb index f7f390c2e..897245aa8 100644 --- a/app/api/task_comments_api.rb +++ b/app/api/task_comments_api.rb @@ -1,190 +1,192 @@ require 'grape' -module Api - class TaskCommentsApi < Grape::API - helpers AuthenticationHelpers - helpers AuthorisationHelpers +class TaskCommentsApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers - before do - authenticated? - end - - desc 'Add a new comment to a task' - params do - optional :comment, type: String, desc: 'The comment text to add to the task' - optional :attachment, type: File, desc: 'Image, sound, PDF or video comment file' - optional :reply_to_id, type: Integer, desc: 'The comment to which this comment is replying' - end - post '/projects/:project_id/task_def_id/:task_definition_id/comments', serializer: TaskCommentSerializer do - project = Project.find(params[:project_id]) - task_definition = project.unit.task_definitions.find(params[:task_definition_id]) + before do + authenticated? + end - unless authorise? current_user, project, :make_submission - error!({ error: 'Not authorised to create a comment for this task' }, 403) - end + desc 'Add a new comment to a task' + params do + optional :comment, type: String, desc: 'The comment text to add to the task' + optional :attachment, type: File, desc: 'Image, sound, PDF or video comment file' + optional :reply_to_id, type: Integer, desc: 'The comment to which this comment is replying' + end + post '/projects/:project_id/task_def_id/:task_definition_id/comments' do + project = Project.find(params[:project_id]) + task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - text_comment = params[:comment] - attached_file = params[:attachment] - reply_to_id = params[:reply_to_id] + unless authorise? current_user, project, :make_submission + error!({ error: 'Not authorised to create a comment for this task' }, 403) + end - if attached_file.present? - error!({error: "Attachment is empty."}) unless File.size?(attached_file["tempfile"].path).present? - error!({error: "Attachment exceeds the maximum attachment size of 30MB."}) unless File.size?(attached_file["tempfile"].path) < 30_000_000 - end + text_comment = params[:comment] + attached_file = params[:attachment] + reply_to_id = params[:reply_to_id] - task = project.task_for_task_definition(task_definition) - type_string = content_type.to_s + if attached_file.present? + error!({error: "Attachment is empty."}) unless File.size?(attached_file["tempfile"].path).present? + error!({error: "Attachment exceeds the maximum attachment size of 30MB."}) unless File.size?(attached_file["tempfile"].path) < 30_000_000 + end - if reply_to_id.present? - originalTaskComment = TaskComment.find(reply_to_id) - error!(error: 'You do not have permission to read the replied comment') unless authorise?(current_user, originalTaskComment.project, :get) || (task.group_task? && task.group.role_for(current_user) != nil) - error!(error: 'Original comment is not in this task.') unless task.all_comments.find(reply_to_id).present? - end + task = project.task_for_task_definition(task_definition) + type_string = content_type.to_s - logger.info("#{current_user.username} - added comment for task #{task.id} (#{task_definition.abbreviation})") + if reply_to_id.present? + originalTaskComment = TaskComment.find(reply_to_id) + error!(error: 'You do not have permission to read the replied comment') unless authorise?(current_user, originalTaskComment.project, :get) || (task.group_task? && task.group.role_for(current_user) != nil) + error!(error: 'Original comment is not in this task.') unless task.all_comments.find(reply_to_id).present? + end - if attached_file.nil? || attached_file.empty? - error!({ error: 'Comment text is empty, unable to add new comment' }, 403) unless text_comment.present? - result = task.add_text_comment(current_user, text_comment, reply_to_id) - else - unless FileHelper.accept_file(attached_file, 'comment attachment - TaskComment', 'comment_attachment') - error!({ error: 'Please upload only images, audio or PDF documents' }, 403) - end + logger.info("#{current_user.username} - added comment for task #{task.id} (#{task_definition.abbreviation})") - result = task.add_comment_with_attachment(current_user, attached_file, reply_to_id) + if attached_file.nil? || attached_file.empty? + error!({ error: 'Comment text is empty, unable to add new comment' }, 403) unless text_comment.present? + result = task.add_text_comment(current_user, text_comment, reply_to_id) + else + unless FileHelper.accept_file(attached_file, 'comment attachment - TaskComment', 'comment_attachment') + error!({ error: 'Please upload only images, audio or PDF documents' }, 403) end - if result.nil? - error!({ error: 'No comment added. Comment duplicates last comment, so ignored.' }, 403) - else - result.serialize(current_user) - end + result = task.add_comment_with_attachment(current_user, attached_file, reply_to_id) end - desc 'Get an attachment related to a task comment' - params do - optional :as_attachment, type: Boolean, desc: 'Whether or not to download file as attachment. Default is false.' + if result.nil? + error!({ error: 'No comment added. Comment duplicates last comment, so ignored.' }, 403) + else + present result, with: Entities::CommentEntity, current_user: current_user end - get '/projects/:project_id/task_def_id/:task_definition_id/comments/:id' do - project = Project.find(params[:project_id]) - task_definition = project.unit.task_definitions.find(params[:task_definition_id]) + end - unless authorise? current_user, project, :get - error!({ error: 'You cannot read the comments for this task' }, 403) - end + desc 'Get an attachment related to a task comment' + params do + optional :as_attachment, type: Boolean, desc: 'Whether or not to download file as attachment. Default is false.' + end + get '/projects/:project_id/task_def_id/:task_definition_id/comments/:id' do + project = Project.find(params[:project_id]) + task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - if project.has_task_for_task_definition? task_definition - task = project.task_for_task_definition(task_definition) + unless authorise? current_user, project, :get + error!({ error: 'You cannot read the comments for this task' }, 403) + end - comment = task.comments.find(params[:id]) + if project.has_task_for_task_definition? task_definition + task = project.task_for_task_definition(task_definition) - error!({ error: 'No attachment for this comment.' }, 404) unless %w(audio image pdf).include? comment.content_type + comment = task.comments.find(params[:id]) - error!({ error: 'File missing' }, 404) unless File.exist? comment.attachment_path + error!({ error: 'No attachment for this comment.' }, 404) unless %w(audio image pdf).include? comment.content_type - # Set return content type - content_type comment.attachment_mime_type + error!({ error: 'File missing' }, 404) unless File.exist? comment.attachment_path - env['api.format'] = :binary + # Set return content type + content_type comment.attachment_mime_type - # mark as attachment - if params[:as_attachment] - header['Content-Disposition'] = "attachment; filename=#{comment.attachment_file_name}" - end + env['api.format'] = :binary - # Work out what part to return - file_size = File.size(comment.attachment_path) - begin_point = 0 - end_point = file_size - 1 + # mark as attachment + if params[:as_attachment] + header['Content-Disposition'] = "attachment; filename=#{comment.attachment_file_name}" + end - # Was it asked for just a part of the file? - if request.headers['Range'] - # indicate partial content - status 206 + # Work out what part to return + file_size = File.size(comment.attachment_path) + begin_point = 0 + end_point = file_size - 1 - # extract part desired from the content - if request.headers['Range'] =~ /bytes\=(\d+)\-(\d*)/ - begin_point = Regexp.last_match(1).to_i - end_point = Regexp.last_match(2).to_i if Regexp.last_match(2).present? - end + # Was it asked for just a part of the file? + if request.headers['Range'] + # indicate partial content + status 206 - end_point = file_size - 1 unless end_point < file_size - 1 + # extract part desired from the content + if request.headers['Range'] =~ /bytes\=(\d+)\-(\d*)/ + begin_point = Regexp.last_match(1).to_i + end_point = Regexp.last_match(2).to_i if Regexp.last_match(2).present? end - # Return the requested content - content_length = end_point - begin_point + 1 - header['Content-Range'] = "bytes #{begin_point}-#{end_point}/#{file_size}" - header['Content-Length'] = content_length.to_s - header['Accept-Ranges'] = 'bytes' - - # Read the binary data and return - IO.binread(comment.attachment_path, content_length, begin_point) + end_point = file_size - 1 unless end_point < file_size - 1 end + + # Return the requested content + content_length = end_point - begin_point + 1 + header['Content-Range'] = "bytes #{begin_point}-#{end_point}/#{file_size}" + header['Content-Length'] = content_length.to_s + header['Accept-Ranges'] = 'bytes' + + # Read the binary data and return + IO.binread(comment.attachment_path, content_length, begin_point) end + end - desc 'Get the comments related to a task' - get '/projects/:project_id/task_def_id/:task_definition_id/comments' do - project = Project.find(params[:project_id]) - task_definition = project.unit.task_definitions.find(params[:task_definition_id]) + desc 'Get the comments related to a task' + get '/projects/:project_id/task_def_id/:task_definition_id/comments' do + project = Project.find(params[:project_id]) + task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - unless authorise? current_user, project, :get - error!({ error: 'You cannot read the comments for this task' }, 403) - end + unless authorise? current_user, project, :get + error!({ error: 'You cannot read the comments for this task' }, 403) + end - if project.has_task_for_task_definition? task_definition - task = project.task_for_task_definition(task_definition) + if project.has_task_for_task_definition? task_definition + task = project.task_for_task_definition(task_definition) - comments = task.all_comments.order('created_at ASC') - result = task.comments_for_user(current_user) - result.each do |d| end # cache results... + comments = task.all_comments.order('created_at ASC') + result = task.comments_for_user(current_user) + result.each do |d| end # cache results... - # mark every comment type except for DiscussionComments so we don't mark it as read. - comments_to_mark_as_read = comments.where("TYPE is null OR TYPE != 'DiscussionComment'") - task.mark_comments_as_read(current_user, comments_to_mark_as_read) - else - result = [] - end - present result, with: Api::Entities::CommentEntity, current_user: current_user + # mark every comment type except for DiscussionComments so we don't mark it as read. + comments_to_mark_as_read = comments.where("TYPE is null OR TYPE != 'DiscussionComment'") + task.mark_comments_as_read(current_user, comments_to_mark_as_read) + else + result = [] end + present result, with: Entities::CommentEntity, current_user: current_user + end - desc 'Delete a comment' - delete '/projects/:project_id/task_def_id/:task_definition_id/comments/:id' do - project = Project.find(params[:project_id]) - task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - - unless authorise? current_user, project, :get - error!({ error: 'You cannot read the comments for this task' }, 403) - end + desc 'Delete a comment' + delete '/projects/:project_id/task_def_id/:task_definition_id/comments/:id' do + project = Project.find(params[:project_id]) + task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - task = project.task_for_task_definition(task_definition) - task_comment = task.all_comments.find(params[:id]) + unless authorise? current_user, project, :get + error!({ error: 'You cannot read the comments for this task' }, 403) + end - key = if current_user == task_comment.user - :delete_own_comment - else - :delete_other_comment - end + task = project.task_for_task_definition(task_definition) + task_comment = task.all_comments.find(params[:id]) - unless authorise? current_user, task, key - error!({ error: 'Not authorised to delete this comment' }, 403) - end + key = if current_user == task_comment.user + :delete_own_comment + else + :delete_other_comment + end - task_comment.destroy + unless authorise? current_user, task, key + error!({ error: 'Not authorised to delete this comment' }, 403) end - desc 'Mark a comment as unread' - post '/projects/:project_id/task_def_id/:task_definition_id/comments/:id' do - project = Project.find(params[:project_id]) - task_definition = project.unit.task_definitions.find(params[:task_definition_id]) + task_comment.destroy - unless authorise? current_user, project, :make_submission - error!({ error: 'Not authorised to mark comment as unread' }, 403) - end + present false + end - task = project.task_for_task_definition(task_definition) + desc 'Mark a comment as unread' + post '/projects/:project_id/task_def_id/:task_definition_id/comments/:id' do + project = Project.find(params[:project_id]) + task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - task_comment = task.comments.find(params[:id]) - task_comment.mark_as_unread(current_user) + unless authorise? current_user, project, :make_submission + error!({ error: 'Not authorised to mark comment as unread' }, 403) end + + task = project.task_for_task_definition(task_definition) + + task_comment = task.comments.find(params[:id]) + task_comment.mark_as_unread(current_user) + + present task_comment, with: Entities::CommentEntity, current_user: current_user end end diff --git a/app/api/task_definitions_api.rb b/app/api/task_definitions_api.rb index e732d374b..51494c17c 100644 --- a/app/api/task_definitions_api.rb +++ b/app/api/task_definitions_api.rb @@ -1,571 +1,574 @@ require 'grape' -require 'task_serializer' -require 'mime-check-helpers' - -module Api - class TaskDefinitionsApi < Grape::API - helpers AuthenticationHelpers - helpers AuthorisationHelpers - helpers FileHelper - helpers MimeCheckHelpers - helpers Submission::GenerateHelpers - - before do - authenticated? - end - - desc 'Add a new task definition to the given unit' - params do - requires :task_def, type: Hash do - optional :tutorial_stream_abbr, type: String, desc: 'The abbreviation of tutorial stream' - requires :name, type: String, desc: 'The name of this task def' - requires :description, type: String, desc: 'The description of this task def' - requires :weighting, type: Integer, desc: 'The weighting of this task' - requires :target_grade, type: Integer, desc: 'Minimum grade for task' - optional :group_set_id, type: Integer, desc: 'Related group set' - requires :start_date, type: Date, desc: 'The date when the task should be started' - requires :target_date, type: Date, desc: 'The date when the task is due' - optional :due_date, type: Date, desc: 'The deadline date' - requires :abbreviation, type: String, desc: 'The abbreviation of the task' - requires :restrict_status_updates, type: Boolean, desc: 'Restrict updating of the status to staff' - optional :upload_requirements, type: String, desc: 'Task file upload requirements' - optional :plagiarism_checks, type: String, desc: 'The list of checks to perform' - requires :plagiarism_warn_pct, type: Integer, desc: 'The percent at which to record and warn about plagiarism' - requires :is_graded, type: Boolean, desc: 'Whether or not this task definition is a graded task' - requires :max_quality_pts, type: Integer, desc: 'A range for quality points when quality is assessed' - optional :assessment_enabled, type: Boolean, desc: 'Enable or disable assessment' - optional :overseer_image_id, type: Integer, desc: 'The id of the Docker image for overseer' - end - end - post '/units/:unit_id/task_definitions/' do - unit = Unit.find(params[:unit_id]) - unless authorise? current_user, unit, :add_task_def - error!({ error: 'Not authorised to create a task definition of this unit' }, 403) - end +class TaskDefinitionsApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers + helpers FileHelper + helpers MimeCheckHelpers + helpers Submission::GenerateHelpers - params[:task_def][:upload_requirements] = '[]' if params[:task_def][:upload_requirements].nil? - - task_params = ActionController::Parameters.new(params) - .require(:task_def) - .permit( - :name, - :description, - :weighting, - :target_grade, - :start_date, - :target_date, - :due_date, - :abbreviation, - :restrict_status_updates, - :upload_requirements, - :plagiarism_checks, - :plagiarism_warn_pct, - :is_graded, - :max_quality_pts, - :assessment_enabled, - :overseer_image_id - ) - - task_params[:unit_id] = unit.id - - task_def = TaskDefinition.new(task_params) - - # Set the tutorial stream - tutorial_stream_abbr = params[:task_def][:tutorial_stream_abbr] - unless tutorial_stream_abbr.nil? - tutorial_stream = unit.tutorial_streams.find_by!(abbreviation: tutorial_stream_abbr) - task_def.tutorial_stream = tutorial_stream - end - - # - # Link in group set if specified - # - if params[:task_def][:group_set_id] && params[:task_def][:group_set_id] >= 0 - gs = GroupSet.find(params[:task_def][:group_set_id]) - task_def.group_set = gs if gs.unit == unit - end + before do + authenticated? + end - task_def.save! - task_def - end - - desc 'Edits the given task definition' - params do - requires :id, type: Integer, desc: 'The task id to edit' - requires :task_def, type: Hash do - optional :tutorial_stream_abbr, type: String, desc: 'The abbreviation of the tutorial stream' - optional :name, type: String, desc: 'The name of this task def' - optional :description, type: String, desc: 'The description of this task def' - optional :weighting, type: Integer, desc: 'The weighting of this task' - optional :target_grade, type: Integer, desc: 'Target grade for task' - optional :group_set_id, type: Integer, desc: 'Related group set' - optional :start_date, type: Date, desc: 'The date when the task should be started' - optional :target_date, type: Date, desc: 'The date when the task is due' - optional :due_date, type: Date, desc: 'The deadline date' - optional :abbreviation, type: String, desc: 'The abbreviation of the task' - optional :restrict_status_updates, type: Boolean, desc: 'Restrict updating of the status to staff' - optional :upload_requirements, type: String, desc: 'Task file upload requirements' - optional :plagiarism_checks, type: String, desc: 'The list of checks to perform' - optional :plagiarism_warn_pct, type: Integer, desc: 'The percent at which to record and warn about plagiarism' - optional :is_graded, type: Boolean, desc: 'Whether or not this task definition is a graded task' - optional :max_quality_pts, type: Integer, desc: 'A range for quality points when quality is assessed' - optional :assessment_enabled, type: Boolean, desc: 'Enable or disable assessment' - optional :overseer_image_id, type: Integer, desc: 'The id of the Docker image name for overseer' - end + desc 'Add a new task definition to the given unit' + params do + requires :task_def, type: Hash do + optional :tutorial_stream_abbr, type: String, desc: 'The abbreviation of tutorial stream' + requires :name, type: String, desc: 'The name of this task def' + requires :description, type: String, desc: 'The description of this task def' + requires :weighting, type: Integer, desc: 'The weighting of this task' + requires :target_grade, type: Integer, desc: 'Minimum grade for task' + optional :group_set_id, type: Integer, desc: 'Related group set' + requires :start_date, type: Date, desc: 'The date when the task should be started' + requires :target_date, type: Date, desc: 'The date when the task is due' + optional :due_date, type: Date, desc: 'The deadline date' + requires :abbreviation, type: String, desc: 'The abbreviation of the task' + requires :restrict_status_updates, type: Boolean, desc: 'Restrict updating of the status to staff' + optional :upload_requirements, type: String, desc: 'Task file upload requirements' + optional :plagiarism_checks, type: String, desc: 'The list of checks to perform' + requires :plagiarism_warn_pct, type: Integer, desc: 'The percent at which to record and warn about plagiarism' + requires :is_graded, type: Boolean, desc: 'Whether or not this task definition is a graded task' + requires :max_quality_pts, type: Integer, desc: 'A range for quality points when quality is assessed' + optional :assessment_enabled, type: Boolean, desc: 'Enable or disable assessment' + optional :overseer_image_id, type: Integer, desc: 'The id of the Docker image for overseer' end - put '/units/:unit_id/task_definitions/:id' do - unit = Unit.find(params[:unit_id]) - task_def = unit.task_definitions.find(params[:id]) - - unless authorise? current_user, task_def.unit, :add_task_def - error!({ error: 'Not authorised to create a task definition of this unit' }, 403) - end + end + post '/units/:unit_id/task_definitions/' do + unit = Unit.find(params[:unit_id]) + + unless authorise? current_user, unit, :add_task_def + error!({ error: 'Not authorised to create a task definition of this unit' }, 403) + end + + params[:task_def][:upload_requirements] = '[]' if params[:task_def][:upload_requirements].nil? + + task_params = ActionController::Parameters.new(params) + .require(:task_def) + .permit( + :name, + :description, + :weighting, + :target_grade, + :start_date, + :target_date, + :due_date, + :abbreviation, + :restrict_status_updates, + :upload_requirements, + :plagiarism_checks, + :plagiarism_warn_pct, + :is_graded, + :max_quality_pts, + :assessment_enabled, + :overseer_image_id + ) + + task_params[:unit_id] = unit.id + + task_def = TaskDefinition.new(task_params) + + # Set the tutorial stream + tutorial_stream_abbr = params[:task_def][:tutorial_stream_abbr] + unless tutorial_stream_abbr.nil? + tutorial_stream = unit.tutorial_streams.find_by!(abbreviation: tutorial_stream_abbr) + task_def.tutorial_stream = tutorial_stream + end + + # + # Link in group set if specified + # + if params[:task_def][:group_set_id] && params[:task_def][:group_set_id] >= 0 + gs = GroupSet.find(params[:task_def][:group_set_id]) + task_def.group_set = gs if gs.unit == unit + end + + task_def.save! + present task_def, with: Entities::TaskDefinitionEntity + end - task_params = ActionController::Parameters.new(params) - .require(:task_def) - .permit( - :name, - :description, - :weighting, - :target_grade, - :start_date, - :target_date, - :due_date, - :abbreviation, - :restrict_status_updates, - :upload_requirements, - :plagiarism_checks, - :plagiarism_warn_pct, - :is_graded, - :max_quality_pts, - :assessment_enabled, - :overseer_image_id - ) - - # Ensure changes to a TD defined as a "draft task definition" are validated - if unit.draft_task_definition_id == params[:id] - if params[:task_def][:upload_requirements] - requirements = JSON.parse(params[:task_def][:upload_requirements]) - if requirements.length != 1 || requirements[0]["type"] != "document" - error!({ error: 'Task is marked as the draft learning summary task definition. A draft learning summary task can only contain a single document upload.' }, 403) - end + desc 'Edits the given task definition' + params do + requires :id, type: Integer, desc: 'The task id to edit' + requires :task_def, type: Hash do + optional :tutorial_stream_abbr, type: String, desc: 'The abbreviation of the tutorial stream' + optional :name, type: String, desc: 'The name of this task def' + optional :description, type: String, desc: 'The description of this task def' + optional :weighting, type: Integer, desc: 'The weighting of this task' + optional :target_grade, type: Integer, desc: 'Target grade for task' + optional :group_set_id, type: Integer, desc: 'Related group set' + optional :start_date, type: Date, desc: 'The date when the task should be started' + optional :target_date, type: Date, desc: 'The date when the task is due' + optional :due_date, type: Date, desc: 'The deadline date' + optional :abbreviation, type: String, desc: 'The abbreviation of the task' + optional :restrict_status_updates, type: Boolean, desc: 'Restrict updating of the status to staff' + optional :upload_requirements, type: String, desc: 'Task file upload requirements' + optional :plagiarism_checks, type: String, desc: 'The list of checks to perform' + optional :plagiarism_warn_pct, type: Integer, desc: 'The percent at which to record and warn about plagiarism' + optional :is_graded, type: Boolean, desc: 'Whether or not this task definition is a graded task' + optional :max_quality_pts, type: Integer, desc: 'A range for quality points when quality is assessed' + optional :assessment_enabled, type: Boolean, desc: 'Enable or disable assessment' + optional :overseer_image_id, type: Integer, desc: 'The id of the Docker image name for overseer' + end + end + put '/units/:unit_id/task_definitions/:id' do + unit = Unit.find(params[:unit_id]) + task_def = unit.task_definitions.find(params[:id]) + + unless authorise? current_user, task_def.unit, :add_task_def + error!({ error: 'Not authorised to create a task definition of this unit' }, 403) + end + + task_params = ActionController::Parameters.new(params) + .require(:task_def) + .permit( + :name, + :description, + :weighting, + :target_grade, + :start_date, + :target_date, + :due_date, + :abbreviation, + :restrict_status_updates, + :upload_requirements, + :plagiarism_checks, + :plagiarism_warn_pct, + :is_graded, + :max_quality_pts, + :assessment_enabled, + :overseer_image_id + ) + + # Ensure changes to a TD defined as a "draft task definition" are validated + if unit.draft_task_definition_id == params[:id] + if params[:task_def][:upload_requirements] + requirements = JSON.parse(params[:task_def][:upload_requirements]) + if requirements.length != 1 || requirements[0]["type"] != "document" + error!({ error: 'Task is marked as the draft learning summary task definition. A draft learning summary task can only contain a single document upload.' }, 403) end end + end - task_def.update!(task_params) + task_def.update!(task_params) - # Set the tutorial stream - tutorial_stream_abbr = params[:task_def][:tutorial_stream_abbr] - unless tutorial_stream_abbr.nil? - tutorial_stream = task_def.unit.tutorial_streams.find_by!(abbreviation: tutorial_stream_abbr) - task_def.tutorial_stream = tutorial_stream - task_def.save! - end + # Set the tutorial stream + tutorial_stream_abbr = params[:task_def][:tutorial_stream_abbr] + unless tutorial_stream_abbr.nil? + tutorial_stream = task_def.unit.tutorial_streams.find_by!(abbreviation: tutorial_stream_abbr) + task_def.tutorial_stream = tutorial_stream + task_def.save! + end - # - # Link in group set if specified - # - if params[:task_def][:group_set_id] - if params[:task_def][:group_set_id] >= 0 - gs = GroupSet.find(params[:task_def][:group_set_id]) - if gs.unit == task_def.unit - task_def.group_set = gs - task_def.save! - end - else - task_def.group_set = nil + # + # Link in group set if specified + # + if params[:task_def][:group_set_id] + if params[:task_def][:group_set_id] >= 0 + gs = GroupSet.find(params[:task_def][:group_set_id]) + if gs.unit == task_def.unit + task_def.group_set = gs task_def.save! end + else + task_def.group_set = nil + task_def.save! end + end - task_def + present task_def, with: Entities::TaskDefinitionEntity + end + + desc 'Upload CSV of task definitions to the provided unit' + params do + requires :file, type: File, desc: 'CSV upload file.' + requires :unit_id, type: Integer, desc: 'The unit to upload tasks to' + end + post '/csv/task_definitions' do + unit = Unit.find(params[:unit_id]) + + unless authorise? current_user, unit, :upload_csv + error!({ error: 'Not authorised to upload CSV of tasks' }, 403) end - desc 'Upload CSV of task definitions to the provided unit' - params do - requires :file, type: File, desc: 'CSV upload file.' - requires :unit_id, type: Integer, desc: 'The unit to upload tasks to' + unless params[:file].present? + error!({ error: "No file uploaded" }, 403) end - post '/csv/task_definitions' do - unit = Unit.find(params[:unit_id]) - unless authorise? current_user, unit, :upload_csv - error!({ error: 'Not authorised to upload CSV of tasks' }, 403) - end + path = params[:file][:tempfile].path - unless params[:file].present? - error!({ error: "No file uploaded" }, 403) - end + # check mime is correct before uploading + ensure_csv!(path) - path = params[:file][:tempfile].path + # Actually import... + unit.import_tasks_from_csv(File.new(path)) + end - # check mime is correct before uploading - ensure_csv!(path) + desc 'Download CSV of all task definitions for the given unit' + params do + requires :unit_id, type: Integer, desc: 'The unit to download tasks from' + end + get '/csv/task_definitions' do + unit = Unit.find(params[:unit_id]) - # Actually import... - unit.import_tasks_from_csv(File.new(path)) + unless authorise? current_user, unit, :download_unit_csv + error!({ error: 'Not authorised to download CSV of tasks' }, 403) end - desc 'Download CSV of all task definitions for the given unit' - params do - requires :unit_id, type: Integer, desc: 'The unit to download tasks from' - end - get '/csv/task_definitions' do - unit = Unit.find(params[:unit_id]) + content_type 'application/octet-stream' + header['Content-Disposition'] = "attachment; filename=#{unit.code}-Tasks.csv " + env['api.format'] = :binary + unit.task_definitions_csv + end - unless authorise? current_user, unit, :download_unit_csv - error!({ error: 'Not authorised to download CSV of tasks' }, 403) - end + desc 'Delete a task definition' + delete '/units/:unit_id/task_definitions/:id' do + task_def = TaskDefinition.find(params[:id]) - content_type 'application/octet-stream' - header['Content-Disposition'] = "attachment; filename=#{unit.code}-Tasks.csv " - env['api.format'] = :binary - unit.task_definitions_csv + unless authorise? current_user, task_def.unit, :add_task_def + error!({ error: 'Not authorised to delete a task definition of this unit' }, 403) end - desc 'Delete a task definition' - delete '/units/:unit_id/task_definitions/:id' do - task_def = TaskDefinition.find(params[:id]) + task_def.destroy + task_def.destroyed? + end - unless authorise? current_user, task_def.unit, :add_task_def - error!({ error: 'Not authorised to delete a task definition of this unit' }, 403) - end + desc 'Upload the task sheet for a given task' + params do + requires :unit_id, type: Integer, desc: 'The related unit' + requires :task_def_id, type: Integer, desc: 'The related task definition' + requires :file, type: File, desc: 'The task sheet pdf' + end + post '/units/:unit_id/task_definitions/:task_def_id/task_sheet' do + unit = Unit.find(params[:unit_id]) - task_def.destroy + unless authorise? current_user, unit, :add_task_def + error!({ error: 'Not authorised to upload tasks of unit' }, 403) end - desc 'Upload the task sheet for a given task' - params do - requires :unit_id, type: Integer, desc: 'The related unit' - requires :task_def_id, type: Integer, desc: 'The related task definition' - requires :file, type: File, desc: 'The task sheet pdf' - end - post '/units/:unit_id/task_definitions/:task_def_id/task_sheet' do - unit = Unit.find(params[:unit_id]) + task_def = unit.task_definitions.find(params[:task_def_id]) - unless authorise? current_user, unit, :add_task_def - error!({ error: 'Not authorised to upload tasks of unit' }, 403) - end + file = params[:file] - task_def = unit.task_definitions.find(params[:task_def_id]) + unless FileHelper.accept_file(file, 'task sheet', 'document') + error!({ error: "'#{file[:name]}' is not a valid #{file[:type]} file" }, 403) + end - file = params[:file] + # Actually import... + task_def.add_task_sheet(file[:tempfile].path) + true + end - unless FileHelper.accept_file(file, 'task sheet', 'document') - error!({ error: "'#{file[:name]}' is not a valid #{file[:type]} file" }, 403) - end + desc 'Test overseer assessment for a given task' + params do + requires :unit_id, type: Integer, desc: 'The related unit' + requires :task_def_id, type: Integer, desc: 'The related task definition' + optional :file0, type: Rack::Multipart::UploadedFile, desc: 'file 0.' + optional :file1, type: Rack::Multipart::UploadedFile, desc: 'file 1.' + # This API accepts more than 2 files, file0 and file1 are just examples. + end + post '/units/:unit_id/task_definitions/:task_def_id/test_overseer_assessment' do + logger.info "********* - Starting overseer test" + return 'Overseer is not enabled' if !Doubtfire::Application.config.overseer_enabled - # Actually import... - task_def.add_task_sheet(file[:tempfile].path) - end + unit = Unit.find(params[:unit_id]) - desc 'Test overseer assessment for a given task' - params do - requires :unit_id, type: Integer, desc: 'The related unit' - requires :task_def_id, type: Integer, desc: 'The related task definition' - optional :file0, type: Rack::Multipart::UploadedFile, desc: 'file 0.' - optional :file1, type: Rack::Multipart::UploadedFile, desc: 'file 1.' - # This API accepts more than 2 files, file0 and file1 are just examples. + unless authorise? current_user, unit, :perform_overseer_assessment_test + error!({ error: 'Not authorised to test overseer assessment of tasks of this unit' }, 403) end - post '/units/:unit_id/task_definitions/:task_def_id/test_overseer_assessment' do - logger.info "********* - Starting overseer test" - return 'Overseer is not enabled' if !Doubtfire::Application.config.overseer_enabled - unit = Unit.find(params[:unit_id]) + task_definition = unit.task_definitions.find(params[:task_def_id]) - unless authorise? current_user, unit, :perform_overseer_assessment_test - error!({ error: 'Not authorised to test overseer assessment of tasks of this unit' }, 403) - end + project = Project.where(unit: unit, user: current_user).first - task_definition = unit.task_definitions.find(params[:task_def_id]) + if project.nil? + # Create a project for the unit chair + project = unit.enrol_student(current_user, Campus.first) + end - project = Project.where(unit: unit, user: current_user).first + task = project.task_for_task_definition(task_definition) - if project.nil? - # Create a project for the unit chair - project = unit.enrol_student(current_user, Campus.first) - end + upload_reqs = task.upload_requirements - task = project.task_for_task_definition(task_definition) + # Copy files to be PDFed + task.accept_submission(current_user, scoop_files(params, upload_reqs), current_user, self, nil, 'ready_for_feedback', nil) - upload_reqs = task.upload_requirements + logger.info "********* - about to perform overseer submission" + overseer_assessment = OverseerAssessment.create_for(task) + if overseer_assessment.present? + comment = overseer_assessment.send_to_overseer + logger.info "Overseer assessment for task_def_id: #{task_definition.id} task_id: #{task.id} was performed" + else + logger.info "Overseer assessment for task_def_id: #{task_definition.id} task_id: #{task.id} was not performed" + end - # Copy files to be PDFed - task.accept_submission(current_user, scoop_files(params, upload_reqs), current_user, self, nil, 'ready_for_feedback', nil) + #todo: Do we need to return additional details here? e.g. the comment, and project? + present task, with: Entities::TaskEntity, include_other_projects: true, update_only: true + end - logger.info "********* - about to perform overseer submission" - overseer_assessment = OverseerAssessment.create_for(task) - if overseer_assessment.present? - comment = overseer_assessment.send_to_overseer - logger.info "Overseer assessment for task_def_id: #{task_definition.id} task_id: #{task.id} was performed" - return { updated_task: TaskUpdateSerializer.new(task), comment: comment, project_id: project.id } - end + desc 'Remove the task sheet for a given task' + params do + requires :unit_id, type: Integer, desc: 'The related unit' + requires :task_def_id, type: Integer, desc: 'The related task definition' + end + delete '/units/:unit_id/task_definitions/:task_def_id/task_sheet' do + unit = Unit.find(params[:unit_id]) - logger.info "Overseer assessment for task_def_id: #{task_definition.id} task_id: #{task.id} was not performed" - { updated_task: TaskUpdateSerializer.new(task), project_id: project.id } + unless authorise? current_user, unit, :add_task_def + error!({ error: 'Not authorised to remove task sheets of unit' }, 403) end - desc 'Remove the task sheet for a given task' - params do - requires :unit_id, type: Integer, desc: 'The related unit' - requires :task_def_id, type: Integer, desc: 'The related task definition' - end - delete '/units/:unit_id/task_definitions/:task_def_id/task_sheet' do - unit = Unit.find(params[:unit_id]) + task_def = unit.task_definitions.find(params[:task_def_id]) - unless authorise? current_user, unit, :add_task_def - error!({ error: 'Not authorised to remove task sheets of unit' }, 403) - end + # Actually delete... + task_def.remove_task_sheet() + true + end - task_def = unit.task_definitions.find(params[:task_def_id]) + desc 'Upload the task resources for a given task' + params do + requires :unit_id, type: Integer, desc: 'The related unit' + requires :task_def_id, type: Integer, desc: 'The related task definition' + requires :file, type: File, desc: 'The task resources zip' + end + post '/units/:unit_id/task_definitions/:task_def_id/task_resources' do + unit = Unit.find(params[:unit_id]) - # Actually delete... - task_def.remove_task_sheet() - task_def + unless authorise? current_user, unit, :add_task_def + error!({ error: 'Not authorised to upload tasks of unit' }, 403) end - desc 'Upload the task resources for a given task' - params do - requires :unit_id, type: Integer, desc: 'The related unit' - requires :task_def_id, type: Integer, desc: 'The related task definition' - requires :file, type: File, desc: 'The task resources zip' - end - post '/units/:unit_id/task_definitions/:task_def_id/task_resources' do - unit = Unit.find(params[:unit_id]) + task_def = unit.task_definitions.find(params[:task_def_id]) - unless authorise? current_user, unit, :add_task_def - error!({ error: 'Not authorised to upload tasks of unit' }, 403) - end + unless params[:file].present? + error!({ error: "No file uploaded" }, 403) + end - task_def = unit.task_definitions.find(params[:task_def_id]) + file_path = params[:file][:tempfile].path - unless params[:file].present? - error!({ error: "No file uploaded" }, 403) - end + check_mime_against_list! file_path, 'zip', ['application/zip', 'multipart/x-gzip', 'multipart/x-zip', 'application/x-gzip', 'application/octet-stream'] - file_path = params[:file][:tempfile].path + # Actually import... + task_def.add_task_resources(file_path) + true + end - check_mime_against_list! file_path, 'zip', ['application/zip', 'multipart/x-gzip', 'multipart/x-zip', 'application/x-gzip', 'application/octet-stream'] + desc 'Remove the task resources for a given task' + params do + requires :unit_id, type: Integer, desc: 'The related unit' + requires :task_def_id, type: Integer, desc: 'The related task definition' + end + delete '/units/:unit_id/task_definitions/:task_def_id/task_resources' do + unit = Unit.find(params[:unit_id]) - # Actually import... - task_def.add_task_resources(file_path) + unless authorise? current_user, unit, :add_task_def + error!({ error: 'Not authorised to remove task resources of unit' }, 403) end - desc 'Remove the task resources for a given task' - params do - requires :unit_id, type: Integer, desc: 'The related unit' - requires :task_def_id, type: Integer, desc: 'The related task definition' - end - delete '/units/:unit_id/task_definitions/:task_def_id/task_resources' do - unit = Unit.find(params[:unit_id]) + task_def = unit.task_definitions.find(params[:task_def_id]) - unless authorise? current_user, unit, :add_task_def - error!({ error: 'Not authorised to remove task resources of unit' }, 403) - end + # Actually remove... + task_def.remove_task_resources + true + end - task_def = unit.task_definitions.find(params[:task_def_id]) + desc 'Upload the task assessment resources for a given task' + params do + requires :unit_id, type: Integer, desc: 'The related unit' + requires :task_def_id, type: Integer, desc: 'The related task definition' + requires :file, type: Rack::Multipart::UploadedFile, desc: 'The task assessment resources zip' + end + post '/units/:unit_id/task_definitions/:task_def_id/task_assessment_resources' do + unit = Unit.find(params[:unit_id]) - # Actually remove... - task_def.remove_task_resources - task_def + unless authorise? current_user, unit, :add_task_def + error!({ error: 'Not authorised to upload task assessment resources of unit' }, 403) end - desc 'Upload the task assessment resources for a given task' - params do - requires :unit_id, type: Integer, desc: 'The related unit' - requires :task_def_id, type: Integer, desc: 'The related task definition' - requires :file, type: Rack::Multipart::UploadedFile, desc: 'The task assessment resources zip' - end - post '/units/:unit_id/task_definitions/:task_def_id/task_assessment_resources' do - unit = Unit.find(params[:unit_id]) + task_def = unit.task_definitions.find(params[:task_def_id]) - unless authorise? current_user, unit, :add_task_def - error!({ error: 'Not authorised to upload task assessment resources of unit' }, 403) - end + file_path = params[:file][:tempfile].path - task_def = unit.task_definitions.find(params[:task_def_id]) + check_mime_against_list! file_path, 'zip', ['application/zip', 'multipart/x-gzip', 'multipart/x-zip', 'application/x-gzip', 'application/octet-stream'] - file_path = params[:file][:tempfile].path + # Actually import... + task_def.add_task_assessment_resources(file_path) + true + end - check_mime_against_list! file_path, 'zip', ['application/zip', 'multipart/x-gzip', 'multipart/x-zip', 'application/x-gzip', 'application/octet-stream'] + desc 'Remove the task assessment resources for a given task' + params do + requires :unit_id, type: Integer, desc: 'The related unit' + requires :task_def_id, type: Integer, desc: 'The related task definition' + end + delete '/units/:unit_id/task_definitions/:task_def_id/task_assessment_resources' do + unit = Unit.find(params[:unit_id]) - # Actually import... - task_def.add_task_assessment_resources(file_path) + unless authorise? current_user, unit, :add_task_def + error!({ error: 'Not authorised to remove task assessment resources of unit' }, 403) end - desc 'Remove the task assessment resources for a given task' - params do - requires :unit_id, type: Integer, desc: 'The related unit' - requires :task_def_id, type: Integer, desc: 'The related task definition' - end - delete '/units/:unit_id/task_definitions/:task_def_id/task_assessment_resources' do - unit = Unit.find(params[:unit_id]) + task_def = unit.task_definitions.find(params[:task_def_id]) - unless authorise? current_user, unit, :add_task_def - error!({ error: 'Not authorised to remove task assessment resources of unit' }, 403) - end + # Actually remove... + task_def.remove_task_assessment_resources + true + end - task_def = unit.task_definitions.find(params[:task_def_id]) + desc 'Upload a zip file containing the task pdfs for a given task' + params do + requires :unit_id, type: Integer, desc: 'The unit to upload tasks for' + requires :file, type: File, desc: 'batch file upload' + end + post '/units/:unit_id/task_definitions/task_pdfs' do + unit = Unit.find(params[:unit_id]) - # Actually remove... - task_def.remove_task_assessment_resources - task_def + unless authorise? current_user, unit, :add_task_def + error!({ error: 'Not authorised to upload tasks of unit' }, 403) end - desc 'Upload a zip file containing the task pdfs for a given task' - params do - requires :unit_id, type: Integer, desc: 'The unit to upload tasks for' - requires :file, type: File, desc: 'batch file upload' + unless params[:file].present? + error!({ error: "No file uploaded" }, 403) end - post '/units/:unit_id/task_definitions/task_pdfs' do - unit = Unit.find(params[:unit_id]) - unless authorise? current_user, unit, :add_task_def - error!({ error: 'Not authorised to upload tasks of unit' }, 403) - end + file = params[:file][:tempfile].path - unless params[:file].present? - error!({ error: "No file uploaded" }, 403) - end + check_mime_against_list! file, 'zip', ['application/zip', 'multipart/x-gzip', 'multipart/x-zip', 'application/x-gzip', 'application/octet-stream'] - file = params[:file][:tempfile].path + # Actually import... + unit.import_task_files_from_zip file + end - check_mime_against_list! file, 'zip', ['application/zip', 'multipart/x-gzip', 'multipart/x-zip', 'application/x-gzip', 'application/octet-stream'] + desc 'Download the tasks related to a task definition' + params do + requires :unit_id, type: Integer, desc: 'The unit containing the task definition' + requires :task_def_id, type: Integer, desc: "The task definition's id" + end + get '/units/:unit_id/task_definitions/:task_def_id/tasks' do + unit = Unit.find(params[:unit_id]) + unless authorise? current_user, unit, :provide_feedback + error!({ error: 'Not authorised to access tasks for this unit' }, 403) + end + + # Which task definition is this for + task_def = unit.task_definitions.find(params[:task_def_id]) + + # What stream does this relate to? + stream = task_def.tutorial_stream + + subquery = unit. + tutorial_enrolments. + joins(:tutorial). + where('tutorials.tutorial_stream_id = :sid OR tutorials.tutorial_stream_id IS NULL', sid: (stream.present? ? stream.id : nil)). + select('tutorials.tutorial_stream_id as tutorial_stream_id', 'tutorials.id as tutorial_id', 'project_id').to_sql + + result = unit.student_tasks. + joins(:project). + joins(:task_status). + joins("LEFT OUTER JOIN (#{subquery}) as sq ON sq.project_id = projects.id"). + select('sq.tutorial_stream_id as tutorial_stream_id', 'sq.tutorial_id as tutorial_id', 'project_id', 'tasks.id as id', 'task_definition_id', 'task_statuses.id as status_id', 'completion_date', 'times_assessed', 'submission_date', 'grade'). + where('task_definition_id = :id', id: params[:task_def_id]). + map do |t| + { + project_id: t.project_id, + id: t.id, + task_definition_id: t.task_definition_id, + tutorial_id: t.tutorial_id, + tutorial_stream_id: t.tutorial_stream_id, + status: TaskStatus.id_to_key(t.status_id), + completion_date: t.completion_date, + submission_date: t.submission_date, + times_assessed: t.times_assessed, + similar_to_count: t.similar_to_count, + grade: t.grade + } + end + + present result, with: Grape::Presenters::Presenter + end - # Actually import... - unit.import_task_files_from_zip file - end + desc 'Download the task sheet containing the details related to performing that task' + params do + requires :unit_id, type: Integer, desc: 'The unit to upload tasks for' + requires :task_def_id, type: Integer, desc: 'The task definition to get the pdf of' + optional :as_attachment, type: Boolean, desc: 'Whether or not to download file as attachment. Default is false.' + end + get '/units/:unit_id/task_definitions/:task_def_id/task_pdf' do + unit = Unit.find(params[:unit_id]) + task_def = unit.task_definitions.find(params[:task_def_id]) - desc 'Download the tasks related to a task definition' - params do - requires :unit_id, type: Integer, desc: 'The unit containing the task definition' - requires :task_def_id, type: Integer, desc: "The task definition's id" + unless authorise? current_user, unit, :get_unit + error!({ error: 'Not authorised to download task details of unit' }, 403) end - get '/units/:unit_id/task_definitions/:task_def_id/tasks' do - unit = Unit.find(params[:unit_id]) - unless authorise? current_user, unit, :provide_feedback - error!({ error: 'Not authorised to access tasks for this unit' }, 403) - end - # Which task definition is this for - task_def = unit.task_definitions.find(params[:task_def_id]) - - # What stream does this relate to? - stream = task_def.tutorial_stream - - subquery = unit. - tutorial_enrolments. - joins(:tutorial). - where('tutorials.tutorial_stream_id = :sid OR tutorials.tutorial_stream_id IS NULL', sid: (stream.present? ? stream.id : nil)). - select('tutorials.tutorial_stream_id as tutorial_stream_id', 'tutorials.id as tutorial_id', 'project_id').to_sql - - unit.student_tasks. - joins(:project). - joins(:task_status). - joins("LEFT OUTER JOIN (#{subquery}) as sq ON sq.project_id = projects.id"). - select('sq.tutorial_stream_id as tutorial_stream_id', 'sq.tutorial_id as tutorial_id', 'project_id', 'tasks.id as id', 'task_definition_id', 'task_statuses.id as status_id', 'completion_date', 'times_assessed', 'submission_date', 'grade'). - where('task_definition_id = :id', id: params[:task_def_id]). - map do |t| - { - project_id: t.project_id, - id: t.id, - task_definition_id: t.task_definition_id, - tutorial_id: t.tutorial_id, - tutorial_stream_id: t.tutorial_stream_id, - status: TaskStatus.id_to_key(t.status_id), - completion_date: t.completion_date, - submission_date: t.submission_date, - times_assessed: t.times_assessed, - similar_to_count: t.similar_to_count, - grade: t.grade - } - end + if task_def.has_task_sheet? + path = task_def.task_sheet + filename = "#{task_def.unit.code}-#{task_def.abbreviation}.pdf" + else + path = Rails.root.join('public', 'resources', 'FileNotFound.pdf') + filename = "FileNotFound.pdf" end - desc 'Download the task sheet containing the details related to performing that task' - params do - requires :unit_id, type: Integer, desc: 'The unit to upload tasks for' - requires :task_def_id, type: Integer, desc: 'The task definition to get the pdf of' - optional :as_attachment, type: Boolean, desc: 'Whether or not to download file as attachment. Default is false.' + if params[:as_attachment] + header['Content-Disposition'] = "attachment; filename=#{filename}" end - get '/units/:unit_id/task_definitions/:task_def_id/task_pdf' do - unit = Unit.find(params[:unit_id]) - task_def = unit.task_definitions.find(params[:task_def_id]) - unless authorise? current_user, unit, :get_unit - error!({ error: 'Not authorised to download task details of unit' }, 403) - end - - if task_def.has_task_sheet? - path = task_def.task_sheet - filename = "#{task_def.unit.code}-#{task_def.abbreviation}.pdf" - else - path = Rails.root.join('public', 'resources', 'FileNotFound.pdf') - filename = "FileNotFound.pdf" - end + content_type 'application/pdf' + env['api.format'] = :binary + File.read(path) + end - if params[:as_attachment] - header['Content-Disposition'] = "attachment; filename=#{filename}" - end + desc 'Download the task resources' + params do + requires :unit_id, type: Integer, desc: 'The unit to upload tasks for' + requires :task_def_id, type: Integer, desc: 'The task definition to get the pdf of' + end + get '/units/:unit_id/task_definitions/:task_def_id/task_resources' do + unit = Unit.find(params[:unit_id]) + task_def = unit.task_definitions.find(params[:task_def_id]) - content_type 'application/pdf' - env['api.format'] = :binary - File.read(path) + unless authorise? current_user, unit, :get_unit + error!({ error: 'Not authorised to download task details of unit' }, 403) end - desc 'Download the task resources' - params do - requires :unit_id, type: Integer, desc: 'The unit to upload tasks for' - requires :task_def_id, type: Integer, desc: 'The task definition to get the pdf of' + if task_def.has_task_resources? + path = task_def.task_resources + content_type 'application/octet-stream' + header['Content-Disposition'] = "attachment; filename=#{task_def.abbreviation}-resources.zip" + else + path = Rails.root.join('public', 'resources', 'FileNotFound.pdf') + content_type 'application/pdf' + header['Content-Disposition'] = 'attachment; filename=FileNotFound.pdf' end - get '/units/:unit_id/task_definitions/:task_def_id/task_resources' do - unit = Unit.find(params[:unit_id]) - task_def = unit.task_definitions.find(params[:task_def_id]) - unless authorise? current_user, unit, :get_unit - error!({ error: 'Not authorised to download task details of unit' }, 403) - end + env['api.format'] = :binary + File.read(path) + end - if task_def.has_task_resources? - path = task_def.task_resources - content_type 'application/octet-stream' - header['Content-Disposition'] = "attachment; filename=#{task_def.abbreviation}-resources.zip" - else - path = Rails.root.join('public', 'resources', 'FileNotFound.pdf') - content_type 'application/pdf' - header['Content-Disposition'] = 'attachment; filename=FileNotFound.pdf' - end + desc 'Download the task assessment resources' + params do + requires :unit_id, type: Integer, desc: 'The unit to upload tasks for' + requires :task_def_id, type: Integer, desc: 'The task definition to get the assessment resources of' + end + get '/units/:unit_id/task_definitions/:task_def_id/task_assessment_resources' do + unit = Unit.find(params[:unit_id]) + task_def = unit.task_definitions.find(params[:task_def_id]) - env['api.format'] = :binary - File.read(path) + unless authorise? current_user, unit, :add_task_def + error!({ error: 'Not authorised to download task details of unit' }, 403) end - desc 'Download the task assessment resources' - params do - requires :unit_id, type: Integer, desc: 'The unit to upload tasks for' - requires :task_def_id, type: Integer, desc: 'The task definition to get the assessment resources of' + if task_def.has_task_assessment_resources? + path = task_def.task_assessment_resources + content_type 'application/octet-stream' + header['Content-Disposition'] = "attachment; filename=#{task_def.abbreviation}-assessment-resources.zip" + else + path = Rails.root.join('public', 'resources', 'FileNotFound.pdf') + content_type 'application/pdf' + header['Content-Disposition'] = 'attachment; filename=FileNotFound.pdf' end - get '/units/:unit_id/task_definitions/:task_def_id/task_assessment_resources' do - unit = Unit.find(params[:unit_id]) - task_def = unit.task_definitions.find(params[:task_def_id]) - unless authorise? current_user, unit, :add_task_def - error!({ error: 'Not authorised to download task details of unit' }, 403) - end - - if task_def.has_task_assessment_resources? - path = task_def.task_assessment_resources - content_type 'application/octet-stream' - header['Content-Disposition'] = "attachment; filename=#{task_def.abbreviation}-assessment-resources.zip" - else - path = Rails.root.join('public', 'resources', 'FileNotFound.pdf') - content_type 'application/pdf' - header['Content-Disposition'] = 'attachment; filename=FileNotFound.pdf' - end - - env['api.format'] = :binary - File.read(path) - end + env['api.format'] = :binary + File.read(path) end end diff --git a/app/api/tasks_api.rb b/app/api/tasks_api.rb index 5f8731a51..e825bd291 100644 --- a/app/api/tasks_api.rb +++ b/app/api/tasks_api.rb @@ -1,353 +1,350 @@ require 'grape' -require 'task_serializer' -module Api - class TasksApi < Grape::API - helpers AuthenticationHelpers - helpers AuthorisationHelpers +class TasksApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers - before do - authenticated? - end + before do + authenticated? + end - # - # Tasks only used for the task summary stats view... - # - desc "Get all the current user's tasks" - params do - requires :unit_id, type: Integer, desc: 'Unit to fetch the task details for' + # + # Tasks only used for the task summary stats view... + # + desc "Get all the current user's tasks" + params do + requires :unit_id, type: Integer, desc: 'Unit to fetch the task details for' + end + get '/tasks' do + unit = Unit.find(params[:unit_id]) + + unless authorise? current_user, unit, :get_students + error!({ error: 'You do not have permission to read these task details' }, 403) end - get '/tasks' do - unit = Unit.find(params[:unit_id]) - unless authorise? current_user, unit, :get_students - error!({ error: 'You do not have permission to read these task details' }, 403) - end + result = unit.student_tasks. + joins(:task_status). + joins('LEFT OUTER JOIN tutorial_enrolments ON tutorial_enrolments.project_id = projects.id'). + joins('LEFT OUTER JOIN tutorials ON tutorial_enrolments.tutorial_id = tutorials.id AND (tutorials.tutorial_stream_id = task_definitions.tutorial_stream_id OR tutorials.tutorial_stream_id IS NULL)'). + select( + 'tasks.id', + 'task_statuses.id as status_id', + 'task_definition_id', + 'tutorials.id AS tutorial_id', + 'tutorials.tutorial_stream_id AS tutorial_stream_id' + ). + where('tasks.task_status_id > 1'). + map do |r| + { + id: r.id, + task_definition_id: r.task_definition_id, + status: TaskStatus.id_to_key(r.status_id), + tutorial_id: r.tutorial_id, + tutorial_stream_id: r.tutorial_stream_id + } + end - result = unit.student_tasks. - joins(:task_status). - joins('LEFT OUTER JOIN tutorial_enrolments ON tutorial_enrolments.project_id = projects.id'). - joins('LEFT OUTER JOIN tutorials ON tutorial_enrolments.tutorial_id = tutorials.id AND (tutorials.tutorial_stream_id = task_definitions.tutorial_stream_id OR tutorials.tutorial_stream_id IS NULL)'). - select( - 'tasks.id', - 'task_statuses.id as status_id', - 'task_definition_id', - 'tutorials.id AS tutorial_id', - 'tutorials.tutorial_stream_id AS tutorial_stream_id' - ). - where('tasks.task_status_id > 1'). - map do |r| - { - id: r.id, - task_definition_id: r.task_definition_id, - status: TaskStatus.id_to_key(r.status_id), - tutorial_id: r.tutorial_id, - tutorial_stream_id: r.tutorial_stream_id - } - end - - present result, with: Grape::Presenters::Presenter - end + present result, with: Grape::Presenters::Presenter + end - desc 'Refresh the most frequently changed task details for a project - allowing easy refresh of student details' - params do - requires :project_id, type: Integer, desc: 'The id of the project with the task, or tasks to get' - requires :task_definition_id, type: Integer, desc: 'The id of the task definition to get, when not provided all tasks are returned' + desc 'Refresh the most frequently changed task details for a project - allowing easy refresh of student details' + params do + requires :project_id, type: Integer, desc: 'The id of the project with the task, or tasks to get' + requires :task_definition_id, type: Integer, desc: 'The id of the task definition to get, when not provided all tasks are returned' + end + get '/projects/:project_id/refresh_tasks/:task_definition_id' do + project = Project.find(params[:project_id]) + + unless authorise? current_user, project, :get + error!({ error: 'You do not have permission to access this project' }, 403) end - get '/projects/:project_id/refresh_tasks/:task_definition_id' do - project = Project.find(params[:project_id]) - unless authorise? current_user, project, :get - error!({ error: 'You do not have permission to access this project' }, 403) - end + base = project.tasks - base = project.tasks + if params[:task_definition_id].present? + base = base.where('tasks.task_definition_id = :task_definition_id', task_definition_id: params[:task_definition_id]) + end - if params[:task_definition_id].present? - base = base.where('tasks.task_definition_id = :task_definition_id', task_definition_id: params[:task_definition_id]) + result = base. + map do |task| + { + task_definition_id: task.task_definition_id, + status: TaskStatus.id_to_key(task.task_status_id), + due_date: task.due_date, + extensions: task.extensions + } end - result = base. - map do |task| - { - task_definition_id: task.task_definition_id, - status: TaskStatus.id_to_key(task.task_status_id), - due_date: task.due_date, - extensions: task.extensions - } - end + if params[:task_definition_id].present? + result = result.first + end - if params[:task_definition_id].present? - result = result.first - end + present result, with: Grape::Presenters::Presenter + end - present result, with: Grape::Presenters::Presenter + desc 'Get a similarity match for a given task' + get '/tasks/:id/similarity/:count' do + unless authenticated? + error!({ error: "Not authorised to download details for task '#{params[:id]}'" }, 401) end + task = Task.find(params[:id]) - desc 'Get a similarity match for a given task' - get '/tasks/:id/similarity/:count' do - unless authenticated? - error!({ error: "Not authorised to download details for task '#{params[:id]}'" }, 401) - end - task = Task.find(params[:id]) + unless authorise? current_user, task, :get_submission + error!({ error: "Not authorised to download details for task '#{params[:id]}'" }, 401) + end - unless authorise? current_user, task, :get_submission - error!({ error: "Not authorised to download details for task '#{params[:id]}'" }, 401) - end + match = params[:count].to_i % task.similar_to_count + if match < 0 + error!({ error: 'Invalid match sequence, must be 0 or larger' }, 403) + end - match = params[:count].to_i % task.similar_to_count - if match < 0 - error!({ error: 'Invalid match sequence, must be 0 or larger' }, 403) - end + match_link = task.plagiarism_match_links.order('created_at DESC')[match] + return if match_link.nil? - match_link = task.plagiarism_match_links.order('created_at DESC')[match] - return if match_link.nil? + logger.debug "Plagiarism match link 1: #{match_link}" + other_match_link = match_link.other_party + logger.debug "Plagiarism match link 2: #{other_match_link}" + output = FileHelper.path_to_plagarism_html(match_link) - logger.debug "Plagiarism match link 1: #{match_link}" - other_match_link = match_link.other_party - logger.debug "Plagiarism match link 2: #{other_match_link}" - output = FileHelper.path_to_plagarism_html(match_link) + if output.nil? || !File.exist?(output) + error!({ error: 'No files to download' }, 403) + end - if output.nil? || !File.exist?(output) - error!({ error: 'No files to download' }, 403) - end + if authorise? current_user, match_link.task, :view_plagiarism + student_url = match_link.plagiarism_report_url + end - if authorise? current_user, match_link.task, :view_plagiarism - student_url = match_link.plagiarism_report_url + student_hash = { + username: match_link.student.username, + email: match_link.student.email, + name: match_link.student.name, + tutor: match_link.tutor.name, + tutorial: match_link.tutorial, + html: File.read(output), + url: student_url, + pct: match_link.pct, + dismissed: match_link.dismissed + } + other_student_hash = { + username: nil, + email: nil, + name: nil, + tutor: match_link.other_tutor.name, + tutorial: match_link.other_tutorial, + html: nil, + url: nil, + pct: other_match_link.pct, + dismissed: other_match_link.dismissed + } + + # Check if returning both parties + authorised_to_view_both = authorise? current_user, other_match_link.task, :get_submission + if authorised_to_view_both + other_output = FileHelper.path_to_plagarism_html(other_match_link) + if authorise? current_user, other_match_link.task, :view_plagiarism + other_student_url = other_match_link.plagiarism_report_url end + # Update other_student_hash to include details + other_student_hash[:username] = match_link.other_student.username + other_student_hash[:email] = match_link.other_student.email + other_student_hash[:name] = match_link.other_student.name + other_student_hash[:tutor] = match_link.other_tutor.name + other_student_hash[:tutorial] = match_link.other_tutorial + other_student_hash[:html] = File.read(other_output) + other_student_hash[:url] = other_student_url + other_student_hash[:pct] = other_match_link.pct + other_student_hash[:dismissed] = other_match_link.dismissed + end - student_hash = { - username: match_link.student.username, - email: match_link.student.email, - name: match_link.student.name, - tutor: match_link.tutor.name, - tutorial: match_link.tutorial, - html: File.read(output), - url: student_url, - pct: match_link.pct, - dismissed: match_link.dismissed - } - other_student_hash = { - username: nil, - email: nil, - name: nil, - tutor: match_link.other_tutor.name, - tutorial: match_link.other_tutorial, - html: nil, - url: nil, - pct: other_match_link.pct, - dismissed: other_match_link.dismissed - } - - # Check if returning both parties - authorised_to_view_both = authorise? current_user, other_match_link.task, :get_submission - if authorised_to_view_both - other_output = FileHelper.path_to_plagarism_html(other_match_link) - if authorise? current_user, other_match_link.task, :view_plagiarism - other_student_url = other_match_link.plagiarism_report_url - end - # Update other_student_hash to include details - other_student_hash[:username] = match_link.other_student.username - other_student_hash[:email] = match_link.other_student.email - other_student_hash[:name] = match_link.other_student.name - other_student_hash[:tutor] = match_link.other_tutor.name - other_student_hash[:tutorial] = match_link.other_tutorial - other_student_hash[:html] = File.read(other_output) - other_student_hash[:url] = other_student_url - other_student_hash[:pct] = other_match_link.pct - other_student_hash[:dismissed] = other_match_link.dismissed - end + result = { + student: student_hash, + other_student: other_student_hash + } - result = { - student: student_hash, - other_student: other_student_hash - } + present result, with: Grape::Presenters::Presenter + end - present result, with: Grape::Presenters::Presenter + desc 'Dismiss a similarity match for a given task' + params do + requires :dismissed, type: Boolean, desc: 'Should this similarity be dismissed?' + requires :other, type: Boolean, desc: 'This tasks match or its reverse?' + end + put '/tasks/:id/similarity/:count' do + unless authenticated? + error!({ error: "Not authorised to access this task '#{params[:id]}'" }, 401) end + task = Task.find(params[:id]) - desc 'Dismiss a similarity match for a given task' - params do - requires :dismissed, type: Boolean, desc: 'Should this similarity be dismissed?' - requires :other, type: Boolean, desc: 'This tasks match or its reverse?' + unless authorise? current_user, task, :delete_plagiarism + error!({ error: "Not authorised to remove similarity for task '#{params[:id]}'" }, 401) end - put '/tasks/:id/similarity/:count' do - unless authenticated? - error!({ error: "Not authorised to access this task '#{params[:id]}'" }, 401) - end - task = Task.find(params[:id]) - unless authorise? current_user, task, :delete_plagiarism - error!({ error: "Not authorised to remove similarity for task '#{params[:id]}'" }, 401) - end + match = params[:count].to_i % task.similar_to_count + if match < 0 + error!({ error: 'Invalid match sequence, must be 0 or larger' }, 403) + end - match = params[:count].to_i % task.similar_to_count - if match < 0 - error!({ error: 'Invalid match sequence, must be 0 or larger' }, 403) - end + match_link = task.plagiarism_match_links.order('created_at DESC')[match] + return if match_link.nil? - match_link = task.plagiarism_match_links.order('created_at DESC')[match] - return if match_link.nil? + match_link = match_link.other_party if params[:other] - match_link = match_link.other_party if params[:other] + logger.info "#{current_user.username} changing plagiarism: setting dismissed for #{task.task_definition.abbreviation} by #{task.student.username} to #{params[:dismissed]}" - logger.info "#{current_user.username} changing plagiarism: setting dismissed for #{task.task_definition.abbreviation} by #{task.student.username} to #{params[:dismissed]}" + logger.debug " plagiarism match link 1: #{match_link}" - logger.debug " plagiarism match link 1: #{match_link}" + match_link.dismissed = params[:dismissed] + match_link.save! + present match_link.dismissed, with: Grape::Presenters::Presenter + end - match_link.dismissed = params[:dismissed] - match_link.save! - present match_link.dismissed, with: Grape::Presenters::Presenter - end + desc 'Pin a task to the user\'s task inbox' + params do + requires :id, type: Integer, desc: 'The ID of the task to be pinned' + end + post '/tasks/:id/pin' do + task = Task.find(params[:id]) - desc 'Pin a task to the user\'s task inbox' - params do - requires :id, type: Integer, desc: 'The ID of the task to be pinned' + unless authorise? current_user, task.unit, :provide_feedback + error!({ error: 'Not authorised to pin task' }, 403) end - post '/tasks/:id/pin' do - task = Task.find(params[:id]) - unless authorise? current_user, task.unit, :provide_feedback - error!({ error: 'Not authorised to pin task' }, 403) - end + TaskPin.find_or_create_by(task: task, user: current_user) - TaskPin.find_or_create_by(task: task, user: current_user) + present true, Grape::Presenters::Presenter + end - present true, Grape::Presenters::Presenter - end + desc 'Unpin a task from the user\'s task inbox' + params do + requires :id, type: Integer, desc: 'The ID of the task to be unpinned' + end + delete '/tasks/:id/pin' do + TaskPin.find_by!(user: current_user, task_id: params[:id]).destroy + present true, Grape::Presenters::Presenter + end - desc 'Unpin a task from the user\'s task inbox' - params do - requires :id, type: Integer, desc: 'The ID of the task to be unpinned' - end - delete '/tasks/:id/pin' do - TaskPin.find_by!(user: current_user, task_id: params[:id]).destroy - present true, Grape::Presenters::Presenter - end + desc 'Update a task using its related project and task definition' + params do + # requires :id, type: Integer, desc: 'The project id to locate' + # requires :task_definition_id, type: Integer, desc: 'The id of the task definition of the task to update in this project' + optional :trigger, type: String, desc: 'New status' + optional :include_in_portfolio, type: Boolean, desc: 'Indicate if this task should be in the portfolio' + optional :grade, type: Integer, desc: 'Grade value if task is a graded task (required if task definition is a graded task)' + optional :quality_pts, type: Integer, desc: 'Quality points value if task has quality assessment' + end + put '/projects/:id/task_def_id/:task_definition_id' do + project = Project.find(params[:id]) + grade = params[:grade] + task_definition = project.unit.task_definitions.find(params[:task_definition_id]) + needs_upload_docs = !task_definition.upload_requirements.empty? + + # check the user can put this task + if authorise? current_user, project, :make_submission + task = project.task_for_task_definition(task_definition) - desc 'Update a task using its related project and task definition' - params do - # requires :id, type: Integer, desc: 'The project id to locate' - # requires :task_definition_id, type: Integer, desc: 'The id of the task definition of the task to update in this project' - optional :trigger, type: String, desc: 'New status' - optional :include_in_portfolio, type: Boolean, desc: 'Indicate if this task should be in the portfolio' - optional :grade, type: Integer, desc: 'Grade value if task is a graded task (required if task definition is a graded task)' - optional :quality_pts, type: Integer, desc: 'Quality points value if task has quality assessment' - end - put '/projects/:id/task_def_id/:task_definition_id' do - project = Project.find(params[:id]) - grade = params[:grade] - task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - needs_upload_docs = !task_definition.upload_requirements.empty? - - # check the user can put this task - if authorise? current_user, project, :make_submission - task = project.task_for_task_definition(task_definition) - - # if trigger supplied... - unless params[:trigger].nil? - # Check if they should be using portfolio_evidence api - if needs_upload_docs && params[:trigger] == 'ready_for_feedback' - error!({ error: 'Cannot set this task status to ready to mark without uploading documents.' }, 403) - end - - if task.group_task? && !task.group - error!({ error: "This task requires a group. Ensure you are in a group for the unit's #{task.task_definition.group_set.name}" }, 403) - end - - logger.info "#{current_user.username} assessing task #{task.id} to #{params[:trigger]}" - result = task.trigger_transition(trigger: params[:trigger], by_user: current_user, quality: params[:quality_pts]) - if result.nil? && task.task_definition.restrict_status_updates - error!({ error: 'This task can only be updated by your tutor.' }, 403) - end + # if trigger supplied... + unless params[:trigger].nil? + # Check if they should be using portfolio_evidence api + if needs_upload_docs && params[:trigger] == 'ready_for_feedback' + error!({ error: 'Cannot set this task status to ready to mark without uploading documents.' }, 403) end - # if grade was supplied - unless grade.nil? - # try to grade the task - task.grade_task grade, self + if task.group_task? && !task.group + error!({ error: "This task requires a group. Ensure you are in a group for the unit's #{task.task_definition.group_set.name}" }, 403) end - # if include in portfolio supplied - unless params[:include_in_portfolio].nil? - task.include_in_portfolio = params[:include_in_portfolio] - task.save + logger.info "#{current_user.username} assessing task #{task.id} to #{params[:trigger]}" + result = task.trigger_transition(trigger: params[:trigger], by_user: current_user, quality: params[:quality_pts]) + if result.nil? && task.task_definition.restrict_status_updates + error!({ error: 'This task can only be updated by your tutor.' }, 403) end + end - present task, with: Api::Entities::TaskEntity, include_other_projects: true, update_only: true - else - error!({ error: "Couldn't find Task with id=#{params[:id]}" }, 403) + # if grade was supplied + unless grade.nil? + # try to grade the task + task.grade_task grade, self end - end - desc 'Get the submission details of a task, indicating if it has a pdf to view' - params do - requires :id, type: Integer, desc: 'The project id to locate' - requires :task_definition_id, type: Integer, desc: 'The id of the task definition of the task to update in this project' - end - get '/projects/:id/task_def_id/:task_definition_id/submission_details' do - # Get the project and task_definition based on uploaded details. - project = Project.find(params[:id]) - task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - - # check the user can put this task - error!(error: 'You do not have permission to read submissions for this project.') unless authorise? current_user, project, :get_submission - - # ensure there can be a pdf... - needs_upload_docs = !task_definition.upload_requirements.empty? - - # check if we actually have this task... if not must be false. - if needs_upload_docs && project.has_task_for_task_definition?(task_definition) - task = project.task_for_task_definition(task_definition) - - # return the details as json - result = { - has_pdf: task.has_pdf, - submission_date: task.submission_date, - processing_pdf: task.processing_pdf? - } - else - result = { - has_pdf: false, - processing_pdf: false - } + # if include in portfolio supplied + unless params[:include_in_portfolio].nil? + task.include_in_portfolio = params[:include_in_portfolio] + task.save end - present result, with: Grape::Presenters::Presenter + present task, with: Entities::TaskEntity, include_other_projects: true, update_only: true + else + error!({ error: "Couldn't find Task with id=#{params[:id]}" }, 403) end + end - desc 'Get the files associated with a submission' - params do - requires :id, type: Integer, desc: 'The project id to locate' - requires :task_definition_id, type: Integer, desc: 'The id of the task definition of the task to get the files from' - end - get '/projects/:id/task_def_id/:task_definition_id/submission_files' do - # Get the project and task_definition based on uploaded details. - project = Project.find(params[:id]) - task_definition = project.unit.task_definitions.find(params[:task_definition_id]) + desc 'Get the submission details of a task, indicating if it has a pdf to view' + params do + requires :id, type: Integer, desc: 'The project id to locate' + requires :task_definition_id, type: Integer, desc: 'The id of the task definition of the task to update in this project' + end + get '/projects/:id/task_def_id/:task_definition_id/submission_details' do + # Get the project and task_definition based on uploaded details. + project = Project.find(params[:id]) + task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - # check the user can put this task - error!(error: 'You do not have permission to read submissions for this project.') unless authorise? current_user, project, :get_submission + # check the user can put this task + error!(error: 'You do not have permission to read submissions for this project.') unless authorise? current_user, project, :get_submission - # Get the actual task... + # ensure there can be a pdf... + needs_upload_docs = !task_definition.upload_requirements.empty? + + # check if we actually have this task... if not must be false. + if needs_upload_docs && project.has_task_for_task_definition?(task_definition) task = project.task_for_task_definition(task_definition) - # Find the file - file_loc = FileHelper.zip_file_path_for_done_task(task) + # return the details as json + result = { + has_pdf: task.has_pdf, + submission_date: task.submission_date, + processing_pdf: task.processing_pdf? + } + else + result = { + has_pdf: false, + processing_pdf: false + } + end - if file_loc.nil? - file_loc = Rails.root.join('public', 'resources', 'FileNotFound.pdf') - header['Content-Disposition'] = 'attachment; filename=FileNotFound.pdf' - else - header['Content-Disposition'] = "attachment; filename=#{project.student.username}-#{task.task_definition.abbreviation}.zip" - end + present result, with: Grape::Presenters::Presenter + end + + desc 'Get the files associated with a submission' + params do + requires :id, type: Integer, desc: 'The project id to locate' + requires :task_definition_id, type: Integer, desc: 'The id of the task definition of the task to get the files from' + end + get '/projects/:id/task_def_id/:task_definition_id/submission_files' do + # Get the project and task_definition based on uploaded details. + project = Project.find(params[:id]) + task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - # Set download headers... - content_type 'application/octet-stream' - env['api.format'] = :binary + # check the user can put this task + error!(error: 'You do not have permission to read submissions for this project.') unless authorise? current_user, project, :get_submission - # Return the file data - File.read(file_loc) + # Get the actual task... + task = project.task_for_task_definition(task_definition) + + # Find the file + file_loc = FileHelper.zip_file_path_for_done_task(task) + + if file_loc.nil? + file_loc = Rails.root.join('public', 'resources', 'FileNotFound.pdf') + header['Content-Disposition'] = 'attachment; filename=FileNotFound.pdf' + else + header['Content-Disposition'] = "attachment; filename=#{project.student.username}-#{task.task_definition.abbreviation}.zip" end + + # Set download headers... + content_type 'application/octet-stream' + env['api.format'] = :binary + + # Return the file data + File.read(file_loc) end end diff --git a/app/api/teaching_periods_authenticated_api.rb b/app/api/teaching_periods_authenticated_api.rb index 098c43090..7e61816bd 100644 --- a/app/api/teaching_periods_authenticated_api.rb +++ b/app/api/teaching_periods_authenticated_api.rb @@ -1,99 +1,97 @@ require 'grape' -module Api - class TeachingPeriodsAuthenticatedApi < Grape::API - helpers AuthenticationHelpers - helpers AuthorisationHelpers +class TeachingPeriodsAuthenticatedApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers - before do - authenticated? - end + before do + authenticated? + end - desc 'Add a Teaching Period' - params do - requires :teaching_period, type: Hash do - requires :period, type: String, desc: 'The name of the teaching period' - requires :year, type: Integer, desc: 'The year of the teaching period' - requires :start_date, type: Date, desc: 'The start date of the teaching period' - requires :end_date, type: Date, desc: 'The end date of the teaching period' - requires :active_until, type: Date, desc: 'The teaching period will be active until this date' - end + desc 'Add a Teaching Period' + params do + requires :teaching_period, type: Hash do + requires :period, type: String, desc: 'The name of the teaching period' + requires :year, type: Integer, desc: 'The year of the teaching period' + requires :start_date, type: Date, desc: 'The start date of the teaching period' + requires :end_date, type: Date, desc: 'The end date of the teaching period' + requires :active_until, type: Date, desc: 'The teaching period will be active until this date' + end + end + post '/teaching_periods' do + unless authorise? current_user, User, :handle_teaching_period + error!({ error: 'Not authorised to create a teaching period' }, 403) end - post '/teaching_periods' do - unless authorise? current_user, User, :handle_teaching_period - error!({ error: 'Not authorised to create a teaching period' }, 403) - end - teaching_period_parameters = ActionController::Parameters.new(params) - .require(:teaching_period) - .permit(:period, - :year, - :start_date, - :end_date, - :active_until) + teaching_period_parameters = ActionController::Parameters.new(params) + .require(:teaching_period) + .permit(:period, + :year, + :start_date, + :end_date, + :active_until) - result = TeachingPeriod.create!(teaching_period_parameters) + result = TeachingPeriod.create!(teaching_period_parameters) - if result.nil? - error!({ error: 'No teaching period added.' }, 403) - else - result - end + if result.nil? + error!({ error: 'No teaching period added.' }, 403) + else + present result, with: Entities::TeachingPeriodEntity end + end - desc 'Update teaching period' - params do - requires :id, type: Integer, desc: 'The teaching period id to update' - requires :teaching_period, type: Hash do - optional :period, type: String, desc: 'The name of the teaching period' - optional :year, type: Integer, desc: 'The year of the teaching period' - optional :start_date, type: Date, desc: 'The start date of the teaching period' - optional :end_date, type: Date, desc: 'The end date of the teaching period' - optional :active_until, type: Date, desc: 'The teaching period will be active until this date' - end + desc 'Update teaching period' + params do + requires :id, type: Integer, desc: 'The teaching period id to update' + requires :teaching_period, type: Hash do + optional :period, type: String, desc: 'The name of the teaching period' + optional :year, type: Integer, desc: 'The year of the teaching period' + optional :start_date, type: Date, desc: 'The start date of the teaching period' + optional :end_date, type: Date, desc: 'The end date of the teaching period' + optional :active_until, type: Date, desc: 'The teaching period will be active until this date' end - put '/teaching_periods/:id' do - teaching_period = TeachingPeriod.find(params[:id]) - unless authorise? current_user, User, :handle_teaching_period - error!({ error: 'Not authorised to update a teaching period' }, 403) - end - teaching_period_parameters = ActionController::Parameters.new(params) - .require(:teaching_period) - .permit(:period, - :year, - :start_date, - :end_date, - :active_until) - - teaching_period.update!(teaching_period_parameters) - teaching_period + end + put '/teaching_periods/:id' do + teaching_period = TeachingPeriod.find(params[:id]) + unless authorise? current_user, User, :handle_teaching_period + error!({ error: 'Not authorised to update a teaching period' }, 403) end + teaching_period_parameters = ActionController::Parameters.new(params) + .require(:teaching_period) + .permit(:period, + :year, + :start_date, + :end_date, + :active_until) - desc 'Delete a teaching period' - delete '/teaching_periods/:teaching_period_id' do - unless authorise? current_user, User, :handle_teaching_period - error!({ error: 'Not authorised to delete a teaching period' }, 403) - end + teaching_period.update!(teaching_period_parameters) + teaching_period + end - teaching_period_id = params[:teaching_period_id] - TeachingPeriod.find(teaching_period_id).destroy + desc 'Delete a teaching period' + delete '/teaching_periods/:teaching_period_id' do + unless authorise? current_user, User, :handle_teaching_period + error!({ error: 'Not authorised to delete a teaching period' }, 403) end - desc 'Rollover a Teaching Period' - params do - requires :new_teaching_period_id, type: Integer, desc: 'The id of the rolled over teaching period' - optional :rollover_inactive, type: Boolean, default: false, desc: 'Are in active units included in the roll over' - optional :search_forward, type: Boolean, default: true, desc: 'When rolling over units, ensure that latest version is rolled over to new teaching period' + teaching_period_id = params[:teaching_period_id] + TeachingPeriod.find(teaching_period_id).destroy + end + + desc 'Rollover a Teaching Period' + params do + requires :new_teaching_period_id, type: Integer, desc: 'The id of the rolled over teaching period' + optional :rollover_inactive, type: Boolean, default: false, desc: 'Are in active units included in the roll over' + optional :search_forward, type: Boolean, default: true, desc: 'When rolling over units, ensure that latest version is rolled over to new teaching period' + end + post '/teaching_periods/:existing_teaching_period_id/rollover' do + unless authorise? current_user, User, :rollover + error!({ error: 'Not authorised to rollover a teaching period' }, 403) end - post '/teaching_periods/:existing_teaching_period_id/rollover' do - unless authorise? current_user, User, :rollover - error!({ error: 'Not authorised to rollover a teaching period' }, 403) - end - new_teaching_period_id = params[:new_teaching_period_id] - new_teaching_period = TeachingPeriod.find(new_teaching_period_id) + new_teaching_period_id = params[:new_teaching_period_id] + new_teaching_period = TeachingPeriod.find(new_teaching_period_id) - existing_teaching_period = TeachingPeriod.find(params[:existing_teaching_period_id]) - error!({error: existing_teaching_period.errors.full_messages.first}, 403) unless existing_teaching_period.rollover(new_teaching_period, params[:search_forward], params[:rollover_inactive]) - end + existing_teaching_period = TeachingPeriod.find(params[:existing_teaching_period_id]) + error!({error: existing_teaching_period.errors.full_messages.first}, 403) unless existing_teaching_period.rollover(new_teaching_period, params[:search_forward], params[:rollover_inactive]) end end diff --git a/app/api/teaching_periods_public_api.rb b/app/api/teaching_periods_public_api.rb index 8c4b6fe89..e40446ea1 100644 --- a/app/api/teaching_periods_public_api.rb +++ b/app/api/teaching_periods_public_api.rb @@ -1,30 +1,16 @@ require 'grape' -module Api - class TeachingPeriodsPublicApi < Grape::API +class TeachingPeriodsPublicApi < Grape::API - desc "Get a teaching period's details" - get '/teaching_periods/:id' do - teaching_period = TeachingPeriod.find(params[:id]) - teaching_period - end - - desc 'Get all the Teaching Periods' - get '/teaching_periods' do - teaching_periods = TeachingPeriod.all - result = teaching_periods.map do |c| - { - id: c.id, - period: c.period, - year: c.year, - start_date: c.start_date, - end_date: c.end_date, - active_until: c.active_until - } - end + desc "Get a teaching period's details" + get '/teaching_periods/:id' do + teaching_period = TeachingPeriod.find(params[:id]) + present teaching_period, with: Entities::TeachingPeriodEntity, full_details: true + end - content_type 'text/plain' - body result - end + desc 'Get all the Teaching Periods' + get '/teaching_periods' do + teaching_periods = TeachingPeriod.all + present teaching_periods, with: Entities::TeachingPeriodEntity end end diff --git a/app/api/tutorial_enrolments_api.rb b/app/api/tutorial_enrolments_api.rb index 7c3cbdadb..b3e00f783 100644 --- a/app/api/tutorial_enrolments_api.rb +++ b/app/api/tutorial_enrolments_api.rb @@ -1,58 +1,56 @@ require 'grape' -module Api - class TutorialEnrolmentsApi < Grape::API - helpers AuthenticationHelpers - helpers AuthorisationHelpers +class TutorialEnrolmentsApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers - before do - authenticated? + before do + authenticated? + end + + desc 'Enrol project in a tutorial' + post '/units/:unit_id/tutorials/:tutorial_abbr/enrolments/:project_id' do + unit = Unit.find(params[:unit_id]) + project = unit.active_projects.find(params[:project_id]) + unless authorise? current_user, project, :change_tutorial, ->(role, perm_hash, other) { project.specific_permission_hash(role, perm_hash, other) } + error!({ error: 'Not authorised to change tutorial' }, 403) end - desc 'Enrol project in a tutorial' - post '/units/:unit_id/tutorials/:tutorial_abbr/enrolments/:project_id' do - unit = Unit.find(params[:unit_id]) - project = unit.active_projects.find(params[:project_id]) - unless authorise? current_user, project, :change_tutorial, ->(role, perm_hash, other) { project.specific_permission_hash(role, perm_hash, other) } - error!({ error: 'Not authorised to change tutorial' }, 403) - end + tutorial = unit.tutorials.find_by(abbreviation: params[:tutorial_abbr]) + error!({ error: "No tutorial with abbreviation #{params[:tutorial_abbr]} exists for the unit" }, 403) unless tutorial.present? - tutorial = unit.tutorials.find_by(abbreviation: params[:tutorial_abbr]) - error!({ error: "No tutorial with abbreviation #{params[:tutorial_abbr]} exists for the unit" }, 403) unless tutorial.present? + # If the tutorial has a capacity, and we are at that capacity, and the user does not have permissions to exceed capacity... + if tutorial.capacity > 0 && tutorial.tutorial_enrolments.count >= tutorial.capacity && ! authorise?(current_user, unit, :exceed_capacity) + error!({ error: "Tutorial #{params[:tutorial_abbr]} is full and cannot accept further student enrolments" }, 403) + end - # If the tutorial has a capacity, and we are at that capacity, and the user does not have permissions to exceed capacity... - if tutorial.capacity > 0 && tutorial.tutorial_enrolments.count >= tutorial.capacity && ! authorise?(current_user, unit, :exceed_capacity) - error!({ error: "Tutorial #{params[:tutorial_abbr]} is full and cannot accept further student enrolments" }, 403) - end + result = project.enrol_in(tutorial) - result = project.enrol_in(tutorial) + if result.nil? + error!({ error: 'No enrolment added' }, 403) + else + result + end - if result.nil? - error!({ error: 'No enrolment added' }, 403) - else - result - end + present :enrolments, project.tutorial_enrolments, with: Entities::TutorialEnrolmentEntity + end - present :enrolments, project.tutorial_enrolments, with: Api::Entities::TutorialEnrolmentEntity + desc 'Delete an enrolment in the tutorial' + delete '/units/:unit_id/tutorials/:tutorial_abbr/enrolments/:project_id' do + unit = Unit.find(params[:unit_id]) + project = unit.projects.find(params[:project_id]) + unless authorise? current_user, project, :change_tutorial + error!({ error: 'Not authorised to change tutorials' }, 403) end - desc 'Delete an enrolment in the tutorial' - delete '/units/:unit_id/tutorials/:tutorial_abbr/enrolments/:project_id' do - unit = Unit.find(params[:unit_id]) - project = unit.projects.find(params[:project_id]) - unless authorise? current_user, project, :change_tutorial - error!({ error: 'Not authorised to change tutorials' }, 403) - end - - tutorial = unit.tutorials.find_by(abbreviation: params[:tutorial_abbr]) - error!({ error: "No tutorial with abbreviation #{params[:tutorial_abbr]} exists for the unit" }, 403) unless tutorial.present? + tutorial = unit.tutorials.find_by(abbreviation: params[:tutorial_abbr]) + error!({ error: "No tutorial with abbreviation #{params[:tutorial_abbr]} exists for the unit" }, 403) unless tutorial.present? - tutorial_enrolment = tutorial.tutorial_enrolments.find_by(project_id: params[:project_id]) - error!({ error: "Project not enrolled in the selected tutorial" }, 403) unless tutorial_enrolment.present? - tutorial_enrolment.destroy + tutorial_enrolment = tutorial.tutorial_enrolments.find_by(project_id: params[:project_id]) + error!({ error: "Project not enrolled in the selected tutorial" }, 403) unless tutorial_enrolment.present? + tutorial_enrolment.destroy - # present :enrolments, project.tutorial_enrolments, with: Api::Entities::TutorialEnrolmentEntity - present true, with: Grape::Presenters::Presenter - end + # present :enrolments, project.tutorial_enrolments, with: Entities::TutorialEnrolmentEntity + present true, with: Grape::Presenters::Presenter end end diff --git a/app/api/tutorial_streams_api.rb b/app/api/tutorial_streams_api.rb index 84c6d601d..fe508fe07 100644 --- a/app/api/tutorial_streams_api.rb +++ b/app/api/tutorial_streams_api.rb @@ -1,61 +1,59 @@ require 'grape' -module Api - class TutorialStreamsApi < Grape::API - helpers AuthenticationHelpers - helpers AuthorisationHelpers +class TutorialStreamsApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers - before do - authenticated? - end + before do + authenticated? + end - desc 'Add a tutorial stream to the unit' - params do - requires :activity_type_abbr, type: String, desc: 'Abbreviation of the activity type' + desc 'Add a tutorial stream to the unit' + params do + requires :activity_type_abbr, type: String, desc: 'Abbreviation of the activity type' + end + post '/units/:unit_id/tutorial_streams' do + unit = Unit.find(params[:unit_id]) + unless authorise? current_user, unit, :add_tutorial + error!({ error: 'Not authorised to add tutorial stream to this unit' }, 403) end - post '/units/:unit_id/tutorial_streams' do - unit = Unit.find(params[:unit_id]) - unless authorise? current_user, unit, :add_tutorial - error!({ error: 'Not authorised to add tutorial stream to this unit' }, 403) - end - activity_type = ActivityType.find_by(abbreviation: params[:activity_type_abbr]) - institution_settings = Doubtfire::Application.config.institution_settings + activity_type = ActivityType.find_by(abbreviation: params[:activity_type_abbr]) + institution_settings = Doubtfire::Application.config.institution_settings - name,abbreviation = institution_settings.details_for_next_tutorial_stream(unit, activity_type) + name,abbreviation = institution_settings.details_for_next_tutorial_stream(unit, activity_type) - unit.add_tutorial_stream(name, abbreviation, activity_type) - end + unit.add_tutorial_stream(name, abbreviation, activity_type) + end - desc 'Update a tutorial stream in the unit' - params do - optional :name, type: String, desc: 'The name of the tutorial stream' - optional :abbreviation, type: String, desc: 'The abbreviation for the tutorial stream' - optional :activity_type_abbr, type: String, desc: 'Abbreviation of the activity type' - end - put '/units/:unit_id/tutorial_streams/:tutorial_stream_abbr' do - unit = Unit.find(params[:unit_id]) - unless authorise? current_user, unit, :add_tutorial - error!({ error: 'Not authorised to update tutorial stream in this unit' }, 403) - end - - tutorial_stream = unit.tutorial_streams.find_by!(abbreviation: params[:tutorial_stream_abbr]) - activity_type = ActivityType.find_by!(abbreviation: params[:activity_type_abbr]) if params[:activity_type_abbr].present? - unit.update_tutorial_stream(tutorial_stream, params[:name], params[:abbreviation], activity_type) + desc 'Update a tutorial stream in the unit' + params do + optional :name, type: String, desc: 'The name of the tutorial stream' + optional :abbreviation, type: String, desc: 'The abbreviation for the tutorial stream' + optional :activity_type_abbr, type: String, desc: 'Abbreviation of the activity type' + end + put '/units/:unit_id/tutorial_streams/:tutorial_stream_abbr' do + unit = Unit.find(params[:unit_id]) + unless authorise? current_user, unit, :add_tutorial + error!({ error: 'Not authorised to update tutorial stream in this unit' }, 403) end - desc 'Delete a tutorial stream in the unit' - delete '/units/:unit_id/tutorial_streams/:tutorial_stream_abbr' do - unit = Unit.find(params[:unit_id]) - unless authorise? current_user, unit, :add_tutorial - error!({ error: 'Not authorised to delete tutorial stream in this unit' }, 403) - end - - tutorial_stream = unit.tutorial_streams.find_by!(abbreviation: params[:tutorial_stream_abbr]) - tutorial_stream.destroy - error!({ error: tutorial_stream.errors.full_messages.last }, 403) unless tutorial_stream.destroyed? - tutorial_stream.destroyed? + tutorial_stream = unit.tutorial_streams.find_by!(abbreviation: params[:tutorial_stream_abbr]) + activity_type = ActivityType.find_by!(abbreviation: params[:activity_type_abbr]) if params[:activity_type_abbr].present? + unit.update_tutorial_stream(tutorial_stream, params[:name], params[:abbreviation], activity_type) + end + + desc 'Delete a tutorial stream in the unit' + delete '/units/:unit_id/tutorial_streams/:tutorial_stream_abbr' do + unit = Unit.find(params[:unit_id]) + unless authorise? current_user, unit, :add_tutorial + error!({ error: 'Not authorised to delete tutorial stream in this unit' }, 403) end + tutorial_stream = unit.tutorial_streams.find_by!(abbreviation: params[:tutorial_stream_abbr]) + tutorial_stream.destroy + error!({ error: tutorial_stream.errors.full_messages.last }, 403) unless tutorial_stream.destroyed? + tutorial_stream.destroyed? end -end \ No newline at end of file + +end diff --git a/app/api/tutorials_api.rb b/app/api/tutorials_api.rb index 064a3e6d6..24b24106a 100644 --- a/app/api/tutorials_api.rb +++ b/app/api/tutorials_api.rb @@ -1,107 +1,105 @@ require 'grape' -module Api - class TutorialsApi < Grape::API - helpers AuthenticationHelpers - helpers AuthorisationHelpers +class TutorialsApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers - before do - authenticated? - end + before do + authenticated? + end - desc 'Update a tutorial' - params do - requires :id, type: Integer, desc: 'The user id to update' - requires :tutorial, type: Hash do - optional :abbreviation, type: String, desc: 'The tutorials code' - optional :meeting_location, type: String, desc: 'The tutorials location' - optional :meeting_day, type: String, desc: 'Day of the tutorial' - optional :tutor_id, type: Integer, desc: 'Id of the tutor' - optional :campus_id, type: Integer, desc: 'Id of the campus' - optional :capacity, type: Integer, desc: 'Capacity of the tutorial' - optional :meeting_time, type: String, desc: 'Time of the tutorial' - end - end - put '/tutorials/:id' do - tutorial = Tutorial.find(params[:id]) - tut_params = params[:tutorial] - # can only modify if current_user.id is same as :id provided - # (i.e., user wants to update their own data) or if update_user token - unless authorise? current_user, tutorial.unit, :add_tutorial - error!({ error: "Cannot update tutorial with id=#{params[:id]} - not authorised" }, 403) - end - - tutorial_parameters = ActionController::Parameters.new(params) - .require(:tutorial) - .permit( - :abbreviation, - :meeting_location, - :meeting_day, - :meeting_time, - :campus_id, - :capacity - ) - - if tut_params[:tutor_id] - tutor = User.find(tut_params[:tutor_id]) - tutorial.assign_tutor(tutor) - end - - if tutorial_parameters[:campus_id] == -1 - tutorial_parameters[:campus_id] = nil - end - - tutorial.update!(tutorial_parameters) - present tutorial, with: Api::Entities::TutorialEntity + desc 'Update a tutorial' + params do + requires :id, type: Integer, desc: 'The user id to update' + requires :tutorial, type: Hash do + optional :abbreviation, type: String, desc: 'The tutorials code' + optional :meeting_location, type: String, desc: 'The tutorials location' + optional :meeting_day, type: String, desc: 'Day of the tutorial' + optional :tutor_id, type: Integer, desc: 'Id of the tutor' + optional :campus_id, type: Integer, desc: 'Id of the campus' + optional :capacity, type: Integer, desc: 'Capacity of the tutorial' + optional :meeting_time, type: String, desc: 'Time of the tutorial' end - - desc 'Create tutorial' - params do - requires :tutorial, type: Hash do - requires :unit_id, type: Integer, desc: 'Id of the unit' - requires :tutor_id, type: Integer, desc: 'Id of the tutor' - requires :campus_id, type: Integer, desc: 'Id of the campus', allow_blank: false - requires :capacity, type: Integer, desc: 'Capacity of the tutorial', allow_blank: false - requires :abbreviation, type: String, desc: 'The tutorials code', allow_blank: false - requires :meeting_location, type: String, desc: 'The tutorials location', allow_blank: false - requires :meeting_day, type: String, desc: 'Day of the tutorial', allow_blank: false - requires :meeting_time, type: String, desc: 'Time of the tutorial', allow_blank: false - optional :tutorial_stream_abbr, type: String, desc: 'Abbreviation of the associated tutorial stream', allow_blank: false - end + end + put '/tutorials/:id' do + tutorial = Tutorial.find(params[:id]) + tut_params = params[:tutorial] + # can only modify if current_user.id is same as :id provided + # (i.e., user wants to update their own data) or if update_user token + unless authorise? current_user, tutorial.unit, :add_tutorial + error!({ error: "Cannot update tutorial with id=#{params[:id]} - not authorised" }, 403) end - post '/tutorials' do - tut_params = params[:tutorial] - unit = Unit.find(tut_params[:unit_id]) - - unless authorise? current_user, unit, :add_tutorial - error!({ error: 'Not authorised to create new tutorials' }, 403) - end + tutorial_parameters = ActionController::Parameters.new(params) + .require(:tutorial) + .permit( + :abbreviation, + :meeting_location, + :meeting_day, + :meeting_time, + :campus_id, + :capacity + ) + + if tut_params[:tutor_id] tutor = User.find(tut_params[:tutor_id]) - campus = tut_params[:campus_id] == -1 ? nil : Campus.find(tut_params[:campus_id]) + tutorial.assign_tutor(tutor) + end - # Set Tutorial Stream if available - tutorial_stream_abbr = tut_params[:tutorial_stream_abbr] - tutorial_stream = unit.tutorial_streams.find_by!(abbreviation: tutorial_stream_abbr) unless tutorial_stream_abbr.nil? + if tutorial_parameters[:campus_id] == -1 + tutorial_parameters[:campus_id] = nil + end - tutorial = unit.add_tutorial(tut_params[:meeting_day], tut_params[:meeting_time], tut_params[:meeting_location], tutor, campus, tut_params[:capacity], tut_params[:abbreviation], tutorial_stream) + tutorial.update!(tutorial_parameters) + present tutorial, with: Entities::TutorialEntity + end - present tutorial, with: Api::Entities::TutorialEntity + desc 'Create tutorial' + params do + requires :tutorial, type: Hash do + requires :unit_id, type: Integer, desc: 'Id of the unit' + requires :tutor_id, type: Integer, desc: 'Id of the tutor' + requires :campus_id, type: Integer, desc: 'Id of the campus', allow_blank: false + requires :capacity, type: Integer, desc: 'Capacity of the tutorial', allow_blank: false + requires :abbreviation, type: String, desc: 'The tutorials code', allow_blank: false + requires :meeting_location, type: String, desc: 'The tutorials location', allow_blank: false + requires :meeting_day, type: String, desc: 'Day of the tutorial', allow_blank: false + requires :meeting_time, type: String, desc: 'Time of the tutorial', allow_blank: false + optional :tutorial_stream_abbr, type: String, desc: 'Abbreviation of the associated tutorial stream', allow_blank: false end + end + post '/tutorials' do + tut_params = params[:tutorial] + unit = Unit.find(tut_params[:unit_id]) - desc 'Delete a tutorial' - params do - requires :id, type: Integer, desc: 'The tutorial id to delete' + unless authorise? current_user, unit, :add_tutorial + error!({ error: 'Not authorised to create new tutorials' }, 403) end - delete '/tutorials/:id' do - tutorial = Tutorial.find(params[:id]) - unless authorise? current_user, tutorial.unit, :add_tutorial - error!({ error: 'Cannot delete tutorial - not authorised' }, 403) - end + tutor = User.find(tut_params[:tutor_id]) + campus = tut_params[:campus_id] == -1 ? nil : Campus.find(tut_params[:campus_id]) + + # Set Tutorial Stream if available + tutorial_stream_abbr = tut_params[:tutorial_stream_abbr] + tutorial_stream = unit.tutorial_streams.find_by!(abbreviation: tutorial_stream_abbr) unless tutorial_stream_abbr.nil? + + tutorial = unit.add_tutorial(tut_params[:meeting_day], tut_params[:meeting_time], tut_params[:meeting_location], tutor, campus, tut_params[:capacity], tut_params[:abbreviation], tutorial_stream) + + present tutorial, with: Entities::TutorialEntity + end + + desc 'Delete a tutorial' + params do + requires :id, type: Integer, desc: 'The tutorial id to delete' + end + delete '/tutorials/:id' do + tutorial = Tutorial.find(params[:id]) - tutorial.destroy! - present true, with: Grape::Presenters::Presenter + unless authorise? current_user, tutorial.unit, :add_tutorial + error!({ error: 'Cannot delete tutorial - not authorised' }, 403) end + + tutorial.destroy! + present true, with: Grape::Presenters::Presenter end end diff --git a/app/api/unit_roles_api.rb b/app/api/unit_roles_api.rb index ab8ecb4a7..11a750d1c 100644 --- a/app/api/unit_roles_api.rb +++ b/app/api/unit_roles_api.rb @@ -1,103 +1,106 @@ require 'grape' -module Api - class UnitRolesApi < Grape::API - helpers AuthenticationHelpers - helpers AuthorisationHelpers +class UnitRolesApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers - before do - authenticated? - end + before do + authenticated? + end + + desc 'Get unit roles for authenticated user' + params do + optional :active_only, type: Boolean, desc: 'Show only active roles' + end + get '/unit_roles' do + return [] unless authorise? current_user, User, :act_tutor - desc 'Get unit roles for authenticated user' - params do - optional :unit_id, type: Integer, desc: 'Get user roles in indicated unit' + result = UnitRole.includes(:unit).where(unit_roles: { user_id: current_user.id }) + + if params[:active_only] + result = result.where(unit_roles: { active: true }) end - get '/unit_roles' do - return [] unless authorise? current_user, User, :act_tutor - unit_roles = UnitRole.for_user current_user + present result, with: Entities::UnitRoleWithUnitEntity + end - if params[:unit_id] - unit_roles = unit_roles.where(unit_id: params[:unit_id]) - end + desc 'Delete a unit role' + delete '/unit_roles/:id' do + unit_role = UnitRole.find(params[:id]) - ActiveModel::Serializer::CollectionSerializer.new(unit_roles.joins(:unit).select('unit_roles.*', 'units.start_date', 'units.end_date'), each_serializer: UnitRoleSerializer) + unless (authorise? current_user, unit_role.unit, :employ_staff) || (authorise? current_user, User, :admin_units) + error!({ error: "Couldn't find UnitRole with id=#{params[:id]}" }, 403) end - desc 'Delete a unit role' - delete '/unit_roles/:id' do - unit_role = UnitRole.find(params[:id]) + unit_role.destroy! + end - unless (authorise? current_user, unit_role.unit, :employ_staff) || (authorise? current_user, User, :admin_units) - error!({ error: "Couldn't find UnitRole with id=#{params[:id]}" }, 403) - end + desc "Get a unit_role's details" + get '/unit_roles/:id' do + unit_role = UnitRole.find(params[:id]) - unit_role.destroy! + unless authorise? current_user, unit_role, :get + error!({ error: "Couldn't find UnitRole with id=#{params[:id]}" }, 403) end - desc "Get a unit_role's details" - get '/unit_roles/:id' do - unit_role = UnitRole.find(params[:id]) + present unit_role, with: Entities::UnitRoleEntity + end - unless authorise? current_user, unit_role, :get - error!({ error: "Couldn't find UnitRole with id=#{params[:id]}" }, 403) - end + desc 'Employ a user as a teaching role in a unit' + params do + requires :unit_id, type: Integer, desc: 'The id of the unit to employ the staff for' + requires :user_id, type: Integer, desc: 'The id of the tutor' + requires :role, type: String, desc: 'The role for the staff member' + end + post '/unit_roles' do + unit = Unit.find(params[:unit_id]) - unit_role + unless (authorise? current_user, unit, :employ_staff) || (authorise? current_user, User, :admin_units) + error!({ error: "Couldn't find Unit with id=#{params[:id]}" }, 403) end + user = User.find(params[:user_id]) + role = Role.with_name(params[:role]) - desc 'Employ a user as a teaching role in a unit' - params do - requires :unit_id, type: Integer, desc: 'The id of the unit to employ the staff for' - requires :user_id, type: Integer, desc: 'The id of the tutor' - requires :role, type: String, desc: 'The role for the staff member' + if role.nil? + error!({ error: "Couldn't find Role with name=#{params[:role]}" }, 403) end - post '/unit_roles' do - unit = Unit.find(params[:unit_id]) - unless (authorise? current_user, unit, :employ_staff) || (authorise? current_user, User, :admin_units) - error!({ error: "Couldn't find Unit with id=#{params[:id]}" }, 403) - end - user = User.find(params[:user_id]) - role = Role.with_name(params[:role]) + if role == Role.student + error!({ error: 'Enrol students as projects not unit roles' }, 403) + end - if role.nil? - error!({ error: "Couldn't find Role with name=#{params[:role]}" }, 403) - end + unless user.has_tutor_capability? + error!({ error: 'The selected user is not a tutor. Please update their system role before adding them' }, 403) + end - if role == Role.student - error!({ error: 'Enrol students as projects not unit roles' }, 403) - end + result = unit.employ_staff(user, role) + present result, with: Entities::UnitRoleEntity + end - unit.employ_staff(user, role) + desc 'Update a role' + params do + requires :unit_role, type: Hash do + requires :role_id, type: Integer, desc: 'The role to create with' end + end + put '/unit_roles/:id' do + unit_role = UnitRole.find_by(id: params[:id]) - desc 'Update a role ' - params do - requires :unit_role, type: Hash do - requires :role_id, type: Integer, desc: 'The role to create with' - end + unless (authorise? current_user, unit_role.unit, :employ_staff) || (authorise? current_user, User, :admin_units) + error!({ error: "Couldn't find Unit with id=#{params[:id]}" }, 403) end - put '/unit_roles/:id' do - unit_role = UnitRole.find_by(id: params[:id]) - - unless (authorise? current_user, unit_role.unit, :employ_staff) || (authorise? current_user, User, :admin_units) - error!({ error: "Couldn't find Unit with id=#{params[:id]}" }, 403) - end - unit_role_parameters = ActionController::Parameters.new(params) - .require(:unit_role) - .permit( - :role_id - ) + unit_role_parameters = ActionController::Parameters.new(params) + .require(:unit_role) + .permit( + :role_id + ) - if unit_role_parameters[:role_id] == Role.tutor.id && unit_role.role == Role.convenor && unit_role.unit.convenors.count == 1 - error!({ error: 'There must be at least one convenor for the unit' }, 403) - end - - unit_role.update!(unit_role_parameters) - unit_role + if unit_role_parameters[:role_id] == Role.tutor.id && unit_role.role == Role.convenor && unit_role.unit.convenors.count == 1 + error!({ error: 'There must be at least one convenor for the unit' }, 403) end + + unit_role.update!(unit_role_parameters) + present unit_role, with: Entities::UnitRoleEntity end end diff --git a/app/api/units_api.rb b/app/api/units_api.rb index 50e23cced..d75ea531c 100644 --- a/app/api/units_api.rb +++ b/app/api/units_api.rb @@ -1,384 +1,392 @@ require 'grape' -require 'unit_serializer' -require 'mime-check-helpers' require 'csv_helper' require 'entities/unit_entity' -module Api - class UnitsApi < Grape::API - helpers AuthenticationHelpers - helpers AuthorisationHelpers - helpers MimeCheckHelpers - helpers CsvHelper - - before do - authenticated? - - if params[:unit] - for key in [ :start_date, :end_date ] do - if params[:unit][key].present? - date_val = DateTime.parse(params[:unit][key]) - params[:unit][key] = date_val - end +class UnitsApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers + helpers MimeCheckHelpers + helpers CsvHelper + + before do + authenticated? + + if params[:unit] + for key in [ :start_date, :end_date ] do + if params[:unit][key].present? + date_val = DateTime.parse(params[:unit][key]) + params[:unit][key] = date_val end end end + end - desc 'Get units related to the current user for admin purposes' - params do - optional :include_in_active, type: Boolean, desc: 'Include units that are not active' + desc 'Get units related to the current user for admin purposes' + params do + optional :include_in_active, type: Boolean, desc: 'Include units that are not active' + end + get '/units' do + unless authorise? current_user, User, :convene_units + error!({ error: 'Unable to list units' }, 403) end - get '/units' do - unless authorise? current_user, User, :convene_units - error!({ error: 'Unable to list units' }, 403) - end - # gets only the units the current user can "see" - units = Unit.for_user_admin current_user + # gets only the units the current user can "see" + units = Unit.for_user_admin current_user - units = units.where('active = true') unless params[:include_in_active] + units = units.where('active = true') unless params[:include_in_active] - present units, with: Api::Entities::UnitEntity, user: current_user - end + present units, with: Entities::UnitEntity, user: current_user + end - desc "Get a unit's details" - get '/units/:id' do - unit = Unit.find(params[:id]) - unless (authorise? current_user, unit, :get_unit) || (authorise? current_user, User, :admin_units) - error!({ error: "Couldn't find Unit with id=#{params[:id]}" }, 403) - end - # - # Unit uses user from thread to limit exposure - # - present unit, with: Api::Entities::UnitEntity, user: current_user + desc "Get a unit's details" + get '/units/:id' do + unit = Unit.includes( + {unit_roles: [:role, :user]}, + {task_definitions: :tutorial_stream}, + :learning_outcomes, + {tutorial_streams: :activity_type}, + {tutorials: [:tutor, :tutorial_stream]}, + :tutorial_enrolments, + {staff: [:role, :user]}, + :group_sets, + :groups, + :group_memberships + ).find(params[:id]) + + unless (authorise? current_user, unit, :get_unit) || (authorise? current_user, User, :admin_units) + error!({ error: "Couldn't find Unit with id=#{params[:id]}" }, 403) end + # + # Unit uses user from thread to limit exposure + # + present unit, with: Entities::UnitEntity, user: current_user, in_unit: true + end - desc 'Update unit' - params do - requires :id, type: Integer, desc: 'The unit id to update' - requires :unit, type: Hash do - optional :name, type: String - optional :code, type: String - optional :description, type: String - optional :active, type: Boolean - optional :teaching_period_id, type: Integer - optional :start_date, type: Date - optional :end_date, type: Date - optional :main_convenor_id, type: Integer - optional :auto_apply_extension_before_deadline, type: Boolean, desc: 'Indicates if extensions before the deadline should be automatically applied' - optional :send_notifications, type: Boolean, desc: 'Indicates if emails should be sent on updates each week' - optional :enable_sync_timetable, type: Boolean, desc: 'Sync to timetable automatically if supported by deployment' - optional :enable_sync_enrolments, type: Boolean, desc: 'Sync student enrolments automatically if supported by deployment' - optional :draft_task_definition_id, type: Integer, desc: 'Indicates the ID of the task definition used as the "draft learning summary task"' - optional :allow_student_extension_requests, type: Boolean, desc: 'Can turn on/off student extension requests', default: true - optional :allow_student_change_tutorial, type: Boolean, desc: 'Can turn on/off student ability to change tutorials', default: true - optional :extension_weeks_on_resubmit_request, type: Integer, desc: 'Determines the number of weeks extension on a resubmit request', default: 1 - optional :overseer_image_id, type: Integer, desc: 'The id of the docker image used with ' - optional :assessment_enabled, type: Boolean - - mutually_exclusive :teaching_period_id,:start_date - all_or_none_of :start_date, :end_date - end + desc 'Update unit' + params do + requires :id, type: Integer, desc: 'The unit id to update' + requires :unit, type: Hash do + optional :name, type: String + optional :code, type: String + optional :description, type: String + optional :active, type: Boolean + optional :teaching_period_id, type: Integer + optional :start_date, type: Date + optional :end_date, type: Date + optional :main_convenor_id, type: Integer + optional :auto_apply_extension_before_deadline, type: Boolean, desc: 'Indicates if extensions before the deadline should be automatically applied' + optional :send_notifications, type: Boolean, desc: 'Indicates if emails should be sent on updates each week' + optional :enable_sync_timetable, type: Boolean, desc: 'Sync to timetable automatically if supported by deployment' + optional :enable_sync_enrolments, type: Boolean, desc: 'Sync student enrolments automatically if supported by deployment' + optional :draft_task_definition_id, type: Integer, desc: 'Indicates the ID of the task definition used as the "draft learning summary task"' + optional :allow_student_extension_requests, type: Boolean, desc: 'Can turn on/off student extension requests', default: true + optional :allow_student_change_tutorial, type: Boolean, desc: 'Can turn on/off student ability to change tutorials', default: true + optional :extension_weeks_on_resubmit_request, type: Integer, desc: 'Determines the number of weeks extension on a resubmit request', default: 1 + optional :overseer_image_id, type: Integer, desc: 'The id of the docker image used with ' + optional :assessment_enabled, type: Boolean + + mutually_exclusive :teaching_period_id,:start_date + all_or_none_of :start_date, :end_date + end + end + put '/units/:id' do + unit = Unit.find(params[:id]) + unless authorise? current_user, unit, :update + error!({ error: 'Not authorised to update this unit' }, 403) end - put '/units/:id' do - unit = Unit.find(params[:id]) - unless authorise? current_user, unit, :update - error!({ error: 'Not authorised to update this unit' }, 403) + unit_parameters = ActionController::Parameters.new(params) + .require(:unit) + .permit(:name, + :code, + :description, + :start_date, + :end_date, + :teaching_period_id, + :active, + :main_convenor_id, + :auto_apply_extension_before_deadline, + :send_notifications, + :enable_sync_timetable, + :enable_sync_enrolments, + :draft_task_definition_id, + :allow_student_extension_requests, + :extension_weeks_on_resubmit_request, + :allow_student_change_tutorial, + :overseer_image_id, + :assessment_enabled + ) + + if unit.teaching_period_id.present? && unit_parameters.key?(:start_date) + unit.teaching_period = nil + end + + if unit_parameters[:draft_task_definition_id].present? + # Ensure the task definition belongs to unit + unless unit.task_definitions.exists?(unit_parameters[:draft_task_definition_id]) + error!({ error: 'Draft task definition ID does not belong to unit' }, 403) end - unit_parameters = ActionController::Parameters.new(params) - .require(:unit) - .permit(:name, - :code, - :description, - :start_date, - :end_date, - :teaching_period_id, - :active, - :main_convenor_id, - :auto_apply_extension_before_deadline, - :send_notifications, - :enable_sync_timetable, - :enable_sync_enrolments, - :draft_task_definition_id, - :allow_student_extension_requests, - :extension_weeks_on_resubmit_request, - :allow_student_change_tutorial, - :overseer_image_id, - :assessment_enabled - ) - - if unit.teaching_period_id.present? && unit_parameters.key?(:start_date) - unit.teaching_period = nil + + # Validate that the task only has 1 upload requirement and it is a document + task = TaskDefinition.find(unit_parameters[:draft_task_definition_id]) + if task.upload_requirements.length != 1 || task.upload_requirements.first['type'] != "document" + error!({ error: 'Task definition should contain only a single document upload' }, 403) end + end - if unit_parameters[:draft_task_definition_id].present? - # Ensure the task definition belongs to unit - unless unit.task_definitions.exists?(unit_parameters[:draft_task_definition_id]) - error!({ error: 'Draft task definition ID does not belong to unit' }, 403) - end + unit.update!(unit_parameters) + present unit_parameters, with: Grape::Presenters::Presenter + end - # Validate that the task only has 1 upload requirement and it is a document - task = TaskDefinition.find(unit_parameters[:draft_task_definition_id]) - if task.upload_requirements.length != 1 || task.upload_requirements.first['type'] != "document" - error!({ error: 'Task definition should contain only a single document upload' }, 403) - end - end - - unit.update!(unit_parameters) - present unit_parameters, with: Grape::Presenters::Presenter + desc 'Create unit' + params do + requires :unit, type: Hash do + requires :name, type: String + requires :code, type: String + optional :description, type: String + optional :active, type: Boolean + optional :teaching_period_id, type: Integer + optional :start_date, type: Date + optional :end_date, type: Date + optional :main_convenor_id, type: Integer + optional :auto_apply_extension_before_deadline, type: Boolean, desc: 'Indicates if extensions before the deadline should be automatically applied', default: true + optional :send_notifications, type: Boolean, desc: 'Indicates if emails should be sent on updates each week', default: true + optional :enable_sync_timetable, type: Boolean, desc: 'Sync to timetable automatically if supported by deployment', default: true + optional :enable_sync_enrolments, type: Boolean, desc: 'Sync student enrolments automatically if supported by deployment', default: true + optional :allow_student_extension_requests, type: Boolean, desc: 'Can turn on/off student extension requests', default: true + optional :extension_weeks_on_resubmit_request, type: Integer, desc: 'Determines the number of weeks extension on a resubmit request', default: 1 + optional :allow_student_change_tutorial, type: Boolean, desc: 'Can turn on/off student ability to change tutorials', default: true + + mutually_exclusive :teaching_period_id,:start_date + mutually_exclusive :teaching_period_id,:end_date + end + end + post '/units' do + unless authorise? current_user, User, :create_unit + error!({ error: 'Not authorised to create a unit' }, 403) end - desc 'Create unit' - params do - requires :unit, type: Hash do - requires :name, type: String - requires :code, type: String - optional :description, type: String - optional :active, type: Boolean - optional :teaching_period_id, type: Integer - optional :start_date, type: Date - optional :end_date, type: Date - optional :main_convenor_id, type: Integer - optional :auto_apply_extension_before_deadline, type: Boolean, desc: 'Indicates if extensions before the deadline should be automatically applied', default: true - optional :send_notifications, type: Boolean, desc: 'Indicates if emails should be sent on updates each week', default: true - optional :enable_sync_timetable, type: Boolean, desc: 'Sync to timetable automatically if supported by deployment', default: true - optional :enable_sync_enrolments, type: Boolean, desc: 'Sync student enrolments automatically if supported by deployment', default: true - optional :allow_student_extension_requests, type: Boolean, desc: 'Can turn on/off student extension requests', default: true - optional :extension_weeks_on_resubmit_request, type: Integer, desc: 'Determines the number of weeks extension on a resubmit request', default: 1 - optional :allow_student_change_tutorial, type: Boolean, desc: 'Can turn on/off student ability to change tutorials', default: true - - mutually_exclusive :teaching_period_id,:start_date - mutually_exclusive :teaching_period_id,:end_date - end + unit_parameters = ActionController::Parameters.new(params) + .require(:unit) + .permit( + :name, + :code, + :teaching_period_id, + :description, + :start_date, + :end_date, + :auto_apply_extension_before_deadline, + :send_notifications, + :enable_sync_timetable, + :enable_sync_enrolments, + :allow_student_extension_requests, + :extension_weeks_on_resubmit_request, + :allow_student_change_tutorial, + ) + + if unit_parameters[:description].nil? + unit_parameters[:description] = unit_parameters[:name] end - post '/units' do - unless authorise? current_user, User, :create_unit - error!({ error: 'Not authorised to create a unit' }, 403) + + teaching_period_id = unit_parameters[:teaching_period_id] + if teaching_period_id.blank? + if unit_parameters[:start_date].nil? + start_date = Date.parse('Monday') + delta = start_date > Date.today ? 0 : 7 + unit_parameters[:start_date] = start_date + delta end - unit_parameters = ActionController::Parameters.new(params) - .require(:unit) - .permit( - :name, - :code, - :teaching_period_id, - :description, - :start_date, - :end_date, - :auto_apply_extension_before_deadline, - :send_notifications, - :enable_sync_timetable, - :enable_sync_enrolments, - :allow_student_extension_requests, - :extension_weeks_on_resubmit_request, - :allow_student_change_tutorial, - ) - - if unit_parameters[:description].nil? - unit_parameters[:description] = unit_parameters[:name] + if unit_parameters[:end_date].nil? + unit_parameters[:end_date] = unit_parameters[:start_date] + 16.weeks + end + else + if unit_parameters[:start_date].present? || unit_parameters[:end_date].present? + error!({ error: 'Cannot specify dates as teaching period is selected' }, 403) end + end - teaching_period_id = unit_parameters[:teaching_period_id] - if teaching_period_id.blank? - if unit_parameters[:start_date].nil? - start_date = Date.parse('Monday') - delta = start_date > Date.today ? 0 : 7 - unit_parameters[:start_date] = start_date + delta - end + unit = Unit.create!(unit_parameters) - if unit_parameters[:end_date].nil? - unit_parameters[:end_date] = unit_parameters[:start_date] + 16.weeks - end - else - if unit_parameters[:start_date].present? || unit_parameters[:end_date].present? - error!({ error: 'Cannot specify dates as teaching period is selected' }, 403) - end - end + # Employ current user as convenor + unit.employ_staff(current_user, Role.convenor) + present unit, with: Entities::UnitEntity, user: current_user + end - unit = Unit.create!(unit_parameters) + desc 'Rollover unit' + params do + optional :teaching_period_id + optional :start_date + optional :end_date + + exactly_one_of :teaching_period_id, :start_date + all_or_none_of :start_date, :end_date + end + post '/units/:id/rollover' do + unit = Unit.find(params[:id]) - # Employ current user as convenor - unit.employ_staff(current_user, Role.convenor) - present unit, with: Api::Entities::UnitEntity, user: current_user + if !(authorise?( current_user, User, :rollover) || authorise?( current_user, unit, :rollover_unit)) + error!({ error: 'Not authorised to rollover a unit' }, 403) end - desc 'Rollover unit' - params do - optional :teaching_period_id - optional :start_date - optional :end_date + teaching_period_id = params[:teaching_period_id] - exactly_one_of :teaching_period_id, :start_date - all_or_none_of :start_date, :end_date + if teaching_period_id.present? + tp = TeachingPeriod.find(teaching_period_id) + unit.rollover(tp, nil, nil) + else + unit.rollover(nil, params[:start_date], params[:end_date]) end - post '/units/:id/rollover' do - unit = Unit.find(params[:id]) - - if !(authorise?( current_user, User, :rollover) || authorise?( current_user, unit, :rollover_unit)) - error!({ error: 'Not authorised to rollover a unit' }, 403) - end - teaching_period_id = params[:teaching_period_id] + present unit, with: Entities::UnitEntity, user: current_user + end - if teaching_period_id.present? - tp = TeachingPeriod.find(teaching_period_id) - unit.rollover(tp, nil, nil) - else - unit.rollover(nil, params[:start_date], params[:end_date]) - end + desc 'Download the tasks that are awaiting feedback for a unit' + get '/units/:id/feedback' do + unit = Unit.find(params[:id]) - present unit, with: Api::Entities::UnitEntity, user: current_user + unless authorise? current_user, unit, :provide_feedback + error!({ error: 'Not authorised to provide feedback for this unit' }, 403) end - desc 'Download the tasks that are awaiting feedback for a unit' - get '/units/:id/feedback' do - unit = Unit.find(params[:id]) + tasks = unit.tasks_awaiting_feedback(current_user) + present unit.tasks_as_hash(tasks), with: Grape::Presenters::Presenter + end - unless authorise? current_user, unit, :provide_feedback - error!({ error: 'Not authorised to provide feedback for this unit' }, 403) - end + desc 'Download the tasks that should be listed under the task inbox' + get '/units/:id/tasks/inbox' do + unit = Unit.find(params[:id]) - tasks = unit.tasks_awaiting_feedback(current_user) - present unit.tasks_as_hash(tasks), with: Grape::Presenters::Presenter + unless authorise? current_user, unit, :provide_feedback + error!({ error: 'Not authorised to provide feedback for this unit' }, 403) end - desc 'Download the tasks that should be listed under the task inbox' - get '/units/:id/tasks/inbox' do - unit = Unit.find(params[:id]) - - unless authorise? current_user, unit, :provide_feedback - error!({ error: 'Not authorised to provide feedback for this unit' }, 403) - end + tasks = unit.tasks_for_task_inbox(current_user) + present unit.tasks_as_hash(tasks), with: Grape::Presenters::Presenter + end - tasks = unit.tasks_for_task_inbox(current_user) - present unit.tasks_as_hash(tasks), with: Grape::Presenters::Presenter + desc 'Download the grades for a unit' + get '/units/:id/grades' do + unit = Unit.find(params[:id]) + unless authorise? current_user, unit, :download_grades + error!({ error: 'Not authorised to download grades for this unit' }, 403) end - desc 'Download the grades for a unit' - get '/units/:id/grades' do - unit = Unit.find(params[:id]) - unless authorise? current_user, unit, :download_grades - error!({ error: 'Not authorised to download grades for this unit' }, 403) - end + content_type 'application/octet-stream' + header['Content-Disposition'] = "attachment; filename=#{unit.code}-Students.csv " + env['api.format'] = :binary - content_type 'application/octet-stream' - header['Content-Disposition'] = "attachment; filename=#{unit.code}-Students.csv " - env['api.format'] = :binary + unit.student_grades_csv + end - unit.student_grades_csv + desc 'Upload CSV of all the students in a unit' + params do + requires :file, type: File, desc: 'CSV upload file.' + end + post '/csv/units/:id' do + unit = Unit.find(params[:id]) + unless authorise? current_user, unit, :upload_csv + error!({ error: "Not authorised to upload CSV of students to #{unit.code}" }, 403) end - desc 'Upload CSV of all the students in a unit' - params do - requires :file, type: File, desc: 'CSV upload file.' + unless params[:file].present? + error!({ error: "No file uploaded" }, 403) end - post '/csv/units/:id' do - unit = Unit.find(params[:id]) - unless authorise? current_user, unit, :upload_csv - error!({ error: "Not authorised to upload CSV of students to #{unit.code}" }, 403) - end - unless params[:file].present? - error!({ error: "No file uploaded" }, 403) - end + ensure_csv!(params[:file][:tempfile]) - ensure_csv!(params[:file][:tempfile]) + # Actually import... + unit.import_users_from_csv(params[:file][:tempfile]) + end - # Actually import... - unit.import_users_from_csv(params[:file][:tempfile]) + desc 'Upload CSV with the students to un-enrol from the unit' + params do + requires :file, type: File, desc: 'CSV upload file.' + end + post '/csv/units/:id/withdraw' do + unit = Unit.find(params[:id]) + unless authorise? current_user, unit, :upload_csv + error!({ error: "Not authorised to upload CSV of students to #{unit.code}" }, 403) end - desc 'Upload CSV with the students to un-enrol from the unit' - params do - requires :file, type: File, desc: 'CSV upload file.' + unless params[:file].present? + error!({ error: "No file uploaded" }, 403) end - post '/csv/units/:id/withdraw' do - unit = Unit.find(params[:id]) - unless authorise? current_user, unit, :upload_csv - error!({ error: "Not authorised to upload CSV of students to #{unit.code}" }, 403) - end - unless params[:file].present? - error!({ error: "No file uploaded" }, 403) - end + path = params[:file][:tempfile].path - path = params[:file][:tempfile].path + ensure_csv! path - ensure_csv! path + # Actually withdraw... + response = unit.unenrol_users_from_csv(File.new(path)) + present response, with: Grape::Presenters::Presenter + end - # Actually withdraw... - response = unit.unenrol_users_from_csv(File.new(path)) - present response, with: Grape::Presenters::Presenter + desc 'Download CSV of all students in this unit' + get '/csv/units/:id' do + unit = Unit.find(params[:id]) + unless authorise? current_user, unit, :download_unit_csv + error!({ error: "Not authorised to download CSV of students enrolled in #{unit.code}" }, 403) end - desc 'Download CSV of all students in this unit' - get '/csv/units/:id' do - unit = Unit.find(params[:id]) - unless authorise? current_user, unit, :download_unit_csv - error!({ error: "Not authorised to download CSV of students enrolled in #{unit.code}" }, 403) - end + content_type 'application/octet-stream' + header['Content-Disposition'] = "attachment; filename=#{unit.code}-Students.csv " + env['api.format'] = :binary + unit.export_users_to_csv + end - content_type 'application/octet-stream' - header['Content-Disposition'] = "attachment; filename=#{unit.code}-Students.csv " - env['api.format'] = :binary - unit.export_users_to_csv + desc 'Download CSV of all student tasks in this unit' + get '/csv/units/:id/task_completion' do + unit = Unit.find(params[:id]) + unless authorise? current_user, unit, :download_unit_csv + error!({ error: "Not authorised to download CSV of student tasks in #{unit.code}" }, 403) end - desc 'Download CSV of all student tasks in this unit' - get '/csv/units/:id/task_completion' do - unit = Unit.find(params[:id]) - unless authorise? current_user, unit, :download_unit_csv - error!({ error: "Not authorised to download CSV of student tasks in #{unit.code}" }, 403) - end + content_type 'application/octet-stream' + header['Content-Disposition'] = "attachment; filename=#{unit.code}-TaskCompletion.csv " + env['api.format'] = :binary + unit.task_completion_csv + end - content_type 'application/octet-stream' - header['Content-Disposition'] = "attachment; filename=#{unit.code}-TaskCompletion.csv " - env['api.format'] = :binary - unit.task_completion_csv + desc 'Download the stats related to the number of students aiming for each grade' + get '/units/:id/stats/student_target_grade' do + unit = Unit.find(params[:id]) + unless authorise? current_user, unit, :download_stats + error!({ error: "Not authorised to download stats of student tasks in #{unit.code}" }, 403) end - desc 'Download the stats related to the number of students aiming for each grade' - get '/units/:id/stats/student_target_grade' do - unit = Unit.find(params[:id]) - unless authorise? current_user, unit, :download_stats - error!({ error: "Not authorised to download stats of student tasks in #{unit.code}" }, 403) - end + present unit.student_target_grade_stats, with: Grape::Presenters::Presenter + end - present unit.student_target_grade_stats, with: Grape::Presenters::Presenter + desc 'Download stats related to the status of students with tasks' + get '/units/:id/stats/task_status_pct' do + unit = Unit.find(params[:id]) + unless authorise? current_user, unit, :download_stats + error!({ error: "Not authorised to download stats of student tasks in #{unit.code}" }, 403) end - desc 'Download stats related to the status of students with tasks' - get '/units/:id/stats/task_status_pct' do - unit = Unit.find(params[:id]) - unless authorise? current_user, unit, :download_stats - error!({ error: "Not authorised to download stats of student tasks in #{unit.code}" }, 403) - end + present unit.task_status_stats, with: Grape::Presenters::Presenter + end - present unit.task_status_stats, with: Grape::Presenters::Presenter + desc 'Download stats related to the number of completed tasks' + get '/units/:id/stats/task_completion_stats' do + unit = Unit.find(params[:id]) + unless authorise? current_user, unit, :download_stats + error!({ error: "Not authorised to download stats of student tasks in #{unit.code}" }, 403) end - desc 'Download stats related to the number of completed tasks' - get '/units/:id/stats/task_completion_stats' do - unit = Unit.find(params[:id]) - unless authorise? current_user, unit, :download_stats - error!({ error: "Not authorised to download stats of student tasks in #{unit.code}" }, 403) - end + present unit.student_task_completion_stats, with: Grape::Presenters::Presenter + end - present unit.student_task_completion_stats, with: Grape::Presenters::Presenter + desc 'Download stats related to the number of tasks assessed by each tutor' + get '/csv/units/:id/tutor_assessments' do + unit = Unit.find(params[:id]) + unless authorise? current_user, unit, :download_stats + error!({ error: "Not authorised to download stats of statistics for #{unit.code}" }, 403) end - desc 'Download stats related to the number of tasks assessed by each tutor' - get '/csv/units/:id/tutor_assessments' do - unit = Unit.find(params[:id]) - unless authorise? current_user, unit, :download_stats - error!({ error: "Not authorised to download stats of statistics for #{unit.code}" }, 403) - end - - content_type 'application/octet-stream' - header['Content-Disposition'] = "attachment; filename=#{unit.code}-TutorAssessments.csv " - env['api.format'] = :binary + content_type 'application/octet-stream' + header['Content-Disposition'] = "attachment; filename=#{unit.code}-TutorAssessments.csv " + env['api.format'] = :binary - unit.tutor_assessment_csv - end + unit.tutor_assessment_csv end end diff --git a/app/api/users_api.rb b/app/api/users_api.rb index c1101b3b2..241b87e95 100644 --- a/app/api/users_api.rb +++ b/app/api/users_api.rb @@ -1,227 +1,226 @@ require 'grape' -require 'mime-check-helpers' -module Api - class UsersApi < Grape::API - helpers AuthenticationHelpers - helpers AuthorisationHelpers - helpers MimeCheckHelpers +class UsersApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers + helpers MimeCheckHelpers - before do - authenticated? + before do + authenticated? + end + + desc 'Get the list of users' + get '/users' do + unless authorise? current_user, User, :list_users + error!({ error: 'Cannot list users - not authorised' }, 403) end - desc 'Get the list of users' - get '/users' do - unless authorise? current_user, User, :list_users - error!({ error: 'Cannot list users - not authorised' }, 403) - end + present User.all, with: Entities::UserEntity + end - @users = User.all + desc 'Get user' + get '/users/:id', requirements: { id: /[0-9]*/ } do + user = User.find(params[:id]) + unless (user.id == current_user.id) || (authorise? current_user, User, :admin_users) + error!({ error: "Cannot find User with id #{params[:id]}" }, 403) end - desc 'Get user' - get '/users/:id', requirements: { id: /[0-9]*/ } do - user = User.find(params[:id]) - unless (user.id == current_user.id) || (authorise? current_user, User, :admin_users) - error!({ error: "Cannot find User with id #{params[:id]}" }, 403) - end - user - end + present user, with: Entities::UserEntity + end - desc 'Get convenors' - get '/users/convenors' do - unless authorise? current_user, User, :convene_units - error!({ error: 'Cannot list convenors - not authorised' }, 403) - end - @user_roles = User.convenors + desc 'Get convenors' + get '/users/convenors' do + unless authorise? current_user, User, :convene_units + error!({ error: 'Cannot list convenors - not authorised' }, 403) end - desc 'Get tutors' - get '/users/tutors' do - unless authorise? current_user, User, :convene_units - error!({ error: 'Cannot list tutors - not authorised' }, 403) - end - @user_roles = User.tutors - end + present User.convenors, with: Entities::UserEntity + end - desc 'Update a user' - params do - requires :id, type: Integer, desc: 'The user id to update' - requires :user, type: Hash do - optional :first_name, type: String, desc: 'New first name for user' - optional :last_name, type: String, desc: 'New last name for user' - optional :email, type: String, desc: 'New email address for user' - optional :student_id, type: String, desc: 'New student_id for user' - optional :nickname, type: String, desc: 'New nickname for user' - optional :system_role, type: String, desc: 'New role for user [Admin, Convenor, Tutor, Student]' - optional :receive_task_notifications, type: Boolean, desc: 'Allow user to be sent task notifications' - optional :receive_portfolio_notifications, type: Boolean, desc: 'Allow user to be sent portfolio notifications' - optional :receive_feedback_notifications, type: Boolean, desc: 'Allow user to be sent feedback notifications' - optional :opt_in_to_research, type: Boolean, desc: 'Allow user to opt in to research conducted by Doubtfire' - optional :has_run_first_time_setup, type: Boolean, desc: 'Whether or not user has run first-time setup' - end + desc 'Get tutors' + get '/users/tutors' do + unless authorise? current_user, User, :convene_units + error!({ error: 'Cannot list tutors - not authorised' }, 403) end - put '/users/:id' do - change_self = (params[:id] == current_user.id) - - params[:receive_portfolio_notifications] = true if params.key?(:receive_portfolio_notifications) && params[:receive_portfolio_notifications].nil? - params[:receive_portfolio_notifications] = true if params.key?(:receive_feedback_notifications) && params[:receive_feedback_notifications].nil? - params[:receive_portfolio_notifications] = true if params.key?(:receive_task_notifications) && params[:receive_task_notifications].nil? - - # can only modify if current_user.id is same as :id provided - # (i.e., user wants to update their own data) or if update_user token - if change_self || (authorise? current_user, User, :update_user) - - user = User.find(params[:id]) - - user_parameters = ActionController::Parameters.new(params) - .require(:user) - .permit( - :first_name, - :last_name, - :email, - :student_id, - :nickname, - :receive_task_notifications, - :receive_portfolio_notifications, - :receive_feedback_notifications, - :opt_in_to_research, - :has_run_first_time_setup - ) - - user.role = Role.student if user.role.nil? - old_role = user.role - - # have to translate the system_role -> role - # note we only let user_parameters role if we're actually *changing* the role - # (i.e., not passing in the *same* role) - # - # You cannot change your own permissions - # - if !change_self && params[:user][:system_role] && old_role.id != Role.with_name(params[:user][:system_role]).id - user_parameters[:role] = params[:user][:system_role] - end - - # - # Only allow change of role if current user has permissions to demote/promote the user to the new role - # - if user_parameters[:role] - # work out if promoting or demoting - new_role = Role.with_name(user_parameters[:role]) - - if new_role.nil? - error!({ error: "No such role name #{user_parameters[:role]}" }, 403) - end - action = new_role.id > old_role.id ? :promote_user : :demote_user - - # current user not authorised to peform action with new role? - unless authorise? current_user, User, action, User.get_change_role_perm_fn, [ old_role.to_sym, new_role.to_sym ] - error!({ error: "Not authorised to #{action} user with id=#{params[:id]} to #{new_role.name}" }, 403) - end - # update :role to actual Role object rather than String type - user_parameters[:role] = new_role - end - # Update changes made to user - user.update!(user_parameters) - user + present User.tutors, with: Entities::UserEntity + end - else - error!({ error: "Cannot modify user with id=#{params[:id]} - not authorised" }, 403) - end + desc 'Update a user' + params do + requires :id, type: Integer, desc: 'The user id to update' + requires :user, type: Hash do + optional :first_name, type: String, desc: 'New first name for user' + optional :last_name, type: String, desc: 'New last name for user' + optional :email, type: String, desc: 'New email address for user' + optional :student_id, type: String, desc: 'New student_id for user' + optional :nickname, type: String, desc: 'New nickname for user' + optional :system_role, type: String, desc: 'New role for user [Admin, Convenor, Tutor, Student]' + optional :receive_task_notifications, type: Boolean, desc: 'Allow user to be sent task notifications' + optional :receive_portfolio_notifications, type: Boolean, desc: 'Allow user to be sent portfolio notifications' + optional :receive_feedback_notifications, type: Boolean, desc: 'Allow user to be sent feedback notifications' + optional :opt_in_to_research, type: Boolean, desc: 'Allow user to opt in to research conducted by Doubtfire' + optional :has_run_first_time_setup, type: Boolean, desc: 'Whether or not user has run first-time setup' end + end + put '/users/:id' do + change_self = (params[:id] == current_user.id) - desc 'Create user' - params do - requires :user, type: Hash do - requires :first_name, type: String, desc: 'New first name for user' - requires :last_name, type: String, desc: 'New last name for user' - requires :email, type: String, desc: 'New email address for user' - optional :student_id, type: String, desc: 'New student_id for user' - requires :username, type: String, desc: 'New username for user' - requires :nickname, type: String, desc: 'New nickname for user' - requires :system_role, type: String, desc: 'New system role for user [Admin, Convenor, Tutor, Student]' - end - end - post '/users' do - # - # Only admins and convenors can create users - # - unless authorise? current_user, User, :create_user - error!({ error: 'Not authorised to create new users' }, 403) - end + params[:receive_portfolio_notifications] = true if params.key?(:receive_portfolio_notifications) && params[:receive_portfolio_notifications].nil? + params[:receive_portfolio_notifications] = true if params.key?(:receive_feedback_notifications) && params[:receive_feedback_notifications].nil? + params[:receive_portfolio_notifications] = true if params.key?(:receive_task_notifications) && params[:receive_task_notifications].nil? + + # can only modify if current_user.id is same as :id provided + # (i.e., user wants to update their own data) or if update_user token + if change_self || (authorise? current_user, User, :update_user) + + user = User.find(params[:id]) user_parameters = ActionController::Parameters.new(params) .require(:user) .permit( :first_name, :last_name, - :student_id, :email, - :username, - :nickname + :student_id, + :nickname, + :receive_task_notifications, + :receive_portfolio_notifications, + :receive_feedback_notifications, + :opt_in_to_research, + :has_run_first_time_setup ) - # have to translate the system_role -> role - user_parameters[:role] = params[:user][:system_role] + user.role = Role.student if user.role.nil? + old_role = user.role + # have to translate the system_role -> role + # note we only let user_parameters role if we're actually *changing* the role + # (i.e., not passing in the *same* role) # - # Give new user their new role + # You cannot change your own permissions # - new_role = Role.with_name(user_parameters[:role]) - if new_role.nil? - error!({ error: "No such role name #{user_parameters[:role]}" }, 403) + if !change_self && params[:user][:system_role] && old_role.id != Role.with_name(params[:user][:system_role]).id + user_parameters[:role] = params[:user][:system_role] end # - # Check permission to create user with this role + # Only allow change of role if current user has permissions to demote/promote the user to the new role # - unless authorise? current_user, User, :create_user, User.get_change_role_perm_fn, [ :nil, new_role.name.downcase.to_sym ] - error!({ error: "Not authorised to create new users with role #{new_role.name}" }, 403) + if user_parameters[:role] + # work out if promoting or demoting + new_role = Role.with_name(user_parameters[:role]) + + if new_role.nil? + error!({ error: "No such role name #{user_parameters[:role]}" }, 403) + end + action = new_role.id > old_role.id ? :promote_user : :demote_user + + # current user not authorised to peform action with new role? + unless authorise? current_user, User, action, User.get_change_role_perm_fn, [ old_role.to_sym, new_role.to_sym ] + error!({ error: "Not authorised to #{action} user with id=#{params[:id]} to #{new_role.name}" }, 403) + end + # update :role to actual Role object rather than String type + user_parameters[:role] = new_role end - # update :role to actual Role object rather than String type - user_parameters[:role] = new_role + # Update changes made to user + user.update!(user_parameters) + present user, with: Entities::UserEntity + else + error!({ error: "Cannot modify user with id=#{params[:id]} - not authorised" }, 403) + end + end - logger.info "#{current_user.username}: Created new user #{user_parameters[:username]} with role #{new_role.name}" + desc 'Create user' + params do + requires :user, type: Hash do + requires :first_name, type: String, desc: 'New first name for user' + requires :last_name, type: String, desc: 'New last name for user' + requires :email, type: String, desc: 'New email address for user' + optional :student_id, type: String, desc: 'New student_id for user' + requires :username, type: String, desc: 'New username for user' + requires :nickname, type: String, desc: 'New nickname for user' + requires :system_role, type: String, desc: 'New system role for user [Admin, Convenor, Tutor, Student]' + end + end + post '/users' do + # + # Only admins and convenors can create users + # + unless authorise? current_user, User, :create_user + error!({ error: 'Not authorised to create new users' }, 403) + end - user = User.create!(user_parameters) - user + user_parameters = ActionController::Parameters.new(params) + .require(:user) + .permit( + :first_name, + :last_name, + :student_id, + :email, + :username, + :nickname + ) + + # have to translate the system_role -> role + user_parameters[:role] = params[:user][:system_role] + + # + # Give new user their new role + # + new_role = Role.with_name(user_parameters[:role]) + if new_role.nil? + error!({ error: "No such role name #{user_parameters[:role]}" }, 403) end - desc 'Upload CSV of users' - params do - requires :file, type: File, desc: 'CSV upload file.' + # + # Check permission to create user with this role + # + unless authorise? current_user, User, :create_user, User.get_change_role_perm_fn, [ :nil, new_role.name.downcase.to_sym ] + error!({ error: "Not authorised to create new users with role #{new_role.name}" }, 403) end - post '/csv/users' do - unless authorise? current_user, User, :upload_csv - error!({ error: 'Not authorised to upload CSV of users' }, 403) - end - unless params[:file].present? - error!({ error: "No file uploaded" }, 403) - end + # update :role to actual Role object rather than String type + user_parameters[:role] = new_role - path = params[:file][:tempfile].path + logger.info "#{current_user.username}: Created new user #{user_parameters[:username]} with role #{new_role.name}" - # check mime is correct before uploading - ensure_csv!(path) + user = User.create!(user_parameters) + present user, with: Entities::UserEntity + end - # Actually import... - User.import_from_csv(current_user, File.new(path)) + desc 'Upload CSV of users' + params do + requires :file, type: File, desc: 'CSV upload file.' + end + post '/csv/users' do + unless authorise? current_user, User, :upload_csv + error!({ error: 'Not authorised to upload CSV of users' }, 403) end - desc 'Download CSV of all users' - get '/csv/users' do - unless authorise? current_user, User, :download_system_csv - error!({ error: 'Not authorised to download CSV of all users' }, 403) - end + unless params[:file].present? + error!({ error: "No file uploaded" }, 403) + end + + path = params[:file][:tempfile].path + + # check mime is correct before uploading + ensure_csv!(path) - content_type 'application/octet-stream' - header['Content-Disposition'] = 'attachment; filename=doubtfire_users.csv ' - env['api.format'] = :binary - User.export_to_csv + # Actually import... + User.import_from_csv(current_user, File.new(path)) + end + + desc 'Download CSV of all users' + get '/csv/users' do + unless authorise? current_user, User, :download_system_csv + error!({ error: 'Not authorised to download CSV of all users' }, 403) end + + content_type 'application/octet-stream' + header['Content-Disposition'] = 'attachment; filename=doubtfire_users.csv ' + env['api.format'] = :binary + User.export_to_csv end end diff --git a/app/api/webcal_api.rb b/app/api/webcal_api.rb index cb1b0976f..0a9cc997b 100644 --- a/app/api/webcal_api.rb +++ b/app/api/webcal_api.rb @@ -1,120 +1,125 @@ require 'grape' require 'icalendar' -module Api +class WebcalApi < Grape::API + helpers AuthenticationHelpers + + helpers do + # + # Wraps the specified value (expected to be either `nil` or a `Webcal`) in a hash `{ enabled: true | false }` used + # to prevent the API returning `null`. + # + def present_webcal(webcal) + if webcal.present? + present webcal, with: Entities::WebcalEntity + else + response = { enabled: false } + present response, with: Grape::Presenters::Presenter + end + end + end - class WebcalApi < Grape::API - helpers AuthenticationHelpers + # Declare content types + content_type :txt, 'text/calendar' - # Declare content types - content_type :txt, 'text/calendar' + before do + authenticated? + end - before do - authenticated? - end + desc 'Get webcal details of the authenticated user' + get '/webcal' do + present_webcal current_user.webcal + end - helpers do - # - # Wraps the specified value (expected to be either `nil` or a `Webcal`) in a hash `{ enabled: true | false }` used - # to prevent the API returning `null`. - # - def wrap_webcal(webcal) - { enabled: webcal.present? }.merge(webcal.present? ? WebcalSerializer.new(webcal).as_json : {}) + desc 'Update webcal details of the authenticated user' + params do + requires :webcal, type: Hash do + optional :enabled, type: Boolean, desc: 'Is the webcal enabled?' + optional :should_change_guid, type: Boolean, desc: 'Should the GUID of the webcal be changed?' + optional :include_start_dates, type: Boolean, desc: 'Should events for start dates be included?' + optional :unit_exclusions, type: Array[Integer], desc: 'IDs of units that must be excluded from the webcal' + + # `all_or_none_of` is used here instead of 2 `requires` parameters to allow `reminder` to be set to `null`. + optional :reminder, type: Hash do + optional :time, type: Integer + optional :unit, type: String, values: Webcal.valid_time_units, desc: 'w: weeks, d: days, h: hours, m: minutes' + all_or_none_of :time, :unit end end - - desc 'Get webcal details of the authenticated user' - get '/webcal' do - wrap_webcal current_user.webcal - end - - desc 'Update webcal details of the authenticated user' - params do - requires :webcal, type: Hash do - optional :enabled, type: Boolean, desc: 'Is the webcal enabled?' - optional :should_change_guid, type: Boolean, desc: 'Should the GUID of the webcal be changed?' - optional :include_start_dates, type: Boolean, desc: 'Should events for start dates be included?' - optional :unit_exclusions, type: Array[Integer], desc: 'IDs of units that must be excluded from the webcal' - - # `all_or_none_of` is used here instead of 2 `requires` parameters to allow `reminder` to be set to `null`. - optional :reminder, type: Hash do - optional :time, type: Integer - optional :unit, type: String, values: Webcal.valid_time_units, desc: 'w: weeks, d: days, h: hours, m: minutes' - all_or_none_of :time, :unit - end + end + put '/webcal' do + webcal_params = params[:webcal] + + user = current_user + + cal = Webcal + .includes(:webcal_unit_exclusions) + .where(user_id: user.id) + .load + .first + + # Create or destroy the user's webcal, according to the `enabled` parameter. + if webcal_params.key?(:enabled) + if webcal_params[:enabled] and cal.nil? + cal = user.create_webcal(guid: SecureRandom.uuid) + elsif !webcal_params[:enabled] and cal.present? + cal.destroy end end - put '/webcal' do - webcal_params = params[:webcal] - - user = current_user - - cal = Webcal - .includes(:webcal_unit_exclusions) - .where(user_id: user.id) - .load - .first - - # Create or destroy the user's webcal, according to the `enabled` parameter. - if webcal_params.key?(:enabled) - if webcal_params[:enabled] and cal.nil? - cal = user.create_webcal(guid: SecureRandom.uuid) - elsif !webcal_params[:enabled] and cal.present? - cal.destroy - end - end - return wrap_webcal(nil) if cal.nil? or cal.destroyed? + if cal.nil? || cal.destroyed? + present_webcal nil + return + end - webcal_update_params = {} + webcal_update_params = {} - # Change the GUID if requested. - if webcal_params.key?(:should_change_guid) - webcal_update_params[:guid] = SecureRandom.uuid - end + # Change the GUID if requested. + if webcal_params.key?(:should_change_guid) + webcal_update_params[:guid] = SecureRandom.uuid + end - # Change the reminder if requested. - if webcal_params.key?(:reminder) - if webcal_params[:reminder].nil? - webcal_update_params[:reminder_time] = webcal_update_params[:reminder_unit] = nil - else - webcal_update_params[:reminder_time] = webcal_params[:reminder][:time] - webcal_update_params[:reminder_unit] = webcal_params[:reminder][:unit] - end + # Change the reminder if requested. + if webcal_params.key?(:reminder) + if webcal_params[:reminder].nil? + webcal_update_params[:reminder_time] = webcal_update_params[:reminder_unit] = nil + else + webcal_update_params[:reminder_time] = webcal_params[:reminder][:time] + webcal_update_params[:reminder_unit] = webcal_params[:reminder][:unit] end + end - # Set any other properties that have to be updated verbatim. - webcal_update_params.merge! ActionController::Parameters.new(webcal_params).permit( - :include_start_dates, - :reminder_time, - :reminder_unit - ) - - # Update calendar. - cal.update! webcal_update_params - - # Update unit exclusions, if specified. - if webcal_params.key?(:unit_exclusions) - - # Delete existing exclusions. - cal.webcal_unit_exclusions.destroy_all - - # Add exclusions with valid unit IDs. - if webcal_params[:unit_exclusions].any? - cal.webcal_unit_exclusions.create( - Unit - .joins(:projects) - .where( - projects: { user_id: user.id }, - units: { id: webcal_params[:unit_exclusions], active: true } - ) - .pluck(:id) - .map { |i| { unit_id: i } } - ) - end + # Set any other properties that have to be updated verbatim. + webcal_update_params.merge! ActionController::Parameters.new(webcal_params).permit( + :include_start_dates, + :reminder_time, + :reminder_unit + ) + + # Update calendar. + cal.update! webcal_update_params + + # Update unit exclusions, if specified. + if webcal_params.key?(:unit_exclusions) + + # Delete existing exclusions. + cal.webcal_unit_exclusions.destroy_all + + # Add exclusions with valid unit IDs. + if webcal_params[:unit_exclusions].any? + cal.webcal_unit_exclusions.create( + Unit + .joins(:projects) + .where( + projects: { user_id: user.id }, + units: { id: webcal_params[:unit_exclusions], active: true } + ) + .pluck(:id) + .map { |i| { unit_id: i } } + ) end - - wrap_webcal cal end + + present_webcal cal end end diff --git a/app/api/webcal_public_api.rb b/app/api/webcal_public_api.rb index 190778926..420358238 100644 --- a/app/api/webcal_public_api.rb +++ b/app/api/webcal_public_api.rb @@ -1,25 +1,22 @@ require 'grape' require 'icalendar' -module Api +class WebcalPublicApi < Grape::API - class WebcalPublicApi < Grape::API - - desc 'Serve webcal with the specified GUID' - params do - requires :guid, type: String, desc: 'The GUID of the webcal' - end - get '/webcal/:guid' do - - # Retrieve the specified webcal. - webcal = Webcal.find_by!(guid: params[:guid]) + desc 'Serve webcal with the specified GUID' + params do + requires :guid, type: String, desc: 'The GUID of the webcal' + end + get '/webcal/:guid' do - # Serve the iCalendar with the correct MIME type. - content_type 'text/calendar' + # Retrieve the specified webcal. + webcal = Webcal.find_by!(guid: params[:guid]) - # Seve ical. - webcal.to_ical.to_ical - end + # Serve the iCalendar with the correct MIME type. + content_type 'text/calendar' + # Seve ical. + webcal.to_ical.to_ical end + end diff --git a/app/helpers/authentication_helpers.rb b/app/helpers/authentication_helpers.rb index 9b8266841..7ece78370 100644 --- a/app/helpers/authentication_helpers.rb +++ b/app/helpers/authentication_helpers.rb @@ -48,7 +48,10 @@ def authenticated? # Get the current user either from warden or from the header # def current_user - User.find_by_username(headers['Username']) || User.find_by_username(params['username']) + username = headers['Username'] || params['username'] + Rails.cache.fetch("user/#{username}", expires_in: 1.hours) do + User.find_by_username(username) + end end # diff --git a/app/helpers/mime-check-helpers.rb b/app/helpers/mime_check_helpers.rb similarity index 100% rename from app/helpers/mime-check-helpers.rb rename to app/helpers/mime_check_helpers.rb diff --git a/app/models/auth_token.rb b/app/models/auth_token.rb index 928a35b6f..81b48a708 100644 --- a/app/models/auth_token.rb +++ b/app/models/auth_token.rb @@ -1,15 +1,11 @@ class AuthToken < ApplicationRecord - belongs_to :user + belongs_to :user, optional: false - validates :encrypted_authentication_token, presence: true - validate :ensure_token_unique_for_user, on: :create + encrypts :authentication_token - # Auth token encryption settings - attr_encrypted :authentication_token, - key: Doubtfire::Application.secrets.secret_key_attr[0,32], - mode: :per_attribute_iv, - algorithm: 'aes-256-gcm' + validates :authentication_token, presence: true + validate :ensure_token_unique_for_user, on: :create def self.generate(user, remember, expiry_time = Time.zone.now + 2.hours) diff --git a/app/models/break.rb b/app/models/break.rb index 4bdd77ae2..d87c1b576 100644 --- a/app/models/break.rb +++ b/app/models/break.rb @@ -1,5 +1,5 @@ class Break < ApplicationRecord - belongs_to :teaching_period + belongs_to :teaching_period, optional: false validates :start_date, presence: true validates :number_of_weeks, presence: true diff --git a/app/models/comments/assessment_comment.rb b/app/models/comments/assessment_comment.rb index a0a4cc25b..a6dea9e6c 100644 --- a/app/models/comments/assessment_comment.rb +++ b/app/models/comments/assessment_comment.rb @@ -1,6 +1,6 @@ class AssessmentComment < TaskComment - belongs_to :overseer_assessment + belongs_to :overseer_assessment, optional: false before_create do self.content_type = :assessment diff --git a/app/models/comments/comments_read_receipts.rb b/app/models/comments/comments_read_receipts.rb index ca1e7b7b7..ba51d0a69 100644 --- a/app/models/comments/comments_read_receipts.rb +++ b/app/models/comments/comments_read_receipts.rb @@ -2,6 +2,6 @@ class CommentsReadReceipts < ApplicationRecord validates :user, presence: true validates :task_comment, presence: true - belongs_to :task_comment - belongs_to :user + belongs_to :task_comment, optional: false + belongs_to :user, optional: false end diff --git a/app/models/comments/extension_comment.rb b/app/models/comments/extension_comment.rb index 73237bbcc..c2f0dc973 100644 --- a/app/models/comments/extension_comment.rb +++ b/app/models/comments/extension_comment.rb @@ -1,6 +1,6 @@ class ExtensionComment < TaskComment - belongs_to :assessor, class_name: 'User' + belongs_to :assessor, class_name: 'User', optional: true def serialize(user) json = super(user) diff --git a/app/models/comments/task_comment.rb b/app/models/comments/task_comment.rb index a2ab34052..1220ef28e 100644 --- a/app/models/comments/task_comment.rb +++ b/app/models/comments/task_comment.rb @@ -7,19 +7,19 @@ class TaskComment < ApplicationRecord include FileHelper include AuthorisationHelpers - belongs_to :task # Foreign key - belongs_to :user + belongs_to :task, optional: false # Foreign key + belongs_to :user, optional: false has_one :unit, through: :task has_one :project, through: :task - belongs_to :recipient, class_name: 'User' + belongs_to :recipient, class_name: 'User', optional: false has_one :discussion_comment, class_name: 'DiscussionComment', required: false has_many :comments_read_receipts, class_name: 'CommentsReadReceipts', dependent: :destroy, inverse_of: :task_comment # Can optionally be a reply to a comment - belongs_to :task_comment + belongs_to :task_comment, optional: true validates :task, presence: true validates :user, presence: true diff --git a/app/models/comments/task_status_comment.rb b/app/models/comments/task_status_comment.rb index 743953101..f24c00cf6 100644 --- a/app/models/comments/task_status_comment.rb +++ b/app/models/comments/task_status_comment.rb @@ -1,6 +1,6 @@ class TaskStatusComment < TaskComment - belongs_to :task_status + belongs_to :task_status, optional: false before_create do self.content_type = :status diff --git a/app/models/group.rb b/app/models/group.rb index 10b38d72c..bcaf07de3 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -1,8 +1,8 @@ class Group < ApplicationRecord include LogHelper - belongs_to :group_set - belongs_to :tutorial + belongs_to :group_set, optional: false + belongs_to :tutorial, optional: false has_many :group_memberships, dependent: :destroy has_many :group_submissions diff --git a/app/models/group_membership.rb b/app/models/group_membership.rb index 5f888390d..0ed199fe9 100644 --- a/app/models/group_membership.rb +++ b/app/models/group_membership.rb @@ -4,8 +4,8 @@ class GroupMembership < ApplicationRecord include LogHelper - belongs_to :group - belongs_to :project + belongs_to :group, optional: false + belongs_to :project, optional: false has_one :group_set, through: :group validate :must_be_in_same_tutorial, if: :restricted_to_tutorial? diff --git a/app/models/group_set.rb b/app/models/group_set.rb index 3532c7698..e7005214a 100644 --- a/app/models/group_set.rb +++ b/app/models/group_set.rb @@ -1,5 +1,5 @@ class GroupSet < ApplicationRecord - belongs_to :unit + belongs_to :unit, optional: false has_many :task_definitions has_many :groups, dependent: :destroy diff --git a/app/models/group_submission.rb b/app/models/group_submission.rb index 9ce47f845..52f1b0fb7 100644 --- a/app/models/group_submission.rb +++ b/app/models/group_submission.rb @@ -4,11 +4,12 @@ class GroupSubmission < ApplicationRecord include LogHelper - belongs_to :group - belongs_to :task_definition + belongs_to :group, optional: false + belongs_to :task_definition, optional: false + belongs_to :submitted_by_project, class_name: 'Project', foreign_key: 'submitted_by_project_id', optional: false + has_many :tasks, dependent: :nullify has_many :projects, through: :tasks - belongs_to :submitted_by_project, class_name: 'Project', foreign_key: 'submitted_by_project_id' # # Ensure file is also deleted diff --git a/app/models/learning_outcome.rb b/app/models/learning_outcome.rb index 1a400616a..6532fd56c 100644 --- a/app/models/learning_outcome.rb +++ b/app/models/learning_outcome.rb @@ -1,7 +1,7 @@ class LearningOutcome < ApplicationRecord include ApplicationHelper - belongs_to :unit + belongs_to :unit, optional: false has_many :learning_outcome_task_links, dependent: :destroy # links to learning outcomes has_many :related_task_definitions, -> { where('learning_outcome_task_links.task_id is NULL') }, through: :learning_outcome_task_links, source: :task_definition # only link staff relations diff --git a/app/models/learning_outcome_task_link.rb b/app/models/learning_outcome_task_link.rb index 45f7a498e..4747c732a 100644 --- a/app/models/learning_outcome_task_link.rb +++ b/app/models/learning_outcome_task_link.rb @@ -1,9 +1,9 @@ class LearningOutcomeTaskLink < ApplicationRecord default_scope { all } - belongs_to :task_definition - belongs_to :task - belongs_to :learning_outcome + belongs_to :task_definition, optional: false + belongs_to :task, optional: true + belongs_to :learning_outcome, optional: false validates :task_definition, presence: true validates :learning_outcome, presence: true @@ -13,7 +13,7 @@ class LearningOutcomeTaskLink < ApplicationRecord def ensure_relations_unique return if learning_outcome.nil? || task_definition.nil? - + if id.nil? related_links = LearningOutcomeTaskLink.where('task_definition_id = :task_definition_id AND learning_outcome_id = :learning_outcome_id', task_definition_id: task_definition.id, learning_outcome_id: learning_outcome.id) else diff --git a/app/models/overseer_assessment.rb b/app/models/overseer_assessment.rb index 3219ee033..852972dd5 100644 --- a/app/models/overseer_assessment.rb +++ b/app/models/overseer_assessment.rb @@ -1,5 +1,5 @@ class OverseerAssessment < ApplicationRecord - belongs_to :task + belongs_to :task, optional: false has_one :project, through: :task has_many :assessment_comments, dependent: :destroy diff --git a/app/models/plagiarism_match_link.rb b/app/models/plagiarism_match_link.rb index f776fe6aa..79dc4f298 100644 --- a/app/models/plagiarism_match_link.rb +++ b/app/models/plagiarism_match_link.rb @@ -1,8 +1,8 @@ class PlagiarismMatchLink < ApplicationRecord include LogHelper - belongs_to :task - belongs_to :other_task, class_name: 'Task' + belongs_to :task, optional: false + belongs_to :other_task, class_name: 'Task', optional: false # # Ensure file is also deleted diff --git a/app/models/project.rb b/app/models/project.rb index f9290d122..3dd6a4316 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -15,16 +15,15 @@ class Project < ApplicationRecord include LogHelper include DbHelpers - belongs_to :unit - belongs_to :user - belongs_to :campus + belongs_to :unit, optional: false + belongs_to :user, optional: false + belongs_to :campus, optional: true # has_one :user, through: :student has_many :tasks, dependent: :destroy # Destroying a project will also nuke all of its tasks has_many :group_memberships, dependent: :destroy has_many :groups, -> { where('group_memberships.active = :value', value: true) }, through: :group_memberships - has_many :past_groups, -> { where('group_memberships.active = :value', value: false) }, through: :group_memberships, source: 'group' has_many :task_engagements, through: :tasks has_many :comments, through: :tasks has_many :tutorial_enrolments, dependent: :destroy @@ -856,7 +855,7 @@ def init(project, is_retry) end def make_pdf - render_to_string(template: '/portfolio/portfolio_pdf.pdf.erb', layout: true) + render_to_string(template: '/portfolio/portfolio_pdf', layout: true) end end diff --git a/app/models/task.rb b/app/models/task.rb index 42a466feb..90cfb0ea7 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -93,10 +93,10 @@ def specific_permission_hash(role, perm_hash, _other) before_destroy :delete_associated_files # Model associations - belongs_to :task_definition # Foreign key - belongs_to :project # Foreign key - belongs_to :task_status # Foreign key - belongs_to :group_submission + belongs_to :task_definition, optional: false # Foreign key + belongs_to :project, optional: false # Foreign key + belongs_to :task_status, optional: false # Foreign key + belongs_to :group_submission, optional: true has_one :unit, through: :project @@ -981,7 +981,7 @@ def init(task, is_retry) end def make_pdf - render_to_string(template: '/task/task_pdf.pdf.erb', layout: true) + render_to_string(template: '/task/task_pdf', layout: true) end end diff --git a/app/models/task_definition.rb b/app/models/task_definition.rb index cb5e5584e..afa446f83 100644 --- a/app/models/task_definition.rb +++ b/app/models/task_definition.rb @@ -12,10 +12,10 @@ class TaskDefinition < ApplicationRecord after_update :remove_old_group_submissions, if: :has_removed_group? # Model associations - belongs_to :unit # Foreign key - belongs_to :group_set - belongs_to :tutorial_stream - belongs_to :overseer_image + belongs_to :unit, optional: false # Foreign key + belongs_to :group_set, optional: true + belongs_to :tutorial_stream, optional: true + belongs_to :overseer_image, optional: true has_one :draft_task_definition_unit, foreign_key: 'draft_task_definition_id', class_name: 'Unit', dependent: :nullify @@ -378,7 +378,7 @@ def target_week_and_day= value # Override due date to return either the final date of the unit, or the set due date def due_date return self['due_date'] if self['due_date'].present? - return unit.end_date + return unit.end_date #TODO: use nil as default to improve performance end def due_week diff --git a/app/models/task_engagement.rb b/app/models/task_engagement.rb index 8241be205..9ff726afd 100644 --- a/app/models/task_engagement.rb +++ b/app/models/task_engagement.rb @@ -1,3 +1,3 @@ class TaskEngagement < ApplicationRecord - belongs_to :task + belongs_to :task, optional: false end diff --git a/app/models/task_pin.rb b/app/models/task_pin.rb index 0cbeaba9e..f2f51173c 100644 --- a/app/models/task_pin.rb +++ b/app/models/task_pin.rb @@ -1,4 +1,4 @@ class TaskPin < ApplicationRecord - belongs_to :task - belongs_to :user + belongs_to :task, optional: false + belongs_to :user, optional: false end diff --git a/app/models/task_submission.rb b/app/models/task_submission.rb index 03a57d139..178f565c4 100644 --- a/app/models/task_submission.rb +++ b/app/models/task_submission.rb @@ -1,4 +1,4 @@ class TaskSubmission < ApplicationRecord - belongs_to :task - belongs_to :assessor, class_name: 'User', foreign_key: 'assessor_id' + belongs_to :task, optional: false + belongs_to :assessor, class_name: 'User', foreign_key: 'assessor_id', optional: true end diff --git a/app/models/tutorial.rb b/app/models/tutorial.rb index 1bd36dc24..4a9635fe7 100644 --- a/app/models/tutorial.rb +++ b/app/models/tutorial.rb @@ -1,9 +1,9 @@ class Tutorial < ApplicationRecord # Model associations - belongs_to :unit # Foreign key - belongs_to :unit_role # Foreign key - belongs_to :campus - belongs_to :tutorial_stream + belongs_to :unit, optional: false # Foreign key + belongs_to :unit_role, optional: true # Foreign key + belongs_to :campus, optional: true + belongs_to :tutorial_stream, optional: true has_one :tutor, through: :unit_role, source: :user @@ -42,8 +42,7 @@ def self.find_by_user(user) end def tutor - result = UnitRole.find_by(id: unit_role_id) - result.user unless result.nil? + unit_role.user unless unit_role.nil? end def name diff --git a/app/models/tutorial_enrolment.rb b/app/models/tutorial_enrolment.rb index aeef2f0ac..353bcd8ad 100644 --- a/app/models/tutorial_enrolment.rb +++ b/app/models/tutorial_enrolment.rb @@ -1,6 +1,6 @@ class TutorialEnrolment < ApplicationRecord - belongs_to :tutorial - belongs_to :project + belongs_to :tutorial, optional: false + belongs_to :project, optional: false has_one :tutorial_stream, through: :tutorial diff --git a/app/models/tutorial_stream.rb b/app/models/tutorial_stream.rb index a57643431..81de5ff40 100644 --- a/app/models/tutorial_stream.rb +++ b/app/models/tutorial_stream.rb @@ -1,6 +1,6 @@ class TutorialStream < ApplicationRecord - belongs_to :activity_type - belongs_to :unit + belongs_to :activity_type, optional: false + belongs_to :unit, optional: false # Callbacks - methods called are private after_create :handle_associated_task_defs diff --git a/app/models/unit.rb b/app/models/unit.rb index d0de91794..105848717 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -131,13 +131,13 @@ def role_for(user) has_many :staff, -> { joins(:role).where('roles.name = :role_convenor or roles.name = :role_tutor', role_convenor: 'Convenor', role_tutor: 'Tutor') }, class_name: 'UnitRole' # Unit has a teaching period - belongs_to :teaching_period + belongs_to :teaching_period, optional: true - belongs_to :main_convenor, class_name: 'UnitRole' + belongs_to :main_convenor, class_name: 'UnitRole', optional: true - belongs_to :draft_task_definition, class_name: 'TaskDefinition' + belongs_to :draft_task_definition, class_name: 'TaskDefinition', optional: true - belongs_to :overseer_image + belongs_to :overseer_image, optional: true validates :name, :description, :start_date, :end_date, presence: true diff --git a/app/models/unit_role.rb b/app/models/unit_role.rb index 4a2ce6e57..51a5722d8 100644 --- a/app/models/unit_role.rb +++ b/app/models/unit_role.rb @@ -1,9 +1,9 @@ class UnitRole < ApplicationRecord # Model associations - belongs_to :unit # Foreign key - belongs_to :user # Foreign key + belongs_to :unit, optional: false # Foreign key + belongs_to :user, optional: false # Foreign key - belongs_to :role # Foreign key + belongs_to :role, optional: false # Foreign key has_many :tutorials, class_name: 'Tutorial', dependent: :nullify has_many :projects, through: :tutorials @@ -28,10 +28,6 @@ class UnitRole < ApplicationRecord scope :tutors, -> { joins(:role).where('roles.name = :role', role: 'Tutor') } scope :convenors, -> { joins(:role).where('roles.name = :role', role: 'Convenor') } - def self.for_user(user) - UnitRole.joins(:role, :unit).where("user_id = :user_id and roles.name <> 'Student'", user_id: user.id) - end - def tasks_awaiting_feedback tasks.joins(:task_definition).where('projects.enrolled = TRUE AND projects.target_grade >= task_definitions.target_grade AND tasks.task_status_id = :status', status: TaskStatus.ready_for_feedback) end diff --git a/app/models/user.rb b/app/models/user.rb index 53925d809..fd939b914 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -114,7 +114,6 @@ def authentication_token_expired? # Returns authentication of the user # def token_for_text?(a_token) - list_tokens = [] self.auth_tokens.each do |token| if a_token == token.authentication_token return token @@ -128,7 +127,7 @@ def token_for_text?(a_token) ### # Model associations - belongs_to :role # Foreign Key + belongs_to :role, optional: false # Foreign Key has_many :unit_roles, dependent: :destroy has_many :projects has_many :auth_tokens diff --git a/app/models/webcal.rb b/app/models/webcal.rb index 1b0f8c60c..820d62fc7 100644 --- a/app/models/webcal.rb +++ b/app/models/webcal.rb @@ -2,7 +2,7 @@ class Webcal < ApplicationRecord - belongs_to :user + belongs_to :user, optional: false has_many :webcal_unit_exclusions, dependent: :destroy diff --git a/app/models/webcal_unit_exclusion.rb b/app/models/webcal_unit_exclusion.rb index c78f47f73..2cbb5d57c 100644 --- a/app/models/webcal_unit_exclusion.rb +++ b/app/models/webcal_unit_exclusion.rb @@ -1,4 +1,4 @@ class WebcalUnitExclusion < ApplicationRecord - belongs_to :webcal - belongs_to :unit + belongs_to :webcal, optional: false + belongs_to :unit, optional: false end diff --git a/app/serializers/activity_type_serializer.rb b/app/serializers/activity_type_serializer.rb deleted file mode 100644 index f7105b278..000000000 --- a/app/serializers/activity_type_serializer.rb +++ /dev/null @@ -1,6 +0,0 @@ -# Doubtfire will deprecate ActiveModelSerializer in the future. -# Instead, write a serialize method on the model. - -class ActivityTypeSerializer < DoubtfireSerializer - attributes :id, :name, :abbreviation -end diff --git a/app/serializers/doubtfire_serializer.rb b/app/serializers/doubtfire_serializer.rb deleted file mode 100644 index ee59ef819..000000000 --- a/app/serializers/doubtfire_serializer.rb +++ /dev/null @@ -1,7 +0,0 @@ -class DoubtfireSerializer < ActiveModel::Serializer - def object - result = super() - return result unless result.is_a? ActiveModel::Serializer - result.object - end -end \ No newline at end of file diff --git a/app/serializers/group_serializer.rb b/app/serializers/group_serializer.rb deleted file mode 100644 index b2641915b..000000000 --- a/app/serializers/group_serializer.rb +++ /dev/null @@ -1,16 +0,0 @@ -# Doubtfire will deprecate ActiveModelSerializer in the future. -# Instead, write a serialize method on the - -class GroupSerializer < ActiveModel::Serializer - attributes :id, :name, :tutorial_id, :group_set_id, :student_count, :capacity_adjustment, :locked - - def student_count - return object.student_count if object.has_attribute?(:student_count) - return object[:student_count] if object.has_attribute?(:has_key?) && object.has_key?(:student_count) - return 0 - end -end - -class GroupMembershipSerializer < ActiveModel::Serializer - attributes :group_id, :project_id -end diff --git a/app/serializers/group_set_serializer.rb b/app/serializers/group_set_serializer.rb deleted file mode 100644 index fdb3cdbde..000000000 --- a/app/serializers/group_set_serializer.rb +++ /dev/null @@ -1,11 +0,0 @@ -# Doubtfire will deprecate ActiveModelSerializer in the future. -# Instead, write a serialize method on the model. - -class GroupSetSerializer < DoubtfireSerializer - attributes :id, :name, - :allow_students_to_create_groups, - :allow_students_to_manage_groups, - :keep_groups_in_same_class, - :capacity, - :locked -end diff --git a/app/serializers/hash_serializer.rb b/app/serializers/hash_serializer.rb deleted file mode 100644 index 7c579b2f2..000000000 --- a/app/serializers/hash_serializer.rb +++ /dev/null @@ -1,9 +0,0 @@ -# Serialise a hash using a list of attributes from the active model serialiser -class HashSerializer < DoubtfireSerializer - # Add support for reading the attribute from the hash without Active Model support - def read_attribute_for_serialization(attr) - return object[attr] if object.key?(attr) - return object[attr.to_sym] if object.key?(attr.to_sym) - object[attr.to_s] - end -end diff --git a/app/serializers/learning_outcome_serializer.rb b/app/serializers/learning_outcome_serializer.rb deleted file mode 100644 index 1319baa34..000000000 --- a/app/serializers/learning_outcome_serializer.rb +++ /dev/null @@ -1,6 +0,0 @@ -# Doubtfire will deprecate ActiveModelSerializer in the future. -# Instead, write a serialize method on the model. - -class LearningOutcomeSerializer < DoubtfireSerializer - attributes :id, :ilo_number, :abbreviation, :name, :description -end diff --git a/app/serializers/learning_outcome_task_link_serializer.rb b/app/serializers/learning_outcome_task_link_serializer.rb deleted file mode 100644 index 6ae6eb488..000000000 --- a/app/serializers/learning_outcome_task_link_serializer.rb +++ /dev/null @@ -1,11 +0,0 @@ -# Doubtfire will deprecate ActiveModelSerializer in the future. -# Instead, write a serialize method on the model. - -class LearningOutcomeTaskLinkSerializer < DoubtfireSerializer - attributes :id, - :description, - :rating, - :learning_outcome_id, - :task_definition_id, - :task_id -end diff --git a/app/serializers/project_serializer.rb b/app/serializers/project_serializer.rb deleted file mode 100644 index 104d1ccf3..000000000 --- a/app/serializers/project_serializer.rb +++ /dev/null @@ -1,111 +0,0 @@ -# Doubtfire will deprecate ActiveModelSerializer in the future. -# Instead, write a serialize method on the model. - -require 'task_serializer' - -class ShallowProjectSerializer < HashSerializer - attributes :unit_id, - :unit_code, - :unit_name, - :project_id, - :campus_id, - :target_grade, - :has_portfolio, - :start_date, - :end_date, - :teaching_period_id, - :active -end - -class ProjectSerializer < DoubtfireSerializer - attributes :unit_id, - :project_id, - :student_id, - :campus_id, - :started, - :stats, - :student_name, - :burndown_chart_data, - :enrolled, - :target_grade, - :submitted_grade, - :portfolio_files, - :compile_portfolio, - :portfolio_available, - :grade, - :grade_rationale, - :tasks, - :uses_draft_learning_summary - - has_many :tutorial_enrolments - - def project_id - object.id - end - - def student_name - "#{object.student.name}#{object.student.nickname.nil? ? '' : ' (' << object.student.nickname << ')'}" - end - - def student_id - object.student.username - end - - def stats - object.task_stats - end - - def tasks - object.task_details_for_shallow_serializer(Thread.current[:user]) - end - - has_many :groups, serializer: GroupSerializer - has_many :task_outcome_alignments, serializer: LearningOutcomeTaskLinkSerializer - - def my_role_obj - object.role_for(Thread.current[:user]) if Thread.current[:user] - end - - def include_grade? - ([ Role.convenor, :convenor, Role.tutor, :tutor ].include? my_role_obj) - end - - def include_grade_rationale? - ([ Role.convenor, :convenor, Role.tutor, :tutor ].include? my_role_obj) - end - - def filter(keys) - keys.delete :grade unless include_grade? - keys.delete :grade_rationale unless include_grade_rationale? - keys - end -end - -class GroupMemberProjectSerializer < DoubtfireSerializer - attributes :student_id, :project_id, :student_name, :target_grade - - def project_id - object.id - end - - def student_id - object.student.username - end - - def student_name - "#{object.student.name}#{object.student.nickname.nil? ? '' : ' (' << object.student.nickname << ')'}" - end - - def my_role_obj - object.role_for(Thread.current[:user]) if Thread.current[:user] - end - - def include_student_id? - ([ Role.convenor, Role.tutor, :tutor, :convenor ].include? my_role_obj) - end - - def filter(keys) - keys.delete :student_id unless include_student_id? - keys - end -end diff --git a/app/serializers/role_serializer.rb b/app/serializers/role_serializer.rb deleted file mode 100644 index 4297b75a7..000000000 --- a/app/serializers/role_serializer.rb +++ /dev/null @@ -1,6 +0,0 @@ -# Doubtfire will deprecate ActiveModelSerializer in the future. -# Instead, write a serialize method on the model. - -class RoleSerializer < DoubtfireSerializer - attributes :name, :description -end diff --git a/app/serializers/task_comment_serializer.rb b/app/serializers/task_comment_serializer.rb deleted file mode 100644 index 216f9a2f1..000000000 --- a/app/serializers/task_comment_serializer.rb +++ /dev/null @@ -1,15 +0,0 @@ -class TaskCommentSerializer < HashSerializer - class AuthorSerializer < HashSerializer - attributes :id, :name, :email - end - - attributes :id, - :comment, - :has_attachment, - :type, - :is_new, - :created_at, - :recipient_read_time - has_one :author, serializer: TaskCommentSerializer::AuthorSerializer - has_one :recipient, serializer: TaskCommentSerializer::AuthorSerializer -end \ No newline at end of file diff --git a/app/serializers/task_definition_serializer.rb b/app/serializers/task_definition_serializer.rb deleted file mode 100644 index 6f94d0a50..000000000 --- a/app/serializers/task_definition_serializer.rb +++ /dev/null @@ -1,22 +0,0 @@ -# Doubtfire will deprecate ActiveModelSerializer in the future. -# Instead, write a serialize method on the model. - -class TaskDefinitionSerializer < DoubtfireSerializer - attributes :id, :abbreviation, :name, :description, - :weight, :target_grade, :target_date, - :upload_requirements, - :tutorial_stream, - :plagiarism_checks, :plagiarism_report_url, :plagiarism_warn_pct, - :restrict_status_updates, - :group_set_id, :has_task_sheet?, :has_task_resources?, - :due_date, :start_date, :is_graded, :max_quality_pts, - :overseer_image_id, :assessment_enabled, :has_task_assessment_resources? - - def weight - object.weighting - end - - def tutorial_stream - object.tutorial_stream.abbreviation unless object.tutorial_stream.nil? - end -end diff --git a/app/serializers/task_serializer.rb b/app/serializers/task_serializer.rb deleted file mode 100644 index 85120c8f6..000000000 --- a/app/serializers/task_serializer.rb +++ /dev/null @@ -1,53 +0,0 @@ -# Doubtfire will deprecate ActiveModelSerializer in the future. -# Instead, write a serialize method on the model. - -class TaskUpdateSerializer < DoubtfireSerializer - attributes :id, :status, :project_id, :new_stats, :include_in_portfolio, :other_projects, :times_assessed, :grade, :quality_pts, :due_date, :extensions - - def new_stats - object.project.task_stats - end - - def other_projects - return nil unless object.group_task? && !object.group.nil? - grp = object.group - grp.projects.select { |p| p.id != object.project_id }.map { |p| { id: p.id, new_stats: p.task_stats } } - end - -end - -class TaskStatSerializer < DoubtfireSerializer - attributes :id, :task_abbr, :status, :tutorial_id, :times_assessed - - def task_abbr - object.task_definition.abbreviation - end - - # def tutorial_id - # object.project.tutorial.id unless object.project.tutorial.nil? - # end -end - -class TaskSerializer < DoubtfireSerializer - attributes :id, :status, :completion_date, :due_date, :extensions, :task_name, :task_desc, :task_weight, :task_abbr, :upload_requirements, :pct_similar, :similar_to_count, :times_assessed, :similar_to_dismissed_count - - def task_name - object.task_definition.name - end - - def task_desc - object.task_definition.description - end - - def task_weight - object.task_definition.weighting - end - - def task_abbr - object.task_definition.abbreviation - end - - def upload_requirements - object.task_definition.upload_requirements - end -end diff --git a/app/serializers/teaching_period_serializer.rb b/app/serializers/teaching_period_serializer.rb deleted file mode 100644 index 9f810ca37..000000000 --- a/app/serializers/teaching_period_serializer.rb +++ /dev/null @@ -1,10 +0,0 @@ -# Doubtfire will deprecate ActiveModelSerializer in the future. -# Instead, write a serialize method on the model. - -class TeachingPeriodSerializer < DoubtfireSerializer - attributes :id, :period, :year, :start_date, :end_date, :active_until, :active, :breaks, :units - - def active - object.active_until > DateTime.now - end -end diff --git a/app/serializers/tutorial_enrolment_serializer.rb b/app/serializers/tutorial_enrolment_serializer.rb deleted file mode 100644 index 0e52e231a..000000000 --- a/app/serializers/tutorial_enrolment_serializer.rb +++ /dev/null @@ -1,3 +0,0 @@ -class TutorialEnrolmentSerializer < DoubtfireSerializer - attributes :id, :project_id, :tutorial_id -end diff --git a/app/serializers/tutorial_serializer.rb b/app/serializers/tutorial_serializer.rb deleted file mode 100644 index ab2bb815e..000000000 --- a/app/serializers/tutorial_serializer.rb +++ /dev/null @@ -1,39 +0,0 @@ -# Doubtfire will deprecate ActiveModelSerializer in the future. -# Instead, write a serialize method on the model. - -require 'user_serializer' - -class TutorialSerializer < DoubtfireSerializer - attributes :id, :meeting_day, :meeting_time, :meeting_location, :abbreviation, :campus_id, :capacity, :num_students, - :tutorial_stream - - def tutorial_stream - object.tutorial_stream.abbreviation unless object.tutorial_stream.nil? - end - - def meeting_time - object.meeting_time.to_time - # DateTime.parse("#{object.meeting_time}") - end - - has_one :tutor, serializer: ShallowUserSerializer - - def include_tutor? - if Thread.current[:user] - my_role = object.unit.role_for(Thread.current[:user]) - [ Role.convenor, Role.admin ].include? my_role - end - end - - def include_num_students? - if Thread.current[:user] - my_role = object.unit.role_for(Thread.current[:user]) - [ Role.convenor, Role.tutor, Role.admin ].include? my_role - end - end - - def filter(keys) - keys.delete :num_students unless include_num_students? - keys - end -end diff --git a/app/serializers/tutorial_stream_serializer.rb b/app/serializers/tutorial_stream_serializer.rb deleted file mode 100644 index 7abcfeaa3..000000000 --- a/app/serializers/tutorial_stream_serializer.rb +++ /dev/null @@ -1,10 +0,0 @@ -# Doubtfire will deprecate ActiveModelSerializer in the future. -# Instead, write a serialize method on the model. - -class TutorialStreamSerializer < DoubtfireSerializer - attributes :id, :name, :abbreviation, :activity_type - - def activity_type - object.activity_type.abbreviation - end -end diff --git a/app/serializers/unit_role_serializer.rb b/app/serializers/unit_role_serializer.rb deleted file mode 100644 index dbb78dcf9..000000000 --- a/app/serializers/unit_role_serializer.rb +++ /dev/null @@ -1,82 +0,0 @@ -# Doubtfire will deprecate ActiveModelSerializer in the future. -# Instead, write a serialize method on the model. - -require 'user_serializer' - -class ShallowUnitRoleSerializer < DoubtfireSerializer - attributes :id, :role - - def role - object.role.name - end -end - -class UnitRoleSerializer < DoubtfireSerializer - attributes :id, :role, :user_id, :unit_id, :unit_name, :name, :unit_code, :start_date, :end_date, :teaching_period_id, :active - - # has_one :user, serializer: ShallowUserSerializer - # has_one :unit, serializer: ShallowUnitSerializer - # has_one :role - - def role - object.role.name - end - - def unit_id - object.unit.id - end - - def unit_code - object.unit.code - end - - def unit_name - object.unit.name - end - - def teaching_period_id - object.unit.teaching_period_id - end - - def name - object.user.name - end - - def active - object.unit.active - end - - def include_start_date? - object.has_attribute? :start_date - end - - def include_end_date? - object.has_attribute? :end_date - end - - def filter(keys) - keys.delete :start_date unless include_start_date? - keys.delete :end_date unless include_end_date? - keys - end -end - -class UserUnitRoleSerializer < ActiveModel::Serializer - attributes :id, :user_id, :name, :role, :email - - def role - object.role.name - end - - def name - object.user.name - end - - def user_name - object.user.name - end - - def email - object.user.email - end -end diff --git a/app/serializers/unit_serializer.rb b/app/serializers/unit_serializer.rb deleted file mode 100644 index 0ce27fc1a..000000000 --- a/app/serializers/unit_serializer.rb +++ /dev/null @@ -1,84 +0,0 @@ -# Doubtfire will deprecate ActiveModelSerializer in the future. -# Instead, write a serialize method on the model. - -require 'unit_role_serializer' - -class ShallowUnitSerializer < DoubtfireSerializer - attributes :code, :id, :name, :teaching_period_id, :start_date, :end_date, :active, :overseer_image_id, :assessment_enabled -end - -class UnitSerializer < DoubtfireSerializer - attributes :code, :id, :name, :my_role, :main_convenor_id, :description, :teaching_period_id, :start_date, :end_date, :active, :convenors, :ilos, :auto_apply_extension_before_deadline, :send_notifications, :enable_sync_enrolments, :enable_sync_timetable, :group_memberships, :draft_task_definition_id, :allow_student_extension_requests, :extension_weeks_on_resubmit_request, :allow_student_change_tutorial, :overseer_image_id, :assessment_enabled - - def start_date - object.start_date.to_date - end - - def end_date - object.end_date.to_date - end - - def my_role_obj - object.role_for(Thread.current[:user]) if Thread.current[:user] - end - - def my_user_role - Thread.current[:user].role if Thread.current[:user] - end - - def role - role = my_role_obj - role.name unless role.nil? - end - - def my_role - role - end - - def ilos - object.learning_outcomes - end - - def main_convenor_id - object.main_convenor.id - end - - has_many :tutorial_streams - has_many :tutorials - has_many :tutorial_enrolments - has_many :task_definitions - has_many :convenors, serializer: UserUnitRoleSerializer - has_many :staff, serializer: UserUnitRoleSerializer - has_many :group_sets, serializer: GroupSetSerializer - has_many :ilos, serializer: LearningOutcomeSerializer - has_many :task_outcome_alignments, serializer: LearningOutcomeTaskLinkSerializer - has_many :groups, serializer: GroupSerializer - - def group_memberships - ActiveModel::ArraySerializer.new(object.group_memberships.where(active: true), each_serializer: GroupMembershipSerializer) - end - - def include_convenors? - ([ Role.convenor, :convenor ].include? my_role_obj) || (my_user_role == Role.admin) - end - - def include_staff? - ([ Role.convenor, :convenor, Role.tutor, :tutor ].include? my_role_obj) || (my_user_role == Role.admin) - end - - def include_groups? - ([ Role.convenor, :convenor, Role.tutor, :tutor ].include? my_role_obj) || (my_user_role == Role.admin) - end - - def include_enrolments? - ([ Role.convenor, :convenor, Role.tutor, :tutor ].include? my_role_obj) || (my_user_role == Role.admin) - end - - def filter(keys) - keys.delete :groups unless include_groups? - keys.delete :convenors unless include_convenors? - keys.delete :staff unless include_staff? - keys.delete :tutorial_enrolments unless include_enrolments? - keys - end -end diff --git a/app/serializers/user_role_serializer.rb b/app/serializers/user_role_serializer.rb deleted file mode 100644 index 82b82412d..000000000 --- a/app/serializers/user_role_serializer.rb +++ /dev/null @@ -1,9 +0,0 @@ -# Doubtfire will deprecate ActiveModelSerializer in the future. -# Instead, write a serialize method on the model. - -class UserRoleSerializer < DoubtfireSerializer - attributes :id, :role_id, :user_id - - has_one :role - has_one :user -end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb deleted file mode 100644 index b972aa2d1..000000000 --- a/app/serializers/user_serializer.rb +++ /dev/null @@ -1,18 +0,0 @@ -# Doubtfire will deprecate ActiveModelSerializer in the future. -# Instead, write a serialize method on the model. - -class UserSerializer < DoubtfireSerializer - attributes :id, :student_id, :email, :name, :first_name, :last_name, :username, :nickname, :system_role, :receive_task_notifications, :receive_portfolio_notifications, :receive_feedback_notifications, :opt_in_to_research, :has_run_first_time_setup - - def system_role - object.role.name if object.role - end -end - -class ShallowUserSerializer < DoubtfireSerializer - attributes :id, :name, :email, :student_id -end - -class ShallowTutorSerializer < ActiveModel::Serializer - attributes :id, :name, :email -end diff --git a/app/serializers/webcal_serializer.rb b/app/serializers/webcal_serializer.rb deleted file mode 100644 index 9d3b9ecbf..000000000 --- a/app/serializers/webcal_serializer.rb +++ /dev/null @@ -1,18 +0,0 @@ -class WebcalSerializer < ActiveModel::Serializer - attributes :id, :guid, :include_start_dates, :reminder, :unit_exclusions - - def reminder - if object.reminder_time.nil? || object.reminder_unit.nil? - nil - else - { - time: object.reminder_time, - unit: object.reminder_unit - } - end - end - - def unit_exclusions - object.webcal_unit_exclusions.map(&:unit_id) - end -end diff --git a/config/application.rb b/config/application.rb index 4a9cb8a4a..93dfdc30a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -14,6 +14,8 @@ module Doubtfire # Doubtfire generic application configuration # class Application < Rails::Application + config.load_defaults 7.0 + # Load .env variables Dotenv::Railtie.load @@ -132,6 +134,9 @@ class Application < Rails::Application " DF_SECRET_KEY_ATTR => #{!secrets.secret_key_base.nil?}\n"\ " DF_SECRET_KEY_DEVISE => #{!secrets.secret_key_base.nil?}" end + + config.active_record.legacy_connection_handling = false + # Localization config.i18n.enforce_available_locales = true # Ensure that auth tokens do not appear in log files @@ -141,9 +146,11 @@ class Application < Rails::Application password_confirmation ) # Grape Serialization - config.paths.add 'app/api', glob: '**/*.rb' - config.autoload_paths += Dir["#{Rails.root}/app"] - config.autoload_paths += Dir[Rails.root.join("app", "models", "{*/}")] + + # config.paths.add 'app/api', glob: '**/*.rb' + # config.autoload_paths += Dir["#{Rails.root}/app"] + # config.autoload_paths += Dir[Rails.root.join("app", "models", "{*/}")] + config.eager_load_paths << Rails.root.join('app') << Rails.root.join('app', 'models', 'comments') # CORS config config.middleware.insert_before Warden::Manager, Rack::Cors do diff --git a/config/environment.rb b/config/environment.rb index 8221b35f8..bf2c7af28 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -1,5 +1,5 @@ # Load the rails application -require File.expand_path('../application', __FILE__) +require_relative "application" # Initialize the rails application Doubtfire::Application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb index 731e3b639..8f2664d00 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -57,17 +57,6 @@ # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true - # Debug mode disables concatenation and preprocessing of assets. - # This option may cause significant delays in view rendering with a large - # number of complex assets. - config.assets.debug = true - - # Suppress logger output for asset requests. - config.assets.quiet = true - - # Do not compress assets - config.assets.compress = false - # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true @@ -95,4 +84,9 @@ require_relative 'doubtfire_logger' config.logger = DoubtfireLogger.logger + Rails.logger = DoubtfireLogger.logger + + config.active_record.encryption.key_derivation_salt = ENV['DF_ENCRYPTION_KEY_DERIVATION_SALT'] || 'U9jurHMfZbMpzlbDTMe5OSAhUJYHla9Z' + config.active_record.encryption.deterministic_key = ENV['DF_ENCRYPTION_KEY_DERIVATION_SALT'] || 'zYtzYUlLFaWdvdUO5eIINRT6ZKDddcgx' + config.active_record.encryption.primary_key = ENV['DF_ENCRYPTION_KEY_DERIVATION_SALT'] || '92zoF7RJaQ01JEExOgHbP9bRWldNQUz5' end diff --git a/config/environments/doubtfire_logger.rb b/config/environments/doubtfire_logger.rb index f181c0043..84c8f9671 100644 --- a/config/environments/doubtfire_logger.rb +++ b/config/environments/doubtfire_logger.rb @@ -1,4 +1,4 @@ -class DoubtfireLogger < ActiveSupport::Logger +class DoubtfireLogger # By default, nil is provided # # Arguments match: @@ -13,6 +13,14 @@ class DoubtfireLogger < ActiveSupport::Logger @@logger = @@console_logger.extend(ActiveSupport::Logger.broadcast(@@file_logger)) + @@logger.formatter = proc do |severity, datetime, progname, msg| + "#{datetime},#{DoubtfireLogger.remote_ip},#{severity}: #{msg}\n" + end + + def self.remote_ip + Thread.current.thread_variable_get(:ip) || 'unknown' + end + # # Singleton logger returned # diff --git a/config/environments/production.rb b/config/environments/production.rb index a3f318551..dcf2f7d00 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -10,15 +10,6 @@ # Disable Rails's static asset server (Apache or nginx will already do this) config.serve_static_files = true - # Compress JavaScripts and CSS - config.assets.compress = true - - # Don't fallback to assets pipeline if a precompiled asset is missed - config.assets.compile = false - - # Generate digests for assets URLs - config.assets.digest = true - # Eager loading on models config.eager_load = true @@ -34,6 +25,7 @@ require_relative 'doubtfire_logger' config.logger = DoubtfireLogger.logger + Rails.logger = DoubtfireLogger.logger config.log_level = :info config.action_mailer.delivery_method = (ENV['DF_MAIL_DELIVERY_METHOD'] || 'smtp').to_sym @@ -50,4 +42,8 @@ } end + config.active_record.encryption.key_derivation_salt = ENV['DF_ENCRYPTION_KEY_DERIVATION_SALT'] + config.active_record.encryption.deterministic_key = ENV['DF_ENCRYPTION_DETERMINISTIC_KEY'] + config.active_record.encryption.primary_key = ENV['DF_ENCRYPTION_PRIMARY_KEY'] + end diff --git a/config/environments/staging.rb b/config/environments/staging.rb index 7aed40789..5202b4748 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -10,5 +10,6 @@ require_relative 'doubtfire_logger' config.logger = DoubtfireLogger.logger + Rails.logger = DoubtfireLogger.logger config.log_level = :info end diff --git a/config/environments/test.rb b/config/environments/test.rb index 79b7b3c58..107fd390b 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -36,7 +36,12 @@ require_relative 'doubtfire_logger' config.logger = DoubtfireLogger.logger + Rails.logger = DoubtfireLogger.logger # Logging level (:debug, :info, :warn, :error, :fatal) config.log_level = :warn + + config.active_record.encryption.key_derivation_salt = ENV['DF_ENCRYPTION_KEY_DERIVATION_SALT'] || 'U9jurHMfZbMpzlbDTMe5OSAhUJYHla9Z' + config.active_record.encryption.deterministic_key = ENV['DF_ENCRYPTION_KEY_DERIVATION_SALT'] || 'zYtzYUlLFaWdvdUO5eIINRT6ZKDddcgx' + config.active_record.encryption.primary_key = ENV['DF_ENCRYPTION_KEY_DERIVATION_SALT'] || '92zoF7RJaQ01JEExOgHbP9bRWldNQUz5' end diff --git a/config/initializers/serializers.rb b/config/initializers/serializers.rb deleted file mode 100644 index daeeace77..000000000 --- a/config/initializers/serializers.rb +++ /dev/null @@ -1,7 +0,0 @@ -ActiveSupport.on_load(:active_model_serializers) do - # Disable for all serializers (except ArraySerializer) - ActiveModel::Serializer.root = false - - # Disable for ArraySerializer - ActiveModel::Serializer::CollectionSerializer.root = false -end diff --git a/config/routes.rb b/config/routes.rb index 8fc54b2ee..582a6c860 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,11 +1,9 @@ Doubtfire::Application.routes.draw do - devise_for :users - get 'api/submission/unit/:id/portfolio', to: 'portfolio_downloads#index' get 'api/submission/unit/:id/task_definitions/:task_def_id/download_submissions', to: 'task_downloads#index' get 'api/submission/unit/:id/task_definitions/:task_def_id/student_pdfs', to: 'task_submission_pdfs#index' get 'api/units/:id/all_resources', to: 'lecture_resource_downloads#index' - mount Api::Root => '/' + mount ApiRoot => '/' mount GrapeSwaggerRails::Engine => '/api/docs' end diff --git a/db/migrate/20220110052033_switch_to_rails_encryption.rb b/db/migrate/20220110052033_switch_to_rails_encryption.rb new file mode 100644 index 000000000..db333d19a --- /dev/null +++ b/db/migrate/20220110052033_switch_to_rails_encryption.rb @@ -0,0 +1,9 @@ +class SwitchToRailsEncryption < ActiveRecord::Migration[7.0] + def change + AuthToken.destroy_all + + remove_column :auth_tokens, :encrypted_authentication_token + remove_column :auth_tokens, :encrypted_authentication_token_iv + add_column :auth_tokens, :authentication_token, :string, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 6042df8cc..dc777affe 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_12_08_231733) do +ActiveRecord::Schema.define(version: 2022_01_10_052033) do create_table "activity_types", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.string "name", null: false @@ -22,10 +22,9 @@ end create_table "auth_tokens", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| - t.string "encrypted_authentication_token", null: false - t.string "encrypted_authentication_token_iv" t.datetime "auth_token_expiry", null: false t.bigint "user_id" + t.string "authentication_token", null: false t.index ["user_id"], name: "index_auth_tokens_on_user_id" end diff --git a/test/api/comments/comment_test.rb b/test/api/comments/comment_test.rb index 2619e31fa..c5e69744e 100644 --- a/test/api/comments/comment_test.rb +++ b/test/api/comments/comment_test.rb @@ -120,7 +120,6 @@ def test_replying_to_comments 'comment' => 'Responding!', 'has_attachment' => false, 'type' => 'text', - 'is_new' => false, 'reply_to_id' => TaskComment.last.id, author: { 'id' => user.id }, recipient: { 'id' => tutor.id } @@ -133,13 +132,12 @@ def test_replying_to_comments comment_data = { comment: 'Responding!', reply_to_id: TaskComment.last.id } post_json "/api/projects/#{project.id}/task_def_id/#{task_definition.id}/comments", comment_data assert_equal 201, last_response.status - assert_json_matches_model expected_response, last_response_body, %w(comment type is_new reply_to_id) + assert_json_matches_model expected_response, last_response_body, %w(comment type reply_to_id) expected_response = { 'comment' => 'Responding again!', 'has_attachment' => false, 'type' => 'text', - 'is_new' => false, 'reply_to_id' => TaskComment.last.id, author: { 'id' => user.id }, recipient: { 'id' => tutor.id } @@ -154,7 +152,7 @@ def test_replying_to_comments assert_equal 201, last_response.status # check each is the same - assert_json_matches_model expected_response, last_response_body, %w(comment type is_new reply_to_id) + assert_json_matches_model expected_response, last_response_body, %w(comment type reply_to_id) end def test_student_post_reply_to_invalid_comment diff --git a/test/api/units/task_definitions_api_test.rb b/test/api/units/task_definitions_api_test.rb index 2f2ab2b17..83bc25241 100644 --- a/test/api/units/task_definitions_api_test.rb +++ b/test/api/units/task_definitions_api_test.rb @@ -192,7 +192,7 @@ def test_submission_creates_folders post "/api/projects/#{project.id}/task_def_id/#{td.id}/submission", data_to_post - assert_equal 201, last_response.status + assert_equal 201, last_response.status, last_response_body assert File.directory? path diff --git a/test/api/units_api_test.rb b/test/api/units_api_test.rb index bb9293a49..931d22fc4 100644 --- a/test/api/units_api_test.rb +++ b/test/api/units_api_test.rb @@ -232,11 +232,17 @@ def test_unit_output() assert_equal actual_unit['start_date'].to_date, expected_unit.start_date.to_date assert_equal actual_unit['end_date'].to_date, expected_unit.end_date.to_date - keys = ["code", "id", "name", "main_convenor_id", "description", "teaching_period_id", "active", "auto_apply_extension_before_deadline", "send_notifications", "enable_sync_enrolments", "enable_sync_timetable", "draft_task_definition_id", "allow_student_extension_requests", "extension_weeks_on_resubmit_request", "allow_student_change_tutorial"] + keys = ["code", "id", "name", "main_convenor_id", "description", "active", "auto_apply_extension_before_deadline", "send_notifications", "enable_sync_enrolments", "enable_sync_timetable", "draft_task_definition_id", "allow_student_extension_requests", "extension_weeks_on_resubmit_request", "allow_student_change_tutorial"] assert actual_unit.key?("my_role"), actual_unit.inspect assert_equal expected_unit.role_for(expected_unit.main_convenor_user).name, actual_unit["my_role"] + if expected_unit.teaching_period_id.nil? + assert_nil actual_unit["teaching_period_id"], actual_unit.inspect + else + assert_equal expected_unit.teaching_period_id, actual_unit["teaching_period_id"], actual_unit.inspect + end + assert_json_matches_model expected_unit, actual_unit, keys assert actual_unit.key?("tutorial_streams"), actual_unit.inspect diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index 6fadc85b3..989ed8df8 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -1,5 +1,4 @@ require "test_helper" -require "minitest/rails/capybara" class ApplicationSystemTestCase < ActionDispatch::SystemTestCase driven_by :selenium, using: :chrome, screen_size: [1400, 1400] diff --git a/test/helpers/json_helper.rb b/test/helpers/json_helper.rb index 555e83d74..aba4c16b6 100644 --- a/test/helpers/json_helper.rb +++ b/test/helpers/json_helper.rb @@ -47,7 +47,7 @@ def assert_json_matches_model(model, response_json, keys_data) mk = keys[k] || keys[k.to_sym] value = model.is_a?(Hash) ? (model[mk].nil? ? model[mk.to_sym] : model[mk]) : model.send(mk) if ! value.nil? - assert_equal value, response_json[k], "Values for model key #{mk} does not matach value of response key #{k} - #{response_json}" + assert_equal value, response_json[k], "Values for model key #{mk} does not match value of response key #{k} - #{response_json}" else assert_nil response_json[k], "Values for key #{k} is not nil - #{response_json}" end diff --git a/test/models/group_test.rb b/test/models/group_test.rb index 0383ce769..3a9cfad58 100644 --- a/test/models/group_test.rb +++ b/test/models/group_test.rb @@ -9,7 +9,7 @@ def test_add_group_members group1.add_member project - assert_includes(group1.projects,project) + assert_includes group1.projects, project assert_equal group1.group_memberships.count, 1 project.unit.destroy end @@ -21,9 +21,10 @@ def test_hides_inactive_members assert group1.valid? group1.add_member project + assert_includes group1.projects, project group1.remove_member project #test project removed correctly - refute_includes(group1.projects,project) + refute_includes group1.projects, project project.unit.destroy end diff --git a/test/models/project_model_test.rb b/test/models/project_model_test.rb index 4223de26c..1c7283c34 100644 --- a/test/models/project_model_test.rb +++ b/test/models/project_model_test.rb @@ -100,17 +100,4 @@ def test_tutor_for_task_def_for_match_all assert_equal project_first.main_convenor_user, tutor end - def test_knows_past_groups - project = Project.create - group1 = FactoryBot.create(:group) - group2 = FactoryBot.create(:group) - - group1.add_member project - group2.add_member project - group1.remove_member project - - assert_includes project.past_groups, group1 - refute_includes project.past_groups, group2 - end - end diff --git a/test/models/task_definition_test.rb b/test/models/task_definition_test.rb index 625d81a6b..b9019e338 100644 --- a/test/models/task_definition_test.rb +++ b/test/models/task_definition_test.rb @@ -39,7 +39,7 @@ def test_default_quality_points end def test_group_tasks - u = Unit.first + u = FactoryBot.create(:unit) activity_type = FactoryBot.create(:activity_type) u.add_tutorial_stream('Group-Tasks-Test', 'group-tasks-test', activity_type) @@ -52,7 +52,7 @@ def test_group_tasks initial_count = u.task_definitions.count - group_set = GroupSet.create!(group_params) + group_set = GroupSet.create(group_params) group_set.unit = u group_set.save! diff --git a/test/models/task_pin_test.rb b/test/models/task_pin_test.rb index 501c19a38..fd1d98a56 100644 --- a/test/models/task_pin_test.rb +++ b/test/models/task_pin_test.rb @@ -1,11 +1,8 @@ require "test_helper" class TaskPinTest < ActiveSupport::TestCase - def task_pin - @task_pin ||= TaskPin.new - end - def test_valid - assert task_pin.valid? - end + #TODO: you cannot pin a task that is not your task + #TODO: you can pin a task is your task + end diff --git a/test/models/task_status_test.rb b/test/models/task_status_test.rb index 71ff3c1a3..95cacd585 100644 --- a/test/models/task_status_test.rb +++ b/test/models/task_status_test.rb @@ -35,13 +35,15 @@ def test_status_chanaged_with_extenssion }) td.save! + # Get the first student - who now has this task + project = unit.active_projects.first + #create a time exceeded task tc = Task.create!( + project_id: project.id, task_definition_id: td.id, task_status_id: 12 ) - # Get the first student - who now has this task - project = unit.active_projects.first data_to_post = { trigger: 'ready_for_feedback' diff --git a/test/models/tutorial_model_test.rb b/test/models/tutorial_model_test.rb index 80e83e3f5..02b563d9a 100644 --- a/test/models/tutorial_model_test.rb +++ b/test/models/tutorial_model_test.rb @@ -4,17 +4,17 @@ class TutorialModelTest < ActiveSupport::TestCase def test_default_create tutorial = FactoryBot.build(:tutorial) - assert tutorial.valid? + assert tutorial.valid?, tutorial.errors tutorial_stream = FactoryBot.create(:tutorial_stream, unit: tutorial.unit) tutorial.tutorial_stream = tutorial_stream - assert tutorial.valid? + assert tutorial.valid?, tutorial.errors assert_equal tutorial.unit, tutorial_stream.unit end def test_unit_inconsistency_raises_error tutorial = FactoryBot.build(:tutorial) - assert tutorial.valid? + assert tutorial.valid?, tutorial.errors tutorial_stream = FactoryBot.create(:tutorial_stream) tutorial.tutorial_stream = tutorial_stream diff --git a/test/test_helper.rb b/test/test_helper.rb index 5868451a9..74b4a6b8a 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,7 +1,6 @@ ENV["RAILS_ENV"] ||= "test" require_relative "../config/environment" require "rails/test_help" -require "minitest/rails" # Consider setting MT_NO_EXPECTATIONS to not add expectations to Object. # ENV["MT_NO_EXPECTATIONS"] = true @@ -26,7 +25,6 @@ end # Require minitest extensions -require 'minitest/rails' require 'minitest/pride' require 'minitest/around'