diff --git a/app/assets/javascripts/components/overview/available_actions.jsx b/app/assets/javascripts/components/overview/available_actions.jsx index 0466dc0d32..0e0b899d54 100644 --- a/app/assets/javascripts/components/overview/available_actions.jsx +++ b/app/assets/javascripts/components/overview/available_actions.jsx @@ -72,7 +72,8 @@ const AvailableActions = createReactClass({ } const enteredTitle = prompt(I18n.t('courses.confirm_course_deletion', { title: this.props.course.title })); - if (enteredTitle.trim() === this.props.course.title.trim()) { + // Check if enteredTitle is not null before calling trim. + if (enteredTitle !== null && enteredTitle.trim() === this.props.course.title.trim()) { return this.props.deleteCourse(this.props.course.slug); } else if (enteredTitle) { return alert(I18n.t('courses.confirm_course_deletion_failed', { title: enteredTitle })); diff --git a/app/assets/javascripts/components/overview/my_articles/components/Categories/List/Assignment/Header/Actions/MarkAsIncompleteButton.jsx b/app/assets/javascripts/components/overview/my_articles/components/Categories/List/Assignment/Header/Actions/MarkAsIncompleteButton.jsx index 2c8a3b024a..3a7a4ac818 100644 --- a/app/assets/javascripts/components/overview/my_articles/components/Categories/List/Assignment/Header/Actions/MarkAsIncompleteButton.jsx +++ b/app/assets/javascripts/components/overview/my_articles/components/Categories/List/Assignment/Header/Actions/MarkAsIncompleteButton.jsx @@ -19,7 +19,7 @@ export const MarkAsIncompleteButton = (props) => { className="button danger small" onClick={update({ ...props, dispatch })} > - Mark as Incomplete + {I18n.t('articles.mark_as_complete')} ); diff --git a/app/assets/javascripts/utils/course.js b/app/assets/javascripts/utils/course.js index 73c030fd52..d95b91b9f8 100644 --- a/app/assets/javascripts/utils/course.js +++ b/app/assets/javascripts/utils/course.js @@ -78,9 +78,9 @@ document.onreadystatechange = () => { } // for use on campaign/programs page - const x = document.querySelectorAll('.remove-course'); - for (let i = 0; i < x.length; i += 1) { - x[i]?.addEventListener('click', (e) => { + const removeCourseBtn = document.querySelectorAll('.remove-course'); + for (let i = 0; i < removeCourseBtn.length; i += 1) { + removeCourseBtn[i]?.addEventListener('click', (e) => { const confirmed = window.confirm(I18n.t('campaign.confirm_course_removal', { title: e.target.dataset.title, campaign_title: e.target.dataset.campaignTitle @@ -91,6 +91,19 @@ document.onreadystatechange = () => { }); } + const deleteCourseBtn = document.getElementsByClassName('delete-course-from-campaign')[0]; + if (deleteCourseBtn) { + deleteCourseBtn.addEventListener('click', (e) => { + const enteredTitle = window.prompt(I18n.t('courses.confirm_course_deletion', { title: e.target.dataset.title })); + if (!enteredTitle) { + e.preventDefault(); + } else if (enteredTitle.trim() !== e.target.dataset.title.trim()) { + e.preventDefault(); + alert(I18n.t('courses.confirm_course_deletion_failed', { title: enteredTitle })); + } + }); +} + return document.querySelectorAll('select.sorts').forEach(item => item?.addEventListener('change', function () { const list = (() => { switch (this.getAttribute('rel')) { diff --git a/app/controllers/courses/delete_from_campaign_controller.rb b/app/controllers/courses/delete_from_campaign_controller.rb new file mode 100644 index 0000000000..c283e36437 --- /dev/null +++ b/app/controllers/courses/delete_from_campaign_controller.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class Courses::DeleteFromCampaignController < CoursesController + include CourseHelper + + def delete_course_from_campaign + validate + if @course.campaigns.size > 1 + remove_course_from_campaign_but_not_deleted + else + remove_and_delete_course_from_campaign + end + end + + def validate + slug = params[:slug].gsub(/\.json$/, '') + @course = find_course_by_slug(slug) + raise NotPermittedError unless current_user&.can_edit?(@course) + end + + def remove_and_delete_course_from_campaign + campaigns_course = find_campaigns_course + result = campaigns_course.destroy + message = result ? 'campaign.course_removed_and_deleted' : 'campaign.course_already_removed' + flash[:notice] = t(message, title: @course.title, campaign_title: params[:campaign_title]) + DeleteCourseWorker.schedule_deletion(course: @course, current_user:) + redirect_to_campaign_path + end + + def remove_course_from_campaign_but_not_deleted + campaigns_course = find_campaigns_course + result = campaigns_course.destroy + message = result ? 'campaign.course_removed_but_not_deleted' : 'campaign.course_already_removed' + flash[:notice] = t(message, title: @course.title, campaign_title: params[:campaign_title]) + redirect_to_campaign_path + end + + def find_campaigns_course + CampaignsCourses.find_by(course_id: @course.id, campaign_id: params[:campaign_id]) + end + + def redirect_to_campaign_path + redirect_to programs_campaign_path(params[:campaign_slug]) + end +end diff --git a/app/presenters/courses_presenter.rb b/app/presenters/courses_presenter.rb index 9f4a23f14b..519b5e0dda 100644 --- a/app/presenters/courses_presenter.rb +++ b/app/presenters/courses_presenter.rb @@ -59,6 +59,8 @@ def can_remove_course? @can_remove ||= current_user&.admin? || campaign_organizer? end + alias can_delete_course? can_remove_course? + def campaign_organizer? return false unless campaign return @campaign_organizer if @campaign_organizer_set diff --git a/app/views/campaigns/_course_row.html.haml b/app/views/campaigns/_course_row.html.haml index 11301403c6..7d1bbfecdc 100644 --- a/app/views/campaigns/_course_row.html.haml +++ b/app/views/campaigns/_course_row.html.haml @@ -7,7 +7,7 @@ %a.course-link{:href => "#{course_slug_path(course.slug)}"} = course.school + "/" + course.term - if Features.wiki_ed? - %td{:class => "table-link-cell"} + %td{:class => "table-link-cell"} %a.course-link{:href => "#{course_slug_path(course.slug)}"} %span.first_instructor = course.courses_users.where(role: 1).first&.real_name @@ -57,5 +57,11 @@ %a.course-link{:href => "#{course_slug_path(course.slug)}"} = form_for(@campaign, url: remove_course_campaign_path(@campaign.slug, course_id: course.id), method: :put, id: "remove_course-#{course.id}", html: { class: 'remove-program-form' }) do = hidden_field_tag('course_title', course.title, id: "course_title-#{course.id}") - %button.button.danger.remove-course{'data-id' => course.id, 'data-title' => course.title, 'data-campaign-title' => @campaign.title} + %button.button.danger.remove-course{ 'data-id' => course.id, 'data-title' => course.title, 'data-campaign-title' => @campaign.title, title: t('campaign.remove_course_tooltip') } = t('assignments.remove') + - if @presenter&.can_delete_course? + %a.course-link{:href => "#{course_slug_path(course.slug)}", style: "padding-top: 0;"} + = form_for(@campaign, url: "/courses/#{course.slug}.json/delete_from_campaign?campaign_title=#{@campaign.title}&campaign_id=#{@campaign.id}&campaign_slug=#{@campaign.slug}", method: :delete, id: "delete_course-#{course.id}", html: { class: 'delete-program-form' }) do + = hidden_field_tag('course_title', course.title, id: "course_title-#{course.id}") + %button.button.danger.delete-course-from-campaign{'data-id' => course.id, 'data-title' => course.title, 'data-campaign-title' => @campaign.title, title: t('campaign.delete_course_tooltip') } + = t('assignments.delete') \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index 7107132695..91d95ef0f9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -188,6 +188,7 @@ en: history: (history) label: Articles loading: Loading articles... + mark_as_complete: Mark as Complete new: (new) none: This course has not edited any articles. preview: Brief preview of Article @@ -293,6 +294,7 @@ en: confirm_add_available: Are you sure you want to add %{title}? confirm_addition: Are you sure you want to assign %{title} to %{username}? confirm_deletion: Are you sure you want to delete this assignment? + delete: Remove and Delete editors: Assigned to group_members: Group members label: Assign @@ -364,6 +366,8 @@ en: course_already_removed: '"%{title}" was already removed from the "%{campaign_title}" campaign' course: Course course_removed: '"%{title}" has been removed from the "%{campaign_title}" campaign' + course_removed_and_deleted: '"%{title}" has been deleted and removed from the "%{campaign_title}" campaign' + course_removed_but_not_deleted: '"%{title}" has been removed from the "%{campaign_title}" but has not been deleted as it is present in other campaigns as well' create_campaign: Create a New Campaign create_my_campaign: Create my Campaign! customize_passcode: a default passcode @@ -387,6 +391,7 @@ en: default_passcode_explanation: "By default, new programs in this campaign should have:" description: Description delete_campaign: Delete this campaign + delete_course_tooltip: Delete and remove from the campaign. disable_account_requests: Disable account requests enable_account_requests: Enable account requests newest_campaigns: Newest Campaigns @@ -401,6 +406,7 @@ en: When an organizer creates a program in this campaign, the text in this area will be copied over as a description on the program page random_passcode: a random passcode + remove_course_tooltip: Remove from campaign. requested_accounts: Requested accounts title: Title too_many_articles: The edited article list for this campaign is too long to be displayed. Please view individual courses/programs/events to see the articles edited. @@ -487,7 +493,7 @@ en: campaign_courses: "%{title} Courses" campaign_select: "Select a campaign" campaign_users: "%{title} Editors" - confirm_course_deletion: Are you sure you want to delete the course titled %{title}? If so, type the title of the course to proceed. + confirm_course_deletion: 'Are you sure you want to delete the course titled %{title}? If so, type the title of the course to proceed.' confirm_course_deletion_failed: '"%{title}" is not the title of this course. The course has not been deleted.' confirm_manual_update: Are you sure you want to run a manual update? The most recent revisions will be imported and then the page will reload. This process may take some time. controlled_by_event_center: Enrollment in this event is controlled by Wikimedia Event Center. It cannot be joined from the Dashboard. diff --git a/config/routes.rb b/config/routes.rb index d3c3d15447..41cb03fcf5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -168,7 +168,10 @@ post '/courses/:slug/students/add_to_watchlist', to: 'courses/watchlist#add_to_watchlist', as: 'add_to_watchlist', constraints: { slug: /.*/ } - + delete 'courses/:slug/delete_from_campaign' => 'courses/delete_from_campaign#delete_course_from_campaign', as: 'delete_from_campaign', + constraints: { + slug: /.*/ + } get 'embed/course_stats/:school/:titleterm(/:_subpage(/:_subsubpage))' => 'embed#course_stats', constraints: { school: /[^\/]*/,