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/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;
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/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/assets/javascripts/reducers/user_revisions.js b/app/assets/javascripts/reducers/user_revisions.js
index 822b045bf3..0d46e4d988 100644
--- a/app/assets/javascripts/reducers/user_revisions.js
+++ b/app/assets/javascripts/reducers/user_revisions.js
@@ -5,19 +5,24 @@ const initialState = {};
export default function userRevisions(state = initialState, action) {
switch (action.type) {
case RECEIVE_USER_REVISIONS: {
- // Merge project information into article, so that titles
+ // Merge wiki information, so that titles
// can correctly show
- const revisions = action.data.course.revisions.map(rev => ({
+ 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/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(),
+ };
+ });
+ }
+);
+
+
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/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/app/assets/javascripts/utils/mediawiki_revisions_utils.js b/app/assets/javascripts/utils/mediawiki_revisions_utils.js
index c6a5ebd2b1..d83c52512e 100644
--- a/app/assets/javascripts/utils/mediawiki_revisions_utils.js
+++ b/app/assets/javascripts/utils/mediawiki_revisions_utils.js
@@ -154,3 +154,98 @@ 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) };
+};
+
+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 };
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/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/controllers/analytics_controller.rb b/app/controllers/analytics_controller.rb
index 5372bd7df0..de765ceb0b 100644
--- a/app/controllers/analytics_controller.rb
+++ b/app/controllers/analytics_controller.rb
@@ -3,11 +3,9 @@
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"
-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,8 +16,8 @@ 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
- course_students_csv course_articles_csv course_revisions_csv
+ before_action :set_course, only: %i[course_csv course_uploads_csv
+ course_students_csv course_articles_csv
course_wikidata_csv]
########################
@@ -56,12 +54,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,
@@ -83,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/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/controllers/courses_controller.rb b/app/controllers/courses_controller.rb
index fb926f20bf..65941982a6 100644
--- a/app/controllers/courses_controller.rb
+++ b/app/controllers/courses_controller.rb
@@ -139,6 +139,60 @@ def alerts
@alerts = current_user&.admin? ? @course.alerts : @course.public_alerts
end
+ def classroom_program_students_json
+ courses = Course.classroom_program_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.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.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.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
+
##########################
# User-initiated actions #
##########################
@@ -177,7 +231,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
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/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
diff --git a/app/helpers/course_helper.rb b/app/helpers/course_helper.rb
index 0667259bd0..5cb3392636 100644
--- a/app/helpers/course_helper.rb
+++ b/app/helpers/course_helper.rb
@@ -53,4 +53,9 @@ 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)
+ return ''
+ end
end
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/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/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/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
diff --git a/app/models/course.rb b/app/models/course.rb
index 871265a307..c67e254798 100644
--- a/app/models/course.rb
+++ b/app/models/course.rb
@@ -164,6 +164,38 @@ def self.unsubmitted
.where(submitted: false).references(:campaigns)
end
+ def self.classroom_program_students
+ nonprivate
+ .where(type: 'ClassroomProgramCourse', withdrawn: false)
+ .joins(:campaigns)
+ .distinct
+ .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) }
scope :current, -> { current_and_future.where('start < ?', Time.zone.now) }
scope :ended, -> { where('end < ?', Time.zone.now) }
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/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
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..4a6f564332
--- /dev/null
+++ b/app/views/blocked_user_alert_mailer/email.html.haml
@@ -0,0 +1,7 @@
+%link{rel: 'stylesheet', href:'/mailer.css'}
+%p.paragraph
+ 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/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
diff --git a/app/views/courses/articles.json.jbuilder b/app/views/courses/articles.json.jbuilder
index b4dc0c0acb..98e91f1f46 100644
--- a/app/views/courses/articles.json.jbuilder
+++ b/app/views/courses/articles.json.jbuilder
@@ -3,15 +3,13 @@
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.view_count view_count(ac.first_revision, article.average_views)
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
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
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
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|
diff --git a/config/locales/ar.yml b/config/locales/ar.yml
index 4a7a7d78de..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
@@ -984,7 +985,7 @@ ar:
user_uploads_none: لم يساهم المستخدمون المختارون بأية صور أو ملفات وسائط أخرى
في هذا المشروع.
view_other: عرض حملات أخرى
- view_page: عرض صفحة البرنامج
+ view_page: عرض الصفحة
word_count_doc: تقدير عدد الكلمات التي تمت إضافتها إلى مقالات النطاق الرئيسي من
قبل طلاب البرنامج أثناء فترة البرنامج
word_count_doc_wikidata: تقدير لعدد الكلمات المضافة إلى عناصر المساحة الرئيسية
@@ -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/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'
...
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 8e82b21e90..ba7dd1307b 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!
@@ -1021,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
@@ -1869,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
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index b8a89103d7..b4a893c305 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
@@ -2111,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/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/ko.yml b/config/locales/ko.yml
index f14ee26dba..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: 기능
@@ -588,6 +589,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 3550727247..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:
@@ -144,6 +144,7 @@ pa:
none: ਕੋਈ ਨਹੀਂ
categories:
add_this_template: ਫਰਮੇ ਜੋੜੋ
+ template_name: ਫ਼ਰਮੇ ਦਾ ਨਾਂ
courses:
actions: ਕਾਰਵਾਈਆਂ
campaign_users: '%{title} ਸੰਪਾਦਕ'
@@ -165,10 +166,11 @@ pa:
templates: ਫਰਮੇ
enrollment: ਸੰਪਾਦਕਾਂ ਨੂੰ ਜੋੜੋ/ਹਟਾਓ
expected_students: ਸੰਭਾਵਿਤ ਸੰਪਾਦਕ
- student_editors: ਸੰਪਾਦਕ
- students: ਸੰਪਾਦਕ
+ student_editors: ਸੋਧਕ
+ students: ਸੋਧਕ
students_taught: ਕੁੱਲ ਸੰਪਾਦਕ
- students_short: ਸੰਪਾਦਕ
+ students_short: ਸੋਧਕ
+ view_page: ਸਫ਼ਾ ਵੇਖੋ
editable:
edit: ਸੋਧੋ
cancel: ਰੱਦ ਕਰੋ
@@ -192,15 +194,20 @@ pa:
namespace:
article: ਲੇਖ
mediaWiki: ਮੀਡੀਆਵਿਕੀ
+ template: ਫਰਮਾ
help: ਮਦਦ
translation: ਤਰਜਮਾ
draft: ਖਰੜਾ
+ query: ਪੁੱਛਗਿੱਛ
recent_activity:
article_title: ਸਿਰਲੇਖ
datetime: ਮਿਤੀ/ਵੇਲਾ
image: ਤਸਵੀਰ
file_name: ਫ਼ਾਈਲ ਦਾ ਨਾਂ
recent_edits: ਹਾਲੀਆ ਸੋਧਾਂ
+ settings:
+ categories:
+ users: ਵਰਤੋਂਕਾਰ
suggestions:
suggestion_docs:
does_not_exist: ਇਹ ਲੇਖ ਵਿਕੀਪੀਡੀਆ 'ਤੇ ਮੌਜੂਦ ਨਹੀਂ ਹੈ।
@@ -221,7 +228,7 @@ pa:
assign_articles_done: ਪੂਰਾ ਹੋਇਆ
assigned: ਸੌਂਪੇ ਗਏ ਲੇਖ
chars_added: ਅੱਖਰ ਜੋੜੇ ਗਏ
(ਲੇਖ। ਵਰਤੋਂਕਾਰ। ਖਰੜਾ)
- editors: ਸੰਪਾਦਕ
+ editors: ਸੋਧਕ
edits: ਸੋਧਾਂ
first_name: ਮੂਹਰਲਾ ਨਾਂ
last_name: ਪਿਛਲਾ ਨਾਂ
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
diff --git a/config/locales/se.yml b/config/locales/se.yml
index 843c2c07ee..abbcbada41 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
@@ -157,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 ac31b2bdea..43e2314004 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
@@ -208,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/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...
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:
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}} 未選擇'
...
diff --git a/config/routes.rb b/config/routes.rb
index 802d702bd8..c453a7ac2c 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -68,11 +68,7 @@
end
# Users
- resources :users, only: [:index, :show], param: :username, constraints: { username: /.*/ } do
- collection do
- get 'revisions'
- end
- end
+ resources :users, only: [:index, :show], param: :username, constraints: { username: /.*/ }
resources :assignments do
patch '/status' => 'assignments#update_status'
@@ -175,6 +171,19 @@
_subsubsubpage: /.*/
}
+ 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',
@@ -223,8 +232,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'
@@ -251,12 +258,10 @@
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'
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/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
new file mode 100644
index 0000000000..1440d47bf6
--- /dev/null
+++ b/docs/admin_guide.md
@@ -0,0 +1,150 @@
+[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.
+
+## Table of Contents
+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)
+4. [More Resources](#more-resources)
+
+## 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 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`: 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:
+ - **`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-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.
+
+4. **Redis Server**
+ - **`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)**
+ 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)**
+ 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)**
+ 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 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)**
+ 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/)**
+ 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/)**
+ 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/)**
+ 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)**
+ 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)**
+ 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
+
+#### Toolforge
+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)
+- [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.
+
+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)
+- [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)
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/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_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/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/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/setup/populate_dashboard.rb b/setup/populate_dashboard.rb
index 6b615c8fd5..585eeb8219 100755
--- a/setup/populate_dashboard.rb
+++ b/setup/populate_dashboard.rb
@@ -57,17 +57,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
diff --git a/spec/controllers/analytics_controller_spec.rb b/spec/controllers/analytics_controller_spec.rb
index 24f8328e94..a7e128bed3 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)') }
@@ -118,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/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
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/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
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
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'
diff --git a/spec/features/assigned_articles_spec.rb b/spec/features/assigned_articles_spec.rb
index 72d966f9d1..9f2822673c 100644
--- a/spec/features/assigned_articles_spec.rb
+++ b/spec/features/assigned_articles_spec.rb
@@ -17,12 +17,14 @@
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"
expect(page).to have_content('Nancy Tuana')
find('a', text: 'Feedback').click
+ 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'
find('a', text: 'Delete').click
diff --git a/spec/features/course_overview_spec.rb b/spec/features/course_overview_spec.rb
index f6138eb025..5bcb827e54 100644
--- a/spec/features/course_overview_spec.rb
+++ b/spec/features/course_overview_spec.rb
@@ -48,7 +48,10 @@
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:)
@@ -63,9 +66,20 @@
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| # 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
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
diff --git a/spec/features/survey_admin_spec.rb b/spec/features/survey_admin_spec.rb
index b5bf690714..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!')
@@ -222,5 +227,97 @@
end
expect(page).not_to have_content instructor.username
end
+
+ 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
+ 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 the 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 flavor?'
+ 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_record = Rapidfire::Question.find_by(question_text: first_question_text)
+
+ # 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_record.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_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)
+ # 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 and click the clone link for the newly created question group
+ within("li#question_group_#{Rapidfire::QuestionGroup.last.id}") do
+ click_link 'Clone'
+ end
+
+ # Edit the cloned question group
+ within("li#question_group_#{Rapidfire::QuestionGroup.last.id}") do
+ click_link 'Edit'
+ end
+
+ # Edit the conditional question of the cloned group
+ 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 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
+ 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
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/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
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
diff --git a/spec/lib/article_status_manager_spec.rb b/spec/lib/article_status_manager_spec.rb
index e11e80019d..896f3ddffb 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,
diff --git a/spec/mailers/previews/alert_mailer_preview.rb b/spec/mailers/previews/alert_mailer_preview.rb
index 8e947f1f80..d9f7ee9f30 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,9 @@ def example_blocked_edits_alert
user: example_user,
details:)
end
+
+ def example_user_blocked_alert
+ user = example_user
+ Alert.new(type: 'BlockedUserAlert', user:, course: example_course)
+ end
end
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
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);
}
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);
- }
- );
-});
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