Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/course/util.ts
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/4import { SizeType } from "antd/lib/config-provider/SizeContext";5import { Map } from "immutable";6import { IntlShape } from "react-intl";78import {9TypedMap,10useEffect,11useState,12useWindowDimensions,13} from "@cocalc/frontend/app-framework";14import { IconName } from "@cocalc/frontend/components/icon";15import { labels } from "@cocalc/frontend/i18n";16import { KUCALC_COCALC_COM } from "@cocalc/util/db-schema/site-defaults";17import {18cmp,19defaults,20merge,21required,22search_match,23search_split,24separate_file_extension,25} from "@cocalc/util/misc";26import { ProjectsStore } from "../projects/store";27import { UserMap } from "../todo-types";28import { StudentsMap } from "./store";29import { AssignmentCopyStep } from "./types";3031// Pure functions used in the course manager32export function STEPS(peer: boolean): AssignmentCopyStep[] {33if (peer) {34return [35"assignment",36"collect",37"peer_assignment",38"peer_collect",39"return_graded",40];41} else {42return ["assignment", "collect", "return_graded"];43}44}4546export function previous_step(47step: AssignmentCopyStep,48peer: boolean,49): AssignmentCopyStep {50let prev: AssignmentCopyStep | undefined;51for (const s of STEPS(peer)) {52if (step === s) {53if (prev === undefined) break;54return prev;55}56prev = s;57}58throw Error(`BUG! previous_step('${step}, ${peer}')`);59}6061export function step_direction(step: AssignmentCopyStep): "to" | "from" {62switch (step) {63case "assignment":64return "to";65case "collect":66return "from";67case "return_graded":68return "to";69case "peer_assignment":70return "to";71case "peer_collect":72return "from";73default:74throw Error(`BUG! step_direction('${step}')`);75}76}7778export function step_verb(step: AssignmentCopyStep) {79switch (step) {80case "assignment":81return "assign";82case "collect":83return "collect";84case "return_graded":85return "return";86case "peer_assignment":87return "assign";88case "peer_collect":89return "collect";90default:91throw Error(`BUG! step_verb('${step}')`);92}93}9495export function step_ready(step: AssignmentCopyStep, n) {96switch (step) {97case "assignment":98return "";99case "collect":100if (n > 1) {101return " who have already received it";102} else {103return " who has already received it";104}105case "return_graded":106return " whose work you have graded";107case "peer_assignment":108return " for peer grading";109case "peer_collect":110return " who should have peer graded it";111}112}113114// Takes a student immutable.Map with key 'student_id'115// Returns a list of students `x` shaped like:116// {117// first_name : string118// last_name : string119// last_active : integer120// hosting : string121// email_address : string122// }123export function parse_students(124student_map: StudentsMap,125user_map: UserMap,126redux,127intl: IntlShape,128) {129const v = immutable_to_list(student_map, "student_id");130for (const x of v) {131if (x.account_id != null) {132const user = user_map.get(x.account_id);133if (x.first_name == null) {134x.first_name = user == null ? "" : user.get("first_name", "");135}136if (x.last_name == null) {137x.last_name = user == null ? "" : user.get("last_name", "");138}139if (x.project_id != null) {140const projects_store = redux.getStore("projects");141if (projects_store != null) {142const last_active = projects_store.get_last_active(x.project_id);143if (last_active != null) {144x.last_active = last_active.get(x.account_id);145}146}147}148}149const { description, state } = projectStatus(x.project_id, redux, intl);150x.hosting = description + state;151152if (x.first_name == null) {153x.first_name = "";154}155if (x.last_name == null) {156x.last_name = "";157}158if (x.last_active == null) {159x.last_active = 0;160}161if (x.email_address == null) {162x.email_address = "";163}164}165return v;166}167168// Transforms Iterable<K, M<i, m>> to [M<i + primary_key, m + K>] where primary_key maps to K169// Dunno if either of these is readable...170// Turns Map(Keys -> Objects{...}) into [Objects{primary_key : Key, ...}]171// TODO: Type return array better172export function immutable_to_list(x: undefined): undefined;173export function immutable_to_list<T, P>(174x: Map<string, T>,175primary_key: P,176): T extends TypedMap<infer S>177? S[]178: T extends Map<string, infer S>179? S[]180: any;181export function immutable_to_list(x: any, primary_key?): any {182if (x == null || x == undefined) {183return;184}185const v: any[] = [];186x.map((val, key) => v.push(merge(val.toJS(), { [primary_key]: key })));187return v;188}189190// Returns a list of matched objects and the number of objects191// which were in the original list but omitted in the returned list192export function compute_match_list(opts: {193list: any[];194search_key: string;195search: string;196}) {197opts = defaults(opts, {198list: required, // list of objects<M>199search_key: required, // M.search_key property to match over200search: required, // matches to M.search_key201});202let { list, search, search_key } = opts;203if (!search) {204// why are you even calling this..205return { list, num_omitted: 0 };206}207208const words = search_split(search);209const matches = (x) =>210search_match(x[search_key]?.toLowerCase?.() ?? "", words);211const n = list.length;212list = list.filter(matches);213const num_omitted = n - list.length;214return { list, num_omitted };215}216217// Returns218// `list` partitioned into [not deleted, deleted]219// where each partition is sorted based on the given `compare_function`220// deleted is not included by default221export function order_list<T extends { deleted: boolean }>(opts: {222list: T[];223compare_function: (a: T, b: T) => number;224reverse: boolean;225include_deleted: boolean;226}) {227opts = defaults(opts, {228list: required,229compare_function: required,230reverse: false,231include_deleted: false,232});233let { list, compare_function, include_deleted } = opts;234235const x = list.filter((x) => x.deleted);236const sorted_deleted = x.sort(compare_function);237238const y = list.filter((x) => !x.deleted);239list = y.sort(compare_function);240241if (opts.reverse) {242list.reverse();243}244245if (include_deleted) {246list = list.concat(sorted_deleted);247}248249return { list, deleted: x, num_deleted: sorted_deleted.length };250}251252const cmp_strings = (a, b, field) => {253return cmp(a[field]?.toLowerCase() ?? "", b[field]?.toLowerCase() ?? "");254};255256// first sort by domain, then address at that domain... since there will be many students257// at same domain, and '[email protected]' > '[email protected]' > '[email protected]' is true but not helpful258const cmp_email = (a, b) => {259const v = a.split("@");260const w = b.split("@");261const c = cmp(v[1], w[1]);262if (c) {263return c;264}265return cmp(v[0], w[0]);266};267268const sort_on_string_field = (field, field2) => (a, b) => {269const c =270field == "email_address"271? cmp_email(a[field], b[field])272: cmp_strings(a, b, field);273return c != 0 ? c : cmp_strings(a, b, field2);274};275276const sort_on_numerical_field = (field, field2) => (a, b) => {277const c = cmp((a[field] ?? 0) * -1, (b[field] ?? 0) * -1);278return c != 0 ? c : cmp_strings(a, b, field2);279};280281type StudentField =282| "email"283| "first_name"284| "last_name"285| "last_active"286| "hosting";287288export function pick_student_sorter<T extends { column_name: StudentField }>(289sort: T,290) {291switch (sort.column_name) {292case "email":293return sort_on_string_field("email_address", "last_name");294case "first_name":295return sort_on_string_field("first_name", "last_name");296case "last_name":297return sort_on_string_field("last_name", "first_name");298case "last_active":299return sort_on_numerical_field("last_active", "last_name");300case "hosting":301return sort_on_string_field("hosting", "email_address");302}303}304305export function assignment_identifier(306assignment_id: string,307student_id: string,308): string {309return assignment_id + student_id;310}311312export function autograded_filename(filename: string): string {313const { name, ext } = separate_file_extension(filename);314return name + "_autograded." + ext;315}316317interface ProjectStatus {318description: string;319icon: IconName;320state: string;321tip?: string;322}323324export function projectStatus(325project_id: string | undefined,326redux,327intl: IntlShape,328): ProjectStatus {329if (!project_id) {330return { description: "(not created)", icon: "hourglass-half", state: "" };331}332const store = redux.getStore("projects");333const state = ` (${store.get_state(project_id)})`;334const kucalc = redux.getStore("customize").get("kucalc");335if (kucalc === KUCALC_COCALC_COM) {336return projectStatusCoCalcCom({ project_id, state, store, intl });337} else {338const tip = intl.formatMessage({339id: "course.util.project_status.ready",340defaultMessage: "Project exists and is ready.",341});342return {343icon: "exclamation-triangle",344description: intl.formatMessage(labels.ready),345tip,346state,347};348}349}350351function projectStatusCoCalcCom({352project_id,353state,354store,355intl,356}: {357project_id: string;358state: string;359store: ProjectsStore;360intl: IntlShape;361}): ProjectStatus {362const upgrades = store.get_total_project_quotas(project_id);363if (upgrades == null) {364// user opening the course, but isn't a collaborator on365// this student project for some reason. This will get fixed366// when configure all projects runs.367const description = intl.formatMessage({368id: "course.util.status-cocalc-com.project_not_available",369defaultMessage: "(not available)",370});371return {372description,373icon: "question-circle",374state: "",375};376}377378if (upgrades.member_host) {379return {380icon: "check",381description: "Members-only hosting",382tip: "Projects is on a members-only server, which is much more robust and has priority support.",383state,384};385}386const licenses = store.get_site_license_ids(project_id);387if (licenses.length > 0) {388const description = intl.formatMessage({389id: "course.util.status-cocalc-com.licensed.description",390defaultMessage: "Licensed",391});392const tip = intl.formatMessage({393id: "course.util.status-cocalc-com.licensed.tooltip",394defaultMessage:395"Project is properly licensed and should work well. Thank you!",396});397return { description, icon: "check", state, tip };398} else {399const description = intl.formatMessage({400id: "course.util.status-cocalc-com.free.description",401defaultMessage: "Free Trial",402});403const tip = intl.formatMessage({404id: "course.util.status-cocalc-com.free.tooltip",405defaultMessage: `Project is a trial project hosted on a free server,406so it may be overloaded and will be rebooted frequently.407Please upgrade in course configuration.`,408});409return {410description,411icon: "exclamation-triangle",412state,413tip,414};415}416}417418// the list of assignments, in particular with peer grading, has a large number of buttons419// in a single row. We mitigate this by rendering the buttons smaller if the screen is narrower.420export function useButtonSize(): SizeType {421const [size, setSize] = useState<SizeType>("small");422const { width } = useWindowDimensions();423useEffect(() => {424const next = width < 1024 ? "small" : "middle";425if (next != size) {426setSize(next);427}428});429return size;430}431432433