From df6c6f8d9240f7b1de8ab76f22dc12f5dc6b9580 Mon Sep 17 00:00:00 2001 From: cyrillefr Date: Thu, 2 May 2024 22:10:54 +0200 Subject: [PATCH 01/66] Improve BlockedUserAlert emails - a new mailer + new template - a new method in BlockUserAlert to mail to a set of concerned people which replace the current one in blocked_user_monitor.rb - the corresponding spec - new line in preview mailer --- app/mailers/blocked_user_alert_mailer.rb | 28 +++++++++++++ app/models/alert_types/blocked_user_alert.rb | 6 +++ .../blocked_user_alert_mailer/email.html.haml | 35 +++++++++++++++++ lib/alerts/blocked_user_monitor.rb | 5 +-- spec/lib/alerts/blocked_user_monitor_spec.rb | 39 +++++++++++++++---- spec/mailers/previews/alert_mailer_preview.rb | 17 ++++++++ 6 files changed, 119 insertions(+), 11 deletions(-) create mode 100644 app/mailers/blocked_user_alert_mailer.rb create mode 100644 app/views/blocked_user_alert_mailer/email.html.haml diff --git a/app/mailers/blocked_user_alert_mailer.rb b/app/mailers/blocked_user_alert_mailer.rb new file mode 100644 index 0000000000..9cfa4f6f06 --- /dev/null +++ b/app/mailers/blocked_user_alert_mailer.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class BlockedUserAlertMailer < ApplicationMailer + def self.send_mails_to_concerned(alert) + return unless Features.email? + email(alert).deliver_now + end + + def email(alert) + @alert = alert + set_recipients + return if @recipients.empty? + params = { to: @recipients, + subject: @alert.main_subject } + params[:reply_to] = @alert.reply_to unless @alert.reply_to.nil? + mail(params) + end + + private + + def set_recipients + @course = @alert.course + @user = @alert.user + @recipients = (@course.instructors.pluck(:email) + + @course.nonstudents.where(greeter: true).pluck(:email)) << + @user.email + end +end diff --git a/app/models/alert_types/blocked_user_alert.rb b/app/models/alert_types/blocked_user_alert.rb index b69e63d702..45ecd5e6c2 100644 --- a/app/models/alert_types/blocked_user_alert.rb +++ b/app/models/alert_types/blocked_user_alert.rb @@ -29,4 +29,10 @@ def main_subject def url "https://en.wikipedia.org/wiki/Special:Log?type=block&user=&page=User%3A#{user.url_encoded_username}&wpdate=&tagfilter=&subtype=" end + + def send_mails_to_concerned + BlockedUserAlertMailer.send_mails_to_concerned(self) + return if emails_disabled? + update(email_sent_at: Time.zone.now) + end end diff --git a/app/views/blocked_user_alert_mailer/email.html.haml b/app/views/blocked_user_alert_mailer/email.html.haml new file mode 100644 index 0000000000..b6353f5a8c --- /dev/null +++ b/app/views/blocked_user_alert_mailer/email.html.haml @@ -0,0 +1,35 @@ +%link{rel: 'stylesheet', href:'/mailer.css'} +%p.paragraph + Please investigate: + %a.link{ href: @alert.url }= @alert.main_subject + +- if @alert.user_contributions_url + %p.paragraph + %a.link{href: @alert.user_contributions_url}= "#{@alert.user.username} contributions" + +- if @alert.message + %p.paragraph.preserve-whitespace + = "Message regarding #{@alert.user.username}:" + = @alert.message + + %p.paragraph + User email: + = @alert.user.email + + %p.paragraph + User real name: + = @alert.user.real_name + +- if @alert.article + %p.paragraph + Article: + %a.link{href: @alert.article.url}= @alert.article.title + +- if @alert.revision + %p.paragraph + %a.link{href: @alert.revision.url} diff + +- if @alert.details + %p.paragraph.preserve-whitespace + Alert details: + = @alert.details diff --git a/lib/alerts/blocked_user_monitor.rb b/lib/alerts/blocked_user_monitor.rb index a0a7f34ea3..ba0d417e4b 100644 --- a/lib/alerts/blocked_user_monitor.rb +++ b/lib/alerts/blocked_user_monitor.rb @@ -68,8 +68,7 @@ def alert_exists?(block, user) end def create_alert_and_send_email(block, user) - BlockedUserAlert - .create(user:, details: block, course: user.courses.last) - .email_content_expert + alert = BlockedUserAlert.create(user:, details: block, course: user.courses.last) + alert.send_mails_to_concerned end end diff --git a/spec/lib/alerts/blocked_user_monitor_spec.rb b/spec/lib/alerts/blocked_user_monitor_spec.rb index b779fcd38d..c157fd35f1 100644 --- a/spec/lib/alerts/blocked_user_monitor_spec.rb +++ b/spec/lib/alerts/blocked_user_monitor_spec.rb @@ -5,25 +5,48 @@ describe BlockedUserMonitor do describe '.create_alerts_for_recently_blocked_users' do - let(:user) { create(:user, username: 'Verdantpowerinc') } - let(:course) { create(:course) } + let(:user) { create(:user, username: 'Verdantpowerinc', email: 'student@kiwi.com') } + let(:instructor_1) { create(:user, username: 'Instructor1', email: 'nospan@nospam.com') } + let(:instructor_2) { create(:user, username: 'Instructor2', email: 'instructor@course.com') } + let(:staff) { create(:user, username: 'staff', email: 'staff@kiwi.com', greeter: true) } + let(:course) do + now = Time.zone.now + create(:course, start: now.days_ago(7), end: now.days_since(7)) + end before do create(:courses_user, user:, course:) + create(:courses_user, course:, user: instructor_1, + role: CoursesUsers::Roles::INSTRUCTOR_ROLE) + create(:courses_user, course:, user: instructor_2, + role: CoursesUsers::Roles::INSTRUCTOR_ROLE) + create(:courses_user, course:, user: staff, + role: CoursesUsers::Roles::WIKI_ED_STAFF_ROLE) stub_block_log_query end it 'creates an Alert record for a blocked user' do - expect(Alert.count).to eq(0) - described_class.create_alerts_for_recently_blocked_users - expect(Alert.count).to eq(1) + expect { described_class.create_alerts_for_recently_blocked_users } + .to change(BlockedUserAlert, :count).by(1) end it 'does not create multiple alerts for the same block' do - expect(Alert.count).to eq(0) - described_class.create_alerts_for_recently_blocked_users + expect do + 2.times { described_class.create_alerts_for_recently_blocked_users } + end.to change(BlockedUserAlert, :count).by(1) + end + + it 'uses the proper mailer' do + expect(BlockedUserAlertMailer).to receive(:send_mails_to_concerned) described_class.create_alerts_for_recently_blocked_users - expect(Alert.count).to eq(1) + end + + it 'sends a mail to staff, instructors & student' do + expect do + described_class.create_alerts_for_recently_blocked_users + end.to change { BlockedUserAlertMailer.deliveries.count }.by(1) + msg = BlockedUserAlertMailer.deliveries.first + expect(msg.to).to match_array([instructor_1, instructor_2, user, staff].map(&:email)) end end end diff --git a/spec/mailers/previews/alert_mailer_preview.rb b/spec/mailers/previews/alert_mailer_preview.rb index 8e947f1f80..df176d1058 100644 --- a/spec/mailers/previews/alert_mailer_preview.rb +++ b/spec/mailers/previews/alert_mailer_preview.rb @@ -12,6 +12,10 @@ def blocked_edits_alert AlertMailer.alert(example_blocked_edits_alert, example_user) end + def blocked_student_alert + BlockedUserAlertMailer.email(example_user_blocked_alert) + end + def check_timeline_alert AlertMailer.alert(example_alert(type: 'CheckTimelineAlert'), example_user) end @@ -136,4 +140,17 @@ def example_blocked_edits_alert user: example_user, details:) end + + def example_user_blocked_alert + user = example_user + message = "Student #{user.username} have been blocked on Wikipedia. +This mail to inform staff, student as well as instructors." + alert = Alert.new(type: 'BlockedUserAlert', user:, + course: example_course, message:) + alert.tap do |alrt| + alrt.define_singleton_method(:main_subject) do + "User #{user.username} have been blocked on Wikipedia" + end + end + end end From 14204232bb1b1986a9f7d7b4107e5fc925f51fe5 Mon Sep 17 00:00:00 2001 From: Wulan Seruniati Salim Date: Thu, 24 Oct 2024 22:39:45 +0700 Subject: [PATCH 02/66] Extend error logging for course data updates Refactor error handling in ArticleStatusManager to use ApiErrorHandling consistently --- app/services/update_course_stats.rb | 2 +- lib/article_status_manager.rb | 18 ++++++++++++++---- spec/lib/article_status_manager_spec.rb | 16 ++++++++++++++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/app/services/update_course_stats.rb b/app/services/update_course_stats.rb index 6d0772ed3d..92aa89a9c9 100644 --- a/app/services/update_course_stats.rb +++ b/app/services/update_course_stats.rb @@ -61,7 +61,7 @@ def update_categories end def update_article_status - ArticleStatusManager.update_article_status_for_course(@course) + ArticleStatusManager.update_article_status_for_course(@course, update_service: self) log_update_progress :article_status_updated end diff --git a/lib/article_status_manager.rb b/lib/article_status_manager.rb index c6ffc0ca01..4c879072f0 100644 --- a/lib/article_status_manager.rb +++ b/lib/article_status_manager.rb @@ -2,20 +2,26 @@ require_dependency "#{Rails.root}/lib/modified_revisions_manager" require_dependency "#{Rails.root}/lib/assignment_updater" +require_dependency "#{Rails.root}/lib/errors/api_error_handling" #= Updates articles to reflect deletion and page moves on Wikipedia class ArticleStatusManager - def initialize(wiki = nil) + include ApiErrorHandling + + def initialize(wiki = nil, update_service: nil) @wiki = wiki || Wiki.default_wiki + @update_service = update_service end ################ # Entry points # ################ - def self.update_article_status_for_course(course) + def self.update_article_status_for_course(course, update_service: nil) course.wikis.each do |wiki| # Updating only those articles which are updated more than 1 day ago + manager = new(wiki, update_service:) + course.pages_edited .where(wiki_id: wiki.id) .where('articles.updated_at < ?', 1.day.ago) @@ -23,11 +29,14 @@ def self.update_article_status_for_course(course) # Using in_batches so that the update_at of all articles in the batch can be # excuted in a single query, otherwise if we use find_in_batches, query for # each article for updating the same would be required - new(wiki).update_status(article_batch) + manager.update_status(article_batch) # rubocop:disable Rails/SkipsModelValidations article_batch.touch_all(:updated_at) # rubocop:enable Rails/SkipsModelValidations end + rescue StandardError => e + manager.log_error(e, update_service:, + sentry_extra: { course_id: course.id, wiki_id: wiki.id }) end end @@ -83,7 +92,8 @@ def identify_deleted_and_synced_page_ids(articles) def article_data_from_replica(articles) @failed_request_count = 0 synced_articles = Utils.chunk_requests(articles, 100) do |block| - request_results = Replica.new(@wiki).get_existing_articles_by_id block + request_results = Replica.new(@wiki, update_service: @update_service) + .get_existing_articles_by_id(block) @failed_request_count += 1 if request_results.nil? request_results end diff --git a/spec/lib/article_status_manager_spec.rb b/spec/lib/article_status_manager_spec.rb index aa638fcab7..eac6091d28 100644 --- a/spec/lib/article_status_manager_spec.rb +++ b/spec/lib/article_status_manager_spec.rb @@ -13,8 +13,24 @@ let(:course) { create(:course, start: 1.year.ago, end: 1.year.from_now) } let(:user) { create(:user) } let!(:courses_user) { create(:courses_user, course:, user:) } + let(:update_service) { instance_double('UpdateService') } describe '.update_article_status_for_course' do + it 'logs unexpected errors with the update_service passed' do + # Simulate an unexpected error + allow_any_instance_of(Course).to receive(:pages_edited) + .and_raise(StandardError, 'Unexpected error occurred') + + expect_any_instance_of(described_class).to receive(:log_error).with( + instance_of(StandardError), + update_service:, + sentry_extra: { course_id: course.id, wiki_id: anything } + ).once + + # Call the method to trigger the error + described_class.update_article_status_for_course(course, update_service:) + end + it 'marks deleted articles as "deleted"' do VCR.use_cassette 'article_status_manager/main' do create(:article, From f1667b64401caf96adaa57d352c2ede3291dc034 Mon Sep 17 00:00:00 2001 From: Abishek Das Date: Sun, 8 Dec 2024 11:26:15 +0530 Subject: [PATCH 03/66] Add navigation utility hook for opening student details view --- .../javascripts/hooks/useNavigationUtils.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 app/assets/javascripts/hooks/useNavigationUtils.js diff --git a/app/assets/javascripts/hooks/useNavigationUtils.js b/app/assets/javascripts/hooks/useNavigationUtils.js new file mode 100644 index 0000000000..a053c64dca --- /dev/null +++ b/app/assets/javascripts/hooks/useNavigationUtils.js @@ -0,0 +1,16 @@ +import { useNavigate } from 'react-router-dom'; + +const useNavigationsUtils = () => { + const navigate = useNavigate(); + + const openStudentDetailsView = (courseSlug, studentUsername) => { + const url = `/courses/${courseSlug}/students/articles/${encodeURIComponent(studentUsername)}`; + navigate(url); + }; + + return { + openStudentDetailsView, + }; +}; + +export default useNavigationsUtils; From ef80c36b8ea190038ac231fd40d4cbbffd0a1df7 Mon Sep 17 00:00:00 2001 From: Abishek Das Date: Sun, 8 Dec 2024 11:30:27 +0530 Subject: [PATCH 04/66] Use navigation utility hook in multiple components for opening student details view --- .../AssignmentLinks/AssignmentLinks.jsx | 4 ++-- .../common/AssignmentLinks/EditorLink.jsx | 4 ++-- .../AssignmentLinks/GroupMembersLink.jsx | 5 ++--- .../my_articles/common/AssignedToLink.jsx | 7 +++++-- .../components/revisions/revision.jsx | 12 +++++------ .../shared/StudentList/Student/Student.jsx | 20 +++++++++---------- .../shared/StudentList/StudentRow.jsx | 3 +++ 7 files changed, 29 insertions(+), 26 deletions(-) diff --git a/app/assets/javascripts/components/common/AssignmentLinks/AssignmentLinks.jsx b/app/assets/javascripts/components/common/AssignmentLinks/AssignmentLinks.jsx index 954dd0bb50..c74ecc526c 100644 --- a/app/assets/javascripts/components/common/AssignmentLinks/AssignmentLinks.jsx +++ b/app/assets/javascripts/components/common/AssignmentLinks/AssignmentLinks.jsx @@ -59,9 +59,9 @@ const AssignmentLinks = ({ assignment, courseType, user, course, project, editMo let groupMembers; if (editors) { if (role === ASSIGNED_ROLE) { - groupMembers = ; + groupMembers = ; } else { - groupMembers = ; + groupMembers = ; } } diff --git a/app/assets/javascripts/components/common/AssignmentLinks/EditorLink.jsx b/app/assets/javascripts/components/common/AssignmentLinks/EditorLink.jsx index b4f86e13af..fa9f128157 100644 --- a/app/assets/javascripts/components/common/AssignmentLinks/EditorLink.jsx +++ b/app/assets/javascripts/components/common/AssignmentLinks/EditorLink.jsx @@ -3,8 +3,8 @@ import PropTypes from 'prop-types'; import AssignedToLink from '@components/overview/my_articles/common/AssignedToLink.jsx'; -export const EditorLink = ({ editors }) => { - return ; +export const EditorLink = ({ editors, course }) => { + return ; }; EditorLink.propTypes = { diff --git a/app/assets/javascripts/components/common/AssignmentLinks/GroupMembersLink.jsx b/app/assets/javascripts/components/common/AssignmentLinks/GroupMembersLink.jsx index cabbeda1c9..c44324a01a 100644 --- a/app/assets/javascripts/components/common/AssignmentLinks/GroupMembersLink.jsx +++ b/app/assets/javascripts/components/common/AssignmentLinks/GroupMembersLink.jsx @@ -1,10 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; - import AssignedToLink from '@components/overview/my_articles/common/AssignedToLink.jsx'; -export const GroupMembersLink = ({ members }) => { - return ; +export const GroupMembersLink = ({ members, course }) => { + return ; }; GroupMembersLink.propTypes = { diff --git a/app/assets/javascripts/components/overview/my_articles/common/AssignedToLink.jsx b/app/assets/javascripts/components/overview/my_articles/common/AssignedToLink.jsx index cbecaad48a..3b3544071e 100644 --- a/app/assets/javascripts/components/overview/my_articles/common/AssignedToLink.jsx +++ b/app/assets/javascripts/components/overview/my_articles/common/AssignedToLink.jsx @@ -1,15 +1,18 @@ import React from 'react'; import PropTypes from 'prop-types'; +import useNavigationsUtils from '../../../../hooks/useNavigationUtils'; -export const AssignedToLink = ({ name, members }) => { +export const AssignedToLink = ({ name, members, course }) => { if (!members) return null; const label = {I18n.t(`assignments.${name}`)}: ; const list = [...members].sort((a, b) => a > b); + const { openStudentDetailsView } = useNavigationsUtils(); + const links = list.map((username, index, collection) => { return ( - + openStudentDetailsView(course.slug, username)} style={{ cursor: 'pointer' }}> {username} {index < collection.length - 1 ? ', ' : null} diff --git a/app/assets/javascripts/components/revisions/revision.jsx b/app/assets/javascripts/components/revisions/revision.jsx index aefc724a80..cde4a12dd0 100644 --- a/app/assets/javascripts/components/revisions/revision.jsx +++ b/app/assets/javascripts/components/revisions/revision.jsx @@ -5,19 +5,17 @@ import CourseUtils from '../../utils/course_utils.js'; import { formatDateWithTime } from '../../utils/date_utils.js'; import { trunc } from '../../utils/strings.js'; import withRouter from '@components/util/withRouter.jsx'; +import useNavigationsUtils from '../../hooks/useNavigationUtils.js'; -const Revision = ({ revision, index, wikidataLabel, course, setSelectedIndex, lastIndex, selectedIndex, student, router }) => { +const Revision = ({ revision, index, wikidataLabel, course, setSelectedIndex, lastIndex, selectedIndex, student }) => { const ratingClass = `rating ${revision.rating}`; const ratingMobileClass = `${ratingClass} tablet-only`; const formattedTitle = CourseUtils.formattedArticleTitle({ title: revision.title, project: revision.wiki.project, language: revision.wiki.language }, course.home_wiki, wikidataLabel); const subtitle = wikidataLabel ? `(${CourseUtils.removeNamespace(revision.title)})` : ''; const isWikipedia = revision.wiki.project === 'wikipedia'; const showRealName = student.real_name; + const { openStudentDetailsView } = useNavigationsUtils(); - const openStudentDetails = () => { - const url = `/courses/${course.slug}/students/articles/${encodeURIComponent(student.username)}`; - router.navigate(url); - }; return ( @@ -38,7 +36,7 @@ const Revision = ({ revision, index, wikidataLabel, course, setSelectedIndex, la {trunc(student.real_name)}  ( openStudentDetailsView(course.slug, student.username)} style={{ cursor: 'pointer' }} > @@ -49,7 +47,7 @@ const Revision = ({ revision, index, wikidataLabel, course, setSelectedIndex, la ) : ( openStudentDetailsView(course.slug, student.username)} style={{ cursor: 'pointer' }} > diff --git a/app/assets/javascripts/components/students/shared/StudentList/Student/Student.jsx b/app/assets/javascripts/components/students/shared/StudentList/Student/Student.jsx index 854fb6fda4..30f01e96fa 100644 --- a/app/assets/javascripts/components/students/shared/StudentList/Student/Student.jsx +++ b/app/assets/javascripts/components/students/shared/StudentList/Student/Student.jsx @@ -34,6 +34,7 @@ const Student = createReactClass({ fetchTrainingStatus: PropTypes.func.isRequired, minimalView: PropTypes.bool, student: PropTypes.object.isRequired, + openStudentDetailsView: PropTypes.func.isRequired }, setUploadFilters(selectedFilters) { @@ -44,10 +45,9 @@ const Student = createReactClass({ return e.stopPropagation(); }, - openStudentDetails() { - const { course, router, student } = this.props; - const url = `/courses/${course.slug}/students/articles/${encodeURIComponent(student.username)}`; - return router.navigate(url); + openStudentDetailsView() { + const { course, student, openStudentDetailsView } = this.props; + openStudentDetailsView(course.slug, student.username); }, _shouldShowRealName() { @@ -64,7 +64,7 @@ const Student = createReactClass({ let recentRevisions; if (showRecent) { recentRevisions = ( - + {student.recent_revisions} ); @@ -111,7 +111,7 @@ const Student = createReactClass({ return ( - +
@@ -123,17 +123,17 @@ const Student = createReactClass({ - + {assignButton} - + {reviewButton} {recentRevisions} - + - + {student.references_count} diff --git a/app/assets/javascripts/components/students/shared/StudentList/StudentRow.jsx b/app/assets/javascripts/components/students/shared/StudentList/StudentRow.jsx index a1adb0ecd6..1b3af6137c 100644 --- a/app/assets/javascripts/components/students/shared/StudentList/StudentRow.jsx +++ b/app/assets/javascripts/components/students/shared/StudentList/StudentRow.jsx @@ -2,10 +2,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import Student from './Student/Student.jsx'; +import useNavigationsUtils from '../../../../hooks/useNavigationUtils.js'; export const StudentRow = ({ assignments, course, current_user, editAssignments, showRecent, student, wikidataLabels }) => { + const { openStudentDetailsView } = useNavigationsUtils(); return ( ); }; From 9a07fca8f18d04f0c701c583c485f611f1e566d1 Mon Sep 17 00:00:00 2001 From: Abishek Das Date: Sun, 8 Dec 2024 17:46:21 +0530 Subject: [PATCH 05/66] Fix campaigns list rendering issue to consistently appear below Recent News --- app/views/dashboard/index.html.haml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/views/dashboard/index.html.haml b/app/views/dashboard/index.html.haml index d1eae126e3..fbf2d66162 100644 --- a/app/views/dashboard/index.html.haml +++ b/app/views/dashboard/index.html.haml @@ -1,11 +1,5 @@ - content_for :after_title, " - #{t("application.my_dashboard")}" --# this is the react root when the route is /course_creator -- if request.path.include?('/course_creator') - #react_root{data: {default_course_type: @pres.default_course_type, - course_string_prefix: Features.default_course_string_prefix, - course_creation_notice: Deadlines.course_creation_notice, - use_start_and_end_times: @pres.default_use_start_and_end_times ? 'true' : 'false'}} .container.dashboard %header %h1= @pres.heading_message @@ -88,4 +82,11 @@ - if @pres.campaign_organizer? && !request.path.include?('/course_creator') -# this is the react root when at the root page - #react_root \ No newline at end of file + #react_root + +-# this is the react root when the route is /course_creator +- if request.path.include?('/course_creator') + #react_root{data: {default_course_type: @pres.default_course_type, + course_string_prefix: Features.default_course_string_prefix, + course_creation_notice: Deadlines.course_creation_notice, + use_start_and_end_times: @pres.default_use_start_and_end_times ? 'true' : 'false'}} \ No newline at end of file From 8e507b74d86c87e94317cacae377bc55fdc5854e Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 9 Dec 2024 13:18:44 +0100 Subject: [PATCH 06/66] Localisation updates from https://translatewiki.net. --- config/locales/scn.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/locales/scn.yml b/config/locales/scn.yml index a33e6ef751..79faafec07 100644 --- a/config/locales/scn.yml +++ b/config/locales/scn.yml @@ -3,6 +3,7 @@ # Export driver: phpyaml # Author: Ajeje Brazorf # Author: GianAntonucci +# Author: Gmelfi --- scn: number: @@ -25,7 +26,7 @@ scn: greeting_extended: Bommegna supra Wikipedia greeting: Ciau! help: Aiutu - home: Pàggina mastra + home: Pàggina principali log_in_extended: Trasi supra Wikipedia log_in: Trasi log_out: Nesci @@ -135,7 +136,7 @@ scn: new_account_email_placeholder: nomu@example.org none: Nuḍḍu ores_plot: ORES - overview: Pàggina mastra + overview: Pàggina principali please_log_in: Pi favuri trasi. private: Privatu requested_accounts_alert_view: Talìa From 24b9abbddf048349c60e25e4210d7bf2a511144b Mon Sep 17 00:00:00 2001 From: Abishek Das Date: Wed, 11 Dec 2024 16:20:27 +0530 Subject: [PATCH 07/66] chore: Remove unused ReviewerLink component and related code - Deleted the unused ReviewerLink component. - Removed the commented-out reference to ReviewerLink in the AssignmentsLinks component. --- .../common/AssignmentLinks/AssignmentLinks.jsx | 1 - .../common/AssignmentLinks/ReviewerLink.jsx | 18 ------------------ 2 files changed, 19 deletions(-) delete mode 100644 app/assets/javascripts/components/common/AssignmentLinks/ReviewerLink.jsx diff --git a/app/assets/javascripts/components/common/AssignmentLinks/AssignmentLinks.jsx b/app/assets/javascripts/components/common/AssignmentLinks/AssignmentLinks.jsx index 954dd0bb50..347ec36c42 100644 --- a/app/assets/javascripts/components/common/AssignmentLinks/AssignmentLinks.jsx +++ b/app/assets/javascripts/components/common/AssignmentLinks/AssignmentLinks.jsx @@ -70,7 +70,6 @@ const AssignmentLinks = ({ assignment, courseType, user, course, project, editMo reviewers = ; } - // const reviewers = ; const links = actions.reduce(interleaveSeparators, []); return ( diff --git a/app/assets/javascripts/components/common/AssignmentLinks/ReviewerLink.jsx b/app/assets/javascripts/components/common/AssignmentLinks/ReviewerLink.jsx deleted file mode 100644 index 12df4de691..0000000000 --- a/app/assets/javascripts/components/common/AssignmentLinks/ReviewerLink.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -// components -import AssignedToLink from '@components/overview/my_articles/common/AssignedToLink.jsx'; - -export const ReviewerLink = ({ reviewers }) => { - return ; -}; - -ReviewerLink.propTypes = { - // props - reviewers: PropTypes.arrayOf( - PropTypes.string - ), -}; - -export default ReviewerLink; From c6c219e437bc84069c2690ca988ce537eb34f27b Mon Sep 17 00:00:00 2001 From: Sage Ross Date: Wed, 4 Dec 2024 15:59:59 -0800 Subject: [PATCH 08/66] [WIP] Bypass Revisions table queries for DiffViewer --- .../javascripts/actions/article_actions.js | 13 +-- .../components/revisions/diff_viewer.jsx | 40 ++++------ .../javascripts/reducers/article_details.js | 2 +- .../utils/mediawiki_revisions_utils.js | 79 +++++++++++++++++++ app/controllers/articles_controller.rb | 3 - app/views/articles/details.json.jbuilder | 13 +-- 6 files changed, 110 insertions(+), 40 deletions(-) diff --git a/app/assets/javascripts/actions/article_actions.js b/app/assets/javascripts/actions/article_actions.js index 5d634545c6..17243a6890 100644 --- a/app/assets/javascripts/actions/article_actions.js +++ b/app/assets/javascripts/actions/article_actions.js @@ -1,5 +1,6 @@ import * as types from '../constants'; import API from '../utils/api.js'; +import { getRevisionRange } from '../utils/mediawiki_revisions_utils'; // This action uses the Thunk middleware pattern: instead of returning a plain // action object, it returns a function that takes the store dispatch fucntion — @@ -10,11 +11,13 @@ import API from '../utils/api.js'; export function fetchArticleDetails(articleId, courseId) { return function (dispatch) { return API.fetchArticleDetails(articleId, courseId) - .then(response => (dispatch({ - type: types.RECEIVE_ARTICLE_DETAILS, - articleId, - data: response - }))) + .then((response) => { + // eslint-disable-next-line no-console + const details = response.article_details; + return getRevisionRange(details.apiUrl, details.articleTitle, details.editors, details.startDate, details.endDate) + // eslint-disable-next-line no-console + .then(revisionRange => dispatch({ type: types.RECEIVE_ARTICLE_DETAILS, articleId, details, revisionRange })); + }) .catch(response => (dispatch({ type: types.API_FAIL, data: response }))); }; } diff --git a/app/assets/javascripts/components/revisions/diff_viewer.jsx b/app/assets/javascripts/components/revisions/diff_viewer.jsx index e8214ba74a..e136ac9d8e 100644 --- a/app/assets/javascripts/components/revisions/diff_viewer.jsx +++ b/app/assets/javascripts/components/revisions/diff_viewer.jsx @@ -121,15 +121,15 @@ const DiffViewer = createReactClass({ return; } this.setState({ diffFetchInitiated: true }); + this.fetchDiff(this.diffUrl(props.revision)); - if (props.first_revision) { + if (props.first_revision && !this.state.parentRevisionId) { return this.findParentOfFirstRevision(props); } - this.fetchDiff(this.diffUrl(props.revision)); }, wikiUrl(revision) { - return `https://${toWikiDomain(revision.wiki)}`; + return `https://${toWikiDomain(revision.wiki || this.props.article)}`; }, diffUrl(lastRevision, firstRevision) { @@ -138,11 +138,11 @@ const DiffViewer = createReactClass({ // eg, "https://en.wikipedia.org/w/api.php?action=query&prop=revisions&revids=139993&rvdiffto=prev&format=json", let diffUrl; if (this.state.parentRevisionId) { - diffUrl = `${queryBase}&revids=${this.state.parentRevisionId}|${lastRevision.mw_rev_id}&rvdiffto=${lastRevision.mw_rev_id}`; + diffUrl = `${queryBase}&revids=${this.state.parentRevisionId}|${lastRevision.revid}&rvdiffto=${lastRevision.revid}`; } else if (firstRevision) { - diffUrl = `${queryBase}&revids=${firstRevision.mw_rev_id}|${lastRevision.mw_rev_id}&rvdiffto=${lastRevision.mw_rev_id}`; + diffUrl = `${queryBase}&revids=${firstRevision.revid}|${lastRevision.revid}&rvdiffto=${lastRevision.revid}`; } else { - diffUrl = `${queryBase}&revids=${lastRevision.mw_rev_id}&rvdiffto=prev`; + diffUrl = `${queryBase}&revids=${lastRevision.revid}&rvdiffto=prev`; } return diffUrl; @@ -151,26 +151,16 @@ const DiffViewer = createReactClass({ webDiffUrl() { const wikiUrl = this.wikiUrl(this.props.revision); if (this.state.parentRevisionId) { - return `${wikiUrl}/w/index.php?oldid=${this.state.parentRevisionId}&diff=${this.props.revision.mw_rev_id}`; + return `${wikiUrl}/w/index.php?oldid=${this.state.parentRevisionId}&diff=${this.props.revision.revid}`; } else if (this.props.first_revision) { - return `${wikiUrl}/w/index.php?oldid=${this.props.first_revision.mw_rev_id}&diff=${this.props.revision.mw_rev_id}`; + return `${wikiUrl}/w/index.php?oldid=${this.props.first_revision.revid}&diff=${this.props.revision.revid}`; } - return `${wikiUrl}/w/index.php?diff=${this.props.revision.mw_rev_id}`; + return `${wikiUrl}/w/index.php?diff=${this.props.revision.revid}`; }, findParentOfFirstRevision(props) { - const wikiUrl = this.wikiUrl(props.revision); - const queryBase = `${wikiUrl}/w/api.php?action=query&prop=revisions&origin=*&format=json`; - const diffUrl = `${queryBase}&revids=${props.first_revision.mw_rev_id}`; - - fetch(diffUrl) - .then(resp => resp.json()) - .then((data) => { - const revisionData = data.query.pages[props.first_revision.mw_page_id].revisions[0]; - const parentRevisionId = revisionData.parentid; - this.setState({ parentRevisionId }); - this.fetchDiff(this.diffUrl(props.revision, props.first_revision)); - }); + const parentRevisionId = props.first_revision.parentid; + this.setState({ parentRevisionId }); }, fetchDiff(diffUrl) { @@ -179,13 +169,13 @@ const DiffViewer = createReactClass({ .then((data) => { let firstRevisionData; try { - firstRevisionData = data.query.pages[this.props.revision.mw_page_id].revisions[0]; + firstRevisionData = data.query.pages[this.props.revision.pageid].revisions[0]; } catch (_err) { firstRevisionData = {}; } let lastRevisionData; try { - lastRevisionData = data.query.pages[this.props.revision.mw_page_id].revisions[1]; + lastRevisionData = data.query.pages[this.props.revision.pageid].revisions[1]; } catch (_err) { /* noop */ } // Data may or may not include the diff. @@ -282,7 +272,7 @@ const DiffViewer = createReactClass({ editDate = I18n.t('revisions.edited_on', { edit_date: formatedDate }); finalDate =
{editDate}
; charactersCount =
{this.props.revision.characters} {I18n.t('revisions.chars_added')}
; - } else { + } else if (this.state.fetched) { firstRevTime = formatDateWithTime(this.state.firstRevDateTime); lastRevTime = formatDateWithTime(this.state.lastRevDateTime); timeSpan = I18n.t('revisions.edit_time_span', { first_time: firstRevTime, last_time: lastRevTime }); @@ -310,7 +300,7 @@ const DiffViewer = createReactClass({ article={this.props.article} editors={this.props.editors} before_rev_id={this.state.parentRevisionId} - after_rev_id={this.props.revision.mw_rev_id} + after_rev_id={this.props.revision.revid} /> ); } diff --git a/app/assets/javascripts/reducers/article_details.js b/app/assets/javascripts/reducers/article_details.js index df5d82083d..164fb7380c 100644 --- a/app/assets/javascripts/reducers/article_details.js +++ b/app/assets/javascripts/reducers/article_details.js @@ -6,7 +6,7 @@ export default function articleDetails(state = initialState, action) { switch (action.type) { case RECEIVE_ARTICLE_DETAILS: { const newState = { ...state }; - newState[action.articleId] = action.data.article_details; + newState[action.articleId] = { ...action.details, ...action.revisionRange }; return newState; } default: diff --git a/app/assets/javascripts/utils/mediawiki_revisions_utils.js b/app/assets/javascripts/utils/mediawiki_revisions_utils.js index c6a5ebd2b1..1811e3b9b8 100644 --- a/app/assets/javascripts/utils/mediawiki_revisions_utils.js +++ b/app/assets/javascripts/utils/mediawiki_revisions_utils.js @@ -154,3 +154,82 @@ const fetchRevisionsFromWiki = async (days, wiki, usernames, course_start, last_ return { revisions, wiki, exitNext }; }; +const parseRevisionResponse = (json) => { + const page = Object.values(json.query.pages)[0]; + if (page.revisions) { + return { pageid: page.pageid, ...page.revisions[0] }; + } +}; + +const fetchEarliestRef = async (API_URL, articleTitle, username, startDate, endDate) => { + const params = { + action: 'query', + format: 'json', + prop: 'revisions', + titles: articleTitle, + rvprop: 'timestamp|user|ids', + rvuser: username, + rvdir: 'newer', + rvlimit: 1, + rvstart: formatISO(toDate(startDate)), + rvend: formatISO(toDate(endDate)), + }; + + const response = await request(`${API_URL}?${stringify(params)}&origin=*`); + const json = await response.json(); + return parseRevisionResponse(json); +}; + +const fetchLatestRef = async (API_URL, articleTitle, username, startDate, endDate) => { + const params = { + action: 'query', + format: 'json', + prop: 'revisions', + titles: articleTitle, + rvprop: 'timestamp|user|ids', + rvuser: username, + rvdir: 'older', + rvlimit: 1, + rvstart: formatISO(toDate(endDate)), + rvend: formatISO(toDate(startDate)) + }; + + const response = await request(`${API_URL}?${stringify(params)}&origin=*`); + const json = await response.json(); + return parseRevisionResponse(json); +}; + +const earliestRev = (revs) => { + let earliest; + for (const rev of revs) { + if (rev) { + if (!earliest) { earliest = rev; } + if (toDate(rev.timestamp) < toDate(earliest.timestamp)) { earliest = rev; } + } + } + return earliest; +}; + +const latestRev = (revs) => { + let latest; + for (const rev of revs) { + if (rev) { + if (!latest) { latest = rev; } + if (toDate(rev.timestamp) > toDate(latest.timestamp)) { latest = rev; } + } + } + return latest; +}; + +export const getRevisionRange = async (API_URL, articleTitle, usernames, startDate, endDate) => { + let firstRevs = []; + let lastRevs = []; + for (const username of usernames) { + firstRevs.push(fetchEarliestRef(API_URL, articleTitle, username, startDate, endDate)); + lastRevs.push(fetchLatestRef(API_URL, articleTitle, username, startDate, endDate)); + } + firstRevs = await Promise.all(firstRevs); + lastRevs = await Promise.all(lastRevs); + return { first_revision: earliestRev(firstRevs), last_revision: latestRev(lastRevs) }; +}; + diff --git a/app/controllers/articles_controller.rb b/app/controllers/articles_controller.rb index 5dbfe8bec6..7462d8945a 100644 --- a/app/controllers/articles_controller.rb +++ b/app/controllers/articles_controller.rb @@ -12,9 +12,6 @@ def article_data # returns details about how an article changed during a course def details @article = Article.find(params[:article_id]) - revisions = @course.tracked_revisions.where(article_id: @article.id).order(:date) - @first_revision = revisions.first - @last_revision = revisions.last article_course = ArticlesCourses.find_by(course: @course, article: @article, tracked: true) @editors = User.where(id: article_course&.user_ids) end diff --git a/app/views/articles/details.json.jbuilder b/app/views/articles/details.json.jbuilder index 4953f5c96d..a5469a4dd7 100644 --- a/app/views/articles/details.json.jbuilder +++ b/app/views/articles/details.json.jbuilder @@ -1,12 +1,13 @@ # frozen_string_literal: true json.article_details do - json.first_revision do - json.call(@first_revision, :wiki, :mw_rev_id, :mw_page_id) if @first_revision - end - - json.last_revision do - json.call(@last_revision, :wiki, :mw_rev_id, :mw_page_id) if @last_revision + json.startDate @course.start + json.endDate @course.end + json.articleTitle @article.escaped_full_title + json.apiUrl @article.wiki.api_url + json.wiki do + json.project @article.wiki.project + json.language @article.wiki.language end json.editors do From eaa6ac7ac61f285083bca80373c6619b24fb710a Mon Sep 17 00:00:00 2001 From: Sage Ross Date: Wed, 4 Dec 2024 16:33:12 -0800 Subject: [PATCH 09/66] Fix DiffViewer error when there's only 1 revision --- app/assets/javascripts/components/revisions/diff_viewer.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/components/revisions/diff_viewer.jsx b/app/assets/javascripts/components/revisions/diff_viewer.jsx index e136ac9d8e..828b3c406d 100644 --- a/app/assets/javascripts/components/revisions/diff_viewer.jsx +++ b/app/assets/javascripts/components/revisions/diff_viewer.jsx @@ -191,7 +191,7 @@ const DiffViewer = createReactClass({ comment: firstRevisionData.comment, fetched: true, firstRevDateTime: firstRevisionData.timestamp, - lastRevDateTime: lastRevisionData ? lastRevisionData.timestamp : null + lastRevDateTime: lastRevisionData ? lastRevisionData.timestamp : firstRevisionData.timestamp }); }); }, From bd96bcef41d0c9268a8177cbed42d0b9a38da776 Mon Sep 17 00:00:00 2001 From: Sage Ross Date: Mon, 9 Dec 2024 12:51:23 -0800 Subject: [PATCH 10/66] Fix broken JS test --- test/reducers/article_details.spec.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/reducers/article_details.spec.js b/test/reducers/article_details.spec.js index bb9abdcda1..594c05a279 100644 --- a/test/reducers/article_details.spec.js +++ b/test/reducers/article_details.spec.js @@ -21,12 +21,14 @@ describe('article_details reducer', () => { const mockAction = { type: RECEIVE_ARTICLE_DETAILS, articleId: 586, - data: { - article_details: 'best article ever' - } + details: { detailsData: 'best article ever' }, + revisionRange: { revisionRangeData: 'more data' } }; const expectedState = { - 586: 'best article ever' + 586: { + detailsData: 'best article ever', + revisionRangeData: 'more data' + } }; expect(article_details(undefined, mockAction)).toEqual(expectedState); } From ff3a87a3b3e91dfc1117c8ead3eb3314f84ad0fe Mon Sep 17 00:00:00 2001 From: Sage Ross Date: Mon, 9 Dec 2024 16:28:07 -0800 Subject: [PATCH 11/66] Fix broken rspec test for new details.json behavior --- spec/controllers/articles_controller_spec.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/spec/controllers/articles_controller_spec.rb b/spec/controllers/articles_controller_spec.rb index 33140fe45e..805f7cf767 100644 --- a/spec/controllers/articles_controller_spec.rb +++ b/spec/controllers/articles_controller_spec.rb @@ -39,15 +39,11 @@ expect(assigns(:course)).to eq(course) end - it 'sets the first revision, last revision, and list of editors' do + it 'sets the list of editors' do get '/articles/details', params: request_params expect(assigns(:article)).to eq(article) expect(assigns(:course)).to eq(course) json_response = Oj.load(response.body) - expect(json_response['article_details']['first_revision']['mw_rev_id']) - .to eq(revision1.mw_rev_id) - expect(json_response['article_details']['last_revision']['mw_rev_id']) - .to eq(revision2.mw_rev_id) expect(json_response['article_details']['editors']).to include(user.username) expect(json_response['article_details']['editors']).to include(second_user.username) end From 80b78337eea3f3b18e858426947c0d240c91fcc6 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 12 Dec 2024 13:22:47 +0100 Subject: [PATCH 12/66] Localisation updates from https://translatewiki.net. --- config/locales/se.yml | 1 + config/locales/sms.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/config/locales/se.yml b/config/locales/se.yml index 843c2c07ee..43f62f073d 100644 --- a/config/locales/se.yml +++ b/config/locales/se.yml @@ -119,6 +119,7 @@ se: tags: Gilkorat removed: Sihkkojuvvon added: Lasihuvvon + descriptions: Govvádusat namespace: article: Artihkal file: Fiila diff --git a/config/locales/sms.yml b/config/locales/sms.yml index ac31b2bdea..c2bbfe3f51 100644 --- a/config/locales/sms.yml +++ b/config/locales/sms.yml @@ -161,6 +161,7 @@ sms: tags: Ǩeâlggal update_statistics_link: Lââʹssteâđ removed: Jaukkuum + descriptions: Deskriipt namespace: article: Artikkel project: Projeʹktt From def64a2734cc661655cf83c86318613511a55aef Mon Sep 17 00:00:00 2001 From: 0x_!nit <113853868+shishiro26@users.noreply.github.com> Date: Sat, 14 Dec 2024 01:02:17 +0530 Subject: [PATCH 13/66] feat(milestones):add dates to milestones section (#6020) --- .../components/overview/milestones.jsx | 8 ++++--- .../components/overview/overview_handler.jsx | 8 ++++--- .../components/timeline/timeline.jsx | 9 ++++---- .../components/timeline/timeline_handler.jsx | 4 +++- app/assets/javascripts/selectors/index.js | 22 ++++++++++++++++++- 5 files changed, 38 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/components/overview/milestones.jsx b/app/assets/javascripts/components/overview/milestones.jsx index 3671441de2..bcf1105f0c 100644 --- a/app/assets/javascripts/components/overview/milestones.jsx +++ b/app/assets/javascripts/components/overview/milestones.jsx @@ -14,7 +14,8 @@ const Milestones = createReactClass({ timelineStart: PropTypes.string.isRequired, weeks: PropTypes.array.isRequired, allWeeks: PropTypes.array.isRequired, - course: PropTypes.object.isRequired + course: PropTypes.object.isRequired, + allWeeksDates: PropTypes.array.isRequired, }, milestoneBlockType: 2, @@ -28,8 +29,9 @@ const Milestones = createReactClass({ const weekNumberOffset = CourseDateUtils.weeksBeforeTimeline(this.props.course); const blocks = []; - this.props.allWeeks.map((week) => { + this.props.allWeeks.map((week, navIndex) => { if (week.empty) return null; + const weekDates = this.props.allWeeksDates; const milestoneBlocks = filter(week.blocks, block => block.kind === this.milestoneBlockType); return milestoneBlocks.map((block) => { @@ -40,7 +42,7 @@ const Milestones = createReactClass({ return blocks.push(
-

Week {week.weekNumber + weekNumberOffset} {completionNote}

+

Week {week.weekNumber + weekNumberOffset} ({weekDates[navIndex].start} - {weekDates[navIndex].end}) {completionNote}


diff --git a/app/assets/javascripts/components/overview/overview_handler.jsx b/app/assets/javascripts/components/overview/overview_handler.jsx index 8723b45599..ff630c065f 100644 --- a/app/assets/javascripts/components/overview/overview_handler.jsx +++ b/app/assets/javascripts/components/overview/overview_handler.jsx @@ -23,7 +23,7 @@ import { deleteCourse, updateCourse, resetCourse, persistCourse, nameHasChanged, import { fetchOnboardingAlert } from '../../actions/course_alert_actions'; import { fetchTags } from '../../actions/tag_actions'; import { addValidation, setValid, setInvalid, activateValidations } from '../../actions/validation_actions'; -import { getStudentUsers, getWeeksArray, getAllWeeksArray, firstValidationErrorMessage, isValid } from '../../selectors'; +import { getStudentUsers, getWeeksArray, getAllWeeksArray, firstValidationErrorMessage, isValid, getAllWeekDates } from '../../selectors'; import OverviewStatsTabs from '../common/overview_stats_tabs'; const Overview = createReactClass({ @@ -44,12 +44,13 @@ const Overview = createReactClass({ updateClonedCourse: PropTypes.func.isRequired, weeks: PropTypes.array.isRequired, allWeeks: PropTypes.array.isRequired, + allWeeksDates: PropTypes.array.isRequired, setValid: PropTypes.func.isRequired, setInvalid: PropTypes.func.isRequired, activateValidations: PropTypes.func.isRequired, firstErrorMessage: PropTypes.string, isValid: PropTypes.bool.isRequired, - courseCreationNotice: PropTypes.string + courseCreationNotice: PropTypes.string, }, componentDidMount() { @@ -159,7 +160,7 @@ const Overview = createReactClass({ refetchCourse={this.props.refetchCourse} /> - +
) : (
@@ -192,6 +193,7 @@ const mapStateToProps = state => ({ campaigns: state.campaigns.campaigns, weeks: getWeeksArray(state), allWeeks: getAllWeeksArray(state), + allWeeksDates: getAllWeekDates(state), loading: state.timeline.loading || state.course.loading, firstErrorMessage: firstValidationErrorMessage(state), isValid: isValid(state), diff --git a/app/assets/javascripts/components/timeline/timeline.jsx b/app/assets/javascripts/components/timeline/timeline.jsx index 56db05e579..98a12d1c87 100644 --- a/app/assets/javascripts/components/timeline/timeline.jsx +++ b/app/assets/javascripts/components/timeline/timeline.jsx @@ -12,7 +12,6 @@ import CourseLink from '../common/course_link.jsx'; import Affix from '../common/affix.jsx'; import EditableRedux from '../high_order/editable_redux'; -import DateCalculator from '../../utils/date_calculator.js'; import CourseUtils from '../../utils/course_utils.js'; import CourseDateUtils from '../../utils/course_date_utils.js'; import { toDate } from '../../utils/date_utils.js'; @@ -26,6 +25,7 @@ const Timeline = createReactClass({ course: PropTypes.object.isRequired, weeks: PropTypes.array, allWeeks: PropTypes.array, + allWeeksDates: PropTypes.array, week_meetings: PropTypes.array, editableBlockIds: PropTypes.array, editable: PropTypes.bool, @@ -387,8 +387,7 @@ const Timeline = createReactClass({ if (navIndex === 0) { navClassName += ' is-current'; } - - const dateCalc = new DateCalculator(this.props.course.timeline_start, this.props.course.timeline_end, navIndex, { zeroIndexed: true }); + const weekDates = this.props.allWeeksDates; const navWeekKey = `week-${navIndex}`; const navWeekLink = `#week-${navIndex + 1 + weeksBeforeTimeline}`; @@ -397,7 +396,7 @@ const Timeline = createReactClass({ if (usingCustomTitles) { let navTitle = ''; if (weekInfo.emptyWeek) { - const datesStr = `${dateCalc.start()} - ${dateCalc.end()}`; + const datesStr = `${weekDates[navIndex].start} - ${weekDates[navIndex].end}`; navTitle = I18n.t('timeline.week_number', { number: datesStr }); } else { navTitle = weekInfo.title ? weekInfo.title : I18n.t('timeline.week_number', { number: navIndex + 1 + weeksBeforeTimeline }); @@ -411,7 +410,7 @@ const Timeline = createReactClass({ navItem = (
  • {I18n.t('timeline.week_number', { number: navIndex + 1 + weeksBeforeTimeline })} - {dateCalc.start()} - {dateCalc.end()} + {weekDates[navIndex].start} - {weekDates[navIndex].end}
  • ); } diff --git a/app/assets/javascripts/components/timeline/timeline_handler.jsx b/app/assets/javascripts/components/timeline/timeline_handler.jsx index 6d28a66f6a..a497f20a82 100644 --- a/app/assets/javascripts/components/timeline/timeline_handler.jsx +++ b/app/assets/javascripts/components/timeline/timeline_handler.jsx @@ -16,7 +16,7 @@ import { addWeek, deleteWeek, persistTimeline, setBlockEditable, cancelBlockEditable, updateBlock, addBlock, deleteBlock, insertBlock, updateTitle, resetTitles, restoreTimeline, deleteAllWeeks } from '../../actions/timeline_actions'; -import { getWeeksArray, getAllWeeksArray, getAvailableTrainingModules, editPermissions } from '../../selectors'; +import { getWeeksArray, getAllWeeksArray, getAvailableTrainingModules, editPermissions, getAllWeekDates } from '../../selectors'; // Define TimelineHandler as a functional component using an arrow function // Move propTypes outside the component definition @@ -96,6 +96,7 @@ const TimelineHandler = (props) => { course={props.course} weeks={props.weeks} allWeeks={props.allWeeks} + allWeeksDates={props.allWeeksDates} week_meetings={weekMeetings} editableBlockIds={props.editableBlockIds} reorderable={reorderable} @@ -145,6 +146,7 @@ TimelineHandler.propTypes = { const mapStateToProps = state => ({ weeks: getWeeksArray(state), allWeeks: getAllWeeksArray(state), + allWeeksDates: getAllWeekDates(state), loading: state.timeline.loading || state.course.loading, editableBlockIds: state.timeline.editableBlockIds, availableTrainingModules: getAvailableTrainingModules(state), diff --git a/app/assets/javascripts/selectors/index.js b/app/assets/javascripts/selectors/index.js index 07acf7da97..c6e9ab8d7a 100644 --- a/app/assets/javascripts/selectors/index.js +++ b/app/assets/javascripts/selectors/index.js @@ -5,7 +5,7 @@ import { STUDENT_ROLE, INSTRUCTOR_ROLE, ONLINE_VOLUNTEER_ROLE, CAMPUS_VOLUNTEER_ import UserUtils from '../utils/user_utils.js'; import { PageAssessmentGrades } from '../utils/article_finder_language_mappings.js'; import CourseDateUtils from '../utils/course_date_utils.js'; - +import DateCalculator from '../utils/date_calculator'; const getUsers = state => state.users.users; const getCurrentUserFromHtml = state => state.currentUserFromHtml; @@ -357,3 +357,23 @@ export const getTicketsById = createSelector( return tickets.all.reduce((acc, ticket) => ({ ...acc, [ticket.id]: ticket }), {}); } ); + +export const getAllWeekDates = createSelector( + [getAllWeeksArray, getCourse], + (weeksArray, course) => { + return weeksArray.map((_, index) => { + const dateCalc = new DateCalculator( + course.timeline_start, + course.timeline_end, + index, + { zeroIndexed: true } + ); + return { + start: dateCalc.start(), + end: dateCalc.end(), + }; + }); + } +); + + From f0ed9fcaab1f896993e211d10fbb2219bcdc389d Mon Sep 17 00:00:00 2001 From: Formasitchijoh Date: Tue, 17 Dec 2024 05:47:30 +0100 Subject: [PATCH 14/66] fix: add missing interface message does_not_exist, used in the feedback modal when a course has no rating --- config/locales/en.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/locales/en.yml b/config/locales/en.yml index 8e82b21e90..29958d22a3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -481,6 +481,7 @@ en: active_courses: Active Courses add_trainings: Please add student trainings to your assignment timeline. Assigning training modules is an essential part of Wiki Ed's best practices. alerts: Alerts + does_not_exist: The Item doesn't exist. Create a Sandbox page and assign yourself to that to receive suggestions. all_courses: All Courses already_enrolled: You are already a part of '%{title}'! already_exists: That already exists for this course! From 677d73179e26b0d5fc27d265e12fe331b1459353 Mon Sep 17 00:00:00 2001 From: Formasitchijoh Date: Tue, 17 Dec 2024 05:59:55 +0100 Subject: [PATCH 15/66] fix: failing test in assigned_article_spec by waiting for the component to be available before interacting with it --- spec/features/assigned_articles_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/features/assigned_articles_spec.rb b/spec/features/assigned_articles_spec.rb index 72d966f9d1..0537888615 100644 --- a/spec/features/assigned_articles_spec.rb +++ b/spec/features/assigned_articles_spec.rb @@ -23,6 +23,8 @@ visit "/courses/#{course.slug}/articles/assigned" expect(page).to have_content('Nancy Tuana') find('a', text: 'Feedback').click + expect(page).to have_no_content(I18n.t('courses.feedback_loading')) + expect(page).to have_selector('textarea.feedback-form') find('textarea.feedback-form').fill_in with: 'This is a great article!' click_button 'Add Suggestion' find('a', text: 'Delete').click From bde526854bed03faaf2b8e3a88927bff357c8675 Mon Sep 17 00:00:00 2001 From: Formasitchijoh Date: Tue, 17 Dec 2024 12:03:11 +0100 Subject: [PATCH 16/66] fix:failing test in account_requests_spec by ensuring the confirm modal is visible and the ok button rendered before interacting with it --- spec/features/account_requests_spec.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/features/account_requests_spec.rb b/spec/features/account_requests_spec.rb index 7583b4d328..b1ad0bb9ac 100644 --- a/spec/features/account_requests_spec.rb +++ b/spec/features/account_requests_spec.rb @@ -18,7 +18,10 @@ login_as(instructor) visit "/courses/#{course.slug}" click_button 'Enable account requests' - click_button 'OK' + expect(page).to have_selector('.confirm-modal-overlay') + within('.confirm-modal') do + click_button 'OK' + end expect(page).to have_content('Account request generation enabled') end From 5220f4e279290c736d1390ec54759ccc4a361d75 Mon Sep 17 00:00:00 2001 From: Bhushan Deshmukh Date: Tue, 17 Dec 2024 22:38:56 +0530 Subject: [PATCH 17/66] Generic label for Programs & Events Dashboard (#6061) --------- Co-authored-by: bhushan354 --- .../javascripts/components/user_profiles/course_details.jsx | 3 ++- config/locales/en.yml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/components/user_profiles/course_details.jsx b/app/assets/javascripts/components/user_profiles/course_details.jsx index a84bec8e05..bad3f3b9cb 100644 --- a/app/assets/javascripts/components/user_profiles/course_details.jsx +++ b/app/assets/javascripts/components/user_profiles/course_details.jsx @@ -2,10 +2,11 @@ import React from 'react'; import PropTypes from 'prop-types'; const CourseDetails = ({ courses }) => { + const labelKey = Features.wikiEd ? 'courses.view_page' : 'courses_generic.view_page'; const elements = courses.map((course) => { return ( -
    {I18n.t('courses.view_page')}
    +
    {I18n.t(labelKey)}
    {course.course_title}
    diff --git a/config/locales/en.yml b/config/locales/en.yml index 29958d22a3..590544a53c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1022,7 +1022,7 @@ en: uploads_none: This project has not contributed any images or other media files to Wikimedia Commons. user_uploads_none: Selected users have not contributed any images or other media files to this project. view_other: View other campaigns - view_page: View Program Page + view_page: View Page word_count_doc: An estimate of the number of words added to mainspace articles by the program's editors during the program term word_count_doc_wikidata: An estimate of the number of words added to mainspace items by the program's editors during the program term yourcourses: Your Programs From f951240fcf41625a779fccee28a9d2eed36f31bb Mon Sep 17 00:00:00 2001 From: Sage Ross Date: Thu, 12 Dec 2024 09:16:38 -0800 Subject: [PATCH 18/66] Remove the 'revisions' CSV download This feature isn't compatible with removing the Revisions table --- .../overview/course_stats_download_modal.jsx | 6 --- app/controllers/analytics_controller.rb | 9 +--- config/routes.rb | 1 - lib/analytics/course_edits_csv_builder.rb | 46 ------------------- spec/controllers/analytics_controller_spec.rb | 9 ---- .../course_edits_csv_builder_spec.rb | 26 ----------- 6 files changed, 1 insertion(+), 96 deletions(-) delete mode 100644 lib/analytics/course_edits_csv_builder.rb delete mode 100644 spec/lib/analytics/course_edits_csv_builder_spec.rb diff --git a/app/assets/javascripts/components/overview/course_stats_download_modal.jsx b/app/assets/javascripts/components/overview/course_stats_download_modal.jsx index a3eb8052b4..f7c2247eb6 100644 --- a/app/assets/javascripts/components/overview/course_stats_download_modal.jsx +++ b/app/assets/javascripts/components/overview/course_stats_download_modal.jsx @@ -17,7 +17,6 @@ const CourseStatsDownloadModal = ({ course }) => { } const overviewCsvLink = `/course_csv?course=${course.slug}`; - const editsCsvLink = `/course_edits_csv?course=${course.slug}`; const uploadsCsvLink = `/course_uploads_csv?course=${course.slug}`; const studentsCsvLink = `/course_students_csv?course=${course.slug}`; const articlesCsvLink = `/course_articles_csv?course=${course.slug}`; @@ -47,11 +46,6 @@ const CourseStatsDownloadModal = ({ course }) => { {I18n.t('courses.data_overview_info')}


    -

    - {I18n.t('courses.data_edits')} - {I18n.t('courses.data_edits_info')} -

    -

    {I18n.t('courses.data_uploads')} {I18n.t('courses.data_uploads_info')} diff --git a/app/controllers/analytics_controller.rb b/app/controllers/analytics_controller.rb index 5372bd7df0..353f679640 100644 --- a/app/controllers/analytics_controller.rb +++ b/app/controllers/analytics_controller.rb @@ -3,7 +3,6 @@ require_dependency "#{Rails.root}/lib/analytics/monthly_report" require_dependency "#{Rails.root}/lib/analytics/course_statistics" require_dependency "#{Rails.root}/lib/analytics/course_csv_builder" -require_dependency "#{Rails.root}/lib/analytics/course_edits_csv_builder" require_dependency "#{Rails.root}/lib/analytics/course_uploads_csv_builder" require_dependency "#{Rails.root}/lib/analytics/course_students_csv_builder" require_dependency "#{Rails.root}/lib/analytics/course_articles_csv_builder" @@ -18,7 +17,7 @@ class AnalyticsController < ApplicationController layout 'admin' include CourseHelper before_action :require_signed_in, only: :ungreeted - before_action :set_course, only: %i[course_csv course_edits_csv course_uploads_csv + before_action :set_course, only: %i[course_csv course_uploads_csv course_students_csv course_articles_csv course_revisions_csv course_wikidata_csv] @@ -56,12 +55,6 @@ def course_csv filename: "#{@course.slug}-#{Time.zone.today}.csv" end - def course_edits_csv - course = find_course_by_slug(params[:course]) - send_data CourseEditsCsvBuilder.new(course).generate_csv, - filename: "#{course.slug}-edits-#{Time.zone.today}.csv" - end - def tagged_courses_csv tag = params[:tag] send_data TaggedCoursesCsvBuilder.new(tag).generate_csv, diff --git a/config/routes.rb b/config/routes.rb index 61866bff13..0872bbf629 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -249,7 +249,6 @@ get 'usage' => 'analytics#usage' get 'ungreeted' => 'analytics#ungreeted' get 'course_csv' => 'analytics#course_csv' - get 'course_edits_csv' => 'analytics#course_edits_csv' get 'course_uploads_csv' => 'analytics#course_uploads_csv' get 'course_students_csv' => 'analytics#course_students_csv' get 'course_articles_csv' => 'analytics#course_articles_csv' diff --git a/lib/analytics/course_edits_csv_builder.rb b/lib/analytics/course_edits_csv_builder.rb deleted file mode 100644 index 649aef20d2..0000000000 --- a/lib/analytics/course_edits_csv_builder.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require 'csv' - -class CourseEditsCsvBuilder - include ArticleHelper - - def initialize(course) - @course = course - end - - def generate_csv - csv_data = [CSV_HEADERS] - @course.revisions.includes(:wiki, :article, :user).each do |revision| - csv_data << row(revision) - end - CSV.generate { |csv| csv_data.each { |line| csv << line } } - end - - private - - CSV_HEADERS = %w[ - revision_id - timestamp - wiki - article_title - diff - username - bytes_added - references_added - new_article - dashboard_edit - ].freeze - def row(revision) - row = [revision.mw_rev_id] - row << revision.date - row << revision.wiki.base_url - row << revision.article.full_title - row << revision.url - row << revision.user.username - row << revision.characters - row << revision.references_added - row << revision.new_article - row << revision.system - end -end diff --git a/spec/controllers/analytics_controller_spec.rb b/spec/controllers/analytics_controller_spec.rb index 24f8328e94..c5d02b0e46 100644 --- a/spec/controllers/analytics_controller_spec.rb +++ b/spec/controllers/analytics_controller_spec.rb @@ -82,15 +82,6 @@ end end - describe '#course_edits_csv' do - let(:course) { create(:course, slug: 'foo/bar_(baz)') } - - it 'returns a CSV' do - get '/course_edits_csv', params: { course: course.slug } - expect(response.body).to include('revision_id') - end - end - describe '#course_uploads_csv' do let(:course) { create(:course, slug: 'foo/bar_(baz)') } diff --git a/spec/lib/analytics/course_edits_csv_builder_spec.rb b/spec/lib/analytics/course_edits_csv_builder_spec.rb deleted file mode 100644 index fb8d18d629..0000000000 --- a/spec/lib/analytics/course_edits_csv_builder_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require "#{Rails.root}/lib/analytics/course_edits_csv_builder" - -describe CourseEditsCsvBuilder do - let(:course) { create(:course) } - let(:user) { create(:user) } - let(:article) { create(:article) } - let!(:courses_user) { create(:courses_user, course:, user:) } - let(:revision_count) { 5 } - let(:subject) { described_class.new(course).generate_csv } - - before do - # revisions during the course - revision_count.times do |i| - create(:revision, mw_rev_id: i, user:, date: course.start + 1.minute, article:) - end - # revision outside the course - create(:revision, mw_rev_id: 123, user:, date: course.start - 1.minute) - end - - it 'creates a CSV with a header and a row of data for each course revision' do - expect(subject.split("\n").count).to eq(revision_count + 1) - end -end From 0708c458255feb62ea6e461793a069a9331c7748 Mon Sep 17 00:00:00 2001 From: Sage Ross Date: Tue, 17 Dec 2024 09:51:48 -0800 Subject: [PATCH 19/66] Remove features for generating CSVs of course/campaign revisions This is necessary as part of the project to deprecate the Revisions table. --- .../campaign_stats_download_modal.jsx | 6 - .../overview/course_stats_download_modal.jsx | 5 - app/controllers/analytics_controller.rb | 8 +- config/routes.rb | 1 - lib/analytics/campaign_csv_builder.rb | 13 --- lib/analytics/course_revisions_csv_builder.rb | 105 ------------------ spec/controllers/analytics_controller_spec.rb | 9 -- spec/controllers/campaigns_controller_spec.rb | 11 -- .../course_revisions_csv_builder_spec.rb | 32 ------ 9 files changed, 1 insertion(+), 189 deletions(-) delete mode 100644 lib/analytics/course_revisions_csv_builder.rb delete mode 100644 spec/lib/analytics/course_revisions_csv_builder_spec.rb diff --git a/app/assets/javascripts/components/campaign/campaign_stats_download_modal.jsx b/app/assets/javascripts/components/campaign/campaign_stats_download_modal.jsx index 92082dcffb..80369304db 100644 --- a/app/assets/javascripts/components/campaign/campaign_stats_download_modal.jsx +++ b/app/assets/javascripts/components/campaign/campaign_stats_download_modal.jsx @@ -5,7 +5,6 @@ const CampaignStatsDownloadModal = ({ campaign_slug }) => { const courseDataLink = `/campaigns/${campaign_slug}/courses.csv`; const articlesEditedLink = `/campaigns/${campaign_slug}/articles_csv.csv`; - const RevisionsLink = `/campaigns/${campaign_slug}/revisions_csv.csv`; const editorsLink = `/campaigns/${campaign_slug}/students.csv`; const editorsByCourseLink = `/campaigns/${campaign_slug}/students.csv?course=true`; const instructorsLink = `/campaigns/${campaign_slug}/instructors.csv?course=true`; @@ -32,11 +31,6 @@ const CampaignStatsDownloadModal = ({ campaign_slug }) => { {I18n.t('campaign.data_articles_info')}


    -

    - {I18n.t('campaign.data_revisions')} - {I18n.t('campaign.data_revisions_info')} -

    -

    {I18n.t('campaign.data_editor_usernames')} {I18n.t('campaign.data_editor_usernames_info')} diff --git a/app/assets/javascripts/components/overview/course_stats_download_modal.jsx b/app/assets/javascripts/components/overview/course_stats_download_modal.jsx index f7c2247eb6..cd9bd4c000 100644 --- a/app/assets/javascripts/components/overview/course_stats_download_modal.jsx +++ b/app/assets/javascripts/components/overview/course_stats_download_modal.jsx @@ -20,7 +20,6 @@ const CourseStatsDownloadModal = ({ course }) => { const uploadsCsvLink = `/course_uploads_csv?course=${course.slug}`; const studentsCsvLink = `/course_students_csv?course=${course.slug}`; const articlesCsvLink = `/course_articles_csv?course=${course.slug}`; - const revisionsCsvLink = `/course_revisions_csv?course=${course.slug}`; const wikidataCsvLink = `/course_wikidata_csv?course=${course.slug}`; let wikidataLink; @@ -61,10 +60,6 @@ const CourseStatsDownloadModal = ({ course }) => { {I18n.t('courses.data_articles_info')}


    -

    - {I18n.t('courses.data_revisions')} - {I18n.t('courses.data_revisions_info')} -

    {wikidataLink}
    ); diff --git a/app/controllers/analytics_controller.rb b/app/controllers/analytics_controller.rb index 353f679640..de765ceb0b 100644 --- a/app/controllers/analytics_controller.rb +++ b/app/controllers/analytics_controller.rb @@ -6,7 +6,6 @@ require_dependency "#{Rails.root}/lib/analytics/course_uploads_csv_builder" require_dependency "#{Rails.root}/lib/analytics/course_students_csv_builder" require_dependency "#{Rails.root}/lib/analytics/course_articles_csv_builder" -require_dependency "#{Rails.root}/lib/analytics/course_revisions_csv_builder" require_dependency "#{Rails.root}/lib/analytics/course_wikidata_csv_builder" require_dependency "#{Rails.root}/lib/analytics/campaign_csv_builder" require_dependency "#{Rails.root}/lib/analytics/ungreeted_list" @@ -18,7 +17,7 @@ class AnalyticsController < ApplicationController include CourseHelper before_action :require_signed_in, only: :ungreeted before_action :set_course, only: %i[course_csv course_uploads_csv - course_students_csv course_articles_csv course_revisions_csv + course_students_csv course_articles_csv course_wikidata_csv] ######################## @@ -76,11 +75,6 @@ def course_articles_csv filename: "#{@course.slug}-articles-#{Time.zone.today}.csv" end - def course_revisions_csv - send_data CourseRevisionsCsvBuilder.new(@course).generate_csv, - filename: "#{@course.slug}-revisions-#{Time.zone.today}.csv" - end - def course_wikidata_csv send_data CourseWikidataCsvBuilder.new(@course).generate_csv, filename: "#{@course.slug}-wikidata-#{Time.zone.today}.csv" diff --git a/config/routes.rb b/config/routes.rb index 0872bbf629..ddccc8e33e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -253,7 +253,6 @@ get 'course_students_csv' => 'analytics#course_students_csv' get 'course_articles_csv' => 'analytics#course_articles_csv' get 'tagged_courses_csv/:tag' => 'analytics#tagged_courses_csv' - get 'course_revisions_csv' => 'analytics#course_revisions_csv' get 'course_wikidata_csv' => 'analytics#course_wikidata_csv' get 'all_courses_csv' => 'analytics#all_courses_csv' get 'all_courses' => 'analytics#all_courses' diff --git a/lib/analytics/campaign_csv_builder.rb b/lib/analytics/campaign_csv_builder.rb index 6705a38634..737797a772 100644 --- a/lib/analytics/campaign_csv_builder.rb +++ b/lib/analytics/campaign_csv_builder.rb @@ -3,7 +3,6 @@ require 'csv' require_dependency "#{Rails.root}/lib/analytics/course_csv_builder" require_dependency "#{Rails.root}/lib/analytics/course_articles_csv_builder" -require_dependency "#{Rails.root}/lib/analytics/course_revisions_csv_builder" require_dependency "#{Rails.root}/app/workers/campaign_csv_worker" require "#{Rails.root}/lib/analytics/course_wikidata_csv_builder" @@ -33,18 +32,6 @@ def articles_to_csv CSV.generate { |csv| csv_data.each { |line| csv << line } } end - def revisions_to_csv - csv_data = [CourseRevisionsCsvBuilder::CSV_HEADERS + ['course_slug']] - @campaign.courses.find_each do |course| - CourseRevisionsCsvBuilder.new(course).revisions_rows.each do |row| - row_with_slug = row + [course.slug] - csv_data << row_with_slug - end - end - - CSV.generate { |csv| csv_data.each { |line| csv << line } } - end - def wikidata_to_csv csv_data = [CourseWikidataCsvBuilder::CSV_HEADERS] courses = @campaign.courses diff --git a/lib/analytics/course_revisions_csv_builder.rb b/lib/analytics/course_revisions_csv_builder.rb deleted file mode 100644 index bd096b5329..0000000000 --- a/lib/analytics/course_revisions_csv_builder.rb +++ /dev/null @@ -1,105 +0,0 @@ -# frozen_string_literal: true - -require 'csv' - -class CourseRevisionsCsvBuilder - def initialize(course) - @course = course - set_revisions - end - - def generate_csv - csv_data = [CSV_HEADERS] - revisions_rows.each do |row| - csv_data << row - end - CSV.generate { |csv| csv_data.each { |line| csv << line } } - end - - def revisions_rows - @new_revisions.values.map do |revision_data| - build_row(revision_data) - end - end - - # rubocop:disable Metrics/AbcSize - def set_revisions - @new_revisions = {} - @course.revisions.includes(:wiki, :article, :user).map do |edit| - # Skip if the Article record is missing - next unless edit.article - - revision_edits = @new_revisions[edit.article_id] || new_revision(edit) - update_title_username(revision_edits, edit) - revision_edits[:mw_rev_id] = edit.mw_rev_id - revision_edits[:mw_page_id] = edit.mw_page_id - revision_edits[:wiki] = edit.wiki.domain - update_characters_references_views(revision_edits, edit) - revision_edits[:new_article] = true if edit.new_article - revision_edits[:deleted] = true if edit.deleted - revision_edits[:wp10] = edit.wp10 - revision_edits[:wp10_previous] = edit.wp10_previous - @new_revisions[edit.article_id] = revision_edits - end - end - # rubocop:enable Metrics/AbcSize - - def new_revision(edit) - article = edit.article - { - title: article.title, - new_article: false, - views: 0, - characters: {}, - references: {}, - deleted: edit.deleted, - wp10: {}, - wp10_previous: {} - } - end - - def update_title_username(revision_edits, edit) - revision_edits[:title] = edit.article.title - revision_edits[:username] = edit.user.username - end - - def update_characters_references_views(revision_edits, edit) - revision_edits[:characters] = edit.characters - revision_edits[:references] = edit.references_added - revision_edits[:views] = edit.views - end - - CSV_HEADERS = %w[ - Article_title - User - revision_id - page_id - wiki - characters_added - references_added - new - pageviews - deleted - wp10 - wp10_previous - ].freeze - - def build_row(revision_data) - row = [revision_data[:title]] - row << revision_data[:username] - row << revision_data[:mw_rev_id] - row << revision_data[:mw_page_id] - row << revision_data[:wiki] - add_characters_references(revision_data, row) - row << revision_data[:new_article] - row << revision_data[:views] - row << revision_data[:deleted] - row << revision_data[:wp10] - row << revision_data[:wp10_previous] - end - - def add_characters_references(revision_data, row) - row << revision_data[:characters] - row << revision_data[:references] - end -end diff --git a/spec/controllers/analytics_controller_spec.rb b/spec/controllers/analytics_controller_spec.rb index c5d02b0e46..a7e128bed3 100644 --- a/spec/controllers/analytics_controller_spec.rb +++ b/spec/controllers/analytics_controller_spec.rb @@ -109,15 +109,6 @@ end end - describe '#course_revisions_csv' do - let(:course) { create(:course, slug: 'foo/bar_(baz)') } - - it 'returns a CSV' do - get '/course_revisions_csv', params: { course: course.slug } - expect(response.body).to include('references_added') - end - end - describe '#course_wikidata_csv' do let(:wikidata) { Wiki.get_or_create(language: nil, project: 'wikidata') } let(:course) { create(:course, slug: 'foo/bar_(baz)', home_wiki: wikidata) } diff --git a/spec/controllers/campaigns_controller_spec.rb b/spec/controllers/campaigns_controller_spec.rb index 68d57f2306..4cd03b1275 100644 --- a/spec/controllers/campaigns_controller_spec.rb +++ b/spec/controllers/campaigns_controller_spec.rb @@ -417,17 +417,6 @@ expect(csv).to include(article.title) end - it 'return a csv of revision data' do - expect(CsvCleanupWorker).to receive(:perform_at) - get "/campaigns/#{campaign.slug}/revisions_csv", params: request_params - get "/campaigns/#{campaign.slug}/revisions_csv", params: request_params - follow_redirect! - csv = response.body.force_encoding('utf-8') - expect(csv).to include(course.slug) - expect(csv).to include(article.title) - expect(csv).to include('references_added') - end - it 'returns a csv of wikidata' do expect(CsvCleanupWorker).to receive(:perform_at) get "/campaigns/#{campaign.slug}/wikidata.csv" diff --git a/spec/lib/analytics/course_revisions_csv_builder_spec.rb b/spec/lib/analytics/course_revisions_csv_builder_spec.rb deleted file mode 100644 index 799c537d8e..0000000000 --- a/spec/lib/analytics/course_revisions_csv_builder_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require "#{Rails.root}/lib/analytics/course_revisions_csv_builder" - -describe CourseRevisionsCsvBuilder do - let(:course) { create(:course) } - let(:user) { create(:user, registered_at: course.start + 1.minute) } - let!(:courses_user) { create(:courses_user, course:, user:) } - - let(:article) { create(:article) } - let(:article2) { create(:article, title: 'Second_Article') } - let(:revision_count) { 5 } - let(:subject) { described_class.new(course).generate_csv } - - before do - revision_count.times do |i| - create(:revision, mw_rev_id: i, user:, date: course.start + 1.minute, article:) - end - # one revision for second article - create(:revision, mw_rev_id: 123, user:, date: course.start + 1.minute, article: article2) - # revisions with nil and characters, to make sure this does not cause problems - create(:revision, mw_rev_id: 124, user:, date: course.start + 1.minute, article: article2, - characters: nil) - create(:revision, mw_rev_id: 125, user:, date: course.start + 1.minute, article: article2, - characters: -500) - end - - it 'creates a CSV with a header and a row of data for each revision' do - expect(subject.split("\n").count).to eq(3) - end -end From a97297e6c05168e77df93a98accf53d7703c3398 Mon Sep 17 00:00:00 2001 From: Sage Ross Date: Tue, 17 Dec 2024 13:22:09 -0800 Subject: [PATCH 20/66] Tidy up email and preview for BlockedUserAlert mail --- .../blocked_user_alert_mailer/email.html.haml | 38 +++---------------- spec/mailers/previews/alert_mailer_preview.rb | 10 +---- 2 files changed, 6 insertions(+), 42 deletions(-) diff --git a/app/views/blocked_user_alert_mailer/email.html.haml b/app/views/blocked_user_alert_mailer/email.html.haml index b6353f5a8c..4a6f564332 100644 --- a/app/views/blocked_user_alert_mailer/email.html.haml +++ b/app/views/blocked_user_alert_mailer/email.html.haml @@ -1,35 +1,7 @@ %link{rel: 'stylesheet', href:'/mailer.css'} %p.paragraph - Please investigate: - %a.link{ href: @alert.url }= @alert.main_subject - -- if @alert.user_contributions_url - %p.paragraph - %a.link{href: @alert.user_contributions_url}= "#{@alert.user.username} contributions" - -- if @alert.message - %p.paragraph.preserve-whitespace - = "Message regarding #{@alert.user.username}:" - = @alert.message - - %p.paragraph - User email: - = @alert.user.email - - %p.paragraph - User real name: - = @alert.user.real_name - -- if @alert.article - %p.paragraph - Article: - %a.link{href: @alert.article.url}= @alert.article.title - -- if @alert.revision - %p.paragraph - %a.link{href: @alert.revision.url} diff - -- if @alert.details - %p.paragraph.preserve-whitespace - Alert details: - = @alert.details + We detected that + =@alert.user.username + has been blocked by an administrator on Wikipedia. The + %a.link{ href: @alert.url } block log + may indicate the reason for the block. diff --git a/spec/mailers/previews/alert_mailer_preview.rb b/spec/mailers/previews/alert_mailer_preview.rb index df176d1058..d9f7ee9f30 100644 --- a/spec/mailers/previews/alert_mailer_preview.rb +++ b/spec/mailers/previews/alert_mailer_preview.rb @@ -143,14 +143,6 @@ def example_blocked_edits_alert def example_user_blocked_alert user = example_user - message = "Student #{user.username} have been blocked on Wikipedia. -This mail to inform staff, student as well as instructors." - alert = Alert.new(type: 'BlockedUserAlert', user:, - course: example_course, message:) - alert.tap do |alrt| - alrt.define_singleton_method(:main_subject) do - "User #{user.username} have been blocked on Wikipedia" - end - end + Alert.new(type: 'BlockedUserAlert', user:, course: example_course) end end From ad1f07cb30b21441866a80ba928e900274a98f98 Mon Sep 17 00:00:00 2001 From: Formasitchijoh Date: Tue, 17 Dec 2024 23:32:41 +0100 Subject: [PATCH 21/66] fixes the previous fix which failed, by adding an extra wait time to the default, to ensure all data is fetched --- spec/features/assigned_articles_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/assigned_articles_spec.rb b/spec/features/assigned_articles_spec.rb index 0537888615..410b5e036f 100644 --- a/spec/features/assigned_articles_spec.rb +++ b/spec/features/assigned_articles_spec.rb @@ -23,7 +23,7 @@ visit "/courses/#{course.slug}/articles/assigned" expect(page).to have_content('Nancy Tuana') find('a', text: 'Feedback').click - expect(page).to have_no_content(I18n.t('courses.feedback_loading')) + expect(page).to have_no_content(I18n.t('courses.feedback_loading'), wait: 10) expect(page).to have_selector('textarea.feedback-form') find('textarea.feedback-form').fill_in with: 'This is a great article!' click_button 'Add Suggestion' From 1e1e8b2964ad456072edb7a527b4b12b8be35221 Mon Sep 17 00:00:00 2001 From: Formasitchijoh Date: Tue, 17 Dec 2024 23:35:40 +0100 Subject: [PATCH 22/66] updated comment from referencing ores to using liftwing api --- spec/features/assigned_articles_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/assigned_articles_spec.rb b/spec/features/assigned_articles_spec.rb index 410b5e036f..9f2822673c 100644 --- a/spec/features/assigned_articles_spec.rb +++ b/spec/features/assigned_articles_spec.rb @@ -17,7 +17,7 @@ end it 'lets users submit feedback about articles' do - # Since this makes a call to the ORES API from the server, + # This makes a call to the LiftWing API from the server, # we need to use VCR to avoid getting stopped by WebMock VCR.use_cassette('assigned_articles_view') do visit "/courses/#{course.slug}/articles/assigned" From a8b016daa6b0066770651458c27a70a1a111662a Mon Sep 17 00:00:00 2001 From: swayam-agrahari <109474492+swayam-agrahari@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:40:30 -0500 Subject: [PATCH 23/66] feat: Add visual indicator for closed courses (#6066) --- .../overview/admin_quick_actions.jsx | 82 ++++++++++++------- 1 file changed, 52 insertions(+), 30 deletions(-) diff --git a/app/assets/javascripts/components/overview/admin_quick_actions.jsx b/app/assets/javascripts/components/overview/admin_quick_actions.jsx index 24a09f4a63..548c7d9cf0 100644 --- a/app/assets/javascripts/components/overview/admin_quick_actions.jsx +++ b/app/assets/javascripts/components/overview/admin_quick_actions.jsx @@ -20,6 +20,8 @@ const DetailsText = ({ flags }) => (

    ); +const isCourseClosed = flags => !!(flags && flags.closed_date); + const NoDetailsText = () => (

    This course has not yet been marked as having been reviewed by a staff member. @@ -27,36 +29,56 @@ const NoDetailsText = () => (

    ); -export const AdminQuickActions = ({ course, current_user, persistCourse, greetStudents }) => ( -
    - {current_user.isStaff && ( - <> - {course.flags && course.flags.last_reviewed && course.flags.last_reviewed.username ? ( - - ) : ( - - )} - -
    -
    - -
    - - )} - {current_user.admin &&
    } -
    -); +export const AdminQuickActions = ({ course, current_user, persistCourse, greetStudents }) => { + const closedCourseStyle = isCourseClosed(course.flags) + ? { + backgroundColor: '#f8d7da', + color: '#721c24', + padding: '15px', + borderRadius: '2px', + fontWeight: 'bold', + } + : {}; + + return ( +
    + {isCourseClosed(course.flags) && ( +
    +

    + This course was closed on:  + {format(toDate(parseISO(course.flags.closed_date)), 'PPPP')}. +

    +
    + )} + {current_user.isStaff && ( + <> + {course.flags && course.flags.last_reviewed && course.flags.last_reviewed.username ? ( + + ) : ( + + )} + +
    +
    + +
    + + )} + {current_user.admin &&
    } +
    + ); +}; AdminQuickActions.propTypes = { course: PropTypes.shape({ From 83f294f46a5e48f80beaf4f1bb1cd737e9ef9a41 Mon Sep 17 00:00:00 2001 From: Abishek Das Date: Sat, 14 Dec 2024 14:32:02 +0530 Subject: [PATCH 24/66] Fix broken conditionals in cloned question groups by updating references and adding validations --- app/controllers/surveys_controller.rb | 36 ++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/app/controllers/surveys_controller.rb b/app/controllers/surveys_controller.rb index 8239a18549..d75c07fd59 100644 --- a/app/controllers/surveys_controller.rb +++ b/app/controllers/surveys_controller.rb @@ -119,9 +119,13 @@ def destroy end def clone_question_group - clone = Rapidfire::QuestionGroup.find(params[:id]).deep_clone include: [:questions] - clone.name = "#{clone.name} (Copy)" - clone.save + # Fetch the original question group and its questions + @original_group = Rapidfire::QuestionGroup.includes(:questions).find(params[:id]) + @clone_group = @original_group.deep_clone include: [:questions] + @clone_group.name = "#{@clone_group.name} (Copy)" + @clone_group.save + + update_cloned_conditionals redirect_to rapidfire.question_groups_path end @@ -216,5 +220,31 @@ def protect_confidentiality yield end + def update_cloned_conditionals # rubocop:disable Metrics/AbcSize + # Cache all questions related to the original group for fast lookup + original_questions = @original_group.questions.index_by(&:id) + + # Cache all cloned questions for fast lookup + cloned_questions = @clone_group.questions.index_by(&:position) + + @clone_group.questions.each do |question| + next unless question.conditionals.present? + + # Extract the original question ID from cloned conditionals question + original_question_id = question.conditionals.split('|').first.to_i + original_question = original_questions[original_question_id] + + # Skip if no matching original question and update cloned question conditionals to nil + next question.update(conditionals: nil) unless original_question.present? + + # Find the cloned equivalent of the original question + cloned_question = cloned_questions[original_question.position] + + # Update the conditionals with the cloned question's ID + updated_conditionals = question.conditionals.gsub(/\d+/, cloned_question.id.to_s) + question.update(conditionals: updated_conditionals) + end + end + class FailedSaveError < StandardError; end end From 7724ceca899c57f018eb58b7fc4e0dbe7ab85dba Mon Sep 17 00:00:00 2001 From: Abishek Das Date: Thu, 19 Dec 2024 15:29:28 +0530 Subject: [PATCH 25/66] Update test suite to validate conditional parameters for cloned question groups - Added robust tests to ensure conditional questions have valid conditional parameters after cloning a question group. - Fixed an issue in `SurveyAdmin.js` where CSRF token access was not using optional chaining `(?.)`, which could cause test failures due to unhandled `undefined` values. --- .../surveys/modules/SurveyAdmin.js | 2 +- spec/features/survey_admin_spec.rb | 92 +++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/surveys/modules/SurveyAdmin.js b/app/assets/javascripts/surveys/modules/SurveyAdmin.js index 69bf0f9701..adb9df986e 100644 --- a/app/assets/javascripts/surveys/modules/SurveyAdmin.js +++ b/app/assets/javascripts/surveys/modules/SurveyAdmin.js @@ -182,7 +182,7 @@ const SurveyAdmin = { credentials: 'include', headers: { 'Content-Type': 'application/json', - 'X-CSRF-Token': SurveyAdmin.$csrf_token.content + 'X-CSRF-Token': SurveyAdmin.$csrf_token?.content }, }).then(response => response.json()) .then((data) => { diff --git a/spec/features/survey_admin_spec.rb b/spec/features/survey_admin_spec.rb index b5bf690714..df32da4493 100644 --- a/spec/features/survey_admin_spec.rb +++ b/spec/features/survey_admin_spec.rb @@ -222,5 +222,97 @@ end expect(page).not_to have_content instructor.username end + + it 'correctly clones question groups with conditionals question', js: true do + # Create a base question group with conditional questions + + # Visit question groups page and create Question Group + visit 'surveys/rapidfire/question_groups' + click_link 'New Question Group' + fill_in('question_group_name', with: 'Conditional Questions Group') + page.find('input.button[value="Save Question Group"]').click + + # Create first question + click_link 'Edit' + omniclick(find('a.button', text: 'Add New Question')) + first_question_text = 'Do you like ice cream?' + find('textarea#question_text').set(first_question_text) + find('textarea#question_answer_options').set("Yes\nNo") + page.find('input.button').click + + # Create a conditional follow-up question + omniclick(find('a.button', text: 'Add New Question')) + follow_up_question_text = 'What is your favorite flavour?' + find('textarea#question_text').set(follow_up_question_text) + find('textarea#question_answer_options').set("Vanilla\nChocolate") + + # Set conditional logic + page.find('label', text: 'Conditionally show this question').click + + # Wait and verify the conditional elements are present + first_question = Rapidfire::Question.find_by(question_text: first_question_text) + + # Use a way to interact with the conditional elements + within('.survey__question__conditional-row') do + # Trigger the conditional select to populate options + page.find('select[data-conditional-select="true"]').click + + # Wait for and select the first question + option = page.find('select[data-conditional-select="true"] option', + text: first_question.question_text) + page.execute_script( + "arguments[0].selected = true; + arguments[0].parentNode.dispatchEvent(new Event('change'))", option.native + ) + + # Select the condition value + find('select[data-conditional-value-select=""]') + .select(first_question.answer_options[/^\s*Yes\b/]) + end + + # Verify the hidden input has been populated correctly + hidden_input = page.find('input[data-conditional-field-input="true"]', visible: false) + expect(hidden_input.value) + .to include("#{first_question.id}|=|#{first_question.answer_options[/^\s*Yes\b/]}|multi") + + page.find('input.button').click + + # Visit the question groups page to clone the newly created question group + visit 'surveys/rapidfire/question_groups' + + # Find the clone link for the newly created question group and click it to clone it + within("li#question_group_#{Rapidfire::QuestionGroup.last.id}") do + click_link 'Clone' + end + + # Find the Edit link for the cloned question group + within("li#question_group_#{Rapidfire::QuestionGroup.last.id}") do + click_link 'Edit' + end + + # Find the conditional question of the cloned question group and click edit + # If successfully cloned then there should be no error + within "tr[data-item-id=\"#{Rapidfire::Question.last.id}\"]" do + click_link 'Edit' + end + + # Verify manually to check if the cloned group exists + expect(Rapidfire::QuestionGroup.count).to eq(2) + cloned_group = Rapidfire::QuestionGroup.last + + # Verify questions were cloned + expect(cloned_group.questions.count).to eq(2) + + # Check conditional question + conditional_question = cloned_group.questions.detect { |q| q.question_text == follow_up_question_text } # rubocop:disable Layout/LineLength + expect(conditional_question).not_to be_nil + + # Verify the conditional logic points to the cloned first question + cloned_first_question = cloned_group.questions.detect do |q| + q.question_text == first_question_text + end + expect(conditional_question.conditionals) + .to eq("#{cloned_first_question.id}|=|#{cloned_first_question.answer_options[/^\s*Yes\b/]}|multi") # rubocop:disable Layout/LineLength + end end end From 1e3bd097908b8cc597504db3a36fd7accca7f5ff Mon Sep 17 00:00:00 2001 From: Abishek Das Date: Thu, 19 Dec 2024 17:45:08 +0530 Subject: [PATCH 26/66] Improved readability by renaming variables and comments for better context --- spec/features/survey_admin_spec.rb | 34 +++++++++++++++--------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/spec/features/survey_admin_spec.rb b/spec/features/survey_admin_spec.rb index df32da4493..7b3d5787fc 100644 --- a/spec/features/survey_admin_spec.rb +++ b/spec/features/survey_admin_spec.rb @@ -223,7 +223,7 @@ expect(page).not_to have_content instructor.username end - it 'correctly clones question groups with conditionals question', js: true do + it 'correctly clones question groups with conditional questions', js: true do # Create a base question group with conditional questions # Visit question groups page and create Question Group @@ -232,7 +232,7 @@ fill_in('question_group_name', with: 'Conditional Questions Group') page.find('input.button[value="Save Question Group"]').click - # Create first question + # Create the first question click_link 'Edit' omniclick(find('a.button', text: 'Add New Question')) first_question_text = 'Do you like ice cream?' @@ -242,7 +242,7 @@ # Create a conditional follow-up question omniclick(find('a.button', text: 'Add New Question')) - follow_up_question_text = 'What is your favorite flavour?' + follow_up_question_text = 'What is your favorite flavor?' find('textarea#question_text').set(follow_up_question_text) find('textarea#question_answer_options').set("Vanilla\nChocolate") @@ -250,16 +250,16 @@ page.find('label', text: 'Conditionally show this question').click # Wait and verify the conditional elements are present - first_question = Rapidfire::Question.find_by(question_text: first_question_text) + first_question_record = Rapidfire::Question.find_by(question_text: first_question_text) - # Use a way to interact with the conditional elements + # Interact with conditional elements within('.survey__question__conditional-row') do # Trigger the conditional select to populate options page.find('select[data-conditional-select="true"]').click # Wait for and select the first question option = page.find('select[data-conditional-select="true"] option', - text: first_question.question_text) + text: first_question_record.question_text) page.execute_script( "arguments[0].selected = true; arguments[0].parentNode.dispatchEvent(new Event('change'))", option.native @@ -267,31 +267,31 @@ # Select the condition value find('select[data-conditional-value-select=""]') - .select(first_question.answer_options[/^\s*Yes\b/]) + .select(first_question_record.answer_options[/^\s*Yes\b/]) end # Verify the hidden input has been populated correctly hidden_input = page.find('input[data-conditional-field-input="true"]', visible: false) - expect(hidden_input.value) - .to include("#{first_question.id}|=|#{first_question.answer_options[/^\s*Yes\b/]}|multi") + # rubocop:disable Layout/LineLength,Lint/MissingCopEnableDirective + expected_conditionals = "#{first_question_record.id}|=|#{first_question_record.answer_options[/^\s*Yes\b/]}|multi" + expect(hidden_input.value).to include(expected_conditionals) page.find('input.button').click # Visit the question groups page to clone the newly created question group visit 'surveys/rapidfire/question_groups' - # Find the clone link for the newly created question group and click it to clone it + # Find and click the clone link for the newly created question group within("li#question_group_#{Rapidfire::QuestionGroup.last.id}") do click_link 'Clone' end - # Find the Edit link for the cloned question group + # Edit the cloned question group within("li#question_group_#{Rapidfire::QuestionGroup.last.id}") do click_link 'Edit' end - # Find the conditional question of the cloned question group and click edit - # If successfully cloned then there should be no error + # Edit the conditional question of the cloned group within "tr[data-item-id=\"#{Rapidfire::Question.last.id}\"]" do click_link 'Edit' end @@ -303,16 +303,16 @@ # Verify questions were cloned expect(cloned_group.questions.count).to eq(2) - # Check conditional question - conditional_question = cloned_group.questions.detect { |q| q.question_text == follow_up_question_text } # rubocop:disable Layout/LineLength + # Check the conditional question + conditional_question = cloned_group.questions.detect { |q| q.question_text == follow_up_question_text } expect(conditional_question).not_to be_nil # Verify the conditional logic points to the cloned first question cloned_first_question = cloned_group.questions.detect do |q| q.question_text == first_question_text end - expect(conditional_question.conditionals) - .to eq("#{cloned_first_question.id}|=|#{cloned_first_question.answer_options[/^\s*Yes\b/]}|multi") # rubocop:disable Layout/LineLength + expected_cloned_conditionals = "#{cloned_first_question.id}|=|#{cloned_first_question.answer_options[/^\s*Yes\b/]}|multi" + expect(conditional_question.conditionals).to eq(expected_cloned_conditionals) end end end From 4856f0b3e04d256f00a7bbe75a90e3502fed0e56 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 19 Dec 2024 13:20:31 +0100 Subject: [PATCH 27/66] Localisation updates from https://translatewiki.net. --- config/locales/ar.yml | 2 +- config/locales/fr.yml | 4 +++- config/locales/hi.yml | 1 + config/locales/hy.yml | 1 + config/locales/pa.yml | 1 + config/locales/sr-ec.yml | 2 +- config/locales/tr.yml | 36 ++++++++++++++++++++++++------------ 7 files changed, 32 insertions(+), 15 deletions(-) diff --git a/config/locales/ar.yml b/config/locales/ar.yml index 4a7a7d78de..23338293c9 100644 --- a/config/locales/ar.yml +++ b/config/locales/ar.yml @@ -984,7 +984,7 @@ ar: user_uploads_none: لم يساهم المستخدمون المختارون بأية صور أو ملفات وسائط أخرى في هذا المشروع. view_other: عرض حملات أخرى - view_page: عرض صفحة البرنامج + view_page: عرض الصفحة word_count_doc: تقدير عدد الكلمات التي تمت إضافتها إلى مقالات النطاق الرئيسي من قبل طلاب البرنامج أثناء فترة البرنامج word_count_doc_wikidata: تقدير لعدد الكلمات المضافة إلى عناصر المساحة الرئيسية diff --git a/config/locales/fr.yml b/config/locales/fr.yml index b8a89103d7..3d6c23c6dd 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -551,6 +551,8 @@ fr: d’affectation. Affecter les modules de formation est une part essentielle des meilleures pratiques du Wiki Éducation. alerts: Alertes + does_not_exist: L'élément n'existe pas. Créez une page Sandbox et affectez-vous-y + pour recevoir des suggestions. all_courses: Tous les cours already_enrolled: Vous faites déjà partie de « %{title} » ! already_exists: Cela existe déjà pour ce cours ! @@ -1173,7 +1175,7 @@ fr: user_uploads_none: Les utilisateurs sélectionnés n’ont contribué à ce projet avec aucune image ni aucun autre fichier média. view_other: Afficher les autres campagnes - view_page: Afficher la page du programme + view_page: Afficher la page word_count_doc: Une estimation du nombre de mots ajoutés aux articles de l’espace principal par les rédacteurs du programme durant son déroulement word_count_doc_wikidata: Une estimation du nombre de mots ajoutés aux articles diff --git a/config/locales/hi.yml b/config/locales/hi.yml index 138180f398..f862094a76 100644 --- a/config/locales/hi.yml +++ b/config/locales/hi.yml @@ -379,6 +379,7 @@ hi: school: संस्थान student_editors: सम्पादक students_short: संपादक + view_page: पृष्ठ देखें editable: edit: सम्पादन cancel: रद्द करें diff --git a/config/locales/hy.yml b/config/locales/hy.yml index c45772052e..b3ee847964 100644 --- a/config/locales/hy.yml +++ b/config/locales/hy.yml @@ -289,6 +289,7 @@ hy: term: Երբ uploads_none: Այս նախագծի շրջանակներում ոչ մի պատկեր կամ մեդիա նիշք չի բեռնվել Վիքիպահեստ։ + view_page: Դիտել էջը editable: edit: Խմբագրել cancel: Չեղարկել diff --git a/config/locales/pa.yml b/config/locales/pa.yml index 3550727247..20332fbe93 100644 --- a/config/locales/pa.yml +++ b/config/locales/pa.yml @@ -192,6 +192,7 @@ pa: namespace: article: ਲੇਖ mediaWiki: ਮੀਡੀਆਵਿਕੀ + template: ਫਰਮਾ help: ਮਦਦ translation: ਤਰਜਮਾ draft: ਖਰੜਾ diff --git a/config/locales/sr-ec.yml b/config/locales/sr-ec.yml index 918267ad45..34ecafa6b2 100644 --- a/config/locales/sr-ec.yml +++ b/config/locales/sr-ec.yml @@ -764,7 +764,7 @@ sr-ec: user_uploads_none: Одабрани уредници нису отпремили никакве фотографије или друге медијске датотеке у овај пројекат. view_other: Погледај остале кампање - view_page: Погледај страницу програма + view_page: Види страницу word_count_doc: Процена броја речи које су уредници програма додали у чланке из главног именског простора током трајања програма yourcourses: Твоји програми diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 21d36e52fe..87394b4edb 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -14,6 +14,7 @@ # Author: Hedda # Author: Incelemeelemani # Author: JackUKElliott +# Author: Jelican9 # Author: Joseph # Author: Katpatuka # Author: KorkmazO @@ -47,6 +48,7 @@ tr: number_field: Bu bir numara alanı. Çoğu tarayıcı tarafından oluşturulan butonlar, girişi artırır ve azaltır. application: + back: Geri cancel: İptal cookie_consent: Bu site oturumunuzu açık tutmak için çerezleri kullanır ve Vikipedi hesabınızla ilgili bilgileri saklar. @@ -80,6 +82,8 @@ tr: one: '%{count} Sonucu: ''%{search_terms}''' other: '%{count} Sonucu: ''%{search_terms}''' no_results: '''%{query}'' için sonuç bulunamadı' + show_current_revision: Mevcut sürümü göster + show_last_revision: Son düzenlenmiş versiyonu göster sign_up_extended: Vikipedi ile Kaydol sign_up_wikipedia: Veya Vikipedi'ye kaydol sign_up_log_in_extended: Vikipedi ile @@ -93,6 +97,7 @@ tr: opt_out: Hayır opt_yes: evet opt_no: hayır + print: Yazdır article_statuses: not_yet_started: Başlangıç in_progress: Deneme Tahtası Üzerinde Çalışıyor @@ -132,9 +137,17 @@ tr: explore: Keşfet log_in: Vikipedi ile Oturum Aç sign_up: Vikipedi ile Kaydol + registration_advice: + username_rules: + heading: Kullanıcı adı kuralları + avoid_offensive_usernames: Saldırgan, yanıltıcı, rahatsız edici veya reklam + amaçlı bir kullanıcı adı seçmeyin. + represent_individual: Kullanıcı adları bireysel bir editörü temsil etmelidir. + Bir okul veya kuruluşun adını kullanmayın. sign_up_help: Gösterge panelini kullanmak için Vikipedi ile oturum açmanız ve hesabınıza erişme izni vermeniz gerekir. Var olan bir Vikipedi hesabıyla giriş - yapabilir veya yeni bir Vikipedi hesabı oluşturabilirsiniz. + yapabilir veya yeni bir Vikipedi hesabı oluşturabilirsiniz. Bir hesap oluşturamıyorsanız, + dashboard@wikiedu.org ile iletişime geçiniz. alerts: alert_label: Uyarılar no_alerts: Uyarı bulunamadı. @@ -383,10 +396,10 @@ tr: add_psid: PetScan PSID Ekle add_pileid: PagePile kimliği ekle add_template: Şablon ekle - add_this_category: Bu kategoriyi ekle - add_this_psid: Bu PSID'yi ekle - add_this_pileid: Bunu PagePile kimliğini ekle - add_this_template: Bu şablonu ekle + add_this_category: Kateogileri ekle + add_this_psid: PetScan PSID'lerini ekle + add_this_pileid: PagePile id'lerini ekle + add_this_template: Şablonları ekle import_articles_description: Bir kategorideki makalelerin listesi düzenli aralıklarla güncellenir ve listenin eklenmesinden önce eklenmesinden sonra biraz zaman alabilir. depth: Alt kategori derinliği @@ -936,8 +949,7 @@ tr: o kadar iyidir. thank_you: Teşekkür ederim! Başka sorularınız varsa Viki Uzmanınıza başvurun. exercises_and_trainings: '%{prefix} Egzersizler ve Eğitimler' - no_assignments: Şu anda bu öğrencinin atanmış maddesi yok. Yukarıdaki düğmeleri - kullanarak madde atayabilirsiniz. + no_assignments: Şu anda bu öğrencinin atanmış maddesi yok. overview: '%{prefix} Genel Bakış' reviewing_articles: Makaleleri Gözden Geçirme reviewing_articles_wikidata: İncelenen Ögeler @@ -989,9 +1001,9 @@ tr: oldu ve %{error_count} hata ile karşılaştı. revisions: Son Düzenlemeler references_count: Referanslar Eklendi - references_doc: Bu makalelere eklenen referans etiketi sayısıdır ve aynı kaynağa - birden fazla referans içerebilir. Veriler ORES ürün kalitesi modelinden gelmektedir - ve sadece bazı diller için mevcuttur. + references_doc: Bu makalelere eklenen referans etiketlerinin ve kısaltılmış dipnot + şablonlarının sayısıdır ve aynı kaynağa ait birden fazla referans içerebilir. + Veriler referans sayacı Toolforge API'sinden gelmektedir. replag_info: Gösterge Tablosu, düzenleme verilerini WMF Bulut çoğaltma veritabanlarından alır. Bazen bu veritabanları, çoğaltma gecikmesi nedeniyle en son düzenlemeleri kaçırır. Bu durumda, eksik düzenlemeler daha sonra görünecektir. @@ -1053,8 +1065,8 @@ tr: diff: fark diff_show: Göster diff_hide: Gizle - edit_time_span: Düzenlemeler %{first_time} tarihinden %{last_time} tarihine kadar - gerçekleşti + edit_time_span: 'Düzenlemeler: %{first_time} tarihinden %{last_time} tarihine + kadar gerçekleşti' edited_by: Düzenleyen edited_on: '%{edit_date} tarihinde düzenlendi' loading: Sürümler yükleniyor... From e85116cd7ec7d11ab16625cadf3adfab730b8be1 Mon Sep 17 00:00:00 2001 From: Sage Ross Date: Thu, 19 Dec 2024 10:47:49 -0800 Subject: [PATCH 28/66] Quick fix for plagiarism alerts with no Article record If there's an alert for an article that hasn't yet showed up in the Dashboard's Article table, this field can be empty on the Alert record. --- app/mailers/suspected_plagiarism_mailer.rb | 2 +- .../suspected_plagiarism_mailer/content_expert_email.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/mailers/suspected_plagiarism_mailer.rb b/app/mailers/suspected_plagiarism_mailer.rb index 8beb463022..da553d29ac 100644 --- a/app/mailers/suspected_plagiarism_mailer.rb +++ b/app/mailers/suspected_plagiarism_mailer.rb @@ -14,7 +14,7 @@ def content_expert_email(alert, content_experts) @diff_url = alert.diff_url @user = alert.user @article = alert.article - @article_url = @article.url + @article_url = @article&.url @courses_user = @user.courses_users.last @course = alert.course @talk_page_new_section_url = @courses_user.talk_page_url + '?action=edit§ion=new' diff --git a/app/views/suspected_plagiarism_mailer/content_expert_email.html.haml b/app/views/suspected_plagiarism_mailer/content_expert_email.html.haml index de543e09ac..12ac7abf26 100644 --- a/app/views/suspected_plagiarism_mailer/content_expert_email.html.haml +++ b/app/views/suspected_plagiarism_mailer/content_expert_email.html.haml @@ -17,7 +17,7 @@ made #{link_to 'this change', @diff_url, class: 'link'} to the article - "#{link_to @article.full_title, @article_url, class: 'link'}", which Turnitin/iThenticate matched to published + "#{link_to @article&.full_title, @article_url, class: 'link'}", which Turnitin/iThenticate matched to published text. %p.paragraph These reports are not 100% accurate. For example, a direct quote with proper attribution From c16d5f8df39a183e238afd6814d262dbfac55458 Mon Sep 17 00:00:00 2001 From: Sage Ross Date: Thu, 19 Dec 2024 11:26:57 -0800 Subject: [PATCH 29/66] Check Salesforce for closing timestamp as soon as courses end We sometimes close courses before they reach the 'archived' threshold, so we should start syncing that bit of Salesforce data without waiting for the update window to end. --- app/workers/daily_update/salesforce_sync_worker.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/workers/daily_update/salesforce_sync_worker.rb b/app/workers/daily_update/salesforce_sync_worker.rb index a3d0d598b3..48a457f491 100644 --- a/app/workers/daily_update/salesforce_sync_worker.rb +++ b/app/workers/daily_update/salesforce_sync_worker.rb @@ -11,7 +11,7 @@ def perform PushCourseToSalesforce.new(course) end ClassroomProgramCourse - .archived + .ended .where(withdrawn: false) .reject(&:closed?) .select(&:approved?).each do |course| From 2f658ef93a4caf91da32fd6efebbf04cdd5f8186 Mon Sep 17 00:00:00 2001 From: Sage Ross Date: Thu, 19 Dec 2024 12:11:33 -0800 Subject: [PATCH 30/66] Don't do full update for a manual_update We can also mark a course as needing a full update if that's necessary, but in most cases, I don't want to redo work, I just want to quickly get an update processed. --- app/controllers/courses_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index c71f71582f..436557d183 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -177,7 +177,7 @@ def tag def manual_update require_super_admin_permissions @course = find_course_by_slug(params[:id]) - UpdateCourseStats.new(@course, full: true) + UpdateCourseStats.new(@course) redirect_to "/courses/#{@course.slug}" end From 317209303332fc722a20c9a83b33c1b634983535 Mon Sep 17 00:00:00 2001 From: Formasitchijoh Date: Thu, 19 Dec 2024 21:18:29 +0100 Subject: [PATCH 31/66] fix:increase Capybara default wait time to resolve category_scopes_specs failures --- spec/rails_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index c54f21ab90..a89ea159a5 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -26,7 +26,7 @@ Capybara::Screenshot.prune_strategy = :keep_last_run Capybara.save_path = 'tmp/screenshots/' Capybara.server = :puma, { Silent: true } - +Capybara.default_max_wait_time = 10 # Requires supporting ruby files with custom matchers and macros, etc, in # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are # run as spec files by default. This means that files in spec/support that end From da90e459ba663846d182b83c4a113da01dc48056 Mon Sep 17 00:00:00 2001 From: Formasitchijoh Date: Thu, 19 Dec 2024 21:21:06 +0100 Subject: [PATCH 32/66] fix: use CampaignList index to ensure unique keys for course campaigns --- app/assets/javascripts/components/overview/campaign_list.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/components/overview/campaign_list.jsx b/app/assets/javascripts/components/overview/campaign_list.jsx index bf6baaecce..ad05b32e24 100644 --- a/app/assets/javascripts/components/overview/campaign_list.jsx +++ b/app/assets/javascripts/components/overview/campaign_list.jsx @@ -11,7 +11,7 @@ const CampaignList = ({ campaigns, course }) => { let comma = ''; const url = `/campaigns/${campaign.slug}`; if (index !== lastIndex) { comma = ', '; } - return {campaign.title}{comma}; + return {campaign.title}{comma}; }) : I18n.t('courses.none')); From e43a8f240d5426940ba102c26d3be8c36f1ab702 Mon Sep 17 00:00:00 2001 From: Formasitchijoh Date: Thu, 19 Dec 2024 22:40:50 +0100 Subject: [PATCH 33/66] fix: handle undefined 'valid' and 'search' values in course_reator page --- .../components/course_creator/course_creator.jsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/components/course_creator/course_creator.jsx b/app/assets/javascripts/components/course_creator/course_creator.jsx index 1b7593ea37..fda1d2c5d9 100644 --- a/app/assets/javascripts/components/course_creator/course_creator.jsx +++ b/app/assets/javascripts/components/course_creator/course_creator.jsx @@ -114,7 +114,7 @@ const CourseCreator = createReactClass({ campaignParam() { // The regex allows for any number of URL parameters, while only capturing the campaign_slug parameter - const campaignParam = window.location.search.match(/\?.*?campaign_slug=(.*?)(?:$|&)/); + const campaignParam = window.location?.search?.match(/\?.*?campaign_slug=(.*?)(?:$|&)/); if (campaignParam) { return campaignParam[1]; } @@ -162,8 +162,11 @@ const CourseCreator = createReactClass({ cleanedCourse.scoping_methods = getScopingMethods(this.props.scopingMethods); this.props.submitCourse({ course: cleanedCourse }, onSaveFailure); } - } else if (!this.props.validations.exists.valid) { - this.setState({ isSubmitting: false }); + } else { + const existsValidation = this.props.validations?.exists?.valid; + if (existsValidation === false) { + this.setState({ isSubmitting: false }); + } } }, From c2623bf7a06489588ae02d55ed19e2934033ef55 Mon Sep 17 00:00:00 2001 From: Swayam Agrahari Date: Fri, 20 Dec 2024 17:54:54 +0530 Subject: [PATCH 34/66] add visual indicator for campaign view --- app/assets/stylesheets/modules/_tables.styl | 4 ++++ app/helpers/course_helper.rb | 6 ++++++ app/views/campaigns/_course_row.html.haml | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/modules/_tables.styl b/app/assets/stylesheets/modules/_tables.styl index 0e32965fbd..168592b2a7 100644 --- a/app/assets/stylesheets/modules/_tables.styl +++ b/app/assets/stylesheets/modules/_tables.styl @@ -216,6 +216,10 @@ .table > tbody > .table-row--faded background-color #fafafa +.table > tbody > .table-row--closed + background-color: #f8d7da; + color: #721c24; + font-weight: bold; // Align buttons/text to the right of the action cell diff --git a/app/helpers/course_helper.rb b/app/helpers/course_helper.rb index 231cf9740a..f742316426 100644 --- a/app/helpers/course_helper.rb +++ b/app/helpers/course_helper.rb @@ -50,4 +50,10 @@ def format_wiki_namespaces(wiki_namespaces) "#{wiki_domain}-namespace-#{namespace}" end end + + def closed_course_class(course) + return 'table-row--closed' if course.flags&.dig(:closed_date) + '' +end + end diff --git a/app/views/campaigns/_course_row.html.haml b/app/views/campaigns/_course_row.html.haml index 0657a1b1d0..c53cfe47cf 100644 --- a/app/views/campaigns/_course_row.html.haml +++ b/app/views/campaigns/_course_row.html.haml @@ -1,4 +1,4 @@ -%tr{:class => "#{user ? date_highlight_class(course) : private_highlight_class(course)}", "data-link" => "#{course_slug_path(course.slug)}"} +%tr{:class => "#{user ? date_highlight_class(course) : private_highlight_class(course)} #{closed_course_class(course)}", "data-link" => "#{course_slug_path(course.slug)}"} %td{:class => "table-link-cell", :role => "button", :tabindex => "0"} %a.course-link{:href => "#{course_slug_path(course.slug)}"} %span.title From a7eaabcada31c1d3783a7a7c647578da133c7d58 Mon Sep 17 00:00:00 2001 From: Abishek Das Date: Wed, 11 Dec 2024 15:14:30 +0530 Subject: [PATCH 35/66] feat: enhance WeekdayPicker accessibility with aria-label and aria-live support - Add `aria-label` and `aria-live` for improved screen reader compatibility. - Add CSS styling for the `aria-live` region to ensure proper visibility and user experience. - remove aria-pressed --- .../components/common/weekday_picker.jsx | 22 ++++++++++++++----- app/assets/stylesheets/modules/_calendar.styl | 12 ++++++++++ config/locales/en.yml | 11 ++++++++-- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/components/common/weekday_picker.jsx b/app/assets/javascripts/components/common/weekday_picker.jsx index 56f352e667..93e48f4dd5 100644 --- a/app/assets/javascripts/components/common/weekday_picker.jsx +++ b/app/assets/javascripts/components/common/weekday_picker.jsx @@ -25,7 +25,6 @@ const WeekdayPicker = ({ style, tabIndex, - ariaModifier, modifiers, locale, @@ -145,7 +144,7 @@ const WeekdayPicker = ({ dayClassName += customModifiers.map(modifier => ` ${dayClassName}--${modifier}`).join(''); - const ariaSelected = customModifiers.indexOf(ariaModifier) > -1; + const ariaSelected = customModifiers.indexOf('selected') > -1; let tabIndexValue = null; if (onWeekdayClick) { @@ -159,10 +158,20 @@ const WeekdayPicker = ({ const onMouseEnterHandler = onWeekdayMouseEnter ? e => handleWeekdayMouseEnter(e, weekday, customModifiers) : null; const onMouseLeaveHandler = onWeekdayMouseLeave ? e => handleWeekdayMouseLeave(e, weekday, customModifiers) : null; + const ariaLabelMessage = ariaSelected + ? I18n.t('weekday_picker.aria.weekday_selected', { weekday: localeUtils.formatWeekdayLong(weekday), }) + : I18n.t('weekday_picker.aria.weekday_select', { weekday: localeUtils.formatWeekdayLong(weekday), }); + + const ariaLiveMessage = ariaSelected + ? I18n.t('weekday_picker.aria.weekday_selected', { weekday: localeUtils.formatWeekdayLong(weekday), }) + : I18n.t('weekday_picker.aria.weekday_unselected', { weekday: localeUtils.formatWeekdayLong(weekday), }); + return ( ); }; @@ -201,7 +214,6 @@ WeekdayPicker.propTypes = { style: PropTypes.object, tabIndex: PropTypes.number, - ariaModifier: PropTypes.string, modifiers: PropTypes.object, locale: PropTypes.string, diff --git a/app/assets/stylesheets/modules/_calendar.styl b/app/assets/stylesheets/modules/_calendar.styl index 1182b48b4f..67e1711531 100644 --- a/app/assets/stylesheets/modules/_calendar.styl +++ b/app/assets/stylesheets/modules/_calendar.styl @@ -172,3 +172,15 @@ .DayPicker--ar direction rtl + +// CSS for screen reader support for weekday picker +.sr-WeekdayPicker-aria-live { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} diff --git a/config/locales/en.yml b/config/locales/en.yml index 590544a53c..ba7dd1307b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1870,9 +1870,16 @@ en: note_edit_cancel_button_focused: "Cancel note edit {{title}}" note_delete_button_focused: "Delete the note {{title}}" note_edit_mode: "Entered Edit mode for note titled: {{title}}. You can now edit the title and content." - + customize_error_message: JSONP_request_failed: "The request to fetch data from Wikipedia has timed out. This may be due to a slow internet connection or temporary server issues. Please try again later." tagged_courses: - download_csv: 'Download CSV' \ No newline at end of file + download_csv: 'Download CSV' + + weekday_picker: + aria: + weekday_select: "{{weekday}} Press Return key to select" + weekday_selected: "{{weekday}} Selected Press Return Key to unselect" + weekday_unselected: "{{weekday}} Unselected" + \ No newline at end of file From 5223d0d94e891031378aff0b36c30331870581de Mon Sep 17 00:00:00 2001 From: Swayam Agrahari Date: Fri, 20 Dec 2024 18:43:01 +0530 Subject: [PATCH 36/66] fixed some linting issue --- app/helpers/course_helper.rb | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/helpers/course_helper.rb b/app/helpers/course_helper.rb index f742316426..ae09ea05d7 100644 --- a/app/helpers/course_helper.rb +++ b/app/helpers/course_helper.rb @@ -51,9 +51,8 @@ def format_wiki_namespaces(wiki_namespaces) end end - def closed_course_class(course) - return 'table-row--closed' if course.flags&.dig(:closed_date) - '' -end - + def closed_course_class(course) + return 'table-row--closed' if course.flags&.dig(:closed_date) + return '' + end end From 759e1fc3304b68f2e40a77a2f81008ccce73fff2 Mon Sep 17 00:00:00 2001 From: Sage Ross Date: Fri, 20 Dec 2024 09:40:37 -0800 Subject: [PATCH 37/66] Remove fetchUserRevisions from StudentExercise --- .../ExercisesList/StudentExercise/StudentExercise.jsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/assets/javascripts/components/students/components/Articles/SelectedStudent/ExercisesList/StudentExercise/StudentExercise.jsx b/app/assets/javascripts/components/students/components/Articles/SelectedStudent/ExercisesList/StudentExercise/StudentExercise.jsx index 75bd162208..aa1462281d 100644 --- a/app/assets/javascripts/components/students/components/Articles/SelectedStudent/ExercisesList/StudentExercise/StudentExercise.jsx +++ b/app/assets/javascripts/components/students/components/Articles/SelectedStudent/ExercisesList/StudentExercise/StudentExercise.jsx @@ -3,7 +3,6 @@ import createReactClass from 'create-react-class'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { setUploadFilters } from '~/app/assets/javascripts/actions/uploads_actions'; -import { fetchUserRevisions } from '~/app/assets/javascripts/actions/user_revisions_actions'; import { fetchTrainingStatus } from '~/app/assets/javascripts/actions/training_status_actions'; // Components @@ -23,7 +22,6 @@ const Student = createReactClass({ course: PropTypes.object.isRequired, current_user: PropTypes.object, editable: PropTypes.bool, - fetchUserRevisions: PropTypes.func.isRequired, fetchTrainingStatus: PropTypes.func.isRequired, isOpen: PropTypes.bool, minimalView: PropTypes.bool, @@ -43,7 +41,6 @@ const Student = createReactClass({ openDrawer() { if (!this.props.isOpen) { const { course, student } = this.props; - this.props.fetchUserRevisions(course.id, student.id); this.props.fetchTrainingStatus(student.id, course.id); this.props.fetchExercises(course.id, student.id); } @@ -75,7 +72,6 @@ const Student = createReactClass({ const mapDispatchToProps = { setUploadFilters, - fetchUserRevisions, fetchTrainingStatus, fetchExercises: fetchTrainingModuleExercisesByUser }; From ff904ab279859d1c316523295d63d9424728e046 Mon Sep 17 00:00:00 2001 From: Sage Ross Date: Fri, 20 Dec 2024 09:48:46 -0800 Subject: [PATCH 38/66] Remove unused prop --- .../components/students/shared/StudentList/Student/Student.jsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/assets/javascripts/components/students/shared/StudentList/Student/Student.jsx b/app/assets/javascripts/components/students/shared/StudentList/Student/Student.jsx index 30f01e96fa..62f36d50ec 100644 --- a/app/assets/javascripts/components/students/shared/StudentList/Student/Student.jsx +++ b/app/assets/javascripts/components/students/shared/StudentList/Student/Student.jsx @@ -6,7 +6,6 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import AssignCell from '@components/common/AssignCell/AssignCell.jsx'; import { setUploadFilters } from '~/app/assets/javascripts/actions/uploads_actions'; -import { fetchUserRevisions } from '~/app/assets/javascripts/actions/user_revisions_actions'; import { fetchTrainingStatus } from '~/app/assets/javascripts/actions/training_status_actions'; import { groupByAssignmentType } from '@components/util/helpers'; import { ASSIGNED_ROLE, REVIEWING_ROLE } from '@constants/assignments'; @@ -30,7 +29,6 @@ const Student = createReactClass({ course: PropTypes.object.isRequired, current_user: PropTypes.object, editable: PropTypes.bool, - fetchUserRevisions: PropTypes.func.isRequired, fetchTrainingStatus: PropTypes.func.isRequired, minimalView: PropTypes.bool, student: PropTypes.object.isRequired, @@ -154,7 +152,6 @@ const Student = createReactClass({ const mapDispatchToProps = { setUploadFilters, - fetchUserRevisions, fetchTrainingStatus, fetchExercises: fetchTrainingModuleExercisesByUser }; From 00de9de10d03e6f3f778a9d4cbb5bc64aa596a7a Mon Sep 17 00:00:00 2001 From: Sage Ross Date: Fri, 20 Dec 2024 10:52:09 -0800 Subject: [PATCH 39/66] Get recent revisions from wiki instead Dashboard --- .../actions/user_revisions_actions.js | 26 +++++-------------- .../RevisionsList/StudentRevisionsList.jsx | 8 +++--- .../StudentDrawer/Contributions.jsx | 2 -- .../StudentList/StudentDrawer/RevisionRow.jsx | 15 +++++------ .../javascripts/reducers/user_revisions.js | 15 +++++++---- .../utils/mediawiki_revisions_utils.js | 16 ++++++++++++ app/assets/javascripts/utils/wiki_utils.js | 6 ++++- 7 files changed, 48 insertions(+), 40 deletions(-) diff --git a/app/assets/javascripts/actions/user_revisions_actions.js b/app/assets/javascripts/actions/user_revisions_actions.js index 414a27df4b..689f67dc1c 100644 --- a/app/assets/javascripts/actions/user_revisions_actions.js +++ b/app/assets/javascripts/actions/user_revisions_actions.js @@ -1,30 +1,18 @@ import { RECEIVE_USER_REVISIONS, API_FAIL } from '../constants'; -import logErrorMessage from '../utils/log_error_message'; -import request from '../utils/request'; +import { fetchLatestRevisionsForUser } from '../utils/mediawiki_revisions_utils'; - -const fetchUserRevisionsPromise = async (courseId, userId) => { - const response = await request(`/revisions.json?user_id=${userId}&course_id=${courseId}`); - if (!response.ok) { - logErrorMessage(response); - const data = await response.text(); - response.responseText = data; - throw response; - } - return response.json(); -}; - -export const fetchUserRevisions = (courseId, userId) => (dispatch, getState) => { +export const fetchUserRevisions = (course, user) => (dispatch, getState) => { // Don't refetch a user's revisions if they are already in the store. - if (getState().userRevisions[userId]) { return; } + if (getState().userRevisions[user.username]) { return; } return ( - fetchUserRevisionsPromise(courseId, userId) + fetchLatestRevisionsForUser(user.username, course.home_wiki) .then((resp) => { dispatch({ type: RECEIVE_USER_REVISIONS, - data: resp, - userId + revisions: resp, + username: user.username, + wiki: course.home_wiki }); }) .catch(response => (dispatch({ type: API_FAIL, data: response }))) diff --git a/app/assets/javascripts/components/students/components/Articles/SelectedStudent/RevisionsList/StudentRevisionsList.jsx b/app/assets/javascripts/components/students/components/Articles/SelectedStudent/RevisionsList/StudentRevisionsList.jsx index b1b822f90f..aee9d05630 100644 --- a/app/assets/javascripts/components/students/components/Articles/SelectedStudent/RevisionsList/StudentRevisionsList.jsx +++ b/app/assets/javascripts/components/students/components/Articles/SelectedStudent/RevisionsList/StudentRevisionsList.jsx @@ -27,10 +27,10 @@ export const StudentRevisionsList = ({ course, student, wikidataLabels, userRevi // filter the revisions according to namespace const getfilteredRevisions = () => { let revisions = []; - if (userRevisions[student.id] !== undefined && userRevisions[student.id] !== null) { + if (userRevisions[student.username] !== undefined && userRevisions[student.username] !== null) { revisions = (namespace === 'all') - ? userRevisions[student.id] - : userRevisions[student.id].filter((rev) => { + ? userRevisions[student.username] + : userRevisions[student.username].filter((rev) => { const current_ns_id = ArticleUtils.getNamespaceId(namespace); return rev.article.namespace === current_ns_id; }); @@ -43,7 +43,7 @@ export const StudentRevisionsList = ({ course, student, wikidataLabels, userRevi }; - if (!userRevisions[student.id]) dispatch(fetchUserRevisions(course.id, student.id)); + if (!userRevisions[student.id]) dispatch(fetchUserRevisions(course, student)); const filteredRevisions = getfilteredRevisions(); const uploadsLink = `/courses/${course.slug}/uploads`; const elements = [ diff --git a/app/assets/javascripts/components/students/shared/StudentList/StudentDrawer/Contributions.jsx b/app/assets/javascripts/components/students/shared/StudentList/StudentDrawer/Contributions.jsx index 1730fe448c..035340b3a2 100644 --- a/app/assets/javascripts/components/students/shared/StudentList/StudentDrawer/Contributions.jsx +++ b/app/assets/javascripts/components/students/shared/StudentList/StudentDrawer/Contributions.jsx @@ -31,8 +31,6 @@ export const Contributions = ({ course, revisions, selectedIndex, student, wikid {I18n.t('users.contributions')} {I18n.t('metrics.date_time')} {I18n.t('metrics.char_added')} - {I18n.t('metrics.references_count')} - {I18n.t('metrics.view')} diff --git a/app/assets/javascripts/components/students/shared/StudentList/StudentDrawer/RevisionRow.jsx b/app/assets/javascripts/components/students/shared/StudentList/StudentDrawer/RevisionRow.jsx index c901ca3e63..cee6a4b436 100644 --- a/app/assets/javascripts/components/students/shared/StudentList/StudentDrawer/RevisionRow.jsx +++ b/app/assets/javascripts/components/students/shared/StudentList/StudentDrawer/RevisionRow.jsx @@ -7,26 +7,23 @@ import DiffViewer from '@components/revisions/diff_viewer.jsx'; // Helpers import CourseUtils from '~/app/assets/javascripts/utils/course_utils'; import { formatDateWithTime } from '../../../../../utils/date_utils'; +import { getArticleUrl, getDiffUrl } from '../../../../../utils/wiki_utils'; export const RevisionRow = ({ course, index, revision, revisions, selectedIndex, student, wikidataLabels, showDiff }) => { const article = revision.article; const label = wikidataLabels[article.title]; const formattedTitle = CourseUtils.formattedArticleTitle(article, course.home_wiki, label); - const details = I18n.t('users.revision_characters_and_views', { characters: revision.characters, views: revision.views }); - const revisionDate = formatDateWithTime(revision.date); + const articleUrl = getArticleUrl(revision.wiki, formattedTitle); + const revisionDate = formatDateWithTime(revision.timestamp); return (

    - {formattedTitle} -
    - {details} + {formattedTitle}

    - {revisionDate} - {revision.characters} - {revision.references_added} - {revision.views} + {revisionDate} + {revision.sizediff} ({ + const revisions = action.revisions.map(rev => ({ ...rev, + wiki: { + language: action.wiki.language, + project: action.wiki.project + }, article: { - ...rev.article, - project: rev.wiki.project + title: rev.title, + language: action.wiki.language, + project: action.wiki.project } })); return { ...state, - [action.userId]: revisions + [action.username]: revisions }; } default: diff --git a/app/assets/javascripts/utils/mediawiki_revisions_utils.js b/app/assets/javascripts/utils/mediawiki_revisions_utils.js index 1811e3b9b8..d83c52512e 100644 --- a/app/assets/javascripts/utils/mediawiki_revisions_utils.js +++ b/app/assets/javascripts/utils/mediawiki_revisions_utils.js @@ -233,3 +233,19 @@ export const getRevisionRange = async (API_URL, articleTitle, usernames, startDa return { first_revision: earliestRev(firstRevs), last_revision: latestRev(lastRevs) }; }; +export const fetchLatestRevisionsForUser = async (username, wiki) => { + const prefix = `https://${toWikiDomain(wiki)}`; + const API_URL = `${prefix}/w/api.php`; + + const params = { + action: 'query', + format: 'json', + list: 'usercontribs', + ucuser: username, + ucprop: 'ids|title|timestamp|sizediff' + }; + + const response = await request(`${API_URL}?${stringify(params)}&origin=*`); + const json = await response.json(); + return json.query.usercontribs; +}; diff --git a/app/assets/javascripts/utils/wiki_utils.js b/app/assets/javascripts/utils/wiki_utils.js index 6c89aa2dee..64997e461d 100644 --- a/app/assets/javascripts/utils/wiki_utils.js +++ b/app/assets/javascripts/utils/wiki_utils.js @@ -41,4 +41,8 @@ const getArticleUrl = (wiki, title) => { return `https://${domain}/wiki/${title}`; }; -export { trackedWikisMaker, formatOption, toWikiDomain, wikiNamespaceLabel, getArticleUrl }; +const getDiffUrl = (revision) => { + return `https://${toWikiDomain(revision.wiki)}/w/index.php?diff=${revision.revid}`; +}; + +export { trackedWikisMaker, formatOption, toWikiDomain, wikiNamespaceLabel, getArticleUrl, getDiffUrl }; From 9248acf1964eb17516a8d32a84a6f59a40aa5615 Mon Sep 17 00:00:00 2001 From: Sage Ross Date: Fri, 20 Dec 2024 10:54:27 -0800 Subject: [PATCH 40/66] Remove RevisionsController --- app/controllers/revisions_controller.rb | 18 ---- config/routes.rb | 2 - spec/controllers/revisions_controller_spec.rb | 87 ------------------- 3 files changed, 107 deletions(-) delete mode 100644 app/controllers/revisions_controller.rb delete mode 100644 spec/controllers/revisions_controller_spec.rb diff --git a/app/controllers/revisions_controller.rb b/app/controllers/revisions_controller.rb deleted file mode 100644 index 6ce49f7f95..0000000000 --- a/app/controllers/revisions_controller.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -#=Controller for the Revisions API. -class RevisionsController < ApplicationController - respond_to :json - DEFAULT_REVISION_LIMIT = 10 - - # Returns revisions for a single user within the scope of a single course. - def index - user = User.find(params[:user_id]) - course = Course.find(params[:course_id]) - - @revisions = course.tracked_revisions.where(user_id: user.id) - .order('revisions.date DESC') - .eager_load(:article, :wiki) - .limit(params[:limit] || DEFAULT_REVISION_LIMIT) - end -end diff --git a/config/routes.rb b/config/routes.rb index ddccc8e33e..b96a84bfb6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -221,8 +221,6 @@ post 'courses/:course_id/disable_timeline' => 'timeline#disable_timeline', constraints: { course_id: /.*/ } - get 'revisions' => 'revisions#index' - get 'articles/article_data' => 'articles#article_data' get 'articles/details' => 'articles#details' post 'articles/status' => 'articles#update_tracked_status' diff --git a/spec/controllers/revisions_controller_spec.rb b/spec/controllers/revisions_controller_spec.rb deleted file mode 100644 index 5950810624..0000000000 --- a/spec/controllers/revisions_controller_spec.rb +++ /dev/null @@ -1,87 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe RevisionsController, type: :request do - # This spec involves multiple course types to check the behaviour of course start/end times - # and how they interact with the Course.revisions scope - describe '#index' do - let(:course_start) { Time.new(2015, 1, 1, 0, 0, 0, '+00:00') } - let(:course_end) { Time.new(2016, 1, 1, 20, 0, 0, '+00:00') } - let!(:course) { create(:course, start: course_start, end: course_end.end_of_day) } - let!(:basic_course) { create(:basic_course, start: course_start, end: course_end) } - let!(:user) { create(:user) } - let!(:user2) { create(:user, id: 2, username: 'user2') } - let!(:courses_user) { create(:courses_user, course_id: course.id, user_id: user.id) } - let!(:courses_user2) { create(:courses_user, course_id: course.id, user_id: 2) } - - let!(:article) { create(:article, mw_page_id: 1) } - let!(:course_revisions) do - (1..5).map do |i| - create(:revision, article_id: article.id, mw_page_id: 1, - user_id: user.id, mw_rev_id: i, date: course_end - 6.hours) - end - end - let!(:non_course_revisions) do - (6..10).map do |i| - create(:revision, article_id: article.id, mw_page_id: 1, - user_id: user.id, mw_rev_id: i, date: course_end.end_of_day + 1.hour) - end - end - let!(:non_basic_course_revisions) do - (11..15).map do |i| - create(:revision, article_id: article.id, mw_page_id: 1, - user_id: user.id, mw_rev_id: i, date: course_end + 1.hour) - end - end - - let!(:non_user_revisions) do - (16..20).map do |i| - create(:revision, article_id: article.id, mw_page_id: 1, - user_id: 2, mw_rev_id: i, date: course_end - 1.day) - end - end - let(:params) { { course_id: course.id, user_id: user.id, format: 'json' } } - let(:params2) { { course_id: basic_course.id, user_id: user.id, format: 'json' } } - - it 'returns only tracked revisions' do - # Doesn't include untracked revisions - create(:articles_course, course:, article:, tracked: false) - get '/revisions', params: params - expect(assigns(:revisions).count).to eq(0) - - # Includes tracked revisions - ArticlesCourses.first.update(tracked: true) - get '/revisions', params: params - expect(assigns(:revisions).count).not_to eq(0) - end - - it 'returns revisions that happened during the course' do - get '/revisions', params: params - course_revisions.each do |revision| - expect(assigns(:revisions)).to include(revision) - end - end - - it 'does not return revisions that happened after the last day of the course' do - get '/revisions', params: params - non_course_revisions.each do |revision| - expect(assigns(:revisions)).not_to include(revision) - end - end - - it 'does return revisions from the final day of the basic course but not after it ended' do - get '/revisions', params: params2 - non_basic_course_revisions.each do |revision| - expect(assigns(:revisions)).not_to include(revision) - end - end - - it 'does not return course revisions by other users' do - get '/revisions', params: params - non_user_revisions.each do |revision| - expect(assigns(:revisions)).not_to include(revision) - end - end - end -end From 07fe87ae985006a3b64b7ac43df3fabc13315e5f Mon Sep 17 00:00:00 2001 From: Sage Ross Date: Fri, 20 Dec 2024 10:56:23 -0800 Subject: [PATCH 41/66] Remove unused routes I think the corresponding actions were already gone, but we forgot to clean up the routes. --- config/routes.rb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index b96a84bfb6..d18080f782 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -67,13 +67,6 @@ get 'download_personal_data' => 'personal_data#show' end - # Users - resources :users, only: [:index, :show], param: :username, constraints: { username: /.*/ } do - collection do - get 'revisions' - end - end - resources :assignments do patch '/status' => 'assignments#update_status' resources :assignment_suggestions @@ -270,7 +263,6 @@ get 'courses' get 'ores_plot' get 'articles_csv' - get 'revisions_csv' get 'alerts' get 'wikidata' put 'add_organizer' From 53b823a94511ede5b7260d52715e107e7af6c420 Mon Sep 17 00:00:00 2001 From: Sage Ross Date: Fri, 20 Dec 2024 10:59:00 -0800 Subject: [PATCH 42/66] Remove broken reducer spec I rewrote this reducer to handle revision data from mediawiki instead of the previous format. The spec would need to be rewritten completely. --- test/reducers/user_revisions.spec.js | 36 ---------------------------- 1 file changed, 36 deletions(-) delete mode 100644 test/reducers/user_revisions.spec.js diff --git a/test/reducers/user_revisions.spec.js b/test/reducers/user_revisions.spec.js deleted file mode 100644 index e2402486bc..0000000000 --- a/test/reducers/user_revisions.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -import deepFreeze from 'deep-freeze'; -import '../testHelper'; -import userRevisions from '../../app/assets/javascripts/reducers/user_revisions'; -import { RECEIVE_USER_REVISIONS } from '../../app/assets/javascripts/constants'; - -describe('user revisions reducer', () => { - test( - 'should return initial state when no action nor state is provided', - () => { - const newState = userRevisions(undefined, { type: null }); - expect(typeof newState).toBe('object'); - } - ); - - test( - 'receives user revisions data with user identification as key via RECEIVE_USER_REVISIONS', - () => { - const initialState = {}; - deepFreeze(initialState); - const mockedAction = { - type: RECEIVE_USER_REVISIONS, - data: { course: { revisions: [] } }, - userId: 3 - }; - - const firstState = userRevisions(initialState, mockedAction); - expect(Array.isArray(firstState[3])).toBe(true); - expect(firstState[4]).toBeUndefined(); - - mockedAction.userId = 4; - const secondState = userRevisions(firstState, mockedAction); - expect(Array.isArray(secondState[4])).toBe(true); - expect(Array.isArray(secondState[3])).toBe(true); - } - ); -}); From 738ceac5cf0977e8daae81c53ff0138670349b22 Mon Sep 17 00:00:00 2001 From: Sage Ross Date: Fri, 20 Dec 2024 11:49:22 -0800 Subject: [PATCH 43/66] Fix users_path --- config/routes.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/routes.rb b/config/routes.rb index d18080f782..7b48dd80f2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -67,6 +67,9 @@ get 'download_personal_data' => 'personal_data#show' end + # Users + resources :users, only: [:index, :show], param: :username, constraints: { username: /.*/ } + resources :assignments do patch '/status' => 'assignments#update_status' resources :assignment_suggestions @@ -263,6 +266,7 @@ get 'courses' get 'ores_plot' get 'articles_csv' + get 'revisions_csv' get 'alerts' get 'wikidata' put 'add_organizer' From 32227a6e1dba88274ac6fc9a1ee6d3fc647f1fb2 Mon Sep 17 00:00:00 2001 From: Abishek Das Date: Sun, 22 Dec 2024 13:58:46 +0530 Subject: [PATCH 44/66] Fix commented-out tests and Poltergeist selector issues in SurveyAdmin spec --- spec/features/survey_admin_spec.rb | 39 +++++++++++++++++------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/spec/features/survey_admin_spec.rb b/spec/features/survey_admin_spec.rb index 7b3d5787fc..8165cdc0d5 100644 --- a/spec/features/survey_admin_spec.rb +++ b/spec/features/survey_admin_spec.rb @@ -41,11 +41,13 @@ click_link 'New Question Group' fill_in('question_group_name', with: 'New Question Group') - # FIXME: Fails to find the div with Poltergeist - # within('div#question_group_campaign_ids_chosen') do - # find('input').set('Spring 2015') - # find('input').native.send_keys(:return) - # end + within('#question_group_campaign_ids') do + option = find('option', text: 'Spring 2015') + page.execute_script( + "arguments[0].selected = true; + arguments[0].parentNode.dispatchEvent(new Event('change'))", option.native + ) + end page.find('input.button[value="Save Question Group"]').click # Create a question @@ -68,13 +70,13 @@ expect(Rapidfire::Question.count).to eq(2) page.find('label', text: 'Conditionally show this question').click - # FIXME: fails to find the div with Poltergeist - # within 'div.survey__question__conditional-row' do - # select('Who is awesome?') - # end - # within 'select[data-conditional-value-select=""]' do - # select('Me!') - # end + + within 'div.survey__question__conditional-row' do + select('Who is awesome?') + end + within 'select[data-conditional-value-select=""]' do + select('Me!') + end page.find('input.button').click # Create two more question groups, so that we can reorder them. @@ -127,11 +129,14 @@ visit '/surveys/assignments' click_link 'New Survey Assignment' - # FIXME: Fails to find the div with Poltergeist - # within('div#survey_assignment_campaign_ids_chosen') do - # find('input').set('Spring 2015') - # find('input').native.send_keys(:return) - # end + within('#survey_assignment_campaign_ids') do + option = find('option', text: 'Spring 2015') + page.execute_script( + "arguments[0].selected = true; + arguments[0].parentNode.dispatchEvent(new Event('change'))", option.native + ) + end + fill_in('survey_assignment_send_date_days', with: '7') check 'survey_assignment_published' fill_in('survey_assignment_custom_email_subject', with: 'My Custom Subject!') From 4ecaf096d72b85bfe3717907df07c46744a2d31d Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 23 Dec 2024 13:20:59 +0100 Subject: [PATCH 45/66] Localisation updates from https://translatewiki.net. --- config/locales/ko.yml | 1 + config/locales/lb.yml | 2 +- config/locales/pa.yml | 3 +++ config/locales/zh-CN.yml | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/config/locales/ko.yml b/config/locales/ko.yml index f14ee26dba..bc83d2253a 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -588,6 +588,7 @@ ko: uploads_none: 이 프로젝트는 이미지나 다른 미디어 파일을 위키미디어 공용에 기여하지 않았습니다. user_uploads_none: 선택된 사용자는 이 프로젝트에 어떠한 그림이나 그 밖의 미디어 파일을 기여하지 않았습니다. view_other: 다른 캠페인 보기 + view_page: 문서 보기 yourcourses: 당신의 프로그램 editable: edit: 편집 diff --git a/config/locales/lb.yml b/config/locales/lb.yml index 725bc52831..5e78823525 100644 --- a/config/locales/lb.yml +++ b/config/locales/lb.yml @@ -437,7 +437,7 @@ lb: students_count: Zuel vu Participanten term: Wéini view_other: Aner Campagne weisen - view_page: Säit vum Programm weisen + view_page: Säit weisen yourcourses: Är Programmer editable: edit: Änneren diff --git a/config/locales/pa.yml b/config/locales/pa.yml index 20332fbe93..1d958f3465 100644 --- a/config/locales/pa.yml +++ b/config/locales/pa.yml @@ -144,6 +144,7 @@ pa: none: ਕੋਈ ਨਹੀਂ categories: add_this_template: ਫਰਮੇ ਜੋੜੋ + template_name: ਫ਼ਰਮੇ ਦਾ ਨਾਂ courses: actions: ਕਾਰਵਾਈਆਂ campaign_users: '%{title} ਸੰਪਾਦਕ' @@ -169,6 +170,7 @@ pa: students: ਸੰਪਾਦਕ students_taught: ਕੁੱਲ ਸੰਪਾਦਕ students_short: ਸੰਪਾਦਕ + view_page: ਸਫ਼ਾ ਵੇਖੋ editable: edit: ਸੋਧੋ cancel: ਰੱਦ ਕਰੋ @@ -196,6 +198,7 @@ pa: help: ਮਦਦ translation: ਤਰਜਮਾ draft: ਖਰੜਾ + query: ਪੁੱਛਗਿੱਛ recent_activity: article_title: ਸਿਰਲੇਖ datetime: ਮਿਤੀ/ਵੇਲਾ diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index f925581317..1b3f99916b 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -667,7 +667,7 @@ zh-CN: unsubmitted: 未提交的计划 uploads_none: 此项目没有贡献任何图片或其他媒体文件至维基共享资源。 view_other: 查看其它活动 - view_page: 查看计划页面 + view_page: 查看页面 word_count_doc: 在课程的学期期间,由计划的编辑者添加至主命名空间条目字数的估计 yourcourses: 您的计划 editable: From 9fd13fc4048ec1477c3196292d38ca27410eb789 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 26 Dec 2024 13:17:32 +0100 Subject: [PATCH 46/66] Localisation updates from https://translatewiki.net. --- config/locales/ko.yml | 1 + config/locales/pa.yml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/config/locales/ko.yml b/config/locales/ko.yml index bc83d2253a..e462c2f4e4 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -319,6 +319,7 @@ ko: accounts_generation_confirm_message: 계정 요청을 사용하시겠습니까? activity: 활동 alerts: 경보 + does_not_exist: 항목이 존재하지 않습니다. 제안을 받으려면 연습장 문서를 만들고 해당 문서에 자신을 할당하세요. all_courses: 모든 프로그램 already_enrolled: 이미 %{title}에 참여하고 계세요! actions: 기능 diff --git a/config/locales/pa.yml b/config/locales/pa.yml index 1d958f3465..e9b7705df1 100644 --- a/config/locales/pa.yml +++ b/config/locales/pa.yml @@ -205,6 +205,9 @@ pa: image: ਤਸਵੀਰ file_name: ਫ਼ਾਈਲ ਦਾ ਨਾਂ recent_edits: ਹਾਲੀਆ ਸੋਧਾਂ + settings: + categories: + users: ਵਰਤੋਂਕਾਰ suggestions: suggestion_docs: does_not_exist: ਇਹ ਲੇਖ ਵਿਕੀਪੀਡੀਆ 'ਤੇ ਮੌਜੂਦ ਨਹੀਂ ਹੈ। From b2b9aa1d68f58e64ab4d46be8b129ce3f4c8c518 Mon Sep 17 00:00:00 2001 From: Amine Hassou Date: Thu, 26 Dec 2024 03:52:53 +0100 Subject: [PATCH 47/66] Initial work on adding new student endpoint --- app/controllers/courses_controller.rb | 6 ++++++ app/models/course.rb | 8 ++++++++ config/routes.rb | 4 +++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index 436557d183..0b67f73dc0 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -139,6 +139,12 @@ def alerts @alerts = current_user&.admin? ? @course.alerts : @course.public_alerts end + def approved_classroom_courses_json + courses = Course.approved_classroom_courses_with_users + + render json: courses, include: :users + end + ########################## # User-initiated actions # ########################## diff --git a/app/models/course.rb b/app/models/course.rb index 80e1fb2c35..f7bf7ce011 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -160,6 +160,14 @@ def self.unsubmitted .where(submitted: false).references(:campaigns) end + def self.approved_classroom_courses_with_users + nonprivate + .where(type: 'ClassroomProgramCourse', withdrawn: false) + .joins(:campaigns) + .distinct + .includes(:users) + end + scope :strictly_current, -> { where('? BETWEEN start AND end', Time.zone.now) } scope :current, -> { current_and_future.where('start < ?', Time.zone.now) } scope :ended, -> { where('end < ?', Time.zone.now) } diff --git a/config/routes.rb b/config/routes.rb index 7b48dd80f2..fd1b378d25 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -168,7 +168,9 @@ titleterm: /[^\/]*/, _subsubsubpage: /.*/ } - + get '/courses/approved_classroom_courses.json', + to: 'courses#approved_classroom_courses_json', + as: :approved_classroom_courses 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', From 32ddb9b7d2f09f992282272eed9a4a0fa633d03d Mon Sep 17 00:00:00 2001 From: Jiya Gupta Date: Sun, 29 Dec 2024 02:59:10 -0500 Subject: [PATCH 48/66] fixing Unnecessary Logs, Preventing System Inefficiencies and Inefficient Retries while populating the dashboard database --- setup/populate_dashboard.rb | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/setup/populate_dashboard.rb b/setup/populate_dashboard.rb index dbdbf52b77..dc03822270 100755 --- a/setup/populate_dashboard.rb +++ b/setup/populate_dashboard.rb @@ -56,17 +56,36 @@ def make_copy_of(url) # Set up some example data in the dashboard def populate_dashboard - puts "setting up example courses..." + puts "Setting up example courses..." example_courses = [ 'https://outreachdashboard.wmflabs.org/courses/Uffizi/WDG_-_AF_2018_Florence', 'https://outreachdashboard.wmflabs.org/courses/QCA/Brisbane_QCA_ArtandFeminism_2018', 'https://dashboard.wikiedu.org/courses/Stanford_Law_School/Advanced_Legal_Research_Winter_2020_(Winter)' ] + example_courses.each do |url| - default_campaign = Campaign.find_or_create_by!(title: 'Default Campaign', slug: ENV['default_campaign']) - course = make_copy_of(url) - default_campaign.courses << course - puts "getting data for #{course.slug}..." - UpdateCourseStats.new(course) + begin + # Try to find or create the default campaign + default_campaign = Campaign.find_or_create_by!(title: 'Default Campaign', slug: ENV['default_campaign']) + + # Attempt to make a copy of the course + course = make_copy_of(url) + + # Check if the course already exists before associating it with the campaign + if default_campaign.courses.exists?(slug: course.slug) + Rails.logger.error("Course with slug #{course.slug} already exists in the campaign. Skipping...") + else + # Add the course to the campaign if it doesn't already exist + default_campaign.courses << course + puts "Getting data for #{course.slug}..." + UpdateCourseStats.new(course) + end + rescue ActiveRecord::RecordInvalid => e + # Handle specific error when record creation fails + Rails.logger.error("Error processing course at #{url}: #{e.message}") + rescue StandardError => e + # Generic error handling for other issues + Rails.logger.error("An error occurred for course at #{url}: #{e.message}") + end end -end +end \ No newline at end of file From 23e4a782f5a66d3e1c27638459fec77caf9a083e Mon Sep 17 00:00:00 2001 From: Amine Hassou Date: Sun, 29 Dec 2024 12:12:29 +0100 Subject: [PATCH 49/66] Added endpoints for classroomprogram courses and fellowscohort courses --- app/controllers/courses_controller.rb | 24 ++++++++++++++++++++--- app/models/course.rb | 28 +++++++++++++++++++++++++-- config/routes.rb | 17 +++++++++++++--- 3 files changed, 61 insertions(+), 8 deletions(-) diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index 0b67f73dc0..9dfb51e404 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -139,10 +139,28 @@ def alerts @alerts = current_user&.admin? ? @course.alerts : @course.public_alerts end - def approved_classroom_courses_json - courses = Course.approved_classroom_courses_with_users + def classroom_program_students_json + courses = Course.classroom_program_students - render json: courses, include: :users + render json: courses, include: :students + end + + def classroom_program_students_and_instructors_json + courses = Course.classroom_program_students_and_instructors + + render json: courses, include: [:students, :instructors] + end + + def fellows_cohort_students_json + courses = Course.fellows_cohort_students + + render json: courses, include: :students + end + + def fellows_cohort_students_and_instructors_json + courses = Course.fellows_cohort_students_and_instructors + + render json: courses, include: [:students, :instructors] end ########################## diff --git a/app/models/course.rb b/app/models/course.rb index f7bf7ce011..02e50ddfe4 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -160,12 +160,36 @@ def self.unsubmitted .where(submitted: false).references(:campaigns) end - def self.approved_classroom_courses_with_users + def self.classroom_program_students nonprivate .where(type: 'ClassroomProgramCourse', withdrawn: false) .joins(:campaigns) .distinct - .includes(:users) + .includes(:students) + end + + def self.classroom_program_students_and_instructors + nonprivate + .where(type: 'ClassroomProgramCourse', withdrawn: false) + .joins(:campaigns) + .distinct + .includes(:students, :instructors) + end + + def self.fellows_cohort_students + nonprivate + .where(type: 'FellowsCohort', withdrawn: false) + .joins(:campaigns) + .distinct + .includes(:students) + end + + def self.fellows_cohort_students_and_instructors + nonprivate + .where(type: 'FellowsCohort', withdrawn: false) + .joins(:campaigns) + .distinct + .includes(:students, :instructors) end scope :strictly_current, -> { where('? BETWEEN start AND end', Time.zone.now) } diff --git a/config/routes.rb b/config/routes.rb index fd1b378d25..af7c002c19 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -168,9 +168,20 @@ titleterm: /[^\/]*/, _subsubsubpage: /.*/ } - get '/courses/approved_classroom_courses.json', - to: 'courses#approved_classroom_courses_json', - as: :approved_classroom_courses + + get '/courses/classroom_program_students.json', + to: 'courses#classroom_program_students_json', + as: :classroom_program_students + get '/courses/classroom_program_students_and_instructors.json', + to: 'courses#classroom_program_students_and_instructors_json', + as: :classroom_program_students_and_instructors + get '/courses/fellows_cohort_students.json', + to: 'courses#fellows_cohort_students_json', + as: :fellows_cohort_students + get '/courses/fellows_cohort_students_and_instructors.json', + to: 'courses#fellows_cohort_students_and_instructors_json', + as: :fellows_cohort_students_and_instructors + 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', From 9e5b8e8286af62968f6eed7d14532fb2d0f00124 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 30 Dec 2024 13:18:38 +0100 Subject: [PATCH 50/66] Localisation updates from https://translatewiki.net. --- config/locales/pa.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/config/locales/pa.yml b/config/locales/pa.yml index e9b7705df1..fc8e8430a1 100644 --- a/config/locales/pa.yml +++ b/config/locales/pa.yml @@ -85,7 +85,7 @@ pa: no_alerts: ਕੋਈ ਚਿਤਾਵਨੀਆਂ ਨਹੀਂ ਮਿਲੀਆਂ। activity: all: ਸਰਗਰਮੀ - edited_by: ਦੁਆਰਾ ਸੰਪਾਦਿਤ + edited_by: ਵੱਲੋਂ ਸੋਧਿਆ label: ਸਰਗਰਮੀ articles: articles: ਲੇਖ @@ -102,7 +102,7 @@ pa: assigned_to: ਨੂੰ ਸੌਂਪਿਆ ਗਿਆ deleted: (ਮਿਟਾਏ) edited: ਸੰਪਾਦਿਤ ਲੇਖ - edited_by: 'ਦੁਆਰਾ ਸੰਪਾਦਿਤ:' + edited_by: 'ਵੱਲੋਂ ਸੋਧਿਆ:' edited_mobile: ਲੇਖ edited_none: ਹਾਲੇ ਤੱਕ ਕੋਈ ਸੰਪਾਦਿਤ ਲੇਖ ਨਹੀਂ ਹਨ। filter: @@ -166,10 +166,10 @@ pa: templates: ਫਰਮੇ enrollment: ਸੰਪਾਦਕਾਂ ਨੂੰ ਜੋੜੋ/ਹਟਾਓ expected_students: ਸੰਭਾਵਿਤ ਸੰਪਾਦਕ - student_editors: ਸੰਪਾਦਕ - students: ਸੰਪਾਦਕ + student_editors: ਸੋਧਕ + students: ਸੋਧਕ students_taught: ਕੁੱਲ ਸੰਪਾਦਕ - students_short: ਸੰਪਾਦਕ + students_short: ਸੋਧਕ view_page: ਸਫ਼ਾ ਵੇਖੋ editable: edit: ਸੋਧੋ @@ -228,7 +228,7 @@ pa: assign_articles_done: ਪੂਰਾ ਹੋਇਆ assigned: ਸੌਂਪੇ ਗਏ ਲੇਖ chars_added: ਅੱਖਰ ਜੋੜੇ ਗਏ
    (ਲੇਖ। ਵਰਤੋਂਕਾਰ। ਖਰੜਾ) - editors: ਸੰਪਾਦਕ + editors: ਸੋਧਕ edits: ਸੋਧਾਂ first_name: ਮੂਹਰਲਾ ਨਾਂ last_name: ਪਿਛਲਾ ਨਾਂ From 6f8a743abf0ac0fa6139e6723a4274063bc5b460 Mon Sep 17 00:00:00 2001 From: Sage Ross Date: Mon, 30 Dec 2024 08:55:00 -0800 Subject: [PATCH 51/66] Update 'cite systematic reviews' exercise We have new PCORI reviews, and the links for all of them have changed. --- .../wiki_ed/libraries/students.yml | 2 +- .../cite-systematic-reviews-exercise.yml | 2 +- .../6302-evaluate-the-reviews.yml | 25 +++++++++---------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/training_content/wiki_ed/libraries/students.yml b/training_content/wiki_ed/libraries/students.yml index f69c4e67e4..5e57310c7a 100644 --- a/training_content/wiki_ed/libraries/students.yml +++ b/training_content/wiki_ed/libraries/students.yml @@ -98,7 +98,7 @@ categories: name: 'Exercise: Copyedit an article' description: Practice editing by improving an existing article through small copyedits. - slug: cite-systematic-reviews-exercise - name: 'Exercise: Cite a systemic review' + name: 'Exercise: Cite a systematic review' description: Practice adding citations to medical content using systematic reviews. - slug: peer-review name: 'Exercise: Peer review' diff --git a/training_content/wiki_ed/modules/cite-systematic-reviews-exercise.yml b/training_content/wiki_ed/modules/cite-systematic-reviews-exercise.yml index 0777484372..ba468ec572 100644 --- a/training_content/wiki_ed/modules/cite-systematic-reviews-exercise.yml +++ b/training_content/wiki_ed/modules/cite-systematic-reviews-exercise.yml @@ -1,4 +1,4 @@ -name: 'Cite a systemic review' +name: 'Cite a systematic review' id: 63 kind: 'exercise' description: | diff --git a/training_content/wiki_ed/slides/63-cite-systematic-reviews-exercise/6302-evaluate-the-reviews.yml b/training_content/wiki_ed/slides/63-cite-systematic-reviews-exercise/6302-evaluate-the-reviews.yml index 764a5a3402..395d5e1f24 100644 --- a/training_content/wiki_ed/slides/63-cite-systematic-reviews-exercise/6302-evaluate-the-reviews.yml +++ b/training_content/wiki_ed/slides/63-cite-systematic-reviews-exercise/6302-evaluate-the-reviews.yml @@ -5,20 +5,19 @@ content: | types of content they cover. The reviews are lengthy, so you may wish to only review small sections. - * [Management Strategies for Infantile Epilepsy](https://www.pcori.org/sites/default/files/PCORI-AHRQ-Management-Strategies-Infantile-Epilepsy-Systematic-Review-Report-October-2022.pdf) (DOI: 10.23970/AHRQEPCCER252) - * [Radiation Therapy for Brain Metastases](https://www.pcori.org/sites/default/files/PCORI-AHRQ-Radiation-Therapy-Brain-Metastases-Systematic-Review-Report-June-2021.pdf) (DOI: 10.23970/AHRQEPCCER242) - * [Cervical Ripening in the Outpatient Setting](https://www.pcori.org/sites/default/files/PCORI-AHRQ-Cervical-Ripening-in-the-Outpatient-Setting-Systematic-Review-Report-March-2021.pdf) (DOI: 10.23970/AHRQEPCCER238) - * [Interventions for Breathlessness in Patients With Advanced Cancer](https://www.pcori.org/sites/default/files/PCORI-AHRQ-Interventions-Breathlessness-Patients-With-Advanced-Cancer-Systematic-Review-Update-Report-November-2020.pdf) (DOI: 10.23970/AHRQEPCCER232) - * [Stroke Prevention in Atrial Fibrillation Patients](https://www.pcori.org/sites/default/files/PCORI-AHRQ-Stroke-Prevention-Atrial-Fibrillation-Patients-Systematic-Review-Update-Report-October-2018.pdf) (DOI: 10.23970/AHRQEPCCER214) - * [Nonsurgical Treatments for Urinary Incontinence in Women](https://www.pcori.org/sites/default/files/PCORI-AHRQ-Nonsurgical-Treatments-Urinary-Incontinence-Women-Systematic-Review-Update-Report-August-2018.pdf) (DOI: 10.23970/AHRQEPCCER212) - * [Drug Therapy for Early Rheumatoid Arthritis](https://www.pcori.org/sites/default/files/PCORI-AHRQ-Drug-Therapy-Early-Rheumatoid-Arthritis-Systematic-Review-Update-Report-July-2018.pdf) (DOI: 10.23970/AHRQEPCCER211) - * [Psychological and Pharmacological Treatments for Adults with Posttraumatic Stress Disorder (PTSD)](https://www.pcori.org/sites/default/files/PCORI-AHRQ-Psychological-Pharmacological-Treatments-PTSD-Systematic-Review-Update-Report-May-2018.pdf) (DOI: 10.23970/AHRQEPCCER207) + * [Behavioral Interventions for Migraine Prevention](https://effectivehealthcare.ahrq.gov/sites/default/files/related_files/cer-270-bimp.pdf) (DOI: 10.23970/AHRQEPCCER270) + * [Diagnosis and Management of OCD in Children](https://effectivehealthcare.ahrq.gov/sites/default/files/related_files/cer-276-obsessive-compulsive-disorders.pdf) (DOI: 10.23970/AHRQEPCCER276) + * [ADHD Diagnosis and Treatment in Children and Adolescents](https://effectivehealthcare.ahrq.gov/sites/default/files/related_files/cer-267-adhd.pdf) (DOI: 10.23970/AHRQEPCCER267) + * [Postpartum Care up to 1 Year After Pregnancy](https://effectivehealthcare.ahrq.gov/sites/default/files/related_files/cer-261-postpartum-care.pdf) (DOI: 10.23970/AHRQEPCCER261) + * [Management of Postpartum Hypertensive Disorders of Pregnancy](https://effectivehealthcare.ahrq.gov/sites/default/files/related_files/cer-263-postpartum-hypertensive-pregnancy.pdf) (DOI: 10.23970/AHRQEPCCER263) + * [Management of Infantile Epilepsies](https://effectivehealthcare.ahrq.gov/sites/default/files/product/pdf/cer-252-infantile-epilepsies-final.pdf) (DOI: 10.23970/AHRQEPCCER252) + * [Radiation Therapy for Brain Metastases](https://effectivehealthcare.ahrq.gov/sites/default/files/cer-242-radiation-therapy-brain-metastases-evidence-summary.pdf) (DOI: 10.23970/AHRQEPCCER242) + * [Cervical Ripening in the Outpatient Setting](https://effectivehealthcare.ahrq.gov/sites/default/files/pdf/cer-238-cervical-ripening-final-report.pdf) (DOI: 10.23970/AHRQEPCCER238) + * [Interventions for Breathlessness in Patients With Advanced Cancer](https://effectivehealthcare.ahrq.gov/sites/default/files/pdf/dyspnea-advanced-cancer-report.pdf) (DOI: 10.23970/AHRQEPCCER232) 2. Navigate to Wikipedia and read one or more corresponding articles (or the relevant sections of articles) to see how those topics are covered. - 3. When you find existing Wikipedia coverage that matches what the review discusses, use the review to fact-check the Wikipedia content. - If the Wikipedia content is consistent with the review, look at the sources (if any) that Wikipedia currently cites for that content. - What kinds of sources are they? Primary research? A report that summarizes only a few studies? An older systematic review? + 3. Identify a key message from the PCORI review that doesn’t already exist in a relevant Wikipedia article. + Add a sentence or two to the relevant article to represent this information. - 4. If citing the PCORI review would improve the article's sourcing, it's time to add that citation. You may also wish to add a sentence or two to the - article if there is relevant information in the systematic review not yet covered on Wikipedia. \ No newline at end of file + 4. Be sure to add a citation to the PCORI review! The next slide will remind you how. \ No newline at end of file From a01b22c38c5e21a485fe625c6c22cdc58608e19b Mon Sep 17 00:00:00 2001 From: Sage Ross Date: Mon, 30 Dec 2024 09:13:41 -0800 Subject: [PATCH 52/66] Fix 'show last revision' toggle I overlooked this change when I reimplemented the articleDetails to be completely client-side. --- .../common/ArticleViewer/containers/ArticleViewer.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/components/common/ArticleViewer/containers/ArticleViewer.jsx b/app/assets/javascripts/components/common/ArticleViewer/containers/ArticleViewer.jsx index 916002cde4..4e3cec2294 100644 --- a/app/assets/javascripts/components/common/ArticleViewer/containers/ArticleViewer.jsx +++ b/app/assets/javascripts/components/common/ArticleViewer/containers/ArticleViewer.jsx @@ -55,7 +55,7 @@ const ArticleViewer = ({ showOnMount, users, showArticleFinder, showButtonLabel, const [unhighlightedContributors, setUnhighlightedContributors] = useState([]); const [revisionId, setRevisionId] = useState(null); const [pendingRequest, setPendingRequest] = useState(false); - const lastRevisionId = useSelector(state => state.articleDetails[article.id]?.last_revision?.mw_rev_id); + const lastRevisionId = useSelector(state => state.articleDetails[article.id]?.last_revision?.revid); const dispatch = useDispatch(); const ref = useRef(); From 41e0c8ea3a0c4c4d2fe762d228e6dbcc25f28a36 Mon Sep 17 00:00:00 2001 From: empty-codes Date: Mon, 30 Dec 2024 18:38:35 +0100 Subject: [PATCH 53/66] Adds Admin guide explaining how the dashboard is run on Toolforge and CloudVPS --- docs/admin_guide.md | 97 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 docs/admin_guide.md diff --git a/docs/admin_guide.md b/docs/admin_guide.md new file mode 100644 index 0000000000..42879efb43 --- /dev/null +++ b/docs/admin_guide.md @@ -0,0 +1,97 @@ +# Admin Guide for Program & Events Dashboard + +This guide provides an overview of the Program & Events Dashboard. It also offers resources for managing and troubleshooting the system. + +## Table of Contents +1. [Overview](#overview) +2. [Monitoring and Logs](#monitoring-and-logs) + - [Toolforge](#toolforge) + - [Cloud VPS](#cloud-vps) +3. [Troubleshooting](#troubleshooting) + - [Web Server Issues](#web-server-issues) + - [Database Issues](#database-issues) + - [Data Dumps and Recovery](#data-dumps-and-recovery) +4. [More Resources](#more-resources) + +## Overview + +The **Programs & Events Dashboard** ([outreachdashboard.wmflabs.org](https://outreachdashboard.wmflabs.org)) is a web application designed to support the global Wikimedia community in organizing various programs, including edit-a-thons, education initiatives, and other events. **[Source Code](https://github.com/WikiEducationFoundation/WikiEduDashboard/tree/wmflabs)** **[Phabricator Project](https://phabricator.wikimedia.org/project/manage/1052/)** + +### Infrastructure Overview +- **Toolforge Tool**: [wikiedudashboard](https://toolsadmin.wikimedia.org/tools/id/wikiedudashboard) + - **Source code: [WikiEduDashboardTools](https://github.com/WikiEducationFoundation/WikiEduDashboardTools)**: A collection of PHP endpoints that retrieve revision and article data from Wikimedia Replica databases in the Wikimedia Cloud environment. + - **Deployed at**: [wikiedudashboard.toolforge.org](https://wikiedudashboard.toolforge.org/), [replica-revision-tools.wmcloud.org](https://replica-revision-tools.wmcloud.org/) + +- **Cloud VPS Project**: [globaleducation](https://openstack-browser.toolforge.org/project/globaleducation) + +### Servers + +1. **Web Server** + - **`peony-web.globaleducation.eqiad1.wikimedia.cloud`** + - Hosts the main web application and core Sidekiq processes using **RVM (Ruby Version Manager)**, **Phusion Passenger**, and **Apache**. + - **Capistrano** is used for deployments + - Sidekiq processes hosted: + - `sidekiq-default`: Handles transactional jobs (e.g., wiki edits, email notifications). + - `sidekiq-constant`: Manages frequently run tasks (e.g., adding courses to update queues). + - `sidekiq-daily`: Executes long-running daily update tasks. + +2. **Sidekiq Servers**: These dedicated servers handle the other Sidekiq processes to isolate bottlenecks and failures: + - **`peony-sidekiq.globaleducation.eqiad1.wikimedia.cloud`**: Hosts `sidekiq-short` for short-running course updates. + - **`peony-sidekiq-medium.globaleducation.eqiad1.wikimedia.cloud`**: Hosts `sidekiq-medium` for typical course updates. + - **`peony-sidekiq-3.globaleducation.eqiad1.wikimedia.cloud`**: Hosts `sidekiq-long` for long-running course updates with higher queue latency. + +3. **Database Server** + - **`peony-database.globaleducation.eqiad1.wikimedia.cloud`**: Manages the Dashboard's primary database. + +4. **Redis Server** + - **`p-and-e-dashboard-redis.globaleducation.eqiad1.wikimedia.cloud`**: Shared across all Sidekiq processes for task queuing and caching. + + +## Monitoring and Logs + +#### Toolforge +- [Kubernetes Namespace Details](https://k8s-status.toolforge.org/namespaces/tool-wikiedudashboard/) +- [Kubernetes Pod Details](https://k8s-status.toolforge.org/namespaces/tool-wikiedudashboard/pods/wikiedudashboard-5954f86c86-pm8d5/) + + +#### Cloud VPS +- [Grafana](https://grafana.wmcloud.org/d/0g9N-7pVz/cloud-vps-project-board?orgId=1&var-project=globaleducation) +- [Server Admin Logs (SAL)](https://sal.toolforge.org/globaleducation) +- [Alerts](https://prometheus-alerts.wmcloud.org/?q=%40state%3Dactive&q=project%3Dglobaleducation) +- [Puppet agent logs for the globaleducation project](https://grafana.wmcloud.org/d/SQM7MJZSz/cloud-vps-puppet-agents?orgId=1&var-project=globaleducation&from=now-2d&to=now) + + +## Troubleshooting + +### Web Server Issues +- **Internal Server Error**: Restart the web server. +- **Unresponsive Web Service**: + - Usually caused by high-activity events or surges in ongoing activity, leading to system overload. + - **Solution**: Reboot the VM (instance) running the web server. + - The web service typically recovers within a few hours. + +### Database Issues +- **Full Disk**: Free up space by deleting temporary tables. +- **High-Edit / Long Courses Causing Errors**: + - Consider turning off the 'long' and 'very_long_update' queues. +- **Stuck Transactions**: If results in the Rails server becoming unresponsive, restart MySQL. +- **Database Errors**: + - Verify that the app and database server versions are compatible. + +### Data Dumps and Recovery +- **Performing a Dump for a table**: + 1. Put the database in `innodb_force_recovery=1` mode. + - Note: `OPTIMIZE TABLE revisions;` cannot run in recovery mode because the database is read-only. + 2. Start the dump process. + 3. Once the dump is complete, drop the table. + 4. Remove the database from recovery mode and restore the table. + +### Third-Party Dependencies +Issues could also be caused by maintenance or outages in third-party dependencies such as Openstack, Toolforge, or other services. + + +## More Resources +- [Toolforge Documentation](https://wikitech.wikimedia.org/wiki/Help:Toolforge) +- [Cloud VPS Documentation](https://wikitech.wikimedia.org/wiki/Help:Cloud_VPS) +- [Cloud VPS Admin Documentation](https://wikitech.wikimedia.org/wiki/Portal:Cloud_VPS/Admin) +- [Details of most recent P&E server update](https://github.com/WikiEducationFoundation/WikiEduDashboard/commit/df271f1c54fd0520e42445fcc88f19b6d03a603b#diff-f8eaa8feeef99c2b098e875ccdace93998b84eeb4110dc9f49b1327df7d96e21) From a2cbae32ba59ad53b1ad2395befaefc4dcdf278d Mon Sep 17 00:00:00 2001 From: Sage Ross Date: Mon, 30 Dec 2024 09:51:38 -0800 Subject: [PATCH 54/66] Include registration date in campaign user CSV data --- app/models/campaign.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/campaign.rb b/app/models/campaign.rb index 98554ee0c7..bd5d147c0d 100644 --- a/app/models/campaign.rb +++ b/app/models/campaign.rb @@ -70,12 +70,14 @@ class Campaign < ApplicationRecord #################### def users_to_csv(role, opts = {}) - csv_data = [] + headers = opts[:course] ? %w[username course registered_at] : ['username'] + csv_data = [headers] courses.nonprivate.each do |course| users = course.send(role) users.each do |user| line = [user.username] line << course.slug if opts[:course] + line << user.registered_at if opts[:course] csv_data << line end end From 6afed6ee9bbe9b83e884961c3b823016bedb0e6d Mon Sep 17 00:00:00 2001 From: Amine Hassou Date: Tue, 31 Dec 2024 17:13:53 +0100 Subject: [PATCH 55/66] Filtered out data from newly-added endpoints for easier access --- app/controllers/courses_controller.rb | 46 ++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index 9dfb51e404..49cbcf7ad5 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -141,26 +141,56 @@ def alerts def classroom_program_students_json courses = Course.classroom_program_students - - render json: courses, include: :students + render json: courses.as_json( + only: %i[title created_at updated_at start end school term slug], + include: { + students: { + only: %i[username created_at updated_at permissions] + } + } + ) end def classroom_program_students_and_instructors_json courses = Course.classroom_program_students_and_instructors - - render json: courses, include: [:students, :instructors] + render json: courses.as_json( + only: %i[title created_at updated_at start end school term slug], + include: { + students: { + only: %i[username created_at updated_at permissions] + }, + instructors: { + only: %i[username created_at updated_at permissions] + } + } + ) end def fellows_cohort_students_json courses = Course.fellows_cohort_students - - render json: courses, include: :students + render json: courses.as_json( + only: %i[title created_at updated_at start end school term slug], + include: { + students: { + only: %i[username created_at updated_at permissions] + } + } + ) end def fellows_cohort_students_and_instructors_json courses = Course.fellows_cohort_students_and_instructors - - render json: courses, include: [:students, :instructors] + render json: courses.as_json( + only: %i[title created_at updated_at start end school term slug], + include: { + students: { + only: %i[username created_at updated_at permissions] + }, + instructors: { + only: %i[username created_at updated_at permissions] + } + } + ) end ########################## From cb553365e4d5ea30cd746bca0a0cee966865a715 Mon Sep 17 00:00:00 2001 From: Abishek Das Date: Tue, 31 Dec 2024 22:18:14 +0530 Subject: [PATCH 56/66] fix(DiffViewer & ArticleViewer): handle moved articles using MediaWiki page ID - Added "crossCheckArticleTitle" function to verify and update article titles and URLs using the MediaWiki API based on the page ID. - Introduced "UPDATE_ARTICLE_TITLE_AND_URL" action in the reducer to update article titles and URLs in the Redux store when article title is changed. - Updated "fetchArticleDetails" to integrate "crossCheckArticleTitle" for dynamic title validation and updates before if fetching article first and last revision fails. - Enhanced handling of moved articles by verifying and updating titles via MediaWiki API by using "crossCheckArticleTitle" in ArticleViewer. --- .../javascripts/actions/article_actions.js | 87 +++++++++++++++++-- .../containers/ArticleViewer.jsx | 30 +++++++ app/assets/javascripts/constants/articles.js | 1 + app/assets/javascripts/reducers/articles.js | 13 ++- app/views/courses/articles.json.jbuilder | 6 +- 5 files changed, 127 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/actions/article_actions.js b/app/assets/javascripts/actions/article_actions.js index 17243a6890..5f47610bff 100644 --- a/app/assets/javascripts/actions/article_actions.js +++ b/app/assets/javascripts/actions/article_actions.js @@ -1,27 +1,70 @@ import * as types from '../constants'; import API from '../utils/api.js'; import { getRevisionRange } from '../utils/mediawiki_revisions_utils'; +import { find } from 'lodash-es'; // This action uses the Thunk middleware pattern: instead of returning a plain -// action object, it returns a function that takes the store dispatch fucntion — +// action object, it returns a function that takes the store dispatch function — // which Thunk automatically provides — and can then dispatch a series of plain // actions to be handled by the store. // This is how actions with side effects — such as API calls — are handled in // Redux. export function fetchArticleDetails(articleId, courseId) { - return function (dispatch) { + return async function (dispatch, getState) { return API.fetchArticleDetails(articleId, courseId) .then((response) => { - // eslint-disable-next-line no-console const details = response.article_details; + return getRevisionRange(details.apiUrl, details.articleTitle, details.editors, details.startDate, details.endDate) - // eslint-disable-next-line no-console - .then(revisionRange => dispatch({ type: types.RECEIVE_ARTICLE_DETAILS, articleId, details, revisionRange })); + .then(async (revisionRange) => { + // If no revisions are found (both first and last revisions are missing), + // it may indicate a mismatch in the article title as the article was moved to a new title. + if (!revisionRange.first_revision && !revisionRange.last_revision) { + const { title: articleTitle, mw_page_id: article_mw_page_id } = find( + getState().articles.articles, + { id: articleId } + ); + + // Dispatch an action to cross-check the article title with its metadata. + const crossCheckedArticleTitle = await dispatch(crossCheckArticleTitle(articleId, articleTitle, article_mw_page_id)); + + // Re-fetch the article details using the cross-checked title for accuracy. + fetchArticleDetailsAgain(crossCheckedArticleTitle, articleId, courseId, dispatch); + } else { + dispatch({ type: types.RECEIVE_ARTICLE_DETAILS, articleId, details, revisionRange }); + } + }); }) .catch(response => (dispatch({ type: types.API_FAIL, data: response }))); }; } +// Re-fetches article details using the corrected or cross-checked article title. +// This function is used when the initial fetch fails to retrieve valid revision data, +// likely due to a mismatch in the article title. It ensures the Redux store is updated +// with accurate article details and revision ranges after the re-fetch. +function fetchArticleDetailsAgain(crossCheckedArticleTitle, articleId, courseId, dispatch) { + return API.fetchArticleDetails(articleId, courseId) + .then((response) => { + const details = response.article_details; + + // Calculate the revision range for the updated article title. + return getRevisionRange( + details.apiUrl, + crossCheckedArticleTitle, + details.editors, + details.startDate, + details.endDate + ).then((revisionRange) => { + // Dispatch the updated article details and revision range to Redux. + dispatch({ type: types.RECEIVE_ARTICLE_DETAILS, articleId, details, revisionRange }); + }); + }) + .catch((response) => { + (dispatch({ type: types.API_FAIL, data: response })); + }); +} + export function updateArticleTrackedStatus(articleId, courseId, tracked) { return function (dispatch) { return API.updateArticleTrackedStatus(articleId, courseId, tracked).then(response => (dispatch({ @@ -32,3 +75,37 @@ export function updateArticleTrackedStatus(articleId, courseId, tracked) { }))).catch(response => (dispatch({ type: types.API_FAIL, data: response }))); }; } + +export const crossCheckArticleTitle = (articleId, articleTitle, article_mw_page_id) => { + return async (dispatch) => { + try { + // Fetch the page title from Wikipedia API + const response = await fetch( + `https://en.wikipedia.org/w/api.php?action=query&pageids=${article_mw_page_id}&format=json&origin=*` + ); + + if (!response.ok) { + throw new Error(`API request failed with status ${response.status}`); + } + + const apiResponse = await response.json(); + const wikipediaArticleTitle = apiResponse.query.pages[article_mw_page_id]?.title; + + if (wikipediaArticleTitle && wikipediaArticleTitle !== articleTitle) { + const baseUrl = 'https://en.wikipedia.org/wiki/'; + const updatedUrl = `${baseUrl}${wikipediaArticleTitle.replace(/ /g, '_')}`; + + dispatch({ + type: types.UPDATE_ARTICLE_TITLE_AND_URL, + payload: { articleId, title: wikipediaArticleTitle, url: updatedUrl }, + }); + + return wikipediaArticleTitle; + } + + return articleTitle; + } catch (error) { + dispatch({ type: types.API_FAIL, data: error }); + } + }; +}; diff --git a/app/assets/javascripts/components/common/ArticleViewer/containers/ArticleViewer.jsx b/app/assets/javascripts/components/common/ArticleViewer/containers/ArticleViewer.jsx index 4e3cec2294..5720a8f1bf 100644 --- a/app/assets/javascripts/components/common/ArticleViewer/containers/ArticleViewer.jsx +++ b/app/assets/javascripts/components/common/ArticleViewer/containers/ArticleViewer.jsx @@ -27,6 +27,7 @@ import colors from '@components/common/ArticleViewer/constants/colors'; // Actions import { resetBadWorkAlert, submitBadWorkAlert } from '~/app/assets/javascripts/actions/alert_actions.js'; +import { crossCheckArticleTitle } from '@actions/article_actions'; /* Quick summary of the ArticleViewer component's main logic @@ -57,6 +58,10 @@ const ArticleViewer = ({ showOnMount, users, showArticleFinder, showButtonLabel, const [pendingRequest, setPendingRequest] = useState(false); const lastRevisionId = useSelector(state => state.articleDetails[article.id]?.last_revision?.revid); + // State to track whether the article title needs to be verified and updated + // (i.e., if a fetch failed due to the article title being moved) + const [checkArticleTitle, setCheckArticleTitle] = useState(false); + const dispatch = useDispatch(); const ref = useRef(); const isFirstRender = useRef(true); @@ -245,6 +250,8 @@ const ArticleViewer = ({ showOnMount, users, showArticleFinder, showButtonLabel, setFailureMessage(error.message); setFetched(true); setWhoColorFailed(true); + // Set flag to verify and fetch the article title if the fetch failed, possibly due to the article being moved + setCheckArticleTitle(true); }); }; @@ -257,9 +264,32 @@ const ArticleViewer = ({ showOnMount, users, showArticleFinder, showButtonLabel, }).catch((error) => { setWhoColorFailed(true); setFailureMessage(error.message); + // Set flag to verify and fetch the article title if the fetch failed, possibly due to the article being moved + setCheckArticleTitle(true); }); }; + // Function to verify if the article title has changed and fetch updated data accordingly + const verifyAndFetchArticle = async () => { + // Dispatch an action to cross-check the current article title using its ID and MediaWiki page ID + const crossCheckedArticleTitle = await dispatch(crossCheckArticleTitle(article.id, article.title, article.mw_page_id)); + + if (crossCheckedArticleTitle === article.title) { + setWhoColorFailed(false); // Clear the failure state for WhoColor data + setCheckArticleTitle(false); // Stop further title verification checks + fetchParsedArticle(); // Re-fetch the parsed article content with the current title + fetchWhocolorHtml(); // Re-fetch the WhoColor HTML for the article using the current title + } else if (crossCheckArticleTitle !== article.title) { + setFetched(false); // Indicate a loading state until the Redux store updates the new article title and the component re-renders + } + }; + + // Trigger the article title verification and data fetching process if a previous fetch failed + if (checkArticleTitle) { + verifyAndFetchArticle(); + } + + // These are mediawiki user ids, and don't necessarily match the dashboard // database user ids, so we must fetch them by username from the wiki. const fetchUserIds = () => { diff --git a/app/assets/javascripts/constants/articles.js b/app/assets/javascripts/constants/articles.js index 04885ee364..2451898bad 100644 --- a/app/assets/javascripts/constants/articles.js +++ b/app/assets/javascripts/constants/articles.js @@ -7,3 +7,4 @@ export const UPDATE_ARTICLE_TRACKED_STATUS = 'UPDATE_ARTICLE_TRACKED_STATUS'; export const SET_ARTICLES_PAGE = 'SET_ARTICLES_PAGE'; export const ARTICLES_PER_PAGE = 100; export const RESET_PAGES = 'RESET_PAGES'; +export const UPDATE_ARTICLE_TITLE_AND_URL = 'UPDATE_ARTICLE_TITLE_AND_URL'; diff --git a/app/assets/javascripts/reducers/articles.js b/app/assets/javascripts/reducers/articles.js index ace7c924da..93ad2010a5 100644 --- a/app/assets/javascripts/reducers/articles.js +++ b/app/assets/javascripts/reducers/articles.js @@ -9,7 +9,8 @@ import { UPDATE_ARTICLE_TRACKED_STATUS, SET_ARTICLES_PAGE, ARTICLES_PER_PAGE, - RESET_PAGES + RESET_PAGES, + UPDATE_ARTICLE_TITLE_AND_URL, } from '../constants'; const initialState = { @@ -147,6 +148,16 @@ export default function articles(state = initialState, action) { return { ...state, currentPage: 1, totalPages: action.totalPages }; } + case UPDATE_ARTICLE_TITLE_AND_URL: { + return { + ...state, + articles: _.map(state.articles, article => + ((action.payload.articleId === article.id) + ? { ...article, title: action.payload.title, url: action.payload.url } + : article)) + }; + } + default: return state; } diff --git a/app/views/courses/articles.json.jbuilder b/app/views/courses/articles.json.jbuilder index e9da0c4f5f..ab0fab3363 100644 --- a/app/views/courses/articles.json.jbuilder +++ b/app/views/courses/articles.json.jbuilder @@ -3,14 +3,12 @@ json.course do json.articles @course.articles_courses.includes(article: :wiki).limit(@limit) do |ac| article = ac.article - json.call(ac, :character_sum, :references_count, :view_count, :new_article, :tracked) - json.call(article, :id, :namespace, :rating, :deleted) + json.call(ac, :character_sum, :references_count, :view_count, :new_article, :tracked, :user_ids) + json.call(article, :id, :namespace, :rating, :deleted, :mw_page_id, :url) json.title article.full_title json.language article.wiki.language json.project article.wiki.project - json.url article.url json.rating_num rating_priority(article.rating) json.pretty_rating rating_display(article.rating) - json.user_ids ac.user_ids end end From d8d46eb0b08b0bab6914de28f558c99e3d4a040a Mon Sep 17 00:00:00 2001 From: Abishek Das Date: Tue, 31 Dec 2024 22:19:36 +0530 Subject: [PATCH 57/66] fix-failing-test: apply 10-second wait for article viewer spec tests --- spec/features/article_viewer_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/article_viewer_spec.rb b/spec/features/article_viewer_spec.rb index b1386fe8f8..f585be486d 100644 --- a/spec/features/article_viewer_spec.rb +++ b/spec/features/article_viewer_spec.rb @@ -20,7 +20,7 @@ it 'shows list of students who edited the article' do visit "/courses/#{course.slug}/articles" find('button.icon-article-viewer').click - expect(page).to have_content("Edits by: \nRagesoss") + expect(page).to have_content("Edits by: \nRagesoss", wait: 10) # once authorship date loads within(:css, '.article-viewer-legend.user-legend-name.user-highlight-1', wait: 20) do # click to scroll to next highlight @@ -33,7 +33,7 @@ login_as(instructor) visit "/courses/#{course.slug}/articles" find('button.icon-article-viewer').click - expect(page).to have_content("Edits by: \nRagesoss") + expect(page).to have_content("Edits by: \nRagesoss", wait: 10) find('a', text: 'Quality Problems?').click fill_in 'submit-bad-work-alert', with: 'Something has gone terribly wrong' click_button 'Notify Wiki Expert' From d43dc2804d91327dbb6a071ed0e7401e9c4e43db Mon Sep 17 00:00:00 2001 From: 0x_!nit <113853868+shishiro26@users.noreply.github.com> Date: Wed, 1 Jan 2025 00:47:54 +0530 Subject: [PATCH 58/66] fix:issue with meeting dates extending beyond the ending date. (#6028) * Fix: Improve meeting date extraction and validation in Week component * Refactored the regex into a named-component * changed the date extraction to the course_date_utils.js * fixed the linting errors * Fix issue with meeting display after the end date * fix rspec test * fix rspec tests and check if meetings go beyond timeline.end --- .../javascripts/utils/course_date_utils.js | 7 ++++++- spec/features/course_overview_spec.rb | 20 +++++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/utils/course_date_utils.js b/app/assets/javascripts/utils/course_date_utils.js index fb68397f65..7eedf49ae9 100644 --- a/app/assets/javascripts/utils/course_date_utils.js +++ b/app/assets/javascripts/utils/course_date_utils.js @@ -141,6 +141,11 @@ const CourseDateUtils = { for (const week of range(0, (courseWeeks - 1), true)) { weekStart = addWeeks(startOfWeek(toDate(course.timeline_start)), week); + let weekendDate = endOfWeek(toDate(weekStart)); + if (isAfter(weekendDate, toDate(course.end))) { + weekendDate = toDate(course.end); + } + // Account for the first partial week, which may not have 7 days. let firstDayOfWeek; if (week === 0) { @@ -153,7 +158,7 @@ const CourseDateUtils = { // eslint-disable-next-line no-restricted-syntax for (const i of range(firstDayOfWeek, 6, true)) { const day = addDays(weekStart, i); - if (course && this.courseMeets(course.weekdays, i, format(day, 'yyyyMMdd'), exceptions)) { + if (course && this.courseMeets(course.weekdays, i, format(day, 'yyyyMMdd'), exceptions) && !isAfter(day, weekendDate)) { ms.push(format(day, 'EEEE (MM/dd)')); } } diff --git a/spec/features/course_overview_spec.rb b/spec/features/course_overview_spec.rb index f6138eb025..176e2687e6 100644 --- a/spec/features/course_overview_spec.rb +++ b/spec/features/course_overview_spec.rb @@ -48,14 +48,17 @@ end context 'when course starts in future' do - let(:timeline_start) { '2025-02-11'.to_date + 2.weeks } # a Tuesday - + let(:course_start) { '2025-02-11'.to_date } + let(:course_end) { course_start + 6.months } + let(:timeline_start) { '2025-02-11'.to_date + 2.weeks } + let(:timeline_end) { course_end.to_date } + before do course.update(timeline_start:) visit "/courses/#{course.slug}" sleep 1 end - + it 'displays week activity for the first week' do within '.course__this-week' do expect(page).to have_content('First Active Week') @@ -63,9 +66,18 @@ end within '.week-range' do expect(page).to have_content(timeline_start.beginning_of_week(:sunday).strftime('%m/%d')) - # Class normally meets on Sun, W, Sat, but timeline starts on Tuesday. end within '.margin-bottom' do + meeting_dates = [ + Date.parse('2025-02-23'), # Sunday (02/23) + Date.parse('2025-02-26'), # Wednesday (02/26) + Date.parse('2025-03-01') # Saturday (03/01) + ] + + meeting_dates.each do |meeting_date| + expect(meeting_date).to be_between(timeline_start, timeline_end).or be_between(course_start, course_end) + end + expect(page).to have_content('Meetings: Wednesday (02/26), Saturday (03/01)') end within '.week-index' do From b382331c8d5ed24a2e6d4ec3666c8bb5e1d75941 Mon Sep 17 00:00:00 2001 From: empty-codes Date: Tue, 31 Dec 2024 20:27:40 +0100 Subject: [PATCH 59/66] Restructures contents + Adds details about 3rd party APIs and tools the dashboard relies on --- docs/admin_guide.md | 98 +++++++++++++++++++++++++++++++++------------ 1 file changed, 73 insertions(+), 25 deletions(-) diff --git a/docs/admin_guide.md b/docs/admin_guide.md index 42879efb43..81b18e6d28 100644 --- a/docs/admin_guide.md +++ b/docs/admin_guide.md @@ -1,38 +1,39 @@ # Admin Guide for Program & Events Dashboard +The **Programs & Events Dashboard** ([outreachdashboard.wmflabs.org](https://outreachdashboard.wmflabs.org)) is a web application designed to support the global Wikimedia community in organizing various programs, including edit-a-thons, education initiatives, and other events. See the **[Source Code](https://github.com/WikiEducationFoundation/WikiEduDashboard/tree/wmflabs)** and **[Phabricator Project](https://phabricator.wikimedia.org/project/manage/1052/)** for more details. -This guide provides an overview of the Program & Events Dashboard. It also offers resources for managing and troubleshooting the system. +This guide provides an overview of the **Program & Events Dashboard** infrastructure, detailing the servers, tools, and third-party dependencies that power the system. It also provides resources for managing and troubleshooting the system. ## Table of Contents -1. [Overview](#overview) +1. [Infrastructure Overview](#infrastructure-overview) + - [Servers](#servers) + - [Integrated Toolforge Tools](#integrated-toolforge-tools) + - [Other Integrated APIs and Third-Party Dependencies](#other-integrated-apis-and-third-party-dependencies) 2. [Monitoring and Logs](#monitoring-and-logs) - [Toolforge](#toolforge) - [Cloud VPS](#cloud-vps) 3. [Troubleshooting](#troubleshooting) - - [Web Server Issues](#web-server-issues) - - [Database Issues](#database-issues) - - [Data Dumps and Recovery](#data-dumps-and-recovery) + - [Web Server Issues](#web-server-issues) + - [Database Issues](#database-issues) + - [Data Dumps and Recovery](#data-dumps-and-recovery) 4. [More Resources](#more-resources) -## Overview +## Infrastructure Overview +The **Program & Events Dashboard** is hosted within the **Wikimedia Cloud VPS** project [globaleducation](https://openstack-browser.toolforge.org/project/globaleducation), which provides the infrastructure for all servers, allowing the dashboard to run on virtual machines that are flexible and easily managed within Wikimedia Cloud. -The **Programs & Events Dashboard** ([outreachdashboard.wmflabs.org](https://outreachdashboard.wmflabs.org)) is a web application designed to support the global Wikimedia community in organizing various programs, including edit-a-thons, education initiatives, and other events. **[Source Code](https://github.com/WikiEducationFoundation/WikiEduDashboard/tree/wmflabs)** **[Phabricator Project](https://phabricator.wikimedia.org/project/manage/1052/)** - -### Infrastructure Overview -- **Toolforge Tool**: [wikiedudashboard](https://toolsadmin.wikimedia.org/tools/id/wikiedudashboard) - - **Source code: [WikiEduDashboardTools](https://github.com/WikiEducationFoundation/WikiEduDashboardTools)**: A collection of PHP endpoints that retrieve revision and article data from Wikimedia Replica databases in the Wikimedia Cloud environment. - - **Deployed at**: [wikiedudashboard.toolforge.org](https://wikiedudashboard.toolforge.org/), [replica-revision-tools.wmcloud.org](https://replica-revision-tools.wmcloud.org/) - -- **Cloud VPS Project**: [globaleducation](https://openstack-browser.toolforge.org/project/globaleducation) +The dashboard relies on several core servers and external tools to function. These components ensure that different tasks are isolated to avoid bottlenecks and improve system performance. ### Servers +The dashboard operates on a distributed server architecture to handle web requests, process background jobs, and store application data. Each server is dedicated to specific roles, minimizing competition for resources and improving reliability by isolating potential bottlenecks and failures. + +Below is a breakdown of the key servers and their roles within the infrastructure: 1. **Web Server** - **`peony-web.globaleducation.eqiad1.wikimedia.cloud`** - Hosts the main web application and core Sidekiq processes using **RVM (Ruby Version Manager)**, **Phusion Passenger**, and **Apache**. - **Capistrano** is used for deployments - Sidekiq processes hosted: - - `sidekiq-default`: Handles transactional jobs (e.g., wiki edits, email notifications). - - `sidekiq-constant`: Manages frequently run tasks (e.g., adding courses to update queues). + - `sidekiq-default`: Manages frequently run tasks (e.g., adding courses to update queues). + - `sidekiq-constant`: Handles transactional jobs (e.g., wiki edits, email notifications). - `sidekiq-daily`: Executes long-running daily update tasks. 2. **Sidekiq Servers**: These dedicated servers handle the other Sidekiq processes to isolate bottlenecks and failures: @@ -41,18 +42,68 @@ The **Programs & Events Dashboard** ([outreachdashboard.wmflabs.org](https://out - **`peony-sidekiq-3.globaleducation.eqiad1.wikimedia.cloud`**: Hosts `sidekiq-long` for long-running course updates with higher queue latency. 3. **Database Server** - - **`peony-database.globaleducation.eqiad1.wikimedia.cloud`**: Manages the Dashboard's primary database. + - **`peony-database.globaleducation.eqiad1.wikimedia.cloud`**: Stores program, user, and revision data. It supports the dashboard’s data queries and updates. 4. **Redis Server** - - **`p-and-e-dashboard-redis.globaleducation.eqiad1.wikimedia.cloud`**: Shared across all Sidekiq processes for task queuing and caching. + - **`p-and-e-dashboard-redis.globaleducation.eqiad1.wikimedia.cloud`**: Stores all task (job) details and is shared across all Sidekiq processes for task queuing and caching. + +### Integrated Toolforge Tools + +- **[wikiedudashboard](https://toolsadmin.wikimedia.org/tools/id/wikiedudashboard)** + A collection of PHP endpoints used to retrieve revision and article data from Wikimedia Replica databases. + See [replica.rb](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/lib/replica.rb) for an example of its usage. + **[[Live Tool](https://replica-revision-tools.wmcloud.org/), [Source Code](https://github.com/WikiEducationFoundation/WikiEduDashboardTools)]** + +- **[Reference Counter API](https://toolsadmin.wikimedia.org/tools/id/reference-counter)** + Flask API to count the number of existing references in a specified revision ID for a Wiki. This API has two main endpoints to retrieve number of references for a given revision, one using [wikitext](https://gitlab.wikimedia.org/toolforge-repos/reference-counter#based-on-wikitext), the other [using HTML](https://gitlab.wikimedia.org/toolforge-repos/reference-counter#based-on-html). + See [reference_counter_api.rb](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/lib/reference_counter_api.rb) for an example of its usage. + **[[Live Tool](https://reference-counter.toolforge.org/), [Source Code](https://gitlab.wikimedia.org/toolforge-repos/reference-counter), [Phabricator Documentation](https://phabricator.wikimedia.org/T352177)]** + +- **[Suspected Plagiarism API](https://toolsadmin.wikimedia.org/tools/id/ruby-suspected-plagiarism)** + API for fetching recent suspected plagiarism detected by CopyPatrol and accessing Turnitin reports. + See [plagiabot_importer.rb](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/lib/importers/plagiabot_importer.rb) for an example of its usage. + **[[Live Tool](https://ruby-suspected-plagiarism.toolforge.org/), [Source Code](https://github.com/WikiEducationFoundation/ruby-suspected-plagiarism)]** + +- **[Copypatrol](https://toolsadmin.wikimedia.org/tools/id/copypatrol)** + A plagiarism detection tool, that allows you to see recent Wikipedia edits that are flagged as possible copyright violations. It serves as the database for the ruby-suspected-plagiarism tool. + **[[Live Tool](https://copypatrol.wmcloud.org/en), [Source Code](https://github.com/wikimedia/CopyPatrol/), [Documentation](https://meta.wikimedia.org/wiki/CopyPatrol), [Phabricator Project](https://phabricator.wikimedia.org/project/profile/1638/)]** + +- **[PagePile](https://toolsadmin.wikimedia.org/tools/id/pagepile)** + Manages static lists of Wiki pages. You can use a PetScan query (among other options) to create a PagePile, essentially creating a permanent snapshot of the PetScan query results. You can also create a PagePile from a simple one-per-line text list of article titles. + See [pagepile_scoping.jsx](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/app/assets/javascripts/components/course_creator/scoping_methods/pagepile_scoping.jsx) and [pagepile_api.rb](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/lib/pagepile_api.rb) for examples of its usage. + **[[Live Tool](https://pagepile.toolforge.org/), [Source Code](https://bitbucket.org/magnusmanske/pagepile/src/master/), [Documentation](https://pagepile.toolforge.org/howto.html)]** +### Other Integrated APIs and Third-Party Dependencies + +- **[PetScan](https://petscan.wmcloud.org/)** + A powerful tool that can assemble lists of articles using a wide variety of data sources (including categories and templates, as well incoming and outgoing links, Wikidata relationships, and more). Users create a query on the PetScan website, which returns a PSID for that query, and that PSID is how the Dashboard connects to the PetScan API to get the list of articles. PetScan queries are dynamic; while the query for a given PSID cannot be modified, the results may change each time the the query is run, based on changes that happened on Wikipedia and Wikidata. + See [petscan_scoping.jsx](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/app/assets/javascripts/components/course_creator/scoping_methods/petscan_scoping.jsx) and [petscan_api.rb](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/lib/petscan_api.rb#L5) for examples of its usage. + **[[Source Code](https://github.com/magnusmanske/petscan_rs), [Documentation](https://meta.wikimedia.org/wiki/PetScan/en)]** + +- **[WikiWho API](https://wikiwho-api.wmcloud.org/en/api/v1.0.0-beta/)** + Set of APIs to parse historical revisions of Wikipedia articles, providing detailed provenance of each token (word) in terms of who added, removed, or reintroduced it across different revisions. + See [`ArticleViewerAPI.js`](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/app/assets/javascripts/components/common/ArticleViewer/utils/ArticleViewerAPI.js#L96) and the [`wikiwhoColorURL`](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/app/assets/javascripts/components/common/ArticleViewer/utils/URLBuilder.js#L35) for examples of its usage. + **[[Source Code](https://github.com/wikimedia/wikiwho_api), [Documentation](https://wikiwho-api.wmcloud.org/gesis_home)]** + +- **[WhoColor API](https://wikiwho-api.wmcloud.org/en/whocolor/v1.0.0-beta/)** + Set of APIs built on top of the WikiWho API that allow for the visualization of authorship data by color-coding tokens in the text based on their original authors. The dashboard employs this to show authorship data on its dashboard for students. + **[[Source Code](https://github.com/wikimedia/wikiwho_api), [Documentation](https://wikiwho-api.wmcloud.org/gesis_home)]** + +- **[WikidataDiffAnalyzer](https://github.com/WikiEducationFoundation/wikidata-diff-analyzer)** + Ruby gem for analyzing differences between revisions. + See [update_wikidata_stats.rb](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/app/services/update_wikidata_stats.rb#L91) for an example of its usage. + **[[Source Code and Documentation](https://github.com/WikiEducationFoundation/wikidata-diff-analyzer)]** + +- **[Liftwing API](https://api.wikimedia.org/wiki/Lift_Wing_API/Reference)** + Makes predictions about pages and edits using machine learning. The dashboard uses this API to fetch items and article quality data. + See [article_finder_action.js](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/app/assets/javascripts/actions/article_finder_action.js#L18) and [lift_wing_api.rb](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/lib/lift_wing_api.rb#L8) for examples of its usage. + **[[Source Code](https://gerrit.wikimedia.org/g/machinelearning/liftwing/), [Documentation](https://api.wikimedia.org/wiki/Lift_Wing_API), [Phabricator Project](https://phabricator.wikimedia.org/project/profile/5020/)]** + ## Monitoring and Logs #### Toolforge -- [Kubernetes Namespace Details](https://k8s-status.toolforge.org/namespaces/tool-wikiedudashboard/) -- [Kubernetes Pod Details](https://k8s-status.toolforge.org/namespaces/tool-wikiedudashboard/pods/wikiedudashboard-5954f86c86-pm8d5/) - +To view Kubernetes namespace details for a Toolforge tool, go to https://k8s-status.toolforge.org/namespaces/tool-toolName/, replacing `toolName` with the name of the tool. #### Cloud VPS - [Grafana](https://grafana.wmcloud.org/d/0g9N-7pVz/cloud-vps-project-board?orgId=1&var-project=globaleducation) @@ -60,7 +111,6 @@ The **Programs & Events Dashboard** ([outreachdashboard.wmflabs.org](https://out - [Alerts](https://prometheus-alerts.wmcloud.org/?q=%40state%3Dactive&q=project%3Dglobaleducation) - [Puppet agent logs for the globaleducation project](https://grafana.wmcloud.org/d/SQM7MJZSz/cloud-vps-puppet-agents?orgId=1&var-project=globaleducation&from=now-2d&to=now) - ## Troubleshooting ### Web Server Issues @@ -86,9 +136,7 @@ The **Programs & Events Dashboard** ([outreachdashboard.wmflabs.org](https://out 3. Once the dump is complete, drop the table. 4. Remove the database from recovery mode and restore the table. -### Third-Party Dependencies -Issues could also be caused by maintenance or outages in third-party dependencies such as Openstack, Toolforge, or other services. - +Issues could also be caused by maintenance or outages in third-party dependencies or other services stated above. ## More Resources - [Toolforge Documentation](https://wikitech.wikimedia.org/wiki/Help:Toolforge) From 14e2da51531b496e5a1db08aa48e258380f56022 Mon Sep 17 00:00:00 2001 From: Sage Ross Date: Tue, 31 Dec 2024 12:05:47 -0800 Subject: [PATCH 60/66] Fix linting violations --- spec/features/course_overview_spec.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/spec/features/course_overview_spec.rb b/spec/features/course_overview_spec.rb index 176e2687e6..5bcb827e54 100644 --- a/spec/features/course_overview_spec.rb +++ b/spec/features/course_overview_spec.rb @@ -52,13 +52,13 @@ let(:course_end) { course_start + 6.months } let(:timeline_start) { '2025-02-11'.to_date + 2.weeks } let(:timeline_end) { course_end.to_date } - + before do course.update(timeline_start:) visit "/courses/#{course.slug}" sleep 1 end - + it 'displays week activity for the first week' do within '.course__this-week' do expect(page).to have_content('First Active Week') @@ -73,11 +73,13 @@ Date.parse('2025-02-26'), # Wednesday (02/26) Date.parse('2025-03-01') # Saturday (03/01) ] - - meeting_dates.each do |meeting_date| - expect(meeting_date).to be_between(timeline_start, timeline_end).or be_between(course_start, course_end) + + meeting_dates.each do |meeting_date| # rubocop:disable RSpec/IteratedExpectation + expect(meeting_date) + .to be_between(timeline_start, timeline_end) + .or be_between(course_start, course_end) end - + expect(page).to have_content('Meetings: Wednesday (02/26), Saturday (03/01)') end within '.week-index' do From 5ba44516ab02c7a6c0014c9cbd413bf73c217ebb Mon Sep 17 00:00:00 2001 From: empty-codes Date: Tue, 31 Dec 2024 23:09:27 +0100 Subject: [PATCH 61/66] Updates tool descriptions to focus on how the Dashboard uses each + Minor formatting edits/ corrections --- docs/admin_guide.md | 46 +++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/docs/admin_guide.md b/docs/admin_guide.md index 81b18e6d28..88d24eff4c 100644 --- a/docs/admin_guide.md +++ b/docs/admin_guide.md @@ -37,9 +37,9 @@ Below is a breakdown of the key servers and their roles within the infrastructur - `sidekiq-daily`: Executes long-running daily update tasks. 2. **Sidekiq Servers**: These dedicated servers handle the other Sidekiq processes to isolate bottlenecks and failures: - - **`peony-sidekiq.globaleducation.eqiad1.wikimedia.cloud`**: Hosts `sidekiq-short` for short-running course updates. + - **`peony-sidekiq.globaleducation.eqiad1.wikimedia.cloud`**: Hosts `sidekiq-long` for long-running course updates with higher queue latency. - **`peony-sidekiq-medium.globaleducation.eqiad1.wikimedia.cloud`**: Hosts `sidekiq-medium` for typical course updates. - - **`peony-sidekiq-3.globaleducation.eqiad1.wikimedia.cloud`**: Hosts `sidekiq-long` for long-running course updates with higher queue latency. + - **`peony-sidekiq-3.globaleducation.eqiad1.wikimedia.cloud`**: Hosts `sidekiq-short` for short-running course updates. 3. **Database Server** - **`peony-database.globaleducation.eqiad1.wikimedia.cloud`**: Stores program, user, and revision data. It supports the dashboard’s data queries and updates. @@ -50,54 +50,56 @@ Below is a breakdown of the key servers and their roles within the infrastructur ### Integrated Toolforge Tools - **[wikiedudashboard](https://toolsadmin.wikimedia.org/tools/id/wikiedudashboard)** - A collection of PHP endpoints used to retrieve revision and article data from Wikimedia Replica databases. - See [replica.rb](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/lib/replica.rb) for an example of its usage. - **[[Live Tool](https://replica-revision-tools.wmcloud.org/), [Source Code](https://github.com/WikiEducationFoundation/WikiEduDashboardTools)]** + The Dashboard uses this tool's PHP endpoints to query Wikimedia Replica databases for detailed revision and article data. The specific replica database the tool connects to is dependent on the wiki being queried. These endpoints support features like retrieving user contributions, identifying existing articles or revisions, and checking for deleted content. For example, the Dashboard uses the `/revisions.php` endpoint to fetch revisions by specific users within a time range, and `/articles.php` to verify the existence of articles or revisions. See [replica.rb](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/lib/replica.rb) for implementation details. + + **[[Live Tool](https://replica-revision-tools.wmcloud.org/), [Source Code](https://github.com/WikiEducationFoundation/WikiEduDashboardTools)]** - **[Reference Counter API](https://toolsadmin.wikimedia.org/tools/id/reference-counter)** - Flask API to count the number of existing references in a specified revision ID for a Wiki. This API has two main endpoints to retrieve number of references for a given revision, one using [wikitext](https://gitlab.wikimedia.org/toolforge-repos/reference-counter#based-on-wikitext), the other [using HTML](https://gitlab.wikimedia.org/toolforge-repos/reference-counter#based-on-html). - See [reference_counter_api.rb](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/lib/reference_counter_api.rb) for an example of its usage. + The Reference Counter API is used to retrieve the number of references in a specified revision ID from a Wiki. The Dashboard interacts with the API through the [`ReferenceCounterApi`](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/lib/reference_counter_api.rb) class, which handles requests for reference counts by revision ID and processes multiple revisions in batch. It's important to note that the `ReferenceCounterApi` class and the `reference-counter` Toolforge API do not support Wikidata, as it uses a different method for calculating references. + **[[Live Tool](https://reference-counter.toolforge.org/), [Source Code](https://gitlab.wikimedia.org/toolforge-repos/reference-counter), [Phabricator Documentation](https://phabricator.wikimedia.org/T352177)]** - **[Suspected Plagiarism API](https://toolsadmin.wikimedia.org/tools/id/ruby-suspected-plagiarism)** - API for fetching recent suspected plagiarism detected by CopyPatrol and accessing Turnitin reports. - See [plagiabot_importer.rb](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/lib/importers/plagiabot_importer.rb) for an example of its usage. + This API is used to detect and report suspected plagiarism in course-related content. It leverages CopyPatrol to detect instances of potential plagiarism by comparing revisions of Wikipedia articles. The API then retrieves data on suspected plagiarism, which includes information such as the revision ID, the user responsible, and the article involved. The [`PlagiabotImporter`](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/lib/importers/plagiabot_importer.rb) class uses this data to identify recent instances of suspected plagiarism and match them with relevant revisions in the Dashboard's database. If a new case is found, an alert is generated for suspected plagiarism in course materials and sent to content experts for review. + **[[Live Tool](https://ruby-suspected-plagiarism.toolforge.org/), [Source Code](https://github.com/WikiEducationFoundation/ruby-suspected-plagiarism)]** - **[Copypatrol](https://toolsadmin.wikimedia.org/tools/id/copypatrol)** - A plagiarism detection tool, that allows you to see recent Wikipedia edits that are flagged as possible copyright violations. It serves as the database for the ruby-suspected-plagiarism tool. + A plagiarism detection tool, that allows you to see recent Wikipedia edits that are flagged as possible copyright violations. It is responsible for detecting instances of potential plagiarism by comparing revisions of Wikipedia articles. + **[[Live Tool](https://copypatrol.wmcloud.org/en), [Source Code](https://github.com/wikimedia/CopyPatrol/), [Documentation](https://meta.wikimedia.org/wiki/CopyPatrol), [Phabricator Project](https://phabricator.wikimedia.org/project/profile/1638/)]** - **[PagePile](https://toolsadmin.wikimedia.org/tools/id/pagepile)** - Manages static lists of Wiki pages. You can use a PetScan query (among other options) to create a PagePile, essentially creating a permanent snapshot of the PetScan query results. You can also create a PagePile from a simple one-per-line text list of article titles. - See [pagepile_scoping.jsx](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/app/assets/javascripts/components/course_creator/scoping_methods/pagepile_scoping.jsx) and [pagepile_api.rb](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/lib/pagepile_api.rb) for examples of its usage. + PagePile manages static lists of Wiki pages. The Dashboard utilizes it to fetch a permanent snapshot of article titles through PagePile IDs or URLs. This is integrated into the course creation process, where users can input PagePile IDs or URLs to define a set of articles for the course. The [`PagePileApi`](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/lib/pagepile_api.rb) class is responsible for retrieving page titles from PagePile, ensuring the category's wiki is consistent with the PagePile data, and updating the system with the retrieved titles. The data is then used to scope course materials to specific articles - see [pagepile_scoping.jsx](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/app/assets/javascripts/components/course_creator/scoping_methods/pagepile_scoping.jsx). + **[[Live Tool](https://pagepile.toolforge.org/), [Source Code](https://bitbucket.org/magnusmanske/pagepile/src/master/), [Documentation](https://pagepile.toolforge.org/howto.html)]** ### Other Integrated APIs and Third-Party Dependencies - **[PetScan](https://petscan.wmcloud.org/)** - A powerful tool that can assemble lists of articles using a wide variety of data sources (including categories and templates, as well incoming and outgoing links, Wikidata relationships, and more). Users create a query on the PetScan website, which returns a PSID for that query, and that PSID is how the Dashboard connects to the PetScan API to get the list of articles. PetScan queries are dynamic; while the query for a given PSID cannot be modified, the results may change each time the the query is run, based on changes that happened on Wikipedia and Wikidata. - See [petscan_scoping.jsx](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/app/assets/javascripts/components/course_creator/scoping_methods/petscan_scoping.jsx) and [petscan_api.rb](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/lib/petscan_api.rb#L5) for examples of its usage. - **[[Source Code](https://github.com/magnusmanske/petscan_rs), [Documentation](https://meta.wikimedia.org/wiki/PetScan/en)]** + The PetScan API is used in the Dashboard to integrate dynamic lists of articles based on user-defined queries. Users can enter PetScan IDs (PSIDs) or URLs to fetch a list of articles relevant to a course. The [`PetScanApi`](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/lib/petscan_api.rb#L5) class handles retrieving the list of page titles associated with a given PSID by querying PetScan's API. This data is used for scoping course materials to specific sets of articles - see [petscan_scoping.jsx](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/app/assets/javascripts/components/course_creator/scoping_methods/petscan_scoping.jsx), ensuring the Dashboard reflects the most up-to-date information from PetScan queries. The system ensures proper error handling for invalid or unreachable PSIDs to avoid disrupting the course creation process. + + **[[Source Code](https://github.com/magnusmanske/petscan_rs), [Documentation](https://meta.wikimedia.org/wiki/PetScan/en)]** - **[WikiWho API](https://wikiwho-api.wmcloud.org/en/api/v1.0.0-beta/)** - Set of APIs to parse historical revisions of Wikipedia articles, providing detailed provenance of each token (word) in terms of who added, removed, or reintroduced it across different revisions. - See [`ArticleViewerAPI.js`](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/app/assets/javascripts/components/common/ArticleViewer/utils/ArticleViewerAPI.js#L96) and the [`wikiwhoColorURL`](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/app/assets/javascripts/components/common/ArticleViewer/utils/URLBuilder.js#L35) for examples of its usage. + The WikiWho API is used in the Dashboard to parse historical revisions of Wikipedia articles and track the provenance of each word in the article. This data is particularly useful for displaying authorship information, such as identifying who added, removed, or reintroduced specific tokens (words) across different revisions. The [`URLBuilder`](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/app/assets/javascripts/components/common/ArticleViewer/utils/URLBuilder.js#L35) class constructs the necessary URLs to interact with the WikiWho API, allowing the Dashboard to fetch parsed article data and token-level authorship highlights. This data is then used in the [`ArticleViewer`](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/app/assets/javascripts/components/common/ArticleViewer/utils/ArticleViewerAPI.js#L96) component to enhance the display of articles by showing detailed authorship information, providing insights into the contributions of different editors over time. + **[[Source Code](https://github.com/wikimedia/wikiwho_api), [Documentation](https://wikiwho-api.wmcloud.org/gesis_home)]** - **[WhoColor API](https://wikiwho-api.wmcloud.org/en/whocolor/v1.0.0-beta/)** - Set of APIs built on top of the WikiWho API that allow for the visualization of authorship data by color-coding tokens in the text based on their original authors. The dashboard employs this to show authorship data on its dashboard for students. + The WhoColor API is used in the Dashboard to add color-coding to the authorship data provided by the WikiWho API. It enhances the parsed article revisions by highlighting each token (word) with a color corresponding to its original author, making it easier to visualize contributions. The Dashboard processes this color-coded data by using the [`highlightAuthors`](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/app/assets/javascripts/components/common/ArticleViewer/containers/ArticleViewer.jsx#L163) function, which replaces the span elements in the HTML with styled versions that include user-specific color classes. This allows the [`ArticleViewer`](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/app/assets/javascripts/components/common/ArticleViewer/utils/ArticleViewerAPI.js#L96) component to display the article text with visual cues, highlighting which user contributed each part of the article, helping quick identification of the contributions of different authors. + **[[Source Code](https://github.com/wikimedia/wikiwho_api), [Documentation](https://wikiwho-api.wmcloud.org/gesis_home)]** - **[WikidataDiffAnalyzer](https://github.com/WikiEducationFoundation/wikidata-diff-analyzer)** - Ruby gem for analyzing differences between revisions. - See [update_wikidata_stats.rb](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/app/services/update_wikidata_stats.rb#L91) for an example of its usage. + The WikidataDiffAnalyzer gem is used to analyze differences between Wikidata revisions. It is utilized by the [`update_wikidata_stats.rb`](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/app/services/update_wikidata_stats.rb#L91) service to process a list of revision IDs and determine the changes made between them, such as diffs added, removed, or changed claims, references, and labels. The results of the analysis are serialized and stored in the summary field of Wikidata revisions, providing detailed statistics about the nature of the edits. This enables the Dashboard to track and display revision-level changes. + **[[Source Code and Documentation](https://github.com/WikiEducationFoundation/wikidata-diff-analyzer)]** - **[Liftwing API](https://api.wikimedia.org/wiki/Lift_Wing_API/Reference)** - Makes predictions about pages and edits using machine learning. The dashboard uses this API to fetch items and article quality data. - See [article_finder_action.js](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/app/assets/javascripts/actions/article_finder_action.js#L18) and [lift_wing_api.rb](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/lib/lift_wing_api.rb#L8) for examples of its usage. + The Liftwing API is used to fetch article quality and item quality data by making predictions about pages and edits using machine learning models. The Dashboard interacts with this API to assess the quality of articles and revisions, utilizing the [`LiftWingApi`](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/lib/lift_wing_api.rb#L8) service to retrieve scores and features associated with each revision. The [`article_finder_action.js`](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/app/assets/javascripts/actions/article_finder_action.js#L18) class is responsible for fetching and processing article data. It takes the revision IDs from fetched revision data and sends them to the LiftWing API for processing by calling the [`fetchPageRevisionScore`](https://github.com/WikiEducationFoundation/WikiEduDashboard/blob/wmflabs/app/assets/javascripts/actions/article_finder_action.js#L180) function. The LiftWing API then processes the revision data and returns the quality scores for the articles. + **[[Source Code](https://gerrit.wikimedia.org/g/machinelearning/liftwing/), [Documentation](https://api.wikimedia.org/wiki/Lift_Wing_API), [Phabricator Project](https://phabricator.wikimedia.org/project/profile/5020/)]** ## Monitoring and Logs From efc5e329e7110a3d4e8619b4804962232f2ad4c8 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 2 Jan 2025 13:20:45 +0100 Subject: [PATCH 62/66] Localisation updates from https://translatewiki.net. --- config/locales/de.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/config/locales/de.yml b/config/locales/de.yml index 84c3af0700..3d4be0d9fc 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -18,6 +18,7 @@ # Author: Metalhead64 # Author: Ngschaider # Author: Sebastian Wallroth +# Author: SergeCroise # Author: Taresi # Author: ThisCarthing # Author: ToBeFree @@ -1598,4 +1599,10 @@ de: title: Titel cancel_note_creation: Abbrechen save_note: Speichern + weekday_picker: + aria: + weekday_select: '{{weekday}} Drücken Sie die Eingabetaste, um auszuwählen' + weekday_selected: '{{weekday}} ausgewählt Drücken Sie die Eingabetaste, um die + Auswahl aufzuheben' + weekday_unselected: '{{weekday}} Nicht ausgewählt' ... From 3d3a960857573fb96e9ca41e68be0e99c8178259 Mon Sep 17 00:00:00 2001 From: empty-codes Date: Fri, 3 Jan 2025 00:13:13 +0100 Subject: [PATCH 63/66] Adds links to admin guide in relevant docs + Add link to main README in admin guide --- CONTRIBUTING.md | 1 + README.md | 1 + docs/README.md | 1 + docs/admin_guide.md | 5 ++++- 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 213f0572de..f593168b15 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,6 +36,7 @@ If you're a new developer and you're looking for an easy way to get involved, tr - [MediaWiki API Sandbox](https://en.wikipedia.org/wiki/Special%3aApiSandbox) - [Quarry](http://quarry.wmflabs.org/): Public querying interface for the Labs replica database. Very useful for testing SQL queries and for figuring out what data is available. - [Guide to the front end](docs/frontend.md) +- [Admin Guide](docs/admin_guide.md): Overview of the Dashboard infrastructure, including servers, tools, dependencies, and troubleshooting resources. - [Vagrant](https://github.com/marxarelli/wikied-vagrant): a configuration to quickly get a development environment up and running using Vagrant. If you already have VirtualBox and/or Vagrant on your machine, this might be a simple way to set up a dev environment. However, it is not actively maintained. If you try it and run into problems, let us know! #### Code Style diff --git a/README.md b/README.md index a37f079398..9713a0bb3c 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ This project welcomes contributions, and we try to be as newbie-friendly as poss - [Interface strings & Internationalization](docs/i18n.md) - [OAuth setup](docs/oauth.md) - [Deployment](docs/deploy.md) +- [Admin Guide](docs/admin_guide.md) - Overview of the Dashboard infrastructure, including servers, tools, dependencies, and troubleshooting resources. - [Tools & Integrations](docs/tools.md) - [Using Docker for development](docs/docker.md) - [Model diagram](erd.pdf) diff --git a/docs/README.md b/docs/README.md index 45b1264fb4..37a17ef274 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,6 +12,7 @@ - [Contributing](../CONTRIBUTING.md) - [Development Process outline](dev_process.md) - [Deployment](deploy.md) +- [Admin Guide](docs/admin_guide.md) - [Upgrading dependencies](upgrade_dependencies.md) - [Tools & Integrations](tools.md) - [Model diagram](../erd.pdf) diff --git a/docs/admin_guide.md b/docs/admin_guide.md index 88d24eff4c..1440d47bf6 100644 --- a/docs/admin_guide.md +++ b/docs/admin_guide.md @@ -1,4 +1,7 @@ -# Admin Guide for Program & Events Dashboard +[Back to README](../README.md) + +## Admin Guide for Program & Events Dashboard + The **Programs & Events Dashboard** ([outreachdashboard.wmflabs.org](https://outreachdashboard.wmflabs.org)) is a web application designed to support the global Wikimedia community in organizing various programs, including edit-a-thons, education initiatives, and other events. See the **[Source Code](https://github.com/WikiEducationFoundation/WikiEduDashboard/tree/wmflabs)** and **[Phabricator Project](https://phabricator.wikimedia.org/project/manage/1052/)** for more details. This guide provides an overview of the **Program & Events Dashboard** infrastructure, detailing the servers, tools, and third-party dependencies that power the system. It also provides resources for managing and troubleshooting the system. From 14a906bd5513b21b8990b4082c71b252de94fcc1 Mon Sep 17 00:00:00 2001 From: Bhushan Deshmukh Date: Fri, 3 Jan 2025 23:09:51 +0530 Subject: [PATCH 64/66] Resolve Style/StringConcatenation rubocop violations and enable the check for it (#6085) * move StringConcatenation rule to 'permanent exceptions' section --- .rubocop.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 0dda636305..9fe6c64747 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -108,6 +108,8 @@ Layout/HashAlignment: Enabled: false # Hashes look uglier when corrected Rails/I18nLocaleTexts: Enabled: false # Generally the translations are done manually so most times many language translations will be missing while the fallback message is English +Style/StringConcatenation: + Enabled: false ######################## # Temporary exceptions # @@ -171,8 +173,6 @@ Style/DateTime: Enabled: false Rails/ContentTag: Enabled: false -Style/StringConcatenation: - Enabled: false Style/OptionalBooleanParameter: Enabled: false Style/ExplicitBlockArgument: From d5894144803ce3e06105d15ebc823bdd5ce4422e Mon Sep 17 00:00:00 2001 From: Formasitchijoh Date: Sat, 4 Jan 2025 02:26:25 +0100 Subject: [PATCH 65/66] fix: made end date for course creation in student role spec dynamic --- spec/features/student_role_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/features/student_role_spec.rb b/spec/features/student_role_spec.rb index d6c124ae2c..361904049d 100644 --- a/spec/features/student_role_spec.rb +++ b/spec/features/student_role_spec.rb @@ -15,8 +15,8 @@ slug: 'University/An_Example_Course_(Term)', submitted: true, passcode: 'passcode', - start: '2015-01-01'.to_date, - end: '2025-01-01'.to_date) + start: '2025-01-01'.to_date, + end: 1.month.from_now) end let!(:editathon) do create(:editathon, @@ -26,8 +26,8 @@ slug: 'University/An_Example_Editathon_(Term)', submitted: true, passcode: '', - start: '2015-01-01'.to_date, - end: '2025-01-01'.to_date) + start: '2025-01-01'.to_date, + end: 1.month.from_now) end before do From ac19ee05d03ec8cd387506f82f06ec61870f8625 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 6 Jan 2025 13:18:00 +0100 Subject: [PATCH 66/66] Localisation updates from https://translatewiki.net. --- config/locales/ar.yml | 6 ++++++ config/locales/fr.yml | 6 ++++++ config/locales/se.yml | 1 + config/locales/smn.yml | 2 ++ config/locales/sms.yml | 1 + config/locales/zh-TW.yml | 6 ++++++ 6 files changed, 22 insertions(+) diff --git a/config/locales/ar.yml b/config/locales/ar.yml index 23338293c9..f44202a669 100644 --- a/config/locales/ar.yml +++ b/config/locales/ar.yml @@ -6,6 +6,7 @@ # Author: Alshamiri1 # Author: Ayatun # Author: Azouz.anis +# Author: Cigaryno # Author: Dr. Mohammed # Author: Eyas # Author: FShbib @@ -1691,4 +1692,9 @@ ar: assignment_status: مهمة التدريب (%{status}) multi_wiki: selector_placeholder: ابدأ في كتابة نطاق الويكي + weekday_picker: + aria: + weekday_select: '{{weekday}} اضغط على مفتاح الرجوع للتحديد' + weekday_selected: '{{weekday}} تم ضغط على مفتاح الرجوع لإلغاء التحديد' + weekday_unselected: '{{weekday}} غير محدد' ... diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 3d6c23c6dd..b4a893c305 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -2113,4 +2113,10 @@ fr: de serveur. Veuillez réessayer plus tard. tagged_courses: download_csv: Télécharger en CSV + weekday_picker: + aria: + weekday_select: '{{weekday}} Appuyez sur la touche Entrée pour sélectionner' + weekday_selected: '{{weekday}} sélectionné, Appuyez sur la touche Entrée pour + désélectionner' + weekday_unselected: '{{weekday}} non sélectionné' ... diff --git a/config/locales/se.yml b/config/locales/se.yml index 43f62f073d..abbcbada41 100644 --- a/config/locales/se.yml +++ b/config/locales/se.yml @@ -158,6 +158,7 @@ se: label: Ođđa geavaheaddjinamma uploads: file_name: Fiilanamma + license: 'Liseansa:' users: editors: Geavaheddjiid mearri first_name: Ovdanamma diff --git a/config/locales/smn.yml b/config/locales/smn.yml index 177f3ba6bd..4a63584423 100644 --- a/config/locales/smn.yml +++ b/config/locales/smn.yml @@ -80,6 +80,7 @@ smn: download_stats_data: Luođii lovottuvâid feedback: Macâttâs home_wiki: Päikkiwiki + loading: Luođiimin… new_account_email: Šleđgâpostâčujottâs overview: Päikki please_log_in: Čáládât siisâ. @@ -134,6 +135,7 @@ smn: label: Uđđâ kevtteenommâ uploads: image: Kove + license: 'Liiseens:' users: first_name: Ovdânommâ last_name: Suhânommâ diff --git a/config/locales/sms.yml b/config/locales/sms.yml index c2bbfe3f51..43e2314004 100644 --- a/config/locales/sms.yml +++ b/config/locales/sms.yml @@ -209,6 +209,7 @@ sms: file_name: Teâttõsnõmm image: Kartt leʼbe snimldõk label: Ruõkkmõõžž + license: 'Liseʹnss:' uploaded_by: 'Ruõkkâm:' users: contributions: Õõʹnni muttâz diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index 3269ec2e79..9e29809679 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -16,6 +16,7 @@ # Author: Wwycheuk # Author: Xiplus # Author: 列维劳德 +# Author: 張詠涵 # Author: 捍粵者 --- zh-TW: @@ -1609,4 +1610,9 @@ zh-TW: JSONP_request_failed: 從維基百科取得資料的請求已逾時。這可能是由於網路連線速度太慢,或是臨時伺服器問題造成的。請稍後重試。 tagged_courses: download_csv: 下載 CSV + weekday_picker: + aria: + weekday_select: '{{weekday}},按下返回鍵以選擇' + weekday_selected: '{{weekday}} 已選擇,按下返回鍵以取消選擇' + weekday_unselected: '{{weekday}} 未選擇' ...