Skip to content
Snippets Groups Projects
Select Git revision
  • cd03dcd8de0ee61cea1566fd81bae6c20399d1e2
  • main default protected
  • old_beta_site
  • smart_caching
  • 51-endpoint-course-slow-to-load
  • dork
  • dork2
  • v2.0.8 protected
  • v2.0.7 protected
  • v2.0.6 protected
  • v2.0.5 protected
  • v2.0.4 protected
  • v2.0.3 protected
  • v2.0.2 protected
  • v2.0.1 protected
  • v2.0.0 protected
  • v1.1.10 protected
  • v1.1.9 protected
  • v1.1.8 protected
  • v1.1.7 protected
  • v1.1.6 protected
  • v1.1.5 protected
  • v1.1.4 protected
  • v1.1.3 protected
  • v1.1.2 protected
  • v1.1.1 protected
  • v1.1 protected
27 results

import.tsx

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    import.tsx 17.90 KiB
    import { useBackendContext } from "@/components/BackendProvider";
    import ModeratorBarrier from "@/components/ModeratorBarrier";
    import { ReloadBoundary, useReloadBoundary } from "@/components/ReloadBoundary";
    import { ICalEvent, parseICal } from "@/misc/Calendar";
    import { DateTime } from "luxon";
    import Link from "next/link";
    import { useSearchParams } from "next/navigation";
    import { MutableRefObject, useEffect, useRef, useState } from "react";
    import { showError, showErrorMessage } from "@/misc/Util";
    
    function TerminBody({
        course,
        imported_events,
    }: {
        course: course;
        imported_events: MutableRefObject<ICalEvent[]>;
    }) {
        const api = useBackendContext();
        const reloadFunc = useReloadBoundary();
    
        const existingEvents: ICalEvent[] = (course.lectures ?? []).map((l) => {
            const obj: ICalEvent = {
                location: l.location,
                duration: l.duration,
                startDate: DateTime.fromISO(l.time),
                foreign_id: l.id,
            };
            return obj;
        });
    
        const removeDuplicates = (event: ICalEvent, index: number, self: ICalEvent[]) => {
            // remove duplicates
            return (
                index ===
                self.findIndex(
                    (e) =>
                        e.location === event.location &&
                        e.duration === event.duration &&
                        Math.abs(e.startDate.diff(event.startDate).as("seconds")) < 1,
                )
            );
        };
        // find events that are new (place, duration and time unique)
        const new_events = imported_events.current
            .filter((event) => {
                const eventMatch = existingEvents.some((existing) => {
                    const match =
                        existing.location === event.location &&
                        existing.duration === event.duration &&
                        Math.abs(existing.startDate.diff(event.startDate).as("seconds")) < 1;
                    return match;
                });
                return !eventMatch;
            })
            .filter(removeDuplicates);
        // find existing events that are not in the imported events
        const not_imported_events = existingEvents
            .filter((event) => {
                return !imported_events.current.some((imported) => {
                    return (
                        imported.location === event.location &&
                        imported.duration === event.duration &&
                        Math.abs(imported.startDate.diff(event.startDate).as("seconds")) < 1
                    );
                });
            })
            .filter(removeDuplicates);
        // number of full matches
        const full_matches = imported_events.current
            .filter((event) => {
                return existingEvents.some((existing) => {
                    return (
                        existing.location === event.location &&
                        existing.duration === event.duration &&
                        Math.abs(existing.startDate.diff(event.startDate).as("seconds")) < 1
                    );
                });
            })
            .filter(removeDuplicates).length;
    
        const importEvent = (event: ICalEvent) => {
            // time must be YYYY-MM-ddTHH:mm:ss
            api.getNewOMConfiguration("lecture")
                .then((res) => {
                    const defaultValues: any = {};
                    res.fields?.forEach((field) => {
                        if (field.may_be_null) {
                            return;
                        }
                        if (field.default_value !== undefined && field.default_value !== null) {
                            defaultValues[field.id] = field.default_value;
                        } else if (
                            field.type === "string" &&
                            (field.min_length === undefined || field.min_length <= 0)
                        ) {
                            defaultValues[field.id] = "";
                        }
                    });
    
                    const convertedTime = event.startDate?.toFormat("yyyy-MM-dd'T'HH:mm:ss");
                    return api.createOMObject("lecture", {
                        parent_type: "course",
                        parent_id: course.id,
                        values: {
                            ...defaultValues,
                            title: event.summary,
                            location: event.location,
                            time: convertedTime,
                            duration: event.duration,
                        },
                    });
                })
                .then((res) => {
                    reloadFunc();
                })
                .catch((e) => {
                    showError(e, "Unable to create lecture");
                });
        };
    
        const importAllEvents = () => {
            api.getNewOMConfiguration("lecture")
                .then((res) => {
                    const defaultValues: any = {};
                    res.fields?.forEach((field) => {
                        if (field.may_be_null) {
                            return;
                        }
                        if (field.default_value !== undefined && field.default_value !== null) {
                            defaultValues[field.id] = field.default_value;
                        } else if (
                            field.type === "string" &&
                            (field.min_length === undefined || field.min_length <= 0)
                        ) {
                            defaultValues[field.id] = "";
                        }
                    });
    
                    return Promise.allSettled(
                        new_events.map((event) => {
                            const convertedTime = event.startDate?.toFormat("yyyy-MM-dd'T'HH:mm:ss");
                            return api.createOMObject("lecture", {
                                parent_type: "course",
                                parent_id: course.id,
                                values: {
                                    ...defaultValues,
                                    title: event.summary,
                                    location: event.location,
                                    time: convertedTime,
                                    duration: event.duration,
                                },
                            });
                        }),
                    );
                })
                .then((res) => {
                    reloadFunc();
                })
                .catch((e) => {
                    showError(e, "Unable to create lectures");
                    reloadFunc();
                });
        };
    
        const removeEvent = (event: ICalEvent) => {
            if (!confirm("Really delete?")) return;
            if (event.foreign_id === undefined) {
                showErrorMessage("Event not found");
                return;
            }
    
            api.deleteOMObject("lecture", event.foreign_id)
                .then((res) => {
                    reloadFunc();
                })
                .catch((e) => {
                    showError(e, "Unable to delete lecture");
                });
        };
    
        const removeAllEvents = () => {
            if (!confirm("Really delete all?")) return;
            const promises = existingEvents.flatMap((event) => {
                if (event.foreign_id === undefined) {
                    return [];
                }
    
                return [api.deleteOMObject("lecture", event.foreign_id)];
            });
            Promise.allSettled(promises)
                .then((res) => {
                    reloadFunc();
                })
                .catch((e) => {
                    showError(e, "Unable to delete lectures");
                    reloadFunc();
                });
        };
    
        return (
            <div>
                <table className="table table-bordered w-100 table-sm text-center">
                    <thead>
                        <tr>
                            <th scope="col">Nur im Import</th>
                            <th scope="col">In beiden</th>
                            <th scope="col">Nur bei uns</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr>
                            <td className="text-warning">{new_events.length}</td>
                            <td className="text-success">{full_matches}</td>
                            <td className="text-warning">{not_imported_events.length}</td>
                        </tr>
                    </tbody>
                </table>
    
                <div className="card mb-2">
                    <div className="card-header d-flex align-items-center">
                        <span className="flex-grow-1">Im Import, nicht bei uns</span>
    
                        <button className="btn btn-primary" onClick={importAllEvents}>
                            Alle anlegen
                        </button>
                    </div>
                    <div className="card-body">
                        <table className="table">
                            <thead>
                                <tr>
                                    <th>Zeit</th>
                                    <th>Ort</th>
                                    <th>Dauer</th>
                                    <th></th>
                                </tr>
                            </thead>
                            <tbody>
                                {new_events.map((event, index) => (
                                    <tr key={index}>
                                        <td>
                                            {event.startDate
                                                ? event.startDate.toFormat("yyyy-MM-dd HH:mm:ss")
                                                : "kein datum"}
                                        </td>
                                        <td>{event.location}</td>
                                        <td>{event.duration}</td>
                                        <td>
                                            <button
                                                className="btn btn-primary"
                                                onClick={() => importEvent(event)}
                                            >
                                                Anlegen
                                            </button>
                                        </td>
                                    </tr>
                                ))}
                            </tbody>
                        </table>
                    </div>
                </div>
                <div className="card">
                    <div className="card-header d-flex align-items-center">
                        <span className="flex-grow-1">Bei uns, nicht im Import</span>
    
                        <button className="btn btn-primary" onClick={removeAllEvents}>
                            Alle entfernen
                        </button>
                    </div>
                    <div className="card-body">
                        <table className="table">
                            <thead>
                                <tr>
                                    <th>Zeit</th>
                                    <th>Ort</th>
                                    <th>Dauer</th>
                                    <th></th>
                                </tr>
                            </thead>
                            <tbody>
                                {not_imported_events.map((event, index) => (
                                    <tr key={index}>
                                        <td>
                                            {event.startDate
                                                ? event.startDate.toFormat("yyyy-MM-dd HH:mm:ss")
                                                : "kein datum"}
                                        </td>
                                        <td>{event.location}</td>
                                        <td>{event.duration}</td>
                                        <td>
                                            <button
                                                className="btn btn-primary"
                                                onClick={() => removeEvent(event)}
                                            >
                                                Entfernen
                                            </button>
                                        </td>
                                    </tr>
                                ))}
                            </tbody>
                        </table>
                    </div>
                </div>
            </div>
        );
    }
    
    function RWTHOnlineImportImpl() {
        const api = useBackendContext();
        const searchParam = useSearchParams();
        const course_id = searchParam.get("course_id");
        const [course, setCourse] = useState<course>();
        const imported_events = useRef<ICalEvent[]>([]);
        const imported_courses = useRef<number[]>([]);
        const inputRef = useRef<HTMLInputElement>(null);
        const [reload, setReload] = useState(false);
        const [isImporting, setIsImporting] = useState(false);
        const exampleImportUrl =
            "https://online.rwth-aachen.de/RWTHonline/pl/ui/%24ctx/wbTvw_List.lehrveranstaltung?pStpSpNr=...";
    
        useEffect(() => {
            if (course_id === null) return;
    
            api.getCourse(course_id, true).then((res) => {
                setCourse(res);
            });
        }, [course_id, api, reload]);
    
        if (course_id === null) {
            return (
                <div>
                    <p>Invalid ?course_id</p>
                </div>
            );
        }
    
        if (course === undefined) {
            return (
                <div>
                    <p>Loading...</p>
                </div>
            );
        }
    
        const onImport = () => {
            if (inputRef.current === null) return;
    
            const url = inputRef.current.value;
    
            let courseIdNr: number | undefined;
            if (url.includes("pStpSpNr=")) {
                courseIdNr = parseInt(url.split("pStpSpNr=")[1].split("&")[0]);
            } else {
                // https://online.rwth-aachen.de/RWTHonline/ee/ui/ca2/app/desktop/#/slc.tm.cp/student/courses/487373?$ctx=lang=de&$scrollTo=toc_overview
                const regex = /online\.rwth-aachen\.de\/.*\/courses\/(\d+)/;
                const match = url.match(regex);
                if (match) {
                    courseIdNr = parseInt(match[1]);
                } else {
                    showErrorMessage(
                        `The provided URL is invalid. Must be of format: ${exampleImportUrl}`,
                    );
                    return;
                }
            }
            if (courseIdNr === undefined) {
                showErrorMessage(`The provided URL is invalid. Must be of format: ${exampleImportUrl}`);
                return;
            }
            if (imported_courses.current.includes(courseIdNr)) {
                showErrorMessage(e, "The specified course is already in the import");
                return;
            }
    
            setIsImporting(true);
            api.importRWTHOnlineCourse(courseIdNr)
                .then((ical) => {
                    imported_courses.current.push(courseIdNr!);
                    const parsed = parseICal(ical);
                    // add id to events
                    parsed.forEach((event) => {
                        event.foreign_id = courseIdNr;
                    });
                    // append
                    imported_events.current = imported_events.current.concat(parsed);
    
                    setIsImporting(false);
                })
                .catch((e) => {
                    showError(e, "Unable to import lectures from RWTHonline");
                    setIsImporting(false);
                });
        };
    
        const removeCourse = (courseId: number) => {
            if (!confirm("Wirklich löschen?")) return;
    
            imported_courses.current = imported_courses.current.filter((id) => id !== courseId);
            imported_events.current = imported_events.current.filter(
                (event) => event.foreign_id !== courseId,
            );
            setReload(!reload);
        };
    
        return (
            <ReloadBoundary reloadFunc={() => setReload(!reload)}>
                <Link href={`/${course.id_string}`} className="btn btn-primary mb-2">
                    <span className="bi bi-chevron-left" /> Zum Kurs
                </Link>
                <div className="card mb-2">
                    <div className="card-header d-flex">Import von RWTHOnline</div>
                    <div className="card-body">
                        <a
                            href="https://online.rwth-aachen.de/RWTHonline/pl/ui/%24ctx/wbSuche.LVSuche"
                            target="_blank"
                        >
                            Suche den Kurs auf RWTHOnline
                        </a>
                        , und kopiere die URL hier rein:
                        <div>
                            <input
                                type="text"
                                className="w-100"
                                placeholder={exampleImportUrl}
                                ref={inputRef}
                            />
                            {isImporting ? (
                                <div className="spinner-border mt-2" role="status" />
                            ) : (
                                <button
                                    className="btn btn-primary mt-2"
                                    type="button"
                                    onClick={onImport}
                                >
                                    Zum Import hinzufügen
                                </button>
                            )}
                        </div>
                        <div>
                            Bereits importierte Termine: {imported_events.current.length}
                            <table className="table table-bordered">
                                <tbody>
                                    {imported_courses.current.map((courseId, index) => (
                                        <tr key={index}>
                                            <td>
                                                {courseId}
    
                                                <button
                                                    type="button"
                                                    className="ms-2 btn btn-danger"
                                                    onClick={() => removeCourse(courseId)}
                                                >
                                                    <i className="bi bi-trash-fill"></i>
                                                </button>
                                            </td>
                                        </tr>
                                    ))}
                                </tbody>
                            </table>
                        </div>
                    </div>
                </div>
                <TerminBody course={course} imported_events={imported_events} />
            </ReloadBoundary>
        );
    }
    
    export default function RWTHOnlineImport() {
        return (
            <ModeratorBarrier>
                <RWTHOnlineImportImpl />
            </ModeratorBarrier>
        );
    }