WikiEducationFoundation/WikiEduDashboard

View on GitHub
app/assets/javascripts/selectors/index.js

Summary

Maintainability
D
1 day
Test Coverage
A
94%
import { createSelector } from 'reselect';
import { sortBy, difference, uniq, filter, includes, pickBy, find } from 'lodash-es';
import { getFiltered } from '../utils/model_utils';
import { STUDENT_ROLE, INSTRUCTOR_ROLE, ONLINE_VOLUNTEER_ROLE, CAMPUS_VOLUNTEER_ROLE, STAFF_ROLE, fetchStates, ARTICLES_PER_PAGE, REVIEWING_ROLE } from '../constants';
import UserUtils from '../utils/user_utils.js';
import { PageAssessmentGrades } from '../utils/article_finder_language_mappings.js';
import CourseDateUtils from '../utils/course_date_utils.js';


const getUsers = state => state.users.users;
const getCurrentUserFromHtml = state => state.currentUserFromHtml;
const getCourseCampaigns = state => state.campaigns.campaigns;
const getAllCampaigns = state => state.campaigns.all_campaigns;
const getUserCourses = state => state.userCourses.userCourses;
const assignments = state => state.assignments.assignments;
const getAllEditedArticles = state => state.articles.articles;
const getWikiFilter = state => state.articles.wikiFilter;
const getNewnessFilter = state => state.articles.newnessFilter;
const getTrackedStatusFilter = state => state.articles.trackedStatusFilter;
const getCurrentArticlesPage = state => state.articles.currentPage;
const getAlerts = state => state.alerts.alerts;
const getAlertFilters = state => state.alerts.selectedFilters;
const getArticleFinderState = state => state.articleFinder;
const getUploads = state => state.uploads.uploads;
const getUploadFilters = state => state.uploads.selectedFilters;
const getTags = state => state.tags.tags;
const getAllTags = state => state.tags.allTags;
const getWeeks = state => state.timeline.weeks;
const getBlocks = state => state.timeline.blocks;
const getCourseType = state => state.course.type;
const getTraining = state => state.training;
const getValidations = state => state.validations.validations;
const getValidationErrors = state => state.validations.errorQueue;
const getCourse = state => state.course;
const getTickets = state => state.tickets;
const getSpecialUsers = state => state.settings.specialUsers;

export const getInstructorUsers = createSelector(
  [getUsers], users => sortBy(getFiltered(users, { role: INSTRUCTOR_ROLE }), 'enrolled_at')
);

export const getStudentUsers = createSelector(
  [getUsers], users => getFiltered(users, { role: STUDENT_ROLE })
);

export const getStudentCount = createSelector(
  [getStudentUsers], users => users.length
);

export const getStaffUsers = createSelector(
  [getUsers], users => sortBy(getFiltered(users, { role: STAFF_ROLE }), 'enrolled_at')
);

export const getProgramManagers = createSelector(
  [getStaffUsers], users => getFiltered(users, { program_manager: true })
);

export const getContentExperts = createSelector(
  [getStaffUsers], users => getFiltered(users, { content_expert: true })
);

export const getCourseApprovalStaff = createSelector(
  [getSpecialUsers, getStaffUsers], (specialUsers, staffUsers) => {
    // Compare staff users of a course with classroom program manager and
    // wiki expert users. Make a list of all Special Users objects with
    // required information. Mark any user in the list as already_selected
    // if they have already been selected as a staff user, to prevent them
    // from being selected again and raising an error.
    const staffList = [];
    // Add classroom program manager
    if (specialUsers.classroom_program_manager) {
      staffList.push({
        role: 'classroom_program_manager',
        username: specialUsers.classroom_program_manager[0].username,
        realname: specialUsers.classroom_program_manager[0].real_name,
        already_selected: false
      });
    }
    // Add wikipedia experts
    if (specialUsers.wikipedia_experts && specialUsers.wikipedia_experts.length > 0) {
      specialUsers.wikipedia_experts.forEach((user) => {
        staffList.push({
          role: 'wikipedia_expert',
          username: user.username,
          realname: user.real_name,
          already_selected: false
        });
      });
    }
    // Check if any of the staff users were already selected
    const staffUsernames = staffList.map((user) => { return user.username; });
    staffUsers.forEach((staffUser) => {
      if (staffUsernames.includes(staffUser.username)) {
        const userObject = staffList.find(user => user.username === staffUser.username);
        userObject.already_selected = true;
      }
    });
    return staffList;
  }
);

