Skip to content

Commit

Permalink
Merge branch 'master' into str_concat_fix
Browse files Browse the repository at this point in the history
  • Loading branch information
bhushan354 committed Jan 3, 2025
2 parents f45afa8 + c70d184 commit f1c4401
Show file tree
Hide file tree
Showing 22 changed files with 456 additions and 27 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ If you're a new developer and you're looking for an easy way to get involved, tr
- [MediaWiki API Sandbox](https://en.wikipedia.org/wiki/Special%3aApiSandbox)
- [Quarry](http://quarry.wmflabs.org/): Public querying interface for the Labs replica database. Very useful for testing SQL queries and for figuring out what data is available.
- [Guide to the front end](docs/frontend.md)
- [Admin Guide](docs/admin_guide.md): Overview of the Dashboard infrastructure, including servers, tools, dependencies, and troubleshooting resources.
- [Vagrant](https://github.com/marxarelli/wikied-vagrant): a configuration to quickly get a development environment up and running using Vagrant. If you already have VirtualBox and/or Vagrant on your machine, this might be a simple way to set up a dev environment. However, it is not actively maintained. If you try it and run into problems, let us know!

#### Code Style
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ This project welcomes contributions, and we try to be as newbie-friendly as poss
- [Interface strings & Internationalization](docs/i18n.md)
- [OAuth setup](docs/oauth.md)
- [Deployment](docs/deploy.md)
- [Admin Guide](docs/admin_guide.md) - Overview of the Dashboard infrastructure, including servers, tools, dependencies, and troubleshooting resources.
- [Tools & Integrations](docs/tools.md)
- [Using Docker for development](docs/docker.md)
- [Model diagram](erd.pdf)
Expand Down
87 changes: 82 additions & 5 deletions app/assets/javascripts/actions/article_actions.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,70 @@
import * as types from '../constants';
import API from '../utils/api.js';
import { getRevisionRange } from '../utils/mediawiki_revisions_utils';
import { find } from 'lodash-es';

// This action uses the Thunk middleware pattern: instead of returning a plain
// action object, it returns a function that takes the store dispatch fucntion
// action object, it returns a function that takes the store dispatch function
// which Thunk automatically provides — and can then dispatch a series of plain
// actions to be handled by the store.
// This is how actions with side effects — such as API calls — are handled in
// Redux.
export function fetchArticleDetails(articleId, courseId) {
return function (dispatch) {
return async function (dispatch, getState) {
return API.fetchArticleDetails(articleId, courseId)
.then((response) => {
// eslint-disable-next-line no-console
const details = response.article_details;

return getRevisionRange(details.apiUrl, details.articleTitle, details.editors, details.startDate, details.endDate)
// eslint-disable-next-line no-console
.then(revisionRange => dispatch({ type: types.RECEIVE_ARTICLE_DETAILS, articleId, details, revisionRange }));
.then(async (revisionRange) => {
// If no revisions are found (both first and last revisions are missing),
// it may indicate a mismatch in the article title as the article was moved to a new title.
if (!revisionRange.first_revision && !revisionRange.last_revision) {
const { title: articleTitle, mw_page_id: article_mw_page_id } = find(
getState().articles.articles,
{ id: articleId }
);

// Dispatch an action to cross-check the article title with its metadata.
const crossCheckedArticleTitle = await dispatch(crossCheckArticleTitle(articleId, articleTitle, article_mw_page_id));

// Re-fetch the article details using the cross-checked title for accuracy.
fetchArticleDetailsAgain(crossCheckedArticleTitle, articleId, courseId, dispatch);
} else {
dispatch({ type: types.RECEIVE_ARTICLE_DETAILS, articleId, details, revisionRange });
}
});
})
.catch(response => (dispatch({ type: types.API_FAIL, data: response })));
};
}

// Re-fetches article details using the corrected or cross-checked article title.
// This function is used when the initial fetch fails to retrieve valid revision data,
// likely due to a mismatch in the article title. It ensures the Redux store is updated
// with accurate article details and revision ranges after the re-fetch.
function fetchArticleDetailsAgain(crossCheckedArticleTitle, articleId, courseId, dispatch) {
return API.fetchArticleDetails(articleId, courseId)
.then((response) => {
const details = response.article_details;

// Calculate the revision range for the updated article title.
return getRevisionRange(
details.apiUrl,
crossCheckedArticleTitle,
details.editors,
details.startDate,
details.endDate
).then((revisionRange) => {
// Dispatch the updated article details and revision range to Redux.
dispatch({ type: types.RECEIVE_ARTICLE_DETAILS, articleId, details, revisionRange });
});
})
.catch((response) => {
(dispatch({ type: types.API_FAIL, data: response }));
});
}

export function updateArticleTrackedStatus(articleId, courseId, tracked) {
return function (dispatch) {
return API.updateArticleTrackedStatus(articleId, courseId, tracked).then(response => (dispatch({
Expand All @@ -32,3 +75,37 @@ export function updateArticleTrackedStatus(articleId, courseId, tracked) {
}))).catch(response => (dispatch({ type: types.API_FAIL, data: response })));
};
}

export const crossCheckArticleTitle = (articleId, articleTitle, article_mw_page_id) => {
return async (dispatch) => {
try {
// Fetch the page title from Wikipedia API
const response = await fetch(
`https://en.wikipedia.org/w/api.php?action=query&pageids=${article_mw_page_id}&format=json&origin=*`
);

if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
}

const apiResponse = await response.json();
const wikipediaArticleTitle = apiResponse.query.pages[article_mw_page_id]?.title;

if (wikipediaArticleTitle && wikipediaArticleTitle !== articleTitle) {
const baseUrl = 'https://en.wikipedia.org/wiki/';
const updatedUrl = `${baseUrl}${wikipediaArticleTitle.replace(/ /g, '_')}`;

dispatch({
type: types.UPDATE_ARTICLE_TITLE_AND_URL,
payload: { articleId, title: wikipediaArticleTitle, url: updatedUrl },
});

return wikipediaArticleTitle;
}

return articleTitle;
} catch (error) {
dispatch({ type: types.API_FAIL, data: error });
}
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import colors from '@components/common/ArticleViewer/constants/colors';

// Actions
import { resetBadWorkAlert, submitBadWorkAlert } from '~/app/assets/javascripts/actions/alert_actions.js';
import { crossCheckArticleTitle } from '@actions/article_actions';

/*
Quick summary of the ArticleViewer component's main logic
Expand Down Expand Up @@ -57,6 +58,10 @@ const ArticleViewer = ({ showOnMount, users, showArticleFinder, showButtonLabel,
const [pendingRequest, setPendingRequest] = useState(false);
const lastRevisionId = useSelector(state => state.articleDetails[article.id]?.last_revision?.revid);

// State to track whether the article title needs to be verified and updated
// (i.e., if a fetch failed due to the article title being moved)
const [checkArticleTitle, setCheckArticleTitle] = useState(false);

const dispatch = useDispatch();
const ref = useRef();
const isFirstRender = useRef(true);
Expand Down Expand Up @@ -245,6 +250,8 @@ const ArticleViewer = ({ showOnMount, users, showArticleFinder, showButtonLabel,
setFailureMessage(error.message);
setFetched(true);
setWhoColorFailed(true);
// Set flag to verify and fetch the article title if the fetch failed, possibly due to the article being moved
setCheckArticleTitle(true);
});
};

Expand All @@ -257,9 +264,32 @@ const ArticleViewer = ({ showOnMount, users, showArticleFinder, showButtonLabel,
}).catch((error) => {
setWhoColorFailed(true);
setFailureMessage(error.message);
// Set flag to verify and fetch the article title if the fetch failed, possibly due to the article being moved
setCheckArticleTitle(true);
});
};

// Function to verify if the article title has changed and fetch updated data accordingly
const verifyAndFetchArticle = async () => {
// Dispatch an action to cross-check the current article title using its ID and MediaWiki page ID
const crossCheckedArticleTitle = await dispatch(crossCheckArticleTitle(article.id, article.title, article.mw_page_id));

if (crossCheckedArticleTitle === article.title) {
setWhoColorFailed(false); // Clear the failure state for WhoColor data
setCheckArticleTitle(false); // Stop further title verification checks
fetchParsedArticle(); // Re-fetch the parsed article content with the current title
fetchWhocolorHtml(); // Re-fetch the WhoColor HTML for the article using the current title
} else if (crossCheckArticleTitle !== article.title) {
setFetched(false); // Indicate a loading state until the Redux store updates the new article title and the component re-renders
}
};

// Trigger the article title verification and data fetching process if a previous fetch failed
if (checkArticleTitle) {
verifyAndFetchArticle();
}


// These are mediawiki user ids, and don't necessarily match the dashboard
// database user ids, so we must fetch them by username from the wiki.
const fetchUserIds = () => {
Expand Down
22 changes: 17 additions & 5 deletions app/assets/javascripts/components/common/weekday_picker.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ const WeekdayPicker = ({
style,
tabIndex,

ariaModifier,
modifiers,

locale,
Expand Down Expand Up @@ -145,7 +144,7 @@ const WeekdayPicker = ({

dayClassName += customModifiers.map(modifier => ` ${dayClassName}--${modifier}`).join('');

const ariaSelected = customModifiers.indexOf(ariaModifier) > -1;
const ariaSelected = customModifiers.indexOf('selected') > -1;

let tabIndexValue = null;
if (onWeekdayClick) {
Expand All @@ -159,10 +158,20 @@ const WeekdayPicker = ({
const onMouseEnterHandler = onWeekdayMouseEnter ? e => handleWeekdayMouseEnter(e, weekday, customModifiers) : null;
const onMouseLeaveHandler = onWeekdayMouseLeave ? e => handleWeekdayMouseLeave(e, weekday, customModifiers) : null;

const ariaLabelMessage = ariaSelected
? I18n.t('weekday_picker.aria.weekday_selected', { weekday: localeUtils.formatWeekdayLong(weekday), })
: I18n.t('weekday_picker.aria.weekday_select', { weekday: localeUtils.formatWeekdayLong(weekday), });

const ariaLiveMessage = ariaSelected
? I18n.t('weekday_picker.aria.weekday_selected', { weekday: localeUtils.formatWeekdayLong(weekday), })
: I18n.t('weekday_picker.aria.weekday_unselected', { weekday: localeUtils.formatWeekdayLong(weekday), });

return (
<button
key={weekday} className={dayClassName} tabIndex={tabIndexValue}
aria-pressed={ariaSelected}
key={weekday}
className={dayClassName}
tabIndex={tabIndexValue}
aria-label= {ariaLabelMessage}
onClick={onClickHandler}
onKeyDown={e => handleDayKeyDown(e, weekday, customModifiers)}
onMouseEnter={onMouseEnterHandler}
Expand All @@ -171,6 +180,10 @@ const WeekdayPicker = ({
<span title={localeUtils.formatWeekdayLong(weekday)}>
{localeUtils.formatWeekdayShort(weekday)}
</span>
{/* Aria-live region for screen reader announcements for confirmation of when a week day is selected or unselected */}
<div aria-live="assertive" aria-atomic="true" className="sr-WeekdayPicker-aria-live">
{ariaLiveMessage}
</div>
</button>
);
};
Expand Down Expand Up @@ -201,7 +214,6 @@ WeekdayPicker.propTypes = {
style: PropTypes.object,
tabIndex: PropTypes.number,

ariaModifier: PropTypes.string,
modifiers: PropTypes.object,

locale: PropTypes.string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ const CourseCreator = createReactClass({

campaignParam() {
// The regex allows for any number of URL parameters, while only capturing the campaign_slug parameter
const campaignParam = window.location.search.match(/\?.*?campaign_slug=(.*?)(?:$|&)/);
const campaignParam = window.location?.search?.match(/\?.*?campaign_slug=(.*?)(?:$|&)/);
if (campaignParam) {
return campaignParam[1];
}
Expand Down Expand Up @@ -162,8 +162,11 @@ const CourseCreator = createReactClass({
cleanedCourse.scoping_methods = getScopingMethods(this.props.scopingMethods);
this.props.submitCourse({ course: cleanedCourse }, onSaveFailure);
}
} else if (!this.props.validations.exists.valid) {
this.setState({ isSubmitting: false });
} else {
const existsValidation = this.props.validations?.exists?.valid;
if (existsValidation === false) {
this.setState({ isSubmitting: false });
}
}
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const CampaignList = ({ campaigns, course }) => {
let comma = '';
const url = `/campaigns/${campaign.slug}`;
if (index !== lastIndex) { comma = ', '; }
return <span key={campaign.slug}><a href={url}>{campaign.title}</a>{comma}</span>;
return <span key={`${campaign.slug}-${index}`}><a href={url}>{campaign.title}</a>{comma}</span>;
})
: I18n.t('courses.none'));

Expand Down
1 change: 1 addition & 0 deletions app/assets/javascripts/constants/articles.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
13 changes: 12 additions & 1 deletion app/assets/javascripts/reducers/articles.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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;
}
Expand Down
7 changes: 6 additions & 1 deletion app/assets/javascripts/utils/course_date_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)'));
}
}
Expand Down
12 changes: 12 additions & 0 deletions app/assets/stylesheets/modules/_calendar.styl
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading

0 comments on commit f1c4401

Please sign in to comment.