app/controllers/courses_controller.rb
# frozen_string_literal: true
require 'oauth'
require_dependency "#{Rails.root}/lib/wiki_edits"
require_dependency "#{Rails.root}/lib/list_course_manager"
require_dependency "#{Rails.root}/lib/tag_manager"
require_dependency "#{Rails.root}/lib/course_creation_manager"
require_dependency "#{Rails.root}/app/workers/update_course_worker"
require_dependency "#{Rails.root}/app/workers/notify_untrained_users_worker"
require_dependency "#{Rails.root}/app/workers/announce_course_worker"
require_dependency "#{Rails.root}/lib/alerts/check_timeline_alert_manager"
#= Controller for course functionality
class CoursesController < ApplicationController
include CourseHelper
respond_to :html, :json
before_action :require_permissions, only: %i[notify_untrained
delete_all_weeks]
################
# CRUD methods #
################
def create
require_signed_in
course_creation_manager = CourseCreationManager.new(course_params, wiki_params,
params[:course][:scoping_methods],
initial_campaign_params,
instructor_role_description, current_user,
params[:course][:ta_support])
unless course_creation_manager.valid?
render json: { message: course_creation_manager.invalid_reason },
status: :not_found
return
end
@course = course_creation_manager.create
# return early if the course was not persisted to the db
return if @course.id.nil?
handle_post_course_creation_updates
end
def update
validate
handle_course_announcement(@course.instructors.first)
slug_from_params if should_set_slug?
@course.update update_params
update_courses_wikis
update_course_wiki_namespaces
update_flags
ensure_passcode_set
UpdateCourseWorker.schedule_edits(course: @course, editing_user: current_user)
render json: { course: @course }
rescue Wiki::InvalidWikiError => e
message = I18n.t('courses.error.invalid_wiki', domain: e.domain)
render json: { errors: e, message: },
status: :not_found
end
def destroy
validate
DeleteCourseWorker.schedule_deletion(course: @course, current_user:)
render json: { success: true }
end
# /courses/school/title_(term)
# /courses/school/title_(term)/subpage
def show
@course = find_course_by_slug("#{params[:school]}/#{params[:titleterm]}")
protect_privacy
verify_edit_credentials { return }
set_enrollment_details_in_session
# Only responds to HTML, so spiders fetching index.php will get a 404.
respond_to do |format|
format.html { render }
end
end
def find
course = Course.find(params[:course_id])
redirect_to "/courses/#{course.slug}"
end
def search
search_presenter = CoursesPresenter.new(
current_user:,
courses_list: Course.where(private: false)
)
@query = params[:search]
@courses = search_presenter.search_courses(@query)
end
##############################
# Course data json endpoints #
##############################
# /courses/school/title_(term)/course.json
def course
set_course
verify_edit_credentials { return }
end
def articles
set_course
set_limit
end
def users
set_course
end
def assignments
set_course
end
def campaigns
set_course
end
def categories
set_course
end
def tags
set_course
end
def timeline
set_course
end
def uploads
set_course
end
def alerts
set_course
@alerts = current_user&.admin? ? @course.alerts : @course.public_alerts
end
##########################
# User-initiated actions #
##########################
def check
course_exists = Course.exists?(slug: params[:id])
render json: { course_exists: }
end
# JSON method for listing/unlisting course
def list
@course = find_course_by_slug(params[:id])
campaign = Campaign.find_by(title: campaign_params[:title])
unless campaign
render json: {
message: "Sorry, #{campaign_params[:title]} is not a valid campaign."
}, status: :not_found
return
end
method = request.request_method.downcase
manager = ListCourseManager.new(@course, campaign)
case method
when 'post'
manager.handle_post
when 'delete'
manager.handle_delete
end
end
def tag
@course = find_course_by_slug(params[:id])
TagManager.new(@course).manage(request)
end
def manual_update
require_super_admin_permissions
@course = find_course_by_slug(params[:id])
UpdateCourseStats.new(@course, full: true)
redirect_to "/courses/#{@course.slug}"
end
def needs_update
@course = find_course_by_slug(params[:id])
@course.update(needs_update: true)
render json: { result: I18n.t('courses.creator.update_scheduled') },
status: :ok
end
def notify_untrained
@course = find_course_by_slug(params[:id])
NotifyUntrainedUsersWorker.schedule_notifications(course: @course, notifying_user: current_user)
render plain: '', status: :ok
end
helper_method :notify_untrained
def delete_all_weeks
@course = find_course_by_slug(params[:id])
@course.weeks.destroy_all
CheckTimelineAlertManager.new(@course)
render plain: '', status: :ok
end
##################
# Helper methods #
##################
private
def set_course
@course = find_course_by_slug(params[:slug])
protect_privacy
end
def campaign_params
params.require(:campaign).permit(:title)
end
def validate
slug = params[:id].gsub(/\.json$/, '')
@course = find_course_by_slug(slug)
course_cloned_status = @course.cloned_status == 3
raise NotPermittedError unless current_user&.can_edit?(@course) || course_cloned_status
end
def handle_course_announcement(instructor)
# Course announcements aren't particularly necessary, but we'll keep them on
# for Wiki Ed for now.
return unless Features.wiki_ed?
newly_submitted = !@course.submitted? && course_params[:submitted] == true
return unless newly_submitted
# Needs to be switched to submitted before the announcement edits are made
@course.update(submitted: true)
AddSubmittedTag.new(@course)
CourseSubmissionMailerWorker.schedule_email(@course, instructor)
AnnounceCourseWorker.schedule_announcement(course: @course,
editing_user: current_user,
instructor:)
end
def should_set_slug?
%i[title school].all? { |key| params[:course].key?(key) }
end
def slug_from_params(course = params[:course])
slug = +"#{course[:school]}/#{course[:title]}"
slug << "_(#{course[:term]})" if course[:term].present?
course[:slug] = slug.tr(' ', '_')
end
def ensure_passcode_set
return unless course_params[:passcode].nil?
@course.update(passcode: GeneratePasscode.call)
end
def initial_campaign_params
params
.require(:course)
.permit(:initial_campaign_id, :template_description)
end
def wiki_params
params
.require(:course)
.fetch(:home_wiki, {})
.permit(:language, :project)
end
def courses_wikis_params
params
.require(:course)
.permit(wikis: [:language, :project])
end
def update_courses_wikis
multi_wikis = courses_wikis_params[:wikis]
return if multi_wikis.nil?
new_wikis = multi_wikis.map do |wiki|
Wiki.get_or_create(language: wiki[:language], project: wiki[:project])
end
@course.update_wikis(new_wikis)
end
def course_wiki_namespaces_params
params.require(:course).permit(namespaces: [])[:namespaces]
end
def update_course_wiki_namespaces
namespaces = course_wiki_namespaces_params || []
# Each entry in namespaces uses wiki domain and namespace
# Eg.: for cookbook, its "en.wikibooks.org-namespace-102"
@course.courses_wikis.each do |course_wiki|
wiki_domain = course_wiki.wiki.domain
course_wiki_namespaces = []
namespaces.each do |ns|
ns_wiki_domain = ns.split('-', 2)[0]
next if ns_wiki_domain != wiki_domain
ns_id = ns.split('-', 3)[2].to_i
cw_ns = course_wiki.course_wiki_namespaces.find_or_create_by(namespace: ns_id)
course_wiki_namespaces << cw_ns
end
course_wiki.update_namespaces(course_wiki_namespaces)
end
end
def update_flags
update_boolean_flag :timeline_enabled
update_boolean_flag :wiki_edits_enabled
update_boolean_flag :online_volunteers_enabled
update_boolean_flag :disable_student_emails
update_boolean_flag :stay_in_sandbox
update_boolean_flag :retain_available_articles
update_edit_settings
update_academic_system
update_course_format
update_last_reviewed
end
def update_boolean_flag(flag)
case params.dig(:course, flag)
when true
@course.flags[flag] = true
@course.save
when false
@course.flags[flag] = false
@course.save
end
end
EDIT_SETTING_KEYS = %w[
wiki_course_page_enabled assignment_edits_enabled enrollment_edits_enabled
].freeze
def update_edit_settings
update_flags = {}
EDIT_SETTING_KEYS.each do |key|
update_flags[key] = params.dig(:course, key)
end
@course.flags['edit_settings'] = update_flags
@course.save
end
def update_academic_system
@course.flags['academic_system'] = params.dig(:course, 'academic_system')
@course.save
end
def update_course_format
@course.flags['format'] = params.dig(:course, 'format')
@course.save
end
def update_last_reviewed
username = params.dig(:course, 'last_reviewed', 'username')
timestamp = params.dig(:course, 'last_reviewed', 'timestamp')
if username && timestamp
@course.flags['last_reviewed'] = {
'username' => username,
'timestamp' => timestamp
}
@course.save
end
end
def handle_post_course_creation_updates
update_courses_wikis
update_course_wiki_namespaces
update_academic_system
update_course_format
end
def course_params
params
.require(:course)
.permit(:id, :title, :description, :school, :term, :slug, :subject,
:expected_students, :start, :end, :submitted, :passcode,
:timeline_start, :timeline_end, :day_exceptions, :weekdays,
:no_day_exceptions, :cloned_status, :type, :level, :private, :withdrawn)
end
def update_params
course_attributes = course_params.to_h
if params[:course].key?(:home_wiki)
home_wiki = Wiki.get_or_create language: params.dig(:course, :home_wiki, :language),
project: params.dig(:course, :home_wiki, :project)
course_attributes[:home_wiki_id] = home_wiki[:id]
end
course_attributes.delete(:passcode) if params[:course][:passcode] == '****'
course_attributes
end
def instructor_role_description
params.require(:course).permit(:role_description)[:role_description]
end
def set_limit
@limit = params[:limit]
end
# If the user could make an edit to the course, this verifies that
# their tokens are working. If their credentials are found to be invalid,
# they get logged out immediately, and this method redirects them to the home
# page, so that they don't make edits that fail upon save.
# We don't need to do this too often, though.
# rubocop:disable Metrics/CyclomaticComplexity
def verify_edit_credentials
return if Features.disable_wiki_output?
return unless @course.home_wiki.edits_enabled?
return unless current_user&.can_edit?(@course)
return if current_user.wiki_token && current_user.updated_at > 12.hours.ago
return if WikiEdits.new(@course.home_wiki).oauth_credentials_valid?(current_user)
redirect_to root_path
yield
end
# rubocop:enable Metrics/CyclomaticComplexity
def protect_privacy
return unless @course.private
# Admins and enrolled users have non-visitor roles
return if current_user && current_user.role(@course) != CoursesUsers::Roles::VISITOR_ROLE
raise ActionController::RoutingError, 'not found'
end
# If this is an enroll link, save the slug and enroll code
# in the session so that it can be used upon successful
# oauth login.
# The session data will be used in
# OmniauthCallbacksController.
def set_enrollment_details_in_session
return unless params.key? 'enroll'
session['course_slug'] = @course.slug
session['enroll_code'] = params['enroll'] || ''
end
end