export const getOnlineVolunteerUsers = createSelector(
  [getUsers], users => sortBy(getFiltered(users, { role: ONLINE_VOLUNTEER_ROLE }), 'enrolled_at')
);

export const getCampusVolunteerUsers = createSelector(
  [getUsers], users => sortBy(getFiltered(users, { role: CAMPUS_VOLUNTEER_ROLE }), 'enrolled_at')
);

export const getCurrentUser = createSelector(
  [getCurrentUserFromHtml, getUsers], (currentUserFromHtml, users) => {
    const currentUserFromUsers = getFiltered(users, { id: currentUserFromHtml.id })[0];
    const currentUser = currentUserFromUsers || currentUserFromHtml;
    const userRoles = UserUtils.userRoles(currentUser, users);
    return { ...currentUser, ...userRoles };
  }
);

export const getAvailableCampaigns = createSelector(
  [getCourseCampaigns, getAllCampaigns], (campaigns, allCampaigns) => {
    campaigns = campaigns.map(campaign => campaign.title);
    allCampaigns = allCampaigns.map(campaign => campaign.title);
    return difference(allCampaigns, campaigns);
  }
);

export const getAvailableTags = createSelector(
  [getTags, getAllTags], (tags, allTags) => {
    tags = tags.map(tag => tag.tag);
    allTags = uniq(allTags);
    return difference(allTags, tags);
  }
);

export const getCloneableCourses = createSelector(
  [getUserCourses], (userCourses) => {
    return getFiltered(userCourses, { cloneable: true });
  }
);

// This returns all the Articles with user_id null
export const getAvailableArticles = createSelector(
  [assignments], (assignment) => {
    return getFiltered(assignment, { user_id: null });
  }
);

export const getReviewAssignments = createSelector(
  [assignments], (assignment) => {
    return getFiltered(assignment, { role: REVIEWING_ROLE });
  }
);

export const getWikiArticles = createSelector(
  [getAllEditedArticles, getWikiFilter], (editedArticles, wikiFilter) => {
    if (wikiFilter.project === 'all') {
      return editedArticles;
    }
    return getFiltered(editedArticles, { ...wikiFilter });
  }
);

export const getArticlesByNewness = createSelector(
  [getWikiArticles, getNewnessFilter], (articles, newnessFilter) => {
    switch (newnessFilter) {
      case 'new':
        return articles.filter(a => a.new_article);
      case 'existing':
        return articles.filter(a => !a.new_article);
      default:
        return articles;
    }
  }
);

export const getArticlesByTrackedStatus = createSelector(
  [getArticlesByNewness, getTrackedStatusFilter], (articles, trackedStatusFilter) => {
    switch (trackedStatusFilter) {
      case 'tracked':
        return articles.filter(a => a.tracked);
      case 'untracked':
        return articles.filter(a => !a.tracked);
      default:
        return articles;
    }
  }
);

export const getArticlesByPage = createSelector(
  [getArticlesByTrackedStatus, getCurrentArticlesPage], (articles, page) => {
    const toSkip = (page - 1) * ARTICLES_PER_PAGE;
    return articles.slice(toSkip, toSkip + ARTICLES_PER_PAGE);
  }
);

export const getFilteredAlerts = createSelector(
  [getAlerts, getAlertFilters], (alerts, alertFilters) => {
    if (!alertFilters.length) { return alerts; }
    const alertTypes = alertFilters.map(_filter => _filter.value);
    return filter(alerts, alert => includes(alertTypes, alert.type));
  }
);

export const getFilteredArticleFinder = createSelector(
  [getArticleFinderState], (articleFinder) => {
    return pickBy(articleFinder.articles, (article) => {
      const language = articleFinder.home_wiki.language;
      const project = articleFinder.home_wiki.project;
      if (article.grade && !includes(Object.keys(PageAssessmentGrades[project][language]), article.grade)) {
        return false;
      }
      let quality;
      if (article.grade && article.revScore) {
        quality = Math.max(article.revScore, PageAssessmentGrades[project][language][article.grade].score);
      } else if (article.grade) {
        quality = PageAssessmentGrades[project][language][article.grade].score;
      } else if (article.revScore) {
        quality = article.revScore;
      } else {
        quality = 0;
      }
      const qualityFilter = articleFinder.article_quality;
      if (fetchStates[article.fetchState] >= fetchStates.PAGEVIEWS_RECEIVED && article.pageviews < articleFinder.min_views) {
        return false;
      }
      if (fetchStates[article.fetchState] >= fetchStates.REVISIONSCORE_RECEIVED && quality > qualityFilter) {
        return false;
      }
      return true;
    });
  }
);

export const getFilteredUploads = createSelector(
  [getUploads, getUploadFilters], (uploads, uploadFilters) => {
    if (!uploadFilters.length) { return uploads; }
    const uploaders = uploadFilters.map(_filter => _filter.value);
    return filter(uploads, upload => includes(uploaders, upload.uploader));
  }
);

export const getWeeksArray = createSelector(
  [getWeeks, getBlocks], (weeks, blocks) => {
    const weeksArray = [];
    const weekIds = Object.keys(weeks);
    const blocksByWeek = {};
    Object.keys(blocks).forEach((blockId) => {
      const block = blocks[blockId];
      if (blocksByWeek[block.week_id]) {
        blocksByWeek[block.week_id].push(block);
      } else {
        blocksByWeek[block.week_id] = [block];
      }
    });

    weekIds.forEach((weekId) => {
      const newWeek = weeks[weekId];
      newWeek.blocks = blocksByWeek[weekId] || [];
      weeksArray.push(newWeek);
    });

    return weeksArray;
  }
);

export const getAllWeeksArray = createSelector(
  [getWeeksArray, getCourse], (weeksArray, course) => {
    // For each week, first insert an extra empty week for each week with empty
    // week meetings, which indicates a blackout week. Then insert the week itself.
    // The index 'i' represents the zero-index week number. However, weekNumber
    // property indicates week number starting from 1. Both empty and non-empty
    // weeks are included in this numbering scheme.
    const weekMeetings = CourseDateUtils.weekMeetings(course, course.day_exceptions);
    const allWeeks = [];
    let i = 0;
    weeksArray.forEach((week) => {
      while (weekMeetings[i] && weekMeetings[i].length === 0) {
        const emptyWeek = { empty: true, weekNumber: i + 1 };
        allWeeks.push(emptyWeek);
        i += 1;
      }
      const nonEmptyWeek = week;
      nonEmptyWeek.empty = false;
      nonEmptyWeek.weekNumber = i + 1;
      nonEmptyWeek.meetings = weekMeetings[i];
      allWeeks.push(nonEmptyWeek);
      i += 1;
    });

    return allWeeks;
  }
);

export const getAvailableTrainingModules = createSelector(
  [getCourseType, getTraining], (courseType, training) => {
    // We only do filtering for ClassroomProgramCourse type.
    if (courseType !== 'ClassroomProgramCourse') {
      return training.modules;
    }
    // Find the Student library that we want to filter by.
    const studentsLibrary = training.libraries.find(library => library.slug === 'students');
    if (!studentsLibrary) { return training.modules; }

    // Only include modules that are part of the Student library.
    const studentModules = training.modules.filter(module => studentsLibrary.modules.includes(module.slug));
    return studentModules;
  }
);

export const isValid = createSelector(
  [getValidations], (validations) => {
    // If any validation is not valid, return false.
    const invalidValue = find(validations, (value) => { return value.valid === false; });
    if (invalidValue) { return false; }
    return true;
  }
);

export const firstValidationErrorMessage = createSelector(
  [getValidations, getValidationErrors], (validations, validationErrors) => {
    if (validationErrors.length > 0) {
      return validations[validationErrors[0]].message;
    }
    return null;
  }
);

export const editPermissions = createSelector(
  [getCourse, getCurrentUser], (course, user) => {
    if (!user.isAdvancedRole) { return false; }
    return user.isAdmin || !course.closed;
  }
);

export const getFilteredTickets = createSelector(
  [getTickets], (tickets) => {
    const ownerIds = tickets.filters.owners.map(_filter => _filter.value);
    const statuses = tickets.filters.statuses.map(_filter => parseInt(_filter.value));

    let ownerFilter = ticket => ticket;
    let statusFilter = ticket => ticket;
    if (ownerIds.length) {
      ownerFilter = ticket => ownerIds.includes(ticket.owner?.id);
    }
    if (statuses.length) {
      statusFilter = ticket => statuses.includes(ticket.status);
    }

    return tickets.all
      .filter(ownerFilter)
      .filter(statusFilter);
  }
);

export const getTicketsById = createSelector(
  [getTickets], (tickets) => {
    return tickets.all.reduce((acc, ticket) => ({ ...acc, [ticket.id]: ticket }), {});
  }
);