diff --git a/.travis.yml b/.travis.yml index 04f6dca8..771ed147 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,12 +15,12 @@ jobs: script: - "./mvnw clean package -B -q" after_script: - - cd .. - - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin - - docker build -t vscode4teaching/vscode4teaching:2.1.4 . - - docker build -t vscode4teaching/vscode4teaching:latest . - - docker push vscode4teaching/vscode4teaching:2.1.4 - - docker push vscode4teaching/vscode4teaching:latest + - cd .. + - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin + - docker build -t vscode4teaching/vscode4teaching:2.2.0 . + - docker build -t vscode4teaching/vscode4teaching:latest . + - docker push vscode4teaching/vscode4teaching:2.2.0 + - docker push vscode4teaching/vscode4teaching:latest - name: V4T Extension (Node.js) language: node_js os: diff --git a/README.md b/README.md index 6a03cca2..bcb69977 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # VSCode4Teaching -[![Build Status](https://api.travis-ci.com/codeurjc-students/2019-VSCode4Teaching.svg?branch=master)](https://app.travis-ci.com/github/codeurjc-students/2019-VSCode4Teaching) -[![Extension version](https://vsmarketplacebadge.apphb.com/version-short/VSCode4Teaching.vscode4teaching.svg)](https://marketplace.visualstudio.com/items?itemName=VSCode4Teaching.vscode4teaching) -[![Extension installs](https://vsmarketplacebadge.apphb.com/installs/VSCode4Teaching.vscode4teaching.svg)](https://marketplace.visualstudio.com/items?itemName=VSCode4Teaching.vscode4teaching) +[![Travis CI build status](https://img.shields.io/travis/com/codeurjc-students/2019-VSCode4Teaching?label=Travis%20CI&style=flat-square)](https://app.travis-ci.com/github/codeurjc-students/2019-VSCode4Teaching) +[![Docker Hub Repository](https://img.shields.io/docker/v/vscode4teaching/vscode4teaching?color=0db7ed&label=Docker%20Hub&sort=date&style=flat-square)](https://hub.docker.com/r/vscode4teaching/vscode4teaching) +[![Docker Hub pulls](https://img.shields.io/docker/pulls/vscode4teaching/vscode4teaching?color=0db7ed&label=Docker%20Hub%20pulls&style=flat-square)](https://hub.docker.com/r/vscode4teaching/vscode4teaching) +[![VS Marketplace extension's version](https://img.shields.io/visual-studio-marketplace/v/vscode4teaching.vscode4teaching?color=0078d7&label=VS%20Marketplace&style=flat-square)](https://marketplace.visualstudio.com/items?itemName=VSCode4Teaching.vscode4teaching) +[![VS Marketplace extension's installs](https://img.shields.io/visual-studio-marketplace/i/vscode4teaching.vscode4teaching?color=0078d7&label=VS%20Marketplace%20installs&style=flat-square)](https://marketplace.visualstudio.com/items?itemName=VSCode4Teaching.vscode4teaching) VSCode4Teaching is a [Visual Studio Code](https://code.visualstudio.com) extension that brings the programming exercises of a course directly to the student’s editor, so that the teacher of that course can check the progress of the students and help them. It was created and expanded by Iván Chicano Capelo (whose blog can be read clicking [here](https://medium.com/@ivchicano)) and Álvaro Justo Rivas Alcobendas. Currently, this project is being developed by Diego Guerrero Carrasco. All the information about the progress of this stage of the project can be read in [this blog](https://medium.com/@diego-guerrero). @@ -44,7 +46,7 @@ VSCode4Teaching is composed of three components that work cooperatively with eac ### How to quickly start up a server To set up a VSCode4Teaching server, the fastest method is to use **Docker**, which is a lightweight container-based technology to speed up application deployment. For this purpose, some relevant files are inserted into the repository: - A [``Dockerfile``](Dockerfile) file containing the necessary coding to compile the webapp and insert it as a server view, which is compiled and launched in a Java container. The image resulting from this compilation is published in [*Docker Hub*](https://hub.docker.com/r/vscode4teaching/vscode4teaching) each time a new version of the application is released. -- A file [``vscode4teaching-server/docker/docker-compose.yml``](vscode4teaching-server/docker/docker-compose.yml``) that allows using Docker Compose to quickly run two containers: one for the MySQL database used (``db``), for the image compiled from the ``Dockerfile`` above (``app``) and for the execution of a graphical database manager (``adminer``), which is optional and can be removed without affecting the operation of the server. +- A file [``vscode4teaching-server/docker/docker-compose.yml``](vscode4teaching-server/docker/docker-compose.yml) that allows using Docker Compose to quickly run two containers: one for the MySQL database used (``db``), for the image compiled from the ``Dockerfile`` above (``app``) and for the execution of a graphical database manager (``adminer``), which is optional and can be removed without affecting the operation of the server. - A file [``vscode4teaching-server/docker/.env``](vscode4teaching-server/docker/.env) with user-customizable environment variables for the execution of the ``docker-compose.yml`` above. Therefore, it is possible to run a VSCode4Teaching server directly using the ``docker-compose.yml`` file, by pointing a terminal to the directory containing it and running the command ``docker compose up -d`` (or ``docker-compose up -d`` in earlier versions of Docker). diff --git a/docs-resources/AddMultipleExercisesWithSolution.gif b/docs-resources/AddMultipleExercisesWithSolution.gif new file mode 100644 index 00000000..ca949010 Binary files /dev/null and b/docs-resources/AddMultipleExercisesWithSolution.gif differ diff --git a/docs-resources/AddSingleMultipleExercises.png b/docs-resources/AddSingleMultipleExercises.png new file mode 100644 index 00000000..eaf89da9 Binary files /dev/null and b/docs-resources/AddSingleMultipleExercises.png differ diff --git a/docs-resources/InstallExtension.gif b/docs-resources/InstallExtension.gif index 6a1b297f..99dd4726 100644 Binary files a/docs-resources/InstallExtension.gif and b/docs-resources/InstallExtension.gif differ diff --git a/docs-resources/NewTeacherLogsIn.gif b/docs-resources/NewTeacherLogsIn.gif index b8283a7b..c4ba6da9 100644 Binary files a/docs-resources/NewTeacherLogsIn.gif and b/docs-resources/NewTeacherLogsIn.gif differ diff --git a/docs-resources/SharingAddRemoveUsers.png b/docs-resources/SharingAddRemoveUsers.png index d3078562..0139f4e8 100644 Binary files a/docs-resources/SharingAddRemoveUsers.png and b/docs-resources/SharingAddRemoveUsers.png differ diff --git a/docs-resources/SharingLinkGetCourseCode.gif b/docs-resources/SharingLinkGetCourseCode.gif index 6aba2776..55960dd7 100644 Binary files a/docs-resources/SharingLinkGetCourseCode.gif and b/docs-resources/SharingLinkGetCourseCode.gif differ diff --git a/docs-resources/StudentChecksDiffWithSolution.gif b/docs-resources/StudentChecksDiffWithSolution.gif new file mode 100644 index 00000000..6b3c01c1 Binary files /dev/null and b/docs-resources/StudentChecksDiffWithSolution.gif differ diff --git a/docs-resources/StudentChecksHelpPage.gif b/docs-resources/StudentChecksHelpPage.gif index 1e8c47a7..74e3063b 100644 Binary files a/docs-resources/StudentChecksHelpPage.gif and b/docs-resources/StudentChecksHelpPage.gif differ diff --git a/docs-resources/StudentDownloadsExercise.gif b/docs-resources/StudentDownloadsExercise.gif new file mode 100644 index 00000000..e5d64218 Binary files /dev/null and b/docs-resources/StudentDownloadsExercise.gif differ diff --git a/docs-resources/StudentFinishingExercise.gif b/docs-resources/StudentFinishingExercise.gif index 5f12cef9..9989cf86 100644 Binary files a/docs-resources/StudentFinishingExercise.gif and b/docs-resources/StudentFinishingExercise.gif differ diff --git a/docs-resources/StudentJoinsCourse.gif b/docs-resources/StudentJoinsCourse.gif index fc0a21e8..8774c3d4 100644 Binary files a/docs-resources/StudentJoinsCourse.gif and b/docs-resources/StudentJoinsCourse.gif differ diff --git a/docs-resources/StudentRefreshDownloadSolution.gif b/docs-resources/StudentRefreshDownloadSolution.gif new file mode 100644 index 00000000..652275cc Binary files /dev/null and b/docs-resources/StudentRefreshDownloadSolution.gif differ diff --git a/docs-resources/TeacherBulkAddsExercises.gif b/docs-resources/TeacherBulkAddsExercises.gif index c9722c27..76300e5c 100644 Binary files a/docs-resources/TeacherBulkAddsExercises.gif and b/docs-resources/TeacherBulkAddsExercises.gif differ diff --git a/docs-resources/TeacherChangesExerciseSettings.gif b/docs-resources/TeacherChangesExerciseSettings.gif new file mode 100644 index 00000000..4f26fedc Binary files /dev/null and b/docs-resources/TeacherChangesExerciseSettings.gif differ diff --git a/docs-resources/TeacherChangingPassword.gif b/docs-resources/TeacherChangingPassword.gif index a88fe8dd..ea319b32 100644 Binary files a/docs-resources/TeacherChangingPassword.gif and b/docs-resources/TeacherChangingPassword.gif differ diff --git a/docs-resources/TeacherCreatesCourse.gif b/docs-resources/TeacherCreatesCourse.gif index efa08533..a52d7500 100644 Binary files a/docs-resources/TeacherCreatesCourse.gif and b/docs-resources/TeacherCreatesCourse.gif differ diff --git a/docs-resources/TeacherDownloadsExercise.gif b/docs-resources/TeacherDownloadsExercise.gif new file mode 100644 index 00000000..f921f333 Binary files /dev/null and b/docs-resources/TeacherDownloadsExercise.gif differ diff --git a/docs-resources/TeacherFullDashboardHideButton.gif b/docs-resources/TeacherFullDashboardHideButton.gif deleted file mode 100644 index 9223d29b..00000000 Binary files a/docs-resources/TeacherFullDashboardHideButton.gif and /dev/null differ diff --git a/docs-resources/TeacherGeneratesSharingLink.gif b/docs-resources/TeacherGeneratesSharingLink.gif index 39bd2f7f..c1ac6f3c 100644 Binary files a/docs-resources/TeacherGeneratesSharingLink.gif and b/docs-resources/TeacherGeneratesSharingLink.gif differ diff --git a/docs-resources/TeacherInvitingTeacher.gif b/docs-resources/TeacherInvitingTeacher.gif index f8417519..8d87e2b3 100644 Binary files a/docs-resources/TeacherInvitingTeacher.gif and b/docs-resources/TeacherInvitingTeacher.gif differ diff --git a/docs-resources/TeacherLightDashboard.gif b/docs-resources/TeacherLightDashboard.gif deleted file mode 100644 index 330fca8b..00000000 Binary files a/docs-resources/TeacherLightDashboard.gif and /dev/null differ diff --git a/docs-resources/TeacherLightDashboardChangeSolutionParameters.gif b/docs-resources/TeacherLightDashboardChangeSolutionParameters.gif new file mode 100644 index 00000000..59c0130d Binary files /dev/null and b/docs-resources/TeacherLightDashboardChangeSolutionParameters.gif differ diff --git a/docs-resources/TeacherLightDashboardHideButton.gif b/docs-resources/TeacherLightDashboardHideButton.gif index 2264bb68..e4961878 100644 Binary files a/docs-resources/TeacherLightDashboardHideButton.gif and b/docs-resources/TeacherLightDashboardHideButton.gif differ diff --git a/docs-resources/TeacherLightDashboardNoStudents.png b/docs-resources/TeacherLightDashboardNoStudents.png deleted file mode 100644 index 2e626a8f..00000000 Binary files a/docs-resources/TeacherLightDashboardNoStudents.png and /dev/null differ diff --git a/docs-resources/TeacherLogIn.gif b/docs-resources/TeacherLogIn.gif deleted file mode 100644 index 2ace4f6c..00000000 Binary files a/docs-resources/TeacherLogIn.gif and /dev/null differ diff --git a/docs-resources/TeacherLogsIn.gif b/docs-resources/TeacherLogsIn.gif new file mode 100644 index 00000000..8e483f3d Binary files /dev/null and b/docs-resources/TeacherLogsIn.gif differ diff --git a/docs-resources/VSCodeSettingsView.png b/docs-resources/VSCodeSettingsView.png index e2202f42..2b44ff33 100644 Binary files a/docs-resources/VSCodeSettingsView.png and b/docs-resources/VSCodeSettingsView.png differ diff --git a/vscode4teaching-extension/README.md b/vscode4teaching-extension/README.md index b2b561ca..87a5d0cc 100644 --- a/vscode4teaching-extension/README.md +++ b/vscode4teaching-extension/README.md @@ -8,7 +8,9 @@ This document introduces a user's guide to the application and a developer's gui ## Table of contents - [User guide](#user-guide) - - [Introduction](#introduction) + - [How to use VSCode4Teaching: user roles and functionalities](#how-to-use-vscode4teaching-user-roles-and-functionalities) + - [Teachers](#teachers) + - [Students](#students) - [Download and installation](#download-and-installation) - [Typical use cases](#typical-use-cases) - [Developer guide](#developer-guide) @@ -20,34 +22,57 @@ This document introduces a user's guide to the application and a developer's gui ## User guide **Sections** -- [Introduction](#introduction) +- [How to use VSCode4Teaching: user roles and functionalities](#how-to-use-vscode4teaching-user-roles-and-functionalities) + - [Teachers](#teachers) + - [Students](#students) - [Download and installation](#download-and-installation) - [Typical use cases](#typical-use-cases) -### Introduction -VSCode4Teaching users can be either teachers or students. - -- **Teachers** can create, modify and delete courses on the platform, and each course can contain several exercises. In this way, teachers can add exercises (one at a time or in bulk) based on pre-existing code templates, proposing to students to work on these templates to fulfill the exercises. - - Teachers can either manually enroll students in their courses using the buttons provided in each course or generate a sharing link to send to their students, who will open it in a browser where they will see a help screen explaining how to enroll in the course. - ![Image showing all the possibilities to register users](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/SharingAddRemoveUsers.png) - - Teachers can display a dashboard, which is a screen that allows them to view the progress of students in completing the exercises in real time. They can see which students are working yet or have already completed the exercises, how long ago the last update of an exercise took place, which files have been created, modified or deleted in each of the students' proposals and, in addition, graphically view the differences between the students' files and the template originally uploaded by the teacher. - ![Animated image of the dashboard of an exercise for teachers](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherLightDashboardHideButton.gif) - - Teachers can visualize in their file system the students' exercises saved in an anonymized way, which will be synchronized in real time as they are completed by the students. To find out which directory corresponds to which student, the teacher will have to use the dashboard. -- **Students**, on the other hand, can join courses and complete the exercises proposed by their teachers. - - To join courses, students must wait for a teacher to register them manually or they can receive an invitation link from the teacher. When opening the link in a web browser, students can view a help page that explains textually and graphically how to register for courses and how to use the extension in detail. - ![Animated image of the process to get the link to share courses and enroll from the help page](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/SharingLinkGetCourseCode.gif) - - When students start a new exercise, the template proposed by the teacher is downloaded to their local file system to be filled in. - - Once an exercise has been completed, students have a button to indicate to the teacher that the exercise has been completed and, from that moment on, it will not be possible to edit the proposed submission. - ![Animated image showing the process of completion of an exercise](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/StudentFinishingExercise.gif) +### How to use VSCode4Teaching: user roles and functionalities +VSCode4Teaching users can be either [teachers](#teachers) or [students](#students). + +#### Teachers +**Teachers** can create, modify and delete courses on the platform, and each course can contain several exercises. In this way, teachers can add exercises (one at a time or in bulk) based on pre-existing code templates, proposing students to work on these templates. In addition, they can provide solution proposals for the exercises so that students can compare them with their own proposals when they finish their own exercises. + +- Teachers can either manually enroll students in their courses using the buttons provided in each course or generate a sharing link to send to their students, who will open it in a browser where they will see a help screen explaining how to enroll in the course. +[![Image showing all the possibilities to register users](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/SharingAddRemoveUsers.png)](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/SharingAddRemoveUsers.png) +- Teachers can create, modify and delete exercises in their courses. To create them, they can do it one at a time or in bulk, for which they have two different buttons. They must provide a directory containing, in the first case, all the files that will be linked to the exercise created and, in the second case, a subdirectory for each of the exercises to be created. +[![Image showing all the possibilities to add new exercises](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/AddSingleMultipleExercises.png)](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/AddSingleMultipleExercises.png) +- Teachers can include a predefined solution proposal in the exercises when uploading them. To do this, the directory of each exercise (regardless of the uploading procedure executed) should contain only two subdirectories: ``template`` (containing the exercise template) and ``solution`` (containing the teacher's solution proposal). +[![Animated image of the simultaneous uploading of several exercises with solution included](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/AddMultipleExercisesWithSolution.gif)](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/AddMultipleExercisesWithSolution.gif) +- Teachers can display a _dashboard_ for each exercise, which is a screen that allows them to get a real-time view of the students' progress and to modify some exercise settings. It contains three sections: + - The "General statistics" section, where an overall progress graph and numerical values on how many students have completed, are in progress, or have not started an exercise, as well as how many have modified their exercise in the last 5 minutes, 30 minutes, 1 hour, and 2 hours, can be seen. + - The "Exercise configuration" section, which includes a dropdown panel with a quick guide to the exercise controls included in the _dashboard_ and, if the exercise includes a solution, two controls to modify the availability of the solution to students and to determine whether editing of the exercise is allowed once the solution is downloaded. + - The "Student's progress" section, which includes a table that allows to visualize the individual data of each student, displaying the status of the exercise for each of them, which files have been created, modified or deleted and, in addition, to see graphically the differences between the students' files and the template originally added by the teacher. + + [![Animated image of the teacher's dashboard of an exercise](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherLightDashboardHideButton.gif)](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherLightDashboardHideButton.gif) +- Teachers can visualize in their file system the students' exercises saved in an anonymized way, which will be synchronized in real time as they are completed by the students. To find out which directory corresponds to which student, the teacher will have to use the _dashboard_. +[![Animated image of the download of an exercise by a teacher, including template, solution and all student proposals](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherDownloadsExercise.gif)](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherDownloadsExercise.gif) +- Specifically for exercises with solution, teachers can make it available by using the checkboxes introduced for this purpose in the _dashboard_. There are two controls: "allow editing after downloading solution", which allows teachers to determine whether students can continue to modify an exercise once the solution has been downloaded; and "publish solution", which allows teachers to choose when the proposed solution is published to students, this being an irreversible change. +[![Animated image showing modifications to the controls](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherChangesExerciseSettings.gif)](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherChangesExerciseSettings.gif) + +#### Students +**Students**, on the other hand, can join courses and complete the exercises proposed by their teachers. +- To join courses, students must wait for a teacher to register them manually or they can receive an invitation link from the teacher. When opening the link in a web browser, students can view a help page that explains textually and graphically how to register for courses and how to use the extension in detail. +[![Animated image of the process to get the link to share courses and enroll from the help page](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/SharingLinkGetCourseCode.gif)](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/SharingLinkGetCourseCode.gif) +- When students start a new exercise, the template proposed by the teacher is downloaded to their local file system to be filled in. +[![Animated image of a student downloading and activating an exercise](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/StudentDownloadsExercise.gif)](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/StudentDownloadsExercise.gif) +- Once an exercise has been completed, students have a button to indicate to the teacher that the exercise has been completed and, from that moment on, it will not be possible to edit the proposed submission. +[![Animated image showing the process of completion of an exercise](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/StudentFinishingExercise.gif)](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/StudentFinishingExercise.gif) +- In case a student is performing an exercise and the teacher communicates that his or her solution is already available, he or she can download it by reloading and using the button at the bottom. As determined by the teacher, this action may imply the completion of the exercise (the student is informed in a warning before downloading). +[![Animated image of the extension reloading and downloading of the active exercise solution](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/StudentRefreshDownloadSolution.gif)](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/StudentRefreshDownloadSolution.gif) +- Once the solution is downloaded, students can use the "Diff with teacher's solution" button to check the differences between their proposal and the exercise solution created by the teacher. +[![Imagen animada de la visualización de diferencias entre la propuesta del estudiante y la solución del profesor](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/StudentChecksDiffWithSolution.gif)](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/StudentChecksDiffWithSolution.gif) ### Download and installation To start using VSCode4Teaching as a user in your local Visual Studio Code installation, you need to: - Access the Visual Studio Code *Marketplace* by clicking on the corresponding icon in the sidebar. - Search for the extension using the top bar ("VSCode4Teaching"). - Install the latest available version and all required dependencies (like, for example, the *LiveShare* extension). A prompt will be displayed when installing in the IDE so as to install the associated dependencies. - ![Animated image showing the installation process of the extension from the Marketplace](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/InstallExtension.gif) + [![Animated image showing the installation process of the extension from the Marketplace](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/InstallExtension.gif)](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/InstallExtension.gif) + - When the extension is installed, it could be necessary to change its own settings to modify the local directory used or the URL of the server. These preferences can be set in the IDE preferences within the extension's specific section. - ![Image showing the extension-specific preferences in Visual Studio Code](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/VSCodeSettingsView.png) + [![Imagen mostrando las preferencias específicas de la extensión en Visual Studio Code](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/VSCodeSettingsView.png)](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/VSCodeSettingsView.png) ### Typical use cases Some of the main use cases of user-system and user-to-user interaction in VSCode4Teaching are introduced below in textual and graphical sequence format. @@ -59,23 +84,23 @@ For this case two users are assumed: Teacher 1 (previously registered teacher) a | Teacher 1 | Teacher 2 | | :-------: | :-------: | -| **1**. The user logs in. ![Animated image showing the login of a previously registered teacher](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherLogIn.gif) | | +| **1**. The user logs in. [![Animated image showing the login of a previously registered teacher](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherLogsIn.gif)](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherLogsIn.gif) | | | **2**. The user fills in the invitation for the new teacher using the button and the form provided for this purpose. You must enter: name and surname, e-mail and user name of the new teacher. | | -| **3**. An invitation link is generated. It has to be sent to the new teacher. ![Animated image showing the process for inviting a new teacher to the extension](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherInvitingTeacher.gif) | | -| | **4**. The new teacher will open the link received in a browser to complete the registration process. To do so, they enter their user name (for identity verification) and choose their new password. ![Animated image showing the process for changing the password of a new teacher](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherChangingPassword.gif) | -| | **5**. Done! The new teacher can now log in to VSCode4Teaching successfully. ![Animated image showing the login of a previously registered teacher](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/NewTeacherLogsIn.gif) | +| **3**. An invitation link is generated. It has to be sent to the new teacher. [![Animated image showing the process for inviting a new teacher to the extension](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherInvitingTeacher.gif)](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherInvitingTeacher.gif) | | +| | **4**. The new teacher will open the link received in a browser to complete the registration process. To do so, they enter their user name (for identity verification) and choose their new password. [![Animated image showing the process for changing the password of a new teacher](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherChangingPassword.gif)](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherChangingPassword.gif) | +| | **5**. Done! The new teacher can now log in to VSCode4Teaching successfully. [![Animated image showing the login of a previously registered teacher](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/NewTeacherLogsIn.gif)](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/NewTeacherLogsIn.gif) | #### Course creation and student participation In this case, a teacher logs in, creates a subject, adds exercises and invites students who, once enrolled, proceed to fill in the exercises and send them to the teacher. | Teacher | Student | | :-----: | :-----: | -| **1**. The user logs in. ![Animated image showing teacher login](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/NewTeacherLogsIn.gif) | | -| **2**. The user creates a new course. ![Animated image showing the process of creating a new course](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherCreatesCourse.gif) | | -| **3**. The user adds some new exercises in bulk. ![Animated image showing the process of simultaneous exercise uploading](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherBulkAddsExercises.gif) | | -| **4**. The user generates the course invitation link and sends it to one (or more) students. ![Animated image showing how a teacher generates a link to share a course](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherGeneratesSharingLink.gif) | | -| | **5**. The student accesses the link in a web browser, where they can view a help page and copy a specific code to join the course. ![Animated image showing the help page for enrolling in a course](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/StudentChecksHelpPage.gif) | -| | **6**. The student uses the code copied in the extension to join the course shared by their teacher. ![Animated image showing how the student registers for a course by entering the code obtained](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/StudentJoinsCourse.gif) | +| **1**. The user logs in. [![Animated image showing teacher login](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/NewTeacherLogsIn.gif)](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/NewTeacherLogsIn.gif) | | +| **2**. The user creates a new course. [![Animated image showing the process of creating a new course](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherCreatesCourse.gif)](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherCreatesCourse.gif) | | +| **3**. The user adds some new exercises in bulk. [![Animated image showing the process of simultaneous exercise uploading](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherBulkAddsExercises.gif)](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherBulkAddsExercises.gif) | | +| **4**. The user generates the course invitation link and sends it to one (or more) students. [![Animated image showing how a teacher generates a link to share a course](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherGeneratesSharingLink.gif)](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/TeacherGeneratesSharingLink.gif) | | +| | **5**. The student accesses the link in a web browser, where they can view a help page and copy a specific code to join the course. [![Animated image showing the help page for enrolling in a course](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/StudentChecksHelpPage.gif)](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/StudentChecksHelpPage.gif) | +| | **6**. The student uses the code copied in the extension to join the course shared by their teacher. [![Animated image showing how the student registers for a course by entering the code obtained](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/StudentJoinsCourse.gif)](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/StudentJoinsCourse.gif) | ## Developer guide @@ -104,4 +129,4 @@ The extension starts automatically when Visual Studio Code is opened if it is in - Building the source code provided as indicated and installing the extension in VSIX format using the "Install from VSIX" option available in the context menu of the Extensions panel. Once installed, it may be necessary to modify the server and download directory settings. To do this, in the IDE preferences, it is necessary to look for the VSCode4Teaching preferences, which look as follows: -![Extension Settings in Visual Studio Code](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/VSCodeSettingsView.png) +[![Extension Settings in Visual Studio Code](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/VSCodeSettingsView.png)](https://github.com/codeurjc-students/2019-VSCode4Teaching/raw/master/docs-resources/VSCodeSettingsView.png) diff --git a/vscode4teaching-extension/jest.config.js b/vscode4teaching-extension/jest.config.js index 81a2fd7b..d224cfc5 100644 --- a/vscode4teaching-extension/jest.config.js +++ b/vscode4teaching-extension/jest.config.js @@ -4,5 +4,9 @@ module.exports = { roots: ['./test/unitSuite'], testResultsProcessor: 'jest-sonar-reporter', verbose: true, - setupFilesAfterEnv: ["/test/setup.js"] -}; \ No newline at end of file + setupFilesAfterEnv: ["/test/setup.js"], + transformIgnorePatterns: ['/node_modules/(?!axios/)'], + moduleNameMapper: { + '^axios$': require.resolve('axios'), + } +}; diff --git a/vscode4teaching-extension/package-lock.json b/vscode4teaching-extension/package-lock.json index 774e287f..3495d68e 100644 --- a/vscode4teaching-extension/package-lock.json +++ b/vscode4teaching-extension/package-lock.json @@ -1,23 +1,23 @@ { "name": "vscode4teaching", - "version": "2.1.4", + "version": "2.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "vscode4teaching", - "version": "2.1.4", + "version": "2.2.0", "license": "SEE LICENSE IN LICENSE", "dependencies": { - "axios": "^0.27.2", + "axios": "^1.1.3", "form-data": "^4.0.0", "ignore": "^5.1.6", - "jszip": "^3.4.0", + "jszip": "~3.7.0", "lodash.escaperegexp": "^4.1.2", "mkdirp": "^1.0.4", "vsls": "^1.0.4753", - "winston": "^3.8.1", - "ws": "^8.8.0" + "winston": "^3.8.2", + "ws": "^8.9.0" }, "devDependencies": { "@types/jest": "^26.0.24", @@ -25,19 +25,19 @@ "@types/mkdirp": "^1.0.2", "@types/node": "^10.17.60", "@types/rimraf": "^3.0.2", - "@types/vscode": "^1.45.1", + "@types/vscode": "1.64.0", "@types/ws": "^8.5.3", - "esbuild": "^0.14.49", - "eslint": "^8.19.0", + "esbuild": "^0.14.54", + "eslint": "^8.26.0", "jest": "^26.6.3", "jest-sonar-reporter": "^2.0.0", - "node-html-parser": "^5.3.3", + "node-html-parser": "^6.1.1", "rimraf": "^3.0.2", "ts-jest": "^26.5.6", - "typescript": "^4.7.4" + "typescript": "^4.8.4" }, "engines": { - "vscode": "^1.61.0" + "vscode": "^1.64.0" } }, "node_modules/@ampproject/remapping": { @@ -104,16 +104,6 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -513,16 +503,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/@babel/types": { "version": "7.18.8", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.8.tgz", @@ -590,15 +570,31 @@ "kuler": "^2.0.0" } }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz", + "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint/eslintrc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", - "integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", + "integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.3.2", + "espree": "^9.4.0", "globals": "^13.15.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -608,6 +604,9 @@ }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/@eslint/eslintrc/node_modules/argparse": { @@ -616,27 +615,10 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "node_modules/@eslint/eslintrc/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.16.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.16.0.tgz", - "integrity": "sha512-A1lrQfpNF+McdPOnnFqY3kSN0AFTy485bTi1bkLk4mVPODIUEcSfhHgRqA+QdXPksrSTTztYXx37NFV+GpGk3Q==", + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", + "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -673,9 +655,9 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", - "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.6.tgz", + "integrity": "sha512-jJr+hPTJYKyDILJfhNSHsjiwXYf26Flsz8DvNndOsHs5pwSnpGUEy8yzF0JYhCEvTDdV2vuOK5tt8BVhwO5/hg==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", @@ -686,21 +668,17 @@ "node": ">=10.10.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, - "dependencies": { - "ms": "2.1.2" - }, "engines": { - "node": ">=6.0" + "node": ">=12.22" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@humanwhocodes/object-schema": { @@ -1265,6 +1243,41 @@ "vscode-jsonrpc": "^4.0.0" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@sinonjs/commons": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", @@ -1490,9 +1503,9 @@ "dev": true }, "node_modules/@types/vscode": { - "version": "1.45.1", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.45.1.tgz", - "integrity": "sha512-0NO9qrrEJBO8FsqHCrFMgR2suKnwCsKBWvRSb2OzH5gs4i3QO5AhEMQYrSzDbU/wLPt7N617/rN9lPY213gmwg==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.64.0.tgz", + "integrity": "sha512-bSlAWz5WtcSL3cO9tAT/KpEH9rv5OBnm93OIIFwdCshaAiqr2bp1AUyEwW9MWeCvZBHEXc3V0fTYVdVyzDNwHA==", "dev": true }, "node_modules/@types/ws": { @@ -1526,9 +1539,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", - "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -1732,12 +1745,13 @@ "integrity": "sha512-d1W2aNSYcz/sxYO4pMGX9vq65qOTu0P800epMud+6cYYX0QcT7zyqcxec3VWzpgvdXo57UWmVbZpLMjX2m1I7Q==" }, "node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", + "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==", "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/babel-jest": { @@ -1945,7 +1959,7 @@ "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true }, "node_modules/brace-expansion": { @@ -2400,6 +2414,34 @@ "node": ">= 8" } }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/cssom": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", @@ -2438,6 +2480,23 @@ "node": ">=10" } }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -2539,6 +2598,32 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, "node_modules/domexception": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", @@ -2560,6 +2645,35 @@ "node": ">=8" } }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", + "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.1" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.191", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.191.tgz", @@ -2598,6 +2712,18 @@ "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", + "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -2608,9 +2734,9 @@ } }, "node_modules/esbuild": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.49.tgz", - "integrity": "sha512-/TlVHhOaq7Yz8N1OJrjqM3Auzo5wjvHFLk+T8pIue+fhnhIMpfAzsG6PLVMbFveVxqD2WOp3QHei+52IMUNmCw==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz", + "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==", "dev": true, "hasInstallScript": true, "bin": { @@ -2620,32 +2746,33 @@ "node": ">=12" }, "optionalDependencies": { - "esbuild-android-64": "0.14.49", - "esbuild-android-arm64": "0.14.49", - "esbuild-darwin-64": "0.14.49", - "esbuild-darwin-arm64": "0.14.49", - "esbuild-freebsd-64": "0.14.49", - "esbuild-freebsd-arm64": "0.14.49", - "esbuild-linux-32": "0.14.49", - "esbuild-linux-64": "0.14.49", - "esbuild-linux-arm": "0.14.49", - "esbuild-linux-arm64": "0.14.49", - "esbuild-linux-mips64le": "0.14.49", - "esbuild-linux-ppc64le": "0.14.49", - "esbuild-linux-riscv64": "0.14.49", - "esbuild-linux-s390x": "0.14.49", - "esbuild-netbsd-64": "0.14.49", - "esbuild-openbsd-64": "0.14.49", - "esbuild-sunos-64": "0.14.49", - "esbuild-windows-32": "0.14.49", - "esbuild-windows-64": "0.14.49", - "esbuild-windows-arm64": "0.14.49" + "@esbuild/linux-loong64": "0.14.54", + "esbuild-android-64": "0.14.54", + "esbuild-android-arm64": "0.14.54", + "esbuild-darwin-64": "0.14.54", + "esbuild-darwin-arm64": "0.14.54", + "esbuild-freebsd-64": "0.14.54", + "esbuild-freebsd-arm64": "0.14.54", + "esbuild-linux-32": "0.14.54", + "esbuild-linux-64": "0.14.54", + "esbuild-linux-arm": "0.14.54", + "esbuild-linux-arm64": "0.14.54", + "esbuild-linux-mips64le": "0.14.54", + "esbuild-linux-ppc64le": "0.14.54", + "esbuild-linux-riscv64": "0.14.54", + "esbuild-linux-s390x": "0.14.54", + "esbuild-netbsd-64": "0.14.54", + "esbuild-openbsd-64": "0.14.54", + "esbuild-sunos-64": "0.14.54", + "esbuild-windows-32": "0.14.54", + "esbuild-windows-64": "0.14.54", + "esbuild-windows-arm64": "0.14.54" } }, "node_modules/esbuild-android-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.49.tgz", - "integrity": "sha512-vYsdOTD+yi+kquhBiFWl3tyxnj2qZJsl4tAqwhT90ktUdnyTizgle7TjNx6Ar1bN7wcwWqZ9QInfdk2WVagSww==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz", + "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==", "cpu": [ "x64" ], @@ -2659,9 +2786,9 @@ } }, "node_modules/esbuild-android-arm64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.49.tgz", - "integrity": "sha512-g2HGr/hjOXCgSsvQZ1nK4nW/ei8JUx04Li74qub9qWrStlysaVmadRyTVuW32FGIpLQyc5sUjjZopj49eGGM2g==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz", + "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==", "cpu": [ "arm64" ], @@ -2675,9 +2802,9 @@ } }, "node_modules/esbuild-darwin-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.49.tgz", - "integrity": "sha512-3rvqnBCtX9ywso5fCHixt2GBCUsogNp9DjGmvbBohh31Ces34BVzFltMSxJpacNki96+WIcX5s/vum+ckXiLYg==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz", + "integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==", "cpu": [ "x64" ], @@ -2691,9 +2818,9 @@ } }, "node_modules/esbuild-darwin-arm64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.49.tgz", - "integrity": "sha512-XMaqDxO846srnGlUSJnwbijV29MTKUATmOLyQSfswbK/2X5Uv28M9tTLUJcKKxzoo9lnkYPsx2o8EJcTYwCs/A==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz", + "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==", "cpu": [ "arm64" ], @@ -2707,9 +2834,9 @@ } }, "node_modules/esbuild-freebsd-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.49.tgz", - "integrity": "sha512-NJ5Q6AjV879mOHFri+5lZLTp5XsO2hQ+KSJYLbfY9DgCu8s6/Zl2prWXVANYTeCDLlrIlNNYw8y34xqyLDKOmQ==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz", + "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==", "cpu": [ "x64" ], @@ -2723,9 +2850,9 @@ } }, "node_modules/esbuild-freebsd-arm64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.49.tgz", - "integrity": "sha512-lFLtgXnAc3eXYqj5koPlBZvEbBSOSUbWO3gyY/0+4lBdRqELyz4bAuamHvmvHW5swJYL7kngzIZw6kdu25KGOA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz", + "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==", "cpu": [ "arm64" ], @@ -2739,9 +2866,9 @@ } }, "node_modules/esbuild-linux-32": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.49.tgz", - "integrity": "sha512-zTTH4gr2Kb8u4QcOpTDVn7Z8q7QEIvFl/+vHrI3cF6XOJS7iEI1FWslTo3uofB2+mn6sIJEQD9PrNZKoAAMDiA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz", + "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==", "cpu": [ "ia32" ], @@ -2755,9 +2882,9 @@ } }, "node_modules/esbuild-linux-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.49.tgz", - "integrity": "sha512-hYmzRIDzFfLrB5c1SknkxzM8LdEUOusp6M2TnuQZJLRtxTgyPnZZVtyMeCLki0wKgYPXkFsAVhi8vzo2mBNeTg==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz", + "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==", "cpu": [ "x64" ], @@ -2771,9 +2898,9 @@ } }, "node_modules/esbuild-linux-arm": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.49.tgz", - "integrity": "sha512-iE3e+ZVv1Qz1Sy0gifIsarJMQ89Rpm9mtLSRtG3AH0FPgAzQ5Z5oU6vYzhc/3gSPi2UxdCOfRhw2onXuFw/0lg==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz", + "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==", "cpu": [ "arm" ], @@ -2787,9 +2914,9 @@ } }, "node_modules/esbuild-linux-arm64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.49.tgz", - "integrity": "sha512-KLQ+WpeuY+7bxukxLz5VgkAAVQxUv67Ft4DmHIPIW+2w3ObBPQhqNoeQUHxopoW/aiOn3m99NSmSV+bs4BSsdA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz", + "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==", "cpu": [ "arm64" ], @@ -2803,9 +2930,9 @@ } }, "node_modules/esbuild-linux-mips64le": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.49.tgz", - "integrity": "sha512-n+rGODfm8RSum5pFIqFQVQpYBw+AztL8s6o9kfx7tjfK0yIGF6tm5HlG6aRjodiiKkH2xAiIM+U4xtQVZYU4rA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz", + "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==", "cpu": [ "mips64el" ], @@ -2819,9 +2946,9 @@ } }, "node_modules/esbuild-linux-ppc64le": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.49.tgz", - "integrity": "sha512-WP9zR4HX6iCBmMFH+XHHng2LmdoIeUmBpL4aL2TR8ruzXyT4dWrJ5BSbT8iNo6THN8lod6GOmYDLq/dgZLalGw==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz", + "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==", "cpu": [ "ppc64" ], @@ -2835,9 +2962,9 @@ } }, "node_modules/esbuild-linux-riscv64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.49.tgz", - "integrity": "sha512-h66ORBz+Dg+1KgLvzTVQEA1LX4XBd1SK0Fgbhhw4akpG/YkN8pS6OzYI/7SGENiN6ao5hETRDSkVcvU9NRtkMQ==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz", + "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==", "cpu": [ "riscv64" ], @@ -2851,9 +2978,9 @@ } }, "node_modules/esbuild-linux-s390x": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.49.tgz", - "integrity": "sha512-DhrUoFVWD+XmKO1y7e4kNCqQHPs6twz6VV6Uezl/XHYGzM60rBewBF5jlZjG0nCk5W/Xy6y1xWeopkrhFFM0sQ==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz", + "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==", "cpu": [ "s390x" ], @@ -2867,9 +2994,9 @@ } }, "node_modules/esbuild-netbsd-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.49.tgz", - "integrity": "sha512-BXaUwFOfCy2T+hABtiPUIpWjAeWK9P8O41gR4Pg73hpzoygVGnj0nI3YK4SJhe52ELgtdgWP/ckIkbn2XaTxjQ==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz", + "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==", "cpu": [ "x64" ], @@ -2883,9 +3010,9 @@ } }, "node_modules/esbuild-openbsd-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.49.tgz", - "integrity": "sha512-lP06UQeLDGmVPw9Rg437Btu6J9/BmyhdoefnQ4gDEJTtJvKtQaUcOQrhjTq455ouZN4EHFH1h28WOJVANK41kA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz", + "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==", "cpu": [ "x64" ], @@ -2899,9 +3026,9 @@ } }, "node_modules/esbuild-sunos-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.49.tgz", - "integrity": "sha512-4c8Zowp+V3zIWje329BeLbGh6XI9c/rqARNaj5yPHdC61pHI9UNdDxT3rePPJeWcEZVKjkiAS6AP6kiITp7FSw==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz", + "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==", "cpu": [ "x64" ], @@ -2915,9 +3042,9 @@ } }, "node_modules/esbuild-windows-32": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.49.tgz", - "integrity": "sha512-q7Rb+J9yHTeKr9QTPDYkqfkEj8/kcKz9lOabDuvEXpXuIcosWCJgo5Z7h/L4r7rbtTH4a8U2FGKb6s1eeOHmJA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz", + "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==", "cpu": [ "ia32" ], @@ -2931,9 +3058,9 @@ } }, "node_modules/esbuild-windows-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.49.tgz", - "integrity": "sha512-+Cme7Ongv0UIUTniPqfTX6mJ8Deo7VXw9xN0yJEN1lQMHDppTNmKwAM3oGbD/Vqff+07K2gN0WfNkMohmG+dVw==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz", + "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==", "cpu": [ "x64" ], @@ -2947,9 +3074,9 @@ } }, "node_modules/esbuild-windows-arm64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.49.tgz", - "integrity": "sha512-v+HYNAXzuANrCbbLFJ5nmO3m5y2PGZWLe3uloAkLt87aXiO2mZr3BTmacZdjwNkNEHuH3bNtN8cak+mzVjVPfA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz", + "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==", "cpu": [ "arm64" ], @@ -3003,13 +3130,15 @@ } }, "node_modules/eslint": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.19.0.tgz", - "integrity": "sha512-SXOPj3x9VKvPe81TjjUJCYlV4oJjQw68Uek+AM0X4p+33dj2HY5bpTZOgnQHcG2eAm1mtCU9uNMnJi7exU/kYw==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.26.0.tgz", + "integrity": "sha512-kzJkpaw1Bfwheq4VXUezFriD1GxszX6dUekM7Z3aC2o4hju+tsR/XyTC3RcoSD7jmy9VkPU3+N6YjVU2e96Oyg==", "dev": true, "dependencies": { - "@eslint/eslintrc": "^1.3.0", - "@humanwhocodes/config-array": "^0.9.2", + "@eslint/eslintrc": "^1.3.3", + "@humanwhocodes/config-array": "^0.11.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -3019,18 +3148,21 @@ "eslint-scope": "^7.1.1", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.2", + "espree": "^9.4.0", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", "globals": "^13.15.0", + "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", @@ -3041,8 +3173,7 @@ "regexpp": "^3.2.0", "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" + "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" @@ -3158,28 +3289,27 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/eslint/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, - "dependencies": { - "ms": "2.1.2" - }, "engines": { - "node": ">=6.0" + "node": ">=10" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, "engines": { "node": ">=10" }, @@ -3227,6 +3357,21 @@ "node": ">= 0.8.0" } }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint/node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -3244,6 +3389,36 @@ "node": ">= 0.8.0" } }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint/node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3278,17 +3453,20 @@ } }, "node_modules/espree": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", - "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", + "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==", "dev": true, "dependencies": { - "acorn": "^8.7.1", + "acorn": "^8.8.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.3.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/esprima": { @@ -3659,6 +3837,15 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fb-watchman": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", @@ -3735,9 +3922,9 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, "node_modules/follow-redirects": { - "version": "1.14.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", - "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", "funding": [ { "type": "individual", @@ -3813,12 +4000,6 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", - "dev": true - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3917,6 +4098,12 @@ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "dev": true }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, "node_modules/growly": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", @@ -4314,6 +4501,15 @@ "node": ">=0.12.0" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -4452,23 +4648,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-source-maps/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/istanbul-reports": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", @@ -5743,6 +5922,12 @@ "node": ">= 10.13.0" } }, + "node_modules/js-sdsl": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", + "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==", + "dev": true + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5820,23 +6005,6 @@ "node": ">= 6.0.0" } }, - "node_modules/jsdom/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/jsdom/node_modules/form-data": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", @@ -6131,16 +6299,16 @@ "dev": true }, "node_modules/micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "dev": true, "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" + "braces": "^3.0.2", + "picomatch": "^2.3.1" }, "engines": { - "node": ">=8" + "node": ">=8.6" } }, "node_modules/mime-db": { @@ -6278,119 +6446,15 @@ "dev": true }, "node_modules/node-html-parser": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-5.3.3.tgz", - "integrity": "sha512-ncg1033CaX9UexbyA7e1N0aAoAYRDiV8jkTvzEnfd1GDvzFdrsXLzR4p4ik8mwLgnaKP/jyUFWDy9q3jvRT2Jw==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.1.tgz", + "integrity": "sha512-eYYblUeoMg0nR6cYGM4GRb1XncNa9FXEftuKAU1qyMIr6rXVtNyUKduvzZtkqFqSHVByq2lLjC7WO8tz7VDmnA==", "dev": true, "dependencies": { - "css-select": "^4.2.1", + "css-select": "^5.1.0", "he": "1.2.0" } }, - "node_modules/node-html-parser/node_modules/css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/node-html-parser/node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/node-html-parser/node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/node-html-parser/node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/node-html-parser/node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "dev": true, - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/node-html-parser/node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/node-html-parser/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/node-html-parser/node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -6467,6 +6531,18 @@ "node": ">=8" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nwsapi": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.1.tgz", @@ -6775,9 +6851,9 @@ "dev": true }, "node_modules/picomatch": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.1.tgz", - "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "engines": { "node": ">=8.6" @@ -6891,6 +6967,11 @@ "node": ">= 6" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -6916,6 +6997,26 @@ "node": ">=6" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -7104,6 +7205,16 @@ "node": ">=0.12" } }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -7128,6 +7239,29 @@ "node": "6.* || >= 7.*" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -7471,7 +7605,7 @@ "node_modules/set-immediate-shim": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "integrity": "sha512-Li5AOqrZWCVA2n5kryzEmqai6bKSIvpz5oUJHPVj6+dsbD3X1ixtsY5tEnsaNpH3pFAHmG8eIHUrtEtohrg+UQ==", "engines": { "node": ">=0.10.0" } @@ -8421,9 +8555,9 @@ } }, "node_modules/typescript": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -8541,9 +8675,9 @@ } }, "node_modules/uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "dependencies": { "punycode": "^2.1.0" @@ -8580,12 +8714,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -8733,10 +8861,11 @@ "dev": true }, "node_modules/winston": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.1.tgz", - "integrity": "sha512-r+6YAiCR4uI3N8eQNOg8k3P3PqwAm20cLKlzVD9E66Ch39+LZC+VH1UKf9JemQj2B3QoUHfKD7Poewn0Pr3Y1w==", + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz", + "integrity": "sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew==", "dependencies": { + "@colors/colors": "1.5.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", @@ -8866,9 +8995,9 @@ } }, "node_modules/ws": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", - "integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.9.0.tgz", + "integrity": "sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==", "engines": { "node": ">=10.0.0" }, @@ -8969,6 +9098,18 @@ "engines": { "node": ">=6" } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } }, "dependencies": { @@ -9020,15 +9161,6 @@ "semver": "^6.3.0" }, "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -9330,17 +9462,6 @@ "@babel/types": "^7.18.8", "debug": "^4.1.0", "globals": "^11.1.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } } }, "@babel/types": { @@ -9395,15 +9516,22 @@ "kuler": "^2.0.0" } }, + "@esbuild/linux-loong64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz", + "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==", + "dev": true, + "optional": true + }, "@eslint/eslintrc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", - "integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", + "integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.3.2", + "espree": "^9.4.0", "globals": "^13.15.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -9418,19 +9546,10 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, "globals": { - "version": "13.16.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.16.0.tgz", - "integrity": "sha512-A1lrQfpNF+McdPOnnFqY3kSN0AFTy485bTi1bkLk4mVPODIUEcSfhHgRqA+QdXPksrSTTztYXx37NFV+GpGk3Q==", + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", + "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -9454,27 +9573,22 @@ } }, "@humanwhocodes/config-array": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", - "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.6.tgz", + "integrity": "sha512-jJr+hPTJYKyDILJfhNSHsjiwXYf26Flsz8DvNndOsHs5pwSnpGUEy8yzF0JYhCEvTDdV2vuOK5tt8BVhwO5/hg==", "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", "minimatch": "^3.0.4" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - } } }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, "@humanwhocodes/object-schema": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", @@ -9922,6 +10036,32 @@ "vscode-jsonrpc": "^4.0.0" } }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, "@sinonjs/commons": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", @@ -10144,9 +10284,9 @@ "dev": true }, "@types/vscode": { - "version": "1.45.1", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.45.1.tgz", - "integrity": "sha512-0NO9qrrEJBO8FsqHCrFMgR2suKnwCsKBWvRSb2OzH5gs4i3QO5AhEMQYrSzDbU/wLPt7N617/rN9lPY213gmwg==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.64.0.tgz", + "integrity": "sha512-bSlAWz5WtcSL3cO9tAT/KpEH9rv5OBnm93OIIFwdCshaAiqr2bp1AUyEwW9MWeCvZBHEXc3V0fTYVdVyzDNwHA==", "dev": true }, "@types/ws": { @@ -10180,9 +10320,9 @@ "dev": true }, "acorn": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", - "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", "dev": true }, "acorn-globals": { @@ -10331,12 +10471,13 @@ "integrity": "sha512-d1W2aNSYcz/sxYO4pMGX9vq65qOTu0P800epMud+6cYYX0QcT7zyqcxec3VWzpgvdXo57UWmVbZpLMjX2m1I7Q==" }, "axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", + "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==", "requires": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "babel-jest": { @@ -10502,7 +10643,7 @@ "boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true }, "brace-expansion": { @@ -10872,6 +11013,25 @@ "which": "^2.0.1" } }, + "css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true + }, "cssom": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", @@ -10906,6 +11066,15 @@ "whatwg-url": "^8.0.0" } }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -10980,6 +11149,23 @@ "esutils": "^2.0.2" } }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true + }, "domexception": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", @@ -10997,6 +11183,26 @@ } } }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", + "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", + "dev": true, + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.1" + } + }, "electron-to-chromium": { "version": "1.4.191", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.191.tgz", @@ -11029,6 +11235,12 @@ "once": "^1.4.0" } }, + "entities": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", + "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "dev": true + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -11039,170 +11251,171 @@ } }, "esbuild": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.49.tgz", - "integrity": "sha512-/TlVHhOaq7Yz8N1OJrjqM3Auzo5wjvHFLk+T8pIue+fhnhIMpfAzsG6PLVMbFveVxqD2WOp3QHei+52IMUNmCw==", - "dev": true, - "requires": { - "esbuild-android-64": "0.14.49", - "esbuild-android-arm64": "0.14.49", - "esbuild-darwin-64": "0.14.49", - "esbuild-darwin-arm64": "0.14.49", - "esbuild-freebsd-64": "0.14.49", - "esbuild-freebsd-arm64": "0.14.49", - "esbuild-linux-32": "0.14.49", - "esbuild-linux-64": "0.14.49", - "esbuild-linux-arm": "0.14.49", - "esbuild-linux-arm64": "0.14.49", - "esbuild-linux-mips64le": "0.14.49", - "esbuild-linux-ppc64le": "0.14.49", - "esbuild-linux-riscv64": "0.14.49", - "esbuild-linux-s390x": "0.14.49", - "esbuild-netbsd-64": "0.14.49", - "esbuild-openbsd-64": "0.14.49", - "esbuild-sunos-64": "0.14.49", - "esbuild-windows-32": "0.14.49", - "esbuild-windows-64": "0.14.49", - "esbuild-windows-arm64": "0.14.49" + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz", + "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==", + "dev": true, + "requires": { + "@esbuild/linux-loong64": "0.14.54", + "esbuild-android-64": "0.14.54", + "esbuild-android-arm64": "0.14.54", + "esbuild-darwin-64": "0.14.54", + "esbuild-darwin-arm64": "0.14.54", + "esbuild-freebsd-64": "0.14.54", + "esbuild-freebsd-arm64": "0.14.54", + "esbuild-linux-32": "0.14.54", + "esbuild-linux-64": "0.14.54", + "esbuild-linux-arm": "0.14.54", + "esbuild-linux-arm64": "0.14.54", + "esbuild-linux-mips64le": "0.14.54", + "esbuild-linux-ppc64le": "0.14.54", + "esbuild-linux-riscv64": "0.14.54", + "esbuild-linux-s390x": "0.14.54", + "esbuild-netbsd-64": "0.14.54", + "esbuild-openbsd-64": "0.14.54", + "esbuild-sunos-64": "0.14.54", + "esbuild-windows-32": "0.14.54", + "esbuild-windows-64": "0.14.54", + "esbuild-windows-arm64": "0.14.54" } }, "esbuild-android-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.49.tgz", - "integrity": "sha512-vYsdOTD+yi+kquhBiFWl3tyxnj2qZJsl4tAqwhT90ktUdnyTizgle7TjNx6Ar1bN7wcwWqZ9QInfdk2WVagSww==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz", + "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==", "dev": true, "optional": true }, "esbuild-android-arm64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.49.tgz", - "integrity": "sha512-g2HGr/hjOXCgSsvQZ1nK4nW/ei8JUx04Li74qub9qWrStlysaVmadRyTVuW32FGIpLQyc5sUjjZopj49eGGM2g==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz", + "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==", "dev": true, "optional": true }, "esbuild-darwin-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.49.tgz", - "integrity": "sha512-3rvqnBCtX9ywso5fCHixt2GBCUsogNp9DjGmvbBohh31Ces34BVzFltMSxJpacNki96+WIcX5s/vum+ckXiLYg==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz", + "integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==", "dev": true, "optional": true }, "esbuild-darwin-arm64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.49.tgz", - "integrity": "sha512-XMaqDxO846srnGlUSJnwbijV29MTKUATmOLyQSfswbK/2X5Uv28M9tTLUJcKKxzoo9lnkYPsx2o8EJcTYwCs/A==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz", + "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==", "dev": true, "optional": true }, "esbuild-freebsd-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.49.tgz", - "integrity": "sha512-NJ5Q6AjV879mOHFri+5lZLTp5XsO2hQ+KSJYLbfY9DgCu8s6/Zl2prWXVANYTeCDLlrIlNNYw8y34xqyLDKOmQ==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz", + "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==", "dev": true, "optional": true }, "esbuild-freebsd-arm64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.49.tgz", - "integrity": "sha512-lFLtgXnAc3eXYqj5koPlBZvEbBSOSUbWO3gyY/0+4lBdRqELyz4bAuamHvmvHW5swJYL7kngzIZw6kdu25KGOA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz", + "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==", "dev": true, "optional": true }, "esbuild-linux-32": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.49.tgz", - "integrity": "sha512-zTTH4gr2Kb8u4QcOpTDVn7Z8q7QEIvFl/+vHrI3cF6XOJS7iEI1FWslTo3uofB2+mn6sIJEQD9PrNZKoAAMDiA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz", + "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==", "dev": true, "optional": true }, "esbuild-linux-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.49.tgz", - "integrity": "sha512-hYmzRIDzFfLrB5c1SknkxzM8LdEUOusp6M2TnuQZJLRtxTgyPnZZVtyMeCLki0wKgYPXkFsAVhi8vzo2mBNeTg==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz", + "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==", "dev": true, "optional": true }, "esbuild-linux-arm": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.49.tgz", - "integrity": "sha512-iE3e+ZVv1Qz1Sy0gifIsarJMQ89Rpm9mtLSRtG3AH0FPgAzQ5Z5oU6vYzhc/3gSPi2UxdCOfRhw2onXuFw/0lg==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz", + "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==", "dev": true, "optional": true }, "esbuild-linux-arm64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.49.tgz", - "integrity": "sha512-KLQ+WpeuY+7bxukxLz5VgkAAVQxUv67Ft4DmHIPIW+2w3ObBPQhqNoeQUHxopoW/aiOn3m99NSmSV+bs4BSsdA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz", + "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==", "dev": true, "optional": true }, "esbuild-linux-mips64le": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.49.tgz", - "integrity": "sha512-n+rGODfm8RSum5pFIqFQVQpYBw+AztL8s6o9kfx7tjfK0yIGF6tm5HlG6aRjodiiKkH2xAiIM+U4xtQVZYU4rA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz", + "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==", "dev": true, "optional": true }, "esbuild-linux-ppc64le": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.49.tgz", - "integrity": "sha512-WP9zR4HX6iCBmMFH+XHHng2LmdoIeUmBpL4aL2TR8ruzXyT4dWrJ5BSbT8iNo6THN8lod6GOmYDLq/dgZLalGw==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz", + "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==", "dev": true, "optional": true }, "esbuild-linux-riscv64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.49.tgz", - "integrity": "sha512-h66ORBz+Dg+1KgLvzTVQEA1LX4XBd1SK0Fgbhhw4akpG/YkN8pS6OzYI/7SGENiN6ao5hETRDSkVcvU9NRtkMQ==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz", + "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==", "dev": true, "optional": true }, "esbuild-linux-s390x": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.49.tgz", - "integrity": "sha512-DhrUoFVWD+XmKO1y7e4kNCqQHPs6twz6VV6Uezl/XHYGzM60rBewBF5jlZjG0nCk5W/Xy6y1xWeopkrhFFM0sQ==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz", + "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==", "dev": true, "optional": true }, "esbuild-netbsd-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.49.tgz", - "integrity": "sha512-BXaUwFOfCy2T+hABtiPUIpWjAeWK9P8O41gR4Pg73hpzoygVGnj0nI3YK4SJhe52ELgtdgWP/ckIkbn2XaTxjQ==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz", + "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==", "dev": true, "optional": true }, "esbuild-openbsd-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.49.tgz", - "integrity": "sha512-lP06UQeLDGmVPw9Rg437Btu6J9/BmyhdoefnQ4gDEJTtJvKtQaUcOQrhjTq455ouZN4EHFH1h28WOJVANK41kA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz", + "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==", "dev": true, "optional": true }, "esbuild-sunos-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.49.tgz", - "integrity": "sha512-4c8Zowp+V3zIWje329BeLbGh6XI9c/rqARNaj5yPHdC61pHI9UNdDxT3rePPJeWcEZVKjkiAS6AP6kiITp7FSw==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz", + "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==", "dev": true, "optional": true }, "esbuild-windows-32": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.49.tgz", - "integrity": "sha512-q7Rb+J9yHTeKr9QTPDYkqfkEj8/kcKz9lOabDuvEXpXuIcosWCJgo5Z7h/L4r7rbtTH4a8U2FGKb6s1eeOHmJA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz", + "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==", "dev": true, "optional": true }, "esbuild-windows-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.49.tgz", - "integrity": "sha512-+Cme7Ongv0UIUTniPqfTX6mJ8Deo7VXw9xN0yJEN1lQMHDppTNmKwAM3oGbD/Vqff+07K2gN0WfNkMohmG+dVw==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz", + "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==", "dev": true, "optional": true }, "esbuild-windows-arm64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.49.tgz", - "integrity": "sha512-v+HYNAXzuANrCbbLFJ5nmO3m5y2PGZWLe3uloAkLt87aXiO2mZr3BTmacZdjwNkNEHuH3bNtN8cak+mzVjVPfA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz", + "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==", "dev": true, "optional": true }, @@ -11232,13 +11445,15 @@ } }, "eslint": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.19.0.tgz", - "integrity": "sha512-SXOPj3x9VKvPe81TjjUJCYlV4oJjQw68Uek+AM0X4p+33dj2HY5bpTZOgnQHcG2eAm1mtCU9uNMnJi7exU/kYw==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.26.0.tgz", + "integrity": "sha512-kzJkpaw1Bfwheq4VXUezFriD1GxszX6dUekM7Z3aC2o4hju+tsR/XyTC3RcoSD7jmy9VkPU3+N6YjVU2e96Oyg==", "dev": true, "requires": { - "@eslint/eslintrc": "^1.3.0", - "@humanwhocodes/config-array": "^0.9.2", + "@eslint/eslintrc": "^1.3.3", + "@humanwhocodes/config-array": "^0.11.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -11248,18 +11463,21 @@ "eslint-scope": "^7.1.1", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.2", + "espree": "^9.4.0", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", "globals": "^13.15.0", + "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", @@ -11270,8 +11488,7 @@ "regexpp": "^3.2.0", "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" + "text-table": "^0.2.0" }, "dependencies": { "ansi-styles": { @@ -11314,21 +11531,22 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, "globals": { "version": "13.16.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.16.0.tgz", @@ -11357,6 +11575,15 @@ "type-check": "~0.4.0" } }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -11371,6 +11598,24 @@ "word-wrap": "^1.2.3" } }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -11428,12 +11673,12 @@ "dev": true }, "espree": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", - "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", + "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==", "dev": true, "requires": { - "acorn": "^8.7.1", + "acorn": "^8.8.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.3.0" } @@ -11731,6 +11976,15 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, "fb-watchman": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", @@ -11795,9 +12049,9 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, "follow-redirects": { - "version": "1.14.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", - "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==" + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" }, "for-in": { "version": "1.0.2", @@ -11843,12 +12097,6 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", - "dev": true - }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -11917,6 +12165,12 @@ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "dev": true }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, "growly": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", @@ -12210,6 +12464,12 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -12315,17 +12575,6 @@ "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", "source-map": "^0.6.1" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - } } }, "istanbul-reports": { @@ -13292,6 +13541,12 @@ "supports-color": "^7.0.0" } }, + "js-sdsl": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", + "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==", + "dev": true + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -13352,15 +13607,6 @@ "debug": "4" } }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, "form-data": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", @@ -13591,13 +13837,13 @@ "dev": true }, "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "dev": true, "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" + "braces": "^3.0.2", + "picomatch": "^2.3.1" } }, "mime-db": { @@ -13708,86 +13954,13 @@ "dev": true }, "node-html-parser": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-5.3.3.tgz", - "integrity": "sha512-ncg1033CaX9UexbyA7e1N0aAoAYRDiV8jkTvzEnfd1GDvzFdrsXLzR4p4ik8mwLgnaKP/jyUFWDy9q3jvRT2Jw==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.1.tgz", + "integrity": "sha512-eYYblUeoMg0nR6cYGM4GRb1XncNa9FXEftuKAU1qyMIr6rXVtNyUKduvzZtkqFqSHVByq2lLjC7WO8tz7VDmnA==", "dev": true, "requires": { - "css-select": "^4.2.1", + "css-select": "^5.1.0", "he": "1.2.0" - }, - "dependencies": { - "css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", - "dev": true, - "requires": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - } - }, - "css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true - }, - "dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - } - }, - "domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true - }, - "domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "dev": true, - "requires": { - "domelementtype": "^2.2.0" - } - }, - "domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "requires": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - } - }, - "entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true - }, - "nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "requires": { - "boolbase": "^1.0.0" - } - } } }, "node-int64": { @@ -13856,6 +14029,15 @@ "path-key": "^3.0.0" } }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "requires": { + "boolbase": "^1.0.0" + } + }, "nwsapi": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.1.tgz", @@ -14093,9 +14275,9 @@ "dev": true }, "picomatch": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.1.tgz", - "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, "pirates": { @@ -14178,6 +14360,11 @@ "sisteransi": "^1.0.4" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -14200,6 +14387,12 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -14343,6 +14536,12 @@ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "dev": true }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -14358,6 +14557,15 @@ "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", "dev": true }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -14636,7 +14844,7 @@ "set-immediate-shim": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + "integrity": "sha512-Li5AOqrZWCVA2n5kryzEmqai6bKSIvpz5oUJHPVj6+dsbD3X1ixtsY5tEnsaNpH3pFAHmG8eIHUrtEtohrg+UQ==" }, "set-value": { "version": "2.0.1", @@ -15386,9 +15594,9 @@ } }, "typescript": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", "dev": true }, "union-value": { @@ -15468,9 +15676,9 @@ } }, "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "requires": { "punycode": "^2.1.0" @@ -15500,12 +15708,6 @@ "dev": true, "optional": true }, - "v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, "v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -15631,10 +15833,11 @@ "dev": true }, "winston": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.1.tgz", - "integrity": "sha512-r+6YAiCR4uI3N8eQNOg8k3P3PqwAm20cLKlzVD9E66Ch39+LZC+VH1UKf9JemQj2B3QoUHfKD7Poewn0Pr3Y1w==", + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz", + "integrity": "sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew==", "requires": { + "@colors/colors": "1.5.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", @@ -15743,9 +15946,9 @@ } }, "ws": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", - "integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.9.0.tgz", + "integrity": "sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==", "requires": {} }, "xml": { @@ -15822,6 +16025,12 @@ "dev": true, "optional": true, "peer": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true } } } diff --git a/vscode4teaching-extension/package.json b/vscode4teaching-extension/package.json index e693bd2f..9261b423 100644 --- a/vscode4teaching-extension/package.json +++ b/vscode4teaching-extension/package.json @@ -21,9 +21,9 @@ }, "displayName": "VS Code 4 Teaching", "description": "Bring the programming exercises directly to the student’s editor.", - "version": "2.1.4", + "version": "2.2.0", "engines": { - "vscode": "^1.61.0" + "vscode": "^1.64.0" }, "categories": [ "Other" @@ -404,27 +404,27 @@ "@types/mkdirp": "^1.0.2", "@types/node": "^10.17.60", "@types/rimraf": "^3.0.2", - "@types/vscode": "^1.45.1", + "@types/vscode": "1.64.0", "@types/ws": "^8.5.3", - "esbuild": "^0.14.49", - "eslint": "^8.19.0", + "esbuild": "^0.14.54", + "eslint": "^8.26.0", "jest": "^26.6.3", "jest-sonar-reporter": "^2.0.0", - "node-html-parser": "^5.3.3", + "node-html-parser": "^6.1.1", "rimraf": "^3.0.2", "ts-jest": "^26.5.6", - "typescript": "^4.7.4" + "typescript": "^4.8.4" }, "dependencies": { - "axios": "^0.27.2", + "axios": "^1.1.3", "form-data": "^4.0.0", "ignore": "^5.1.6", - "jszip": "^3.4.0", + "jszip": "~3.7.0", "lodash.escaperegexp": "^4.1.2", "mkdirp": "^1.0.4", "vsls": "^1.0.4753", - "winston": "^3.8.1", - "ws": "^8.8.0" + "winston": "^3.8.2", + "ws": "^8.9.0" }, "jestSonar": { "reportPath": "coverage", diff --git a/vscode4teaching-extension/resources/dashboard/chart.js b/vscode4teaching-extension/resources/dashboard/chart.js new file mode 100644 index 00000000..8f69759e --- /dev/null +++ b/vscode4teaching-extension/resources/dashboard/chart.js @@ -0,0 +1,13 @@ +/*! + * Chart.js v3.9.1 + * https://www.chartjs.org + * (c) 2022 Chart.js Contributors + * Released under the MIT License + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";function t(){}const e=function(){let t=0;return function(){return t++}}();function i(t){return null==t}function s(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.slice(0,7)&&"Array]"===e.slice(-6)}function n(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}const o=t=>("number"==typeof t||t instanceof Number)&&isFinite(+t);function a(t,e){return o(t)?t:e}function r(t,e){return void 0===t?e:t}const l=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:t/e,h=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function c(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function d(t,e,i,o){let a,r,l;if(s(t))if(r=t.length,o)for(a=r-1;a>=0;a--)e.call(i,t[a],a);else for(a=0;at,x:t=>t.x,y:t=>t.y};function y(t,e){const i=_[e]||(_[e]=function(t){const e=v(t);return t=>{for(const i of e){if(""===i)break;t=t&&t[i]}return t}}(e));return i(t)}function v(t){const e=t.split("."),i=[];let s="";for(const t of e)s+=t,s.endsWith("\\")?s=s.slice(0,-1)+".":(i.push(s),s="");return i}function w(t){return t.charAt(0).toUpperCase()+t.slice(1)}const M=t=>void 0!==t,k=t=>"function"==typeof t,S=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0};function P(t){return"mouseup"===t.type||"click"===t.type||"contextmenu"===t.type}const D=Math.PI,O=2*D,C=O+D,A=Number.POSITIVE_INFINITY,T=D/180,L=D/2,E=D/4,R=2*D/3,I=Math.log10,z=Math.sign;function F(t){const e=Math.round(t);t=N(t,e,t/1e3)?e:t;const i=Math.pow(10,Math.floor(I(t))),s=t/i;return(s<=1?1:s<=2?2:s<=5?5:10)*i}function V(t){const e=[],i=Math.sqrt(t);let s;for(s=1;st-e)).pop(),e}function B(t){return!isNaN(parseFloat(t))&&isFinite(t)}function N(t,e,i){return Math.abs(t-e)=t}function j(t,e,i){let s,n,o;for(s=0,n=t.length;sl&&h=Math.min(e,i)-s&&t<=Math.max(e,i)+s}function tt(t,e,i){i=i||(i=>t[i]1;)s=o+n>>1,i(s)?o=s:n=s;return{lo:o,hi:n}}const et=(t,e,i,s)=>tt(t,i,s?s=>t[s][e]<=i:s=>t[s][e]tt(t,i,(s=>t[s][e]>=i));function st(t,e,i){let s=0,n=t.length;for(;ss&&t[n-1]>i;)n--;return s>0||n{const i="_onData"+w(e),s=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...e){const n=s.apply(this,e);return t._chartjs.listeners.forEach((t=>{"function"==typeof t[i]&&t[i](...e)})),n}})})))}function at(t,e){const i=t._chartjs;if(!i)return;const s=i.listeners,n=s.indexOf(e);-1!==n&&s.splice(n,1),s.length>0||(nt.forEach((e=>{delete t[e]})),delete t._chartjs)}function rt(t){const e=new Set;let i,s;for(i=0,s=t.length;iArray.prototype.slice.call(t));let n=!1,o=[];return function(...i){o=s(i),n||(n=!0,lt.call(window,(()=>{n=!1,t.apply(e,o)})))}}function ct(t,e){let i;return function(...s){return e?(clearTimeout(i),i=setTimeout(t,e,s)):t.apply(this,s),e}}const dt=t=>"start"===t?"left":"end"===t?"right":"center",ut=(t,e,i)=>"start"===t?e:"end"===t?i:(e+i)/2,ft=(t,e,i,s)=>t===(s?"left":"right")?i:"center"===t?(e+i)/2:e;function gt(t,e,i){const s=e.length;let n=0,o=s;if(t._sorted){const{iScale:a,_parsed:r}=t,l=a.axis,{min:h,max:c,minDefined:d,maxDefined:u}=a.getUserBounds();d&&(n=Z(Math.min(et(r,a.axis,h).lo,i?s:et(e,l,a.getPixelForValue(h)).lo),0,s-1)),o=u?Z(Math.max(et(r,a.axis,c,!0).hi+1,i?0:et(e,l,a.getPixelForValue(c),!0).hi+1),n,s)-n:s-n}return{start:n,count:o}}function pt(t){const{xScale:e,yScale:i,_scaleRanges:s}=t,n={xmin:e.min,xmax:e.max,ymin:i.min,ymax:i.max};if(!s)return t._scaleRanges=n,!0;const o=s.xmin!==e.min||s.xmax!==e.max||s.ymin!==i.min||s.ymax!==i.max;return Object.assign(s,n),o}var mt=new class{constructor(){this._request=null,this._charts=new Map,this._running=!1,this._lastDate=void 0}_notify(t,e,i,s){const n=e.listeners[s],o=e.duration;n.forEach((s=>s({chart:t,initial:e.initial,numSteps:o,currentStep:Math.min(i-e.start,o)})))}_refresh(){this._request||(this._running=!0,this._request=lt.call(window,(()=>{this._update(),this._request=null,this._running&&this._refresh()})))}_update(t=Date.now()){let e=0;this._charts.forEach(((i,s)=>{if(!i.running||!i.items.length)return;const n=i.items;let o,a=n.length-1,r=!1;for(;a>=0;--a)o=n[a],o._active?(o._total>i.duration&&(i.duration=o._total),o.tick(t),r=!0):(n[a]=n[n.length-1],n.pop());r&&(s.draw(),this._notify(s,i,t,"progress")),n.length||(i.running=!1,this._notify(s,i,t,"complete"),i.initial=!1),e+=n.length})),this._lastDate=t,0===e&&(this._running=!1)}_getAnims(t){const e=this._charts;let i=e.get(t);return i||(i={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},e.set(t,i)),i}listen(t,e,i){this._getAnims(t).listeners[e].push(i)}add(t,e){e&&e.length&&this._getAnims(t).items.push(...e)}has(t){return this._getAnims(t).items.length>0}start(t){const e=this._charts.get(t);e&&(e.running=!0,e.start=Date.now(),e.duration=e.items.reduce(((t,e)=>Math.max(t,e._duration)),0),this._refresh())}running(t){if(!this._running)return!1;const e=this._charts.get(t);return!!(e&&e.running&&e.items.length)}stop(t){const e=this._charts.get(t);if(!e||!e.items.length)return;const i=e.items;let s=i.length-1;for(;s>=0;--s)i[s].cancel();e.items=[],this._notify(t,e,Date.now(),"complete")}remove(t){return this._charts.delete(t)}}; +/*! + * @kurkle/color v0.2.1 + * https://github.com/kurkle/color#readme + * (c) 2022 Jukka Kurkela + * Released under the MIT License + */function bt(t){return t+.5|0}const xt=(t,e,i)=>Math.max(Math.min(t,i),e);function _t(t){return xt(bt(2.55*t),0,255)}function yt(t){return xt(bt(255*t),0,255)}function vt(t){return xt(bt(t/2.55)/100,0,1)}function wt(t){return xt(bt(100*t),0,100)}const Mt={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},kt=[..."0123456789ABCDEF"],St=t=>kt[15&t],Pt=t=>kt[(240&t)>>4]+kt[15&t],Dt=t=>(240&t)>>4==(15&t);function Ot(t){var e=(t=>Dt(t.r)&&Dt(t.g)&&Dt(t.b)&&Dt(t.a))(t)?St:Pt;return t?"#"+e(t.r)+e(t.g)+e(t.b)+((t,e)=>t<255?e(t):"")(t.a,e):void 0}const Ct=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function At(t,e,i){const s=e*Math.min(i,1-i),n=(e,n=(e+t/30)%12)=>i-s*Math.max(Math.min(n-3,9-n,1),-1);return[n(0),n(8),n(4)]}function Tt(t,e,i){const s=(s,n=(s+t/60)%6)=>i-i*e*Math.max(Math.min(n,4-n,1),0);return[s(5),s(3),s(1)]}function Lt(t,e,i){const s=At(t,1,.5);let n;for(e+i>1&&(n=1/(e+i),e*=n,i*=n),n=0;n<3;n++)s[n]*=1-e-i,s[n]+=e;return s}function Et(t){const e=t.r/255,i=t.g/255,s=t.b/255,n=Math.max(e,i,s),o=Math.min(e,i,s),a=(n+o)/2;let r,l,h;return n!==o&&(h=n-o,l=a>.5?h/(2-n-o):h/(n+o),r=function(t,e,i,s,n){return t===n?(e-i)/s+(e>16&255,o>>8&255,255&o]}return t}(),Nt.transparent=[0,0,0,0]);const e=Nt[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}const jt=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;const Ht=t=>t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055,$t=t=>t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4);function Yt(t,e,i){if(t){let s=Et(t);s[e]=Math.max(0,Math.min(s[e]+s[e]*i,0===e?360:1)),s=It(s),t.r=s[0],t.g=s[1],t.b=s[2]}}function Ut(t,e){return t?Object.assign(e||{},t):t}function Xt(t){var e={r:0,g:0,b:0,a:255};return Array.isArray(t)?t.length>=3&&(e={r:t[0],g:t[1],b:t[2],a:255},t.length>3&&(e.a=yt(t[3]))):(e=Ut(t,{r:0,g:0,b:0,a:1})).a=yt(e.a),e}function qt(t){return"r"===t.charAt(0)?function(t){const e=jt.exec(t);let i,s,n,o=255;if(e){if(e[7]!==i){const t=+e[7];o=e[8]?_t(t):xt(255*t,0,255)}return i=+e[1],s=+e[3],n=+e[5],i=255&(e[2]?_t(i):xt(i,0,255)),s=255&(e[4]?_t(s):xt(s,0,255)),n=255&(e[6]?_t(n):xt(n,0,255)),{r:i,g:s,b:n,a:o}}}(t):Ft(t)}class Kt{constructor(t){if(t instanceof Kt)return t;const e=typeof t;let i;var s,n,o;"object"===e?i=Xt(t):"string"===e&&(o=(s=t).length,"#"===s[0]&&(4===o||5===o?n={r:255&17*Mt[s[1]],g:255&17*Mt[s[2]],b:255&17*Mt[s[3]],a:5===o?17*Mt[s[4]]:255}:7!==o&&9!==o||(n={r:Mt[s[1]]<<4|Mt[s[2]],g:Mt[s[3]]<<4|Mt[s[4]],b:Mt[s[5]]<<4|Mt[s[6]],a:9===o?Mt[s[7]]<<4|Mt[s[8]]:255})),i=n||Wt(t)||qt(t)),this._rgb=i,this._valid=!!i}get valid(){return this._valid}get rgb(){var t=Ut(this._rgb);return t&&(t.a=vt(t.a)),t}set rgb(t){this._rgb=Xt(t)}rgbString(){return this._valid?(t=this._rgb)&&(t.a<255?`rgba(${t.r}, ${t.g}, ${t.b}, ${vt(t.a)})`:`rgb(${t.r}, ${t.g}, ${t.b})`):void 0;var t}hexString(){return this._valid?Ot(this._rgb):void 0}hslString(){return this._valid?function(t){if(!t)return;const e=Et(t),i=e[0],s=wt(e[1]),n=wt(e[2]);return t.a<255?`hsla(${i}, ${s}%, ${n}%, ${vt(t.a)})`:`hsl(${i}, ${s}%, ${n}%)`}(this._rgb):void 0}mix(t,e){if(t){const i=this.rgb,s=t.rgb;let n;const o=e===n?.5:e,a=2*o-1,r=i.a-s.a,l=((a*r==-1?a:(a+r)/(1+a*r))+1)/2;n=1-l,i.r=255&l*i.r+n*s.r+.5,i.g=255&l*i.g+n*s.g+.5,i.b=255&l*i.b+n*s.b+.5,i.a=o*i.a+(1-o)*s.a,this.rgb=i}return this}interpolate(t,e){return t&&(this._rgb=function(t,e,i){const s=$t(vt(t.r)),n=$t(vt(t.g)),o=$t(vt(t.b));return{r:yt(Ht(s+i*($t(vt(e.r))-s))),g:yt(Ht(n+i*($t(vt(e.g))-n))),b:yt(Ht(o+i*($t(vt(e.b))-o))),a:t.a+i*(e.a-t.a)}}(this._rgb,t._rgb,e)),this}clone(){return new Kt(this.rgb)}alpha(t){return this._rgb.a=yt(t),this}clearer(t){return this._rgb.a*=1-t,this}greyscale(){const t=this._rgb,e=bt(.3*t.r+.59*t.g+.11*t.b);return t.r=t.g=t.b=e,this}opaquer(t){return this._rgb.a*=1+t,this}negate(){const t=this._rgb;return t.r=255-t.r,t.g=255-t.g,t.b=255-t.b,this}lighten(t){return Yt(this._rgb,2,t),this}darken(t){return Yt(this._rgb,2,-t),this}saturate(t){return Yt(this._rgb,1,t),this}desaturate(t){return Yt(this._rgb,1,-t),this}rotate(t){return function(t,e){var i=Et(t);i[0]=zt(i[0]+e),i=It(i),t.r=i[0],t.g=i[1],t.b=i[2]}(this._rgb,t),this}}function Gt(t){return new Kt(t)}function Zt(t){if(t&&"object"==typeof t){const e=t.toString();return"[object CanvasPattern]"===e||"[object CanvasGradient]"===e}return!1}function Jt(t){return Zt(t)?t:Gt(t)}function Qt(t){return Zt(t)?t:Gt(t).saturate(.5).darken(.1).hexString()}const te=Object.create(null),ee=Object.create(null);function ie(t,e){if(!e)return t;const i=e.split(".");for(let e=0,s=i.length;et.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(t,e)=>Qt(e.backgroundColor),this.hoverBorderColor=(t,e)=>Qt(e.borderColor),this.hoverColor=(t,e)=>Qt(e.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(t)}set(t,e){return se(this,t,e)}get(t){return ie(this,t)}describe(t,e){return se(ee,t,e)}override(t,e){return se(te,t,e)}route(t,e,i,s){const o=ie(this,t),a=ie(this,i),l="_"+e;Object.defineProperties(o,{[l]:{value:o[e],writable:!0},[e]:{enumerable:!0,get(){const t=this[l],e=a[s];return n(t)?Object.assign({},e,t):r(t,e)},set(t){this[l]=t}}})}}({_scriptable:t=>!t.startsWith("on"),_indexable:t=>"events"!==t,hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}});function oe(){return"undefined"!=typeof window&&"undefined"!=typeof document}function ae(t){let e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e}function re(t,e,i){let s;return"string"==typeof t?(s=parseInt(t,10),-1!==t.indexOf("%")&&(s=s/100*e.parentNode[i])):s=t,s}const le=t=>window.getComputedStyle(t,null);function he(t,e){return le(t).getPropertyValue(e)}const ce=["top","right","bottom","left"];function de(t,e,i){const s={};i=i?"-"+i:"";for(let n=0;n<4;n++){const o=ce[n];s[o]=parseFloat(t[e+"-"+o+i])||0}return s.width=s.left+s.right,s.height=s.top+s.bottom,s}function ue(t,e){if("native"in t)return t;const{canvas:i,currentDevicePixelRatio:s}=e,n=le(i),o="border-box"===n.boxSizing,a=de(n,"padding"),r=de(n,"border","width"),{x:l,y:h,box:c}=function(t,e){const i=t.touches,s=i&&i.length?i[0]:t,{offsetX:n,offsetY:o}=s;let a,r,l=!1;if(((t,e,i)=>(t>0||e>0)&&(!i||!i.shadowRoot))(n,o,t.target))a=n,r=o;else{const t=e.getBoundingClientRect();a=s.clientX-t.left,r=s.clientY-t.top,l=!0}return{x:a,y:r,box:l}}(t,i),d=a.left+(c&&r.left),u=a.top+(c&&r.top);let{width:f,height:g}=e;return o&&(f-=a.width+r.width,g-=a.height+r.height),{x:Math.round((l-d)/f*i.width/s),y:Math.round((h-u)/g*i.height/s)}}const fe=t=>Math.round(10*t)/10;function ge(t,e,i,s){const n=le(t),o=de(n,"margin"),a=re(n.maxWidth,t,"clientWidth")||A,r=re(n.maxHeight,t,"clientHeight")||A,l=function(t,e,i){let s,n;if(void 0===e||void 0===i){const o=ae(t);if(o){const t=o.getBoundingClientRect(),a=le(o),r=de(a,"border","width"),l=de(a,"padding");e=t.width-l.width-r.width,i=t.height-l.height-r.height,s=re(a.maxWidth,o,"clientWidth"),n=re(a.maxHeight,o,"clientHeight")}else e=t.clientWidth,i=t.clientHeight}return{width:e,height:i,maxWidth:s||A,maxHeight:n||A}}(t,e,i);let{width:h,height:c}=l;if("content-box"===n.boxSizing){const t=de(n,"border","width"),e=de(n,"padding");h-=e.width+t.width,c-=e.height+t.height}return h=Math.max(0,h-o.width),c=Math.max(0,s?Math.floor(h/s):c-o.height),h=fe(Math.min(h,a,l.maxWidth)),c=fe(Math.min(c,r,l.maxHeight)),h&&!c&&(c=fe(h/2)),{width:h,height:c}}function pe(t,e,i){const s=e||1,n=Math.floor(t.height*s),o=Math.floor(t.width*s);t.height=n/s,t.width=o/s;const a=t.canvas;return a.style&&(i||!a.style.height&&!a.style.width)&&(a.style.height=`${t.height}px`,a.style.width=`${t.width}px`),(t.currentDevicePixelRatio!==s||a.height!==n||a.width!==o)&&(t.currentDevicePixelRatio=s,a.height=n,a.width=o,t.ctx.setTransform(s,0,0,s,0,0),!0)}const me=function(){let t=!1;try{const e={get passive(){return t=!0,!1}};window.addEventListener("test",null,e),window.removeEventListener("test",null,e)}catch(t){}return t}();function be(t,e){const i=he(t,e),s=i&&i.match(/^(\d+)(\.\d+)?px$/);return s?+s[1]:void 0}function xe(t){return!t||i(t.size)||i(t.family)?null:(t.style?t.style+" ":"")+(t.weight?t.weight+" ":"")+t.size+"px "+t.family}function _e(t,e,i,s,n){let o=e[n];return o||(o=e[n]=t.measureText(n).width,i.push(n)),o>s&&(s=o),s}function ye(t,e,i,n){let o=(n=n||{}).data=n.data||{},a=n.garbageCollect=n.garbageCollect||[];n.font!==e&&(o=n.data={},a=n.garbageCollect=[],n.font=e),t.save(),t.font=e;let r=0;const l=i.length;let h,c,d,u,f;for(h=0;hi.length){for(h=0;h0&&t.stroke()}}function Se(t,e,i){return i=i||.5,!e||t&&t.x>e.left-i&&t.xe.top-i&&t.y0&&""!==r.strokeColor;let c,d;for(t.save(),t.font=a.string,function(t,e){e.translation&&t.translate(e.translation[0],e.translation[1]);i(e.rotation)||t.rotate(e.rotation);e.color&&(t.fillStyle=e.color);e.textAlign&&(t.textAlign=e.textAlign);e.textBaseline&&(t.textBaseline=e.textBaseline)}(t,r),c=0;ct[0])){M(s)||(s=$e("_fallback",t));const o={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:t,_rootScopes:i,_fallback:s,_getTarget:n,override:n=>Ee([n,...t],e,i,s)};return new Proxy(o,{deleteProperty:(e,i)=>(delete e[i],delete e._keys,delete t[0][i],!0),get:(i,s)=>Ve(i,s,(()=>function(t,e,i,s){let n;for(const o of e)if(n=$e(ze(o,t),i),M(n))return Fe(t,n)?je(i,s,t,n):n}(s,e,t,i))),getOwnPropertyDescriptor:(t,e)=>Reflect.getOwnPropertyDescriptor(t._scopes[0],e),getPrototypeOf:()=>Reflect.getPrototypeOf(t[0]),has:(t,e)=>Ye(t).includes(e),ownKeys:t=>Ye(t),set(t,e,i){const s=t._storage||(t._storage=n());return t[e]=s[e]=i,delete t._keys,!0}})}function Re(t,e,i,o){const a={_cacheable:!1,_proxy:t,_context:e,_subProxy:i,_stack:new Set,_descriptors:Ie(t,o),setContext:e=>Re(t,e,i,o),override:s=>Re(t.override(s),e,i,o)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete t[i],!0),get:(t,e,i)=>Ve(t,e,(()=>function(t,e,i){const{_proxy:o,_context:a,_subProxy:r,_descriptors:l}=t;let h=o[e];k(h)&&l.isScriptable(e)&&(h=function(t,e,i,s){const{_proxy:n,_context:o,_subProxy:a,_stack:r}=i;if(r.has(t))throw new Error("Recursion detected: "+Array.from(r).join("->")+"->"+t);r.add(t),e=e(o,a||s),r.delete(t),Fe(t,e)&&(e=je(n._scopes,n,t,e));return e}(e,h,t,i));s(h)&&h.length&&(h=function(t,e,i,s){const{_proxy:o,_context:a,_subProxy:r,_descriptors:l}=i;if(M(a.index)&&s(t))e=e[a.index%e.length];else if(n(e[0])){const i=e,s=o._scopes.filter((t=>t!==i));e=[];for(const n of i){const i=je(s,o,t,n);e.push(Re(i,a,r&&r[t],l))}}return e}(e,h,t,l.isIndexable));Fe(e,h)&&(h=Re(h,a,r&&r[e],l));return h}(t,e,i))),getOwnPropertyDescriptor:(e,i)=>e._descriptors.allKeys?Reflect.has(t,i)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(t,i),getPrototypeOf:()=>Reflect.getPrototypeOf(t),has:(e,i)=>Reflect.has(t,i),ownKeys:()=>Reflect.ownKeys(t),set:(e,i,s)=>(t[i]=s,delete e[i],!0)})}function Ie(t,e={scriptable:!0,indexable:!0}){const{_scriptable:i=e.scriptable,_indexable:s=e.indexable,_allKeys:n=e.allKeys}=t;return{allKeys:n,scriptable:i,indexable:s,isScriptable:k(i)?i:()=>i,isIndexable:k(s)?s:()=>s}}const ze=(t,e)=>t?t+w(e):e,Fe=(t,e)=>n(e)&&"adapters"!==t&&(null===Object.getPrototypeOf(e)||e.constructor===Object);function Ve(t,e,i){if(Object.prototype.hasOwnProperty.call(t,e))return t[e];const s=i();return t[e]=s,s}function Be(t,e,i){return k(t)?t(e,i):t}const Ne=(t,e)=>!0===t?e:"string"==typeof t?y(e,t):void 0;function We(t,e,i,s,n){for(const o of e){const e=Ne(i,o);if(e){t.add(e);const o=Be(e._fallback,i,n);if(M(o)&&o!==i&&o!==s)return o}else if(!1===e&&M(s)&&i!==s)return null}return!1}function je(t,e,i,o){const a=e._rootScopes,r=Be(e._fallback,i,o),l=[...t,...a],h=new Set;h.add(o);let c=He(h,l,i,r||i,o);return null!==c&&((!M(r)||r===i||(c=He(h,l,r,c,o),null!==c))&&Ee(Array.from(h),[""],a,r,(()=>function(t,e,i){const o=t._getTarget();e in o||(o[e]={});const a=o[e];if(s(a)&&n(i))return i;return a}(e,i,o))))}function He(t,e,i,s,n){for(;i;)i=We(t,e,i,s,n);return i}function $e(t,e){for(const i of e){if(!i)continue;const e=i[t];if(M(e))return e}}function Ye(t){let e=t._keys;return e||(e=t._keys=function(t){const e=new Set;for(const i of t)for(const t of Object.keys(i).filter((t=>!t.startsWith("_"))))e.add(t);return Array.from(e)}(t._scopes)),e}function Ue(t,e,i,s){const{iScale:n}=t,{key:o="r"}=this._parsing,a=new Array(s);let r,l,h,c;for(r=0,l=s;re"x"===t?"y":"x";function Ge(t,e,i,s){const n=t.skip?e:t,o=e,a=i.skip?e:i,r=X(o,n),l=X(a,o);let h=r/(r+l),c=l/(r+l);h=isNaN(h)?0:h,c=isNaN(c)?0:c;const d=s*h,u=s*c;return{previous:{x:o.x-d*(a.x-n.x),y:o.y-d*(a.y-n.y)},next:{x:o.x+u*(a.x-n.x),y:o.y+u*(a.y-n.y)}}}function Ze(t,e="x"){const i=Ke(e),s=t.length,n=Array(s).fill(0),o=Array(s);let a,r,l,h=qe(t,0);for(a=0;a!t.skip))),"monotone"===e.cubicInterpolationMode)Ze(t,n);else{let i=s?t[t.length-1]:t[0];for(o=0,a=t.length;o0===t||1===t,ei=(t,e,i)=>-Math.pow(2,10*(t-=1))*Math.sin((t-e)*O/i),ii=(t,e,i)=>Math.pow(2,-10*t)*Math.sin((t-e)*O/i)+1,si={linear:t=>t,easeInQuad:t=>t*t,easeOutQuad:t=>-t*(t-2),easeInOutQuad:t=>(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1),easeInCubic:t=>t*t*t,easeOutCubic:t=>(t-=1)*t*t+1,easeInOutCubic:t=>(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2),easeInQuart:t=>t*t*t*t,easeOutQuart:t=>-((t-=1)*t*t*t-1),easeInOutQuart:t=>(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2),easeInQuint:t=>t*t*t*t*t,easeOutQuint:t=>(t-=1)*t*t*t*t+1,easeInOutQuint:t=>(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2),easeInSine:t=>1-Math.cos(t*L),easeOutSine:t=>Math.sin(t*L),easeInOutSine:t=>-.5*(Math.cos(D*t)-1),easeInExpo:t=>0===t?0:Math.pow(2,10*(t-1)),easeOutExpo:t=>1===t?1:1-Math.pow(2,-10*t),easeInOutExpo:t=>ti(t)?t:t<.5?.5*Math.pow(2,10*(2*t-1)):.5*(2-Math.pow(2,-10*(2*t-1))),easeInCirc:t=>t>=1?t:-(Math.sqrt(1-t*t)-1),easeOutCirc:t=>Math.sqrt(1-(t-=1)*t),easeInOutCirc:t=>(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1),easeInElastic:t=>ti(t)?t:ei(t,.075,.3),easeOutElastic:t=>ti(t)?t:ii(t,.075,.3),easeInOutElastic(t){const e=.1125;return ti(t)?t:t<.5?.5*ei(2*t,e,.45):.5+.5*ii(2*t-1,e,.45)},easeInBack(t){const e=1.70158;return t*t*((e+1)*t-e)},easeOutBack(t){const e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack(t){let e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:t=>1-si.easeOutBounce(1-t),easeOutBounce(t){const e=7.5625,i=2.75;return t<1/i?e*t*t:t<2/i?e*(t-=1.5/i)*t+.75:t<2.5/i?e*(t-=2.25/i)*t+.9375:e*(t-=2.625/i)*t+.984375},easeInOutBounce:t=>t<.5?.5*si.easeInBounce(2*t):.5*si.easeOutBounce(2*t-1)+.5};function ni(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:t.y+i*(e.y-t.y)}}function oi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:"middle"===s?i<.5?t.y:e.y:"after"===s?i<1?t.y:e.y:i>0?e.y:t.y}}function ai(t,e,i,s){const n={x:t.cp2x,y:t.cp2y},o={x:e.cp1x,y:e.cp1y},a=ni(t,n,i),r=ni(n,o,i),l=ni(o,e,i),h=ni(a,r,i),c=ni(r,l,i);return ni(h,c,i)}const ri=new Map;function li(t,e,i){return function(t,e){e=e||{};const i=t+JSON.stringify(e);let s=ri.get(i);return s||(s=new Intl.NumberFormat(t,e),ri.set(i,s)),s}(e,i).format(t)}const hi=new RegExp(/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/),ci=new RegExp(/^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/);function di(t,e){const i=(""+t).match(hi);if(!i||"normal"===i[1])return 1.2*e;switch(t=+i[2],i[3]){case"px":return t;case"%":t/=100}return e*t}function ui(t,e){const i={},s=n(e),o=s?Object.keys(e):e,a=n(t)?s?i=>r(t[i],t[e[i]]):e=>t[e]:()=>t;for(const t of o)i[t]=+a(t)||0;return i}function fi(t){return ui(t,{top:"y",right:"x",bottom:"y",left:"x"})}function gi(t){return ui(t,["topLeft","topRight","bottomLeft","bottomRight"])}function pi(t){const e=fi(t);return e.width=e.left+e.right,e.height=e.top+e.bottom,e}function mi(t,e){t=t||{},e=e||ne.font;let i=r(t.size,e.size);"string"==typeof i&&(i=parseInt(i,10));let s=r(t.style,e.style);s&&!(""+s).match(ci)&&(console.warn('Invalid font style specified: "'+s+'"'),s="");const n={family:r(t.family,e.family),lineHeight:di(r(t.lineHeight,e.lineHeight),i),size:i,style:s,weight:r(t.weight,e.weight),string:""};return n.string=xe(n),n}function bi(t,e,i,n){let o,a,r,l=!0;for(o=0,a=t.length;oi&&0===t?0:t+e;return{min:a(s,-Math.abs(o)),max:a(n,o)}}function _i(t,e){return Object.assign(Object.create(t),e)}function yi(t,e,i){return t?function(t,e){return{x:i=>t+t+e-i,setWidth(t){e=t},textAlign:t=>"center"===t?t:"right"===t?"left":"right",xPlus:(t,e)=>t-e,leftForLtr:(t,e)=>t-e}}(e,i):{x:t=>t,setWidth(t){},textAlign:t=>t,xPlus:(t,e)=>t+e,leftForLtr:(t,e)=>t}}function vi(t,e){let i,s;"ltr"!==e&&"rtl"!==e||(i=t.canvas.style,s=[i.getPropertyValue("direction"),i.getPropertyPriority("direction")],i.setProperty("direction",e,"important"),t.prevTextDirection=s)}function wi(t,e){void 0!==e&&(delete t.prevTextDirection,t.canvas.style.setProperty("direction",e[0],e[1]))}function Mi(t){return"angle"===t?{between:G,compare:q,normalize:K}:{between:Q,compare:(t,e)=>t-e,normalize:t=>t}}function ki({start:t,end:e,count:i,loop:s,style:n}){return{start:t%i,end:e%i,loop:s&&(e-t+1)%i==0,style:n}}function Si(t,e,i){if(!i)return[t];const{property:s,start:n,end:o}=i,a=e.length,{compare:r,between:l,normalize:h}=Mi(s),{start:c,end:d,loop:u,style:f}=function(t,e,i){const{property:s,start:n,end:o}=i,{between:a,normalize:r}=Mi(s),l=e.length;let h,c,{start:d,end:u,loop:f}=t;if(f){for(d+=l,u+=l,h=0,c=l;hx||l(n,b,p)&&0!==r(n,b),v=()=>!x||0===r(o,p)||l(o,b,p);for(let t=c,i=c;t<=d;++t)m=e[t%a],m.skip||(p=h(m[s]),p!==b&&(x=l(p,n,o),null===_&&y()&&(_=0===r(p,n)?t:i),null!==_&&v()&&(g.push(ki({start:_,end:t,loop:u,count:a,style:f})),_=null),i=t,b=p));return null!==_&&g.push(ki({start:_,end:d,loop:u,count:a,style:f})),g}function Pi(t,e){const i=[],s=t.segments;for(let n=0;nn&&t[o%e].skip;)o--;return o%=e,{start:n,end:o}}(i,n,o,s);if(!0===s)return Oi(t,[{start:a,end:r,loop:o}],i,e);return Oi(t,function(t,e,i,s){const n=t.length,o=[];let a,r=e,l=t[e];for(a=e+1;a<=i;++a){const i=t[a%n];i.skip||i.stop?l.skip||(s=!1,o.push({start:e%n,end:(a-1)%n,loop:s}),e=r=i.stop?a:null):(r=a,l.skip&&(e=a)),l=i}return null!==r&&o.push({start:e%n,end:r%n,loop:s}),o}(i,a,r{t[a](e[i],n)&&(o.push({element:t,datasetIndex:s,index:l}),r=r||t.inRange(e.x,e.y,n))})),s&&!r?[]:o}var Vi={evaluateInteractionItems:Ei,modes:{index(t,e,i,s){const n=ue(e,t),o=i.axis||"x",a=i.includeInvisible||!1,r=i.intersect?Ri(t,n,o,s,a):zi(t,n,o,!1,s,a),l=[];return r.length?(t.getSortedVisibleDatasetMetas().forEach((t=>{const e=r[0].index,i=t.data[e];i&&!i.skip&&l.push({element:i,datasetIndex:t.index,index:e})})),l):[]},dataset(t,e,i,s){const n=ue(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;let r=i.intersect?Ri(t,n,o,s,a):zi(t,n,o,!1,s,a);if(r.length>0){const e=r[0].datasetIndex,i=t.getDatasetMeta(e).data;r=[];for(let t=0;tRi(t,ue(e,t),i.axis||"xy",s,i.includeInvisible||!1),nearest(t,e,i,s){const n=ue(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;return zi(t,n,o,i.intersect,s,a)},x:(t,e,i,s)=>Fi(t,ue(e,t),"x",i.intersect,s),y:(t,e,i,s)=>Fi(t,ue(e,t),"y",i.intersect,s)}};const Bi=["left","top","right","bottom"];function Ni(t,e){return t.filter((t=>t.pos===e))}function Wi(t,e){return t.filter((t=>-1===Bi.indexOf(t.pos)&&t.box.axis===e))}function ji(t,e){return t.sort(((t,i)=>{const s=e?i:t,n=e?t:i;return s.weight===n.weight?s.index-n.index:s.weight-n.weight}))}function Hi(t,e){const i=function(t){const e={};for(const i of t){const{stack:t,pos:s,stackWeight:n}=i;if(!t||!Bi.includes(s))continue;const o=e[t]||(e[t]={count:0,placed:0,weight:0,size:0});o.count++,o.weight+=n}return e}(t),{vBoxMaxWidth:s,hBoxMaxHeight:n}=e;let o,a,r;for(o=0,a=t.length;o{s[t]=Math.max(e[t],i[t])})),s}return s(t?["left","right"]:["top","bottom"])}function qi(t,e,i,s){const n=[];let o,a,r,l,h,c;for(o=0,a=t.length,h=0;ot.box.fullSize)),!0),s=ji(Ni(e,"left"),!0),n=ji(Ni(e,"right")),o=ji(Ni(e,"top"),!0),a=ji(Ni(e,"bottom")),r=Wi(e,"x"),l=Wi(e,"y");return{fullSize:i,leftAndTop:s.concat(o),rightAndBottom:n.concat(l).concat(a).concat(r),chartArea:Ni(e,"chartArea"),vertical:s.concat(n).concat(l),horizontal:o.concat(a).concat(r)}}(t.boxes),l=r.vertical,h=r.horizontal;d(t.boxes,(t=>{"function"==typeof t.beforeLayout&&t.beforeLayout()}));const c=l.reduce(((t,e)=>e.box.options&&!1===e.box.options.display?t:t+1),0)||1,u=Object.freeze({outerWidth:e,outerHeight:i,padding:n,availableWidth:o,availableHeight:a,vBoxMaxWidth:o/2/c,hBoxMaxHeight:a/2}),f=Object.assign({},n);Yi(f,pi(s));const g=Object.assign({maxPadding:f,w:o,h:a,x:n.left,y:n.top},n),p=Hi(l.concat(h),u);qi(r.fullSize,g,u,p),qi(l,g,u,p),qi(h,g,u,p)&&qi(l,g,u,p),function(t){const e=t.maxPadding;function i(i){const s=Math.max(e[i]-t[i],0);return t[i]+=s,s}t.y+=i("top"),t.x+=i("left"),i("right"),i("bottom")}(g),Gi(r.leftAndTop,g,u,p),g.x+=g.w,g.y+=g.h,Gi(r.rightAndBottom,g,u,p),t.chartArea={left:g.left,top:g.top,right:g.left+g.w,bottom:g.top+g.h,height:g.h,width:g.w},d(r.chartArea,(e=>{const i=e.box;Object.assign(i,t.chartArea),i.update(g.w,g.h,{left:0,top:0,right:0,bottom:0})}))}};class Ji{acquireContext(t,e){}releaseContext(t){return!1}addEventListener(t,e,i){}removeEventListener(t,e,i){}getDevicePixelRatio(){return 1}getMaximumSize(t,e,i,s){return e=Math.max(0,e||t.width),i=i||t.height,{width:e,height:Math.max(0,s?Math.floor(e/s):i)}}isAttached(t){return!0}updateConfig(t){}}class Qi extends Ji{acquireContext(t){return t&&t.getContext&&t.getContext("2d")||null}updateConfig(t){t.options.animation=!1}}const ts={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},es=t=>null===t||""===t;const is=!!me&&{passive:!0};function ss(t,e,i){t.canvas.removeEventListener(e,i,is)}function ns(t,e){for(const i of t)if(i===e||i.contains(e))return!0}function os(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||ns(i.addedNodes,s),e=e&&!ns(i.removedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}function as(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||ns(i.removedNodes,s),e=e&&!ns(i.addedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}const rs=new Map;let ls=0;function hs(){const t=window.devicePixelRatio;t!==ls&&(ls=t,rs.forEach(((e,i)=>{i.currentDevicePixelRatio!==t&&e()})))}function cs(t,e,i){const s=t.canvas,n=s&&ae(s);if(!n)return;const o=ht(((t,e)=>{const s=n.clientWidth;i(t,e),s{const e=t[0],i=e.contentRect.width,s=e.contentRect.height;0===i&&0===s||o(i,s)}));return a.observe(n),function(t,e){rs.size||window.addEventListener("resize",hs),rs.set(t,e)}(t,o),a}function ds(t,e,i){i&&i.disconnect(),"resize"===e&&function(t){rs.delete(t),rs.size||window.removeEventListener("resize",hs)}(t)}function us(t,e,i){const s=t.canvas,n=ht((e=>{null!==t.ctx&&i(function(t,e){const i=ts[t.type]||t.type,{x:s,y:n}=ue(t,e);return{type:i,chart:e,native:t,x:void 0!==s?s:null,y:void 0!==n?n:null}}(e,t))}),t,(t=>{const e=t[0];return[e,e.offsetX,e.offsetY]}));return function(t,e,i){t.addEventListener(e,i,is)}(s,e,n),n}class fs extends Ji{acquireContext(t,e){const i=t&&t.getContext&&t.getContext("2d");return i&&i.canvas===t?(function(t,e){const i=t.style,s=t.getAttribute("height"),n=t.getAttribute("width");if(t.$chartjs={initial:{height:s,width:n,style:{display:i.display,height:i.height,width:i.width}}},i.display=i.display||"block",i.boxSizing=i.boxSizing||"border-box",es(n)){const e=be(t,"width");void 0!==e&&(t.width=e)}if(es(s))if(""===t.style.height)t.height=t.width/(e||2);else{const e=be(t,"height");void 0!==e&&(t.height=e)}}(t,e),i):null}releaseContext(t){const e=t.canvas;if(!e.$chartjs)return!1;const s=e.$chartjs.initial;["height","width"].forEach((t=>{const n=s[t];i(n)?e.removeAttribute(t):e.setAttribute(t,n)}));const n=s.style||{};return Object.keys(n).forEach((t=>{e.style[t]=n[t]})),e.width=e.width,delete e.$chartjs,!0}addEventListener(t,e,i){this.removeEventListener(t,e);const s=t.$proxies||(t.$proxies={}),n={attach:os,detach:as,resize:cs}[e]||us;s[e]=n(t,e,i)}removeEventListener(t,e){const i=t.$proxies||(t.$proxies={}),s=i[e];if(!s)return;({attach:ds,detach:ds,resize:ds}[e]||ss)(t,e,s),i[e]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(t,e,i,s){return ge(t,e,i,s)}isAttached(t){const e=ae(t);return!(!e||!e.isConnected)}}function gs(t){return!oe()||"undefined"!=typeof OffscreenCanvas&&t instanceof OffscreenCanvas?Qi:fs}var ps=Object.freeze({__proto__:null,_detectPlatform:gs,BasePlatform:Ji,BasicPlatform:Qi,DomPlatform:fs});const ms="transparent",bs={boolean:(t,e,i)=>i>.5?e:t,color(t,e,i){const s=Jt(t||ms),n=s.valid&&Jt(e||ms);return n&&n.valid?n.mix(s,i).hexString():e},number:(t,e,i)=>t+(e-t)*i};class xs{constructor(t,e,i,s){const n=e[i];s=bi([t.to,s,n,t.from]);const o=bi([t.from,n,s]);this._active=!0,this._fn=t.fn||bs[t.type||typeof o],this._easing=si[t.easing]||si.linear,this._start=Math.floor(Date.now()+(t.delay||0)),this._duration=this._total=Math.floor(t.duration),this._loop=!!t.loop,this._target=e,this._prop=i,this._from=o,this._to=s,this._promises=void 0}active(){return this._active}update(t,e,i){if(this._active){this._notify(!1);const s=this._target[this._prop],n=i-this._start,o=this._duration-n;this._start=i,this._duration=Math.floor(Math.max(o,t.duration)),this._total+=n,this._loop=!!t.loop,this._to=bi([t.to,e,s,t.from]),this._from=bi([t.from,s,e])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(t){const e=t-this._start,i=this._duration,s=this._prop,n=this._from,o=this._loop,a=this._to;let r;if(this._active=n!==a&&(o||e1?2-r:r,r=this._easing(Math.min(1,Math.max(0,r))),this._target[s]=this._fn(n,a,r))}wait(){const t=this._promises||(this._promises=[]);return new Promise(((e,i)=>{t.push({res:e,rej:i})}))}_notify(t){const e=t?"res":"rej",i=this._promises||[];for(let t=0;t"onProgress"!==t&&"onComplete"!==t&&"fn"!==t}),ne.set("animations",{colors:{type:"color",properties:["color","borderColor","backgroundColor"]},numbers:{type:"number",properties:["x","y","borderWidth","radius","tension"]}}),ne.describe("animations",{_fallback:"animation"}),ne.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:t=>0|t}}}});class ys{constructor(t,e){this._chart=t,this._properties=new Map,this.configure(e)}configure(t){if(!n(t))return;const e=this._properties;Object.getOwnPropertyNames(t).forEach((i=>{const o=t[i];if(!n(o))return;const a={};for(const t of _s)a[t]=o[t];(s(o.properties)&&o.properties||[i]).forEach((t=>{t!==i&&e.has(t)||e.set(t,a)}))}))}_animateOptions(t,e){const i=e.options,s=function(t,e){if(!e)return;let i=t.options;if(!i)return void(t.options=e);i.$shared&&(t.options=i=Object.assign({},i,{$shared:!1,$animations:{}}));return i}(t,i);if(!s)return[];const n=this._createAnimations(s,i);return i.$shared&&function(t,e){const i=[],s=Object.keys(e);for(let e=0;e{t.options=i}),(()=>{})),n}_createAnimations(t,e){const i=this._properties,s=[],n=t.$animations||(t.$animations={}),o=Object.keys(e),a=Date.now();let r;for(r=o.length-1;r>=0;--r){const l=o[r];if("$"===l.charAt(0))continue;if("options"===l){s.push(...this._animateOptions(t,e));continue}const h=e[l];let c=n[l];const d=i.get(l);if(c){if(d&&c.active()){c.update(d,h,a);continue}c.cancel()}d&&d.duration?(n[l]=c=new xs(d,t,l,h),s.push(c)):t[l]=h}return s}update(t,e){if(0===this._properties.size)return void Object.assign(t,e);const i=this._createAnimations(t,e);return i.length?(mt.add(this._chart,i),!0):void 0}}function vs(t,e){const i=t&&t.options||{},s=i.reverse,n=void 0===i.min?e:0,o=void 0===i.max?e:0;return{start:s?o:n,end:s?n:o}}function ws(t,e){const i=[],s=t._getSortedDatasetMetas(e);let n,o;for(n=0,o=s.length;n0||!i&&e<0)return n.index}return null}function Ds(t,e){const{chart:i,_cachedMeta:s}=t,n=i._stacks||(i._stacks={}),{iScale:o,vScale:a,index:r}=s,l=o.axis,h=a.axis,c=function(t,e,i){return`${t.id}.${e.id}.${i.stack||i.type}`}(o,a,s),d=e.length;let u;for(let t=0;ti[t].axis===e)).shift()}function Cs(t,e){const i=t.controller.index,s=t.vScale&&t.vScale.axis;if(s){e=e||t._parsed;for(const t of e){const e=t._stacks;if(!e||void 0===e[s]||void 0===e[s][i])return;delete e[s][i]}}}const As=t=>"reset"===t||"none"===t,Ts=(t,e)=>e?t:Object.assign({},t);class Ls{constructor(t,e){this.chart=t,this._ctx=t.ctx,this.index=e,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.initialize()}initialize(){const t=this._cachedMeta;this.configure(),this.linkScales(),t._stacked=ks(t.vScale,t),this.addElements()}updateIndex(t){this.index!==t&&Cs(this._cachedMeta),this.index=t}linkScales(){const t=this.chart,e=this._cachedMeta,i=this.getDataset(),s=(t,e,i,s)=>"x"===t?e:"r"===t?s:i,n=e.xAxisID=r(i.xAxisID,Os(t,"x")),o=e.yAxisID=r(i.yAxisID,Os(t,"y")),a=e.rAxisID=r(i.rAxisID,Os(t,"r")),l=e.indexAxis,h=e.iAxisID=s(l,n,o,a),c=e.vAxisID=s(l,o,n,a);e.xScale=this.getScaleForId(n),e.yScale=this.getScaleForId(o),e.rScale=this.getScaleForId(a),e.iScale=this.getScaleForId(h),e.vScale=this.getScaleForId(c)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(t){return this.chart.scales[t]}_getOtherScale(t){const e=this._cachedMeta;return t===e.iScale?e.vScale:e.iScale}reset(){this._update("reset")}_destroy(){const t=this._cachedMeta;this._data&&at(this._data,this),t._stacked&&Cs(t)}_dataCheck(){const t=this.getDataset(),e=t.data||(t.data=[]),i=this._data;if(n(e))this._data=function(t){const e=Object.keys(t),i=new Array(e.length);let s,n,o;for(s=0,n=e.length;s0&&i._parsed[t-1];if(!1===this._parsing)i._parsed=o,i._sorted=!0,d=o;else{d=s(o[t])?this.parseArrayData(i,o,t,e):n(o[t])?this.parseObjectData(i,o,t,e):this.parsePrimitiveData(i,o,t,e);const a=()=>null===c[l]||f&&c[l]t&&!e.hidden&&e._stacked&&{keys:ws(i,!0),values:null})(e,i,this.chart),h={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY},{min:c,max:d}=function(t){const{min:e,max:i,minDefined:s,maxDefined:n}=t.getUserBounds();return{min:s?e:Number.NEGATIVE_INFINITY,max:n?i:Number.POSITIVE_INFINITY}}(r);let u,f;function g(){f=s[u];const e=f[r.axis];return!o(f[t.axis])||c>e||d=0;--u)if(!g()){this.updateRangeFromParsed(h,t,f,l);break}return h}getAllParsedValues(t){const e=this._cachedMeta._parsed,i=[];let s,n,a;for(s=0,n=e.length;s=0&&tthis.getContext(i,s)),c);return f.$shared&&(f.$shared=r,n[o]=Object.freeze(Ts(f,r))),f}_resolveAnimations(t,e,i){const s=this.chart,n=this._cachedDataOpts,o=`animation-${e}`,a=n[o];if(a)return a;let r;if(!1!==s.options.animation){const s=this.chart.config,n=s.datasetAnimationScopeKeys(this._type,e),o=s.getOptionScopes(this.getDataset(),n);r=s.createResolver(o,this.getContext(t,i,e))}const l=new ys(s,r&&r.animations);return r&&r._cacheable&&(n[o]=Object.freeze(l)),l}getSharedOptions(t){if(t.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},t))}includeOptions(t,e){return!e||As(t)||this.chart._animationsDisabled}_getSharedOptions(t,e){const i=this.resolveDataElementOptions(t,e),s=this._sharedOptions,n=this.getSharedOptions(i),o=this.includeOptions(e,n)||n!==s;return this.updateSharedOptions(n,e,i),{sharedOptions:n,includeOptions:o}}updateElement(t,e,i,s){As(s)?Object.assign(t,i):this._resolveAnimations(e,s).update(t,i)}updateSharedOptions(t,e,i){t&&!As(e)&&this._resolveAnimations(void 0,e).update(t,i)}_setStyle(t,e,i,s){t.active=s;const n=this.getStyle(e,s);this._resolveAnimations(e,i,s).update(t,{options:!s&&this.getSharedOptions(n)||n})}removeHoverStyle(t,e,i){this._setStyle(t,i,"active",!1)}setHoverStyle(t,e,i){this._setStyle(t,i,"active",!0)}_removeDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!1)}_setDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!0)}_resyncElements(t){const e=this._data,i=this._cachedMeta.data;for(const[t,e,i]of this._syncList)this[t](e,i);this._syncList=[];const s=i.length,n=e.length,o=Math.min(n,s);o&&this.parse(0,o),n>s?this._insertElements(s,n-s,t):n{for(t.length+=e,a=t.length-1;a>=o;a--)t[a]=t[a-e]};for(r(n),a=t;a{s[t]=i[t]&&i[t].active()?i[t]._to:this[t]})),s}}Es.defaults={},Es.defaultRoutes=void 0;const Rs={values:t=>s(t)?t:""+t,numeric(t,e,i){if(0===t)return"0";const s=this.chart.options.locale;let n,o=t;if(i.length>1){const e=Math.max(Math.abs(i[0].value),Math.abs(i[i.length-1].value));(e<1e-4||e>1e15)&&(n="scientific"),o=function(t,e){let i=e.length>3?e[2].value-e[1].value:e[1].value-e[0].value;Math.abs(i)>=1&&t!==Math.floor(t)&&(i=t-Math.floor(t));return i}(t,i)}const a=I(Math.abs(o)),r=Math.max(Math.min(-1*Math.floor(a),20),0),l={notation:n,minimumFractionDigits:r,maximumFractionDigits:r};return Object.assign(l,this.options.ticks.format),li(t,s,l)},logarithmic(t,e,i){if(0===t)return"0";const s=t/Math.pow(10,Math.floor(I(t)));return 1===s||2===s||5===s?Rs.numeric.call(this,t,e,i):""}};var Is={formatters:Rs};function zs(t,e){const s=t.options.ticks,n=s.maxTicksLimit||function(t){const e=t.options.offset,i=t._tickSize(),s=t._length/i+(e?0:1),n=t._maxLength/i;return Math.floor(Math.min(s,n))}(t),o=s.major.enabled?function(t){const e=[];let i,s;for(i=0,s=t.length;in)return function(t,e,i,s){let n,o=0,a=i[0];for(s=Math.ceil(s),n=0;nn)return e}return Math.max(n,1)}(o,e,n);if(a>0){let t,s;const n=a>1?Math.round((l-r)/(a-1)):null;for(Fs(e,h,c,i(n)?0:r-n,r),t=0,s=a-1;te.lineWidth,tickColor:(t,e)=>e.color,offset:!1,borderDash:[],borderDashOffset:0,borderWidth:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:Is.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),ne.route("scale.ticks","color","","color"),ne.route("scale.grid","color","","borderColor"),ne.route("scale.grid","borderColor","","borderColor"),ne.route("scale.title","color","","color"),ne.describe("scale",{_fallback:!1,_scriptable:t=>!t.startsWith("before")&&!t.startsWith("after")&&"callback"!==t&&"parser"!==t,_indexable:t=>"borderDash"!==t&&"tickBorderDash"!==t}),ne.describe("scales",{_fallback:"scale"}),ne.describe("scale.ticks",{_scriptable:t=>"backdropPadding"!==t&&"callback"!==t,_indexable:t=>"backdropPadding"!==t});const Vs=(t,e,i)=>"top"===e||"left"===e?t[e]+i:t[e]-i;function Bs(t,e){const i=[],s=t.length/e,n=t.length;let o=0;for(;oa+r)))return h}function Ws(t){return t.drawTicks?t.tickLength:0}function js(t,e){if(!t.display)return 0;const i=mi(t.font,e),n=pi(t.padding);return(s(t.text)?t.text.length:1)*i.lineHeight+n.height}function Hs(t,e,i){let s=dt(t);return(i&&"right"!==e||!i&&"right"===e)&&(s=(t=>"left"===t?"right":"right"===t?"left":t)(s)),s}class $s extends Es{constructor(t){super(),this.id=t.id,this.type=t.type,this.options=void 0,this.ctx=t.ctx,this.chart=t.chart,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this._margins={left:0,right:0,top:0,bottom:0},this.maxWidth=void 0,this.maxHeight=void 0,this.paddingTop=void 0,this.paddingBottom=void 0,this.paddingLeft=void 0,this.paddingRight=void 0,this.axis=void 0,this.labelRotation=void 0,this.min=void 0,this.max=void 0,this._range=void 0,this.ticks=[],this._gridLineItems=null,this._labelItems=null,this._labelSizes=null,this._length=0,this._maxLength=0,this._longestTextCache={},this._startPixel=void 0,this._endPixel=void 0,this._reversePixels=!1,this._userMax=void 0,this._userMin=void 0,this._suggestedMax=void 0,this._suggestedMin=void 0,this._ticksLength=0,this._borderValue=0,this._cache={},this._dataLimitsCached=!1,this.$context=void 0}init(t){this.options=t.setContext(this.getContext()),this.axis=t.axis,this._userMin=this.parse(t.min),this._userMax=this.parse(t.max),this._suggestedMin=this.parse(t.suggestedMin),this._suggestedMax=this.parse(t.suggestedMax)}parse(t,e){return t}getUserBounds(){let{_userMin:t,_userMax:e,_suggestedMin:i,_suggestedMax:s}=this;return t=a(t,Number.POSITIVE_INFINITY),e=a(e,Number.NEGATIVE_INFINITY),i=a(i,Number.POSITIVE_INFINITY),s=a(s,Number.NEGATIVE_INFINITY),{min:a(t,i),max:a(e,s),minDefined:o(t),maxDefined:o(e)}}getMinMax(t){let e,{min:i,max:s,minDefined:n,maxDefined:o}=this.getUserBounds();if(n&&o)return{min:i,max:s};const r=this.getMatchingVisibleMetas();for(let a=0,l=r.length;as?s:i,s=n&&i>s?i:s,{min:a(i,a(s,i)),max:a(s,a(i,s))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){const t=this.chart.data;return this.options.labels||(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels||[]}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){c(this.options.beforeUpdate,[this])}update(t,e,i){const{beginAtZero:s,grace:n,ticks:o}=this.options,a=o.sampleSize;this.beforeUpdate(),this.maxWidth=t,this.maxHeight=e,this._margins=i=Object.assign({left:0,right:0,top:0,bottom:0},i),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+i.left+i.right:this.height+i.top+i.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=xi(this,n,s),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();const r=a=n||i<=1||!this.isHorizontal())return void(this.labelRotation=s);const h=this._getLabelSizes(),c=h.widest.width,d=h.highest.height,u=Z(this.chart.width-c,0,this.maxWidth);o=t.offset?this.maxWidth/i:u/(i-1),c+6>o&&(o=u/(i-(t.offset?.5:1)),a=this.maxHeight-Ws(t.grid)-e.padding-js(t.title,this.chart.options.font),r=Math.sqrt(c*c+d*d),l=$(Math.min(Math.asin(Z((h.highest.height+6)/o,-1,1)),Math.asin(Z(a/r,-1,1))-Math.asin(Z(d/r,-1,1)))),l=Math.max(s,Math.min(n,l))),this.labelRotation=l}afterCalculateLabelRotation(){c(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){c(this.options.beforeFit,[this])}fit(){const t={width:0,height:0},{chart:e,options:{ticks:i,title:s,grid:n}}=this,o=this._isVisible(),a=this.isHorizontal();if(o){const o=js(s,e.options.font);if(a?(t.width=this.maxWidth,t.height=Ws(n)+o):(t.height=this.maxHeight,t.width=Ws(n)+o),i.display&&this.ticks.length){const{first:e,last:s,widest:n,highest:o}=this._getLabelSizes(),r=2*i.padding,l=H(this.labelRotation),h=Math.cos(l),c=Math.sin(l);if(a){const e=i.mirror?0:c*n.width+h*o.height;t.height=Math.min(this.maxHeight,t.height+e+r)}else{const e=i.mirror?0:h*n.width+c*o.height;t.width=Math.min(this.maxWidth,t.width+e+r)}this._calculatePadding(e,s,c,h)}}this._handleMargins(),a?(this.width=this._length=e.width-this._margins.left-this._margins.right,this.height=t.height):(this.width=t.width,this.height=this._length=e.height-this._margins.top-this._margins.bottom)}_calculatePadding(t,e,i,s){const{ticks:{align:n,padding:o},position:a}=this.options,r=0!==this.labelRotation,l="top"!==a&&"x"===this.axis;if(this.isHorizontal()){const a=this.getPixelForTick(0)-this.left,h=this.right-this.getPixelForTick(this.ticks.length-1);let c=0,d=0;r?l?(c=s*t.width,d=i*e.height):(c=i*t.height,d=s*e.width):"start"===n?d=e.width:"end"===n?c=t.width:"inner"!==n&&(c=t.width/2,d=e.width/2),this.paddingLeft=Math.max((c-a+o)*this.width/(this.width-a),0),this.paddingRight=Math.max((d-h+o)*this.width/(this.width-h),0)}else{let i=e.height/2,s=t.height/2;"start"===n?(i=0,s=t.height):"end"===n&&(i=e.height,s=0),this.paddingTop=i+o,this.paddingBottom=s+o}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){c(this.options.afterFit,[this])}isHorizontal(){const{axis:t,position:e}=this.options;return"top"===e||"bottom"===e||"x"===t}isFullSize(){return this.options.fullSize}_convertTicksToLabels(t){let e,s;for(this.beforeTickToLabelConversion(),this.generateTickLabels(t),e=0,s=t.length;e{const i=t.gc,s=i.length/2;let n;if(s>e){for(n=0;n({width:a[t]||0,height:r[t]||0});return{first:k(0),last:k(e-1),widest:k(w),highest:k(M),widths:a,heights:r}}getLabelForValue(t){return t}getPixelForValue(t,e){return NaN}getValueForPixel(t){}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getPixelForDecimal(t){this._reversePixels&&(t=1-t);const e=this._startPixel+t*this._length;return J(this._alignToPixels?ve(this.chart,e,0):e)}getDecimalForPixel(t){const e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:t,max:e}=this;return t<0&&e<0?e:t>0&&e>0?t:0}getContext(t){const e=this.ticks||[];if(t>=0&&ta*s?a/i:r/s:r*s0}_computeGridLineItems(t){const e=this.axis,i=this.chart,s=this.options,{grid:o,position:a}=s,l=o.offset,h=this.isHorizontal(),c=this.ticks.length+(l?1:0),d=Ws(o),u=[],f=o.setContext(this.getContext()),g=f.drawBorder?f.borderWidth:0,p=g/2,m=function(t){return ve(i,t,g)};let b,x,_,y,v,w,M,k,S,P,D,O;if("top"===a)b=m(this.bottom),w=this.bottom-d,k=b-p,P=m(t.top)+p,O=t.bottom;else if("bottom"===a)b=m(this.top),P=t.top,O=m(t.bottom)-p,w=b+p,k=this.top+d;else if("left"===a)b=m(this.right),v=this.right-d,M=b-p,S=m(t.left)+p,D=t.right;else if("right"===a)b=m(this.left),S=t.left,D=m(t.right)-p,v=b+p,M=this.left+d;else if("x"===e){if("center"===a)b=m((t.top+t.bottom)/2+.5);else if(n(a)){const t=Object.keys(a)[0],e=a[t];b=m(this.chart.scales[t].getPixelForValue(e))}P=t.top,O=t.bottom,w=b+p,k=w+d}else if("y"===e){if("center"===a)b=m((t.left+t.right)/2);else if(n(a)){const t=Object.keys(a)[0],e=a[t];b=m(this.chart.scales[t].getPixelForValue(e))}v=b-p,M=v-d,S=t.left,D=t.right}const C=r(s.ticks.maxTicksLimit,c),A=Math.max(1,Math.ceil(c/C));for(x=0;xe.value===t));if(i>=0){return e.setContext(this.getContext(i)).lineWidth}return 0}drawGrid(t){const e=this.options.grid,i=this.ctx,s=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(t));let n,o;const a=(t,e,s)=>{s.width&&s.color&&(i.save(),i.lineWidth=s.width,i.strokeStyle=s.color,i.setLineDash(s.borderDash||[]),i.lineDashOffset=s.borderDashOffset,i.beginPath(),i.moveTo(t.x,t.y),i.lineTo(e.x,e.y),i.stroke(),i.restore())};if(e.display)for(n=0,o=s.length;n{this.drawBackground(),this.drawGrid(t),this.drawTitle()}},{z:i+1,draw:()=>{this.drawBorder()}},{z:e,draw:t=>{this.drawLabels(t)}}]:[{z:e,draw:t=>{this.draw(t)}}]}getMatchingVisibleMetas(t){const e=this.chart.getSortedVisibleDatasetMetas(),i=this.axis+"AxisID",s=[];let n,o;for(n=0,o=e.length;n{const s=i.split("."),n=s.pop(),o=[t].concat(s).join("."),a=e[i].split("."),r=a.pop(),l=a.join(".");ne.route(o,n,l,r)}))}(e,t.defaultRoutes);t.descriptors&&ne.describe(e,t.descriptors)}(t,o,i),this.override&&ne.override(t.id,t.overrides)),o}get(t){return this.items[t]}unregister(t){const e=this.items,i=t.id,s=this.scope;i in e&&delete e[i],s&&i in ne[s]&&(delete ne[s][i],this.override&&delete te[i])}}var Us=new class{constructor(){this.controllers=new Ys(Ls,"datasets",!0),this.elements=new Ys(Es,"elements"),this.plugins=new Ys(Object,"plugins"),this.scales=new Ys($s,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...t){this._each("register",t)}remove(...t){this._each("unregister",t)}addControllers(...t){this._each("register",t,this.controllers)}addElements(...t){this._each("register",t,this.elements)}addPlugins(...t){this._each("register",t,this.plugins)}addScales(...t){this._each("register",t,this.scales)}getController(t){return this._get(t,this.controllers,"controller")}getElement(t){return this._get(t,this.elements,"element")}getPlugin(t){return this._get(t,this.plugins,"plugin")}getScale(t){return this._get(t,this.scales,"scale")}removeControllers(...t){this._each("unregister",t,this.controllers)}removeElements(...t){this._each("unregister",t,this.elements)}removePlugins(...t){this._each("unregister",t,this.plugins)}removeScales(...t){this._each("unregister",t,this.scales)}_each(t,e,i){[...e].forEach((e=>{const s=i||this._getRegistryForType(e);i||s.isForType(e)||s===this.plugins&&e.id?this._exec(t,s,e):d(e,(e=>{const s=i||this._getRegistryForType(e);this._exec(t,s,e)}))}))}_exec(t,e,i){const s=w(t);c(i["before"+s],[],i),e[t](i),c(i["after"+s],[],i)}_getRegistryForType(t){for(let e=0;et.filter((t=>!e.some((e=>t.plugin.id===e.plugin.id))));this._notify(s(e,i),t,"stop"),this._notify(s(i,e),t,"start")}}function qs(t,e){return e||!1!==t?!0===t?{}:t:null}function Ks(t,{plugin:e,local:i},s,n){const o=t.pluginScopeKeys(e),a=t.getOptionScopes(s,o);return i&&e.defaults&&a.push(e.defaults),t.createResolver(a,n,[""],{scriptable:!1,indexable:!1,allKeys:!0})}function Gs(t,e){const i=ne.datasets[t]||{};return((e.datasets||{})[t]||{}).indexAxis||e.indexAxis||i.indexAxis||"x"}function Zs(t,e){return"x"===t||"y"===t?t:e.axis||("top"===(i=e.position)||"bottom"===i?"x":"left"===i||"right"===i?"y":void 0)||t.charAt(0).toLowerCase();var i}function Js(t){const e=t.options||(t.options={});e.plugins=r(e.plugins,{}),e.scales=function(t,e){const i=te[t.type]||{scales:{}},s=e.scales||{},o=Gs(t.type,e),a=Object.create(null),r=Object.create(null);return Object.keys(s).forEach((t=>{const e=s[t];if(!n(e))return console.error(`Invalid scale configuration for scale: ${t}`);if(e._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${t}`);const l=Zs(t,e),h=function(t,e){return t===e?"_index_":"_value_"}(l,o),c=i.scales||{};a[l]=a[l]||t,r[t]=b(Object.create(null),[{axis:l},e,c[l],c[h]])})),t.data.datasets.forEach((i=>{const n=i.type||t.type,o=i.indexAxis||Gs(n,e),l=(te[n]||{}).scales||{};Object.keys(l).forEach((t=>{const e=function(t,e){let i=t;return"_index_"===t?i=e:"_value_"===t&&(i="x"===e?"y":"x"),i}(t,o),n=i[e+"AxisID"]||a[e]||e;r[n]=r[n]||Object.create(null),b(r[n],[{axis:e},s[n],l[t]])}))})),Object.keys(r).forEach((t=>{const e=r[t];b(e,[ne.scales[e.type],ne.scale])})),r}(t,e)}function Qs(t){return(t=t||{}).datasets=t.datasets||[],t.labels=t.labels||[],t}const tn=new Map,en=new Set;function sn(t,e){let i=tn.get(t);return i||(i=e(),tn.set(t,i),en.add(i)),i}const nn=(t,e,i)=>{const s=y(e,i);void 0!==s&&t.add(s)};class on{constructor(t){this._config=function(t){return(t=t||{}).data=Qs(t.data),Js(t),t}(t),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(t){this._config.type=t}get data(){return this._config.data}set data(t){this._config.data=Qs(t)}get options(){return this._config.options}set options(t){this._config.options=t}get plugins(){return this._config.plugins}update(){const t=this._config;this.clearCache(),Js(t)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(t){return sn(t,(()=>[[`datasets.${t}`,""]]))}datasetAnimationScopeKeys(t,e){return sn(`${t}.transition.${e}`,(()=>[[`datasets.${t}.transitions.${e}`,`transitions.${e}`],[`datasets.${t}`,""]]))}datasetElementScopeKeys(t,e){return sn(`${t}-${e}`,(()=>[[`datasets.${t}.elements.${e}`,`datasets.${t}`,`elements.${e}`,""]]))}pluginScopeKeys(t){const e=t.id;return sn(`${this.type}-plugin-${e}`,(()=>[[`plugins.${e}`,...t.additionalOptionScopes||[]]]))}_cachedScopes(t,e){const i=this._scopeCache;let s=i.get(t);return s&&!e||(s=new Map,i.set(t,s)),s}getOptionScopes(t,e,i){const{options:s,type:n}=this,o=this._cachedScopes(t,i),a=o.get(e);if(a)return a;const r=new Set;e.forEach((e=>{t&&(r.add(t),e.forEach((e=>nn(r,t,e)))),e.forEach((t=>nn(r,s,t))),e.forEach((t=>nn(r,te[n]||{},t))),e.forEach((t=>nn(r,ne,t))),e.forEach((t=>nn(r,ee,t)))}));const l=Array.from(r);return 0===l.length&&l.push(Object.create(null)),en.has(e)&&o.set(e,l),l}chartOptionScopes(){const{options:t,type:e}=this;return[t,te[e]||{},ne.datasets[e]||{},{type:e},ne,ee]}resolveNamedOptions(t,e,i,n=[""]){const o={$shared:!0},{resolver:a,subPrefixes:r}=an(this._resolverCache,t,n);let l=a;if(function(t,e){const{isScriptable:i,isIndexable:n}=Ie(t);for(const o of e){const e=i(o),a=n(o),r=(a||e)&&t[o];if(e&&(k(r)||rn(r))||a&&s(r))return!0}return!1}(a,e)){o.$shared=!1;l=Re(a,i=k(i)?i():i,this.createResolver(t,i,r))}for(const t of e)o[t]=l[t];return o}createResolver(t,e,i=[""],s){const{resolver:o}=an(this._resolverCache,t,i);return n(e)?Re(o,e,void 0,s):o}}function an(t,e,i){let s=t.get(e);s||(s=new Map,t.set(e,s));const n=i.join();let o=s.get(n);if(!o){o={resolver:Ee(e,i),subPrefixes:i.filter((t=>!t.toLowerCase().includes("hover")))},s.set(n,o)}return o}const rn=t=>n(t)&&Object.getOwnPropertyNames(t).reduce(((e,i)=>e||k(t[i])),!1);const ln=["top","bottom","left","right","chartArea"];function hn(t,e){return"top"===t||"bottom"===t||-1===ln.indexOf(t)&&"x"===e}function cn(t,e){return function(i,s){return i[t]===s[t]?i[e]-s[e]:i[t]-s[t]}}function dn(t){const e=t.chart,i=e.options.animation;e.notifyPlugins("afterRender"),c(i&&i.onComplete,[t],e)}function un(t){const e=t.chart,i=e.options.animation;c(i&&i.onProgress,[t],e)}function fn(t){return oe()&&"string"==typeof t?t=document.getElementById(t):t&&t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas),t}const gn={},pn=t=>{const e=fn(t);return Object.values(gn).filter((t=>t.canvas===e)).pop()};function mn(t,e,i){const s=Object.keys(t);for(const n of s){const s=+n;if(s>=e){const o=t[n];delete t[n],(i>0||s>e)&&(t[s+i]=o)}}}class bn{constructor(t,i){const s=this.config=new on(i),n=fn(t),o=pn(n);if(o)throw new Error("Canvas is already in use. Chart with ID '"+o.id+"' must be destroyed before the canvas with ID '"+o.canvas.id+"' can be reused.");const a=s.createResolver(s.chartOptionScopes(),this.getContext());this.platform=new(s.platform||gs(n)),this.platform.updateConfig(s);const r=this.platform.acquireContext(n,a.aspectRatio),l=r&&r.canvas,h=l&&l.height,c=l&&l.width;this.id=e(),this.ctx=r,this.canvas=l,this.width=c,this.height=h,this._options=a,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new Xs,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=ct((t=>this.update(t)),a.resizeDelay||0),this._dataChanges=[],gn[this.id]=this,r&&l?(mt.listen(this,"complete",dn),mt.listen(this,"progress",un),this._initialize(),this.attached&&this.update()):console.error("Failed to create chart: can't acquire context from the given item")}get aspectRatio(){const{options:{aspectRatio:t,maintainAspectRatio:e},width:s,height:n,_aspectRatio:o}=this;return i(t)?e&&o?o:n?s/n:null:t}get data(){return this.config.data}set data(t){this.config.data=t}get options(){return this._options}set options(t){this.config.options=t}_initialize(){return this.notifyPlugins("beforeInit"),this.options.responsive?this.resize():pe(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins("afterInit"),this}clear(){return we(this.canvas,this.ctx),this}stop(){return mt.stop(this),this}resize(t,e){mt.running(this)?this._resizeBeforeDraw={width:t,height:e}:this._resize(t,e)}_resize(t,e){const i=this.options,s=this.canvas,n=i.maintainAspectRatio&&this.aspectRatio,o=this.platform.getMaximumSize(s,t,e,n),a=i.devicePixelRatio||this.platform.getDevicePixelRatio(),r=this.width?"resize":"attach";this.width=o.width,this.height=o.height,this._aspectRatio=this.aspectRatio,pe(this,a,!0)&&(this.notifyPlugins("resize",{size:o}),c(i.onResize,[this,o],this),this.attached&&this._doResize(r)&&this.render())}ensureScalesHaveIDs(){d(this.options.scales||{},((t,e)=>{t.id=e}))}buildOrUpdateScales(){const t=this.options,e=t.scales,i=this.scales,s=Object.keys(i).reduce(((t,e)=>(t[e]=!1,t)),{});let n=[];e&&(n=n.concat(Object.keys(e).map((t=>{const i=e[t],s=Zs(t,i),n="r"===s,o="x"===s;return{options:i,dposition:n?"chartArea":o?"bottom":"left",dtype:n?"radialLinear":o?"category":"linear"}})))),d(n,(e=>{const n=e.options,o=n.id,a=Zs(o,n),l=r(n.type,e.dtype);void 0!==n.position&&hn(n.position,a)===hn(e.dposition)||(n.position=e.dposition),s[o]=!0;let h=null;if(o in i&&i[o].type===l)h=i[o];else{h=new(Us.getScale(l))({id:o,type:l,ctx:this.ctx,chart:this}),i[h.id]=h}h.init(n,t)})),d(s,((t,e)=>{t||delete i[e]})),d(i,(t=>{Zi.configure(this,t,t.options),Zi.addBox(this,t)}))}_updateMetasets(){const t=this._metasets,e=this.data.datasets.length,i=t.length;if(t.sort(((t,e)=>t.index-e.index)),i>e){for(let t=e;te.length&&delete this._stacks,t.forEach(((t,i)=>{0===e.filter((e=>e===t._dataset)).length&&this._destroyDatasetMeta(i)}))}buildOrUpdateControllers(){const t=[],e=this.data.datasets;let i,s;for(this._removeUnreferencedMetasets(),i=0,s=e.length;i{this.getDatasetMeta(e).controller.reset()}),this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(t){const e=this.config;e.update();const i=this._options=e.createResolver(e.chartOptionScopes(),this.getContext()),s=this._animationsDisabled=!i.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),!1===this.notifyPlugins("beforeUpdate",{mode:t,cancelable:!0}))return;const n=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let o=0;for(let t=0,e=this.data.datasets.length;t{t.reset()})),this._updateDatasets(t),this.notifyPlugins("afterUpdate",{mode:t}),this._layers.sort(cn("z","_idx"));const{_active:a,_lastEvent:r}=this;r?this._eventHandler(r,!0):a.length&&this._updateHoverStyles(a,a,!0),this.render()}_updateScales(){d(this.scales,(t=>{Zi.removeBox(this,t)})),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){const t=this.options,e=new Set(Object.keys(this._listeners)),i=new Set(t.events);S(e,i)&&!!this._responsiveListeners===t.responsive||(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){const{_hiddenIndices:t}=this,e=this._getUniformDataChanges()||[];for(const{method:i,start:s,count:n}of e){mn(t,s,"_removeElements"===i?-n:n)}}_getUniformDataChanges(){const t=this._dataChanges;if(!t||!t.length)return;this._dataChanges=[];const e=this.data.datasets.length,i=e=>new Set(t.filter((t=>t[0]===e)).map(((t,e)=>e+","+t.splice(1).join(",")))),s=i(0);for(let t=1;tt.split(","))).map((t=>({method:t[1],start:+t[2],count:+t[3]})))}_updateLayout(t){if(!1===this.notifyPlugins("beforeLayout",{cancelable:!0}))return;Zi.update(this,this.width,this.height,t);const e=this.chartArea,i=e.width<=0||e.height<=0;this._layers=[],d(this.boxes,(t=>{i&&"chartArea"===t.position||(t.configure&&t.configure(),this._layers.push(...t._layers()))}),this),this._layers.forEach(((t,e)=>{t._idx=e})),this.notifyPlugins("afterLayout")}_updateDatasets(t){if(!1!==this.notifyPlugins("beforeDatasetsUpdate",{mode:t,cancelable:!0})){for(let t=0,e=this.data.datasets.length;t=0;--e)this._drawDataset(t[e]);this.notifyPlugins("afterDatasetsDraw")}_drawDataset(t){const e=this.ctx,i=t._clip,s=!i.disabled,n=this.chartArea,o={meta:t,index:t.index,cancelable:!0};!1!==this.notifyPlugins("beforeDatasetDraw",o)&&(s&&Pe(e,{left:!1===i.left?0:n.left-i.left,right:!1===i.right?this.width:n.right+i.right,top:!1===i.top?0:n.top-i.top,bottom:!1===i.bottom?this.height:n.bottom+i.bottom}),t.controller.draw(),s&&De(e),o.cancelable=!1,this.notifyPlugins("afterDatasetDraw",o))}isPointInArea(t){return Se(t,this.chartArea,this._minPadding)}getElementsAtEventForMode(t,e,i,s){const n=Vi.modes[e];return"function"==typeof n?n(this,t,i,s):[]}getDatasetMeta(t){const e=this.data.datasets[t],i=this._metasets;let s=i.filter((t=>t&&t._dataset===e)).pop();return s||(s={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e&&e.order||0,index:t,_dataset:e,_parsed:[],_sorted:!1},i.push(s)),s}getContext(){return this.$context||(this.$context=_i(null,{chart:this,type:"chart"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(t){const e=this.data.datasets[t];if(!e)return!1;const i=this.getDatasetMeta(t);return"boolean"==typeof i.hidden?!i.hidden:!e.hidden}setDatasetVisibility(t,e){this.getDatasetMeta(t).hidden=!e}toggleDataVisibility(t){this._hiddenIndices[t]=!this._hiddenIndices[t]}getDataVisibility(t){return!this._hiddenIndices[t]}_updateVisibility(t,e,i){const s=i?"show":"hide",n=this.getDatasetMeta(t),o=n.controller._resolveAnimations(void 0,s);M(e)?(n.data[e].hidden=!i,this.update()):(this.setDatasetVisibility(t,i),o.update(n,{visible:i}),this.update((e=>e.datasetIndex===t?s:void 0)))}hide(t,e){this._updateVisibility(t,e,!1)}show(t,e){this._updateVisibility(t,e,!0)}_destroyDatasetMeta(t){const e=this._metasets[t];e&&e.controller&&e.controller._destroy(),delete this._metasets[t]}_stop(){let t,e;for(this.stop(),mt.remove(this),t=0,e=this.data.datasets.length;t{e.addEventListener(this,i,s),t[i]=s},s=(t,e,i)=>{t.offsetX=e,t.offsetY=i,this._eventHandler(t)};d(this.options.events,(t=>i(t,s)))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});const t=this._responsiveListeners,e=this.platform,i=(i,s)=>{e.addEventListener(this,i,s),t[i]=s},s=(i,s)=>{t[i]&&(e.removeEventListener(this,i,s),delete t[i])},n=(t,e)=>{this.canvas&&this.resize(t,e)};let o;const a=()=>{s("attach",a),this.attached=!0,this.resize(),i("resize",n),i("detach",o)};o=()=>{this.attached=!1,s("resize",n),this._stop(),this._resize(0,0),i("attach",a)},e.isAttached(this.canvas)?a():o()}unbindEvents(){d(this._listeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._listeners={},d(this._responsiveListeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._responsiveListeners=void 0}updateHoverStyle(t,e,i){const s=i?"set":"remove";let n,o,a,r;for("dataset"===e&&(n=this.getDatasetMeta(t[0].datasetIndex),n.controller["_"+s+"DatasetHoverStyle"]()),a=0,r=t.length;a{const i=this.getDatasetMeta(t);if(!i)throw new Error("No dataset found at index "+t);return{datasetIndex:t,element:i.data[e],index:e}}));!u(i,e)&&(this._active=i,this._lastEvent=null,this._updateHoverStyles(i,e))}notifyPlugins(t,e,i){return this._plugins.notify(this,t,e,i)}_updateHoverStyles(t,e,i){const s=this.options.hover,n=(t,e)=>t.filter((t=>!e.some((e=>t.datasetIndex===e.datasetIndex&&t.index===e.index)))),o=n(e,t),a=i?t:n(t,e);o.length&&this.updateHoverStyle(o,s.mode,!1),a.length&&s.mode&&this.updateHoverStyle(a,s.mode,!0)}_eventHandler(t,e){const i={event:t,replay:e,cancelable:!0,inChartArea:this.isPointInArea(t)},s=e=>(e.options.events||this.options.events).includes(t.native.type);if(!1===this.notifyPlugins("beforeEvent",i,s))return;const n=this._handleEvent(t,e,i.inChartArea);return i.cancelable=!1,this.notifyPlugins("afterEvent",i,s),(n||i.changed)&&this.render(),this}_handleEvent(t,e,i){const{_active:s=[],options:n}=this,o=e,a=this._getActiveElements(t,s,i,o),r=P(t),l=function(t,e,i,s){return i&&"mouseout"!==t.type?s?e:t:null}(t,this._lastEvent,i,r);i&&(this._lastEvent=null,c(n.onHover,[t,a,this],this),r&&c(n.onClick,[t,a,this],this));const h=!u(a,s);return(h||e)&&(this._active=a,this._updateHoverStyles(a,s,e)),this._lastEvent=l,h}_getActiveElements(t,e,i,s){if("mouseout"===t.type)return[];if(!i)return e;const n=this.options.hover;return this.getElementsAtEventForMode(t,n.mode,n,s)}}const xn=()=>d(bn.instances,(t=>t._plugins.invalidate())),_n=!0;function yn(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}Object.defineProperties(bn,{defaults:{enumerable:_n,value:ne},instances:{enumerable:_n,value:gn},overrides:{enumerable:_n,value:te},registry:{enumerable:_n,value:Us},version:{enumerable:_n,value:"3.9.1"},getChart:{enumerable:_n,value:pn},register:{enumerable:_n,value:(...t)=>{Us.add(...t),xn()}},unregister:{enumerable:_n,value:(...t)=>{Us.remove(...t),xn()}}});class vn{constructor(t){this.options=t||{}}init(t){}formats(){return yn()}parse(t,e){return yn()}format(t,e){return yn()}add(t,e,i){return yn()}diff(t,e,i){return yn()}startOf(t,e,i){return yn()}endOf(t,e){return yn()}}vn.override=function(t){Object.assign(vn.prototype,t)};var wn={_date:vn};function Mn(t){const e=t.iScale,i=function(t,e){if(!t._cache.$bar){const i=t.getMatchingVisibleMetas(e);let s=[];for(let e=0,n=i.length;et-e)))}return t._cache.$bar}(e,t.type);let s,n,o,a,r=e._length;const l=()=>{32767!==o&&-32768!==o&&(M(a)&&(r=Math.min(r,Math.abs(o-a)||r)),a=o)};for(s=0,n=i.length;sMath.abs(r)&&(l=r,h=a),e[i.axis]=h,e._custom={barStart:l,barEnd:h,start:n,end:o,min:a,max:r}}(t,e,i,n):e[i.axis]=i.parse(t,n),e}function Sn(t,e,i,s){const n=t.iScale,o=t.vScale,a=n.getLabels(),r=n===o,l=[];let h,c,d,u;for(h=i,c=i+s;ht.x,i="left",s="right"):(e=t.baset.controller.options.grouped)),o=s.options.stacked,a=[],r=t=>{const s=t.controller.getParsed(e),n=s&&s[t.vScale.axis];if(i(n)||isNaN(n))return!0};for(const i of n)if((void 0===e||!r(i))&&((!1===o||-1===a.indexOf(i.stack)||void 0===o&&void 0===i.stack)&&a.push(i.stack),i.index===t))break;return a.length||a.push(void 0),a}_getStackCount(t){return this._getStacks(void 0,t).length}_getStackIndex(t,e,i){const s=this._getStacks(t,i),n=void 0!==e?s.indexOf(e):-1;return-1===n?s.length-1:n}_getRuler(){const t=this.options,e=this._cachedMeta,i=e.iScale,s=[];let n,o;for(n=0,o=e.data.length;n=i?1:-1)}(d,e,a)*o,u===a&&(m-=d/2);const t=e.getPixelForDecimal(0),i=e.getPixelForDecimal(1),s=Math.min(t,i),n=Math.max(t,i);m=Math.max(Math.min(m,n),s),c=m+d}if(m===e.getPixelForValue(a)){const t=z(d)*e.getLineWidthForValue(a)/2;m+=t,d-=t}return{size:d,base:m,head:c,center:c+d/2}}_calculateBarIndexPixels(t,e){const s=e.scale,n=this.options,o=n.skipNull,a=r(n.maxBarThickness,1/0);let l,h;if(e.grouped){const s=o?this._getStackCount(t):e.stackCount,r="flex"===n.barThickness?function(t,e,i,s){const n=e.pixels,o=n[t];let a=t>0?n[t-1]:null,r=t=0;--i)e=Math.max(e,t[i].size(this.resolveDataElementOptions(i))/2);return e>0&&e}getLabelAndValue(t){const e=this._cachedMeta,{xScale:i,yScale:s}=e,n=this.getParsed(t),o=i.getLabelForValue(n.x),a=s.getLabelForValue(n.y),r=n._custom;return{label:e.label,value:"("+o+", "+a+(r?", "+r:"")+")"}}update(t){const e=this._cachedMeta.data;this.updateElements(e,0,e.length,t)}updateElements(t,e,i,s){const n="reset"===s,{iScale:o,vScale:a}=this._cachedMeta,{sharedOptions:r,includeOptions:l}=this._getSharedOptions(e,s),h=o.axis,c=a.axis;for(let d=e;d""}}}};class En extends Ls{constructor(t,e){super(t,e),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(t,e){const i=this.getDataset().data,s=this._cachedMeta;if(!1===this._parsing)s._parsed=i;else{let o,a,r=t=>+i[t];if(n(i[t])){const{key:t="value"}=this._parsing;r=e=>+y(i[e],t)}for(o=t,a=t+e;oG(t,r,l,!0)?1:Math.max(e,e*i,s,s*i),g=(t,e,s)=>G(t,r,l,!0)?-1:Math.min(e,e*i,s,s*i),p=f(0,h,d),m=f(L,c,u),b=g(D,h,d),x=g(D+L,c,u);s=(p-b)/2,n=(m-x)/2,o=-(p+b)/2,a=-(m+x)/2}return{ratioX:s,ratioY:n,offsetX:o,offsetY:a}}(u,d,r),b=(i.width-o)/f,x=(i.height-o)/g,_=Math.max(Math.min(b,x)/2,0),y=h(this.options.radius,_),v=(y-Math.max(y*r,0))/this._getVisibleDatasetWeightTotal();this.offsetX=p*y,this.offsetY=m*y,s.total=this.calculateTotal(),this.outerRadius=y-v*this._getRingWeightOffset(this.index),this.innerRadius=Math.max(this.outerRadius-v*c,0),this.updateElements(n,0,n.length,t)}_circumference(t,e){const i=this.options,s=this._cachedMeta,n=this._getCircumference();return e&&i.animation.animateRotate||!this.chart.getDataVisibility(t)||null===s._parsed[t]||s.data[t].hidden?0:this.calculateCircumference(s._parsed[t]*n/O)}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.chartArea,r=o.options.animation,l=(a.left+a.right)/2,h=(a.top+a.bottom)/2,c=n&&r.animateScale,d=c?0:this.innerRadius,u=c?0:this.outerRadius,{sharedOptions:f,includeOptions:g}=this._getSharedOptions(e,s);let p,m=this._getRotation();for(p=0;p0&&!isNaN(t)?O*(Math.abs(t)/e):0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=li(e._parsed[t],i.options.locale);return{label:s[t]||"",value:n}}getMaxBorderWidth(t){let e=0;const i=this.chart;let s,n,o,a,r;if(!t)for(s=0,n=i.data.datasets.length;s"spacing"!==t,_indexable:t=>"spacing"!==t},En.overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data;if(e.labels.length&&e.datasets.length){const{labels:{pointStyle:i}}=t.legend.options;return e.labels.map(((e,s)=>{const n=t.getDatasetMeta(0).controller.getStyle(s);return{text:e,fillStyle:n.backgroundColor,strokeStyle:n.borderColor,lineWidth:n.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(s),index:s}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}},tooltip:{callbacks:{title:()=>"",label(t){let e=t.label;const i=": "+t.formattedValue;return s(e)?(e=e.slice(),e[0]+=i):e+=i,e}}}}};class Rn extends Ls{initialize(){this.enableOptionSharing=!0,this.supportsDecimation=!0,super.initialize()}update(t){const e=this._cachedMeta,{dataset:i,data:s=[],_dataset:n}=e,o=this.chart._animationsDisabled;let{start:a,count:r}=gt(e,s,o);this._drawStart=a,this._drawCount=r,pt(e)&&(a=0,r=s.length),i._chart=this.chart,i._datasetIndex=this.index,i._decimated=!!n._decimated,i.points=s;const l=this.resolveDatasetElementOptions(t);this.options.showLine||(l.borderWidth=0),l.segment=this.options.segment,this.updateElement(i,void 0,{animated:!o,options:l},t),this.updateElements(s,a,r,t)}updateElements(t,e,s,n){const o="reset"===n,{iScale:a,vScale:r,_stacked:l,_dataset:h}=this._cachedMeta,{sharedOptions:c,includeOptions:d}=this._getSharedOptions(e,n),u=a.axis,f=r.axis,{spanGaps:g,segment:p}=this.options,m=B(g)?g:Number.POSITIVE_INFINITY,b=this.chart._animationsDisabled||o||"none"===n;let x=e>0&&this.getParsed(e-1);for(let g=e;g0&&Math.abs(s[u]-x[u])>m,p&&(_.parsed=s,_.raw=h.data[g]),d&&(_.options=c||this.resolveDataElementOptions(g,e.active?"active":n)),b||this.updateElement(e,g,_,n),x=s}}getMaxOverflow(){const t=this._cachedMeta,e=t.dataset,i=e.options&&e.options.borderWidth||0,s=t.data||[];if(!s.length)return i;const n=s[0].size(this.resolveDataElementOptions(0)),o=s[s.length-1].size(this.resolveDataElementOptions(s.length-1));return Math.max(i,n,o)/2}draw(){const t=this._cachedMeta;t.dataset.updateControlPoints(this.chart.chartArea,t.iScale.axis),super.draw()}}Rn.id="line",Rn.defaults={datasetElementType:"line",dataElementType:"point",showLine:!0,spanGaps:!1},Rn.overrides={scales:{_index_:{type:"category"},_value_:{type:"linear"}}};class In extends Ls{constructor(t,e){super(t,e),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=li(e._parsed[t].r,i.options.locale);return{label:s[t]||"",value:n}}parseObjectData(t,e,i,s){return Ue.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta.data;this._updateRadius(),this.updateElements(e,0,e.length,t)}getMinMax(){const t=this._cachedMeta,e={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY};return t.data.forEach(((t,i)=>{const s=this.getParsed(i).r;!isNaN(s)&&this.chart.getDataVisibility(i)&&(se.max&&(e.max=s))})),e}_updateRadius(){const t=this.chart,e=t.chartArea,i=t.options,s=Math.min(e.right-e.left,e.bottom-e.top),n=Math.max(s/2,0),o=(n-Math.max(i.cutoutPercentage?n/100*i.cutoutPercentage:1,0))/t.getVisibleDatasetCount();this.outerRadius=n-o*this.index,this.innerRadius=this.outerRadius-o}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.options.animation,r=this._cachedMeta.rScale,l=r.xCenter,h=r.yCenter,c=r.getIndexAngle(0)-.5*D;let d,u=c;const f=360/this.countVisibleElements();for(d=0;d{!isNaN(this.getParsed(i).r)&&this.chart.getDataVisibility(i)&&e++})),e}_computeAngle(t,e,i){return this.chart.getDataVisibility(t)?H(this.resolveDataElementOptions(t,e).angle||i):0}}In.id="polarArea",In.defaults={dataElementType:"arc",animation:{animateRotate:!0,animateScale:!0},animations:{numbers:{type:"number",properties:["x","y","startAngle","endAngle","innerRadius","outerRadius"]}},indexAxis:"r",startAngle:0},In.overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data;if(e.labels.length&&e.datasets.length){const{labels:{pointStyle:i}}=t.legend.options;return e.labels.map(((e,s)=>{const n=t.getDatasetMeta(0).controller.getStyle(s);return{text:e,fillStyle:n.backgroundColor,strokeStyle:n.borderColor,lineWidth:n.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(s),index:s}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}},tooltip:{callbacks:{title:()=>"",label:t=>t.chart.data.labels[t.dataIndex]+": "+t.formattedValue}}},scales:{r:{type:"radialLinear",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}};class zn extends En{}zn.id="pie",zn.defaults={cutout:0,rotation:0,circumference:360,radius:"100%"};class Fn extends Ls{getLabelAndValue(t){const e=this._cachedMeta.vScale,i=this.getParsed(t);return{label:e.getLabels()[t],value:""+e.getLabelForValue(i[e.axis])}}parseObjectData(t,e,i,s){return Ue.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta,i=e.dataset,s=e.data||[],n=e.iScale.getLabels();if(i.points=s,"resize"!==t){const e=this.resolveDatasetElementOptions(t);this.options.showLine||(e.borderWidth=0);const o={_loop:!0,_fullLoop:n.length===s.length,options:e};this.updateElement(i,void 0,o,t)}this.updateElements(s,0,s.length,t)}updateElements(t,e,i,s){const n=this._cachedMeta.rScale,o="reset"===s;for(let a=e;a0&&this.getParsed(e-1);for(let c=e;c0&&Math.abs(s[f]-_[f])>b,m&&(p.parsed=s,p.raw=h.data[c]),u&&(p.options=d||this.resolveDataElementOptions(c,e.active?"active":n)),x||this.updateElement(e,c,p,n),_=s}this.updateSharedOptions(d,n,c)}getMaxOverflow(){const t=this._cachedMeta,e=t.data||[];if(!this.options.showLine){let t=0;for(let i=e.length-1;i>=0;--i)t=Math.max(t,e[i].size(this.resolveDataElementOptions(i))/2);return t>0&&t}const i=t.dataset,s=i.options&&i.options.borderWidth||0;if(!e.length)return s;const n=e[0].size(this.resolveDataElementOptions(0)),o=e[e.length-1].size(this.resolveDataElementOptions(e.length-1));return Math.max(s,n,o)/2}}Vn.id="scatter",Vn.defaults={datasetElementType:!1,dataElementType:"point",showLine:!1,fill:!1},Vn.overrides={interaction:{mode:"point"},plugins:{tooltip:{callbacks:{title:()=>"",label:t=>"("+t.label+", "+t.formattedValue+")"}}},scales:{x:{type:"linear"},y:{type:"linear"}}};var Bn=Object.freeze({__proto__:null,BarController:Tn,BubbleController:Ln,DoughnutController:En,LineController:Rn,PolarAreaController:In,PieController:zn,RadarController:Fn,ScatterController:Vn});function Nn(t,e,i){const{startAngle:s,pixelMargin:n,x:o,y:a,outerRadius:r,innerRadius:l}=e;let h=n/r;t.beginPath(),t.arc(o,a,r,s-h,i+h),l>n?(h=n/l,t.arc(o,a,l,i+h,s-h,!0)):t.arc(o,a,n,i+L,s-L),t.closePath(),t.clip()}function Wn(t,e,i,s){const n=ui(t.options.borderRadius,["outerStart","outerEnd","innerStart","innerEnd"]);const o=(i-e)/2,a=Math.min(o,s*e/2),r=t=>{const e=(i-Math.min(o,t))*s/2;return Z(t,0,Math.min(o,e))};return{outerStart:r(n.outerStart),outerEnd:r(n.outerEnd),innerStart:Z(n.innerStart,0,a),innerEnd:Z(n.innerEnd,0,a)}}function jn(t,e,i,s){return{x:i+t*Math.cos(e),y:s+t*Math.sin(e)}}function Hn(t,e,i,s,n,o){const{x:a,y:r,startAngle:l,pixelMargin:h,innerRadius:c}=e,d=Math.max(e.outerRadius+s+i-h,0),u=c>0?c+s+i+h:0;let f=0;const g=n-l;if(s){const t=((c>0?c-s:0)+(d>0?d-s:0))/2;f=(g-(0!==t?g*t/(t+s):g))/2}const p=(g-Math.max(.001,g*d-i/D)/d)/2,m=l+p+f,b=n-p-f,{outerStart:x,outerEnd:_,innerStart:y,innerEnd:v}=Wn(e,u,d,b-m),w=d-x,M=d-_,k=m+x/w,S=b-_/M,P=u+y,O=u+v,C=m+y/P,A=b-v/O;if(t.beginPath(),o){if(t.arc(a,r,d,k,S),_>0){const e=jn(M,S,a,r);t.arc(e.x,e.y,_,S,b+L)}const e=jn(O,b,a,r);if(t.lineTo(e.x,e.y),v>0){const e=jn(O,A,a,r);t.arc(e.x,e.y,v,b+L,A+Math.PI)}if(t.arc(a,r,u,b-v/u,m+y/u,!0),y>0){const e=jn(P,C,a,r);t.arc(e.x,e.y,y,C+Math.PI,m-L)}const i=jn(w,m,a,r);if(t.lineTo(i.x,i.y),x>0){const e=jn(w,k,a,r);t.arc(e.x,e.y,x,m-L,k)}}else{t.moveTo(a,r);const e=Math.cos(k)*d+a,i=Math.sin(k)*d+r;t.lineTo(e,i);const s=Math.cos(S)*d+a,n=Math.sin(S)*d+r;t.lineTo(s,n)}t.closePath()}function $n(t,e,i,s,n,o){const{options:a}=e,{borderWidth:r,borderJoinStyle:l}=a,h="inner"===a.borderAlign;r&&(h?(t.lineWidth=2*r,t.lineJoin=l||"round"):(t.lineWidth=r,t.lineJoin=l||"bevel"),e.fullCircles&&function(t,e,i){const{x:s,y:n,startAngle:o,pixelMargin:a,fullCircles:r}=e,l=Math.max(e.outerRadius-a,0),h=e.innerRadius+a;let c;for(i&&Nn(t,e,o+O),t.beginPath(),t.arc(s,n,h,o+O,o,!0),c=0;c=O||G(n,a,l),g=Q(o,h+u,c+u);return f&&g}getCenterPoint(t){const{x:e,y:i,startAngle:s,endAngle:n,innerRadius:o,outerRadius:a}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius","circumference"],t),{offset:r,spacing:l}=this.options,h=(s+n)/2,c=(o+a+l+r)/2;return{x:e+Math.cos(h)*c,y:i+Math.sin(h)*c}}tooltipPosition(t){return this.getCenterPoint(t)}draw(t){const{options:e,circumference:i}=this,s=(e.offset||0)/2,n=(e.spacing||0)/2,o=e.circular;if(this.pixelMargin="inner"===e.borderAlign?.33:0,this.fullCircles=i>O?Math.floor(i/O):0,0===i||this.innerRadius<0||this.outerRadius<0)return;t.save();let a=0;if(s){a=s/2;const e=(this.startAngle+this.endAngle)/2;t.translate(Math.cos(e)*a,Math.sin(e)*a),this.circumference>=D&&(a=s)}t.fillStyle=e.backgroundColor,t.strokeStyle=e.borderColor;const r=function(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r}=e;let l=e.endAngle;if(o){Hn(t,e,i,s,a+O,n);for(let e=0;er&&o>r;return{count:s,start:l,loop:e.loop,ilen:h(a+(h?r-t:t))%o,_=()=>{f!==g&&(t.lineTo(m,g),t.lineTo(m,f),t.lineTo(m,p))};for(l&&(d=n[x(0)],t.moveTo(d.x,d.y)),c=0;c<=r;++c){if(d=n[x(c)],d.skip)continue;const e=d.x,i=d.y,s=0|e;s===u?(ig&&(g=i),m=(b*m+e)/++b):(_(),t.lineTo(e,i),u=s,b=0,f=g=i),p=i}_()}function Zn(t){const e=t.options,i=e.borderDash&&e.borderDash.length;return!(t._decimated||t._loop||e.tension||"monotone"===e.cubicInterpolationMode||e.stepped||i)?Gn:Kn}Yn.id="arc",Yn.defaults={borderAlign:"center",borderColor:"#fff",borderJoinStyle:void 0,borderRadius:0,borderWidth:2,offset:0,spacing:0,angle:void 0,circular:!0},Yn.defaultRoutes={backgroundColor:"backgroundColor"};const Jn="function"==typeof Path2D;function Qn(t,e,i,s){Jn&&!e.options.segment?function(t,e,i,s){let n=e._path;n||(n=e._path=new Path2D,e.path(n,i,s)&&n.closePath()),Un(t,e.options),t.stroke(n)}(t,e,i,s):function(t,e,i,s){const{segments:n,options:o}=e,a=Zn(e);for(const r of n)Un(t,o,r.style),t.beginPath(),a(t,e,r,{start:i,end:i+s-1})&&t.closePath(),t.stroke()}(t,e,i,s)}class to extends Es{constructor(t){super(),this.animated=!0,this.options=void 0,this._chart=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,this._datasetIndex=void 0,t&&Object.assign(this,t)}updateControlPoints(t,e){const i=this.options;if((i.tension||"monotone"===i.cubicInterpolationMode)&&!i.stepped&&!this._pointsUpdated){const s=i.spanGaps?this._loop:this._fullLoop;Qe(this._points,i,t,s,e),this._pointsUpdated=!0}}set points(t){this._points=t,delete this._segments,delete this._path,this._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=Di(this,this.options.segment))}first(){const t=this.segments,e=this.points;return t.length&&e[t[0].start]}last(){const t=this.segments,e=this.points,i=t.length;return i&&e[t[i-1].end]}interpolate(t,e){const i=this.options,s=t[e],n=this.points,o=Pi(this,{property:e,start:s,end:s});if(!o.length)return;const a=[],r=function(t){return t.stepped?oi:t.tension||"monotone"===t.cubicInterpolationMode?ai:ni}(i);let l,h;for(l=0,h=o.length;l"borderDash"!==t&&"fill"!==t};class io extends Es{constructor(t){super(),this.options=void 0,this.parsed=void 0,this.skip=void 0,this.stop=void 0,t&&Object.assign(this,t)}inRange(t,e,i){const s=this.options,{x:n,y:o}=this.getProps(["x","y"],i);return Math.pow(t-n,2)+Math.pow(e-o,2){uo(t)}))}var go={id:"decimation",defaults:{algorithm:"min-max",enabled:!1},beforeElementsUpdate:(t,e,s)=>{if(!s.enabled)return void fo(t);const n=t.width;t.data.datasets.forEach(((e,o)=>{const{_data:a,indexAxis:r}=e,l=t.getDatasetMeta(o),h=a||e.data;if("y"===bi([r,t.options.indexAxis]))return;if(!l.controller.supportsDecimation)return;const c=t.scales[l.xAxisID];if("linear"!==c.type&&"time"!==c.type)return;if(t.options.parsing)return;let{start:d,count:u}=function(t,e){const i=e.length;let s,n=0;const{iScale:o}=t,{min:a,max:r,minDefined:l,maxDefined:h}=o.getUserBounds();return l&&(n=Z(et(e,o.axis,a).lo,0,i-1)),s=h?Z(et(e,o.axis,r).hi+1,n,i)-n:i-n,{start:n,count:s}}(l,h);if(u<=(s.threshold||4*n))return void uo(e);let f;switch(i(a)&&(e._data=h,delete e.data,Object.defineProperty(e,"data",{configurable:!0,enumerable:!0,get:function(){return this._decimated},set:function(t){this._data=t}})),s.algorithm){case"lttb":f=function(t,e,i,s,n){const o=n.samples||s;if(o>=i)return t.slice(e,e+i);const a=[],r=(i-2)/(o-2);let l=0;const h=e+i-1;let c,d,u,f,g,p=e;for(a[l++]=t[p],c=0;cu&&(u=f,d=t[s],g=s);a[l++]=d,p=g}return a[l++]=t[h],a}(h,d,u,n,s);break;case"min-max":f=function(t,e,s,n){let o,a,r,l,h,c,d,u,f,g,p=0,m=0;const b=[],x=e+s-1,_=t[e].x,y=t[x].x-_;for(o=e;og&&(g=l,d=o),p=(m*p+a.x)/++m;else{const s=o-1;if(!i(c)&&!i(d)){const e=Math.min(c,d),i=Math.max(c,d);e!==u&&e!==s&&b.push({...t[e],x:p}),i!==u&&i!==s&&b.push({...t[i],x:p})}o>0&&s!==u&&b.push(t[s]),b.push(a),h=e,m=0,f=g=l,c=d=u=o}}return b}(h,d,u,n);break;default:throw new Error(`Unsupported decimation algorithm '${s.algorithm}'`)}e._decimated=f}))},destroy(t){fo(t)}};function po(t,e,i,s){if(s)return;let n=e[t],o=i[t];return"angle"===t&&(n=K(n),o=K(o)),{property:t,start:n,end:o}}function mo(t,e,i){for(;e>t;e--){const t=i[e];if(!isNaN(t.x)&&!isNaN(t.y))break}return e}function bo(t,e,i,s){return t&&e?s(t[i],e[i]):t?t[i]:e?e[i]:0}function xo(t,e){let i=[],n=!1;return s(t)?(n=!0,i=t):i=function(t,e){const{x:i=null,y:s=null}=t||{},n=e.points,o=[];return e.segments.forEach((({start:t,end:e})=>{e=mo(t,e,n);const a=n[t],r=n[e];null!==s?(o.push({x:a.x,y:s}),o.push({x:r.x,y:s})):null!==i&&(o.push({x:i,y:a.y}),o.push({x:i,y:r.y}))})),o}(t,e),i.length?new to({points:i,options:{tension:0},_loop:n,_fullLoop:n}):null}function _o(t){return t&&!1!==t.fill}function yo(t,e,i){let s=t[e].fill;const n=[e];let a;if(!i)return s;for(;!1!==s&&-1===n.indexOf(s);){if(!o(s))return s;if(a=t[s],!a)return!1;if(a.visible)return s;n.push(s),s=a.fill}return!1}function vo(t,e,i){const s=function(t){const e=t.options,i=e.fill;let s=r(i&&i.target,i);void 0===s&&(s=!!e.backgroundColor);if(!1===s||null===s)return!1;if(!0===s)return"origin";return s}(t);if(n(s))return!isNaN(s.value)&&s;let a=parseFloat(s);return o(a)&&Math.floor(a)===a?function(t,e,i,s){"-"!==t&&"+"!==t||(i=e+i);if(i===e||i<0||i>=s)return!1;return i}(s[0],e,a,i):["origin","start","end","stack","shape"].indexOf(s)>=0&&s}function wo(t,e,i){const s=[];for(let n=0;n=0;--e){const i=n[e].$filler;i&&(i.line.updateControlPoints(o,i.axis),s&&i.fill&&Po(t.ctx,i,o))}},beforeDatasetsDraw(t,e,i){if("beforeDatasetsDraw"!==i.drawTime)return;const s=t.getSortedVisibleDatasetMetas();for(let e=s.length-1;e>=0;--e){const i=s[e].$filler;_o(i)&&Po(t.ctx,i,t.chartArea)}},beforeDatasetDraw(t,e,i){const s=e.meta.$filler;_o(s)&&"beforeDatasetDraw"===i.drawTime&&Po(t.ctx,s,t.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}};const Lo=(t,e)=>{let{boxHeight:i=e,boxWidth:s=e}=t;return t.usePointStyle&&(i=Math.min(i,e),s=t.pointStyleWidth||Math.min(s,e)),{boxWidth:s,boxHeight:i,itemHeight:Math.max(e,i)}};class Eo extends Es{constructor(t){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e,i){this.maxWidth=t,this.maxHeight=e,this._margins=i,this.setDimensions(),this.buildLabels(),this.fit()}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=this._margins.left,this.right=this.width):(this.height=this.maxHeight,this.top=this._margins.top,this.bottom=this.height)}buildLabels(){const t=this.options.labels||{};let e=c(t.generateLabels,[this.chart],this)||[];t.filter&&(e=e.filter((e=>t.filter(e,this.chart.data)))),t.sort&&(e=e.sort(((e,i)=>t.sort(e,i,this.chart.data)))),this.options.reverse&&e.reverse(),this.legendItems=e}fit(){const{options:t,ctx:e}=this;if(!t.display)return void(this.width=this.height=0);const i=t.labels,s=mi(i.font),n=s.size,o=this._computeTitleHeight(),{boxWidth:a,itemHeight:r}=Lo(i,n);let l,h;e.font=s.string,this.isHorizontal()?(l=this.maxWidth,h=this._fitRows(o,n,a,r)+10):(h=this.maxHeight,l=this._fitCols(o,n,a,r)+10),this.width=Math.min(l,t.maxWidth||this.maxWidth),this.height=Math.min(h,t.maxHeight||this.maxHeight)}_fitRows(t,e,i,s){const{ctx:n,maxWidth:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.lineWidths=[0],h=s+a;let c=t;n.textAlign="left",n.textBaseline="middle";let d=-1,u=-h;return this.legendItems.forEach(((t,f)=>{const g=i+e/2+n.measureText(t.text).width;(0===f||l[l.length-1]+g+2*a>o)&&(c+=h,l[l.length-(f>0?0:1)]=0,u+=h,d++),r[f]={left:0,top:u,row:d,width:g,height:s},l[l.length-1]+=g+a})),c}_fitCols(t,e,i,s){const{ctx:n,maxHeight:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.columnSizes=[],h=o-t;let c=a,d=0,u=0,f=0,g=0;return this.legendItems.forEach(((t,o)=>{const p=i+e/2+n.measureText(t.text).width;o>0&&u+s+2*a>h&&(c+=d+a,l.push({width:d,height:u}),f+=d+a,g++,d=u=0),r[o]={left:f,top:u,col:g,width:p,height:s},d=Math.max(d,p),u+=s+a})),c+=d,l.push({width:d,height:u}),c}adjustHitBoxes(){if(!this.options.display)return;const t=this._computeTitleHeight(),{legendHitBoxes:e,options:{align:i,labels:{padding:s},rtl:n}}=this,o=yi(n,this.left,this.width);if(this.isHorizontal()){let n=0,a=ut(i,this.left+s,this.right-this.lineWidths[n]);for(const r of e)n!==r.row&&(n=r.row,a=ut(i,this.left+s,this.right-this.lineWidths[n])),r.top+=this.top+t+s,r.left=o.leftForLtr(o.x(a),r.width),a+=r.width+s}else{let n=0,a=ut(i,this.top+t+s,this.bottom-this.columnSizes[n].height);for(const r of e)r.col!==n&&(n=r.col,a=ut(i,this.top+t+s,this.bottom-this.columnSizes[n].height)),r.top=a,r.left+=this.left+s,r.left=o.leftForLtr(o.x(r.left),r.width),a+=r.height+s}}isHorizontal(){return"top"===this.options.position||"bottom"===this.options.position}draw(){if(this.options.display){const t=this.ctx;Pe(t,this),this._draw(),De(t)}}_draw(){const{options:t,columnSizes:e,lineWidths:i,ctx:s}=this,{align:n,labels:o}=t,a=ne.color,l=yi(t.rtl,this.left,this.width),h=mi(o.font),{color:c,padding:d}=o,u=h.size,f=u/2;let g;this.drawTitle(),s.textAlign=l.textAlign("left"),s.textBaseline="middle",s.lineWidth=.5,s.font=h.string;const{boxWidth:p,boxHeight:m,itemHeight:b}=Lo(o,u),x=this.isHorizontal(),_=this._computeTitleHeight();g=x?{x:ut(n,this.left+d,this.right-i[0]),y:this.top+d+_,line:0}:{x:this.left+d,y:ut(n,this.top+_+d,this.bottom-e[0].height),line:0},vi(this.ctx,t.textDirection);const y=b+d;this.legendItems.forEach(((v,w)=>{s.strokeStyle=v.fontColor||c,s.fillStyle=v.fontColor||c;const M=s.measureText(v.text).width,k=l.textAlign(v.textAlign||(v.textAlign=o.textAlign)),S=p+f+M;let P=g.x,D=g.y;l.setWidth(this.width),x?w>0&&P+S+d>this.right&&(D=g.y+=y,g.line++,P=g.x=ut(n,this.left+d,this.right-i[g.line])):w>0&&D+y>this.bottom&&(P=g.x=P+e[g.line].width+d,g.line++,D=g.y=ut(n,this.top+_+d,this.bottom-e[g.line].height));!function(t,e,i){if(isNaN(p)||p<=0||isNaN(m)||m<0)return;s.save();const n=r(i.lineWidth,1);if(s.fillStyle=r(i.fillStyle,a),s.lineCap=r(i.lineCap,"butt"),s.lineDashOffset=r(i.lineDashOffset,0),s.lineJoin=r(i.lineJoin,"miter"),s.lineWidth=n,s.strokeStyle=r(i.strokeStyle,a),s.setLineDash(r(i.lineDash,[])),o.usePointStyle){const a={radius:m*Math.SQRT2/2,pointStyle:i.pointStyle,rotation:i.rotation,borderWidth:n},r=l.xPlus(t,p/2);ke(s,a,r,e+f,o.pointStyleWidth&&p)}else{const o=e+Math.max((u-m)/2,0),a=l.leftForLtr(t,p),r=gi(i.borderRadius);s.beginPath(),Object.values(r).some((t=>0!==t))?Le(s,{x:a,y:o,w:p,h:m,radius:r}):s.rect(a,o,p,m),s.fill(),0!==n&&s.stroke()}s.restore()}(l.x(P),D,v),P=ft(k,P+p+f,x?P+S:this.right,t.rtl),function(t,e,i){Ae(s,i.text,t,e+b/2,h,{strikethrough:i.hidden,textAlign:l.textAlign(i.textAlign)})}(l.x(P),D,v),x?g.x+=S+d:g.y+=y})),wi(this.ctx,t.textDirection)}drawTitle(){const t=this.options,e=t.title,i=mi(e.font),s=pi(e.padding);if(!e.display)return;const n=yi(t.rtl,this.left,this.width),o=this.ctx,a=e.position,r=i.size/2,l=s.top+r;let h,c=this.left,d=this.width;if(this.isHorizontal())d=Math.max(...this.lineWidths),h=this.top+l,c=ut(t.align,c,this.right-d);else{const e=this.columnSizes.reduce(((t,e)=>Math.max(t,e.height)),0);h=l+ut(t.align,this.top,this.bottom-e-t.labels.padding-this._computeTitleHeight())}const u=ut(a,c,c+d);o.textAlign=n.textAlign(dt(a)),o.textBaseline="middle",o.strokeStyle=e.color,o.fillStyle=e.color,o.font=i.string,Ae(o,e.text,u,h,i)}_computeTitleHeight(){const t=this.options.title,e=mi(t.font),i=pi(t.padding);return t.display?e.lineHeight+i.height:0}_getLegendItemAt(t,e){let i,s,n;if(Q(t,this.left,this.right)&&Q(e,this.top,this.bottom))for(n=this.legendHitBoxes,i=0;it.chart.options.color,boxWidth:40,padding:10,generateLabels(t){const e=t.data.datasets,{labels:{usePointStyle:i,pointStyle:s,textAlign:n,color:o}}=t.legend.options;return t._getSortedDatasetMetas().map((t=>{const a=t.controller.getStyle(i?0:void 0),r=pi(a.borderWidth);return{text:e[t.index].label,fillStyle:a.backgroundColor,fontColor:o,hidden:!t.visible,lineCap:a.borderCapStyle,lineDash:a.borderDash,lineDashOffset:a.borderDashOffset,lineJoin:a.borderJoinStyle,lineWidth:(r.width+r.height)/4,strokeStyle:a.borderColor,pointStyle:s||a.pointStyle,rotation:a.rotation,textAlign:n||a.textAlign,borderRadius:0,datasetIndex:t.index}}),this)}},title:{color:t=>t.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:t=>!t.startsWith("on"),labels:{_scriptable:t=>!["generateLabels","filter","sort"].includes(t)}}};class Io extends Es{constructor(t){super(),this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e){const i=this.options;if(this.left=0,this.top=0,!i.display)return void(this.width=this.height=this.right=this.bottom=0);this.width=this.right=t,this.height=this.bottom=e;const n=s(i.text)?i.text.length:1;this._padding=pi(i.padding);const o=n*mi(i.font).lineHeight+this._padding.height;this.isHorizontal()?this.height=o:this.width=o}isHorizontal(){const t=this.options.position;return"top"===t||"bottom"===t}_drawArgs(t){const{top:e,left:i,bottom:s,right:n,options:o}=this,a=o.align;let r,l,h,c=0;return this.isHorizontal()?(l=ut(a,i,n),h=e+t,r=n-i):("left"===o.position?(l=i+t,h=ut(a,s,e),c=-.5*D):(l=n-t,h=ut(a,e,s),c=.5*D),r=s-e),{titleX:l,titleY:h,maxWidth:r,rotation:c}}draw(){const t=this.ctx,e=this.options;if(!e.display)return;const i=mi(e.font),s=i.lineHeight/2+this._padding.top,{titleX:n,titleY:o,maxWidth:a,rotation:r}=this._drawArgs(s);Ae(t,e.text,0,0,i,{color:e.color,maxWidth:a,rotation:r,textAlign:dt(e.align),textBaseline:"middle",translation:[n,o]})}}var zo={id:"title",_element:Io,start(t,e,i){!function(t,e){const i=new Io({ctx:t.ctx,options:e,chart:t});Zi.configure(t,i,e),Zi.addBox(t,i),t.titleBlock=i}(t,i)},stop(t){const e=t.titleBlock;Zi.removeBox(t,e),delete t.titleBlock},beforeUpdate(t,e,i){const s=t.titleBlock;Zi.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const Fo=new WeakMap;var Vo={id:"subtitle",start(t,e,i){const s=new Io({ctx:t.ctx,options:i,chart:t});Zi.configure(t,s,i),Zi.addBox(t,s),Fo.set(t,s)},stop(t){Zi.removeBox(t,Fo.get(t)),Fo.delete(t)},beforeUpdate(t,e,i){const s=Fo.get(t);Zi.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"normal"},fullSize:!0,padding:0,position:"top",text:"",weight:1500},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const Bo={average(t){if(!t.length)return!1;let e,i,s=0,n=0,o=0;for(e=0,i=t.length;e-1?t.split("\n"):t}function jo(t,e){const{element:i,datasetIndex:s,index:n}=e,o=t.getDatasetMeta(s).controller,{label:a,value:r}=o.getLabelAndValue(n);return{chart:t,label:a,parsed:o.getParsed(n),raw:t.data.datasets[s].data[n],formattedValue:r,dataset:o.getDataset(),dataIndex:n,datasetIndex:s,element:i}}function Ho(t,e){const i=t.chart.ctx,{body:s,footer:n,title:o}=t,{boxWidth:a,boxHeight:r}=e,l=mi(e.bodyFont),h=mi(e.titleFont),c=mi(e.footerFont),u=o.length,f=n.length,g=s.length,p=pi(e.padding);let m=p.height,b=0,x=s.reduce(((t,e)=>t+e.before.length+e.lines.length+e.after.length),0);if(x+=t.beforeBody.length+t.afterBody.length,u&&(m+=u*h.lineHeight+(u-1)*e.titleSpacing+e.titleMarginBottom),x){m+=g*(e.displayColors?Math.max(r,l.lineHeight):l.lineHeight)+(x-g)*l.lineHeight+(x-1)*e.bodySpacing}f&&(m+=e.footerMarginTop+f*c.lineHeight+(f-1)*e.footerSpacing);let _=0;const y=function(t){b=Math.max(b,i.measureText(t).width+_)};return i.save(),i.font=h.string,d(t.title,y),i.font=l.string,d(t.beforeBody.concat(t.afterBody),y),_=e.displayColors?a+2+e.boxPadding:0,d(s,(t=>{d(t.before,y),d(t.lines,y),d(t.after,y)})),_=0,i.font=c.string,d(t.footer,y),i.restore(),b+=p.width,{width:b,height:m}}function $o(t,e,i,s){const{x:n,width:o}=i,{width:a,chartArea:{left:r,right:l}}=t;let h="center";return"center"===s?h=n<=(r+l)/2?"left":"right":n<=o/2?h="left":n>=a-o/2&&(h="right"),function(t,e,i,s){const{x:n,width:o}=s,a=i.caretSize+i.caretPadding;return"left"===t&&n+o+a>e.width||"right"===t&&n-o-a<0||void 0}(h,t,e,i)&&(h="center"),h}function Yo(t,e,i){const s=i.yAlign||e.yAlign||function(t,e){const{y:i,height:s}=e;return it.height-s/2?"bottom":"center"}(t,i);return{xAlign:i.xAlign||e.xAlign||$o(t,e,i,s),yAlign:s}}function Uo(t,e,i,s){const{caretSize:n,caretPadding:o,cornerRadius:a}=t,{xAlign:r,yAlign:l}=i,h=n+o,{topLeft:c,topRight:d,bottomLeft:u,bottomRight:f}=gi(a);let g=function(t,e){let{x:i,width:s}=t;return"right"===e?i-=s:"center"===e&&(i-=s/2),i}(e,r);const p=function(t,e,i){let{y:s,height:n}=t;return"top"===e?s+=i:s-="bottom"===e?n+i:n/2,s}(e,l,h);return"center"===l?"left"===r?g+=h:"right"===r&&(g-=h):"left"===r?g-=Math.max(c,u)+n:"right"===r&&(g+=Math.max(d,f)+n),{x:Z(g,0,s.width-e.width),y:Z(p,0,s.height-e.height)}}function Xo(t,e,i){const s=pi(i.padding);return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-s.right:t.x+s.left}function qo(t){return No([],Wo(t))}function Ko(t,e){const i=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return i?t.override(i):t}class Go extends Es{constructor(t){super(),this.opacity=0,this._active=[],this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.chart=t.chart||t._chart,this._chart=this.chart,this.options=t.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(t){this.options=t,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){const t=this._cachedAnimations;if(t)return t;const e=this.chart,i=this.options.setContext(this.getContext()),s=i.enabled&&e.options.animation&&i.animations,n=new ys(this.chart,s);return s._cacheable&&(this._cachedAnimations=Object.freeze(n)),n}getContext(){return this.$context||(this.$context=(t=this.chart.getContext(),e=this,i=this._tooltipItems,_i(t,{tooltip:e,tooltipItems:i,type:"tooltip"})));var t,e,i}getTitle(t,e){const{callbacks:i}=e,s=i.beforeTitle.apply(this,[t]),n=i.title.apply(this,[t]),o=i.afterTitle.apply(this,[t]);let a=[];return a=No(a,Wo(s)),a=No(a,Wo(n)),a=No(a,Wo(o)),a}getBeforeBody(t,e){return qo(e.callbacks.beforeBody.apply(this,[t]))}getBody(t,e){const{callbacks:i}=e,s=[];return d(t,(t=>{const e={before:[],lines:[],after:[]},n=Ko(i,t);No(e.before,Wo(n.beforeLabel.call(this,t))),No(e.lines,n.label.call(this,t)),No(e.after,Wo(n.afterLabel.call(this,t))),s.push(e)})),s}getAfterBody(t,e){return qo(e.callbacks.afterBody.apply(this,[t]))}getFooter(t,e){const{callbacks:i}=e,s=i.beforeFooter.apply(this,[t]),n=i.footer.apply(this,[t]),o=i.afterFooter.apply(this,[t]);let a=[];return a=No(a,Wo(s)),a=No(a,Wo(n)),a=No(a,Wo(o)),a}_createItems(t){const e=this._active,i=this.chart.data,s=[],n=[],o=[];let a,r,l=[];for(a=0,r=e.length;at.filter(e,s,n,i)))),t.itemSort&&(l=l.sort(((e,s)=>t.itemSort(e,s,i)))),d(l,(e=>{const i=Ko(t.callbacks,e);s.push(i.labelColor.call(this,e)),n.push(i.labelPointStyle.call(this,e)),o.push(i.labelTextColor.call(this,e))})),this.labelColors=s,this.labelPointStyles=n,this.labelTextColors=o,this.dataPoints=l,l}update(t,e){const i=this.options.setContext(this.getContext()),s=this._active;let n,o=[];if(s.length){const t=Bo[i.position].call(this,s,this._eventPosition);o=this._createItems(i),this.title=this.getTitle(o,i),this.beforeBody=this.getBeforeBody(o,i),this.body=this.getBody(o,i),this.afterBody=this.getAfterBody(o,i),this.footer=this.getFooter(o,i);const e=this._size=Ho(this,i),a=Object.assign({},t,e),r=Yo(this.chart,i,a),l=Uo(i,a,r,this.chart);this.xAlign=r.xAlign,this.yAlign=r.yAlign,n={opacity:1,x:l.x,y:l.y,width:e.width,height:e.height,caretX:t.x,caretY:t.y}}else 0!==this.opacity&&(n={opacity:0});this._tooltipItems=o,this.$context=void 0,n&&this._resolveAnimations().update(this,n),t&&i.external&&i.external.call(this,{chart:this.chart,tooltip:this,replay:e})}drawCaret(t,e,i,s){const n=this.getCaretPosition(t,i,s);e.lineTo(n.x1,n.y1),e.lineTo(n.x2,n.y2),e.lineTo(n.x3,n.y3)}getCaretPosition(t,e,i){const{xAlign:s,yAlign:n}=this,{caretSize:o,cornerRadius:a}=i,{topLeft:r,topRight:l,bottomLeft:h,bottomRight:c}=gi(a),{x:d,y:u}=t,{width:f,height:g}=e;let p,m,b,x,_,y;return"center"===n?(_=u+g/2,"left"===s?(p=d,m=p-o,x=_+o,y=_-o):(p=d+f,m=p+o,x=_-o,y=_+o),b=p):(m="left"===s?d+Math.max(r,h)+o:"right"===s?d+f-Math.max(l,c)-o:this.caretX,"top"===n?(x=u,_=x-o,p=m-o,b=m+o):(x=u+g,_=x+o,p=m+o,b=m-o),y=x),{x1:p,x2:m,x3:b,y1:x,y2:_,y3:y}}drawTitle(t,e,i){const s=this.title,n=s.length;let o,a,r;if(n){const l=yi(i.rtl,this.x,this.width);for(t.x=Xo(this,i.titleAlign,i),e.textAlign=l.textAlign(i.titleAlign),e.textBaseline="middle",o=mi(i.titleFont),a=i.titleSpacing,e.fillStyle=i.titleColor,e.font=o.string,r=0;r0!==t))?(t.beginPath(),t.fillStyle=o.multiKeyBackground,Le(t,{x:e,y:p,w:h,h:l,radius:r}),t.fill(),t.stroke(),t.fillStyle=a.backgroundColor,t.beginPath(),Le(t,{x:i,y:p+1,w:h-2,h:l-2,radius:r}),t.fill()):(t.fillStyle=o.multiKeyBackground,t.fillRect(e,p,h,l),t.strokeRect(e,p,h,l),t.fillStyle=a.backgroundColor,t.fillRect(i,p+1,h-2,l-2))}t.fillStyle=this.labelTextColors[i]}drawBody(t,e,i){const{body:s}=this,{bodySpacing:n,bodyAlign:o,displayColors:a,boxHeight:r,boxWidth:l,boxPadding:h}=i,c=mi(i.bodyFont);let u=c.lineHeight,f=0;const g=yi(i.rtl,this.x,this.width),p=function(i){e.fillText(i,g.x(t.x+f),t.y+u/2),t.y+=u+n},m=g.textAlign(o);let b,x,_,y,v,w,M;for(e.textAlign=o,e.textBaseline="middle",e.font=c.string,t.x=Xo(this,m,i),e.fillStyle=i.bodyColor,d(this.beforeBody,p),f=a&&"right"!==m?"center"===o?l/2+h:l+2+h:0,y=0,w=s.length;y0&&e.stroke()}_updateAnimationTarget(t){const e=this.chart,i=this.$animations,s=i&&i.x,n=i&&i.y;if(s||n){const i=Bo[t.position].call(this,this._active,this._eventPosition);if(!i)return;const o=this._size=Ho(this,t),a=Object.assign({},i,this._size),r=Yo(e,t,a),l=Uo(t,a,r,e);s._to===l.x&&n._to===l.y||(this.xAlign=r.xAlign,this.yAlign=r.yAlign,this.width=o.width,this.height=o.height,this.caretX=i.x,this.caretY=i.y,this._resolveAnimations().update(this,l))}}_willRender(){return!!this.opacity}draw(t){const e=this.options.setContext(this.getContext());let i=this.opacity;if(!i)return;this._updateAnimationTarget(e);const s={width:this.width,height:this.height},n={x:this.x,y:this.y};i=Math.abs(i)<.001?0:i;const o=pi(e.padding),a=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;e.enabled&&a&&(t.save(),t.globalAlpha=i,this.drawBackground(n,t,s,e),vi(t,e.textDirection),n.y+=o.top,this.drawTitle(n,t,e),this.drawBody(n,t,e),this.drawFooter(n,t,e),wi(t,e.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,e){const i=this._active,s=t.map((({datasetIndex:t,index:e})=>{const i=this.chart.getDatasetMeta(t);if(!i)throw new Error("Cannot find a dataset at index "+t);return{datasetIndex:t,element:i.data[e],index:e}})),n=!u(i,s),o=this._positionChanged(s,e);(n||o)&&(this._active=s,this._eventPosition=e,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(t,e,i=!0){if(e&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;const s=this.options,n=this._active||[],o=this._getActiveElements(t,n,e,i),a=this._positionChanged(o,t),r=e||!u(o,n)||a;return r&&(this._active=o,(s.enabled||s.external)&&(this._eventPosition={x:t.x,y:t.y},this.update(!0,e))),r}_getActiveElements(t,e,i,s){const n=this.options;if("mouseout"===t.type)return[];if(!s)return e;const o=this.chart.getElementsAtEventForMode(t,n.mode,n,i);return n.reverse&&o.reverse(),o}_positionChanged(t,e){const{caretX:i,caretY:s,options:n}=this,o=Bo[n.position].call(this,t,e);return!1!==o&&(i!==o.x||s!==o.y)}}Go.positioners=Bo;var Zo={id:"tooltip",_element:Go,positioners:Bo,afterInit(t,e,i){i&&(t.tooltip=new Go({chart:t,options:i}))},beforeUpdate(t,e,i){t.tooltip&&t.tooltip.initialize(i)},reset(t,e,i){t.tooltip&&t.tooltip.initialize(i)},afterDraw(t){const e=t.tooltip;if(e&&e._willRender()){const i={tooltip:e};if(!1===t.notifyPlugins("beforeTooltipDraw",i))return;e.draw(t.ctx),t.notifyPlugins("afterTooltipDraw",i)}},afterEvent(t,e){if(t.tooltip){const i=e.replay;t.tooltip.handleEvent(e.event,i,e.inChartArea)&&(e.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(t,e)=>e.bodyFont.size,boxWidth:(t,e)=>e.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:{beforeTitle:t,title(t){if(t.length>0){const e=t[0],i=e.chart.data.labels,s=i?i.length:0;if(this&&this.options&&"dataset"===this.options.mode)return e.dataset.label||"";if(e.label)return e.label;if(s>0&&e.dataIndex"filter"!==t&&"itemSort"!==t&&"external"!==t,_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]},Jo=Object.freeze({__proto__:null,Decimation:go,Filler:To,Legend:Ro,SubTitle:Vo,Title:zo,Tooltip:Zo});function Qo(t,e,i,s){const n=t.indexOf(e);if(-1===n)return((t,e,i,s)=>("string"==typeof e?(i=t.push(e)-1,s.unshift({index:i,label:e})):isNaN(e)&&(i=null),i))(t,e,i,s);return n!==t.lastIndexOf(e)?i:n}class ta extends $s{constructor(t){super(t),this._startValue=void 0,this._valueRange=0,this._addedLabels=[]}init(t){const e=this._addedLabels;if(e.length){const t=this.getLabels();for(const{index:i,label:s}of e)t[i]===s&&t.splice(i,1);this._addedLabels=[]}super.init(t)}parse(t,e){if(i(t))return null;const s=this.getLabels();return((t,e)=>null===t?null:Z(Math.round(t),0,e))(e=isFinite(e)&&s[e]===t?e:Qo(s,t,r(e,t),this._addedLabels),s.length-1)}determineDataLimits(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let{min:i,max:s}=this.getMinMax(!0);"ticks"===this.options.bounds&&(t||(i=0),e||(s=this.getLabels().length-1)),this.min=i,this.max=s}buildTicks(){const t=this.min,e=this.max,i=this.options.offset,s=[];let n=this.getLabels();n=0===t&&e===n.length-1?n:n.slice(t,e+1),this._valueRange=Math.max(n.length-(i?0:1),1),this._startValue=this.min-(i?.5:0);for(let i=t;i<=e;i++)s.push({value:i});return s}getLabelForValue(t){const e=this.getLabels();return t>=0&&te.length-1?null:this.getPixelForValue(e[t].value)}getValueForPixel(t){return Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange)}getBasePixel(){return this.bottom}}function ea(t,e,{horizontal:i,minRotation:s}){const n=H(s),o=(i?Math.sin(n):Math.cos(n))||.001,a=.75*e*(""+t).length;return Math.min(e/o,a)}ta.id="category",ta.defaults={ticks:{callback:ta.prototype.getLabelForValue}};class ia extends $s{constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._endValue=void 0,this._valueRange=0}parse(t,e){return i(t)||("number"==typeof t||t instanceof Number)&&!isFinite(+t)?null:+t}handleTickRangeOptions(){const{beginAtZero:t}=this.options,{minDefined:e,maxDefined:i}=this.getUserBounds();let{min:s,max:n}=this;const o=t=>s=e?s:t,a=t=>n=i?n:t;if(t){const t=z(s),e=z(n);t<0&&e<0?a(0):t>0&&e>0&&o(0)}if(s===n){let e=1;(n>=Number.MAX_SAFE_INTEGER||s<=Number.MIN_SAFE_INTEGER)&&(e=Math.abs(.05*n)),a(n+e),t||o(s-e)}this.min=s,this.max=n}getTickLimit(){const t=this.options.ticks;let e,{maxTicksLimit:i,stepSize:s}=t;return s?(e=Math.ceil(this.max/s)-Math.floor(this.min/s)+1,e>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${s} would result generating up to ${e} ticks. Limiting to 1000.`),e=1e3)):(e=this.computeTickLimit(),i=i||11),i&&(e=Math.min(i,e)),e}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const t=this.options,e=t.ticks;let s=this.getTickLimit();s=Math.max(2,s);const n=function(t,e){const s=[],{bounds:n,step:o,min:a,max:r,precision:l,count:h,maxTicks:c,maxDigits:d,includeBounds:u}=t,f=o||1,g=c-1,{min:p,max:m}=e,b=!i(a),x=!i(r),_=!i(h),y=(m-p)/(d+1);let v,w,M,k,S=F((m-p)/g/f)*f;if(S<1e-14&&!b&&!x)return[{value:p},{value:m}];k=Math.ceil(m/S)-Math.floor(p/S),k>g&&(S=F(k*S/g/f)*f),i(l)||(v=Math.pow(10,l),S=Math.ceil(S*v)/v),"ticks"===n?(w=Math.floor(p/S)*S,M=Math.ceil(m/S)*S):(w=p,M=m),b&&x&&o&&W((r-a)/o,S/1e3)?(k=Math.round(Math.min((r-a)/S,c)),S=(r-a)/k,w=a,M=r):_?(w=b?a:w,M=x?r:M,k=h-1,S=(M-w)/k):(k=(M-w)/S,k=N(k,Math.round(k),S/1e3)?Math.round(k):Math.ceil(k));const P=Math.max(Y(S),Y(w));v=Math.pow(10,i(l)?P:l),w=Math.round(w*v)/v,M=Math.round(M*v)/v;let D=0;for(b&&(u&&w!==a?(s.push({value:a}),w0?i:null;this._zero=!0}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=o(t)?Math.max(0,t):null,this.max=o(e)?Math.max(0,e):null,this.options.beginAtZero&&(this._zero=!0),this.handleTickRangeOptions()}handleTickRangeOptions(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let i=this.min,s=this.max;const n=e=>i=t?i:e,o=t=>s=e?s:t,a=(t,e)=>Math.pow(10,Math.floor(I(t))+e);i===s&&(i<=0?(n(1),o(10)):(n(a(i,-1)),o(a(s,1)))),i<=0&&n(a(s,-1)),s<=0&&o(a(i,1)),this._zero&&this.min!==this._suggestedMin&&i===a(this.min,0)&&n(a(i,-1)),this.min=i,this.max=s}buildTicks(){const t=this.options,e=function(t,e){const i=Math.floor(I(e.max)),s=Math.ceil(e.max/Math.pow(10,i)),n=[];let o=a(t.min,Math.pow(10,Math.floor(I(e.min)))),r=Math.floor(I(o)),l=Math.floor(o/Math.pow(10,r)),h=r<0?Math.pow(10,Math.abs(r)):1;do{n.push({value:o,major:na(o)}),++l,10===l&&(l=1,++r,h=r>=0?1:h),o=Math.round(l*Math.pow(10,r)*h)/h}while(rn?{start:e-i,end:e}:{start:e,end:e+i}}function la(t){const e={l:t.left+t._padding.left,r:t.right-t._padding.right,t:t.top+t._padding.top,b:t.bottom-t._padding.bottom},i=Object.assign({},e),n=[],o=[],a=t._pointLabels.length,r=t.options.pointLabels,l=r.centerPointLabels?D/a:0;for(let u=0;ue.r&&(r=(s.end-e.r)/o,t.r=Math.max(t.r,e.r+r)),n.starte.b&&(l=(n.end-e.b)/a,t.b=Math.max(t.b,e.b+l))}function ca(t){return 0===t||180===t?"center":t<180?"left":"right"}function da(t,e,i){return"right"===i?t-=e:"center"===i&&(t-=e/2),t}function ua(t,e,i){return 90===i||270===i?t-=e/2:(i>270||i<90)&&(t-=e),t}function fa(t,e,i,s){const{ctx:n}=t;if(i)n.arc(t.xCenter,t.yCenter,e,0,O);else{let i=t.getPointPosition(0,e);n.moveTo(i.x,i.y);for(let o=1;o{const i=c(this.options.pointLabels.callback,[t,e],this);return i||0===i?i:""})).filter(((t,e)=>this.chart.getDataVisibility(e)))}fit(){const t=this.options;t.display&&t.pointLabels.display?la(this):this.setCenterPoint(0,0,0,0)}setCenterPoint(t,e,i,s){this.xCenter+=Math.floor((t-e)/2),this.yCenter+=Math.floor((i-s)/2),this.drawingArea-=Math.min(this.drawingArea/2,Math.max(t,e,i,s))}getIndexAngle(t){return K(t*(O/(this._pointLabels.length||1))+H(this.options.startAngle||0))}getDistanceFromCenterForValue(t){if(i(t))return NaN;const e=this.drawingArea/(this.max-this.min);return this.options.reverse?(this.max-t)*e:(t-this.min)*e}getValueForDistanceFromCenter(t){if(i(t))return NaN;const e=t/(this.drawingArea/(this.max-this.min));return this.options.reverse?this.max-e:this.min+e}getPointLabelContext(t){const e=this._pointLabels||[];if(t>=0&&t=0;o--){const e=n.setContext(t.getPointLabelContext(o)),a=mi(e.font),{x:r,y:l,textAlign:h,left:c,top:d,right:u,bottom:f}=t._pointLabelItems[o],{backdropColor:g}=e;if(!i(g)){const t=gi(e.borderRadius),i=pi(e.backdropPadding);s.fillStyle=g;const n=c-i.left,o=d-i.top,a=u-c+i.width,r=f-d+i.height;Object.values(t).some((t=>0!==t))?(s.beginPath(),Le(s,{x:n,y:o,w:a,h:r,radius:t}),s.fill()):s.fillRect(n,o,a,r)}Ae(s,t._pointLabels[o],r,l+a.lineHeight/2,a,{color:e.color,textAlign:h,textBaseline:"middle"})}}(this,o),n.display&&this.ticks.forEach(((t,e)=>{if(0!==e){r=this.getDistanceFromCenterForValue(t.value);!function(t,e,i,s){const n=t.ctx,o=e.circular,{color:a,lineWidth:r}=e;!o&&!s||!a||!r||i<0||(n.save(),n.strokeStyle=a,n.lineWidth=r,n.setLineDash(e.borderDash),n.lineDashOffset=e.borderDashOffset,n.beginPath(),fa(t,i,o,s),n.closePath(),n.stroke(),n.restore())}(this,n.setContext(this.getContext(e-1)),r,o)}})),s.display){for(t.save(),a=o-1;a>=0;a--){const i=s.setContext(this.getPointLabelContext(a)),{color:n,lineWidth:o}=i;o&&n&&(t.lineWidth=o,t.strokeStyle=n,t.setLineDash(i.borderDash),t.lineDashOffset=i.borderDashOffset,r=this.getDistanceFromCenterForValue(e.ticks.reverse?this.min:this.max),l=this.getPointPosition(a,r),t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(l.x,l.y),t.stroke())}t.restore()}}drawBorder(){}drawLabels(){const t=this.ctx,e=this.options,i=e.ticks;if(!i.display)return;const s=this.getIndexAngle(0);let n,o;t.save(),t.translate(this.xCenter,this.yCenter),t.rotate(s),t.textAlign="center",t.textBaseline="middle",this.ticks.forEach(((s,a)=>{if(0===a&&!e.reverse)return;const r=i.setContext(this.getContext(a)),l=mi(r.font);if(n=this.getDistanceFromCenterForValue(this.ticks[a].value),r.showLabelBackdrop){t.font=l.string,o=t.measureText(s.label).width,t.fillStyle=r.backdropColor;const e=pi(r.backdropPadding);t.fillRect(-o/2-e.left,-n-l.size/2-e.top,o+e.width,l.size+e.height)}Ae(t,s.label,0,-n,l,{color:r.color})})),t.restore()}drawTitle(){}}ga.id="radialLinear",ga.defaults={display:!0,animate:!0,position:"chartArea",angleLines:{display:!0,lineWidth:1,borderDash:[],borderDashOffset:0},grid:{circular:!1},startAngle:0,ticks:{showLabelBackdrop:!0,callback:Is.formatters.numeric},pointLabels:{backdropColor:void 0,backdropPadding:2,display:!0,font:{size:10},callback:t=>t,padding:5,centerPointLabels:!1}},ga.defaultRoutes={"angleLines.color":"borderColor","pointLabels.color":"color","ticks.color":"color"},ga.descriptors={angleLines:{_fallback:"grid"}};const pa={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},ma=Object.keys(pa);function ba(t,e){return t-e}function xa(t,e){if(i(e))return null;const s=t._adapter,{parser:n,round:a,isoWeekday:r}=t._parseOpts;let l=e;return"function"==typeof n&&(l=n(l)),o(l)||(l="string"==typeof n?s.parse(l,n):s.parse(l)),null===l?null:(a&&(l="week"!==a||!B(r)&&!0!==r?s.startOf(l,a):s.startOf(l,"isoWeek",r)),+l)}function _a(t,e,i,s){const n=ma.length;for(let o=ma.indexOf(t);o=e?i[s]:i[n]]=!0}}else t[e]=!0}function va(t,e,i){const s=[],n={},o=e.length;let a,r;for(a=0;a=0&&(e[l].major=!0);return e}(t,s,n,i):s}class wa extends $s{constructor(t){super(t),this._cache={data:[],labels:[],all:[]},this._unit="day",this._majorUnit=void 0,this._offsets={},this._normalized=!1,this._parseOpts=void 0}init(t,e){const i=t.time||(t.time={}),s=this._adapter=new wn._date(t.adapters.date);s.init(e),b(i.displayFormats,s.formats()),this._parseOpts={parser:i.parser,round:i.round,isoWeekday:i.isoWeekday},super.init(t),this._normalized=e.normalized}parse(t,e){return void 0===t?null:xa(this,t)}beforeLayout(){super.beforeLayout(),this._cache={data:[],labels:[],all:[]}}determineDataLimits(){const t=this.options,e=this._adapter,i=t.time.unit||"day";let{min:s,max:n,minDefined:a,maxDefined:r}=this.getUserBounds();function l(t){a||isNaN(t.min)||(s=Math.min(s,t.min)),r||isNaN(t.max)||(n=Math.max(n,t.max))}a&&r||(l(this._getLabelBounds()),"ticks"===t.bounds&&"labels"===t.ticks.source||l(this.getMinMax(!1))),s=o(s)&&!isNaN(s)?s:+e.startOf(Date.now(),i),n=o(n)&&!isNaN(n)?n:+e.endOf(Date.now(),i)+1,this.min=Math.min(s,n-1),this.max=Math.max(s+1,n)}_getLabelBounds(){const t=this.getLabelTimestamps();let e=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;return t.length&&(e=t[0],i=t[t.length-1]),{min:e,max:i}}buildTicks(){const t=this.options,e=t.time,i=t.ticks,s="labels"===i.source?this.getLabelTimestamps():this._generate();"ticks"===t.bounds&&s.length&&(this.min=this._userMin||s[0],this.max=this._userMax||s[s.length-1]);const n=this.min,o=st(s,n,this.max);return this._unit=e.unit||(i.autoSkip?_a(e.minUnit,this.min,this.max,this._getLabelCapacity(n)):function(t,e,i,s,n){for(let o=ma.length-1;o>=ma.indexOf(i);o--){const i=ma[o];if(pa[i].common&&t._adapter.diff(n,s,i)>=e-1)return i}return ma[i?ma.indexOf(i):0]}(this,o.length,e.minUnit,this.min,this.max)),this._majorUnit=i.major.enabled&&"year"!==this._unit?function(t){for(let e=ma.indexOf(t)+1,i=ma.length;e+t.value)))}initOffsets(t){let e,i,s=0,n=0;this.options.offset&&t.length&&(e=this.getDecimalForValue(t[0]),s=1===t.length?1-e:(this.getDecimalForValue(t[1])-e)/2,i=this.getDecimalForValue(t[t.length-1]),n=1===t.length?i:(i-this.getDecimalForValue(t[t.length-2]))/2);const o=t.length<3?.5:.25;s=Z(s,0,o),n=Z(n,0,o),this._offsets={start:s,end:n,factor:1/(s+1+n)}}_generate(){const t=this._adapter,e=this.min,i=this.max,s=this.options,n=s.time,o=n.unit||_a(n.minUnit,e,i,this._getLabelCapacity(e)),a=r(n.stepSize,1),l="week"===o&&n.isoWeekday,h=B(l)||!0===l,c={};let d,u,f=e;if(h&&(f=+t.startOf(f,"isoWeek",l)),f=+t.startOf(f,h?"day":o),t.diff(i,e,o)>1e5*a)throw new Error(e+" and "+i+" are too far apart with stepSize of "+a+" "+o);const g="data"===s.ticks.source&&this.getDataTimestamps();for(d=f,u=0;dt-e)).map((t=>+t))}getLabelForValue(t){const e=this._adapter,i=this.options.time;return i.tooltipFormat?e.format(t,i.tooltipFormat):e.format(t,i.displayFormats.datetime)}_tickFormatFunction(t,e,i,s){const n=this.options,o=n.time.displayFormats,a=this._unit,r=this._majorUnit,l=a&&o[a],h=r&&o[r],d=i[e],u=r&&h&&d&&d.major,f=this._adapter.format(t,s||(u?h:l)),g=n.ticks.callback;return g?c(g,[f,e,i],this):f}generateTickLabels(t){let e,i,s;for(e=0,i=t.length;e0?a:1}getDataTimestamps(){let t,e,i=this._cache.data||[];if(i.length)return i;const s=this.getMatchingVisibleMetas();if(this._normalized&&s.length)return this._cache.data=s[0].controller.getAllParsedValues(this);for(t=0,e=s.length;t=t[r].pos&&e<=t[l].pos&&({lo:r,hi:l}=et(t,"pos",e)),({pos:s,time:o}=t[r]),({pos:n,time:a}=t[l])):(e>=t[r].time&&e<=t[l].time&&({lo:r,hi:l}=et(t,"time",e)),({time:s,pos:o}=t[r]),({time:n,pos:a}=t[l]));const h=n-s;return h?o+(a-o)*(e-s)/h:o}wa.id="time",wa.defaults={bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{source:"auto",major:{enabled:!1}}};class ka extends wa{constructor(t){super(t),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){const t=this._getTimestampsForTable(),e=this._table=this.buildLookupTable(t);this._minPos=Ma(e,this.min),this._tableRange=Ma(e,this.max)-this._minPos,super.initOffsets(t)}buildLookupTable(t){const{min:e,max:i}=this,s=[],n=[];let o,a,r,l,h;for(o=0,a=t.length;o=e&&l<=i&&s.push(l);if(s.length<2)return[{time:e,pos:0},{time:i,pos:1}];for(o=0,a=s.length;o .row { + display: flex; + flex-direction: row; + width: 100%; + + max-height: 200px; +} + +section.generalStatistics > .row > .col { + flex: 1; + display: flex; + flex-direction: column; + width: 100%; + justify-content: center; + padding: 0 0.5rem; +} + +section.generalStatistics > .row > .col#col-2 > .rowTotals { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + background-color: rgba(220, 220, 220, 0.2); + padding: 0.25rem 2rem; + margin-bottom: 0.5rem; +} + +section.generalStatistics > .row > .col#col-2 .label { + text-align: center; + padding-right: 0.25rem; +} + + +section.generalStatistics > .row > .col#col-2 .value { + font-weight: 700; + font-size: 1.5rem; +} + +section.generalStatistics > .row > .col#col-2 > .rowStatus { + display: flex; + flex-direction: row; +} + +section.generalStatistics > .row > .col#col-2 > .rowStatus > .status { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 0.5rem 1.25rem; + margin-right: 0.5rem; +} + +section.generalStatistics > .row > .col#col-2 > .rowStatus > .status:last-child { + margin-right: 0; +} + +section.generalStatistics > .row > .col#col-2 > .rowStatus > .status > .info { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + width: 100%; +} + +section.generalStatistics > .row > .col#col-2 > .rowStatus > .status#notStartedStatus { + background-color: rgba(239, 83, 80, 0.2); +} + +section.generalStatistics > .row > .col#col-2 > .rowStatus > .status#inProgressStatus { + background-color: rgba(249, 168, 37, 0.2); +} + +section.generalStatistics > .row > .col#col-2 > .rowStatus > .status#finishedStatus { + background-color: rgba(102, 187, 106, 0.2); +} + +section.generalStatistics > .row > .col#col-2 .icon > img { + max-height: 50px; +} + +section.generalStatistics > .row > .col#col-3 > .rowTime { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + background-color: rgba(220, 220, 220, 0.2); + padding: 0.25rem 1rem; + margin-bottom: 0.5rem; +} + +section.generalStatistics > .row > .col#col-3 > .rowTime:last-child { + margin-bottom: 0; +} + +section.generalStatistics > .row > .col#col-3 .value { + font-weight: 700; + font-size: 1.5rem; +} + +/** + * EXERCISE CONFIGURATION SECTION + * STUDENT'S PROGRESS SECTION + */ +.optionsPanel { + display: flex; + flex-direction: column; + margin: 0.5rem 0; +} + +.optionsPanel > * { + width: 100%; + margin-bottom: 0.5rem; +} + +.optionsPanel > *:last-child { + margin-bottom: 0; +} + +.optionsPanel > .checkboxRow { + display: flex; + flex-direction: row; + justify-content: space-around; +} + +.optionsPanel > .checkboxRow > .option { + display: flex; + align-items: center; + flex-direction: column; + text-align: center; +} + +/** + * HELP DROPDOWN PANEL + */ +details { + margin: 0 auto; + margin-bottom: .5rem; + border: 1px solid var(--vscode-button-background); + border-radius: 5px; + overflow: hidden; +} + +summary { + padding: 0.5rem 1rem; + display: block; + padding-left: 1.6rem; + position: relative; + cursor: pointer; +} + +summary:before { + content: ''; + border-width: .3rem; + border-style: solid; + border-color: transparent transparent transparent var(--vscode-button-background); + position: absolute; + top: 0.7rem; + left: 0.75rem; + transform: rotate(0); + transform-origin: .2rem 50%; + transition: .25s transform ease; +} + +details[open] > summary:before { + transform: rotate(90deg); +} + +details > ul { + padding-bottom: 1rem; + margin-bottom: 0; +} + +details > .details-content { + padding: 0.75rem; +} + +details > .details-content > ul { + margin-bottom: 0; +} + + +/** + * TABLE + */ table { border-collapse: collapse; width: 100%; } -body.vscode-light td, -body.vscode-light th, -body.vscode-dark td, -body.vscode-dark th { - border: 1px solid; +th { + border: 0; + border-bottom: 1px solid var(--vscode-button-background); text-align: center; + line-height: 30px; } -body.vscode-high-contrast td, -th { - border: 1px solid; +td { text-align: center; + padding: 0.5rem 0; +} + +tr:nth-child(2n) > td:not(.not-started-cell, .finished-cell, .inprogress-cell) { + background-color: rgba(230, 230, 230, 0.2); } -body.vscode-light td, -body.vscode-dark td { - padding: 0.25rem 0; + +/** + * TABLE - STATUS CELLS + */ +.status-icon-img { + height: 30px; + padding-right: 8px; } +.not-started-cell, +.finished-cell, +.inprogress-cell { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +} .not-started-cell { - background-color: var(--vscode-diffEditor-removedTextBackground); + background-color: rgba(239, 83, 80, 0.2); } .finished-cell { - background-color: var(--vscode-diffEditor-insertedTextBackground); + background-color: rgba(102, 187, 106, 0.2); } -.onprogress-cell { - background-color: rgba(76, 149, 218, 0.644); +.inprogress-cell { + background-color: rgba(249, 168, 37, 0.2); } +/** + * USER INTERACTION ELEMENTS 1 - BUTTONS + */ button { background-color: var(--vscode-button-background); color: var(--vscode-button-foreground); border: none; + padding: 0.25rem 1rem; margin-right: 0.5rem; } @@ -54,89 +316,78 @@ body.vscode-high-contrast button { border: 1px solid var(--vscode-contrastActiveBorder); } -select { - background-color: var(--vscode-dropdown-background); - border: 1px solid var(--vscode-dropdown-border); - color: var(--vscode-foreground); + +/** + * USER INTERACTION ELEMENTS 2 - CHECKBOXES + */ +.control { + display: block; + position: relative; + padding-left: 30px; + margin-bottom: 22px; + cursor: pointer; + font-size: 18px; } -option:hover { - background-color: var(--vscode-selection-background); +.control input { + display: none; } -.reload-options { - border-top: 1px solid; - border-bottom: 1px solid; - border-color: var(--vscode-button-background); - padding: 5px; - margin-top: 2px; - margin-bottom: 10px; - display: flex; - justify-content: space-around; +.checkbox_switch { + position: absolute; + top: 2px; + left: 0; + height: 20px; + width: 20px; + background: #e6e6e6; } -.option { - display: flex; - align-items: center; - flex-direction: column; - text-align: center; +.control:hover input ~ .checkbox_switch { + background: #ccc; } -.switch { - position: relative; - display: inline-block; - width: 30px; - height: 17px; +.control input:checked ~ .checkbox_switch { + background: var(--vscode-button-background); } -.switch input { - opacity: 0; - width: 0; - height: 0; +.control:hover input:not([disabled]):checked ~ .checkbox_switch { + background: var(--vscode-button-hoverBackground); } -.slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - outline: none; - background-color: #ccc; - transition: 0.25s; - border-radius: 17px; +.control input:disabled ~ .checkbox_switch { + background: #e6e6e6; + opacity: 0.6; + pointer-events: none; } -.slider:before { +.checkbox_switch:after { + content: ''; position: absolute; - content: ""; - height: 13px; - width: 13px; - left: 2px; - bottom: 2px; - background-color: white; - -webkit-transition: 0.25s; - transition: 0.25s; - border-radius: 50%; -} - -input:checked + .slider { - background-color: var(--vscode-button-background); + display: none; } -input:focus + .slider { - box-shadow: 0 0 1px var(--vscode-button-background); +.control input:checked ~ .checkbox_switch:after { + display: block; } -input:checked + .slider:before { - transform: translateX(13px); +.checkbox_label .checkbox_switch:after { + left: 8px; + top: 4px; + width: 3px; + height: 8px; + border: solid #fff; + border-width: 0 2px 2px 0; + transform: rotate(45deg); } -table th { - line-height: 30px; +.checkbox_label input:disabled ~ .checkbox_switch:after { + border-color: #7b7b7b; } + +/** + * USER INTERACTION ELEMENTS 3 - SORTER CHEVRON + */ .sorter { width: 1.25rem; height: 1.25rem; @@ -211,15 +462,4 @@ table th { .sorter-ls.active span:last-of-type { transform: rotate(45deg); -} - -.alert-info { - text-align: center; - position: relative; - padding: 1rem; - margin-bottom: 1rem; - color: #055160; - background-color: #cff4fc; - border: 1px solid #b6effb; - border-radius: 0.375rem; -} +} \ No newline at end of file diff --git a/vscode4teaching-extension/resources/dashboard/dashboard.js b/vscode4teaching-extension/resources/dashboard/dashboard.js index d227b295..1e1e9bf0 100644 --- a/vscode4teaching-extension/resources/dashboard/dashboard.js +++ b/vscode4teaching-extension/resources/dashboard/dashboard.js @@ -5,9 +5,14 @@ window.addEventListener("message", (event) => { const message = event.data; switch (message.type) { - case "updateDate": - for (const key in message.update) { - document.getElementById(key).textContent = message.update[key]; + case "updateLastModificationTimes": + for (const key in message.content) { + document.getElementById(key).textContent = message.content[key]; + } + break; + case "updateGeneralStatistics": + for (const key in message.content) { + document.getElementById(`${key}`).textContent = message.content[key]; } break; case "openDone": @@ -65,4 +70,72 @@ value: event.target.checked, }); }); + + if (document.getElementById("publishSolution")) { + document.getElementById("publishSolution").addEventListener("click", (event) => { + vscode.postMessage({ + type: "publishSolution", + value: event.target.checked, + }); + }); + } + + if (document.getElementById("allowEditionAfterSolutionDownloaded")) { + document.getElementById("allowEditionAfterSolutionDownloaded").addEventListener("click", (event) => { + vscode.postMessage({ + type: "allowEditionAfterSolutionDownloaded", + value: event.target.checked, + }); + }); + } + + + const chart = document.getElementById("statusChart"); + new Chart(chart.getContext('2d'), { + type: 'doughnut', + data: { + labels: ["Not Started", "In Progress", "Finished"], + datasets: [ + { + data: [chart.dataset["notstarted"], chart.dataset["inprogress"], chart.dataset["finished"]], + backgroundColor: [ + "rgba(239, 83, 80, 0.2)", + "rgba(249, 168, 37, 0.2)", + "rgba(102, 187, 106, 0.2)" + ], + hoverBackgroundColor: [ + "#EF5350", + "#F9A825", + "#66BB6A" + ], + borderWidth: 1, + hoverBorderWidth: 1, + borderColor: "#FFFFFF", + hoverBorderColor: "#FFFFFF", + hoverOffset: 0, + }, + ] + }, + options: { + animation: false, + responsive: true, + maintainAspectRatio: false, + rotation: -90, + circumference: 180, + cutout: "80%", + scales: { + x: { + display: false + }, + y: { + display: false + } + }, + plugins: { + legend: { + display: false + } + } + } + }); })(); diff --git a/vscode4teaching-extension/resources/dashboard/img/exercise_finished.png b/vscode4teaching-extension/resources/dashboard/img/exercise_finished.png new file mode 100644 index 00000000..2255d52d Binary files /dev/null and b/vscode4teaching-extension/resources/dashboard/img/exercise_finished.png differ diff --git a/vscode4teaching-extension/resources/dashboard/img/exercise_in_progress.png b/vscode4teaching-extension/resources/dashboard/img/exercise_in_progress.png new file mode 100644 index 00000000..6caf416a Binary files /dev/null and b/vscode4teaching-extension/resources/dashboard/img/exercise_in_progress.png differ diff --git a/vscode4teaching-extension/resources/dashboard/img/exercise_not_started.png b/vscode4teaching-extension/resources/dashboard/img/exercise_not_started.png new file mode 100644 index 00000000..78ec434b Binary files /dev/null and b/vscode4teaching-extension/resources/dashboard/img/exercise_not_started.png differ diff --git a/vscode4teaching-extension/resources/dashboard/img/no_solution.png b/vscode4teaching-extension/resources/dashboard/img/no_solution.png new file mode 100644 index 00000000..1dd2ea58 Binary files /dev/null and b/vscode4teaching-extension/resources/dashboard/img/no_solution.png differ diff --git a/vscode4teaching-extension/resources/dashboard/img/solution_not_public.png b/vscode4teaching-extension/resources/dashboard/img/solution_not_public.png new file mode 100644 index 00000000..63cbf044 Binary files /dev/null and b/vscode4teaching-extension/resources/dashboard/img/solution_not_public.png differ diff --git a/vscode4teaching-extension/resources/dashboard/img/solution_public.png b/vscode4teaching-extension/resources/dashboard/img/solution_public.png new file mode 100644 index 00000000..6843beea Binary files /dev/null and b/vscode4teaching-extension/resources/dashboard/img/solution_public.png differ diff --git a/vscode4teaching-extension/resources/exercises_solutions_treeicons/no_solution.png b/vscode4teaching-extension/resources/exercises_solutions_treeicons/no_solution.png new file mode 100644 index 00000000..debe5018 Binary files /dev/null and b/vscode4teaching-extension/resources/exercises_solutions_treeicons/no_solution.png differ diff --git a/vscode4teaching-extension/resources/exercises_solutions_treeicons/solution_not_public.png b/vscode4teaching-extension/resources/exercises_solutions_treeicons/solution_not_public.png new file mode 100644 index 00000000..e19fba29 Binary files /dev/null and b/vscode4teaching-extension/resources/exercises_solutions_treeicons/solution_not_public.png differ diff --git a/vscode4teaching-extension/resources/exercises_solutions_treeicons/solution_public.png b/vscode4teaching-extension/resources/exercises_solutions_treeicons/solution_public.png new file mode 100644 index 00000000..1f34e4d2 Binary files /dev/null and b/vscode4teaching-extension/resources/exercises_solutions_treeicons/solution_public.png differ diff --git a/vscode4teaching-extension/resources/exercises_status_treeicons/exercise_finished.png b/vscode4teaching-extension/resources/exercises_status_treeicons/exercise_finished.png new file mode 100644 index 00000000..19a98df3 Binary files /dev/null and b/vscode4teaching-extension/resources/exercises_status_treeicons/exercise_finished.png differ diff --git a/vscode4teaching-extension/resources/exercises_status_treeicons/exercise_in_progress.png b/vscode4teaching-extension/resources/exercises_status_treeicons/exercise_in_progress.png new file mode 100644 index 00000000..0dcff156 Binary files /dev/null and b/vscode4teaching-extension/resources/exercises_status_treeicons/exercise_in_progress.png differ diff --git a/vscode4teaching-extension/resources/exercises_status_treeicons/exercise_not_started.png b/vscode4teaching-extension/resources/exercises_status_treeicons/exercise_not_started.png new file mode 100644 index 00000000..f6936fa8 Binary files /dev/null and b/vscode4teaching-extension/resources/exercises_status_treeicons/exercise_not_started.png differ diff --git a/vscode4teaching-extension/src/client/APIClient.ts b/vscode4teaching-extension/src/client/APIClient.ts index 03161953..59fb778e 100644 --- a/vscode4teaching-extension/src/client/APIClient.ts +++ b/vscode4teaching-extension/src/client/APIClient.ts @@ -8,6 +8,7 @@ import { CourseEdit } from "../model/serverModel/course/CourseEdit"; import { ManageCourseUsers } from "../model/serverModel/course/ManageCourseUsers"; import { Exercise } from "../model/serverModel/exercise/Exercise"; import { ExerciseEdit } from "../model/serverModel/exercise/ExerciseEdit"; +import { ExerciseStatus } from "../model/serverModel/exercise/ExerciseStatus"; import { ExerciseUserInfo } from "../model/serverModel/exercise/ExerciseUserInfo"; import { FileInfo } from "../model/serverModel/file/FileInfo"; import { User } from "../model/serverModel/user/User"; @@ -207,6 +208,15 @@ class APIClientSingleton { return APIClient.createRequest(options, "Deleting course..."); } + public getExercise(exerciseId: number): AxiosPromise { + const options: AxiosBuildOptions = { + url: "/api/exercises/" + exerciseId, + method: "GET", + responseType: "json" + }; + return APIClient.createRequest(options, "Getting exercise information..."); + } + public addExercises(courseId: number, exercisesData: ExerciseEdit[]): AxiosPromise { const options: AxiosBuildOptions = { url: "/api/v2/courses/" + courseId + "/exercises", @@ -239,6 +249,18 @@ class APIClientSingleton { return APIClient.createRequest(options, "Uploading template...", showNotification ?? true); } + public uploadExerciseSolution(id: number, data: Buffer, showNotification?: boolean): AxiosPromise { + const dataForm = new FormData(); + dataForm.append("file", data, { filename: "solution.zip" }); + const options: AxiosBuildOptions = { + url: "/api/exercises/" + id + "/files/solution", + method: "POST", + responseType: "json", + data: dataForm, + }; + return APIClient.createRequest(options, "Uploading solution...", showNotification ?? true); + } + public deleteExercise(id: number): AxiosPromise { const options: AxiosBuildOptions = { url: "/api/exercises/" + id, @@ -316,13 +338,13 @@ class APIClientSingleton { return APIClient.createRequest(options, "Downloading student files...", true); } - public getTemplate(exerciseId: number): AxiosPromise { + public getExerciseResourceById(exerciseId: number, resourceType: "template" | "solution"): AxiosPromise { const options: AxiosBuildOptions = { - url: "/api/exercises/" + exerciseId + "/files/template", + url: "/api/exercises/" + exerciseId + "/files/" + resourceType, method: "GET", responseType: "arraybuffer", }; - return APIClient.createRequest(options, "Downloading exercise template...", true); + return APIClient.createRequest(options, "Downloading exercise " + resourceType + "...", true); } public getFilesInfo(username: string, exerciseId: number): AxiosPromise { @@ -404,7 +426,7 @@ class APIClientSingleton { return APIClient.createRequest(options, "Fetching exercise info for current user..."); } - public updateExerciseUserInfo(exerciseId: number, status: number, modifiedFiles?: string[]): AxiosPromise { + public updateExerciseUserInfo(exerciseId: number, status: ExerciseStatus.StatusEnum, modifiedFiles?: string[]): AxiosPromise { const options: AxiosBuildOptions = { url: "/api/exercises/" + exerciseId + "/info", method: "PUT", @@ -450,6 +472,7 @@ class APIClientSingleton { * Sets vscode status bar and returns axios promise for given options. * @param options Options from to build axios request * @param statusMessage message to add to the vscode status bar + * @param notification True if notification should be shown to user, false otherwise */ private createRequest(options: AxiosBuildOptions, statusMessage: string, notification: boolean = false): AxiosPromise { const axiosOptions = APIClientSession.buildOptions(options); @@ -512,5 +535,6 @@ class APIClientSingleton { return APIClient.createRequest(options, "Logging in to VS Code 4 Teaching..."); } } + // API Client is a singleton export let APIClient = new APIClientSingleton(); diff --git a/vscode4teaching-extension/src/client/APIClientSession.ts b/vscode4teaching-extension/src/client/APIClientSession.ts index ce8e6bfa..c0e2ac1e 100644 --- a/vscode4teaching-extension/src/client/APIClientSession.ts +++ b/vscode4teaching-extension/src/client/APIClientSession.ts @@ -72,14 +72,13 @@ class APIClientSessionSingleton { baseURL: this.baseUrl, method: options.method, data: options.data, - headers: { - }, + headers: {}, responseType: options.responseType, maxContentLength: Infinity, maxBodyLength: Infinity, }; let timeout; - if (axiosConfig.headers !== undefined){ + if (axiosConfig.headers !== undefined) { if (this.jwtToken) { Object.assign(axiosConfig.headers, { Authorization: "Bearer " + this.jwtToken }); } @@ -103,4 +102,5 @@ class APIClientSessionSingleton { return { axiosOptions: axiosConfig, timeout }; } } + export let APIClientSession = new APIClientSessionSingleton(); diff --git a/vscode4teaching-extension/src/client/CurrentUser.ts b/vscode4teaching-extension/src/client/CurrentUser.ts index fdad1316..8b282378 100644 --- a/vscode4teaching-extension/src/client/CurrentUser.ts +++ b/vscode4teaching-extension/src/client/CurrentUser.ts @@ -1,4 +1,3 @@ - import { Course } from "../model/serverModel/course/Course"; import { User } from "../model/serverModel/user/User"; import { APIClient } from "./APIClient"; diff --git a/vscode4teaching-extension/src/client/WebSocketV4TConnection.ts b/vscode4teaching-extension/src/client/WebSocketV4TConnection.ts index ce774fbc..6ac5a7d1 100644 --- a/vscode4teaching-extension/src/client/WebSocketV4TConnection.ts +++ b/vscode4teaching-extension/src/client/WebSocketV4TConnection.ts @@ -11,6 +11,7 @@ export class WebSocketV4TConnection { constructor(private channel: string, private callback: ((data: any) => void)) { this.connect(this.channel, this.callback); } + public send(data: any, cb?: (err?: Error) => void) { this.ws?.send(data, cb); } @@ -24,7 +25,7 @@ export class WebSocketV4TConnection { const wsURL = APIClientSession.baseUrl.replace("http", "ws"); const startConnectionDate = new Date().getTime(); if (authToken && wsURL) { - this.ws = new WebSocket(`${wsURL}/${channel}?bearer=${authToken}`); + this.ws = new WebSocket(`${ wsURL }/${ channel }?bearer=${ authToken }`); const wsHeartbeat = (websocket: WebSocket) => { v4tLogger.debug("ws ping " + this.channel + ": " + new Date(new Date().getTime() - startConnectionDate)); if (this.wsTimeout) { @@ -36,7 +37,7 @@ export class WebSocketV4TConnection { v4tLogger.warn("Timeout on websocket connection. Trying to reconnect..."); websocket.terminate(); this.connect(channel, callback); - }, 31000); + }, 31000); }; this.ws.on("open", wsHeartbeat); this.ws.on("ping", wsHeartbeat); @@ -59,6 +60,8 @@ export class WebSocketV4TConnection { this.wsPingInterval = global.setInterval(() => { this.ws?.ping(); }, 30000); - } else { v4tLogger.error("Could not connect with websockets"); } + } else { + v4tLogger.error("Could not connect with websockets"); + } } } diff --git a/vscode4teaching-extension/src/components/courses/CoursesTreeProvider.ts b/vscode4teaching-extension/src/components/courses/CoursesTreeProvider.ts index ff3581b9..aec6c13c 100644 --- a/vscode4teaching-extension/src/components/courses/CoursesTreeProvider.ts +++ b/vscode4teaching-extension/src/components/courses/CoursesTreeProvider.ts @@ -1,19 +1,21 @@ +import * as fs from "fs"; +import * as path from "path"; import * as vscode from "vscode"; import { APIClient } from "../../client/APIClient"; import { CurrentUser } from "../../client/CurrentUser"; import { Course, instanceOfCourse } from "../../model/serverModel/course/Course"; import { instanceOfExercise } from "../../model/serverModel/exercise/Exercise"; +import { ExerciseUserInfo } from "../../model/serverModel/exercise/ExerciseUserInfo"; import { ModelUtils } from "../../model/serverModel/ModelUtils"; import { User } from "../../model/serverModel/user/User"; import { UserSignup } from "../../model/serverModel/user/UserSignup"; +import { v4tLogger } from "../../services/LoggerService"; import { FileZipUtil } from "../../utils/FileZipUtil"; import { UserPick } from "./UserPick"; import { V4TBuildItems } from "./V4TItem/V4TBuiltItems"; import { V4TItem } from "./V4TItem/V4TItem"; import { V4TItemType } from "./V4TItem/V4TItemType"; import { Validators } from "./Validators"; -import * as fs from "fs"; -import * as path from "path"; /** * Tree view that lists extension's basic options like: @@ -244,12 +246,12 @@ export class CoursesProvider implements vscode.TreeDataProvider { if (CurrentUser.isLoggedIn()) { // If not logged refresh shouldn't do anything CurrentUser.updateUserInfo() - .then(() => { - CoursesProvider.triggerTreeReload(); - }) - .catch((error) => { - APIClient.handleAxiosError(error); - }); + .then(() => { + CoursesProvider.triggerTreeReload(); + }) + .catch((error) => { + APIClient.handleAxiosError(error); + }); } } @@ -262,115 +264,134 @@ export class CoursesProvider implements vscode.TreeDataProvider { } /** - * Show form for adding an exercise then call client. + * Shows a folder picker and sends to the server the new exercises, including their DB information, the template and, if available, the proposed solution (whose existence is detected by an auxiliary function getTemplateSolutionPaths). + * + * This method handles both the case of uploading a single exercise and the case of uploading multiple exercises. + * * @param item course + * @param multiple true if multiple exercises are being added to course, false otherwise */ - public async addExercise(item: V4TItem) { + public async addExercises(item: V4TItem, multiple: boolean) { if (item.item && instanceOfCourse(item.item)) { - // Get exercise name - const name = await this.getInput("Exercise name", Validators.validateExerciseName); - if (name) { - // Select files to use as template for the exercise - const fileUris = await vscode.window.showOpenDialog({ - canSelectFiles: true, - canSelectFolders: true, - canSelectMany: true, - }); - if (fileUris) { - // Create zip file from files and send them - const course: Course = item.item; + // Stage 1: interaction with user + // User picks a folder using the displayed folder picker. + // This operation returns a vscode Uri object (fileUris) that points to selected directory. + + // A message explaining the folder structure required to execute the multiple upload is displayed. + let ans; + if (multiple) ans = await vscode.window.showInformationMessage("To upload multiple exercises, prepare a directory with a folder for each exercise, each folder including the exercise's corresponding template and solution if wanted. When ready, click 'Accept'.", { title: "Accept" }); + if (!((!multiple) || (multiple && ans && ans.title === "Accept"))) return; + + const fileUris = await vscode.window.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + openLabel: "Select a directory" + }); + if (!fileUris) return; + + // Stage 2: interpretation of contents of selected folder + // At the end of this stage, a list of exercises to be added in the next phase is obtained. + // Each exercise in that list is defined by its name and the paths to its template and, if exists, to its proposed solution. + const fsUri = fileUris[0].fsPath; + const course: Course = item.item; + this.loading = true; + CoursesProvider.triggerTreeReload(); + // URLs of locations of both templates and solutions of exercises (one or more) are retrieved. + let uri: vscode.Uri[] = multiple + ? fs.readdirSync(fsUri, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory()) + .map(dirent => vscode.Uri.parse(path.join(fsUri, dirent.name))) + : [fileUris[0]]; + // Names of the exercises are extracted from the names of the directories, as well as the template and solution paths (if exists). + let exercisesDirectories: { name: string, paths: { template: vscode.Uri; solution?: vscode.Uri } }[] = + uri.map((uri) => ({ + name: uri.fsPath.split(path.sep).slice(-1)[0], + paths: this.getTemplateSolutionPaths(vscode.Uri.parse(uri.fsPath)) + })); + // Unsuccessful responses' control (true if there were any during upload process) + let errorCaught = false; + + // Stage 3: uploading of exercises to server + if (exercisesDirectories.length > 0) { + try { + // 3.1: basic information (name, whether they include a solution or not...) of each exercise is sent to server to be persisted. + const exerciseData = await APIClient.addExercises(course.id, exercisesDirectories.map(ex => ({ + name: ex.name, + includesTeacherSolution: (ex.paths.solution !== undefined), + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false + }))); + + // 3.2: promises are sent to server in order and one at a time due to performance reasons. try { - this.loading = true; - CoursesProvider.triggerTreeReload(); - const addExerciseData = await APIClient.addExercises(course.id, [{ name }]); - try { - // When exercise is createdupload template - const zipContent = await FileZipUtil.getZipFromUris(fileUris); - await APIClient.uploadExerciseTemplate(addExerciseData.data[0].id, zipContent); - } catch (uploadError) { - try { - // If upload fails delete the exercise and show error - await APIClient.deleteExercise(addExerciseData.data[0].id); - APIClient.handleAxiosError(uploadError); - } catch (deleteError) { - APIClient.handleAxiosError(deleteError); + for (const [index, exercise] of exerciseData.data.entries()) { + await APIClient.uploadExerciseTemplate(exercise.id, await FileZipUtil.getZipFromUris([exercisesDirectories[index].paths.template]), false); + + if (exercise.includesTeacherSolution) { + await APIClient.uploadExerciseSolution(exercise.id, await FileZipUtil.getZipFromUris([exercisesDirectories[index].paths.solution!]), false); } - } finally { - this.loading = false; - CoursesProvider.triggerTreeReload(); - vscode.window.showInformationMessage("Exercise added."); } - } catch (error) { - APIClient.handleAxiosError(error); + } catch (uploadError) { + // If any upload process fails, all exercises are deleted from database and error is handled (via errorCaught) + errorCaught = true; + v4tLogger.error(uploadError); + try { + exerciseData.data.forEach(async ex => await APIClient.deleteExercise(ex.id)); + APIClient.handleAxiosError(uploadError); + } catch (deleteError) { + APIClient.handleAxiosError(deleteError); + } + } finally { + // The user is informed of the result of this process, whether it was successful or not. + this.loading = false; + if (errorCaught) { + vscode.window.showErrorMessage("One or more exercises were not properly uploaded."); + } else { + vscode.window.showInformationMessage(multiple + ? exercisesDirectories.length + " exercises were added successfully." + : "The new exercise was added successfully." + ); + } + CoursesProvider.triggerTreeReload(); } + } catch (_) { + errorCaught = true; } } } } /** - * Prepare and send multiple exercises' creation request - * @param item course + * Given a folder associated with an exercise, this auxiliary method determines whether it contains a solution or only a template. + * + * @param exerciseRoute Uri of the mentioned folder. + * @returns An object including specific template path and, if exists, also the specific solution path. */ - public async addMultipleExercises(item: V4TItem) { - if (item.item && instanceOfCourse(item.item)) { - const course = item.item; - // Explain user how to organize their exercises' directory - vscode.window.showInformationMessage("To upload multiple exercises, prepare a directory with a folder for each exercise, each folder including the exercise's corresponding template. When ready, click 'Accept'.", "Accept").then(async (ans) => { - if (ans === "Accept") { - // Ask user to select a directory - // This directory has to contain exercises (1 folder = 1 new exercise) - const parentDirectoryUri = await vscode.window.showOpenDialog({ - canSelectFiles: false, - canSelectFolders: true, - canSelectMany: false, - openLabel: "Select directory", - }); - if (parentDirectoryUri) { - const fsUri = parentDirectoryUri[0].fsPath; - // Get every folder from a selected directory - const exercisesDirectories = fs.readdirSync(fsUri, { withFileTypes: true }).filter((d) => d.isDirectory()); - // Get the number of directories - const availableFolderNumber = exercisesDirectories.length; - // Prepare count of successfully uploaded exercises - let uploadedExercises = 0; - // Unsuccessful responses' control (true if there were any) - let errorCaught = false; - if (exercisesDirectories.length > 1) { - // Exercises are uploaded in batches of 3 exercises - while (!errorCaught && exercisesDirectories.length > 0) { - const exercisesDirChunk = exercisesDirectories.splice(0, 3); - // Collect exercises' names from directories' names - try { - const exerciseData = await APIClient.addExercises(course.id, exercisesDirChunk.map((d) => ({ name: d.name }))); - exerciseData.data.map(async (ex, index) => { - const directoryFiles = fs.readdirSync(fsUri + path.sep + exercisesDirChunk[index].name).map((e) => vscode.Uri.parse(fsUri + path.sep + exercisesDirChunk[index].name + path.sep + e)); - APIClient.uploadExerciseTemplate(ex.id, await FileZipUtil.getZipFromUris(directoryFiles), false) - .then((_) => { - uploadedExercises++; - if (!errorCaught && availableFolderNumber === uploadedExercises) { - vscode.window.showInformationMessage("All exercises were successfully uploaded."); - CoursesProvider.triggerTreeReload(); - item.collapsibleState - return; - } - }) - .catch((_) => (errorCaught = true)); - }); - } catch(e: any) { - errorCaught = true; - } - } - if (errorCaught) { - vscode.window.showErrorMessage("One or more exercises were not properly uploaded."); - } - } else { - vscode.window.showErrorMessage("No exercises have been uploaded since there were not any to upload in the selected folder."); - } + private getTemplateSolutionPaths(exerciseRoute: vscode.Uri): { template: vscode.Uri; solution?: vscode.Uri } { + // To determine if the provided path of an exercise includes its solution, it is required that the directory provided includes only two folders inside: template and solution. + // Otherwise, all the contents of the directory will be entered as the template of the exercise and it will be saved without solution. + + // Check if provided path corresponds to an existing directory + if (fs.lstatSync(exerciseRoute.fsPath).isDirectory()) { + // Read directory contents and check if it contains "template" and "solution" folders + const directoryEntries = fs.readdirSync(exerciseRoute.fsPath, { withFileTypes: true }); + if (directoryEntries.length === 2 + && directoryEntries.every(dirent => dirent.isDirectory()) + && directoryEntries.flatMap(dirent => dirent.name).every(name => name === "template" || name === "solution") + ) { + const templateDir = path.join(exerciseRoute.fsPath, "template"); + const solutionDir = path.join(exerciseRoute.fsPath, "solution"); + // If these directories both contain any file, exercise is saved with its solution + if (fs.readdirSync(templateDir).length > 0 && fs.readdirSync(solutionDir).length > 0) { + return { + template: vscode.Uri.parse(templateDir), + solution: vscode.Uri.parse(solutionDir) } } - }); + } } + return { template: exerciseRoute }; } /** @@ -382,7 +403,12 @@ export class CoursesProvider implements vscode.TreeDataProvider { const name = await this.getInput("Exercise name", Validators.validateExerciseName); if (name) { try { - await APIClient.editExercise(item.item.id, { name }); + await APIClient.editExercise(item.item.id, { + name, + includesTeacherSolution: item.item.includesTeacherSolution, + solutionIsPublic: item.item.solutionIsPublic, + allowEditionAfterSolutionDownloaded: item.item.allowEditionAfterSolutionDownloaded + }); CoursesProvider.triggerTreeReload(item.parent); vscode.window.showInformationMessage("Exercise edited successfully"); } catch (error) { @@ -513,15 +539,29 @@ export class CoursesProvider implements vscode.TreeDataProvider { type = V4TItemType.ExerciseStudent; commandName = "vscode4teaching.getexercisefiles"; } - const exerciseItems = course.exercises.map( - (exercise) => - new V4TItem(exercise.name, type, vscode.TreeItemCollapsibleState.None, element, exercise, { - command: commandName, - title: "Get exercise files", - arguments: [course ? course.name : null, exercise], - }) - ); - return exerciseItems.length > 0 ? exerciseItems : [V4TBuildItems.NO_EXERCISES_ITEM]; + if (course.exercises.length > 0) { + if (ModelUtils.isStudent(CurrentUser.getUserInfo())) { + return await Promise.all(course.exercises.map( + async (exercise) => { + const eui: ExerciseUserInfo = (await APIClient.getExerciseUserInfo(exercise.id)).data; + return new V4TItem(exercise.name, type, vscode.TreeItemCollapsibleState.None, element, exercise, { + command: commandName, + title: "Get exercise files", + arguments: [course ? course.name : null, exercise], + }, eui.status); + } + )); + } else { + return course.exercises.map( + (exercise) => + new V4TItem(exercise.name, type, vscode.TreeItemCollapsibleState.None, element, exercise, { + command: commandName, + title: "Get exercise files", + arguments: [course ? course.name : null, exercise], + }, (Number(exercise.includesTeacherSolution) + Number(exercise.solutionIsPublic))) + ); + } + } } } return [V4TBuildItems.NO_EXERCISES_ITEM]; @@ -533,17 +573,17 @@ export class CoursesProvider implements vscode.TreeDataProvider { private updateUserInfo(): V4TItem[] { this.loading = true; CurrentUser.updateUserInfo() - .then(() => { - // Calls getChildren again, which will go through the else statement in this method (logged in and user info initialized) - CoursesProvider.triggerTreeReload(); - }) - .catch((error) => { - APIClient.handleAxiosError(error); - CoursesProvider.triggerTreeReload(); - }) - .finally(() => { - this.loading = false; - }); + .then(() => { + // Calls getChildren again, which will go through the else statement in this method (logged in and user info initialized) + CoursesProvider.triggerTreeReload(); + }) + .catch((error) => { + APIClient.handleAxiosError(error); + CoursesProvider.triggerTreeReload(); + }) + .finally(() => { + this.loading = false; + }); return []; } @@ -625,7 +665,6 @@ export class CoursesProvider implements vscode.TreeDataProvider { * Converts picked items to id array to call client with client call selected (thenable) * @param showArray picked items * @param item course - * @param thenableFunction thenable to call */ private async manageUsersFromCourse(showArray: UserPick[], item: V4TItem) { if (item.item && instanceOfCourse(item.item)) { diff --git a/vscode4teaching-extension/src/components/courses/UserPick.ts b/vscode4teaching-extension/src/components/courses/UserPick.ts index ad9ec29e..b223319e 100644 --- a/vscode4teaching-extension/src/components/courses/UserPick.ts +++ b/vscode4teaching-extension/src/components/courses/UserPick.ts @@ -8,5 +8,6 @@ export class UserPick implements QuickPickItem { constructor( readonly label: string, readonly user: User, - ) { } + ) { + } } diff --git a/vscode4teaching-extension/src/components/courses/V4TItem/V4TItem.ts b/vscode4teaching-extension/src/components/courses/V4TItem/V4TItem.ts index 6fabf4da..de04b2a0 100644 --- a/vscode4teaching-extension/src/components/courses/V4TItem/V4TItem.ts +++ b/vscode4teaching-extension/src/components/courses/V4TItem/V4TItem.ts @@ -2,6 +2,7 @@ import * as path from "path"; import * as vscode from "vscode"; import { Course } from "../../../model/serverModel/course/Course"; import { Exercise } from "../../../model/serverModel/exercise/Exercise"; +import { ExerciseStatus } from "../../../model/serverModel/exercise/ExerciseStatus"; import { V4TItemType } from "./V4TItemType"; /** @@ -13,6 +14,7 @@ export class V4TItem extends vscode.TreeItem { path.join(__dirname, "..", "resources") : path.join(__dirname, "..", "..", "..", "..", "resources"); + public tooltip = this.getTooltip(); public iconPath = this.getIconPath(); public contextValue = this.type.toString(); @@ -23,36 +25,97 @@ export class V4TItem extends vscode.TreeItem { readonly parent?: V4TItem, readonly item?: Course | Exercise, readonly command?: vscode.Command, + readonly exerciseStatusOrSolution?: ExerciseStatus.StatusEnum | number ) { super(label, collapsibleState); } + private getTooltip() { + if (this.exerciseStatusOrSolution !== undefined) { + // A specific tooltip is generated for exercises when shown to students. + // This tooltip includes the name of the exercise and its status (not started, in progress or finished). + if (this.type === V4TItemType.ExerciseStudent) { + switch (this.exerciseStatusOrSolution) { + case 0: + return this.label + " (not started)"; + case 1: + return this.label + " (finished)"; + case 2: + return this.label + " (in progress)"; + } + } + // A specific tooltip is generated for exercises when shown to teachers also. + // This tooltip includes the name of the exercise and its solution's status. + else if (this.type === V4TItemType.ExerciseTeacher) { + switch (this.exerciseStatusOrSolution) { + case 0: + return this.label + " (no solution included)"; + case 1: + return this.label + " (solution included but not published)"; + case 2: + return this.label + " (solution included and published)"; + } + } + } + return this.label; + } + private getIconPath() { switch (this.type) { case V4TItemType.Login: { - return this.iconPaths("login.png"); + return this.getLightDarkPaths("login.png"); + } + case V4TItemType.ExerciseStudent: { + // If all the necessary information is provided, the exercises displayed to the students in the sidebar will include a specific icon for each one. + // This icon will indicate the status of the exercise (not started, in progress or finished) using specific colors and images. + if (this.exerciseStatusOrSolution !== undefined) { + switch (this.exerciseStatusOrSolution) { + case ExerciseStatus.StatusEnum.NOT_STARTED: + return path.join(this.resourcesPath, "exercises_status_treeicons", "exercise_not_started.png"); + case ExerciseStatus.StatusEnum.FINISHED: + return path.join(this.resourcesPath, "exercises_status_treeicons", "exercise_finished.png"); + case ExerciseStatus.StatusEnum.IN_PROGRESS: + return path.join(this.resourcesPath, "exercises_status_treeicons", "exercise_in_progress.png"); + } + } + return this.getLightDarkPaths("noicon.png"); + } + case V4TItemType.ExerciseTeacher: { + // If all the necessary information is provided, the exercises displayed to the teachers in the sidebar will also include a specific icon for each one. + // This icon will indicate the status of the solution of the exercise (not included, not public or published) using specific colors and images. + if (this.exerciseStatusOrSolution !== undefined) { + switch (this.exerciseStatusOrSolution) { + case 0: + return path.join(this.resourcesPath, "exercises_solutions_treeicons", "no_solution.png"); + case 1: + return path.join(this.resourcesPath, "exercises_solutions_treeicons", "solution_not_public.png"); + case 2: + return path.join(this.resourcesPath, "exercises_solutions_treeicons", "solution_public.png"); + } + } + return this.getLightDarkPaths("noicon.png"); } case V4TItemType.AddCourse: { - return this.iconPaths("add.png"); + return this.getLightDarkPaths("add.png"); } case V4TItemType.GetWithCode: { - return this.iconPaths("link.png"); + return this.getLightDarkPaths("link.png"); } - case V4TItemType.Signup: // fall through case below + case V4TItemType.Signup: case V4TItemType.SignupTeacher: { - return this.iconPaths("add_user.png"); + return this.getLightDarkPaths("add_user.png"); } case V4TItemType.Logout: { - return this.iconPaths("logout.png"); + return this.getLightDarkPaths("logout.png"); } case V4TItemType.NoCourses: // fall through case below case V4TItemType.NoExercises: { - return this.iconPaths("noicon.png"); + return this.getLightDarkPaths("noicon.png"); } } } - private iconPaths(iconPath: string) { + private getLightDarkPaths(iconPath: string) { return { light: path.join(this.resourcesPath, "light", iconPath), dark: path.join(this.resourcesPath, "dark", iconPath), diff --git a/vscode4teaching-extension/src/components/courses/Validators.ts b/vscode4teaching-extension/src/components/courses/Validators.ts index 1b2e403a..5c657727 100644 --- a/vscode4teaching-extension/src/components/courses/Validators.ts +++ b/vscode4teaching-extension/src/components/courses/Validators.ts @@ -40,10 +40,10 @@ export class Validators { if (value.length < 4 || value.length > 50) { return "Username must have between 4 and 50 characters"; } - const regexp = /^(?:(?!template).)+$/; + const regexp = /^(?:(?!(template)|(solution)|(student)).)+$/; const pattern = new RegExp(regexp); if (!pattern.test(value)) { - return "Username is not valid (cannot contain the word template)"; + return `Username is not valid (cannot contain the words "template", "solution" or "student")`; } } diff --git a/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts b/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts index e05cb322..029aa880 100644 --- a/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts +++ b/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts @@ -5,8 +5,10 @@ import { APIClient } from "../../client/APIClient"; import { WebSocketV4TConnection } from "../../client/WebSocketV4TConnection"; import { Course } from "../../model/serverModel/course/Course"; import { Exercise } from "../../model/serverModel/exercise/Exercise"; +import { ExerciseStatus } from "../../model/serverModel/exercise/ExerciseStatus"; import { ExerciseUserInfo } from "../../model/serverModel/exercise/ExerciseUserInfo"; import { v4tLogger } from "../../services/LoggerService"; +import { CoursesProvider } from "../courses/CoursesTreeProvider"; import { OpenQuickPick } from "./OpenQuickPick"; export class DashboardWebview { @@ -19,17 +21,17 @@ export class DashboardWebview { public static show(euis: ExerciseUserInfo[], course: Course, exercise: Exercise, fullMode: boolean) { const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined; - // If we already have a panel, show it. + // If we already have a panel, show it if (DashboardWebview.currentPanel) { DashboardWebview.currentPanel.panel.reveal(column); return; } // Otherwise, create a new panel. - const dashboardName = exercise.name; - const dashboardViewName = course.name + " - " + exercise.name; + const dashboardTitle = exercise.name; + const dashboardDisplayInformation = { h1: course.name, h2: exercise.name }; - const panel = vscode.window.createWebviewPanel(DashboardWebview.viewType, "V4T Dashboard: " + dashboardName, column || vscode.ViewColumn.One, { + const panel = vscode.window.createWebviewPanel(DashboardWebview.viewType, "V4T Dashboard: " + dashboardTitle, column || vscode.ViewColumn.One, { // Enable javascript in the webview enableScripts: true, @@ -37,7 +39,7 @@ export class DashboardWebview { localResourceRoots: [vscode.Uri.file(DashboardWebview.resourcesPath)], }); - DashboardWebview.currentPanel = new DashboardWebview(panel, dashboardName, dashboardViewName, euis, course, exercise, fullMode); + DashboardWebview.currentPanel = new DashboardWebview(panel, dashboardTitle, dashboardDisplayInformation, euis, course, exercise, fullMode); } public static exists(): boolean { @@ -48,165 +50,53 @@ export class DashboardWebview { private ws: WebSocketV4TConnection; - private readonly _dashboardName: string; - private readonly _dashboardViewName: string; + private readonly _dashboardTitle: string; + private readonly _dashboardDisplayInformation: { h1: string, h2: string }; private _euis: ExerciseUserInfo[]; - // private _reloadInterval: NodeJS.Timeout | undefined; - private lastUpdatedInterval: NodeJS.Timeout; + private updateGeneralStatisticsInterval: NodeJS.Timeout; + private updateStudentsProgressInterval: NodeJS.Timeout; + private readonly _course: Course; private _exercise: Exercise; private sortAsc: boolean; private hiddenStudentNames: boolean; private fullMode: boolean; - private constructor(panel: vscode.WebviewPanel, dashboardName: string, dashboardViewName: string, euis: ExerciseUserInfo[], course: Course, exercise: Exercise, fullMode: boolean) { + private constructor(panel: vscode.WebviewPanel, dashboardTitle: string, dashboardDisplayInformation: { h1: string, h2: string }, euis: ExerciseUserInfo[], course: Course, exercise: Exercise, fullMode: boolean) { this.panel = panel; - this._dashboardName = dashboardName; - this._dashboardViewName = dashboardViewName; + this._dashboardTitle = dashboardTitle; + this._dashboardDisplayInformation = dashboardDisplayInformation; this._euis = euis; + this._course = course; this._exercise = exercise; this.sortAsc = false; this.hiddenStudentNames = true; this.fullMode = fullMode; - // Set the webview's initial html content - this.updateHtml(); + // Set the webview's initial HTML content + this.updateHTML(); - // Listen for when the panel is disposed + // Set up listeners + // 1. Listen for panel's disposal // This happens when the user closes the panel or when the panel is closed programatically this.panel.onDidDispose(() => { - global.clearInterval(this.lastUpdatedInterval); - // if (this._reloadInterval !== undefined) { - // global.clearInterval(this._reloadInterval); - // } + global.clearInterval(this.updateGeneralStatisticsInterval); + global.clearInterval(this.updateStudentsProgressInterval); this.ws.close(); this.dispose(); }); + // 2. Listen for attributes' changes // Update the content based on view changes this.panel.onDidChangeViewState((e) => { if (this.panel.visible) { - this.updateHtml(); + this.updateHTML(); } }); - this.panel.webview.onDidReceiveMessage(async (message) => { - switch (message.type) { - case "reload": { - this.reloadData(); - break; - } - // case "changeReloadTime": { - // // reloadTime comes in seconds - // const reloadTime = message.reloadTime; - // if (this._reloadInterval) { - // global.clearInterval(this._reloadInterval); - // this._reloadInterval = undefined; - // } - // if (reloadTime > 0) { - // this._reloadInterval = global.setInterval(() => { - // this.reloadData(); - // }, reloadTime * 1000); - // } - // break; - // } - case "goToWorkspace": { - this.showQuickPick(message.username, course, exercise, message.eui_id) - .then(async (filePath) => { - if (filePath !== undefined) { - const doc1 = await vscode.workspace.openTextDocument(filePath); - await vscode.window.showTextDocument(doc1); - } - this.panel.webview.postMessage({ type: "openDone", username: message.username }); - }) - .catch((err) => { - v4tLogger.error(err); - vscode.window.showErrorMessage(err); - }); - break; - } - case "diff": { - this.showQuickPick(message.username, course, exercise, message.eui_id) - .then(async (filePath) => { - if (filePath !== undefined) { - await vscode.commands.executeCommand("vscode4teaching.diff", filePath); - } - this.panel.webview.postMessage({ type: "openDone", username: message.username }); - }) - .catch((err) => { - v4tLogger.error(err); - vscode.window.showErrorMessage(err); - }); - break; - } - - case "sort": { - this.sortAsc = message.desc; - const weight = this.sortAsc ? 1 : -1; - switch (message.column) { - case "fullName": { - this._euis.sort((a, b) => { - const aname = (a.user.name ? a.user.name : "") + " " + (a.user.lastName ? a.user.lastName : ""); - const bname = (b.user.name ? b.user.name : "") + " " + (b.user.lastName ? b.user.lastName : ""); - - if (aname > bname) { - return -1 * weight; - } else if (aname < bname) { - return 1 * weight; - } else { - return 0; - } - }); - break; - } - case "exerciseFolder": { - this._euis.sort((a, b) => { - const adirectory = "student_" + a.id; - const bdirectory = "student_" + b.id; - if (adirectory > bdirectory) { - return -1 * weight; - } else if (adirectory < bdirectory) { - return 1 * weight; - } else { - return 0; - } - }); - break; - } - case "status": { - this._euis.sort((a, b) => { - if (a.status > b.status) { - return -1 * weight; - } else if (a.status < b.status) { - return 1 * weight; - } else { - return 0; - } - }); - break; - } - case "lastModification": { - this._euis.sort((a, b) => { - if (a.updateDateTime > b.updateDateTime) { - return -1 * weight; - } else if (a.updateDateTime < b.updateDateTime) { - return 1 * weight; - } else { - return 0; - } - }); - } - } - this.updateHtml(); - break; - } + // 3. Listen for incoming messages + this.panel.webview.onDidReceiveMessage(async message => this.handleReceivedMessages(message)); - case "changeVisibilityStudentsNames": { - this.hiddenStudentNames = message.value; - this.updateHtml(); - break; - } - } - }); + // Set up WebSocket connection to backend this.ws = new WebSocketV4TConnection("dashboard-refresh", (dataStringified) => { if (dataStringified) { const { handle } = JSON.parse(dataStringified.data); @@ -215,25 +105,205 @@ export class DashboardWebview { } } }); - // Only used to refresh elapsed times - this.lastUpdatedInterval = global.setInterval(this.updateLastDate.bind(this), 1000); + + // Set up re-generation of elapsed times + this.updateGeneralStatisticsInterval = global.setInterval(this.updateNumberModificationsGeneralStatisticsTimes.bind(this), 1000); + this.updateStudentsProgressInterval = global.setInterval(this.updateStudentsProgressTimes.bind(this), 1000); + } + + private onMessageGoToWorkspace = (message: any) => { + this.showQuickPick(message.username, this._course, this._exercise, message.eui_id) + .then(async (filePath) => { + if (filePath !== undefined) { + const doc1 = await vscode.workspace.openTextDocument(filePath); + await vscode.window.showTextDocument(doc1); + } + this.panel.webview.postMessage({ type: "openDone", username: message.username }); + }) + .catch((err) => { + v4tLogger.error(err); + vscode.window.showErrorMessage(err); + }); + }; + + private onMessageDiff = (message: any) => { + this.showQuickPick(message.username, this._course, this._exercise, message.eui_id) + .then(async (filePath) => { + if (filePath !== undefined) { + await vscode.commands.executeCommand("vscode4teaching.diff", filePath); + } + this.panel.webview.postMessage({ type: "openDone", username: message.username }); + }) + .catch((err) => { + v4tLogger.error(err); + vscode.window.showErrorMessage(err); + }); + }; + + private onMessageSort = (message: any) => { + this.sortAsc = message.desc; + const weight = this.sortAsc ? 1 : -1; + switch (message.column) { + case "fullName": { + this._euis.sort((a, b) => { + const aname = (a.user.name ? a.user.name : "") + " " + (a.user.lastName ? a.user.lastName : ""); + const bname = (b.user.name ? b.user.name : "") + " " + (b.user.lastName ? b.user.lastName : ""); + + if (aname > bname) { + return -1 * weight; + } else if (aname < bname) { + return 1 * weight; + } else { + return 0; + } + }); + break; + } + case "exerciseFolder": { + this._euis.sort((a, b) => { + const adirectory = "student_" + a.id; + const bdirectory = "student_" + b.id; + if (adirectory > bdirectory) { + return -1 * weight; + } else if (adirectory < bdirectory) { + return 1 * weight; + } else { + return 0; + } + }); + break; + } + case "status": { + this._euis.sort((a, b) => { + if (a.status > b.status) { + return -1 * weight; + } else if (a.status < b.status) { + return 1 * weight; + } else { + return 0; + } + }); + break; + } + case "lastModification": { + this._euis.sort((a, b) => { + if (a.updateDateTime > b.updateDateTime) { + return -1 * weight; + } else if (a.updateDateTime < b.updateDateTime) { + return 1 * weight; + } else { + return 0; + } + }); + } + } + this.updateHTML(); + }; + + private onMessagePublishSolution = () => { + if (this._exercise.includesTeacherSolution) { + this._exercise.solutionIsPublic = true; + APIClient.editExercise(this._exercise.id, this._exercise) + .then(ex => { + this._exercise = ex.data; + this.updateHTML(); + CoursesProvider.triggerTreeReload(); + vscode.window.showInformationMessage("The solution was published to students successfully."); + }) + .catch(err => { + APIClient.handleAxiosError(err); + vscode.window.showErrorMessage("The solution could not be published."); + }); + } + }; + + private onMesssageAllowEditionAfterSolutionDownloaded = () => { + if (this._exercise.includesTeacherSolution) { + this._exercise.allowEditionAfterSolutionDownloaded = !this._exercise.allowEditionAfterSolutionDownloaded; + APIClient.editExercise(this._exercise.id, this._exercise) + .then(ex => { + this._exercise = ex.data; + this.updateHTML(); + CoursesProvider.triggerTreeReload(); + vscode.window.showInformationMessage(`Edition after downloading solution was ${ this._exercise.allowEditionAfterSolutionDownloaded ? "enabled" : "disabled" }.\nThis parameter can be changed while solution is not published to students.`); + }) + .catch(err => { + APIClient.handleAxiosError(err); + vscode.window.showErrorMessage("This parameter could not be changed."); + }) + } + }; + + private async handleReceivedMessages(message: any) { + switch (message.type) { + case "reload": { + this.reloadData(); + break; + } + case "goToWorkspace": { + this.onMessageGoToWorkspace(message); + break; + } + case "diff": { + this.onMessageDiff(message); + break; + } + + case "sort": { + this.onMessageSort(message); + break; + } + + case "changeVisibilityStudentsNames": { + this.hiddenStudentNames = message.value; + this.updateHTML(); + break; + } + + case "publishSolution": { + this.onMessagePublishSolution(); + break; + } + + case "allowEditionAfterSolutionDownloaded": { + this.onMesssageAllowEditionAfterSolutionDownloaded(); + break; + } + } } public dispose() { DashboardWebview.currentPanel = undefined; - - // Clean up our resources this.panel.dispose(); } - private updateLastDate() { + + // Periodic modifications + // 1. Number of modifications performed in a period of time (general statistics' section) + private getEuisEditedInTheLastNMinutes(minutes: number): number { + return this._euis.filter(eui => + ((new Date().getTime() - new Date(eui.updateDateTime).getTime()) / 60000) < minutes + ).length; + } + + private updateNumberModificationsGeneralStatisticsTimes() { + const message: { [key: string]: string } = {}; + [5, 30, 60, 120].forEach( + minutes => message[`timeValue${ minutes }`] = this.getEuisEditedInTheLastNMinutes(minutes).toString() + ); + this.panel.webview.postMessage({ type: "updateGeneralStatistics", content: message }); + } + + // 2. Last modification column of student's progress table + private updateStudentsProgressTimes() { const message: { [key: string]: string } = {}; for (const eui of this._euis) { message["user-lastmod-" + eui.user.id] = this.getElapsedTime(eui.updateDateTime); } - this.panel.webview.postMessage({ type: "updateDate", update: message }); + this.panel.webview.postMessage({ type: "updateLastModificationTimes", content: message }); } + private async findLastModifiedFile(folder: vscode.WorkspaceFolder, fileRoute: string): Promise<{ uri: vscode.Uri; relativePath: string }> { if (fileRoute === "null") { return this.findMainFile(folder); @@ -268,34 +338,35 @@ export class DashboardWebview { private reloadData() { APIClient.getAllStudentsExerciseUserInfo(this._exercise.id) - .then((response: AxiosResponse) => { - this._euis = response.data; - this.updateHtml(); - }) - .catch((error) => APIClient.handleAxiosError(error)); + .then((response: AxiosResponse) => { + this._euis = response.data; + this.updateHTML(); + }) + .catch((error) => APIClient.handleAxiosError(error)); } - private updateHtml() { - this.panel.webview.html = this.getHtmlForWebview(); + private updateHTML() { + this.panel.webview.html = this.getHTMLForWebview(); } - private getHtmlForWebview() { - // Local path to main script run in the webview - const scriptPath = vscode.Uri.file(path.join(DashboardWebview.resourcesPath, "dashboard.js")); - // And the uri we use to load this script in the webview - const scriptUri = this.panel.webview.asWebviewUri(scriptPath); + private getHTMLForWebview() { + // Corresponding URI to embed CSS stylesheet in the webview + const cssUri = this.panel.webview.asWebviewUri(vscode.Uri.file(path.join(DashboardWebview.resourcesPath, "dashboard.css"))); - // Local path to styles - const cssPath = vscode.Uri.file(path.join(DashboardWebview.resourcesPath, "dashboard.css")); + // Corresponding URI to embed Dashboard script in the webview + const scriptUri = this.panel.webview.asWebviewUri(vscode.Uri.file(path.join(DashboardWebview.resourcesPath, "dashboard.js"))); - // Styles uri - const cssUri = this.panel.webview.asWebviewUri(cssPath); + // Corresponding URI to embed Chart.js (v3.9.1) script in the webview + const chartJsUri = this.panel.webview.asWebviewUri(vscode.Uri.file(path.join(DashboardWebview.resourcesPath, "chart.js"))); - // Transform EUIs to html table data + // Lambda function to return corresponding URI to images + const imgUri = (imgName: string) => this.panel.webview.asWebviewUri(vscode.Uri.file(path.join(DashboardWebview.resourcesPath, "img", imgName + ".png"))); + + + // Transform EUIs to HTML table rows let rows: string = ""; - // for (const eui of this._euis) { for (const eui of this._euis) { - rows = rows + `\n`; + rows = rows + `\n`; if (!this.hiddenStudentNames) { if (eui.user.name && eui.user.lastName) { @@ -304,108 +375,223 @@ export class DashboardWebview { rows = rows + ""; } } - rows = rows + `student_${eui.id}\n`; + rows = rows + `student_${ eui.id }\n`; switch (eui.status) { - case 0: { - // not started - rows = rows + 'Not started\n'; + case ExerciseStatus.StatusEnum.NOT_STARTED: { + rows = rows + `Not started icon
Not started
\n`; break; } - case 1: { - // finished - rows = rows + 'Finished\n'; + case ExerciseStatus.StatusEnum.FINISHED: { + rows = rows + `Finished icon
Finished
\n`; break; } - case 2: { - // started but not finished - rows = rows + 'On progress\n'; + case ExerciseStatus.StatusEnum.IN_PROGRESS: { + rows = rows + `In progress icon
In progress
\n`; break; } } - rows = rows + ``; - if (this.fullMode){ + rows = rows + `${ this.getElapsedTime(eui.updateDateTime) }\n`; + if (this.fullMode) { rows += `\n`; } - rows = rows + `${this.getElapsedTime(eui.updateDateTime)}\n`; rows = rows + "\n"; } // Use a nonce to whitelist which scripts can be run const nonce = this.getNonce(); + + // Return HTML body return ` - - V4T Dashboard: ${this._dashboardName} - + + V4T Dashboard: ${ this._dashboardTitle } + -

VSCode4Teaching Dashboard

-

${this._dashboardViewName}

-
- ${ - this.fullMode - ? '' - : - `
- Preview mode.
Download exercise to be able to open students' exercises. -
` - } -
-
Hide student's names
- -
-
- ${this._euis.length === 0 ? ` -
Warning: There are no students registered in this course.
+ + ${this.fullMode + ? '' + : `
Preview mode. Some features, such as downloading student files or reviewing them, are disabled and are only accessible in full mode.
` + } +

${this._dashboardDisplayInformation.h1}

+

${this._dashboardDisplayInformation.h2}

+ +
+ + ${ this._euis.length === 0 ? ` +
Warning: There are no students registered in this course. Thus, no further information can be displayed about this exercise.
` : ` -
-
- - - ${ - !this.hiddenStudentNames - ? `` + +
+

General statistics

+
+
+ +
+
+
+
Students in course
+
${ this._euis.length }
+
+
+
+
+ Not started icon +
+
+
Not started
+
${ this._euis.filter(eui => eui.status === ExerciseStatus.StatusEnum.NOT_STARTED).length }
+
+
+
+
+ In progress icon +
+
+
In progress
+
${ this._euis.filter(eui => eui.status === ExerciseStatus.StatusEnum.IN_PROGRESS).length }
+
+
+
+
+ Finished icon +
+
+
Finished
+
${ this._euis.filter(eui => eui.status === ExerciseStatus.StatusEnum.FINISHED).length }
+
+
+
+
+
+
+
Modifications in the last 5 mins
+
${ this.getEuisEditedInTheLastNMinutes(5) }
+
+
+
Modifications in the last 30 mins
+
${ this.getEuisEditedInTheLastNMinutes(30) }
+
+
+
Modifications in the last hour
+
${ this.getEuisEditedInTheLastNMinutes(60) }
+
+
+
Modifications in the last 2 hours
+
${ this.getEuisEditedInTheLastNMinutes(120) }
+
+
+
+
+ +
+ + +
+

Exercise configuration

+
+ Teacher short guide +
+ This quick guide summarizes the functionalities that can be used in this Dashboard. + +
    +
  • The Hide student's names option allows you to show/hide a column that includes the first and last names of all students shown in the progress table below. It is enabled by default.
  • +
  • In case the exercises include a solution proposal, two additional controls are displayed: +
      +
    • The publish solution to students option allows teachers to decide when the solution to an exercise can be downloaded by students. This action is irrevocable, disabling this control when it is enabled. By default it is disabled.
    • +
    • The allow edition after downloading solution option allows you to decide whether the exercises can be edited by the students once they have downloaded the solution. This behavior cannot be changed from the moment the solution is published. By default it is deactivated.
    • +
    +
  • +
+
+
+
+
+ ${this._exercise.includesTeacherSolution + ? `
+
Publish solution to students
+ +
+
+
Allow edition after downloading solution
+ +
` + : ''} +
+
+
+ +
+ + +
+

Student's progress

+
+
+
+
Hide student's names
+ +
+
+
+
Full name - - - - -
+ + ${!this.hiddenStudentNames + ? ` + ` : "" - } - - - ${ - this.fullMode - ? `` - : "" - } - - - ${rows} -
Full name + + + + + Exercise folder - - - - - Exercise status - - - - - Last modified fileLast modification - - - - -
- `} - + } + Exercise folder + + + + + + Exercise status + + + + + + Last modification + + + + + + ${this.fullMode + ? `Actions` + : "" + } + + ${ rows } + + + ` } + + `; } @@ -423,7 +609,7 @@ export class DashboardWebview { if (!pastDateStr) { return "-"; } - pastDateStr += "Z"; + pastDateStr = (pastDateStr.slice(-1)[0] === "Z") ? pastDateStr : pastDateStr + "Z"; let elapsedTime = (new Date().getTime() - new Date(pastDateStr).getTime()) / 1000; if (elapsedTime < 0) { elapsedTime = 0; @@ -449,7 +635,7 @@ export class DashboardWebview { } } - return `${Math.floor(elapsedTime)} ${unit}`; + return `${ Math.floor(elapsedTime) } ${ unit }`; } /** @@ -457,28 +643,29 @@ export class DashboardWebview { * @param username string username * @param course Course course * @param exercise Exercise exercise + * @param eui_id EUI identificator * @returns Thenable the selected file */ private async showQuickPick(username: string, course: Course, exercise: Exercise, eui_id: number): Promise { // Download most recent files await vscode.commands.executeCommand("vscode4teaching.getstudentfiles", course.name, exercise); return vscode.window - .withProgress( - { - location: vscode.ProgressLocation.Notification, - cancellable: false, - title: "Getting modified files...", - }, - (progress, token) => this.buildQuickPickItems(username, eui_id) - ) - .then(async (result: OpenQuickPick[]) => { - if (result) { - const selection = await this.showQuickPickRecursive(result); - if (selection) { - return selection; - } - } - }); + .withProgress( + { + location: vscode.ProgressLocation.Notification, + cancellable: false, + title: "Getting modified files...", + }, + (progress, token) => this.buildQuickPickItems(username, eui_id) + ) + .then(async (result: OpenQuickPick[]) => { + if (result) { + const selection = await this.showQuickPickRecursive(result); + if (selection) { + return selection; + } + } + }); } private async buildQuickPickItems(username: string, eui_id: number): Promise { @@ -501,7 +688,7 @@ export class DashboardWebview { } } } else { - vscode.window.showWarningMessage(`No modified files for ${username}`); + vscode.window.showWarningMessage(`No modified files for ${ username }`); } } if (uris.length > 0) { @@ -533,20 +720,6 @@ export class DashboardWebview { }); } - // Combines parent and child as a single path if there is only one child and it is a directory - private shortenPaths(item: OpenQuickPick) { - if (item.children.length === 1 && item.children[0].children.length > 0) { - const child = item.children[0]; - item.name = item.name + "/" + child.name; - item.children = child.children; - this.shortenPaths(item); - } else { - for (const child of item.children) { - this.shortenPaths(child); - } - } - } - // Recursive show quick pick with files given the file or directory name and an array of children private async showQuickPickRecursive(recursiveFiles: OpenQuickPick[]): Promise { if (!recursiveFiles[0].isGoBackButton && recursiveFiles[0].parents && recursiveFiles[0].parents.length > 0) { diff --git a/vscode4teaching-extension/src/components/liveshareBoard/LiveshareWebview.ts b/vscode4teaching-extension/src/components/liveshareBoard/LiveshareWebview.ts index d833ee67..5afd0b2d 100644 --- a/vscode4teaching-extension/src/components/liveshareBoard/LiveshareWebview.ts +++ b/vscode4teaching-extension/src/components/liveshareBoard/LiveshareWebview.ts @@ -1,6 +1,5 @@ import * as path from "path"; import * as vscode from "vscode"; -import * as vsls from "vsls"; import { APIClient } from "../../client/APIClient"; import { CurrentUser } from "../../client/CurrentUser"; import { initializeLiveShare, liveshareService, wsLiveshare } from "../../extension"; @@ -72,7 +71,9 @@ export class LiveshareWebview { (data) => { this.panel.webview.html = data; }, - (err) => { v4tLogger.error(err); }, + (err) => { + v4tLogger.error(err); + }, ); }); @@ -88,7 +89,9 @@ export class LiveshareWebview { (data) => { this.panel.webview.html = data; }, - (err) => { v4tLogger.error(err); }, + (err) => { + v4tLogger.error(err); + }, ); } }, @@ -124,7 +127,9 @@ export class LiveshareWebview { return -1 * weight; } else if (a.username < b.username) { return 1 * weight; - } else { return 0; } + } else { + return 0; + } }); break; } @@ -135,17 +140,29 @@ export class LiveshareWebview { return -1 * weight; } else if (a.username < b.username) { return 1 * weight; - } else { return 0; } + } else { + return 0; + } }); break; } case 2: { // role this.data[message.courseIndex].users.sort((a: User, b: User) => { - if (a.roles.some((r) => r.roleName === "ROLE_TEACHER")) { return -1 * weight; } - if (b.roles.some((r) => r.roleName === "ROLE_TEACHER")) { return 1 * weight; } - if (a.username < b.username) { return -1 * weight; } - if (a.username > b.username) { return 1 * weight; } else { return 0; } + if (a.roles.some((r) => r.roleName === "ROLE_TEACHER")) { + return -1 * weight; + } + if (b.roles.some((r) => r.roleName === "ROLE_TEACHER")) { + return 1 * weight; + } + if (a.username < b.username) { + return -1 * weight; + } + if (a.username > b.username) { + return 1 * weight; + } else { + return 0; + } }); break; } @@ -173,7 +190,9 @@ export class LiveshareWebview { private async getUsersFromCourse(course: Course): Promise { const users = await APIClient.getUsersInCourse(course.id); - if (!users?.data) { return []; } + if (!users?.data) { + return []; + } let currentUsername: string; try { @@ -182,13 +201,23 @@ export class LiveshareWebview { v4tLogger.error(err); } const filteredUsers = users.data - .filter((user) => currentUsername && currentUsername !== user.username) - .sort((user1, user2) => { - if (user1.roles.some((r) => r.roleName === "ROLE_TEACHER")) { return -1; } - if (user2.roles.some((r) => r.roleName === "ROLE_TEACHER")) { return 1; } - if (user1.username < user2.username) { return -1; } - if (user1.username > user2.username) { return 1; } else { return 0; } - }); + .filter((user) => currentUsername && currentUsername !== user.username) + .sort((user1, user2) => { + if (user1.roles.some((r) => r.roleName === "ROLE_TEACHER")) { + return -1; + } + if (user2.roles.some((r) => r.roleName === "ROLE_TEACHER")) { + return 1; + } + if (user1.username < user2.username) { + return -1; + } + if (user1.username > user2.username) { + return 1; + } else { + return 0; + } + }); this.users = new Set([...this.users, ...filteredUsers.map((user) => user.username)]); return filteredUsers; @@ -204,7 +233,9 @@ export class LiveshareWebview { if (liveshareService) { if (!this.shareCode) { const optionalCode = await liveshareService.share(); - if (optionalCode) { this.shareCode = optionalCode; } + if (optionalCode) { + this.shareCode = optionalCode; + } } if (!this.shareCode) { vscode.window.showErrorMessage("Error generating Live Share Code"); @@ -258,7 +289,7 @@ export class LiveshareWebview { let searchbar: string = "\n"; this.users.forEach((username) => { - searchbar = searchbar + `"; @@ -269,19 +300,19 @@ export class LiveshareWebview { - - V4T Dashboard: ${this._dashboardName} - + + V4T Dashboard: ${ this._dashboardName } +

Liveshare - Users in courses


- ${searchbar} + ${ searchbar } - ${tables} - + ${ tables } + `; } @@ -297,7 +328,9 @@ export class LiveshareWebview { private async generateHTMLTableFromCourse(course: Course, users: User[], courseIndex: number): Promise { let rows = ""; - if (!users.length) { return ""; } + if (!users.length) { + return ""; + } users .forEach((user) => { rows = rows + "\n"; @@ -309,33 +342,31 @@ export class LiveshareWebview { }); - const text = `
-

Users in ${course.name}

- + return `
+

Users in ${ course.name }

+
- ${rows} + ${ rows }
Full name - + Username - + Role - + Liveshare
`; - - return text; } } diff --git a/vscode4teaching-extension/src/components/statusBarItems/exercises/DiffWithSolution.ts b/vscode4teaching-extension/src/components/statusBarItems/exercises/DiffWithSolution.ts new file mode 100644 index 00000000..6cf4f019 --- /dev/null +++ b/vscode4teaching-extension/src/components/statusBarItems/exercises/DiffWithSolution.ts @@ -0,0 +1,7 @@ +import { V4TStatusBarItem } from "../V4TStatusBarItem"; + +export class DiffWithSolutionItem extends V4TStatusBarItem { + constructor() { + super("Diff with teacher's solution", "diff", "vscode4teaching.diffwithsolution"); + } +} diff --git a/vscode4teaching-extension/src/components/statusBarItems/exercises/DownloadTeacherSolution.ts b/vscode4teaching-extension/src/components/statusBarItems/exercises/DownloadTeacherSolution.ts new file mode 100644 index 00000000..d98b7b42 --- /dev/null +++ b/vscode4teaching-extension/src/components/statusBarItems/exercises/DownloadTeacherSolution.ts @@ -0,0 +1,12 @@ +import { Exercise } from "../../../model/serverModel/exercise/Exercise"; +import { V4TStatusBarItem } from "../V4TStatusBarItem"; + +export class DownloadTeacherSolutionItem extends V4TStatusBarItem { + constructor(private exercise: Exercise) { + super("Download teacher's solution", "cloud-download", "vscode4teaching.downloadteachersolution"); + } + + public getExerciseInfo(): Exercise { + return this.exercise; + } +} diff --git a/vscode4teaching-extension/src/components/statusBarItems/exercises/FinishItem.ts b/vscode4teaching-extension/src/components/statusBarItems/exercises/FinishItem.ts index d5d66938..5217d073 100644 --- a/vscode4teaching-extension/src/components/statusBarItems/exercises/FinishItem.ts +++ b/vscode4teaching-extension/src/components/statusBarItems/exercises/FinishItem.ts @@ -1,4 +1,5 @@ import { V4TStatusBarItem } from "../V4TStatusBarItem"; + export class FinishItem extends V4TStatusBarItem { constructor(private exerciseId: number) { super("Finish exercise", "checklist", "vscode4teaching.finishexercise"); diff --git a/vscode4teaching-extension/src/components/statusBarItems/liveshare/ShowLiveshareBoardItem.ts b/vscode4teaching-extension/src/components/statusBarItems/liveshare/ShowLiveshareBoardItem.ts index 9a873eca..1151d3d2 100644 --- a/vscode4teaching-extension/src/components/statusBarItems/liveshare/ShowLiveshareBoardItem.ts +++ b/vscode4teaching-extension/src/components/statusBarItems/liveshare/ShowLiveshareBoardItem.ts @@ -1,5 +1,6 @@ import { V4TStatusBarItem } from "../V4TStatusBarItem"; import { Course } from "../../../model/serverModel/course/Course"; + export class ShowLiveshareBoardItem extends V4TStatusBarItem { constructor(private _dashboardName: string, private _courses: Course[]) { super("Liveshare Board", "live-share", "vscode4teaching.showliveshareboard"); diff --git a/vscode4teaching-extension/src/extension.ts b/vscode4teaching-extension/src/extension.ts index 39eb887c..1808ea0c 100644 --- a/vscode4teaching-extension/src/extension.ts +++ b/vscode4teaching-extension/src/extension.ts @@ -12,22 +12,26 @@ import { V4TItem } from "./components/courses/V4TItem/V4TItem"; import { DashboardWebview } from "./components/dashboard/DashboardWebview"; import { LiveshareWebview } from "./components/liveshareBoard/LiveshareWebview"; import { ShowDashboardItem } from "./components/statusBarItems/dashboard/ShowDashboardItem"; +import { DiffWithSolutionItem } from "./components/statusBarItems/exercises/DiffWithSolution"; +import { DownloadTeacherSolutionItem } from "./components/statusBarItems/exercises/DownloadTeacherSolution"; import { FinishItem } from "./components/statusBarItems/exercises/FinishItem"; import { ShowLiveshareBoardItem } from "./components/statusBarItems/liveshare/ShowLiveshareBoardItem"; import { Dictionary } from "./model/Dictionary"; import { Course } from "./model/serverModel/course/Course"; import { Exercise, instanceOfExercise } from "./model/serverModel/exercise/Exercise"; +import { ExerciseStatus } from "./model/serverModel/exercise/ExerciseStatus"; import { ExerciseUserInfo } from "./model/serverModel/exercise/ExerciseUserInfo"; import { FileInfo } from "./model/serverModel/file/FileInfo"; import { ModelUtils } from "./model/serverModel/ModelUtils"; import { V4TExerciseFile } from "./model/V4TExerciseFile"; import { EUIUpdateService } from "./services/EUIUpdateService"; import { LiveShareService } from "./services/LiveShareService"; +import { v4tLogger } from "./services/LoggerService"; import { NoteComment } from "./services/NoteComment"; import { TeacherCommentService } from "./services/TeacherCommentsService"; +import { DiffBetweenDirectories } from "./utils/DiffBetweenDirectories"; import { FileIgnoreUtil } from "./utils/FileIgnoreUtil"; import { FileZipUtil } from "./utils/FileZipUtil"; -import { v4tLogger } from "./services/LoggerService"; // Base URL of server const getServerBaseUrl = () => vscode.workspace.getConfiguration("vscode4teaching").get("defaultServer"); @@ -43,6 +47,8 @@ export let currentCwds: ReadonlyArray | undefined; export let finishItem: FinishItem | undefined; export let showDashboardItem: ShowDashboardItem | undefined; export let showLiveshareBoardItem: ShowLiveshareBoardItem | undefined; +export let downloadTeacherSolutionItem: DownloadTeacherSolutionItem | undefined; +export let diffWithSolutionItem: DiffWithSolutionItem | undefined; export let changeEvent: vscode.Disposable; export let createEvent: vscode.Disposable; export let deleteEvent: vscode.Disposable; @@ -54,16 +60,16 @@ export let liveshareService: LiveShareService | undefined; export function activate(context: vscode.ExtensionContext) { // Set timezone process.env.TZ = "UTC"; - + // Set Axios automatic logging axios.interceptors.request.use(req => { - v4tLogger.info(`Axios request to ${req.url} with params '${req.params}' and timeout '${req.timeout}'.`); - v4tLogger.debug(`Request info:\n${JSON.stringify(req)}`); + v4tLogger.info(`Axios request to ${ req.url } with params '${ req.params }' and timeout '${ req.timeout }'.`); + v4tLogger.debug(`Request info:\n${ JSON.stringify(req) }`); return req; }); axios.interceptors.response.use(res => { - v4tLogger.info(`Axios response ${res.status} with headers\n${JSON.stringify(res.headers)}.`); - v4tLogger.debug(`Response data:\n${JSON.stringify(res.data)}`); + v4tLogger.info(`Axios response ${ res.status } with headers\n${ JSON.stringify(res.headers) }.`); + v4tLogger.debug(`Response data:\n${ JSON.stringify(res.data) }`); return res; }); @@ -71,26 +77,26 @@ export function activate(context: vscode.ExtensionContext) { const sessionInitialized = APIClient.initializeSessionFromFile(); if (sessionInitialized) { CurrentUser.updateUserInfo() - .then() - .catch((error) => { - APIClient.handleAxiosError(error); - }) - .finally(() => { - currentCwds = vscode.workspace.workspaceFolders; - if (currentCwds) { - initializeExtension(currentCwds).then(); - } else { - try { - const courses = CurrentUser.getUserInfo().courses; - if (courses && !showLiveshareBoardItem) { - showLiveshareBoardItem = new ShowLiveshareBoardItem("Liveshare Board", courses); - showLiveshareBoardItem.show(); - } - } catch (err) { - v4tLogger.error(err); - } - } - }); + .then() + .catch((error) => { + APIClient.handleAxiosError(error); + }) + .finally(() => { + currentCwds = vscode.workspace.workspaceFolders; + if (currentCwds) { + initializeExtension(currentCwds).then(); + } else { + try { + const courses = CurrentUser.getUserInfo().courses; + if (courses && !showLiveshareBoardItem) { + showLiveshareBoardItem = new ShowLiveshareBoardItem("Liveshare Board", courses); + showLiveshareBoardItem.show(); + } + } catch (err) { + v4tLogger.error(err); + } + } + }); } const loginDisposable = vscode.commands.registerCommand("vscode4teaching.login", () => { @@ -106,6 +112,9 @@ export function activate(context: vscode.ExtensionContext) { const logoutDisposable = vscode.commands.registerCommand("vscode4teaching.logout", async () => { coursesProvider.logout(); + if (DashboardWebview.exists()) { + DashboardWebview.currentPanel?.dispose(); + } if (showLiveshareBoardItem) { showLiveshareBoardItem.dispose(); showLiveshareBoardItem = undefined; @@ -114,6 +123,14 @@ export function activate(context: vscode.ExtensionContext) { showDashboardItem.dispose(); showDashboardItem = undefined; } + if (downloadTeacherSolutionItem) { + downloadTeacherSolutionItem.dispose(); + downloadTeacherSolutionItem = undefined; + } + if (diffWithSolutionItem) { + diffWithSolutionItem.dispose(); + diffWithSolutionItem = undefined; + } currentCwds = vscode.workspace.workspaceFolders; if (currentCwds) { await initializeExtension(currentCwds); @@ -123,7 +140,16 @@ export function activate(context: vscode.ExtensionContext) { const getFilesDisposable = vscode.commands.registerCommand("vscode4teaching.getexercisefiles", async (courseName: string, exercise: Exercise) => { coursesProvider.changeLoading(true); try { - await getSingleStudentExerciseFiles(courseName, exercise); + // Status has to be changed only if it was NOT_STARTED to IN_PROGRESS, otherwise it should not be changed + const eui: ExerciseUserInfo = (await APIClient.getExerciseUserInfo(exercise.id)).data; + if (eui.status === ExerciseStatus.StatusEnum.NOT_STARTED) { + const response = await APIClient.updateExerciseUserInfo(exercise.id, ExerciseStatus.StatusEnum.IN_PROGRESS); + if (response.data.status === ExerciseStatus.StatusEnum.IN_PROGRESS) { + await getSingleStudentExerciseFiles(courseName, exercise); + } + } else { + await getSingleStudentExerciseFiles(courseName, exercise); + } } finally { coursesProvider.changeLoading(false); } @@ -150,24 +176,38 @@ export function activate(context: vscode.ExtensionContext) { coursesProvider.deleteCourse(item); }); - const refreshView = vscode.commands.registerCommand("vscode4teaching.refreshcourses", () => { + const refreshCoursesInTreeView = vscode.commands.registerCommand("vscode4teaching.refreshcourses", async () => { + // Refreshes currently available courses coursesProvider.refreshCourses(); + // If there is a currently activated exercise, it will check if solution is public or not + if (!downloadTeacherSolutionItem && finishItem && finishItem.getExerciseId() !== 0) { + try { + const exercise = (await APIClient.getExercise(finishItem.getExerciseId())).data; + if (exercise.includesTeacherSolution && exercise.solutionIsPublic) { + // Solution is now public, so button can be showed to student + downloadTeacherSolutionItem = new DownloadTeacherSolutionItem(exercise); + downloadTeacherSolutionItem.show(); + // Students gets a visual alert about this change + vscode.window.showInformationMessage("Solution provided by teacher is now available. You can download it using the corresponding button in toolbar."); + } + } catch (error) { + APIClient.handleAxiosError(error); + } + } }); - const refreshCourse = vscode.commands.registerCommand("vscode4teaching.refreshexercises", (item: V4TItem) => { + const refreshExercisesInTreeView = vscode.commands.registerCommand("vscode4teaching.refreshexercises", (item: V4TItem) => { coursesProvider.refreshExercises(item); }); const addExercise = vscode.commands.registerCommand("vscode4teaching.addexercise", (item: V4TItem) => { - coursesProvider.addExercise(item); + coursesProvider.addExercises(item, false); }); const addMultipleExercises = vscode.commands.registerCommand("vscode4teaching.addmultipleexercises", (item: V4TItem) => { - coursesProvider.addMultipleExercises(item); + coursesProvider.addExercises(item, true); }); - // showExerciseDashboard is defined later in this file - const editExercise = vscode.commands.registerCommand("vscode4teaching.editexercise", (item: V4TItem) => { coursesProvider.editExercise(item); }); @@ -234,7 +274,7 @@ export function activate(context: vscode.ExtensionContext) { const codeThenable = APIClient.getSharingCode(item.item); codeThenable .then((response) => { - const link = `${getServerBaseUrl()}/app?code=${response.data}`; + const link = `${ getServerBaseUrl() }/app?code=${ response.data }`; vscode.window.showInformationMessage("Share this link with your students to give them access to this course:\n" + link, "Copy link").then((clicked) => { if (clicked) { vscode.env.clipboard.writeText(link).then(() => { @@ -259,57 +299,60 @@ export function activate(context: vscode.ExtensionContext) { await coursesProvider.getCourseWithCode(); }); + const setExerciseFinished = async (finishItem: FinishItem) => { + try { + const response = await APIClient.updateExerciseUserInfo(finishItem.getExerciseId(), ExerciseStatus.StatusEnum.FINISHED); + if (response.data.status === ExerciseStatus.StatusEnum.FINISHED && finishItem) { + finishItem.dispose(); + if (changeEvent) + changeEvent.dispose(); + if (createEvent) + createEvent.dispose(); + if (deleteEvent) + deleteEvent.dispose(); + } else { + vscode.window.showErrorMessage("The exercise has not been marked as finished."); + } + } catch (error) { + APIClient.handleAxiosError(error); + } + }; + const finishExercise = vscode.commands.registerCommand("vscode4teaching.finishexercise", async () => { - const warnMessage = "Finish exercise? Exercise will be marked as finished and you will not be able to upload any more updates"; + const warnMessage = "Finish exercise? When the exercise is marked as completed, it will not be possible to send new updates."; const selectedOption = await vscode.window.showWarningMessage(warnMessage, { modal: true }, "Accept"); if (selectedOption === "Accept" && finishItem) { - try { - const response = await APIClient.updateExerciseUserInfo(finishItem.getExerciseId(), 1); - if (response.data.status === 1 && finishItem) { - finishItem.dispose(); - if (changeEvent) { - changeEvent.dispose(); - } - if (createEvent) { - createEvent.dispose(); - } - if (deleteEvent) { - deleteEvent.dispose(); - } - } else { - vscode.window.showErrorMessage("An unexpected error has occurred. The exercise has not been marked as finished. Please try again."); - } - } catch (error) { - APIClient.handleAxiosError(error); - } + await setExerciseFinished(finishItem); + vscode.window.showInformationMessage("The exercise has been successfully finished."); + CoursesProvider.triggerTreeReload(); } }); const showDashboardFunction = (exercise: Exercise, course: Course, fullMode: boolean) => { - if (DashboardWebview.exists()){ - vscode.window.showWarningMessage("You have to close currently opened dashboard before opening another one."); + if (DashboardWebview.exists()) { + vscode.window.showWarningMessage("Currently opened dashboard has to be closed before opening another one."); } else { if (exercise && course) { APIClient.getAllStudentsExerciseUserInfo(exercise.id) - .then((response: AxiosResponse) => { - if (exercise && course) { - DashboardWebview.show(response.data, course, exercise, fullMode); - } - }) - .catch((error) => APIClient.handleAxiosError(error)); - } + .then((response: AxiosResponse) => { + if (exercise && course) { + DashboardWebview.show(response.data, course, exercise, fullMode); + } + }) + .catch((error) => APIClient.handleAxiosError(error)); + } } }; const showExerciseDashboard = vscode.commands.registerCommand("vscode4teaching.showexercisedashboard", (item: V4TItem) => { - if (item.item && instanceOfExercise(item.item) && item.item.course){ + if (item.item && instanceOfExercise(item.item) && item.item.course) { showDashboardFunction(item.item, item.item.course, false); } else { vscode.window.showErrorMessage("Not performabble action. Please try downloading exercise and accessing Dashboard."); - } + } }); - const showDashboard = vscode.commands.registerCommand("vscode4teaching.showcurrentexercisedashboard", () => { + const showCurrentExerciseDashboard = vscode.commands.registerCommand("vscode4teaching.showcurrentexercisedashboard", () => { if (showDashboardItem && showDashboardItem.exercise && showDashboardItem.course) { showDashboardFunction(showDashboardItem.exercise, showDashboardItem.course, true); } @@ -334,23 +377,99 @@ export function activate(context: vscode.ExtensionContext) { } }); + const downloadTeacherSolution = vscode.commands.registerCommand("vscode4teaching.downloadteachersolution", async () => { + if (CurrentUser.isLoggedIn() && downloadTeacherSolutionItem) { + // Get current exercise info + const exercise = downloadTeacherSolutionItem.getExerciseInfo(); + // Interaction with the user: he or she is told of the changes to be made and confirmation is requested and command will only continue if user confirms + let initialWarning = await vscode.window.showInformationMessage( + exercise.allowEditionAfterSolutionDownloaded + ? "The solution will then be downloaded. Once downloaded, you can continue editing the exercise." + : "The solution will then be downloaded. Once downloaded, the exercise will be marked as finished and it will not be possible to continue editing it." + , { modal: true }, { title: "Accept" }); + if (initialWarning && initialWarning.title !== "Accept") + return; + + if (exercise.includesTeacherSolution && exercise.solutionIsPublic) { + try { + // Exercise is marked as finished (and updating events are disabled) only if edition is allowed after downloading solution + if (!exercise.allowEditionAfterSolutionDownloaded && finishItem) { + await setExerciseFinished(finishItem); + } + // Solution is downloaded in a folder called "solution" located at root directory of exercise + const solutionZipInfo = FileZipUtil.studentSolutionZipInfo(exercise); + await FileZipUtil.filesFromZip(solutionZipInfo, APIClient.getExerciseResourceById(exercise.id, "solution"), solutionZipInfo.dir, true); + // Interaction with user: a notification is issued to the user to inform him or her of changes + vscode.window.showInformationMessage( + exercise.allowEditionAfterSolutionDownloaded + ? "The solution has been downloaded but the exercise has not been finished, so further editing is possible." + : "The solution has been downloaded and the exercise has been marked as finished, so subsequent editions will not be saved." + ); + downloadTeacherSolutionItem.dispose(); + // The user is allowed to initiate the functionality to visualize differences between his proposal and the teacher's solution + diffWithSolutionItem = new DiffWithSolutionItem(); + diffWithSolutionItem.show(); + const userResponse = await vscode.window.showInformationMessage("To visualize the differences between the submitted proposal and the solution, you can click on this button or access the function in the toolbar.", { title: "Show diff with solution" }); + if (userResponse && userResponse.title === "Show diff with solution") { + await vscode.commands.executeCommand("vscode4teaching.diffwithsolution"); + } + } catch (err) { + v4tLogger.error(err); + } + } + } + }); + + const diffWithSolution = vscode.commands.registerCommand("vscode4teaching.diffwithsolution", async () => { + // Since this command can only be launched by students with an active exercise, the current workspace has to have only one active directory + const wsDirectory = vscode.workspace.workspaceFolders; + if (wsDirectory && wsDirectory.length === 1) { + // In this directory, both the exercise proposal (root directory) and the proposed solution ("solution" directory) should be found + const proposalPath = path.resolve(wsDirectory[0].uri.fsPath); + const solutionPath = path.resolve(wsDirectory[0].uri.fsPath, "solution"); + + // Trees of the directory structure of both paths are requested + // When searching for information in the root directory, the "solution" directory that it is contained there is ignored + const proposalTree = DiffBetweenDirectories.deepFilteredDirectoryTraversal(proposalPath, [/solution/, /^.*\.v4t$/]); + const solutionTree = DiffBetweenDirectories.deepFilteredDirectoryTraversal(solutionPath, [/^.*\.v4t$/]); + + // The merging algorithm is executed (see documentation associated to this method) + const mergedTree = DiffBetweenDirectories.mergeDirectoryTrees(proposalTree, solutionTree); + + // The user is shown the Quick Pick system designed to allow the user to choose a file from the tree resulting from the merge process + const userSelection = await DiffBetweenDirectories.directorySelectionQuickPick(DiffBetweenDirectories.mergedTreeToQuickPickTree(mergedTree, "")); + + // In case the user has chosen a file, it will be displayed... + if (userSelection) { + const proposalFileUri = vscode.Uri.parse(path.join(proposalPath, userSelection.relativePath)); + const solutionFileUri = vscode.Uri.parse(path.join(solutionPath, userSelection.relativePath)); + + // ... if it exists in both the proposal and the solution, the difference between both files + if (userSelection.source === 0) + await vscode.commands.executeCommand("vscode.diff", proposalFileUri, solutionFileUri); + // ... otherwise, the selected file is just opened (distinguishing whether it exists in the proposal or in the solution) + else + await vscode.commands.executeCommand("vscode.open", (userSelection.source === -1) ? proposalFileUri : solutionFileUri); + } + } + }); + context.subscriptions.push( loginDisposable, logoutDisposable, getFilesDisposable, + getStudentFiles, addCourseDisposable, editCourseDisposable, deleteCourseDisposable, - refreshView, - refreshCourse, + refreshCoursesInTreeView, + refreshExercisesInTreeView, addExercise, addMultipleExercises, - showExerciseDashboard, editExercise, deleteExercise, addUsersToCourse, removeUsersFromCourse, - getStudentFiles, diff, createComment, share, @@ -358,8 +477,11 @@ export function activate(context: vscode.ExtensionContext) { signupTeacher, getWithCode, finishExercise, - showDashboard, - showLiveshareBoard + showExerciseDashboard, + showCurrentExerciseDashboard, + showLiveshareBoard, + downloadTeacherSolution, + diffWithSolution ); // Temp fix for this issue https://github.com/microsoft/vscode/issues/136787 @@ -370,17 +492,17 @@ export function activate(context: vscode.ExtensionContext) { vscode.window.showWarningMessage("There may be issues connecting to the server unless you change your configuration settings.\nClicking the button will automatically make all configuration changes needed.", "Change configuration and restart").then((selected) => { if (selected) { vscode.workspace - .getConfiguration("http") - .update("systemCertificates", false, true) - .then( - () => { - vscode.commands.executeCommand("workbench.action.reloadWindow"); - }, - (error) => { - v4tLogger.error(error); - vscode.window.showErrorMessage("There was an error updating your configuration: " + error); - } - ); + .getConfiguration("http") + .update("systemCertificates", false, true) + .then( + () => { + vscode.commands.executeCommand("workbench.action.reloadWindow"); + }, + (error) => { + v4tLogger.error(error); + vscode.window.showErrorMessage("There was an error updating your configuration: " + error); + } + ); } }); } @@ -400,6 +522,14 @@ export function disableFeatures() { finishItem.dispose(); finishItem = undefined; } + if (downloadTeacherSolutionItem) { + downloadTeacherSolutionItem.dispose(); + downloadTeacherSolutionItem = undefined; + } + if (diffWithSolutionItem) { + diffWithSolutionItem.dispose(); + diffWithSolutionItem = undefined; + } if (showDashboardItem) { showDashboardItem.dispose(); showDashboardItem = undefined; @@ -419,7 +549,7 @@ export async function initializeExtension(cwds: ReadonlyArray 0) { const v4tjson: V4TExerciseFile = JSON.parse(fs.readFileSync(path.resolve(uris[0].fsPath), { encoding: "utf8" })); @@ -455,18 +585,30 @@ export async function initializeExtension(cwds: ReadonlyArray { - if (value === openDashboard) { - return vscode.commands.executeCommand("vscode4teaching.showcurrentexercisedashboard"); - } - }); + .showInformationMessage(message, openDashboard) + .then((value: string | undefined) => { + if (value === openDashboard) { + return vscode.commands.executeCommand("vscode4teaching.showcurrentexercisedashboard"); + } + }); } } }); @@ -601,7 +743,7 @@ async function checkCommentLineChanges(document: vscode.TextDocument) { * @param exercise exercise */ async function getExerciseFiles(courseName: string, exercise: Exercise) { - const zipInfo = FileZipUtil.exerciseZipInfo(courseName, exercise); + const zipInfo = FileZipUtil.studentExerciseZipInfo(courseName, exercise); return FileZipUtil.filesFromZip(zipInfo, APIClient.getExerciseFiles(exercise.id)); } @@ -620,30 +762,75 @@ async function getSingleStudentExerciseFiles(courseName: string, exercise: Exerc initializeExtension(currentCwds, true); } }); - vscode.workspace.updateWorkspaceFolders(0, vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders.length : 0, { uri, name: exercise.name }); + vscode.workspace.updateWorkspaceFolders(0, vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders.length : 0, { + uri, + name: exercise.name + }); } } } /** - * Download exercise's student's files and template from the server + * Download and unzip the student files, the template and the solution (if exists) received from the server. + * * @param courseName course name * @param exercise exercise */ -async function getStudentExerciseFiles(courseName: string, exercise: Exercise) { - const studentZipInfo = FileZipUtil.studentZipInfo(courseName, exercise); - const templateZipInfo = FileZipUtil.templateZipInfo(courseName, exercise); - return await Promise.all([FileZipUtil.filesFromZip(templateZipInfo, APIClient.getTemplate(exercise.id)), FileZipUtil.filesFromZip(studentZipInfo, APIClient.getAllStudentFiles(exercise.id), templateZipInfo.dir)]); +async function _getStudentExerciseFiles(courseName: string, exercise: Exercise) { + const templateZipInfo = FileZipUtil.teacherExerciseZipInfo(courseName, exercise, "template"); + const studentZipInfo = FileZipUtil.teacherExerciseZipInfo(courseName, exercise); + + let templateStudentPromise = [ + FileZipUtil.filesFromZip(studentZipInfo, APIClient.getAllStudentFiles(exercise.id), templateZipInfo.dir), + FileZipUtil.filesFromZip(templateZipInfo, APIClient.getExerciseResourceById(exercise.id, "template")) + ]; + + // Solution is requested only if it is known to exist (whether it is public or not) + if (exercise.includesTeacherSolution) { + const solutionZipInfo = FileZipUtil.teacherExerciseZipInfo(courseName, exercise, "solution"); + templateStudentPromise.push( + FileZipUtil.filesFromZip(solutionZipInfo, APIClient.getExerciseResourceById(exercise.id, "solution")) + ); + } + + return await Promise.all(templateStudentPromise); } +/** + * Auxiliary method for getMultipleStudentFiles() array sorting. + * + * Read further at getMultipleStudentFiles() documentation. + */ +const _comparatorMethodOfDirectoryArray = (a: string, b: string): number => { + // Either a or b can be "template", "solution" or "student_(number)" + // "template" is always the first directory + if (a === "template") return -1; + if (b === "template") return 1; + + // "solution" is the second one if exists + if (a === "solution") return -1; + if (b === "solution") return 1; + + // Otherwise, numbers of "student_(number)" directories are compared + return (parseInt(a.split("student_").splice(-1)[0]) < parseInt(b.split("student_").splice(-1)[0])) ? -1 : 1; +} + +/** + * Method executed when a teacher requests the download of an exercise. + * + * It downloads the template, the proposed solution (if any) and the updated files for all students who have started the exercise. + * + * @param courseName Course name. + * @param exercise Exercise information. + */ async function getMultipleStudentExerciseFiles(courseName: string, exercise: Exercise) { - const newWorkspaceURIs = await getStudentExerciseFiles(courseName, exercise); - if (newWorkspaceURIs && newWorkspaceURIs[1]) { - const wsURI: string = newWorkspaceURIs[1]; - let directories = fs.readdirSync(wsURI, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()); + const newWorkspaceURIs = await _getStudentExerciseFiles(courseName, exercise); + if (newWorkspaceURIs && newWorkspaceURIs[0]) { + const wsURI: string = newWorkspaceURIs[0]; + // Leer los contenidos del directorio padre del ejercicio + let exerciseDirents = fs.readdirSync(wsURI, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()); /* - Move "template" directory to beginning of directory array - As in the documentation for vscode.workspace.onDidChangeWorkspaceFolders: + Documentation for vscode.workspace.onDidChangeWorkspaceFolders: If the first workspace folder is added, removed or changed, the currently executing extensions (including the one that called this method) will be terminated and restarted so that the (deprecated) @@ -651,27 +838,25 @@ async function getMultipleStudentExerciseFiles(courseName: string, exercise: Exe The folder that never changes is the "template" one, so we move it to the beginning of the array to avoid reloading all extensions if the same workspace is opened and there are new students added. + + Hence, array of dirents is always sorted so it looks like [template, solution?, ...students (sorted by ascending number)] + In this way, "template" will always be the first entry of array, followed by solution (if exists) and by student's files. */ - const template = directories.filter((dirent) => dirent.name === "template")[0]; - directories = directories.filter((dirent) => dirent.name !== "template"); - directories.unshift(template); + let sortedStringExerciseDirs = exerciseDirents.map(dirent => dirent.name).sort(_comparatorMethodOfDirectoryArray); + // Get file info for id references if (coursesProvider && CurrentUser.isLoggedIn()) { - const usernames = directories.filter((dirent) => !dirent.name.includes("template")).map((dirent) => dirent.name); + const usernames = sortedStringExerciseDirs.filter(exName => exName.startsWith("student_")); const fileInfoPath = path.resolve(FileZipUtil.INTERNAL_FILES_DIR, CurrentUser.getUserInfo().username, ".fileInfo", exercise.name); await getFilesInfo(exercise, fileInfoPath, usernames); - const subdirectoriesURIs = directories.map((dirent) => { - return { - uri: vscode.Uri.file(path.resolve(wsURI, dirent.name)), - }; - }); + const subdirectoriesURIs = sortedStringExerciseDirs.map(exName => ({ uri: vscode.Uri.file(path.resolve(wsURI, exName)) })); vscode.workspace.onDidChangeWorkspaceFolders(() => { currentCwds = vscode.workspace.workspaceFolders; if (currentCwds) { initializeExtension(currentCwds, true); } }); - // open all student files and template + // Open all student files and template vscode.workspace.updateWorkspaceFolders(0, vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders.length : 0, ...subdirectoriesURIs); } } @@ -691,6 +876,7 @@ async function getFilesInfo(exercise: Exercise, fileInfoPath: string, usernames: } } +// Syntactic sugar functions for command testing export function setCommentProvider(username: string) { commentProvider = new TeacherCommentService(username); } @@ -699,6 +885,10 @@ export function setFinishItem(exerciseId: number) { finishItem = new FinishItem(exerciseId); } +export function setDownloadTeacherSolutionItem(exercise: Exercise) { + downloadTeacherSolutionItem = new DownloadTeacherSolutionItem(exercise); +} + export function setTemplate(cwdName: string, templatePath: string) { templates[cwdName] = templatePath; } diff --git a/vscode4teaching-extension/src/model/serverModel/ModelUtils.ts b/vscode4teaching-extension/src/model/serverModel/ModelUtils.ts index c748bc08..aae46c48 100644 --- a/vscode4teaching-extension/src/model/serverModel/ModelUtils.ts +++ b/vscode4teaching-extension/src/model/serverModel/ModelUtils.ts @@ -8,6 +8,7 @@ export class ModelUtils { return false; } } + public static isTeacher(user: User) { if (user) { return user.roles.filter((role) => role.roleName === "ROLE_TEACHER").length > 0; diff --git a/vscode4teaching-extension/src/model/serverModel/course/Course.ts b/vscode4teaching-extension/src/model/serverModel/course/Course.ts index 6661be52..683926e8 100644 --- a/vscode4teaching-extension/src/model/serverModel/course/Course.ts +++ b/vscode4teaching-extension/src/model/serverModel/course/Course.ts @@ -7,6 +7,7 @@ export interface Course { creator?: User; exercises: Exercise[]; } + export function instanceOfCourse(object: any): object is Course { return "id" in object && "name" in object && "exercises" in object; } diff --git a/vscode4teaching-extension/src/model/serverModel/exercise/Exercise.ts b/vscode4teaching-extension/src/model/serverModel/exercise/Exercise.ts index 042c2e57..f499bb4b 100644 --- a/vscode4teaching-extension/src/model/serverModel/exercise/Exercise.ts +++ b/vscode4teaching-extension/src/model/serverModel/exercise/Exercise.ts @@ -4,7 +4,11 @@ export interface Exercise { id: number; name: string; course?: Course; + includesTeacherSolution: boolean; + solutionIsPublic: boolean; + allowEditionAfterSolutionDownloaded: boolean; } + export function instanceOfExercise(object: any): object is Exercise { return "id" in object && "name" in object; } diff --git a/vscode4teaching-extension/src/model/serverModel/exercise/ExerciseEdit.ts b/vscode4teaching-extension/src/model/serverModel/exercise/ExerciseEdit.ts index 1fe97927..a66df796 100644 --- a/vscode4teaching-extension/src/model/serverModel/exercise/ExerciseEdit.ts +++ b/vscode4teaching-extension/src/model/serverModel/exercise/ExerciseEdit.ts @@ -1,3 +1,6 @@ export interface ExerciseEdit { name: string; + includesTeacherSolution: boolean; + solutionIsPublic: boolean; + allowEditionAfterSolutionDownloaded: boolean; } diff --git a/vscode4teaching-extension/src/model/serverModel/exercise/ExerciseStatus.ts b/vscode4teaching-extension/src/model/serverModel/exercise/ExerciseStatus.ts new file mode 100644 index 00000000..0eaa213c --- /dev/null +++ b/vscode4teaching-extension/src/model/serverModel/exercise/ExerciseStatus.ts @@ -0,0 +1,20 @@ +export namespace ExerciseStatus { + export enum StatusEnum { + NOT_STARTED = "NOT_STARTED", + IN_PROGRESS = "IN_PROGRESS", + FINISHED = "FINISHED" + } + + export function toString(status: StatusEnum): string { + switch (status) { + case "NOT_STARTED": + return "Not started"; + case "FINISHED": + return "Finished"; + case "IN_PROGRESS": + return "In progress"; + default: + return ""; + } + } +} diff --git a/vscode4teaching-extension/src/model/serverModel/exercise/ExerciseUserInfo.ts b/vscode4teaching-extension/src/model/serverModel/exercise/ExerciseUserInfo.ts index 1c4a02f7..50916d58 100644 --- a/vscode4teaching-extension/src/model/serverModel/exercise/ExerciseUserInfo.ts +++ b/vscode4teaching-extension/src/model/serverModel/exercise/ExerciseUserInfo.ts @@ -1,11 +1,12 @@ import { User } from "../user/User"; import { Exercise } from "./Exercise"; +import { ExerciseStatus } from "./ExerciseStatus"; export interface ExerciseUserInfo { id: number; exercise: Exercise; user: User; - status: number; + status: ExerciseStatus.StatusEnum; updateDateTime: string; modifiedFiles?: string[]; } diff --git a/vscode4teaching-extension/src/services/EUIUpdateService.ts b/vscode4teaching-extension/src/services/EUIUpdateService.ts index 42e40891..0ae82ffb 100644 --- a/vscode4teaching-extension/src/services/EUIUpdateService.ts +++ b/vscode4teaching-extension/src/services/EUIUpdateService.ts @@ -7,7 +7,9 @@ export class EUIUpdateService { public static addModifiedPath(uri: vscode.Uri) { const matches = (this.URI_REGEX.exec(uri.path)); - if (!matches) { return null; } + if (!matches) { + return null; + } matches.shift(); this.modifiedPaths.add(matches[0]); } diff --git a/vscode4teaching-extension/src/services/LiveShareService.ts b/vscode4teaching-extension/src/services/LiveShareService.ts index 7e462414..f0371cd8 100644 --- a/vscode4teaching-extension/src/services/LiveShareService.ts +++ b/vscode4teaching-extension/src/services/LiveShareService.ts @@ -43,20 +43,21 @@ export class LiveShareService { } public handleLiveshareMessage(dataStringified: string) { - if (!dataStringified) { return; } + if (!dataStringified) { + return; + } const { handle, from, code } = JSON.parse(dataStringified); if ((handle === "liveshare") && from && code) { - vscode.window.showInformationMessage(`Liveshare invitation by ${from}`, "Accept", "Decline").then( + vscode.window.showInformationMessage(`Liveshare invitation by ${ from }`, "Accept", "Decline").then( (res) => { if (res === "Accept") { const codeParsed: vscode.Uri = vscode.Uri.parse(code); vscode.env.clipboard.writeText(code).then( () => { - vscode.window.showInformationMessage(`Code already set on clipboard: ${code}`); + vscode.window.showInformationMessage(`Code already set on clipboard: ${ code }`); this.liveshareAPI.join(codeParsed); }, - (err) => vscode.window.showErrorMessage(`Could not use clipboard: ${err}`), - + (err) => vscode.window.showErrorMessage(`Could not use clipboard: ${ err }`), ); } }); @@ -67,8 +68,8 @@ export class LiveShareService { this.liveshareAPI = data; } - public async share () { + public async share() { return this.liveshareAPI.share(); } - + } diff --git a/vscode4teaching-extension/src/services/LoggerService.ts b/vscode4teaching-extension/src/services/LoggerService.ts index c6e36583..4e482b4e 100644 --- a/vscode4teaching-extension/src/services/LoggerService.ts +++ b/vscode4teaching-extension/src/services/LoggerService.ts @@ -7,8 +7,8 @@ export let v4tLogger: Logger = winston.createLogger({ winston.format.colorize(), winston.format.splat(), winston.format.timestamp(), - winston.format.printf(({level, message, timestamp}) => - `${timestamp} [${level}]: ${message}` + winston.format.printf(({ level, message, timestamp }) => + `${ timestamp } [${ level }]: ${ message }` ) ), transports: [ diff --git a/vscode4teaching-extension/src/services/NoteComment.ts b/vscode4teaching-extension/src/services/NoteComment.ts index df7e3adc..e64b2314 100644 --- a/vscode4teaching-extension/src/services/NoteComment.ts +++ b/vscode4teaching-extension/src/services/NoteComment.ts @@ -8,6 +8,7 @@ let commentId = 1; export class NoteComment implements Comment { public id: number; public label: string | undefined; + constructor( public body: string | MarkdownString, public mode: CommentMode, diff --git a/vscode4teaching-extension/src/services/TeacherCommentsService.ts b/vscode4teaching-extension/src/services/TeacherCommentsService.ts index 654aba77..9cb78e41 100644 --- a/vscode4teaching-extension/src/services/TeacherCommentsService.ts +++ b/vscode4teaching-extension/src/services/TeacherCommentsService.ts @@ -13,6 +13,7 @@ export class TeacherCommentService { private cwds: vscode.WorkspaceFolder[] = []; // Key: server id, value: thread private threads: Map = new Map(); + constructor(private author: string) { this.commentController = vscode.comments.createCommentController("teacherComments", "Teacher comments"); this.commentController.commentingRangeProvider = { @@ -44,7 +45,6 @@ export class TeacherCommentService { * Adds comment to file * @param reply Comment reply to add (given by vscode and passed to this method) * @param fileId file to add comment to - * @param errorCallback callback to call in case of backend API error */ public async addComment(reply: vscode.CommentReply, fileId: number) { const thread = reply.thread; @@ -128,7 +128,6 @@ export class TeacherCommentService { * @param threadId thread * @param line line location * @param lineText text of the line - * @param errorCallback error callback if request fails */ public async updateThreadLine(threadId: number, line: number, lineText: string) { const response = await APIClient.updateCommentThreadLine(threadId, line, lineText); diff --git a/vscode4teaching-extension/src/utils/DiffBetweenDirectories.ts b/vscode4teaching-extension/src/utils/DiffBetweenDirectories.ts new file mode 100644 index 00000000..8a36422e --- /dev/null +++ b/vscode4teaching-extension/src/utils/DiffBetweenDirectories.ts @@ -0,0 +1,318 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as vscode from "vscode"; + +export type BasicNode = { + fileName: string; + children: BasicNode[]; +} + +export type MergedTreeNode = { + value: string; + children: MergedTreeNode[]; + originalNodes: { + left?: BasicNode, + right?: BasicNode + }; + source: -1 | 0 | 1; +} + +export class QuickPickTreeNode implements vscode.QuickPickItem { + constructor( + public label: string, + public children: QuickPickTreeNode[], + public source: -1 | 0 | 1, + public relativePath: string, + public parent?: QuickPickTreeNode, + public kind?: vscode.QuickPickItemKind, + public description?: string, + public detail?: string, + public picked?: boolean, + public alwaysShow?: boolean, + public buttons?: vscode.QuickInputButton[], + ) { + } +} + +export class DiffBetweenDirectories { + + /** + * Method used to recursively traverse a directory, looking for all the files included. + * In addition, the contents of each directory filtered. + * + * Due to implementation, it is known that children arrays will be alphabetically sorted. + * + * @param absolutePath Absolute path to the directory (grows when entering in recursive calls) + * @param regexFilters (Optional) Regular expressions used to filter and eliminate the desired results of the directory scan. + * @returns Tree-shaped structure (nodes with parent and children) representing the contents of the directory provided. + */ + public static deepFilteredDirectoryTraversal = (absolutePath: string, regexFilters?: RegExp[]): BasicNode => { + // Scan contents of provided directory + let directoryContents: fs.Dirent[] = fs.readdirSync(absolutePath, { withFileTypes: true }); + // Regex filters applied (if included) to the results of the previous scanning process + if (regexFilters) directoryContents = directoryContents.filter(elem => regexFilters.every(regex => !regex.test(elem.name))); + + const nodeList: BasicNode[] = []; + directoryContents.forEach(child => { + nodeList.push( + (fs.statSync(path.join(absolutePath, child.name)).isDirectory()) + // Recursive case: directories have to be recursively scanned in depth + ? DiffBetweenDirectories.deepFilteredDirectoryTraversal(path.join(absolutePath, child.name), regexFilters) + // Base case: node is a file, so it does not have associated children + : { fileName: child.name, children: [] } + ); + }); + + // File name is obtained from the absolute path + // (the last part of the name after dividing the path by the defined path separator) + const fileName = absolutePath.split(path.sep).slice(-1)[0]; + + // Node is returned (made by its file name and the node list associated to its contents) + return { fileName, children: nodeList }; + } + + /** + * Algorithm that combines two trees of directories and files obtained by the {@link deepFilteredDirectoryTraversal()} method. + * + * It introduces in each of them a source attribute: + * -1 if the file exists only on the left. + * 1 if the file exists only on the right. + * 0 if it exists in both trees. + * + * It also keeps the pointers to the nodes of the original trees. + * + * @param leftTree Left tree. + * @param rightTree Right tree. + * @returns Merged tree root. + */ + public static mergeDirectoryTrees = (leftTree: BasicNode, rightTree: BasicNode): MergedTreeNode => { + // New node is generated with default values: name of file, empty list of children, source 0 and pointers to original nodes + const newMergedTreeNode: MergedTreeNode = { + value: leftTree.fileName, + children: [], + source: 0, + originalNodes: { left: leftTree, right: rightTree } + }; + + // The nodes associated with the children of both the left tree and the right tree are converted to the node type of the combined tree + // The necessary provenance is entered (-1 for those coming from the left, 1 for those coming from the right) and the pointers to the original nodes are stored + const leftTreeChildren = leftTree.children.map(elem => ({ + value: elem.fileName, + source: -1, + children: [], + originalNodes: { left: elem } + })); + const rightTreeChildren = rightTree.children.map(elem => ({ + value: elem.fileName, + source: 1, + children: [], + originalNodes: { right: elem } + })); + + // Both lists are traversed with incrementing numeric iterates until both are fully explored + let i = 0; // Iterator for the position of the elements in the list leftTreeChildren + let j = 0; // Iterator for the position of the elements in the list rightTreeChildren + while (i < leftTreeChildren.length || j < rightTreeChildren.length) { + // If elements still exist in both lists and a match is found between them, a new common element has been found. + if (i < leftTreeChildren.length && j < rightTreeChildren.length && leftTreeChildren[i].value === rightTreeChildren[j].value) { + // A new child of the explored node common to both trees is generated (it will have source 0) + const newChild: MergedTreeNode = { + value: leftTreeChildren[i].value, + children: leftTreeChildren[i].children, + source: 0, + originalNodes: { + left: leftTreeChildren[i].originalNodes.left, + right: rightTreeChildren[j].originalNodes.right + } + } + newMergedTreeNode.children.push(newChild); + + // Exploration continues on both lists + i++; + j++; + } + // The item pointed to in both lists is not the same + else { + // There are no more children left to explore from the left tree + if (i >= leftTreeChildren.length) { + // The exploration continues in the list of children coming from the right tree + newMergedTreeNode.children.push(rightTreeChildren[j]); + j++; + } + // There are no more children left to explore from the right tree + else if (j >= rightTreeChildren.length) { + // The exploration continues in the list of children coming from the left tree + newMergedTreeNode.children.push(leftTreeChildren[i]); + i++; + } + // There are still items to be scanned in both lists of children nodes + else { + // The values are compared and it is determined which one should be entered first to preserve order + // The item on the left must be entered first + if (leftTreeChildren[i].value.localeCompare(rightTreeChildren[j].value) < 0) { + newMergedTreeNode.children.push(leftTreeChildren[i]); + i++; + } + // The item on the right must be entered first + else { + newMergedTreeNode.children.push(rightTreeChildren[j]); + j++; + } + } + } + } + + // For children with common provenance to both trees, it is necessary to recursively call the function to continue exploring towards their descendants + newMergedTreeNode.children.forEach(child => { + // Recursive case: this child is of common descent and has descendants on both sides + // The children of this child are reanalyzed with the algorithm described above + if (child.source === 0 && child.originalNodes.left !== undefined && child.originalNodes.right !== undefined) + child.children = DiffBetweenDirectories.mergeDirectoryTrees(child.originalNodes.left, child.originalNodes.right).children; + + // Base case: this child comes from a single tree or has no descendants from both trees + // The children of this child are automatically assigned the given provenance and all its children are copied recursively with the same source + else + child.children = DiffBetweenDirectories.deepCopyWithSource(child).children; + }); + + // Analysis is finished and merged tree node is returned with its attributes completely fulfilled + return newMergedTreeNode; + } + + /** + * mergedTreeToQuickPickTree() + * + * Method applied to convert the previously generated tree (with {@link mergeDirectoryTrees()}) to a tree with nodes prepared for vscode.QuickPick (to interact with user). + * + * Returns a tree structure with the same content as the merged tree but made of {@link QuickPickTreeNode} instances which are ready to be displayed in the Quick Pick + * that will be shown to the user to choose the file from which he or she wants to view diff. + * + * @param mergedTreeNodeRoot Merged tree root node. + * @param relativePath Relative path. + * @param parent (Optional) Parent of current node. Undefined only at root. + * @returns Pointer to root of new Quick Pick tree. + */ + public static mergedTreeToQuickPickTree = (mergedTreeNodeRoot: MergedTreeNode, relativePath: string, parent?: QuickPickTreeNode): QuickPickTreeNode => { + // A new Quick Pick node is generated, including required info to show Quick Pick item (label), to locate node in tree (parent, children and source) and to locate file in filesystem (relativePath). + const newQuickPickTreeNode: QuickPickTreeNode = { + label: mergedTreeNodeRoot.value, + children: [], + source: mergedTreeNodeRoot.source, + relativePath, + parent + }; + + // Children nodes are generated + for (const child of mergedTreeNodeRoot.children) { + // With the information obtained, the new node is formed and a recursive call is introduced to continue investigating the successive descendants + newQuickPickTreeNode.children.push(DiffBetweenDirectories.mergedTreeToQuickPickTree(child, path.join(relativePath, child.value), newQuickPickTreeNode)); + } + + // When children are interpreted, label and description for current node are assigned + newQuickPickTreeNode.label = `$(${ (newQuickPickTreeNode.children.length > 0) ? "folder" : "file" }) ${ newQuickPickTreeNode.label }`; + if (newQuickPickTreeNode.source === -1) + newQuickPickTreeNode.description = "Only available in left folder"; + else if (newQuickPickTreeNode.source === 0) + newQuickPickTreeNode.description = "Available in both left and right folder"; + else + newQuickPickTreeNode.description = "Only available in right folder"; + + // Analysis is finished and merged tree node is returned with its attributes completely fulfilled + return newQuickPickTreeNode; + } + + + /** + * Given a Quick Pick item tree, the user is asked which file to open + * + * This implementation allows the user to browse through the different directories available and shows the origin of each node through the appearance of successive VSCode Quick Pick windows + * + * @param quickPickTreeNode Quick Pick tree root node + * @returns Object with information about user's selection + */ + public static directorySelectionQuickPick = async (quickPickTreeNode: QuickPickTreeNode): Promise<{ relativePath: string, source: -1 | 0 | 1 } | undefined> => { + // If current level of children have a parent, a "Jump to parent" item has to be shown + const elementList: QuickPickTreeNode[] = (quickPickTreeNode.parent) + ? [ + // This item will allow users to go back to the parent level of currently shown children level. + // Note: Both this item and separator include compulsory parameters relativePath, children and source, but they three are ignored. + { + label: "$(panel-maximize) Jump to parent", + children: [], + parent: quickPickTreeNode.parent, + relativePath: "", + source: 0 + }, + { label: "", children: [], kind: vscode.QuickPickItemKind.Separator, relativePath: "", source: 0 }, + ...quickPickTreeNode.children + ] + : quickPickTreeNode.children; + + // Quick Pick element is shown and algorithm waits until user picks up an element + const userSelection = await vscode.window.showQuickPick(elementList, { + title: "Choose a file or navigate to another directory", + ignoreFocusOut: false + }); + + if (userSelection) { + // If user selection is a directory (it has children), another Quick Pick is shown with same information about its children + if (userSelection.children.length > 0) { + return await DiffBetweenDirectories.directorySelectionQuickPick(userSelection); + } + // If user has chosen to return to the parent, a Quick Pick with the information related to the parent node and its siblings will be shown instead + else if (userSelection.label === "$(panel-maximize) Jump to parent" && quickPickTreeNode.parent) { + return await DiffBetweenDirectories.directorySelectionQuickPick(quickPickTreeNode.parent); + } + // Otherwise (user selected a specific file), information about selected node is returned + else { + return { relativePath: userSelection.relativePath, source: userSelection.source }; + } + } else { + return undefined; + } + } + + /** + * Private method used in {@link mergeDirectoryTrees()} to generate new nodes recursively with the desired source and source nodes. + * It implements the base case for the mentioned algorithm. + * + * @param node Root node to transform. + * @returns Root node transformed (with children recursively transformed). + */ + private static deepCopyWithSource = (node: MergedTreeNode): MergedTreeNode => { + // A new node is generated with the value of the relative path obtained, an initially empty list of children and the origin and the original nodes as they come from the original node + const newMergedTreeNode: MergedTreeNode = { + value: node.value, + children: [], + source: node.source, + originalNodes: node.originalNodes + }; + + // If the original node has children on the left and the origin of the node is the left tree, this method is applied recursively to all the children to include them. + if (node.originalNodes.left !== undefined && node.source === -1) { + node.originalNodes.left.children.forEach(child => + newMergedTreeNode.children.push(DiffBetweenDirectories.deepCopyWithSource({ + value: child.fileName, + children: [], + source: node.source, + originalNodes: { left: child } + })) + ); + } + + // If the original node has children on the right and the origin of the node is the right tree, this method is applied recursively to all the children to include them. + if (node.originalNodes.right !== undefined && node.source === 1) { + node.originalNodes.right.children.forEach(child => + newMergedTreeNode.children.push(DiffBetweenDirectories.deepCopyWithSource({ + value: child.fileName, + children: [], + source: node.source, + originalNodes: { right: child } + })) + ); + } + + // Merged tree node is returned with its attributes completely fulfilled + return newMergedTreeNode; + }; +} \ No newline at end of file diff --git a/vscode4teaching-extension/src/utils/FileZipUtil.ts b/vscode4teaching-extension/src/utils/FileZipUtil.ts index 21e325bd..8513e06d 100644 --- a/vscode4teaching-extension/src/utils/FileZipUtil.ts +++ b/vscode4teaching-extension/src/utils/FileZipUtil.ts @@ -25,11 +25,12 @@ export class FileZipUtil { } /** - * Returns zip info from an exercise - * @param courseName course name - * @param exercise exercise + * Returns ZIP info associated with the download of a student's files in an exercise. + * + * @param courseName Course name + * @param exercise Exercise information */ - public static exerciseZipInfo(courseName: string, exercise: Exercise): ZipInfo { + public static studentExerciseZipInfo(courseName: string, exercise: Exercise): ZipInfo { if (CurrentUser.isLoggedIn()) { const currentUser = CurrentUser.getUserInfo(); const dir = path.resolve(FileZipUtil.downloadDir, currentUser.username, courseName, exercise.name); @@ -46,41 +47,52 @@ export class FileZipUtil { } /** - * Returns zip info from student files - * @param courseName course name - * @param exercise exercise + * Returns ZIP info associated with a student's download of an exercise solution. + * + * @param exercise ExerciseInformation */ - public static studentZipInfo(courseName: string, exercise: Exercise): ZipInfo { + public static studentSolutionZipInfo(exercise: Exercise): ZipInfo { if (CurrentUser.isLoggedIn()) { - const currentUser = CurrentUser.getUserInfo(); - const dir = path.resolve(FileZipUtil.downloadDir, "teacher", currentUser.username, courseName, exercise.name); - const zipDir = path.resolve(FileZipUtil.INTERNAL_FILES_DIR, "teacher", currentUser.username); - const studentZipName = exercise.id + ".zip"; - return { - dir, - zipDir, - zipName: studentZipName, - }; + if (exercise.course && exercise.includesTeacherSolution && exercise.solutionIsPublic) { + const currentUser = CurrentUser.getUserInfo(); + const dir = path.resolve(FileZipUtil.downloadDir, currentUser.username, exercise.course.name, exercise.name, "solution"); + const zipDir = path.resolve(FileZipUtil.INTERNAL_FILES_DIR, currentUser.username); + const zipName = exercise.id + "-solution.zip"; + return { + dir, + zipDir, + zipName, + }; + } else { + throw new Error("Referred exercise has no solution or it is not public yet."); + } } else { throw new Error("Not logged in"); } } /** - * Returns zip info from a template of an exercise - * @param courseName course name - * @param exercise exercise + * Returns ZIP info from a exercise resource (either student's files, its template or its solution if exists). + * + * It distinguishes between these cases by means of the resourceType parameter, + * which may not be entered (student's files) or may be set to "template" or "solution". + * + * @param courseName Course name + * @param exercise Exercise information + * @param resourceType Resource type (can be either "template" or "solution") */ - public static templateZipInfo(courseName: string, exercise: Exercise): ZipInfo { + public static teacherExerciseZipInfo(courseName: string, exercise: Exercise, resourceType?: "template" | "solution"): ZipInfo { if (CurrentUser.isLoggedIn()) { const currentUser = CurrentUser.getUserInfo(); - const templateDir = path.resolve(FileZipUtil.downloadDir, "teacher", currentUser.username, courseName, exercise.name, "template"); - const templateZipDir = path.resolve(FileZipUtil.INTERNAL_FILES_DIR, "teacher", currentUser.username); - const templateZipName = exercise.id + "-template.zip"; + // Dónde voy a meter el fichero ZIP una vez se haya descargado y descomprimido + const dir = path.resolve(FileZipUtil.downloadDir, "teacher", currentUser.username, courseName, exercise.name, resourceType ?? ""); + // Dónde voy a guardarme el ZIP que se me descarga y con qué nombre + const zipDir = path.resolve(FileZipUtil.INTERNAL_FILES_DIR, "teacher", currentUser.username); + const zipName = `${ exercise.id }${ resourceType ? "-" + resourceType : '' }.zip`; return { - dir: templateDir, - zipDir: templateZipDir, - zipName: templateZipName, + dir: dir, + zipDir: zipDir, + zipName: zipName, }; } else { throw new Error("Not logged in"); @@ -92,8 +104,9 @@ export class FileZipUtil { * @param zipInfo zip info (check previous functions) * @param requestThenable thenable to call with zip * @param templateDir Optional template dir (use only if zipping a template) + * @param ignoreV4TFile True if V4T files should be ignored */ - public static async filesFromZip(zipInfo: ZipInfo, requestThenable: AxiosPromise, templateDir?: string) { + public static async filesFromZip(zipInfo: ZipInfo, requestThenable: AxiosPromise, templateDir?: string, ignoreV4TFile?: boolean) { if (!fs.existsSync(zipInfo.dir)) { mkdirp.sync(zipInfo.dir); } @@ -133,13 +146,17 @@ export class FileZipUtil { } } // The purpose of this file is to indicate this is an exercise directory to V4T to enable file uploads, etc - const isTeacher = CurrentUser.isLoggedIn() ? ModelUtils.isTeacher(CurrentUser.getUserInfo()) : false; - const fileContent: V4TExerciseFile = { - zipLocation: zipUri, - teacher: isTeacher, - template: templateDir ? templateDir : undefined, - }; - fs.writeFileSync(path.resolve(zipInfo.dir, "v4texercise.v4t"), JSON.stringify(fileContent), { encoding: "utf8" }); + // It is written when ignoreV4TFile is not declared or its value is undefined + // Currently, this parameter is only used when a student downloads the teacher's solution to an exercise + if (!ignoreV4TFile) { + const isTeacher = CurrentUser.isLoggedIn() ? ModelUtils.isTeacher(CurrentUser.getUserInfo()) : false; + const fileContent: V4TExerciseFile = { + zipLocation: zipUri, + teacher: isTeacher, + template: templateDir ? templateDir : undefined, + }; + fs.writeFileSync(path.resolve(zipInfo.dir, "v4texercise.v4t"), JSON.stringify(fileContent), { encoding: "utf8" }); + } return zipInfo.dir; } catch (error) { v4tLogger.error(error); @@ -153,8 +170,8 @@ export class FileZipUtil { */ public static async getZipFromUris(fileUris: vscode.Uri[]) { const zip = new JSZip(); - fileUris.forEach((uri) => { - const uriPath = path.resolve(uri.fsPath)?.replace(/\\/g, "/"); + fs.readdirSync(fileUris[0].fsPath).forEach((uri) => { + const uriPath = path.resolve(fileUris[0].fsPath, uri)?.replace(/\\/g, "/"); const stat = fs.statSync(uriPath); if (stat && stat.isDirectory()) { FileZipUtil.buildZipFromDirectory(uriPath, zip, path.dirname(uriPath)); @@ -172,9 +189,9 @@ export class FileZipUtil { * Updates or adds a single file in zip and uploads it * @param ignoredFiles List of ignored files * @param filePath Path of the file + * @param rootPath Root path * @param exerciseId Exercise id * @param jszipFile JSZip instance - * @param cwd Current Working Directory */ public static async updateFile(jszipFile: JSZip, filePath: string, rootPath: string, ignoredFiles: string[], exerciseId: number) { if (!ignoredFiles.includes(filePath)) { @@ -188,7 +205,7 @@ export class FileZipUtil { const thenable = jszipFile.generateAsync({ type: "nodebuffer" }); vscode.window.setStatusBarMessage("Compressing files...", thenable); return thenable.then((zipData) => APIClient.uploadFiles(exerciseId, zipData)) - .catch((axiosError) => APIClient.handleAxiosError(axiosError)); + .catch((axiosError) => APIClient.handleAxiosError(axiosError)); } } catch (err) { if (filePath.includes("v4texercise.v4t")) { @@ -204,9 +221,9 @@ export class FileZipUtil { * Deletes a single file in zip and uploads it * @param ignoredFiles List of ignored files * @param filePath Path of the file + * @param rootPath Root path * @param exerciseId Exercise id * @param jszipFile JSZip instance - * @param cwd Current Working Directory */ public static async deleteFile(jszipFile: JSZip, filePath: string, rootPath: string, ignoredFiles: string[], exerciseId: number) { if (!ignoredFiles.includes(filePath)) { @@ -215,8 +232,10 @@ export class FileZipUtil { jszipFile.remove(relativeFilePath); const thenable = jszipFile.generateAsync({ type: "nodebuffer" }); vscode.window.setStatusBarMessage("Compressing files...", thenable); - return thenable.then((zipData) => { APIClient.uploadFiles(exerciseId, zipData); }) - .catch((err) => APIClient.handleAxiosError(err)); + return thenable.then((zipData) => { + APIClient.uploadFiles(exerciseId, zipData); + }) + .catch((err) => APIClient.handleAxiosError(err)); } } diff --git a/vscode4teaching-extension/test/unitSuite/Client.test.ts b/vscode4teaching-extension/test/unitSuite/Client.test.ts index 34437deb..c936f94f 100644 --- a/vscode4teaching-extension/test/unitSuite/Client.test.ts +++ b/vscode4teaching-extension/test/unitSuite/Client.test.ts @@ -49,6 +49,13 @@ function expectSessionInvalidated(isInvalidated: boolean) { } describe("Client", () => { + beforeEach(() => { + jest.clearAllMocks(); + + createSessionFile(xsrfToken, jwtToken); + APIClientSession.initializeSessionCredentials(); + }); + afterEach(() => { APIClient.invalidateSession(); const v4tPath = path.resolve(__dirname, "..", "..", "src", "v4t"); @@ -57,25 +64,11 @@ describe("Client", () => { // console.error(error); }); } + mockedAxios.mockReset(); - mockedCoursesTreeProvider.mockClear(); - mockedCoursesTreeProvider.triggerTreeReload.mockClear(); - mockedCurrentUser.getUserInfo.mockClear(); - mockedCurrentUser.isLoggedIn.mockClear(); - mockedCurrentUser.resetUserInfo.mockClear(); - mockedCurrentUser.updateUserInfo.mockClear(); - mockedVscode.window.showWarningMessage.mockClear(); - mockedVscode.window.showErrorMessage.mockClear(); - mockedVscode.window.setStatusBarMessage.mockClear(); - mockedVscode.window.showInformationMessage.mockClear(); mockedLogger.error = jest.fn(); }); - beforeEach(() => { - createSessionFile(xsrfToken, jwtToken); - APIClientSession.initializeSessionCredentials(); - }); - it("should log in", async () => { const username = "johndoe"; const password = "password"; diff --git a/vscode4teaching-extension/test/unitSuite/ClientAPICalls.test.ts b/vscode4teaching-extension/test/unitSuite/ClientAPICalls.test.ts index 61bb8e7f..9aeee270 100644 --- a/vscode4teaching-extension/test/unitSuite/ClientAPICalls.test.ts +++ b/vscode4teaching-extension/test/unitSuite/ClientAPICalls.test.ts @@ -1,5 +1,4 @@ - -import axios, { AxiosPromise, AxiosRequestConfig, AxiosResponse } from "axios"; +import axios, { AxiosPromise, AxiosRequestConfig } from "axios"; import FormData from "form-data"; import { mocked } from "ts-jest/utils"; import * as vscode from "vscode"; @@ -11,6 +10,7 @@ import { CourseEdit } from "../../src/model/serverModel/course/CourseEdit"; import { ManageCourseUsers } from "../../src/model/serverModel/course/ManageCourseUsers"; import { Exercise } from "../../src/model/serverModel/exercise/Exercise"; import { ExerciseEdit } from "../../src/model/serverModel/exercise/ExerciseEdit"; +import { ExerciseStatus } from "../../src/model/serverModel/exercise/ExerciseStatus"; jest.mock("axios"); const mockedAxios = mocked(axios, true); @@ -25,7 +25,7 @@ const mockedVscode = mocked(vscode, true); const baseUrl = "https://edukafora.codeurjc.es"; // This tests don't bother with the response of the calls, only the request parameters -describe("client API calls", () => { +describe("Client API calls", () => { function expectCorrectRequest(options: AxiosRequestConfig, message: string, notification: boolean, thenable: AxiosPromise) { expect(mockedAxios).toHaveBeenCalledTimes(1); @@ -57,18 +57,17 @@ describe("client API calls", () => { APIClientSession.jwtToken = jwtToken; } + beforeEach(() => { + jest.clearAllMocks(); + + setLoggedIn(); + }); + afterEach(() => { - mockedAxios.mockClear(); - mockedVscode.window.setStatusBarMessage.mockClear(); - mockedVscode.window.withProgress.mockClear(); APIClientSession.xsrfToken = undefined; APIClientSession.jwtToken = undefined; }); - beforeEach(() => { - setLoggedIn(); - }); - it("should request get user info correctly", () => { const expectedOptions: AxiosRequestConfig = { baseURL: baseUrl, @@ -134,6 +133,27 @@ describe("client API calls", () => { expectCorrectRequest(expectedOptions, "Downloading exercise files...", true, thenable); }); + it("should request get course correctly", () => { + const expectedOptions: AxiosRequestConfig = { + baseURL: baseUrl, + data: undefined, + headers: { + "Authorization": "Bearer " + jwtToken, + "Cookie": "XSRF-TOKEN=" + xsrfToken, + "X-XSRF-TOKEN": xsrfToken, + }, + maxContentLength: Infinity, + maxBodyLength: Infinity, + method: "GET", + responseType: "json", + url: "/api/courses", + }; + + const thenable = APIClient.getCourses(); + + expectCorrectRequest(expectedOptions, "Getting courses...", false, thenable); + }); + it("should request add course correctly", () => { const course: CourseEdit = { name: "New course", @@ -208,10 +228,36 @@ describe("client API calls", () => { expectCorrectRequest(expectedOptions, "Deleting course...", false, thenable); }); + it("should request get exercise correctly", () => { + const exerciseId = 1; + + const expectedOptions: AxiosRequestConfig = { + baseURL: baseUrl, + data: undefined, + headers: { + "Authorization": "Bearer " + jwtToken, + "Cookie": "XSRF-TOKEN=" + xsrfToken, + "X-XSRF-TOKEN": xsrfToken, + }, + maxContentLength: Infinity, + maxBodyLength: Infinity, + method: "GET", + responseType: "json", + url: "/api/exercises/" + exerciseId, + }; + + const thenable = APIClient.getExercise(exerciseId); + + expectCorrectRequest(expectedOptions, "Getting exercise information...", false, thenable); + }); + it("should request add exercise correctly", () => { const courseId = 1; const exercise: ExerciseEdit = { name: "New exercise", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false }; const expectedOptions: AxiosRequestConfig = { @@ -237,11 +283,36 @@ describe("client API calls", () => { it("should request add multiple exercises correctly", () => { const courseId = 1; const exercises: ExerciseEdit[] = [ - { name: "Exercise 1", }, - { name: "Exercise 2", }, - { name: "Exercise 3", }, - { name: "Exercise 4", }, - { name: "Exercise 5", }, + { + name: "Exercise 1", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false + }, + { + name: "Exercise 2", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false + }, + { + name: "Exercise 3", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false + }, + { + name: "Exercise 4", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false + }, + { + name: "Exercise 5", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false + }, ]; const expectedOptions: AxiosRequestConfig = { @@ -268,6 +339,9 @@ describe("client API calls", () => { const exerciseId = 1; const exercise: ExerciseEdit = { name: "New exercise", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false }; const expectedOptions: AxiosRequestConfig = { @@ -315,6 +389,31 @@ describe("client API calls", () => { expectCorrectRequest(expectedOptions, "Uploading template...", true, thenable); }); + it("should request upload exercise solution correctly", () => { + const exerciseId = 1; + const data: Buffer = Buffer.from("Test"); + const dataForm = new FormData(); + dataForm.append("file", data, { filename: "solution-1.zip" }); + const expectedOptions: AxiosRequestConfig = { + baseURL: baseUrl, + data: dataForm, + headers: { + "Authorization": "Bearer " + jwtToken, + "Cookie": "XSRF-TOKEN=" + xsrfToken, + "X-XSRF-TOKEN": xsrfToken, + }, + maxContentLength: Infinity, + maxBodyLength: Infinity, + method: "POST", + responseType: "json", + url: "/api/exercises/" + exerciseId + "/files/solution", + }; + + const thenable = APIClient.uploadExerciseSolution(exerciseId, data); + + expectCorrectRequest(expectedOptions, "Uploading solution...", true, thenable); + }); + it("should request delete exercise template correctly", () => { const exerciseId = 1; const expectedOptions: AxiosRequestConfig = { @@ -516,11 +615,33 @@ describe("client API calls", () => { url: "/api/exercises/" + exerciseId + "/files/template", }; - const thenable = APIClient.getTemplate(exerciseId); + const thenable = APIClient.getExerciseResourceById(exerciseId, "template"); expectCorrectRequest(expectedOptions, "Downloading exercise template...", true, thenable); }); + it("should request get solution correctly", () => { + const exerciseId = 1; + const expectedOptions: AxiosRequestConfig = { + baseURL: baseUrl, + data: undefined, + headers: { + "Authorization": "Bearer " + jwtToken, + "Cookie": "XSRF-TOKEN=" + xsrfToken, + "X-XSRF-TOKEN": xsrfToken, + }, + maxContentLength: Infinity, + maxBodyLength: Infinity, + method: "GET", + responseType: "arraybuffer", + url: "/api/exercises/" + exerciseId + "/files/solution", + }; + + const thenable = APIClient.getExerciseResourceById(exerciseId, "solution"); + + expectCorrectRequest(expectedOptions, "Downloading exercise solution...", true, thenable); + }); + it("should request get files info correctly", () => { const exerciseId = 1; const username = "johndoejr"; @@ -645,6 +766,9 @@ describe("client API calls", () => { const exercise: Exercise = { name: "Exercise", id: 2, + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false }; const expectedOptions: AxiosRequestConfig = { baseURL: baseUrl, @@ -738,7 +862,7 @@ describe("client API calls", () => { it("should request update exercise user info for exercise correctly", () => { const exerciseId = 1; - const status = 1; + const status = ExerciseStatus.StatusEnum.FINISHED; const expectedOptions: AxiosRequestConfig = { baseURL: baseUrl, data: { diff --git a/vscode4teaching-extension/test/unitSuite/Commands.test.ts b/vscode4teaching-extension/test/unitSuite/Commands.test.ts index dbb76e12..e2221128 100644 --- a/vscode4teaching-extension/test/unitSuite/Commands.test.ts +++ b/vscode4teaching-extension/test/unitSuite/Commands.test.ts @@ -6,61 +6,94 @@ import { mocked } from "ts-jest/utils"; import * as vscode from "vscode"; import { APIClient } from "../../src/client/APIClient"; import { CurrentUser } from "../../src/client/CurrentUser"; -import { WebSocketV4TConnection } from "../../src/client/WebSocketV4TConnection"; -import { FinishItem } from "../../src/components/statusBarItems/exercises/FinishItem"; import * as extension from "../../src/extension"; import { Exercise } from "../../src/model/serverModel/exercise/Exercise"; +import { ExerciseStatus } from "../../src/model/serverModel/exercise/ExerciseStatus"; import { ExerciseUserInfo } from "../../src/model/serverModel/exercise/ExerciseUserInfo"; import { FileInfo } from "../../src/model/serverModel/file/FileInfo"; import { User } from "../../src/model/serverModel/user/User"; -import { LiveShareService } from "../../src/services/LiveShareService"; import { NoteComment } from "../../src/services/NoteComment"; import { TeacherCommentService } from "../../src/services/TeacherCommentsService"; +import { BasicNode, DiffBetweenDirectories, MergedTreeNode, QuickPickTreeNode } from "../../src/utils/DiffBetweenDirectories"; import { FileZipUtil } from "../../src/utils/FileZipUtil"; import { ZipInfo } from "../../src/utils/ZipInfo"; +import { mockFsDirent } from "./__mocks__/mockFsUtils"; +import { mockedPathJoin } from "./__mocks__/mockPathUtils"; -jest.mock("vscode"); -const mockedVscode = mocked(vscode, true); jest.mock("../../src/client/APIClient"); const mockedClient = mocked(APIClient, true); -jest.mock("../../src/services/TeacherCommentsService"); -const mockedTeacherCommentService = mocked(TeacherCommentService, true); jest.mock("../../src/client/CurrentUser"); const mockedCurrentUser = mocked(CurrentUser, true); +jest.mock("../../src/services/TeacherCommentsService"); +const mockedTeacherCommentService = mocked(TeacherCommentService, true); +jest.mock("../../src/utils/DiffBetweenDirectories"); +const mockedDiffBetweenDirectories = mocked(DiffBetweenDirectories, true); +jest.mock("../../src/utils/FileZipUtil"); +const mockedFileZipUtil = mocked(FileZipUtil, true); + jest.mock("path"); const mockedPath = mocked(path, true); jest.mock("fs"); const mockedFs = mocked(fs, true); -jest.mock("../../src/utils/FileZipUtil"); -const mockedFileZipUtil = mocked(FileZipUtil, true); jest.mock("mkdirp"); const mockedMkdirp = mocked(mkdirp, true); -jest.mock("../../src/client/WebSocketV4TConnection"); -const mockedWebSocketV4TConnection = mocked(WebSocketV4TConnection, true); -jest.mock("../../src/services/LiveShareService"); -const mockedLiveShareService = mocked(LiveShareService, true); +jest.mock("vscode"); +const mockedVscode = mocked(vscode, true); + const ec: vscode.ExtensionContext = { subscriptions: [], workspaceState: { get: jest.fn(), update: jest.fn(), + keys: jest.fn() }, globalState: { get: jest.fn(), update: jest.fn(), + keys: jest.fn(), + setKeysForSync: jest.fn() + }, + secrets: { + get: jest.fn(), + store: jest.fn(), + delete: jest.fn(), + onDidChange: jest.fn() }, extensionUri: mockedVscode.Uri.parse("test"), extensionPath: "test", + environmentVariableCollection: { + persistent: true, + replace: jest.fn(), + append: jest.fn(), + prepend: jest.fn(), + get: jest.fn(), + forEach: jest.fn(), + delete: jest.fn(), + clear: jest.fn() + }, asAbsolutePath: jest.fn(), + storageUri: mockedVscode.Uri.parse("test"), storagePath: "test", + globalStorageUri: mockedVscode.Uri.parse("test"), globalStoragePath: "test", + logUri: mockedVscode.Uri.parse("test"), logPath: "test", + extensionMode: 2, + extension: { + id: "test", + extensionUri: mockedVscode.Uri.parse("test"), + extensionPath: "test", + isActive: true, + packageJSON: {}, + extensionKind: 1, + exports: {}, + activate: jest.fn() + } }; describe("Command implementations", () => { - - // command tests + // Command tests const commandFunctions = Object.create(null); mockedVscode.commands.registerCommand.mockImplementation((commandId, commandFn) => { commandFunctions[commandId] = commandFn; @@ -70,113 +103,16 @@ describe("Command implementations", () => { }); beforeEach(() => { + jest.clearAllMocks(); + mockedClient.initializeSessionFromFile.mockReturnValueOnce(false); // Initialization will be covered in another test extension.activate(ec); }); - afterEach(() => { - mockedCurrentUser.isLoggedIn.mockClear(); - mockedCurrentUser.getUserInfo.mockClear(); - mockedPath.resolve.mockClear(); - mockedFs.existsSync.mockClear(); - mockedVscode.commands.executeCommand.mockClear(); - mockedFileZipUtil.filesFromZip.mockClear(); - mockedMkdirp.sync.mockClear(); - mockedClient.getFilesInfo.mockClear(); - mockedFs.writeFileSync.mockClear(); - mockedVscode.workspace.updateWorkspaceFolders.mockClear(); - }); + // Tests are introduced in the same order commands are listed in extension.ts > context.subscriptions.push(...commands); - it("should create comment correctly (teacher)", async () => { - const line = 0; - const lineText = "test"; - const positionMock: vscode.Position = { - line, - character: 0, - compareTo: jest.fn(), - isAfter: jest.fn(), - isAfterOrEqual: jest.fn(), - isBefore: jest.fn(), - isBeforeOrEqual: jest.fn(), - isEqual: jest.fn(), - translate: jest.fn(), - with: jest.fn(), - }; - const rangeMock: vscode.Range = { - start: positionMock, - end: positionMock, - contains: jest.fn(), - intersection: jest.fn(), - isEmpty: true, - isEqual: jest.fn(), - isSingleLine: true, - union: jest.fn(), - with: jest.fn(), - }; - const commentsMock: NoteComment[] = [ - new NoteComment("test1", mockedVscode.CommentMode.Preview, { name: "johndoe" }, lineText), - ]; - const route = path.sep === "/" ? "/v4t/johndoe/course/exercise/johndoejr/file.txt" : "e:\\v4t\\johndoe\\course\\exercise\\johndoejr\\file.txt"; - const threadMock: vscode.CommentThread = { - uri: mockedVscode.Uri.parse(route), - range: rangeMock, - collapsibleState: mockedVscode.CommentThreadCollapsibleState.Expanded, - comments: commentsMock, - dispose: jest.fn(), - }; - const replyMock: vscode.CommentReply = { - thread: threadMock, - text: "test", - }; - const user: User = { - id: 40, - roles: [{ - roleName: "ROLE_STUDENT", - }, { - roleName: "ROLE_TEACHER", - }], - username: "johndoe", - }; - - mockedCurrentUser.isLoggedIn.mockReturnValue(true); - mockedCurrentUser.getUserInfo.mockReturnValue(user); - - mockedFileZipUtil.INTERNAL_FILES_DIR = "v4t"; - - mockedPath.resolve.mockImplementation((...args) => { - let finalRoute = ""; - for (const arg of args) { - finalRoute = finalRoute.concat("/").concat(arg); - } - return finalRoute; - }); - - const fileInfoArray: FileInfo[] = [ - { - id: 101, - path: "file.txt", - }, { - id: 102, - path: "incorrectfile.txt", - }, - ]; - mockedFs.readFileSync.mockReturnValueOnce(JSON.stringify(fileInfoArray)); - - extension.setCommentProvider("johndoe"); - await commandFunctions["vscode4teaching.createComment"](replyMock); - - expect(mockedVscode.window.showErrorMessage).toHaveBeenCalledTimes(0); - expect(mockedPath.resolve).toHaveBeenCalledTimes(1); - expect(mockedPath.resolve).toHaveBeenNthCalledWith(1, "v4t", "johndoe", ".fileInfo", "exercise", "johndoejr.json"); - expect(mockedFs.readFileSync).toHaveBeenCalledTimes(1); - const fileInfoRoute = path.sep === "/" ? "/v4t/johndoe/.fileInfo/exercise/johndoejr.json" : "e:\\v4t\\johndoe\\.fileInfo\\exercise\\johndoejr.json"; - expect(mockedFs.readFileSync).toHaveBeenNthCalledWith(1, "/v4t/johndoe/.fileInfo/exercise/johndoejr.json", { encoding: "utf8" }); - expect(extension.commentProvider?.addComment).toHaveBeenCalledTimes(1); - expect(extension.commentProvider?.addComment).toHaveBeenNthCalledWith(1, replyMock, 101); - }); - - it("should finish item correctly", async () => { - const warnMessage = "Finish exercise? Exercise will be marked as finished and you will not be able to upload any more updates"; + it("should finish exercise correctly (student)", async () => { + const warnMessage = "Finish exercise? When the exercise is marked as completed, it will not be possible to send new updates."; const acceptOption = "Accept"; extension.setFinishItem(2); @@ -188,10 +124,13 @@ describe("Command implementations", () => { const exercise: Exercise = { id: 2, name: "Test exercise", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false }; const eui: ExerciseUserInfo = { id: 3, - status: 1, + status: ExerciseStatus.StatusEnum.FINISHED, user, exercise, updateDateTime: new Date().toISOString(), @@ -211,14 +150,17 @@ describe("Command implementations", () => { expect(mockedVscode.window.showWarningMessage).toHaveBeenCalledTimes(1); expect(mockedVscode.window.showWarningMessage).toHaveBeenNthCalledWith(1, warnMessage, { modal: true }, acceptOption); expect(mockedClient.updateExerciseUserInfo).toHaveBeenCalledTimes(1); - expect(mockedClient.updateExerciseUserInfo).toHaveBeenNthCalledWith(1, 2, 1); + expect(mockedClient.updateExerciseUserInfo).toHaveBeenNthCalledWith(1, 2, ExerciseStatus.StatusEnum.FINISHED); }); - it("should download single student exercise files", async () => { + it("should download single student exercise files (student)", async () => { const courseName = "Test course"; const exercise: Exercise = { id: 10, name: "Test exercise", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false }; const zipInfo: ZipInfo = { dir: "newWorkspace", @@ -232,7 +174,7 @@ describe("Command implementations", () => { }], username: "johndoejr", }; - mockedFileZipUtil.exerciseZipInfo.mockReturnValueOnce(zipInfo); + mockedFileZipUtil.studentExerciseZipInfo.mockReturnValueOnce(zipInfo); mockedFileZipUtil.filesFromZip.mockResolvedValueOnce(zipInfo.dir); mockedCurrentUser.isLoggedIn.mockReturnValueOnce(true); mockedCurrentUser.getUserInfo.mockReturnValueOnce(user); @@ -253,12 +195,40 @@ describe("Command implementations", () => { statusText: "", }; mockedClient.getFilesInfo.mockResolvedValueOnce(fileInfoResponse); + const exerciseUpdateResponse: AxiosResponse = { + data: { + exercise: exercise, + status: ExerciseStatus.StatusEnum.IN_PROGRESS, + user: user, + id: 11, + updateDateTime: "" + }, + config: {}, + headers: {}, + status: 200, + statusText: "", + }; + const euiResponse: AxiosResponse = { + data: { + exercise: exercise, + status: ExerciseStatus.StatusEnum.NOT_STARTED, + user: user, + id: 11, + updateDateTime: "" + }, + config: {}, + headers: {}, + status: 200, + statusText: "", + }; + mockedClient.getExerciseUserInfo.mockResolvedValueOnce(euiResponse); + mockedClient.updateExerciseUserInfo.mockResolvedValueOnce(exerciseUpdateResponse); mockedVscode.workspace.updateWorkspaceFolders.mockReturnValueOnce(true); await commandFunctions["vscode4teaching.getexercisefiles"](courseName, exercise); - expect(mockedFileZipUtil.exerciseZipInfo).toHaveBeenCalledTimes(1); - expect(mockedFileZipUtil.exerciseZipInfo).toHaveBeenNthCalledWith(1, courseName, exercise); + expect(mockedFileZipUtil.studentExerciseZipInfo).toHaveBeenCalledTimes(1); + expect(mockedFileZipUtil.studentExerciseZipInfo).toHaveBeenNthCalledWith(1, courseName, exercise); expect(mockedFileZipUtil.filesFromZip).toHaveBeenCalledTimes(1); expect(mockedCurrentUser.isLoggedIn).toHaveBeenCalledTimes(1); expect(mockedCurrentUser.getUserInfo).toHaveBeenCalledTimes(1); @@ -271,14 +241,20 @@ describe("Command implementations", () => { expect(mockedFs.writeFileSync).toHaveBeenCalledTimes(1); expect(mockedFs.writeFileSync).toHaveBeenNthCalledWith(1, "fileInfo/johndoejr.json", JSON.stringify(fileInfoResponse.data), { encoding: "utf8" }); expect(mockedVscode.workspace.updateWorkspaceFolders).toHaveBeenCalledTimes(1); - expect(mockedVscode.workspace.updateWorkspaceFolders).toHaveBeenNthCalledWith(1, 0, 0, { uri: mockedVscode.Uri.file(zipInfo.dir), name: exercise.name }); + expect(mockedVscode.workspace.updateWorkspaceFolders).toHaveBeenNthCalledWith(1, 0, 0, { + uri: mockedVscode.Uri.file(zipInfo.dir), + name: exercise.name + }); }); - it("should download all student exercise files", async () => { + it("should download all student's files of exercise without solution (teacher)", async () => { const courseName = "Test course"; const exercise: Exercise = { id: 10, name: "Test exercise", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false }; const zipInfo: ZipInfo = { dir: "newWorkspace", @@ -299,55 +275,25 @@ describe("Command implementations", () => { }], username: "johndoejr", }; - mockedFileZipUtil.studentZipInfo.mockReturnValueOnce(zipInfo); - mockedFileZipUtil.templateZipInfo.mockReturnValueOnce(templateZipInfo); + mockedFileZipUtil.studentExerciseZipInfo.mockReturnValueOnce(zipInfo); + mockedFileZipUtil.teacherExerciseZipInfo.mockReturnValueOnce(templateZipInfo); mockedFileZipUtil.filesFromZip.mockResolvedValueOnce(templateZipInfo.dir).mockResolvedValueOnce(zipInfo.dir); - mockedFs.readdirSync.mockReturnValueOnce([{ - name: "johndoejr", - isBlockDevice: jest.fn(), - isCharacterDevice: jest.fn(), - isDirectory: jest.fn().mockReturnValue(true), - isFIFO: jest.fn(), - isFile: jest.fn(), - isSocket: jest.fn(), - isSymbolicLink: jest.fn(), - }, { - name: "johndoejr2", - isBlockDevice: jest.fn(), - isCharacterDevice: jest.fn(), - isDirectory: jest.fn().mockReturnValue(true), - isFIFO: jest.fn(), - isFile: jest.fn(), - isSocket: jest.fn(), - isSymbolicLink: jest.fn(), - }, { - name: "errorfile.txt", - isBlockDevice: jest.fn(), - isCharacterDevice: jest.fn(), - isDirectory: jest.fn().mockReturnValue(false), - isFIFO: jest.fn(), - isFile: jest.fn(), - isSocket: jest.fn(), - isSymbolicLink: jest.fn(), - }, { - name: "template", - isBlockDevice: jest.fn(), - isCharacterDevice: jest.fn(), - isDirectory: jest.fn().mockReturnValue(true), - isFIFO: jest.fn(), - isFile: jest.fn(), - isSocket: jest.fn(), - isSymbolicLink: jest.fn(), - }]); + + mockedFs.readdirSync.mockReturnValueOnce([ + mockFsDirent("student_11", true), + mockFsDirent("student_12", true), + mockFsDirent("errorfile.txt", false), + mockFsDirent("template", true) + ]); mockedCurrentUser.isLoggedIn.mockReturnValueOnce(true); mockedCurrentUser.getUserInfo.mockReturnValueOnce(user); mockedPath.resolve - .mockReturnValueOnce("fileInfo") - .mockReturnValueOnce("fileInfo/johndoejr.json") - .mockReturnValueOnce("fileInfo/johndoejr2.json") - .mockReturnValueOnce("newWorkspace/template") - .mockReturnValueOnce("newWorkspace/johndoejr") - .mockReturnValueOnce("newWorkspace/johndoejr2"); + .mockReturnValueOnce("fileInfo") + .mockReturnValueOnce("fileInfo/johndoejr.json") + .mockReturnValueOnce("fileInfo/johndoejr2.json") + .mockReturnValueOnce("newWorkspace/template") + .mockReturnValueOnce("newWorkspace/student_11") + .mockReturnValueOnce("newWorkspace/student_12"); mockedFs.existsSync.mockReturnValueOnce(false); mockedMkdirp.sync.mockReturnValueOnce("fileInfo"); const fileInfoResponse1: AxiosResponse = { @@ -384,31 +330,149 @@ describe("Command implementations", () => { await commandFunctions["vscode4teaching.getstudentfiles"](courseName, exercise); - expect(mockedFileZipUtil.studentZipInfo).toHaveBeenCalledTimes(1); - expect(mockedFileZipUtil.studentZipInfo).toHaveBeenNthCalledWith(1, courseName, exercise); - expect(mockedFileZipUtil.templateZipInfo).toHaveBeenCalledTimes(1); - expect(mockedFileZipUtil.templateZipInfo).toHaveBeenNthCalledWith(1, courseName, exercise); + expect(mockedFileZipUtil.teacherExerciseZipInfo).toHaveBeenCalledTimes(2); + expect(mockedFileZipUtil.teacherExerciseZipInfo).toHaveBeenNthCalledWith(1, courseName, exercise, "template"); + expect(mockedFileZipUtil.teacherExerciseZipInfo).toHaveBeenNthCalledWith(2, courseName, exercise); expect(mockedFileZipUtil.filesFromZip).toHaveBeenCalledTimes(2); expect(mockedCurrentUser.isLoggedIn).toHaveBeenCalledTimes(1); expect(mockedCurrentUser.getUserInfo).toHaveBeenCalledTimes(1); expect(mockedFs.existsSync).toHaveBeenCalledTimes(1); - expect(mockedPath.resolve).toHaveBeenCalledTimes(6); expect(mockedMkdirp.sync).toHaveBeenCalledTimes(1); expect(mockedMkdirp.sync).toHaveBeenNthCalledWith(1, "fileInfo"); expect(mockedClient.getFilesInfo).toHaveBeenCalledTimes(2); - expect(mockedClient.getFilesInfo).toHaveBeenNthCalledWith(1, "johndoejr", exercise.id); - expect(mockedClient.getFilesInfo).toHaveBeenNthCalledWith(2, "johndoejr2", exercise.id); + expect(mockedClient.getFilesInfo).toHaveBeenNthCalledWith(1, "student_11", exercise.id); + expect(mockedClient.getFilesInfo).toHaveBeenNthCalledWith(2, "student_12", exercise.id); expect(mockedFs.writeFileSync).toHaveBeenCalledTimes(2); expect(mockedFs.writeFileSync).toHaveBeenNthCalledWith(1, "fileInfo/johndoejr.json", JSON.stringify(fileInfoResponse1.data), { encoding: "utf8" }); expect(mockedFs.writeFileSync).toHaveBeenNthCalledWith(2, "fileInfo/johndoejr2.json", JSON.stringify(fileInfoResponse2.data), { encoding: "utf8" }); expect(mockedVscode.workspace.updateWorkspaceFolders).toHaveBeenCalledTimes(1); expect(mockedVscode.workspace.updateWorkspaceFolders).toHaveBeenNthCalledWith(1, 0, 0, { uri: mockedVscode.Uri.file("newWorkspace/template") }, - { uri: mockedVscode.Uri.file("newWorkspace/johndoejr") }, - { uri: mockedVscode.Uri.file("newWorkspace/johndoejr2") }); + { uri: mockedVscode.Uri.file("newWorkspace/student_11") }, + { uri: mockedVscode.Uri.file("newWorkspace/student_12") }); }); - it("should run diff", async () => { + it("should download all student's files of exercise with solution (teacher)", async () => { + const courseName = "Test course"; + const exercise: Exercise = { + id: 10, + name: "Test exercise", + includesTeacherSolution: true, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false + }; + const zipInfo: ZipInfo = { + dir: "newWorkspace", + zipDir: "zipDir", + zipName: "10.zip", + }; + const templateZipInfo: ZipInfo = { + dir: "newWorkspace/template", + zipDir: "zipDir", + zipName: "10-template.zip", + }; + const solutionZipInfo: ZipInfo = { + dir: "newWorkspace/template", + zipDir: "zipDir", + zipName: "10-solution.zip", + }; + const user: User = { + id: 40, + roles: [{ + roleName: "ROLE_STUDENT", + }, { + roleName: "ROLE_TEACHER", + }], + username: "johndoejr", + }; + mockedFileZipUtil.studentExerciseZipInfo.mockReturnValueOnce(zipInfo); + mockedFileZipUtil.teacherExerciseZipInfo.mockImplementation( + (course, exercise, resourceType) => + (resourceType === "template") ? templateZipInfo : solutionZipInfo + ); + mockedFileZipUtil.filesFromZip + .mockResolvedValueOnce(templateZipInfo.dir) + .mockResolvedValueOnce(zipInfo.dir) + .mockResolvedValueOnce(solutionZipInfo.dir); + + mockedFs.readdirSync.mockReturnValueOnce([ + mockFsDirent("student_11", true), + mockFsDirent("student_12", true), + mockFsDirent("errorfile.txt", false), + mockFsDirent("template", true), + mockFsDirent("solution", true), + ]); + mockedCurrentUser.isLoggedIn.mockReturnValueOnce(true); + mockedCurrentUser.getUserInfo.mockReturnValueOnce(user); + mockedPath.resolve + .mockReturnValueOnce("fileInfo") + .mockReturnValueOnce("fileInfo/johndoejr.json") + .mockReturnValueOnce("fileInfo/johndoejr2.json") + .mockReturnValueOnce("newWorkspace/template") + .mockReturnValueOnce("newWorkspace/solution") + .mockReturnValueOnce("newWorkspace/student_11") + .mockReturnValueOnce("newWorkspace/student_12"); + mockedFs.existsSync.mockReturnValueOnce(false); + mockedMkdirp.sync.mockReturnValueOnce("fileInfo"); + const fileInfoResponse1: AxiosResponse = { + data: [{ + id: 100, + path: "file.txt", + }, { + id: 101, + path: "file2.txt", + }], + config: {}, + headers: {}, + status: 200, + statusText: "", + }; + const fileInfoResponse2: AxiosResponse = { + data: [{ + id: 102, + path: "file.txt", + }, { + id: 103, + path: "file2.txt", + }, { + id: 104, + path: "file3.txt", + }], + config: {}, + headers: {}, + status: 200, + statusText: "", + }; + mockedClient.getFilesInfo.mockResolvedValueOnce(fileInfoResponse1).mockResolvedValueOnce(fileInfoResponse2); + mockedVscode.workspace.updateWorkspaceFolders.mockReturnValueOnce(true); + + await commandFunctions["vscode4teaching.getstudentfiles"](courseName, exercise); + + expect(mockedFileZipUtil.teacherExerciseZipInfo).toHaveBeenCalledTimes(3); + expect(mockedFileZipUtil.teacherExerciseZipInfo).toHaveBeenNthCalledWith(1, courseName, exercise, "template"); + expect(mockedFileZipUtil.teacherExerciseZipInfo).toHaveBeenNthCalledWith(2, courseName, exercise); + expect(mockedFileZipUtil.teacherExerciseZipInfo).toHaveBeenNthCalledWith(3, courseName, exercise, "solution"); + expect(mockedFileZipUtil.filesFromZip).toHaveBeenCalledTimes(3); + expect(mockedCurrentUser.isLoggedIn).toHaveBeenCalledTimes(1); + expect(mockedCurrentUser.getUserInfo).toHaveBeenCalledTimes(1); + expect(mockedFs.existsSync).toHaveBeenCalledTimes(1); + expect(mockedMkdirp.sync).toHaveBeenCalledTimes(1); + expect(mockedMkdirp.sync).toHaveBeenNthCalledWith(1, "fileInfo"); + expect(mockedClient.getFilesInfo).toHaveBeenCalledTimes(2); + expect(mockedClient.getFilesInfo).toHaveBeenNthCalledWith(1, "student_11", exercise.id); + expect(mockedClient.getFilesInfo).toHaveBeenNthCalledWith(2, "student_12", exercise.id); + expect(mockedFs.writeFileSync).toHaveBeenCalledTimes(2); + expect(mockedFs.writeFileSync).toHaveBeenNthCalledWith(1, "fileInfo/johndoejr.json", JSON.stringify(fileInfoResponse1.data), { encoding: "utf8" }); + expect(mockedFs.writeFileSync).toHaveBeenNthCalledWith(2, "fileInfo/johndoejr2.json", JSON.stringify(fileInfoResponse2.data), { encoding: "utf8" }); + expect(mockedVscode.workspace.updateWorkspaceFolders).toHaveBeenCalledTimes(1); + expect(mockedVscode.workspace.updateWorkspaceFolders).toHaveBeenNthCalledWith(1, 0, 0, + { uri: mockedVscode.Uri.file("newWorkspace/template") }, + { uri: mockedVscode.Uri.file("newWorkspace/solution") }, + { uri: mockedVscode.Uri.file("newWorkspace/student_11") }, + { uri: mockedVscode.Uri.file("newWorkspace/student_12") }); + }); + + it("should run diff between student's exercise and template (teacher)", async () => { const file = mockedVscode.Uri.file("student_11/file.txt"); const wf: vscode.WorkspaceFolder = { index: 0, @@ -436,4 +500,480 @@ describe("Command implementations", () => { expect(mockedVscode.commands.executeCommand).toHaveBeenCalledTimes(1); expect(mockedVscode.commands.executeCommand).toHaveBeenNthCalledWith(1, "vscode.diff", mockedVscode.Uri.file("template/file.txt"), file); }); + + it("should create comment correctly (teacher)", async () => { + const line = 0; + const lineText = "test"; + const positionMock: vscode.Position = { + line, + character: 0, + compareTo: jest.fn(), + isAfter: jest.fn(), + isAfterOrEqual: jest.fn(), + isBefore: jest.fn(), + isBeforeOrEqual: jest.fn(), + isEqual: jest.fn(), + translate: jest.fn(), + with: jest.fn(), + }; + const rangeMock: vscode.Range = { + start: positionMock, + end: positionMock, + contains: jest.fn(), + intersection: jest.fn(), + isEmpty: true, + isEqual: jest.fn(), + isSingleLine: true, + union: jest.fn(), + with: jest.fn(), + }; + const commentsMock: NoteComment[] = [ + new NoteComment("test1", mockedVscode.CommentMode.Preview, { name: "johndoe" }, lineText), + ]; + const route = path.sep === "/" ? "/v4t/johndoe/course/exercise/johndoejr/file.txt" : "e:\\v4t\\johndoe\\course\\exercise\\johndoejr\\file.txt"; + const threadMock: vscode.CommentThread = { + uri: mockedVscode.Uri.parse(route), + range: rangeMock, + collapsibleState: mockedVscode.CommentThreadCollapsibleState.Expanded, + canReply: true, + comments: commentsMock, + dispose: jest.fn() + }; + const replyMock: vscode.CommentReply = { + thread: threadMock, + text: "test", + }; + const user: User = { + id: 40, + roles: [{ + roleName: "ROLE_STUDENT", + }, { + roleName: "ROLE_TEACHER", + }], + username: "johndoe", + }; + + mockedCurrentUser.isLoggedIn.mockReturnValue(true); + mockedCurrentUser.getUserInfo.mockReturnValue(user); + + mockedFileZipUtil.INTERNAL_FILES_DIR = "v4t"; + + mockedPath.resolve.mockImplementation((...args) => { + let finalRoute = ""; + for (const arg of args) { + finalRoute = finalRoute.concat("/").concat(arg); + } + return finalRoute; + }); + + const fileInfoArray: FileInfo[] = [ + { + id: 101, + path: "file.txt", + }, { + id: 102, + path: "incorrectfile.txt", + }, + ]; + mockedFs.readFileSync.mockReturnValueOnce(JSON.stringify(fileInfoArray)); + + extension.setCommentProvider("johndoe"); + await commandFunctions["vscode4teaching.createComment"](replyMock); + + expect(mockedVscode.window.showErrorMessage).toHaveBeenCalledTimes(0); + expect(mockedPath.resolve).toHaveBeenCalledTimes(1); + expect(mockedPath.resolve).toHaveBeenNthCalledWith(1, "v4t", "johndoe", ".fileInfo", "exercise", "johndoejr.json"); + expect(mockedFs.readFileSync).toHaveBeenCalledTimes(1); + expect(mockedFs.readFileSync).toHaveBeenNthCalledWith(1, "/v4t/johndoe/.fileInfo/exercise/johndoejr.json", { encoding: "utf8" }); + expect(extension.commentProvider?.addComment).toHaveBeenCalledTimes(1); + expect(extension.commentProvider?.addComment).toHaveBeenNthCalledWith(1, replyMock, 101); + }); + + /** + * PENDING traducir desde aquí hasta abajo del todo porque empieza la locura xD + */ + it("should download teacher's solution when available (successful scenario) (student)", async () => { + const user: User = { + id: 1, + roles: [{ + roleName: "ROLE_STUDENT", + }, { + roleName: "ROLE_TEACHER", + }], + username: "johndoejr" + }; + mockedCurrentUser.isLoggedIn.mockReturnValueOnce(true); + mockedCurrentUser.getUserInfo.mockReturnValueOnce(user); + + const exercise: Exercise = { + id: 2, + name: "Exercise", + includesTeacherSolution: true, + solutionIsPublic: true, + allowEditionAfterSolutionDownloaded: false + }; + extension.setFinishItem(exercise.id); + extension.setDownloadTeacherSolutionItem(exercise); + + mockedVscode.window.showInformationMessage + // First button: beginning of process + .mockResolvedValueOnce({ title: "Accept" }) + // Second button: to start diff with solution functionality (not covered in this test) + .mockResolvedValueOnce(undefined); + + const solutionZipInfo: ZipInfo = { + dir: "newWorkspace/solution", + zipDir: "zipDir", + zipName: "1-solution.zip", + }; + mockedFileZipUtil.studentSolutionZipInfo.mockReturnValueOnce(solutionZipInfo); + mockedFileZipUtil.filesFromZip.mockResolvedValueOnce(solutionZipInfo.dir); + + const responseData: Buffer = Buffer.from("Test"); + mockedClient.getExerciseResourceById.mockResolvedValueOnce({ + status: 201, + statusText: "", + headers: {}, + config: {}, + data: responseData + }); + + await commandFunctions["vscode4teaching.downloadteachersolution"](); + + expect(mockedVscode.window.showInformationMessage).toHaveBeenCalledTimes(3); + expect(mockedVscode.window.showInformationMessage).toHaveBeenNthCalledWith(1, "The solution will then be downloaded. Once downloaded, the exercise will be marked as finished and it will not be possible to continue editing it.", { modal: true }, { title: "Accept" }); + expect(mockedVscode.window.showInformationMessage).toHaveBeenNthCalledWith(2, "The solution has been downloaded and the exercise has been marked as finished, so subsequent editions will not be saved."); + expect(mockedVscode.window.showInformationMessage).toHaveBeenNthCalledWith(3, "To visualize the differences between the submitted proposal and the solution, you can click on this button or access the function in the toolbar.", { title: "Show diff with solution" }); + expect(mockedFileZipUtil.studentSolutionZipInfo).toHaveBeenCalledTimes(1); + expect(mockedFileZipUtil.studentSolutionZipInfo).toHaveBeenNthCalledWith(1, exercise); + expect(mockedFileZipUtil.filesFromZip).toHaveBeenCalledTimes(1); + }); + + it("should download teacher's solution when not published yet (fail scenario) (student)", async () => { + const user: User = { + id: 1, + roles: [{ + roleName: "ROLE_STUDENT", + }, { + roleName: "ROLE_TEACHER", + }], + username: "johndoejr" + }; + mockedCurrentUser.isLoggedIn.mockReturnValueOnce(true); + mockedCurrentUser.getUserInfo.mockReturnValueOnce(user); + + const exercise: Exercise = { + id: 2, + name: "Exercise", + includesTeacherSolution: true, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false + }; + extension.setFinishItem(exercise.id); + extension.setDownloadTeacherSolutionItem(exercise); + + mockedVscode.window.showInformationMessage.mockResolvedValueOnce({ title: "Accept" }); + + await commandFunctions["vscode4teaching.downloadteachersolution"](); + + expect(mockedVscode.window.showInformationMessage).toHaveBeenCalledTimes(1); + expect(mockedVscode.window.showInformationMessage).toHaveBeenNthCalledWith(1, "The solution will then be downloaded. Once downloaded, the exercise will be marked as finished and it will not be possible to continue editing it.", { modal: true }, { title: "Accept" }); + expect(mockedFileZipUtil.studentSolutionZipInfo).toHaveBeenCalledTimes(0); + expect(mockedFileZipUtil.filesFromZip).toHaveBeenCalledTimes(0); + }); + + /** + * Tree 1 + * Graphic representation: + * + * A + * ├─ B + * │ ├─ F + * │ ├─ G + * │ ├─ H + * ├─ C + * │ ├─ I + * ├─ D + * ├─ E + * │ ├─ J + * │ │ ├─ L + * │ │ ├─ M + * │ ├─ K + * │ │ ├─ N + */ + // Level 4 nodes + const tree1NodeL: BasicNode = { fileName: "L", children: [] }; + const tree1NodeM: BasicNode = { fileName: "M", children: [] }; + const tree1NodeN: BasicNode = { fileName: "N", children: [] }; + // Level 3 nodes + const tree1NodeF: BasicNode = { fileName: "F", children: [] }; + const tree1NodeG: BasicNode = { fileName: "G", children: [] }; + const tree1NodeH: BasicNode = { fileName: "H", children: [] }; + const tree1NodeI: BasicNode = { fileName: "I", children: [] }; + const tree1NodeJ: BasicNode = { fileName: "J", children: [tree1NodeL, tree1NodeM] }; + const tree1NodeK: BasicNode = { fileName: "K", children: [tree1NodeN] }; + // Level 2 nodes + const tree1NodeB: BasicNode = { fileName: "B", children: [tree1NodeF, tree1NodeG, tree1NodeH] }; + const tree1NodeC: BasicNode = { fileName: "C", children: [tree1NodeI] }; + const tree1NodeD: BasicNode = { fileName: "D", children: [] }; + const tree1NodeE: BasicNode = { fileName: "E", children: [tree1NodeJ, tree1NodeK] }; + // Level 1: root node + const tree1RootNode: BasicNode = { fileName: "A", children: [tree1NodeB, tree1NodeC, tree1NodeD, tree1NodeE] }; + + /** + * Tree 2 + * Graphic representation: + * + * Z + * ├─ B + * │ ├─ G + * │ ├─ H + * │ ├─ T + * ├─ C + * │ ├─ I + * ├─ E + * │ ├─ K + * │ │ ├─ N + * ├─ X + * │ ├─ S + * │ │ ├─ R + * ├─ Y + * │ ├─ U + * │ ├─ V + * │ ├─ W + */ + // Level 4 nodes + const tree2NodeN: BasicNode = { fileName: "N", children: [] }; + const tree2NodeR: BasicNode = { fileName: "R", children: [] }; + // Level 3 nodes + const tree2NodeG: BasicNode = { fileName: "G", children: [] }; + const tree2NodeH: BasicNode = { fileName: "H", children: [] }; + const tree2NodeT: BasicNode = { fileName: "T", children: [] }; + const tree2NodeI: BasicNode = { fileName: "I", children: [] }; + const tree2NodeK: BasicNode = { fileName: "K", children: [tree2NodeN] }; + const tree2NodeS: BasicNode = { fileName: "S", children: [tree2NodeR] }; + const tree2NodeU: BasicNode = { fileName: "U", children: [] }; + const tree2NodeV: BasicNode = { fileName: "V", children: [] }; + const tree2NodeW: BasicNode = { fileName: "W", children: [] }; + // Level 2 nodes + const tree2NodeB: BasicNode = { fileName: "B", children: [tree2NodeG, tree2NodeH, tree2NodeT] }; + const tree2NodeC: BasicNode = { fileName: "C", children: [tree2NodeI] }; + const tree2NodeE: BasicNode = { fileName: "E", children: [tree2NodeK] }; + const tree2NodeX: BasicNode = { fileName: "X", children: [tree2NodeS] }; + const tree2NodeY: BasicNode = { fileName: "Y", children: [tree2NodeU, tree2NodeV, tree2NodeW] }; + // Level 1: root node + const tree2RootNode: BasicNode = { fileName: "Z", children: [tree2NodeB, tree2NodeC, tree2NodeE, tree2NodeX, tree2NodeY] }; + + /** + * Tree 3 + * Graphic representation: + * + * A (0) + * ├─ B (0) + * │ ├─ F (-1) + * │ ├─ G (0) + * │ ├─ H (0) + * │ ├─ T (1) + * ├─ C (0) + * │ ├─ I (0) + * ├─ D (-1) + * ├─ E (0) + * │ ├─ J (-1) + * │ │ ├─ L (-1) + * │ │ ├─ M (-1) + * │ ├─ K (0) + * │ │ ├─ N (0) + * ├─ X (1) + * │ ├─ S (1) + * │ │ ├─ R (1) + * ├─ Y (1) + * │ ├─ U (1) + * │ ├─ V (1) + * │ ├─ W (1) + */ + // Level 4 nodes + const tree3NodeL: MergedTreeNode = { value: "L", source: -1, children: [], originalNodes: { left: tree1NodeL } }; + const tree3NodeM: MergedTreeNode = { value: "M", source: -1, children: [], originalNodes: { left: tree1NodeM } }; + const tree3NodeN: MergedTreeNode = { value: "N", source: 0, children: [], originalNodes: { left: tree1NodeN, right: tree2NodeN } }; + const tree3NodeR: MergedTreeNode = { value: "R", source: 1, children: [], originalNodes: { right: tree2NodeR } }; + // Level 3 nodes + const tree3NodeF: MergedTreeNode = { value: "F", source: -1, children: [], originalNodes: { left: tree1NodeF } }; + const tree3NodeG: MergedTreeNode = { value: "G", source: 0, children: [], originalNodes: { left: tree1NodeG, right: tree2NodeG } }; + const tree3NodeH: MergedTreeNode = { value: "H", source: 0, children: [], originalNodes: { left: tree1NodeH, right: tree2NodeH } }; + const tree3NodeT: MergedTreeNode = { value: "T", source: 1, children: [], originalNodes: { right: tree2NodeT } }; + const tree3NodeI: MergedTreeNode = { value: "I", source: 0, children: [], originalNodes: { left: tree1NodeI, right: tree2NodeI } }; + const tree3NodeJ: MergedTreeNode = { value: "J", source: -1, children: [tree3NodeL, tree3NodeM], originalNodes: { left: tree1NodeJ } }; + const tree3NodeK: MergedTreeNode = { value: "K", source: 0, children: [tree3NodeN], originalNodes: { left: tree1NodeK, right: tree2NodeK } }; + const tree3NodeS: MergedTreeNode = { value: "S", source: 1, children: [tree3NodeR], originalNodes: { right: tree2NodeS } }; + const tree3NodeU: MergedTreeNode = { value: "U", source: 1, children: [], originalNodes: { right: tree2NodeU } }; + const tree3NodeV: MergedTreeNode = { value: "V", source: 1, children: [], originalNodes: { right: tree2NodeV } }; + const tree3NodeW: MergedTreeNode = { value: "W", source: 1, children: [], originalNodes: { right: tree2NodeW } }; + // Level 2 nodes + const tree3NodeB: MergedTreeNode = { value: "B", source: 0, children: [tree3NodeF, tree3NodeG, tree3NodeH, tree3NodeT], originalNodes: { left: tree1NodeB, right: tree2NodeB } }; + const tree3NodeC: MergedTreeNode = { value: "C", source: 0, children: [tree3NodeI], originalNodes: { left: tree1NodeC, right: tree2NodeC } }; + const tree3NodeD: MergedTreeNode = { value: "D", source: -1, children: [], originalNodes: { left: tree1NodeD } }; + const tree3NodeE: MergedTreeNode = { value: "E", source: 0, children: [tree3NodeJ, tree3NodeK], originalNodes: { left: tree1NodeE, right: tree2NodeE } }; + const tree3NodeX: MergedTreeNode = { value: "X", source: 1, children: [tree3NodeS], originalNodes: { right: tree2NodeX } }; + const tree3NodeY: MergedTreeNode = { value: "Y", source: 1, children: [tree3NodeU, tree3NodeV, tree3NodeW], originalNodes: { right: tree2NodeY } }; + // Level 1: root node + const tree3RootNode: MergedTreeNode = { value: "A", source: 0, children: [tree3NodeB, tree3NodeC, tree3NodeD, tree3NodeE, tree3NodeX, tree3NodeY], originalNodes: { left: tree1RootNode, right: tree2RootNode } } + + /** + * Tree 4 + * Graphic representation: + * + * Same structure as tree 3. + */ + // Level 1: root node + const quickPickTreeRootNode: QuickPickTreeNode = { label: "$(folder) A", source: 0, description: "Available in both left and right folder", children: [], relativePath: "", parent: undefined }; + // Level 2 + const quickPickTreeNodeB: QuickPickTreeNode = { label: "$(folder) B", source: 0, description: "Available in both left and right folder", children: [], relativePath: "B", parent: quickPickTreeRootNode }; + const quickPickTreeNodeC: QuickPickTreeNode = { label: "$(folder) C", source: 0, description: "Available in both left and right folder", children: [], relativePath: "C", parent: quickPickTreeRootNode }; + const quickPickTreeNodeD: QuickPickTreeNode = { label: "$(file) D", source: -1, description: "Only available in left folder", children: [], relativePath: "D", parent: quickPickTreeRootNode }; + const quickPickTreeNodeE: QuickPickTreeNode = { label: "$(folder) E", source: 0, description: "Available in both left and right folder", children: [], relativePath: "E", parent: quickPickTreeRootNode }; + const quickPickTreeNodeX: QuickPickTreeNode = { label: "$(folder) X", source: 1, description: "Only available in right folder", children: [], relativePath: "X", parent: quickPickTreeRootNode }; + const quickPickTreeNodeY: QuickPickTreeNode = { label: "$(folder) Y", source: 1, description: "Only available in right folder", children: [], relativePath: "Y", parent: quickPickTreeRootNode }; + // Level 3 nodes + const quickPickTreeNodeF: QuickPickTreeNode = { label: "$(file) F", source: -1, description: "Only available in left folder", children: [], relativePath: "B/F", parent: quickPickTreeNodeB }; + const quickPickTreeNodeG: QuickPickTreeNode = { label: "$(file) G", source: 0, description: "Available in both left and right folder", children: [], relativePath: "B/G", parent: quickPickTreeNodeB }; + const quickPickTreeNodeH: QuickPickTreeNode = { label: "$(file) H", source: 0, description: "Available in both left and right folder", children: [], relativePath: "B/H", parent: quickPickTreeNodeB }; + const quickPickTreeNodeT: QuickPickTreeNode = { label: "$(file) T", source: 1, description: "Only available in right folder", children: [], relativePath: "B/T", parent: quickPickTreeNodeB }; + const quickPickTreeNodeI: QuickPickTreeNode = { label: "$(file) I", source: 0, description: "Available in both left and right folder", children: [], relativePath: "C/I", parent: quickPickTreeNodeC }; + const quickPickTreeNodeJ: QuickPickTreeNode = { label: "$(folder) J", source: -1, description: "Only available in left folder", children: [], relativePath: "E/J", parent: quickPickTreeNodeE }; + const quickPickTreeNodeK: QuickPickTreeNode = { label: "$(folder) K", source: 0, description: "Available in both left and right folder", children: [], relativePath: "E/K", parent: quickPickTreeNodeE }; + const quickPickTreeNodeS: QuickPickTreeNode = { label: "$(folder) S", source: 1, description: "Only available in right folder", children: [], relativePath: "X/S", parent: quickPickTreeNodeX }; + const quickPickTreeNodeU: QuickPickTreeNode = { label: "$(file) U", source: 1, description: "Only available in right folder", children: [], relativePath: "Y/U", parent: quickPickTreeNodeY }; + const quickPickTreeNodeV: QuickPickTreeNode = { label: "$(file) V", source: 1, description: "Only available in right folder", children: [], relativePath: "Y/V", parent: quickPickTreeNodeY }; + const quickPickTreeNodeW: QuickPickTreeNode = { label: "$(file) W", source: 1, description: "Only available in right folder", children: [], relativePath: "Y/W", parent: quickPickTreeNodeY }; + // Level 4 nodes + const quickPickTreeNodeL: QuickPickTreeNode = { label: "$(file) L", source: -1, description: "Only available in left folder", children: [], relativePath: "E/J/L", parent: quickPickTreeNodeJ }; + const quickPickTreeNodeM: QuickPickTreeNode = { label: "$(file) M", source: -1, description: "Only available in left folder", children: [], relativePath: "E/J/M", parent: quickPickTreeNodeJ }; + const quickPickTreeNodeN: QuickPickTreeNode = { label: "$(file) N", source: 0, description: "Available in both left and right folder", children: [], relativePath: "E/K/N", parent: quickPickTreeNodeK }; + const quickPickTreeNodeR: QuickPickTreeNode = { label: "$(file) R", source: 1, description: "Only available in right folder", children: [], relativePath: "X/S/R", parent: quickPickTreeNodeS }; + // Definition of children lists + // Level 1: root node + quickPickTreeRootNode.children = [quickPickTreeNodeB, quickPickTreeNodeC, quickPickTreeNodeD, quickPickTreeNodeE, quickPickTreeNodeX, quickPickTreeNodeY]; + // Level 2 + quickPickTreeNodeB.children = [quickPickTreeNodeF, quickPickTreeNodeG, quickPickTreeNodeH, quickPickTreeNodeT]; + quickPickTreeNodeC.children = [quickPickTreeNodeI]; + quickPickTreeNodeE.children = [quickPickTreeNodeJ, quickPickTreeNodeK]; + quickPickTreeNodeX.children = [quickPickTreeNodeS]; + quickPickTreeNodeY.children = [quickPickTreeNodeU, quickPickTreeNodeV, quickPickTreeNodeW]; + // Level 3 + quickPickTreeNodeJ.children = [quickPickTreeNodeL, quickPickTreeNodeM]; + quickPickTreeNodeK.children = [quickPickTreeNodeN]; + quickPickTreeNodeS.children = [quickPickTreeNodeR]; + + it("should let student check diff with solution when downloaded (case diff) (student)", async () => { + const uri = { + authority: "", + fragment: "", + fsPath: "test", + path: "test", + query: "", + scheme: "file", + with: jest.fn(), + toJSON: jest.fn(), + toString: jest.fn() + }; + + mockedVscode.workspace.workspaceFolders = [ + { + index: 0, + name: "Name", + uri + } + ]; + + mockedPath.resolve + // First call: parent directory + .mockReturnValueOnce("test") + // Second call: solution directory + .mockReturnValueOnce("test/solution"); + + + mockedDiffBetweenDirectories.deepFilteredDirectoryTraversal + .mockReturnValueOnce(tree1RootNode) + .mockReturnValueOnce(tree2RootNode); + + mockedDiffBetweenDirectories.mergeDirectoryTrees.mockReturnValueOnce(tree3RootNode); + + mockedDiffBetweenDirectories.mergedTreeToQuickPickTree.mockReturnValueOnce(quickPickTreeRootNode); + + mockedDiffBetweenDirectories.directorySelectionQuickPick.mockResolvedValueOnce({ + relativePath: "file", + source: 0 + }); + + mockedPath.join.mockImplementation(mockedPathJoin); + + + await commandFunctions["vscode4teaching.diffwithsolution"](); + + + expect(mockedDiffBetweenDirectories.deepFilteredDirectoryTraversal).toHaveBeenCalledTimes(2); + expect(mockedDiffBetweenDirectories.deepFilteredDirectoryTraversal).toHaveBeenNthCalledWith(1, "test", [/solution/, /^.*\.v4t$/]); + expect(mockedDiffBetweenDirectories.deepFilteredDirectoryTraversal).toHaveBeenNthCalledWith(2, "test/solution", [/^.*\.v4t$/]); + expect(mockedDiffBetweenDirectories.mergeDirectoryTrees).toHaveBeenCalledTimes(1); + expect(mockedDiffBetweenDirectories.mergeDirectoryTrees).toHaveBeenNthCalledWith(1, tree1RootNode, tree2RootNode); + expect(mockedVscode.commands.executeCommand).toHaveBeenCalledTimes(1); + expect(mockedVscode.commands.executeCommand).toHaveBeenNthCalledWith(1, "vscode.diff", { fsPath: "test/file" }, { fsPath: "test/solution/file" }); + }); + + it("should let student check diff with solution when downloaded (case open) (student)", async () => { + const uri = { + authority: "", + fragment: "", + fsPath: "test", + path: "test", + query: "", + scheme: "file", + with: jest.fn(), + toJSON: jest.fn(), + toString: jest.fn() + }; + + mockedVscode.workspace.workspaceFolders = [ + { + index: 0, + name: "Name", + uri + } + ]; + + mockedPath.resolve + // First call: parent directory + .mockReturnValueOnce("test") + // Second call: solution directory + .mockReturnValueOnce("test/solution"); + + + mockedDiffBetweenDirectories.deepFilteredDirectoryTraversal + .mockReturnValueOnce(tree1RootNode) + .mockReturnValueOnce(tree2RootNode); + + mockedDiffBetweenDirectories.mergeDirectoryTrees.mockReturnValueOnce(tree3RootNode); + + mockedDiffBetweenDirectories.mergedTreeToQuickPickTree.mockReturnValueOnce(quickPickTreeRootNode); + + mockedDiffBetweenDirectories.directorySelectionQuickPick.mockResolvedValueOnce({ + relativePath: "otherFile", + source: 1 + }); + + mockedPath.join.mockImplementation(mockedPathJoin); + + + await commandFunctions["vscode4teaching.diffwithsolution"](); + + + expect(mockedDiffBetweenDirectories.deepFilteredDirectoryTraversal).toHaveBeenCalledTimes(2); + expect(mockedDiffBetweenDirectories.deepFilteredDirectoryTraversal).toHaveBeenNthCalledWith(1, "test", [/solution/, /^.*\.v4t$/]); + expect(mockedDiffBetweenDirectories.deepFilteredDirectoryTraversal).toHaveBeenNthCalledWith(2, "test/solution", [/^.*\.v4t$/]); + expect(mockedDiffBetweenDirectories.mergeDirectoryTrees).toHaveBeenCalledTimes(1); + expect(mockedDiffBetweenDirectories.mergeDirectoryTrees).toHaveBeenNthCalledWith(1, tree1RootNode, tree2RootNode); + expect(mockedVscode.commands.executeCommand).toHaveBeenCalledTimes(1); + expect(mockedVscode.commands.executeCommand).toHaveBeenNthCalledWith(1, "vscode.open", { fsPath: "test/solution/otherFile" }); + }); + }); diff --git a/vscode4teaching-extension/test/unitSuite/CommentService.test.ts b/vscode4teaching-extension/test/unitSuite/CommentService.test.ts index e361e9bb..09b496c9 100644 --- a/vscode4teaching-extension/test/unitSuite/CommentService.test.ts +++ b/vscode4teaching-extension/test/unitSuite/CommentService.test.ts @@ -20,12 +20,9 @@ describe("Comment Service", () => { const author = "johndoe"; beforeEach(() => { - commentProvider = new TeacherCommentService(author); - }); + jest.clearAllMocks(); - afterEach(() => { - mockedVscode.comments.createCommentController.mockClear(); - mockedVscode.workspace.openTextDocument.mockClear(); + commentProvider = new TeacherCommentService(author); }); it("should create correctly", () => { @@ -75,6 +72,7 @@ describe("Comment Service", () => { uri: mockedVscode.Uri.parse("testURL"), range: rangeMock, collapsibleState: mockedVscode.CommentThreadCollapsibleState.Expanded, + canReply: true, comments: commentsMock, dispose: jest.fn(), }; @@ -254,21 +252,21 @@ describe("Comment Service", () => { isUntitled: false, languageId: "json", lineAt: jest.fn().mockReturnValueOnce({ - firstNonWhitespaceCharacterIndex: 0, - isEmptyOrWhitespace: false, - lineNumber: comments[0].line, - range: rangeMock1, - rangeIncludingLineBreak: rangeMock1, - text: comments[0].lineText, - }, + firstNonWhitespaceCharacterIndex: 0, + isEmptyOrWhitespace: false, + lineNumber: comments[0].line, + range: rangeMock1, + rangeIncludingLineBreak: rangeMock1, + text: comments[0].lineText, + }, ).mockReturnValueOnce({ - firstNonWhitespaceCharacterIndex: 0, - isEmptyOrWhitespace: false, - lineNumber: comments[1].line, - range: rangeMock2, - rangeIncludingLineBreak: rangeMock2, - text: comments[1].lineText, - }, + firstNonWhitespaceCharacterIndex: 0, + isEmptyOrWhitespace: false, + lineNumber: comments[1].line, + range: rangeMock2, + rangeIncludingLineBreak: rangeMock2, + text: comments[1].lineText, + }, ), lineCount: 5, offsetAt: jest.fn(), @@ -332,11 +330,12 @@ describe("Comment Service", () => { }; mockedClient.updateCommentThreadLine.mockResolvedValueOnce(response); const thread = { - dispose: jest.fn(), + uri: mockedVscode.Uri.file("file"), range: rangeMock, comments: [], - uri: mockedVscode.Uri.file("file"), collapsibleState: mockedVscode.CommentThreadCollapsibleState.Collapsed, + canReply: true, + dispose: jest.fn(), }; commentProvider.setThread(threadId, thread); commentProvider.updateThreadLine(threadId, line, lineText); diff --git a/vscode4teaching-extension/test/unitSuite/CurrentUser.test.ts b/vscode4teaching-extension/test/unitSuite/CurrentUser.test.ts index 078361ee..28327fa6 100644 --- a/vscode4teaching-extension/test/unitSuite/CurrentUser.test.ts +++ b/vscode4teaching-extension/test/unitSuite/CurrentUser.test.ts @@ -10,9 +10,12 @@ jest.mock("../../src/client/APIClient"); const mockedClient = mocked(APIClient, true); describe("Current user", () => { - afterEach(() => { + beforeEach(() => { + jest.clearAllMocks(); + CurrentUser.resetUserInfo(); }); + it("should update user info", async () => { const user: User = { id: 40, @@ -84,9 +87,15 @@ describe("Current user", () => { const exercises: Exercise[] = [{ id: 21, name: "Test exercise 1", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false }, { id: 22, name: "Test exercise 2", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false }]; const newCourse: Course = { id: 20, diff --git a/vscode4teaching-extension/test/unitSuite/DashboardWebview.test.ts b/vscode4teaching-extension/test/unitSuite/DashboardWebview.test.ts index aae49b11..ad41e1a7 100644 --- a/vscode4teaching-extension/test/unitSuite/DashboardWebview.test.ts +++ b/vscode4teaching-extension/test/unitSuite/DashboardWebview.test.ts @@ -1,31 +1,42 @@ import { parse } from 'node-html-parser'; import { mocked } from "ts-jest/utils"; import * as vscode from "vscode"; -import { WebSocketV4TConnection } from "../../src/client/WebSocketV4TConnection"; -import { DashboardWebview } from "../../src/components/dashboard/DashboardWebview"; +import { DashboardWebview } from '../../src/components/dashboard/DashboardWebview'; import { Course } from "../../src/model/serverModel/course/Course"; import { Exercise } from "../../src/model/serverModel/exercise/Exercise"; +import { ExerciseStatus } from '../../src/model/serverModel/exercise/ExerciseStatus'; import { ExerciseUserInfo } from "../../src/model/serverModel/exercise/ExerciseUserInfo"; import { User } from "../../src/model/serverModel/user/User"; jest.mock("vscode"); const mockedVscode = mocked(vscode, true); -jest.mock("../../src/client/WebSocketV4TConnection"); -const mockedWebSocketV4TConnection = mocked(WebSocketV4TConnection, true); jest.useFakeTimers(); -describe("Dashboard webview", () => { +describe("Dashboard Webview", () => { + beforeEach(() => { + Date.now = jest.fn(() => new Date().valueOf()); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + it("should be created if it doesn't exist", () => { const course: Course = { id: 1, name: "Course", exercises: [], }; + const exercise: Exercise = { id: 1, name: "Exercise 1", + includesTeacherSolution: true, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: true }; + const student1: User = { id: 2, username: "student1", @@ -47,37 +58,54 @@ describe("Dashboard webview", () => { lastName: "3", roles: [{ roleName: "ROLE_STUDENT" }], }; + const student4: User = { + id: 5, + username: "student4", + name: "Student", + lastName: "4", + roles: [{ roleName: "ROLE_STUDENT" }], + }; + const euis: ExerciseUserInfo[] = []; - let now = new Date(new Date().toLocaleString("en-US", { timeZone: "UTC" })); + let now = new Date(new Date().toLocaleString("en-US")); euis.push({ id: 1, exercise, user: student1, - status: 0, - updateDateTime: new Date(new Date(now.setDate(now.getDate() - 1)).toISOString()).toISOString(), + status: ExerciseStatus.StatusEnum.NOT_STARTED, + updateDateTime: new Date(new Date(now.setHours(now.getHours() - 3)).toISOString()).toISOString(), modifiedFiles: ["/index.html"], }); - now = new Date(new Date().toLocaleString("en-US", { timeZone: "UTC" })); + now = new Date(new Date().toLocaleString("en-US")); euis.push({ id: 2, exercise, user: student2, - status: 1, + status: ExerciseStatus.StatusEnum.FINISHED, updateDateTime: new Date(new Date(now.setMinutes(now.getMinutes() - 13)).toISOString()).toISOString(), modifiedFiles: ["/readme.md"], }); - now = new Date(new Date().toLocaleString("en-US", { timeZone: "UTC" })); + now = new Date(new Date().toLocaleString("en-US")); euis.push({ id: 3, exercise, user: student3, - status: 2, + status: ExerciseStatus.StatusEnum.IN_PROGRESS, updateDateTime: new Date(new Date(now.setSeconds(now.getSeconds() - 35)).toISOString()).toISOString(), modifiedFiles: undefined, }); + euis.push({ + id: 4, + exercise, + user: student4, + status: ExerciseStatus.StatusEnum.IN_PROGRESS, + updateDateTime: new Date(new Date(now.setHours(now.getHours() - 1)).toISOString()).toISOString(), + modifiedFiles: undefined, + }); + DashboardWebview.show(euis, course, exercise, true); + if (DashboardWebview.currentPanel) { - expect(global.setInterval).toHaveBeenCalledTimes(1); expect(mockedVscode.window.createWebviewPanel).toHaveBeenCalledTimes(1); expect(mockedVscode.window.createWebviewPanel.mock.calls[0][0]).toBe("v4tdashboard"); expect(mockedVscode.window.createWebviewPanel.mock.calls[0][1]).toBe("V4T Dashboard: Exercise 1"); @@ -88,54 +116,128 @@ describe("Dashboard webview", () => { } else { fail("Webview options argument missing"); } - const $ = parse(DashboardWebview.currentPanel.panel.webview.html); - // Title is correct - const title = $.querySelector("h2"); - expect(title?.innerText).toBe("Course - Exercise 1"); - // Hide student's names button exists and is checked on - const hideStudentsNames = $.querySelector("#hideStudentNames"); - expect(hideStudentsNames).not.toBeNull(); - expect(hideStudentsNames?.attributes["checked"]).toBeDefined(); - // Table headers are correct - const tableHeaders = $.querySelectorAll("th"); + + expect(global.setInterval).toHaveBeenCalledTimes(2); + + const generatedHTML = parse(DashboardWebview.currentPanel.panel.webview.html); + // HTML is correct + const tabTitle = generatedHTML.querySelector("title"); + expect(tabTitle?.innerHTML).toBe("V4T Dashboard: Exercise 1"); + // "Preview mode alert" and "No students registered" alertsshould not be shown + const shownAlerts = generatedHTML.querySelector(".alert-info"); + expect(shownAlerts).toBeNull(); + // Headers (H1 and H2) are correct + const h1 = generatedHTML.querySelector("h1"); + expect(h1?.innerText).toBe("Course"); + const h2 = generatedHTML.querySelector("h2"); + expect(h2?.innerText).toBe("Exercise 1"); + // General Statistics > Column 1 (chart) should receive proper values + const canvas = generatedHTML.querySelectorAll("canvas"); + expect(canvas.length).toBe(1); + expect(canvas[0].attributes["data-notstarted"]).toBe("1"); + expect(canvas[0].attributes["data-inprogress"]).toBe("2"); + expect(canvas[0].attributes["data-finished"]).toBe("1"); + // General Statistics > Column 2 contains proper values + const rowTotals = generatedHTML.querySelectorAll(".rowTotals"); + expect(rowTotals.length).toBe(1); + const rowTotalsValue = rowTotals[0].querySelectorAll(".value"); + expect(rowTotalsValue.length).toBe(1); + expect(rowTotalsValue[0].innerText).toBe("4"); + const rowsStatusChildren = generatedHTML.querySelectorAll(".rowStatus > .status"); + expect(rowsStatusChildren.length).toBe(3); + const rowStatusChildrenNotStarted = rowsStatusChildren[0].querySelectorAll(".value"); + expect(rowStatusChildrenNotStarted.length).toBe(1); + expect(rowStatusChildrenNotStarted[0].innerText).toBe("1"); + const rowStatusChildrenInProgress = rowsStatusChildren[1].querySelectorAll(".value"); + expect(rowStatusChildrenInProgress.length).toBe(1); + expect(rowStatusChildrenInProgress[0].innerText).toBe("2"); + const rowStatusChildrenFinished = rowsStatusChildren[2].querySelectorAll(".value"); + expect(rowStatusChildrenFinished.length).toBe(1); + expect(rowStatusChildrenFinished[0].innerText).toBe("1"); + // General Statistics > Column 3 contains proper values + const rowsTime = generatedHTML.querySelectorAll(".rowTime"); + expect(rowsTime.length).toBe(4); + const rowsTimeValue5Min = rowsTime[0].querySelector("#timeValue5"); + expect(rowsTimeValue5Min?.innerText).toBe("1"); + const rowsTimeValue30Min = rowsTime[1].querySelector("#timeValue30"); + expect(rowsTimeValue30Min?.innerText).toBe("2"); + const rowsTimeValue60Min = rowsTime[2].querySelector("#timeValue60"); + expect(rowsTimeValue60Min?.innerText).toBe("2"); + const rowsTimeValue120Min = rowsTime[3].querySelector("#timeValue120"); + expect(rowsTimeValue120Min?.innerText).toBe("3"); + // Exercise Configuration contains proper checkboxes + // For specified expected exercise, first checkbox should be unchecked and second should be checked + const checkboxes = generatedHTML.querySelectorAll(".exerciseConfiguration .option"); + expect(checkboxes.length).toBe(2); + const checkboxPublishSolution = checkboxes[0].querySelector("#publishSolution"); + expect(checkboxPublishSolution?.attributes["checked"]).toBeUndefined(); + expect(checkboxPublishSolution?.attributes["disabled"]).toBeUndefined(); + const checkboxAllowEditionAfterSolutionDownloaded = checkboxes[1].querySelector("#allowEditionAfterSolutionDownloaded"); + expect(checkboxAllowEditionAfterSolutionDownloaded?.attributes["checked"]).not.toBeUndefined(); + expect(checkboxAllowEditionAfterSolutionDownloaded?.attributes["disabled"]).toBeUndefined(); + // Student's progress table does not show student's names + const hideStudentsNamesOption = generatedHTML.querySelectorAll(".studentsProgress .option"); + expect(hideStudentsNamesOption.length).toBe(1); + const hideStudentsNamesInput = hideStudentsNamesOption[0].querySelector("input"); + expect(hideStudentsNamesInput?.attributes["checked"]).not.toBeUndefined(); + // Student's progress table contains proper values + const tableHeaders = generatedHTML.querySelectorAll("th"); expect(tableHeaders.length).toBe(4); expect(tableHeaders[0].innerText.trim()).toBe("Exercise folder"); expect(tableHeaders[1].innerText.trim()).toBe("Exercise status"); - expect(tableHeaders[2].innerText.trim()).toBe("Last modified file"); - expect(tableHeaders[3].innerText.trim()).toBe("Last modification"); + expect(tableHeaders[2].innerText.trim()).toBe("Last modification"); + expect(tableHeaders[3].innerText.trim()).toBe("Actions"); // Table data is correct - const tableData = $.querySelectorAll("td"); - expect(tableData.length).toBe(4 * 3); // 4 columns * 3 students; + const tableData = generatedHTML.querySelectorAll("td"); + expect(tableData.length).toBe(4 * 4); // 4 columns * 4 students + // Row 1: student 1 // Cell 0: exercise folder expect(tableData[0].innerText).toBe("student_1"); // Cell 1: exercise status expect(tableData[1].innerText).toBe("Not started"); expect(tableData[1].classNames).toBe("not-started-cell"); - // Cell 2: last modified file - expect(tableData[2].childNodes.length).toBe(2); - expect(tableData[2].childNodes[0].innerText).toBe("Open"); - expect(tableData[2].childNodes[1].innerText).toBe("Diff"); - // Cell 3: last modification + // Cell 2: last modification + expect(tableData[2].innerText).toBe("3 h"); + // Cell 3: actions + expect(tableData[3].childNodes.length).toBe(2); + expect(tableData[3].childNodes[0].innerText).toBe("Open"); + expect(tableData[3].childNodes[1].innerText).toBe("Diff"); + // Row 2: student 2 // Cell 4: exercise folder expect(tableData[4].innerText).toBe("student_2"); // Cell 5: exercise status expect(tableData[5].innerText).toBe("Finished"); expect(tableData[5].classNames).toBe("finished-cell"); - // Cell 6: last modified file - expect(tableData[6].childNodes.length).toBe(2); - expect(tableData[6].childNodes[0].innerText).toBe("Open"); - expect(tableData[6].childNodes[1].innerText).toBe("Diff"); - // Cell 7: last modification + // Cell 6: last modification + expect(tableData[6].innerText).toBe("13 min"); + // Cell 7: actions + expect(tableData[7].childNodes.length).toBe(2); + expect(tableData[7].childNodes[0].innerText).toBe("Open"); + expect(tableData[7].childNodes[1].innerText).toBe("Diff"); + // Row 3: student 3 // Cell 8: exercise folder expect(tableData[8].innerText).toBe("student_3"); // Cell 9: exercise status - expect(tableData[9].innerText).toBe("On progress"); - expect(tableData[9].classNames).toBe("onprogress-cell"); - // Cell 10: last modified file - expect(tableData[10].childNodes.length).toBe(2); - expect(tableData[10].childNodes[0].innerText).toBe("Open"); - expect(tableData[10].childNodes[1].innerText).toBe("Diff"); - // Cell 11: last modification + expect(tableData[9].innerText).toBe("In progress"); + expect(tableData[9].classNames).toBe("inprogress-cell"); + // Cell 10: last modification + expect(tableData[10].innerText).toBe("35 s"); + // Cell 11: actions + expect(tableData[11].childNodes.length).toBe(2); + expect(tableData[11].childNodes[0].innerText).toBe("Open"); + expect(tableData[11].childNodes[1].innerText).toBe("Diff"); + // Row 3: student 3 + // Cell 12: exercise folder + expect(tableData[12].innerText).toBe("student_4"); + // Cell 13: exercise status + expect(tableData[13].innerText).toBe("In progress"); + expect(tableData[13].classNames).toBe("inprogress-cell"); + // Cell 14: last modification + expect(tableData[14].innerText).toBe("1 h"); + // Cell 15: actions + expect(tableData[15].childNodes.length).toBe(2); + expect(tableData[15].childNodes[0].innerText).toBe("Open"); + expect(tableData[15].childNodes[1].innerText).toBe("Diff"); } else { fail("Current panel wasn't created"); } diff --git a/vscode4teaching-extension/test/unitSuite/DiffBetweenDirectories.test.ts b/vscode4teaching-extension/test/unitSuite/DiffBetweenDirectories.test.ts new file mode 100644 index 00000000..d48b5921 --- /dev/null +++ b/vscode4teaching-extension/test/unitSuite/DiffBetweenDirectories.test.ts @@ -0,0 +1,398 @@ +import * as fs from "fs"; +import { mocked } from "ts-jest/utils"; +import { BasicNode, DiffBetweenDirectories, MergedTreeNode, QuickPickTreeNode } from "../../src/utils/DiffBetweenDirectories"; +import { mockFsDirent, mockFsStatus } from "./__mocks__/mockFsUtils"; + +jest.mock("fs"); +const mockedFs = mocked(fs, true); + +describe("Diff between directories Utilities", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + // Definiciones de datos iniciales + /** + * Tree 1: BasicNode example tree + * Graphic representation: + * + * A + * ├─ B + * │ ├─ F + * │ ├─ G + * │ ├─ H + * ├─ C + * │ ├─ I + * ├─ D + * ├─ E + * │ ├─ J + * │ │ ├─ L + * │ │ ├─ M + * │ ├─ K + * │ │ ├─ N + */ + // Level 4 nodes + const tree1NodeL: BasicNode = { fileName: "L", children: [] }; + const tree1NodeM: BasicNode = { fileName: "M", children: [] }; + const tree1NodeN: BasicNode = { fileName: "N", children: [] }; + // Level 3 nodes + const tree1NodeF: BasicNode = { fileName: "F", children: [] }; + const tree1NodeG: BasicNode = { fileName: "G", children: [] }; + const tree1NodeH: BasicNode = { fileName: "H", children: [] }; + const tree1NodeI: BasicNode = { fileName: "I", children: [] }; + const tree1NodeJ: BasicNode = { fileName: "J", children: [tree1NodeL, tree1NodeM] }; + const tree1NodeK: BasicNode = { fileName: "K", children: [tree1NodeN] }; + // Level 2 nodes + const tree1NodeB: BasicNode = { fileName: "B", children: [tree1NodeF, tree1NodeG, tree1NodeH] }; + const tree1NodeC: BasicNode = { fileName: "C", children: [tree1NodeI] }; + const tree1NodeD: BasicNode = { fileName: "D", children: [] }; + const tree1NodeE: BasicNode = { fileName: "E", children: [tree1NodeJ, tree1NodeK] }; + // Level 1: root node + const tree1RootNode: BasicNode = { fileName: "A", children: [tree1NodeB, tree1NodeC, tree1NodeD, tree1NodeE] }; + + /** + * Tree 2: BasicNode example tree + * Graphic representation: + * + * Z + * ├─ B + * │ ├─ G + * │ ├─ H + * │ ├─ T + * ├─ C + * │ ├─ I + * ├─ E + * │ ├─ K + * │ │ ├─ N + * ├─ X + * │ ├─ S + * │ │ ├─ R + * ├─ Y + * │ ├─ U + * │ ├─ V + * │ ├─ W + */ + // Level 4 nodes + const tree2NodeN: BasicNode = { fileName: "N", children: [] }; + const tree2NodeR: BasicNode = { fileName: "R", children: [] }; + // Level 3 nodes + const tree2NodeG: BasicNode = { fileName: "G", children: [] }; + const tree2NodeH: BasicNode = { fileName: "H", children: [] }; + const tree2NodeT: BasicNode = { fileName: "T", children: [] }; + const tree2NodeI: BasicNode = { fileName: "I", children: [] }; + const tree2NodeK: BasicNode = { fileName: "K", children: [tree2NodeN] }; + const tree2NodeS: BasicNode = { fileName: "S", children: [tree2NodeR] }; + const tree2NodeU: BasicNode = { fileName: "U", children: [] }; + const tree2NodeV: BasicNode = { fileName: "V", children: [] }; + const tree2NodeW: BasicNode = { fileName: "W", children: [] }; + // Level 2 nodes + const tree2NodeB: BasicNode = { fileName: "B", children: [tree2NodeG, tree2NodeH, tree2NodeT] }; + const tree2NodeC: BasicNode = { fileName: "C", children: [tree2NodeI] }; + const tree2NodeE: BasicNode = { fileName: "E", children: [tree2NodeK] }; + const tree2NodeX: BasicNode = { fileName: "X", children: [tree2NodeS] }; + const tree2NodeY: BasicNode = { fileName: "Y", children: [tree2NodeU, tree2NodeV, tree2NodeW] }; + // Level 1: root node + const tree2RootNode: BasicNode = { fileName: "Z", children: [tree2NodeB, tree2NodeC, tree2NodeE, tree2NodeX, tree2NodeY] }; + + /** + * Tree 3: MergedTreeNode example tree (result of merging tree 1 as left and tree 2 as right) + * Graphic representation (with sources in parentheses): + * + * A (0) + * ├─ B (0) + * │ ├─ F (-1) + * │ ├─ G (0) + * │ ├─ H (0) + * │ ├─ T (1) + * ├─ C (0) + * │ ├─ I (0) + * ├─ D (-1) + * ├─ E (0) + * │ ├─ J (-1) + * │ │ ├─ L (-1) + * │ │ ├─ M (-1) + * │ ├─ K (0) + * │ │ ├─ N (0) + * ├─ X (1) + * │ ├─ S (1) + * │ │ ├─ R (1) + * ├─ Y (1) + * │ ├─ U (1) + * │ ├─ V (1) + * │ ├─ W (1) + */ + // Level 4 nodes + const tree3NodeL: MergedTreeNode = { value: "L", source: -1, children: [], originalNodes: { left: tree1NodeL } }; + const tree3NodeM: MergedTreeNode = { value: "M", source: -1, children: [], originalNodes: { left: tree1NodeM } }; + const tree3NodeN: MergedTreeNode = { value: "N", source: 0, children: [], originalNodes: { left: tree1NodeN, right: tree2NodeN } }; + const tree3NodeR: MergedTreeNode = { value: "R", source: 1, children: [], originalNodes: { right: tree2NodeR } }; + // Level 3 nodes + const tree3NodeF: MergedTreeNode = { value: "F", source: -1, children: [], originalNodes: { left: tree1NodeF } }; + const tree3NodeG: MergedTreeNode = { value: "G", source: 0, children: [], originalNodes: { left: tree1NodeG, right: tree2NodeG } }; + const tree3NodeH: MergedTreeNode = { value: "H", source: 0, children: [], originalNodes: { left: tree1NodeH, right: tree2NodeH } }; + const tree3NodeT: MergedTreeNode = { value: "T", source: 1, children: [], originalNodes: { right: tree2NodeT } }; + const tree3NodeI: MergedTreeNode = { value: "I", source: 0, children: [], originalNodes: { left: tree1NodeI, right: tree2NodeI } }; + const tree3NodeJ: MergedTreeNode = { value: "J", source: -1, children: [tree3NodeL, tree3NodeM], originalNodes: { left: tree1NodeJ } }; + const tree3NodeK: MergedTreeNode = { value: "K", source: 0, children: [tree3NodeN], originalNodes: { left: tree1NodeK, right: tree2NodeK } }; + const tree3NodeS: MergedTreeNode = { value: "S", source: 1, children: [tree3NodeR], originalNodes: { right: tree2NodeS } }; + const tree3NodeU: MergedTreeNode = { value: "U", source: 1, children: [], originalNodes: { right: tree2NodeU } }; + const tree3NodeV: MergedTreeNode = { value: "V", source: 1, children: [], originalNodes: { right: tree2NodeV } }; + const tree3NodeW: MergedTreeNode = { value: "W", source: 1, children: [], originalNodes: { right: tree2NodeW } }; + // Level 2 nodes + const tree3NodeB: MergedTreeNode = { value: "B", source: 0, children: [tree3NodeF, tree3NodeG, tree3NodeH, tree3NodeT], originalNodes: { left: tree1NodeB, right: tree2NodeB } }; + const tree3NodeC: MergedTreeNode = { value: "C", source: 0, children: [tree3NodeI], originalNodes: { left: tree1NodeC, right: tree2NodeC } }; + const tree3NodeD: MergedTreeNode = { value: "D", source: -1, children: [], originalNodes: { left: tree1NodeD } }; + const tree3NodeE: MergedTreeNode = { value: "E", source: 0, children: [tree3NodeJ, tree3NodeK], originalNodes: { left: tree1NodeE, right: tree2NodeE } }; + const tree3NodeX: MergedTreeNode = { value: "X", source: 1, children: [tree3NodeS], originalNodes: { right: tree2NodeX } }; + const tree3NodeY: MergedTreeNode = { value: "Y", source: 1, children: [tree3NodeU, tree3NodeV, tree3NodeW], originalNodes: { right: tree2NodeY } }; + // Level 1: root node + const tree3RootNode: MergedTreeNode = { value: "A", source: 0, children: [tree3NodeB, tree3NodeC, tree3NodeD, tree3NodeE, tree3NodeX, tree3NodeY], originalNodes: { left: tree1RootNode, right: tree2RootNode } } + + it("should deeply traverse a directory (recursively) and return a tree", () => { + /** + * A directory structure is simulated as shown below: + * A + * ├─ B + * │ ├─ F + * ├─ C + * │ ├─ G + * │ │ ├─ J + * │ │ ├─ K + * │ ├─ H + * ├─ D + * ├─ E + * │ ├─ I + * │ │ ├─ L + * ├─ M <- (filtered using regex) + */ + const expectedResult: BasicNode = { + fileName: "A", + children: [ + { + fileName: "B", + children: [ + { + fileName: "F", + children: [] + } + ] + }, + { + fileName: "C", + children: [ + { + fileName: "G", + children: [ + { + fileName: "J", + children: [] + }, + { + fileName: "K", + children: [] + }, + ] + }, + { + fileName: "H", + children: [] + }, + ] + }, + { + fileName: "D", + children: [] + }, + { + fileName: "E", + children: [ + { + fileName: "I", + children: [ + { + fileName: "L", + children: [] + }, + ] + }, + ] + } + ] + }; + + // Calls to fs library methods must be mocked according to the parameters that they receive + // For this purpose, implementations for readdirSync() and statSync() are being replaced as follows: + mockedFs.readdirSync.mockImplementation((absolutePath, _) => { + if (absolutePath === "A") + return [ + { name: "B", isDirectory: true }, + { name: "C", isDirectory: true }, + { name: "D", isDirectory: false }, + { name: "E", isDirectory: true }, + { name: "M", isDirectory: false } + ].map(elem => mockFsDirent(elem.name, elem.isDirectory)); + else if (absolutePath === "A/B") + return [mockFsDirent("F", false)]; + else if (absolutePath === "A/C") + return [ + { name: "G", isDirectory: true }, + { name: "H", isDirectory: false } + ].map(elem => mockFsDirent(elem.name, elem.isDirectory)); + else if (absolutePath === "A/C/G") + return [ + { name: "J", isDirectory: false }, + { name: "K", isDirectory: false } + ].map(elem => mockFsDirent(elem.name, elem.isDirectory)); + else if (absolutePath === "A/E") + return [mockFsDirent("I", true)]; + else if (absolutePath === "A/E/I") + return [mockFsDirent("L", false)]; + else return []; + }); + + mockedFs.statSync.mockImplementation((path) => { + return mockFsStatus([ + "A", + "A/B", + "A/C", + "A/C/G", + "A/E", + "A/E/I" + ].some(elem => elem === path)); + }); + + + // The method to be tested is called + // Absolute path is root directory of simulated file structure and "M" is going to be filtered + const actualResult = DiffBetweenDirectories.deepFilteredDirectoryTraversal("A", [/M/]); + + + expect(mockedFs.readdirSync).toHaveBeenCalledTimes(6); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(1, "A", { withFileTypes: true }); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(2, "A/B", { withFileTypes: true }); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(3, "A/C", { withFileTypes: true }); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(4, "A/C/G", { withFileTypes: true }); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(5, "A/E", { withFileTypes: true }); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(6, "A/E/I", { withFileTypes: true }); + + expect(mockedFs.statSync).toHaveBeenCalledTimes(11); + expect(mockedFs.statSync).toHaveBeenNthCalledWith(1, "A/B"); + expect(mockedFs.statSync).toHaveBeenNthCalledWith(2, "A/B/F"); + expect(mockedFs.statSync).toHaveBeenNthCalledWith(3, "A/C"); + expect(mockedFs.statSync).toHaveBeenNthCalledWith(4, "A/C/G"); + expect(mockedFs.statSync).toHaveBeenNthCalledWith(5, "A/C/G/J"); + expect(mockedFs.statSync).toHaveBeenNthCalledWith(6, "A/C/G/K"); + expect(mockedFs.statSync).toHaveBeenNthCalledWith(7, "A/C/H"); + expect(mockedFs.statSync).toHaveBeenNthCalledWith(8, "A/D"); + expect(mockedFs.statSync).toHaveBeenNthCalledWith(9, "A/E"); + expect(mockedFs.statSync).toHaveBeenNthCalledWith(10, "A/E/I"); + expect(mockedFs.statSync).toHaveBeenNthCalledWith(11, "A/E/I/L"); + + expect(actualResult).toStrictEqual(expectedResult); + }); + + it("should correctly merge two trees (case 1)", () => { + // Trees 1 and 2 are going to be merged + // This test takes tree 1 as left one and tree 2 as right one + + // The method to be tested is called + const actualResult = DiffBetweenDirectories.mergeDirectoryTrees(tree1RootNode, tree2RootNode); + + + // Expected result is tree 3 + expect(actualResult).toStrictEqual(tree3RootNode); + }); + + it("should correctly merge two trees (case 2)", () => { + // Trees 1 and 2 are going to be merged + // This test takes tree 2 as left one and tree 3 as right one + + // Expected result is tree 3 changing the sign of sources and originalNodes (left by right and vice versa) + // Level 4 nodes + const tree3InverseNodeL = { value: "L", source: 1, children: [], originalNodes: { right: tree1NodeL } }; + const tree3InverseNodeM = { value: "M", source: 1, children: [], originalNodes: { right: tree1NodeM } }; + const tree3InverseNodeN = { value: "N", source: 0, children: [], originalNodes: { right: tree1NodeN, left: tree2NodeN } }; + const tree3InverseNodeR = { value: "R", source: -1, children: [], originalNodes: { left: tree2NodeR } }; + // Level 3 nodes + const tree3InverseNodeF = { value: "F", source: 1, children: [], originalNodes: { right: tree1NodeF } }; + const tree3InverseNodeG = { value: "G", source: 0, children: [], originalNodes: { right: tree1NodeG, left: tree2NodeG } }; + const tree3InverseNodeH = { value: "H", source: 0, children: [], originalNodes: { right: tree1NodeH, left: tree2NodeH } }; + const tree3InverseNodeT = { value: "T", source: -1, children: [], originalNodes: { left: tree2NodeT } }; + const tree3InverseNodeI = { value: "I", source: 0, children: [], originalNodes: { right: tree1NodeI, left: tree2NodeI } }; + const tree3InverseNodeJ = { value: "J", source: 1, children: [tree3InverseNodeL, tree3InverseNodeM], originalNodes: { right: tree1NodeJ } }; + const tree3InverseNodeK = { value: "K", source: 0, children: [tree3InverseNodeN], originalNodes: { right: tree1NodeK, left: tree2NodeK } }; + const tree3InverseNodeS = { value: "S", source: -1, children: [tree3InverseNodeR], originalNodes: { left: tree2NodeS } }; + const tree3InverseNodeU = { value: "U", source: -1, children: [], originalNodes: { left: tree2NodeU } }; + const tree3InverseNodeV = { value: "V", source: -1, children: [], originalNodes: { left: tree2NodeV } }; + const tree3InverseNodeW = { value: "W", source: -1, children: [], originalNodes: { left: tree2NodeW } }; + // Level 2 nodes + const tree3InverseNodeB = { value: "B", source: 0, children: [tree3InverseNodeF, tree3InverseNodeG, tree3InverseNodeH, tree3InverseNodeT], originalNodes: { right: tree1NodeB, left: tree2NodeB } }; + const tree3InverseNodeC = { value: "C", source: 0, children: [tree3InverseNodeI], originalNodes: { right: tree1NodeC, left: tree2NodeC } }; + const tree3InverseNodeD = { value: "D", source: 1, children: [], originalNodes: { right: tree1NodeD } }; + const tree3InverseNodeE = { value: "E", source: 0, children: [tree3InverseNodeJ, tree3InverseNodeK], originalNodes: { right: tree1NodeE, left: tree2NodeE } }; + const tree3InverseNodeX = { value: "X", source: -1, children: [tree3InverseNodeS], originalNodes: { left: tree2NodeX } }; + const tree3InverseNodeY = { value: "Y", source: -1, children: [tree3InverseNodeU, tree3InverseNodeV, tree3InverseNodeW], originalNodes: { left: tree2NodeY } }; + // Level 1: root node + const tree3InverseRootNode = { value: "Z", source: 0, children: [tree3InverseNodeB, tree3InverseNodeC, tree3InverseNodeD, tree3InverseNodeE, tree3InverseNodeX, tree3InverseNodeY], originalNodes: { right: tree1RootNode, left: tree2RootNode } } + + + // The method to be tested is called + const actualResult = DiffBetweenDirectories.mergeDirectoryTrees(tree2RootNode, tree1RootNode); + + + // Expected result is inverse of tree 3 + expect(actualResult).toStrictEqual(tree3InverseRootNode); + }); + + it("should correctly generate a Quick Pick-specific tree from a merged tree", () => { + // Expected result is detailed below + // Level 1: root node + const quickPickTreeRootNode: QuickPickTreeNode = { label: "$(folder) A", source: 0, description: "Available in both left and right folder", children: [], relativePath: "", parent: undefined }; + // Level 2 + const quickPickTreeNodeB: QuickPickTreeNode = { label: "$(folder) B", source: 0, description: "Available in both left and right folder", children: [], relativePath: "B", parent: quickPickTreeRootNode }; + const quickPickTreeNodeC: QuickPickTreeNode = { label: "$(folder) C", source: 0, description: "Available in both left and right folder", children: [], relativePath: "C", parent: quickPickTreeRootNode }; + const quickPickTreeNodeD: QuickPickTreeNode = { label: "$(file) D", source: -1, description: "Only available in left folder", children: [], relativePath: "D", parent: quickPickTreeRootNode }; + const quickPickTreeNodeE: QuickPickTreeNode = { label: "$(folder) E", source: 0, description: "Available in both left and right folder", children: [], relativePath: "E", parent: quickPickTreeRootNode }; + const quickPickTreeNodeX: QuickPickTreeNode = { label: "$(folder) X", source: 1, description: "Only available in right folder", children: [], relativePath: "X", parent: quickPickTreeRootNode }; + const quickPickTreeNodeY: QuickPickTreeNode = { label: "$(folder) Y", source: 1, description: "Only available in right folder", children: [], relativePath: "Y", parent: quickPickTreeRootNode }; + // Level 3 nodes + const quickPickTreeNodeF: QuickPickTreeNode = { label: "$(file) F", source: -1, description: "Only available in left folder", children: [], relativePath: "B/F", parent: quickPickTreeNodeB }; + const quickPickTreeNodeG: QuickPickTreeNode = { label: "$(file) G", source: 0, description: "Available in both left and right folder", children: [], relativePath: "B/G", parent: quickPickTreeNodeB }; + const quickPickTreeNodeH: QuickPickTreeNode = { label: "$(file) H", source: 0, description: "Available in both left and right folder", children: [], relativePath: "B/H", parent: quickPickTreeNodeB }; + const quickPickTreeNodeT: QuickPickTreeNode = { label: "$(file) T", source: 1, description: "Only available in right folder", children: [], relativePath: "B/T", parent: quickPickTreeNodeB }; + const quickPickTreeNodeI: QuickPickTreeNode = { label: "$(file) I", source: 0, description: "Available in both left and right folder", children: [], relativePath: "C/I", parent: quickPickTreeNodeC }; + const quickPickTreeNodeJ: QuickPickTreeNode = { label: "$(folder) J", source: -1, description: "Only available in left folder", children: [], relativePath: "E/J", parent: quickPickTreeNodeE }; + const quickPickTreeNodeK: QuickPickTreeNode = { label: "$(folder) K", source: 0, description: "Available in both left and right folder", children: [], relativePath: "E/K", parent: quickPickTreeNodeE }; + const quickPickTreeNodeS: QuickPickTreeNode = { label: "$(folder) S", source: 1, description: "Only available in right folder", children: [], relativePath: "X/S", parent: quickPickTreeNodeX }; + const quickPickTreeNodeU: QuickPickTreeNode = { label: "$(file) U", source: 1, description: "Only available in right folder", children: [], relativePath: "Y/U", parent: quickPickTreeNodeY }; + const quickPickTreeNodeV: QuickPickTreeNode = { label: "$(file) V", source: 1, description: "Only available in right folder", children: [], relativePath: "Y/V", parent: quickPickTreeNodeY }; + const quickPickTreeNodeW: QuickPickTreeNode = { label: "$(file) W", source: 1, description: "Only available in right folder", children: [], relativePath: "Y/W", parent: quickPickTreeNodeY }; + // Level 4 nodes + const quickPickTreeNodeL: QuickPickTreeNode = { label: "$(file) L", source: -1, description: "Only available in left folder", children: [], relativePath: "E/J/L", parent: quickPickTreeNodeJ }; + const quickPickTreeNodeM: QuickPickTreeNode = { label: "$(file) M", source: -1, description: "Only available in left folder", children: [], relativePath: "E/J/M", parent: quickPickTreeNodeJ }; + const quickPickTreeNodeN: QuickPickTreeNode = { label: "$(file) N", source: 0, description: "Available in both left and right folder", children: [], relativePath: "E/K/N", parent: quickPickTreeNodeK }; + const quickPickTreeNodeR: QuickPickTreeNode = { label: "$(file) R", source: 1, description: "Only available in right folder", children: [], relativePath: "X/S/R", parent: quickPickTreeNodeS }; + + // Definition of children lists + // Level 1: root node + quickPickTreeRootNode.children = [quickPickTreeNodeB, quickPickTreeNodeC, quickPickTreeNodeD, quickPickTreeNodeE, quickPickTreeNodeX, quickPickTreeNodeY]; + // Level 2 + quickPickTreeNodeB.children = [quickPickTreeNodeF, quickPickTreeNodeG, quickPickTreeNodeH, quickPickTreeNodeT]; + quickPickTreeNodeC.children = [quickPickTreeNodeI]; + quickPickTreeNodeE.children = [quickPickTreeNodeJ, quickPickTreeNodeK]; + quickPickTreeNodeX.children = [quickPickTreeNodeS]; + quickPickTreeNodeY.children = [quickPickTreeNodeU, quickPickTreeNodeV, quickPickTreeNodeW]; + // Level 3 + quickPickTreeNodeJ.children = [quickPickTreeNodeL, quickPickTreeNodeM]; + quickPickTreeNodeK.children = [quickPickTreeNodeN]; + quickPickTreeNodeS.children = [quickPickTreeNodeR]; + + + // The method to be tested is called + const actualResult = DiffBetweenDirectories.mergedTreeToQuickPickTree(tree3RootNode, ""); + + + // Expected result is the previously simulated tree + expect(actualResult).toEqual(quickPickTreeRootNode); + }); +}); diff --git a/vscode4teaching-extension/test/unitSuite/EntryPoint.test.ts b/vscode4teaching-extension/test/unitSuite/EntryPoint.test.ts index 99c53eae..cbc75d07 100644 --- a/vscode4teaching-extension/test/unitSuite/EntryPoint.test.ts +++ b/vscode4teaching-extension/test/unitSuite/EntryPoint.test.ts @@ -10,6 +10,7 @@ import { WebSocketV4TConnection } from "../../src/client/WebSocketV4TConnection" import { CoursesProvider } from "../../src/components/courses/CoursesTreeProvider"; import * as extension from "../../src/extension"; import { Exercise } from "../../src/model/serverModel/exercise/Exercise"; +import { ExerciseStatus } from "../../src/model/serverModel/exercise/ExerciseStatus"; import { ExerciseUserInfo } from "../../src/model/serverModel/exercise/ExerciseUserInfo"; import { User } from "../../src/model/serverModel/user/User"; import { LiveShareService } from "../../src/services/LiveShareService"; @@ -46,17 +47,50 @@ const ec: vscode.ExtensionContext = { workspaceState: { get: jest.fn(), update: jest.fn(), + keys: jest.fn() }, globalState: { get: jest.fn(), update: jest.fn(), + keys: jest.fn(), + setKeysForSync: jest.fn() + }, + secrets: { + get: jest.fn(), + store: jest.fn(), + delete: jest.fn(), + onDidChange: jest.fn() }, extensionUri: mockedVscode.Uri.parse("test"), extensionPath: "test", + environmentVariableCollection: { + persistent: true, + replace: jest.fn(), + append: jest.fn(), + prepend: jest.fn(), + get: jest.fn(), + forEach: jest.fn(), + delete: jest.fn(), + clear: jest.fn() + }, asAbsolutePath: jest.fn(), + storageUri: mockedVscode.Uri.parse("test"), storagePath: "test", + globalStorageUri: mockedVscode.Uri.parse("test"), globalStoragePath: "test", + logUri: mockedVscode.Uri.parse("test"), logPath: "test", + extensionMode: 2, + extension: { + id: "test", + extensionUri: mockedVscode.Uri.parse("test"), + extensionPath: "test", + isActive: true, + packageJSON: {}, + extensionKind: 1, + exports: {}, + activate: jest.fn() + } }; describe("Extension entry point", () => { @@ -86,6 +120,8 @@ describe("Extension entry point", () => { "vscode4teaching.showexercisedashboard", "vscode4teaching.showcurrentexercisedashboard", "vscode4teaching.showliveshareboard", + "vscode4teaching.downloadteachersolution", + "vscode4teaching.diffwithsolution" ]; function expectAllCommandsToBeRegistered(subscriptions: any[]) { @@ -96,6 +132,10 @@ describe("Extension entry point", () => { } } + beforeEach(() => { + jest.clearAllMocks(); + }); + it("should activate correctly", () => { mockedClient.initializeSessionFromFile.mockReturnValueOnce(false); // Initialization will be covered in another test @@ -110,6 +150,9 @@ describe("Extension entry point", () => { const exercise: Exercise = { id: 50, name: "Exercise test", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false }; const cwds: vscode.WorkspaceFolder[] = [ { @@ -134,7 +177,7 @@ describe("Extension entry point", () => { id: 1, exercise, user, - status: 0, + status: ExerciseStatus.StatusEnum.NOT_STARTED, updateDateTime: new Date().toISOString(), modifiedFiles: [], }; diff --git a/vscode4teaching-extension/test/unitSuite/FileIgnoreUtil.test.ts b/vscode4teaching-extension/test/unitSuite/FileIgnoreUtil.test.ts index a290e6c3..5e45b31e 100644 --- a/vscode4teaching-extension/test/unitSuite/FileIgnoreUtil.test.ts +++ b/vscode4teaching-extension/test/unitSuite/FileIgnoreUtil.test.ts @@ -1,7 +1,11 @@ import * as path from "path"; import { FileIgnoreUtil } from "../../src/utils/FileIgnoreUtil"; -describe("FileIgnoreUtil", () => { +describe("File Ignore Utilities", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it("should get the ignored files of a single directory's .gitignore file", () => { const ignoredFiles = FileIgnoreUtil.readGitIgnores(path.resolve(__dirname, "..", "files", "exs")); const expectedIgnoredFiles: string[] = [ diff --git a/vscode4teaching-extension/test/unitSuite/FileZipUtil.test.ts b/vscode4teaching-extension/test/unitSuite/FileZipUtil.test.ts index 6352a3e7..861a4ff8 100644 --- a/vscode4teaching-extension/test/unitSuite/FileZipUtil.test.ts +++ b/vscode4teaching-extension/test/unitSuite/FileZipUtil.test.ts @@ -19,7 +19,7 @@ const mockedCurrentUser = mocked(CurrentUser, true); jest.mock("../../src/client/APIClient"); const mockedAPIClient = mocked(APIClient, true); -describe("FileZipUtil", () => { +describe("ZIP Files Utilities", () => { const rootPath = path.resolve(__dirname, "..", "files"); const extractedPath = path.resolve(rootPath, "extracted"); const zipPath = path.resolve(rootPath, "zips"); @@ -31,6 +31,10 @@ describe("FileZipUtil", () => { return JSZip.loadAsync(buffer); } + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { rimraf(zipPath, (error: any) => { return error; @@ -41,7 +45,6 @@ describe("FileZipUtil", () => { rimraf(newFilePath, (error: any) => { return error; }); - mockedAPIClient.uploadFiles.mockClear(); }); it("should obtain files from zip of exercise", async () => { @@ -102,6 +105,9 @@ describe("FileZipUtil", () => { const exercise: Exercise = { id: 2, name: "Test exercise", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false }; const ignoredPaths: string[] = []; // Create file to add @@ -122,6 +128,9 @@ describe("FileZipUtil", () => { const exercise: Exercise = { id: 2, name: "Test exercise", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false }; const ignoredPaths: string[] = []; const filePath = path.relative(rootPath, path.resolve(rootPath, "exs", "ex3.html")); diff --git a/vscode4teaching-extension/test/unitSuite/FileZipUtilInfo.test.ts b/vscode4teaching-extension/test/unitSuite/FileZipUtilInfo.test.ts index c368dcb0..3dc36e2d 100644 --- a/vscode4teaching-extension/test/unitSuite/FileZipUtilInfo.test.ts +++ b/vscode4teaching-extension/test/unitSuite/FileZipUtilInfo.test.ts @@ -12,9 +12,9 @@ mockedPath.resolve.mockImplementation((...args) => "v4t"); // set INTERNAL_FILES import { FileZipUtil } from "../../src/utils/FileZipUtil"; -describe("FileZipUtil", () => { - afterEach(() => { - mockedPath.resolve.mockClear(); +describe("ZIP Files Info Utilities", () => { + beforeEach(() => { + jest.clearAllMocks(); }); it("should get exercise zip info", async () => { @@ -22,6 +22,9 @@ describe("FileZipUtil", () => { exercises: [{ id: 2, name: "exercise", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false }], id: 1, name: "course", @@ -47,7 +50,7 @@ describe("FileZipUtil", () => { return finalRoute; }); - const zipInfo = FileZipUtil.exerciseZipInfo(course.name, course.exercises[0]); + const zipInfo = FileZipUtil.studentExerciseZipInfo(course.name, course.exercises[0]); expect(zipInfo.dir).toBe("/v4tdownloads/johndoejr/course/exercise"); expect(zipInfo.zipDir).toBe("/v4t/johndoejr"); diff --git a/vscode4teaching-extension/test/unitSuite/FinishItem.test.ts b/vscode4teaching-extension/test/unitSuite/FinishItem.test.ts deleted file mode 100644 index ef572ff6..00000000 --- a/vscode4teaching-extension/test/unitSuite/FinishItem.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { mocked } from "ts-jest/utils"; -import * as vscode from "vscode"; -import { FinishItem } from "../../src/components/statusBarItems/exercises/FinishItem"; - -jest.mock("vscode"); -const mockedVscode = mocked(vscode, true); - -describe("FinishItem", () => { - it("should create correctly", () => { - const finishItem = new FinishItem(1); - expect(mockedVscode.window.createStatusBarItem).toHaveBeenCalledTimes(1); - expect(mockedVscode.window.createStatusBarItem).toHaveBeenLastCalledWith(mockedVscode.StatusBarAlignment.Left); - expect(finishItem.item.text).toBe("$(checklist) Finish exercise"); - expect(finishItem.item.tooltip).toBe("Finish exercise"); - expect(finishItem.item.command).toBe("vscode4teaching.finishexercise"); - expect(finishItem.getExerciseId()).toBe(1); - }); - - it("should show correctly", () => { - const item = new FinishItem(1); - item.show(); - expect(item.item.show).toHaveBeenCalledTimes(1); - }); - - it("should hide correctly", () => { - const item = new FinishItem(1); - item.hide(); - expect(item.item.hide).toHaveBeenCalledTimes(1); - }); - - it("should dispose correctly", () => { - const item = new FinishItem(1); - item.dispose(); - expect(item.item.dispose).toHaveBeenCalledTimes(1); - }); -}); diff --git a/vscode4teaching-extension/test/unitSuite/ShowDashboardItem.test.ts b/vscode4teaching-extension/test/unitSuite/ShowDashboardItem.test.ts deleted file mode 100644 index 39019ba5..00000000 --- a/vscode4teaching-extension/test/unitSuite/ShowDashboardItem.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { mocked } from "ts-jest/utils"; -import * as vscode from "vscode"; -import { ShowDashboardItem } from "../../src/components/statusBarItems/dashboard/ShowDashboardItem"; -import { Course } from "../../src/model/serverModel/course/Course"; -import { Exercise } from "../../src/model/serverModel/exercise/Exercise"; - -jest.mock("vscode"); -const mockedVscode = mocked(vscode, true); - -const course: Course = { - id: 1, - name: "Test course", - exercises: [], -}; -const exercise: Exercise = { - id: 2, - name: "Test exercise", - course, -}; -course.exercises.push(exercise); - -describe("ShowDashboardItem", () => { - it("should create correctly", () => { - const dashboardItem = new ShowDashboardItem("Test name", course, exercise); - expect(mockedVscode.window.createStatusBarItem).toHaveBeenCalledTimes(1); - expect(mockedVscode.window.createStatusBarItem).toHaveBeenLastCalledWith(mockedVscode.StatusBarAlignment.Left); - expect(dashboardItem.item.text).toBe("$(dashboard) Dashboard"); - expect(dashboardItem.item.tooltip).toBe("Dashboard"); - expect(dashboardItem.item.command).toBe("vscode4teaching.showcurrentexercisedashboard"); - expect(dashboardItem.dashboardName).toBe("Test name"); - expect(dashboardItem.exercise).toBe(exercise); - expect(dashboardItem.course).toBe(course); - }); - - it("should show correctly", () => { - const item = new ShowDashboardItem("Test name", course, exercise); - item.show(); - expect(item.item.show).toHaveBeenCalledTimes(1); - }); - - it("should hide correctly", () => { - const item = new ShowDashboardItem("Test name", course, exercise); - item.hide(); - expect(item.item.hide).toHaveBeenCalledTimes(1); - }); - - it("should dispose correctly", () => { - const item = new ShowDashboardItem("Test name", course, exercise); - item.dispose(); - expect(item.item.dispose).toHaveBeenCalledTimes(1); - }); -}); diff --git a/vscode4teaching-extension/test/unitSuite/StatusBarItems.test.ts b/vscode4teaching-extension/test/unitSuite/StatusBarItems.test.ts new file mode 100644 index 00000000..0bf9c94e --- /dev/null +++ b/vscode4teaching-extension/test/unitSuite/StatusBarItems.test.ts @@ -0,0 +1,192 @@ +import { mocked } from "ts-jest/utils"; +import * as vscode from "vscode"; +import { ShowDashboardItem } from "../../src/components/statusBarItems/dashboard/ShowDashboardItem"; +import { DiffWithSolutionItem } from "../../src/components/statusBarItems/exercises/DiffWithSolution"; +import { DownloadTeacherSolutionItem } from "../../src/components/statusBarItems/exercises/DownloadTeacherSolution"; +import { FinishItem } from "../../src/components/statusBarItems/exercises/FinishItem"; +import { ShowLiveshareBoardItem } from "../../src/components/statusBarItems/liveshare/ShowLiveshareBoardItem"; +import { Course } from "../../src/model/serverModel/course/Course"; +import { Exercise } from "../../src/model/serverModel/exercise/Exercise"; + +jest.mock("vscode"); +const mockedVscode = mocked(vscode, true); + +const course: Course = { + id: 1, + name: "Test course", + exercises: [], +}; +const exercise: Exercise = { + id: 2, + name: "Test exercise", + course, + includesTeacherSolution: true, + solutionIsPublic: true, + allowEditionAfterSolutionDownloaded: false +}; +course.exercises.push(exercise); + +describe("Status Bar items", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("Finish exercise (student)", () => { + it("should create correctly", () => { + const finishItem = new FinishItem(1); + expect(mockedVscode.window.createStatusBarItem).toHaveBeenCalledTimes(1); + expect(mockedVscode.window.createStatusBarItem).toHaveBeenLastCalledWith(mockedVscode.StatusBarAlignment.Left); + expect(finishItem.item.text).toBe("$(checklist) Finish exercise"); + expect(finishItem.item.tooltip).toBe("Finish exercise"); + expect(finishItem.item.command).toBe("vscode4teaching.finishexercise"); + expect(finishItem.getExerciseId()).toBe(1); + }); + + it("should show correctly", () => { + const item = new FinishItem(1); + item.show(); + expect(item.item.show).toHaveBeenCalledTimes(1); + }); + + it("should hide correctly", () => { + const item = new FinishItem(1); + item.hide(); + expect(item.item.hide).toHaveBeenCalledTimes(1); + }); + + it("should dispose correctly", () => { + const item = new FinishItem(1); + item.dispose(); + expect(item.item.dispose).toHaveBeenCalledTimes(1); + }); + }); + + + describe("Show dashboard (teacher)", () => { + it("should create correctly", () => { + const dashboardItem = new ShowDashboardItem("Test name", course, exercise); + expect(mockedVscode.window.createStatusBarItem).toHaveBeenCalledTimes(1); + expect(mockedVscode.window.createStatusBarItem).toHaveBeenLastCalledWith(mockedVscode.StatusBarAlignment.Left); + expect(dashboardItem.item.text).toBe("$(dashboard) Dashboard"); + expect(dashboardItem.item.tooltip).toBe("Dashboard"); + expect(dashboardItem.item.command).toBe("vscode4teaching.showcurrentexercisedashboard"); + expect(dashboardItem.dashboardName).toBe("Test name"); + expect(dashboardItem.exercise).toBe(exercise); + expect(dashboardItem.course).toBe(course); + }); + + it("should show correctly", () => { + const item = new ShowDashboardItem("Test name", course, exercise); + item.show(); + expect(item.item.show).toHaveBeenCalledTimes(1); + }); + + it("should hide correctly", () => { + const item = new ShowDashboardItem("Test name", course, exercise); + item.hide(); + expect(item.item.hide).toHaveBeenCalledTimes(1); + }); + + it("should dispose correctly", () => { + const item = new ShowDashboardItem("Test name", course, exercise); + item.dispose(); + expect(item.item.dispose).toHaveBeenCalledTimes(1); + }); + }); + + describe("Download teacher's solution (student)", () => { + it("should create correctly", () => { + const exerciseItem = new DownloadTeacherSolutionItem(exercise); + expect(mockedVscode.window.createStatusBarItem).toHaveBeenCalledTimes(1); + expect(mockedVscode.window.createStatusBarItem).toHaveBeenLastCalledWith(mockedVscode.StatusBarAlignment.Left); + expect(exerciseItem.item.text).toBe("$(cloud-download) Download teacher's solution"); + expect(exerciseItem.item.tooltip).toBe("Download teacher's solution"); + expect(exerciseItem.item.command).toBe("vscode4teaching.downloadteachersolution"); + expect(exerciseItem.getExerciseInfo()).toBe(exercise); + }); + + it("should show correctly", () => { + const item = new DownloadTeacherSolutionItem(exercise); + item.show(); + expect(item.item.show).toHaveBeenCalledTimes(1); + }); + + it("should hide correctly", () => { + const item = new DownloadTeacherSolutionItem(exercise); + item.hide(); + expect(item.item.hide).toHaveBeenCalledTimes(1); + }); + + it("should dispose correctly", () => { + const item = new DownloadTeacherSolutionItem(exercise); + item.dispose(); + expect(item.item.dispose).toHaveBeenCalledTimes(1); + }); + }); + + describe("Diff with solution (student)", () => { + it("should create correctly", () => { + const diffWithSolutionItem = new DiffWithSolutionItem(); + expect(mockedVscode.window.createStatusBarItem).toHaveBeenCalledTimes(1); + expect(mockedVscode.window.createStatusBarItem).toHaveBeenLastCalledWith(mockedVscode.StatusBarAlignment.Left); + expect(diffWithSolutionItem.item.text).toBe("$(diff) Diff with teacher's solution"); + expect(diffWithSolutionItem.item.tooltip).toBe("Diff with teacher's solution"); + expect(diffWithSolutionItem.item.command).toBe("vscode4teaching.diffwithsolution"); + }); + + it("should show correctly", () => { + const item = new DiffWithSolutionItem(); + item.show(); + expect(item.item.show).toHaveBeenCalledTimes(1); + }); + + it("should hide correctly", () => { + const item = new DiffWithSolutionItem(); + item.hide(); + expect(item.item.hide).toHaveBeenCalledTimes(1); + }); + + it("should dispose correctly", () => { + const item = new DiffWithSolutionItem(); + item.dispose(); + expect(item.item.dispose).toHaveBeenCalledTimes(1); + }); + }); + + describe("Show LiveShare board (student, teacher)", () => { + const dashboardName = "Course's Dashboard"; + + it("should create correctly", () => { + const diffWithSolutionItem = new ShowLiveshareBoardItem(dashboardName, [course]); + expect(mockedVscode.window.createStatusBarItem).toHaveBeenCalledTimes(1); + expect(mockedVscode.window.createStatusBarItem).toHaveBeenLastCalledWith(mockedVscode.StatusBarAlignment.Left); + expect(diffWithSolutionItem.item.text).toBe("$(live-share) Liveshare Board"); + expect(diffWithSolutionItem.item.tooltip).toBe("Liveshare Board"); + expect(diffWithSolutionItem.item.command).toBe("vscode4teaching.showliveshareboard"); + }); + + it("should show correctly", () => { + const item = new ShowLiveshareBoardItem(dashboardName, [course]); + item.show(); + expect(item.item.show).toHaveBeenCalledTimes(1); + }); + + it("should return item's attributes correctly", () => { + const item = new ShowLiveshareBoardItem(dashboardName, [course]); + expect(item.dashboardName).toStrictEqual(dashboardName); + expect(item.getCourses).toStrictEqual([course]); + }); + + it("should hide correctly", () => { + const item = new ShowLiveshareBoardItem(dashboardName, [course]); + item.hide(); + expect(item.item.hide).toHaveBeenCalledTimes(1); + }); + + it("should dispose correctly", () => { + const item = new ShowLiveshareBoardItem(dashboardName, [course]); + item.dispose(); + expect(item.item.dispose).toHaveBeenCalledTimes(1); + }); + }); +}); \ No newline at end of file diff --git a/vscode4teaching-extension/test/unitSuite/TreeView.test.ts b/vscode4teaching-extension/test/unitSuite/TreeView.test.ts index 00bb109c..47fa264a 100644 --- a/vscode4teaching-extension/test/unitSuite/TreeView.test.ts +++ b/vscode4teaching-extension/test/unitSuite/TreeView.test.ts @@ -1,5 +1,7 @@ import { AxiosResponse } from "axios"; import { mocked } from "ts-jest/utils"; +import * as fs from "fs"; +import * as path from "path"; import * as vscode from "vscode"; import { APIClient } from "../../src/client/APIClient"; import { CurrentUser } from "../../src/client/CurrentUser"; @@ -13,6 +15,11 @@ import { Course } from "../../src/model/serverModel/course/Course"; import { Exercise } from "../../src/model/serverModel/exercise/Exercise"; import { User } from "../../src/model/serverModel/user/User"; import { FileZipUtil } from "../../src/utils/FileZipUtil"; +import { ExerciseEdit } from "../../src/model/serverModel/exercise/ExerciseEdit"; +import { mockFsDirent, mockFsStatus } from "./__mocks__/mockFsUtils"; +import { mockedPathJoin } from "./__mocks__/mockPathUtils"; +import { ExerciseUserInfo } from "../../src/model/serverModel/exercise/ExerciseUserInfo"; +import { ExerciseStatus } from "../../src/model/serverModel/exercise/ExerciseStatus"; jest.mock("../../src/client/CurrentUser"); const mockedCurrentUser = mocked(CurrentUser, true); @@ -20,6 +27,10 @@ jest.mock("../../src/client/APIClient"); const mockedClient = mocked(APIClient, true); jest.mock("../../src/utils/FileZipUtil"); const mockedFileZipUtil = mocked(FileZipUtil, true); +jest.mock("fs"); +const mockedFs = mocked(fs, true); +jest.mock("path"); +const mockedPath = mocked(path, true); jest.mock("vscode"); const mockedVscode = mocked(vscode, true); @@ -46,10 +57,16 @@ const teacherCourses: Course[] = [ { id: 4, name: "Exercise 1", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false }, { id: 40, name: "Exercise 2", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false }, ], creator: mockedUserTeacherModel, @@ -61,6 +78,9 @@ const teacherCourses: Course[] = [ { id: 5, name: "Exercise 1", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false }, ], }, @@ -77,32 +97,23 @@ const mockedUserStudentModel: User = { let mockedUser = mockedUserTeacherModel; function mockGetInput(prompt: string, validateInput: (value: string) => string | undefined | null | Thenable<string | undefined | null>, resolvedValue: any, defaultValue?: string, password?: boolean) { - mockedVscode.window.showInputBox - .mockResolvedValueOnce(resolvedValue); - const expectedInputOptions = { + mockedVscode.window.showInputBox.mockResolvedValueOnce(resolvedValue); + return { prompt, validateInput, value: defaultValue, password, }; - return expectedInputOptions; } describe("Tree View", () => { + beforeEach(() => { + jest.clearAllMocks(); - afterEach(() => { - mockedCurrentUser.isLoggedIn.mockClear(); - mockedCurrentUser.updateUserInfo.mockClear(); - mockedCurrentUser.getUserInfo.mockClear(); - mockedVscode.EventEmitter.mockClear(); - mockedVscode.window.showInputBox.mockClear(); - mockedClient.initializeSessionFromFile.mockClear(); - mockedClient.getUsersInCourse.mockClear(); - mockedVscode.window.showQuickPick.mockClear(); - mockedVscode.window.showWarningMessage.mockClear(); - mockedVscode.window.showInformationMessage.mockClear(); coursesProvider = new CoursesProvider(); mockedUser = mockedUserTeacherModel; + + mockedPath.join.mockImplementation(mockedPathJoin); }); it("should show log in buttons when not logged in and session could not be initialized", () => { @@ -129,7 +140,7 @@ describe("Tree View", () => { expect(elements).toStrictEqual([]); // Don't return anything while updating }); - it("should show courses, add courses, sign up teacher and logout buttons when teacher is logged in", () => { + it("should show courses, add courses, invite teacher and logout buttons when teacher is logged in", () => { mockedCurrentUser.isLoggedIn.mockImplementation(() => true); mockedCurrentUser.getUserInfo.mockImplementation(() => mockedUser); const coursesItemsTeacher = teacherCourses.map((course) => new V4TItem(course.name, V4TItemType.CourseTeacher, vscode.TreeItemCollapsibleState.Collapsed, undefined, course)); @@ -155,7 +166,7 @@ describe("Tree View", () => { expect(elements).toStrictEqual(coursesItemsStudent); }); - it("should show exercises on course click (Teacher)", async () => { + it("should show exercises on course click (teacher)", async () => { const course: Course = { id: 1, name: "Test course", @@ -164,9 +175,15 @@ describe("Tree View", () => { const exercises: Exercise[] = [{ id: 2, name: "Test exercise 1", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false }, { id: 3, name: "Test exercise 2", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false }]; const courseItem = new V4TItem("Test course", V4TItemType.CourseTeacher, mockedVscode.TreeItemCollapsibleState.Collapsed, undefined, course, undefined); @@ -194,7 +211,75 @@ describe("Tree View", () => { command: "vscode4teaching.getstudentfiles", title: "Get exercise files", arguments: [course.name, exercise], - })); + }, 0)); + + const exerciseElements = await coursesProvider.getChildren(courseItem); + + expect(mockedClient.getExercises).toHaveBeenCalledTimes(1); + expect(mockedClient.getExercises).toHaveBeenNthCalledWith(1, course.id); + expect(exerciseElements).toStrictEqual(expectedExerciseItems); + }); + + it("should show exercises on course click (student)", async () => { + const course: Course = { + id: 1, + name: "Test course", + exercises: [], // Exercises will be fetched from the server during call + }; + const exercises: Exercise[] = [{ + id: 2, + name: "Test exercise 1", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false + }, { + id: 3, + name: "Test exercise 2", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false + }]; + const courseItem = new V4TItem("Test course", V4TItemType.CourseStudent, mockedVscode.TreeItemCollapsibleState.Collapsed, undefined, course, undefined); + + const response: AxiosResponse<Exercise[]> = { + data: exercises, + status: 200, + statusText: "", + headers: {}, + config: {}, + }; + mockedClient.getExercises.mockResolvedValueOnce(response); + + mockedCurrentUser.isLoggedIn.mockReturnValueOnce(true); + const user: User = { + id: 4, + roles: [{ + roleName: "ROLE_STUDENT", + }], + username: "johndoe", + }; + mockedCurrentUser.getUserInfo.mockReturnValue(user); + const expectedExerciseItems = exercises.map((exercise) => new V4TItem(exercise.name, V4TItemType.ExerciseStudent, vscode.TreeItemCollapsibleState.None, courseItem, exercise, { + command: "vscode4teaching.getexercisefiles", + title: "Get exercise files", + arguments: [course.name, exercise], + }, ExerciseStatus.StatusEnum.NOT_STARTED)); + const expectedExerciseUserInfo = (exercise: Exercise): AxiosResponse<ExerciseUserInfo> => ({ + data: { + id: 5, + exercise, + status: ExerciseStatus.StatusEnum.NOT_STARTED, + user, + updateDateTime: Date.now().toString() + }, + status: 200, + statusText: "", + headers: {}, + config: {}, + }); + mockedClient.getExerciseUserInfo + .mockResolvedValueOnce(expectedExerciseUserInfo(exercises[0])) + .mockResolvedValueOnce(expectedExerciseUserInfo(exercises[1])); const exerciseElements = await coursesProvider.getChildren(courseItem); @@ -335,7 +420,7 @@ describe("Tree View", () => { expect(mockedCurrentUser.updateUserInfo).toHaveBeenCalledTimes(1); }); - it("should add exercise", async () => { + it("should add an exercise without solution", async () => { const courseModel = new V4TItem( teacherCourses[0].name, V4TItemType.CourseTeacher, @@ -344,60 +429,434 @@ describe("Tree View", () => { teacherCourses[0], undefined, ); - const exerciseModel = { - name: "New exercise", + + const openDialogOptions = { + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + openLabel: "Select a directory" + }; + const openDialogReturnedUri: vscode.Uri = { + authority: "", + fragment: "", + fsPath: "test", + path: "test", + query: "", + scheme: "file", + with: jest.fn(), + toJSON: jest.fn() + }; + mockedVscode.window.showOpenDialog.mockResolvedValueOnce([openDialogReturnedUri]); + + // Into getTemplateSolutionPaths() + mockedFs.lstatSync.mockImplementation(path => mockFsStatus(path === "test")); + mockedFs.readdirSync.mockReturnValue([ + mockFsDirent("folder_test", true), + mockFsDirent("file_test", false) + ]); + + const addExerciseRequestBody: ExerciseEdit = { + name: "test", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false + } + + const addExerciseResponseBody: Exercise = { + id: 10, + name: "test", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false }; + mockedClient.addExercises.mockResolvedValueOnce({ + status: 201, + statusText: "", + headers: {}, + config: {}, + data: [addExerciseResponseBody], + }); + + const mockBufferDoc = Buffer.from("test"); + mockedFileZipUtil.getZipFromUris.mockResolvedValueOnce(mockBufferDoc); + + const axiosResponseMock: AxiosResponse = { + data: new Promise((res, _) => res(mockBufferDoc)), + status: 200, + statusText: "", + config: {}, + headers: {} + } + mockedClient.uploadExerciseTemplate.mockResolvedValue(axiosResponseMock); - const inputOptionsExercise = mockGetInput("Exercise name", Validators.validateExerciseName, exerciseModel.name); + await coursesProvider.addExercises(courseModel, false); + + expect(mockedVscode.window.showOpenDialog).toHaveBeenCalledTimes(1); + expect(mockedVscode.window.showOpenDialog).toHaveBeenNthCalledWith(1, openDialogOptions); + expect(mockedFs.lstatSync).toHaveBeenCalledTimes(1); + expect(mockedFs.lstatSync).toHaveBeenNthCalledWith(1, openDialogReturnedUri.fsPath); + expect(mockedFs.readdirSync).toHaveBeenCalledTimes(1); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(1, openDialogReturnedUri.fsPath, { withFileTypes: true }); + expect(mockedClient.addExercises).toHaveBeenCalledTimes(1); + expect(mockedClient.addExercises).toHaveBeenNthCalledWith(1, teacherCourses[0].id, [addExerciseRequestBody]); + expect(mockedClient.uploadExerciseTemplate).toHaveBeenCalledTimes(1); + expect(mockedClient.uploadExerciseTemplate).toHaveBeenNthCalledWith(1, addExerciseResponseBody.id, mockBufferDoc, false); + expect(mockedClient.uploadExerciseSolution).toHaveBeenCalledTimes(0); + expect(mockedVscode.window.showInformationMessage).toHaveBeenCalledTimes(1); + expect(mockedVscode.window.showInformationMessage).toHaveBeenNthCalledWith(1, "The new exercise was added successfully."); + }); + + it("should add some exercises without solution", async () => { + const courseModel = new V4TItem( + teacherCourses[0].name, + V4TItemType.CourseTeacher, + mockedVscode.TreeItemCollapsibleState.Collapsed, + undefined, + teacherCourses[0], + undefined, + ); + + mockedVscode.window.showInformationMessage.mockResolvedValueOnce({ title: "Accept" } as vscode.MessageItem); const openDialogOptions = { - canSelectFiles: true, + canSelectFiles: false, canSelectFolders: true, - canSelectMany: true, + canSelectMany: false, + openLabel: "Select a directory" + }; + const openDialogReturnedUri: vscode.Uri = { + authority: "", + fragment: "", + fsPath: "parentDirectory", + path: "parentDirectory", + query: "", + scheme: "file", + with: jest.fn(), + toJSON: jest.fn() }; - const fileUrisMocks = [ - mockedVscode.Uri.file("testFile1"), - mockedVscode.Uri.file("testFile2"), - mockedVscode.Uri.file("testFile3"), - ]; - mockedVscode.window.showOpenDialog.mockResolvedValueOnce(fileUrisMocks); + mockedVscode.window.showOpenDialog.mockResolvedValueOnce([openDialogReturnedUri]); + + const exampleExercisesNames = ["ej1", "ej2", "ej3", "ej4", "ej5"]; + + mockedFs.lstatSync.mockImplementation(path => mockFsStatus(/ej[0-9]*/.test(path.toString()))); + const mockedParentDirectoryContents = exampleExercisesNames.map(ej => mockFsDirent(ej, true)); + mockedFs.readdirSync + // First call: scanning contents of parent directory + .mockReturnValueOnce(mockedParentDirectoryContents) + // Successive calls: in getTemplateSolutionPaths(), one per each exercise, can be returned same info on every case + .mockReturnValue([ + mockFsDirent("test_file_1", true), + mockFsDirent("test_file_2", true) + ]); + + const addExerciseRequestBody: ExerciseEdit[] = exampleExercisesNames.map(ej => ({ + name: ej, + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false + })); + + const addExerciseResponseBody: Exercise[] = exampleExercisesNames.map((ej, index) => ({ + id: 10 + index, + name: ej, + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false + })); mockedClient.addExercises.mockResolvedValueOnce({ status: 201, statusText: "", headers: {}, config: {}, - data: [ - { - id: 10, - name: exerciseModel.name, - }, - ], + data: addExerciseResponseBody, }); - const mockBuffer = Buffer.from("test"); - mockedFileZipUtil.getZipFromUris.mockResolvedValueOnce(mockBuffer); + const mockBufferDoc = Buffer.from("test_content_of_zipfiles"); + mockedFileZipUtil.getZipFromUris.mockResolvedValue(mockBufferDoc); - mockedClient.uploadExerciseTemplate.mockResolvedValueOnce({ + const axiosResponseMock: AxiosResponse = { + data: new Promise((res, _) => res(mockBufferDoc)), status: 200, statusText: "", + config: {}, + headers: {} + } + mockedClient.uploadExerciseTemplate.mockResolvedValue(axiosResponseMock); + + await coursesProvider.addExercises(courseModel, true); + + + expect(mockedVscode.window.showInformationMessage).toHaveBeenCalledTimes(2); + expect(mockedVscode.window.showInformationMessage).toHaveBeenNthCalledWith(1, "To upload multiple exercises, prepare a directory with a folder for each exercise, each folder including the exercise's corresponding template and solution if wanted. When ready, click 'Accept'.", { title: "Accept" }); + expect(mockedVscode.window.showOpenDialog).toHaveBeenCalledTimes(1); + expect(mockedVscode.window.showOpenDialog).toHaveBeenNthCalledWith(1, openDialogOptions); + expect(mockedFs.readdirSync).toHaveBeenCalledTimes(6); + expect(mockedFs.lstatSync).toHaveBeenCalledTimes(5); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(1, openDialogReturnedUri.fsPath, { withFileTypes: true }); + expect(mockedFs.lstatSync).toHaveBeenNthCalledWith(1, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[0].name)); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(2, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[0].name), { withFileTypes: true }); + expect(mockedFs.lstatSync).toHaveBeenNthCalledWith(2, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[1].name)); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(3, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[1].name), { withFileTypes: true }); + expect(mockedFs.lstatSync).toHaveBeenNthCalledWith(3, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[2].name)); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(4, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[2].name), { withFileTypes: true }); + expect(mockedFs.lstatSync).toHaveBeenNthCalledWith(4, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[3].name)); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(5, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[3].name), { withFileTypes: true }); + expect(mockedFs.lstatSync).toHaveBeenNthCalledWith(5, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[4].name)); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(6, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[4].name), { withFileTypes: true }); + expect(mockedClient.addExercises).toHaveBeenCalledTimes(1); + expect(mockedClient.addExercises).toHaveBeenNthCalledWith(1, teacherCourses[0].id, addExerciseRequestBody); + expect(mockedClient.uploadExerciseTemplate).toHaveBeenCalledTimes(5); + expect(mockedClient.uploadExerciseTemplate).toHaveBeenNthCalledWith(1, addExerciseResponseBody[0].id, mockBufferDoc, false); + expect(mockedClient.uploadExerciseTemplate).toHaveBeenNthCalledWith(2, addExerciseResponseBody[1].id, mockBufferDoc, false); + expect(mockedClient.uploadExerciseTemplate).toHaveBeenNthCalledWith(3, addExerciseResponseBody[2].id, mockBufferDoc, false); + expect(mockedClient.uploadExerciseTemplate).toHaveBeenNthCalledWith(4, addExerciseResponseBody[3].id, mockBufferDoc, false); + expect(mockedClient.uploadExerciseTemplate).toHaveBeenNthCalledWith(5, addExerciseResponseBody[4].id, mockBufferDoc, false); + expect(mockedClient.uploadExerciseSolution).toHaveBeenCalledTimes(0); + expect(mockedVscode.window.showInformationMessage).toHaveBeenNthCalledWith(2, "5 exercises were added successfully."); + }); + + it("should add an exercise with solution", async () => { + const courseModel = new V4TItem( + teacherCourses[0].name, + V4TItemType.CourseTeacher, + mockedVscode.TreeItemCollapsibleState.Collapsed, + undefined, + teacherCourses[0], + undefined, + ); + + const openDialogOptions = { + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + openLabel: "Select a directory" + }; + const openDialogReturnedUri: vscode.Uri = { + authority: "", + fragment: "", + fsPath: "test", + path: "test", + query: "", + scheme: "file", + with: jest.fn(), + toJSON: jest.fn() + }; + mockedVscode.window.showOpenDialog.mockResolvedValueOnce([openDialogReturnedUri]); + + // Into getTemplateSolutionPaths() + mockedFs.lstatSync.mockImplementation(path => mockFsStatus(path === "test")); + mockedFs.readdirSync + // First call: scanning contents of parent directory + .mockReturnValueOnce([ + mockFsDirent("template", true), + mockFsDirent("solution", true) + ]) + // Successive calls: in getTemplateSolutionPaths(), one per each directory, can be returned same info on every case + .mockReturnValue([ + mockFsDirent("test_file_1", false), + mockFsDirent("test_file_2", false), + mockFsDirent("test_file_3", false) + ]); + + const addExerciseRequestBody: ExerciseEdit = { + name: "test", + includesTeacherSolution: true, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false + } + + const addExerciseResponseBody: Exercise = { + id: 10, + name: "test", + includesTeacherSolution: true, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false + }; + mockedClient.addExercises.mockResolvedValueOnce({ + status: 201, + statusText: "", headers: {}, config: {}, - data: {}, + data: [addExerciseResponseBody], }); - await coursesProvider.addExercise(courseModel); + const mockBufferDoc = Buffer.from("test"); + mockedFileZipUtil.getZipFromUris.mockResolvedValue(mockBufferDoc); + + const axiosResponseMock: AxiosResponse = { + data: new Promise((res, _) => res(mockBufferDoc)), + status: 200, + statusText: "", + config: {}, + headers: {} + } + mockedClient.uploadExerciseTemplate.mockResolvedValue(axiosResponseMock); + mockedClient.uploadExerciseSolution.mockResolvedValue(axiosResponseMock); + + await coursesProvider.addExercises(courseModel, false); - expect(mockedVscode.window.showInputBox).toHaveBeenCalledTimes(1); - expect(mockedVscode.window.showInputBox).toHaveBeenNthCalledWith(1, inputOptionsExercise); expect(mockedVscode.window.showOpenDialog).toHaveBeenCalledTimes(1); expect(mockedVscode.window.showOpenDialog).toHaveBeenNthCalledWith(1, openDialogOptions); + expect(mockedFs.lstatSync).toHaveBeenCalledTimes(1); + expect(mockedFs.lstatSync).toHaveBeenNthCalledWith(1, openDialogReturnedUri.fsPath); + expect(mockedFs.readdirSync).toHaveBeenCalledTimes(3); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(1, openDialogReturnedUri.fsPath, { withFileTypes: true }); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(2, mockedPath.join(openDialogReturnedUri.fsPath, "template")); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(3, mockedPath.join(openDialogReturnedUri.fsPath, "solution")); expect(mockedClient.addExercises).toHaveBeenCalledTimes(1); - expect(mockedClient.addExercises).toHaveBeenNthCalledWith(1, teacherCourses[0].id, [{ name: exerciseModel.name }]); - expect(mockedFileZipUtil.getZipFromUris).toHaveBeenCalledTimes(1); - expect(mockedFileZipUtil.getZipFromUris).toHaveBeenNthCalledWith(1, fileUrisMocks); + expect(mockedClient.addExercises).toHaveBeenNthCalledWith(1, teacherCourses[0].id, [addExerciseRequestBody]); expect(mockedClient.uploadExerciseTemplate).toHaveBeenCalledTimes(1); - expect(mockedClient.uploadExerciseTemplate).toHaveBeenNthCalledWith(1, 10, mockBuffer); + expect(mockedClient.uploadExerciseTemplate).toHaveBeenNthCalledWith(1, addExerciseResponseBody.id, mockBufferDoc, false); + expect(mockedClient.uploadExerciseSolution).toHaveBeenCalledTimes(1); + expect(mockedClient.uploadExerciseSolution).toHaveBeenNthCalledWith(1, addExerciseResponseBody.id, mockBufferDoc, false); + expect(mockedVscode.window.showInformationMessage).toHaveBeenCalledTimes(1); + expect(mockedVscode.window.showInformationMessage).toHaveBeenNthCalledWith(1, "The new exercise was added successfully."); + }); + + it("should add some exercises with solution", async () => { + const courseModel = new V4TItem( + teacherCourses[0].name, + V4TItemType.CourseTeacher, + mockedVscode.TreeItemCollapsibleState.Collapsed, + undefined, + teacherCourses[0], + undefined, + ); + + mockedVscode.window.showInformationMessage.mockResolvedValueOnce({ title: "Accept" } as vscode.MessageItem); + + const openDialogOptions = { + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + openLabel: "Select a directory" + }; + const openDialogReturnedUri: vscode.Uri = { + authority: "", + fragment: "", + fsPath: "parentDirectory", + path: "parentDirectory", + query: "", + scheme: "file", + with: jest.fn(), + toJSON: jest.fn() + }; + mockedVscode.window.showOpenDialog.mockResolvedValueOnce([openDialogReturnedUri]); + + const exampleExercisesNames = ["ej1", "ej2", "ej3", "ej4", "ej5"]; + + /** + * fs library mock + * + * A file structure is simulated for each exercise like this one: + * ejN/ + * ├─ template/ + * │ ├─ file_test_1 + * │ ├─ file_test_2 + * ├─ solution/ + * │ ├─ file_test_1 + * │ ├─ file_test_2 + */ + mockedFs.lstatSync.mockImplementation(path => mockFsStatus(/ej[0-9]*/.test(path.toString()) || path === "template" || path === "solution")); + const mockedParentDirectoryContents = exampleExercisesNames.map(ej => mockFsDirent(ej, true)); + mockedFs.readdirSync.mockImplementation((path, options) => { + if (path.toString() === "parentDirectory") + return exampleExercisesNames.map(ej => mockFsDirent(ej, true)) + else if (/ej[0-9]*/.test(path.toString())) + return [ + mockFsDirent("template", true), + mockFsDirent("solution", true) + ] + else if (["template", "solution"].some(name => name === path)) + return [ + mockFsDirent("file_test_1", false), + mockFsDirent("file_test_2", false) + ] + else return []; + }) + + const addExerciseRequestBody: ExerciseEdit[] = exampleExercisesNames.map(ej => ({ + name: ej, + includesTeacherSolution: true, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false + })); + + const addExerciseResponseBody: Exercise[] = exampleExercisesNames.map((ej, index) => ({ + id: 10 + index, + name: ej, + includesTeacherSolution: true, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false + })); + + mockedClient.addExercises.mockResolvedValueOnce({ + status: 201, + statusText: "", + headers: {}, + config: {}, + data: addExerciseResponseBody, + }); + + const mockBufferDoc = Buffer.from("test_content_of_zipfiles"); + mockedFileZipUtil.getZipFromUris.mockResolvedValue(mockBufferDoc); + + const axiosResponseMock: AxiosResponse = { + data: new Promise((res, _) => res(mockBufferDoc)), + status: 200, + statusText: "", + config: {}, + headers: {} + } + mockedClient.uploadExerciseTemplate.mockResolvedValue(axiosResponseMock); + mockedClient.uploadExerciseSolution.mockResolvedValue(axiosResponseMock); + + await coursesProvider.addExercises(courseModel, true); + + + expect(mockedVscode.window.showInformationMessage).toHaveBeenCalledTimes(2); + expect(mockedVscode.window.showInformationMessage).toHaveBeenNthCalledWith(1, "To upload multiple exercises, prepare a directory with a folder for each exercise, each folder including the exercise's corresponding template and solution if wanted. When ready, click 'Accept'.", { title: "Accept" }); + expect(mockedVscode.window.showOpenDialog).toHaveBeenCalledTimes(1); + expect(mockedVscode.window.showOpenDialog).toHaveBeenNthCalledWith(1, openDialogOptions); + expect(mockedFs.lstatSync).toHaveBeenCalledTimes(5); + expect(mockedFs.lstatSync).toHaveBeenNthCalledWith(1, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[0].name)); + expect(mockedFs.lstatSync).toHaveBeenNthCalledWith(2, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[1].name)); + expect(mockedFs.lstatSync).toHaveBeenNthCalledWith(3, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[2].name)); + expect(mockedFs.lstatSync).toHaveBeenNthCalledWith(4, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[3].name)); + expect(mockedFs.lstatSync).toHaveBeenNthCalledWith(5, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[4].name)); + expect(mockedFs.readdirSync).toHaveBeenCalledTimes(16); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(1, openDialogReturnedUri.fsPath, { withFileTypes: true }); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(2, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[0].name), { withFileTypes: true }); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(3, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[0].name, "template")); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(4, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[0].name, "solution")); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(5, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[1].name), { withFileTypes: true }); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(6, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[1].name, "template")); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(7, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[1].name, "solution")); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(8, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[2].name), { withFileTypes: true }); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(9, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[2].name, "template")); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(10, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[2].name, "solution")); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(11, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[3].name), { withFileTypes: true }); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(12, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[3].name, "template")); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(13, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[3].name, "solution")); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(14, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[4].name), { withFileTypes: true }); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(15, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[4].name, "template")); + expect(mockedFs.readdirSync).toHaveBeenNthCalledWith(16, mockedPath.join(openDialogReturnedUri.fsPath, mockedParentDirectoryContents[4].name, "solution")); + expect(mockedClient.addExercises).toHaveBeenCalledTimes(1); + expect(mockedClient.addExercises).toHaveBeenNthCalledWith(1, teacherCourses[0].id, addExerciseRequestBody); + expect(mockedClient.uploadExerciseTemplate).toHaveBeenCalledTimes(5); + expect(mockedClient.uploadExerciseTemplate).toHaveBeenNthCalledWith(1, addExerciseResponseBody[0].id, mockBufferDoc, false); + expect(mockedClient.uploadExerciseTemplate).toHaveBeenNthCalledWith(2, addExerciseResponseBody[1].id, mockBufferDoc, false); + expect(mockedClient.uploadExerciseTemplate).toHaveBeenNthCalledWith(3, addExerciseResponseBody[2].id, mockBufferDoc, false); + expect(mockedClient.uploadExerciseTemplate).toHaveBeenNthCalledWith(4, addExerciseResponseBody[3].id, mockBufferDoc, false); + expect(mockedClient.uploadExerciseTemplate).toHaveBeenNthCalledWith(5, addExerciseResponseBody[4].id, mockBufferDoc, false); + expect(mockedClient.uploadExerciseSolution).toHaveBeenCalledTimes(5); + expect(mockedClient.uploadExerciseSolution).toHaveBeenNthCalledWith(1, addExerciseResponseBody[0].id, mockBufferDoc, false); + expect(mockedClient.uploadExerciseSolution).toHaveBeenNthCalledWith(2, addExerciseResponseBody[1].id, mockBufferDoc, false); + expect(mockedClient.uploadExerciseSolution).toHaveBeenNthCalledWith(3, addExerciseResponseBody[2].id, mockBufferDoc, false); + expect(mockedClient.uploadExerciseSolution).toHaveBeenNthCalledWith(4, addExerciseResponseBody[3].id, mockBufferDoc, false); + expect(mockedClient.uploadExerciseSolution).toHaveBeenNthCalledWith(5, addExerciseResponseBody[4].id, mockBufferDoc, false); + expect(mockedVscode.window.showInformationMessage).toHaveBeenNthCalledWith(2, "5 exercises were added successfully."); }); it("should edit exercise", async () => { @@ -411,6 +870,9 @@ describe("Tree View", () => { ); const newExerciseModel = { name: "Test course 1 edited", + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false }; const inputOptionsCourse = mockGetInput("Exercise name", Validators.validateExerciseName, newExerciseModel.name); @@ -419,6 +881,9 @@ describe("Tree View", () => { data: { id: teacherCourses[0].exercises[0].id, name: newExerciseModel.name, + includesTeacherSolution: false, + solutionIsPublic: false, + allowEditionAfterSolutionDownloaded: false }, status: 200, statusText: "", @@ -463,7 +928,33 @@ describe("Tree View", () => { expect(mockedVscode.window.showInformationMessage).toHaveBeenNthCalledWith(1, "Exercise deleted successfully"); }); - it("should sign up student correctly", async () => { + it("should fill up sign up student form correctly", async () => { + const userData = { + username: "johndoe", + email: "johndoe@gmail.com", + name: "John", + lastName: "Doe" + }; + + const inputOptionsFirstName = mockGetInput("First name", Validators.validateName, userData.name); + const inputOptionsLastName = mockGetInput("Last name", Validators.validateLastName, userData.lastName); + const inputOptionsUsername = mockGetInput("Username", Validators.validateUsername, userData.username); + const inputOptionsEmail = mockGetInput("E-mail", Validators.validateEmail, userData.email); + + mockedClient.signUpTeacher.mockResolvedValueOnce(); + + await coursesProvider.inviteTeacher(); + + expect(mockedVscode.window.showInputBox).toHaveBeenCalledTimes(4); + expect(mockedVscode.window.showInputBox).toHaveBeenNthCalledWith(1, inputOptionsFirstName); + expect(mockedVscode.window.showInputBox).toHaveBeenNthCalledWith(2, inputOptionsLastName); + expect(mockedVscode.window.showInputBox).toHaveBeenNthCalledWith(3, inputOptionsUsername); + expect(mockedVscode.window.showInputBox).toHaveBeenNthCalledWith(4, inputOptionsEmail); + expect(mockedClient.signUpTeacher).toHaveBeenCalledTimes(1); + expect(mockedClient.signUpTeacher).toHaveBeenNthCalledWith(1, userData); + }); + + it("should fill up invite teacher form correctly", async () => { const userData = { username: "johndoe", password: "password", diff --git a/vscode4teaching-extension/test/unitSuite/V4TTreeItem.test.ts b/vscode4teaching-extension/test/unitSuite/V4TTreeItem.test.ts index 2b33bb1e..b5603203 100644 --- a/vscode4teaching-extension/test/unitSuite/V4TTreeItem.test.ts +++ b/vscode4teaching-extension/test/unitSuite/V4TTreeItem.test.ts @@ -18,16 +18,21 @@ function failIfItemsAreWrong() { fail("Missing items from V4TBuildItems in items array."); } } -describe("V4TItem", () => { + +describe("V4T Items", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it("should get all icons correctly", () => { failIfItemsAreWrong(); for (const item of items) { const iconPaths = item.iconPath; - if (iconPaths) { + if (typeof iconPaths === "string") { + expect(fs.existsSync(iconPaths)).toBeTruthy(); + } else if (typeof iconPaths === "object") { expect(fs.existsSync(iconPaths.dark)).toBeTruthy(); expect(fs.existsSync(iconPaths.light)).toBeTruthy(); - } else { - fail(item.contextValue + " missing icons."); } } }); diff --git a/vscode4teaching-extension/test/unitSuite/Validator.test.ts b/vscode4teaching-extension/test/unitSuite/Validator.test.ts index f9ca3471..36d6bc70 100644 --- a/vscode4teaching-extension/test/unitSuite/Validator.test.ts +++ b/vscode4teaching-extension/test/unitSuite/Validator.test.ts @@ -1,6 +1,10 @@ import { Validators } from "../../src/components/courses/Validators"; describe("Validators", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it("should validate URL correctly", () => { const invalidURLError = "Invalid URL"; const emptyURLError = "Please enter the URL of the server that you want to connect to"; @@ -59,7 +63,7 @@ describe("Validators", () => { it("should validate username correctly", () => { const lengthError = "Username must have between 4 and 50 characters"; const emptyError = "Please enter your username"; - const templateInvalidError = "Username is not valid (cannot contain the word template)"; + const templateInvalidError = "Username is not valid (cannot contain the words \"template\", \"solution\" or \"student\")"; expect(Validators.validateUsername("johndoe")).toBeUndefined(); expect(Validators.validateUsername("jnd")).toBe(lengthError); const longName = "johndoe".repeat(10); diff --git a/vscode4teaching-extension/test/unitSuite/__mocks__/mockFsUtils.ts b/vscode4teaching-extension/test/unitSuite/__mocks__/mockFsUtils.ts new file mode 100644 index 00000000..29dd1a88 --- /dev/null +++ b/vscode4teaching-extension/test/unitSuite/__mocks__/mockFsUtils.ts @@ -0,0 +1,38 @@ +export const mockFsStatus = (isDirectory: boolean) => ({ + isFile: jest.fn(), + isDirectory: jest.fn().mockReturnValue(isDirectory), + isBlockDevice: jest.fn(), + isCharacterDevice: jest.fn(), + isSymbolicLink: jest.fn(), + isFIFO: jest.fn(), + isSocket: jest.fn(), + dev: 2051, + mode: 16893, + nlink: 4, + uid: 1000, + gid: 1000, + rdev: 0, + blksize: 4096, + ino: 2623964, + size: 4096, + blocks: 8, + atimeMs: 1, + mtimeMs: 1, + ctimeMs: 1, + birthtimeMs: 1, + atime: new Date(), + mtime: new Date(), + ctime: new Date(), + birthtime: new Date() +}); + +export const mockFsDirent = (name: string, isDirectory: boolean) => ({ + name, + isBlockDevice: jest.fn(), + isCharacterDevice: jest.fn(), + isDirectory: jest.fn().mockReturnValue(isDirectory), + isFIFO: jest.fn(), + isFile: jest.fn(), + isSocket: jest.fn(), + isSymbolicLink: jest.fn() +}); \ No newline at end of file diff --git a/vscode4teaching-extension/test/unitSuite/__mocks__/mockPathUtils.ts b/vscode4teaching-extension/test/unitSuite/__mocks__/mockPathUtils.ts new file mode 100644 index 00000000..71a2d046 --- /dev/null +++ b/vscode4teaching-extension/test/unitSuite/__mocks__/mockPathUtils.ts @@ -0,0 +1,7 @@ +export const mockedPathJoin = (...chunks: string[]) => { + let finalRoute = ""; + chunks.forEach(chunk => + finalRoute = finalRoute.concat(chunk).concat("/") + ); + return finalRoute.slice(0, -1); +}; \ No newline at end of file diff --git a/vscode4teaching-extension/test/unitSuite/__mocks__/vscode.js b/vscode4teaching-extension/test/unitSuite/__mocks__/vscode.js index 8f8aba7b..e301701b 100644 --- a/vscode4teaching-extension/test/unitSuite/__mocks__/vscode.js +++ b/vscode4teaching-extension/test/unitSuite/__mocks__/vscode.js @@ -47,7 +47,7 @@ const window = { createStatusBarItem: jest.fn(() => StatusBarItem), showErrorMessage: jest.fn(), showWarningMessage: jest.fn((x, y, z) => z), // TODO: Better implementation - showInformationMessage: jest.fn(), + showInformationMessage: jest.fn((x, y, z) => z), setStatusBarMessage: jest.fn(), showInputBox: jest.fn(), showOpenDialog: jest.fn(), @@ -100,7 +100,7 @@ const Uri = jest.fn().mockImplementation((x) => { } }); const mockUriFile = f => { - return { fsPath: f } + return {fsPath: f} }; Uri.file = mockUriFile.bind(Uri); Uri.parse = mockUriFile.bind(Uri); @@ -144,8 +144,8 @@ const Range = jest.fn().mockImplementation((startLine, startCharacter, endLine, }); const commands = { - registerCommand: jest.fn(() => Promise.resolve({ data: {} })), - executeCommand: jest.fn(() => Promise.resolve({ data: {} })) + registerCommand: jest.fn(() => Promise.resolve({data: {}})), + executeCommand: jest.fn(() => Promise.resolve({data: {}})) }; const TreeItemCollapsibleState = { diff --git a/vscode4teaching-extension/tsconfig.json b/vscode4teaching-extension/tsconfig.json index db90dea9..33286b88 100644 --- a/vscode4teaching-extension/tsconfig.json +++ b/vscode4teaching-extension/tsconfig.json @@ -5,7 +5,7 @@ "outDir": "out", "lib": [ "dom", - "es6" + "es2019" ], "sourceMap": true, "strict": true, /* enable all strict type-checking options */ @@ -20,4 +20,4 @@ "test" ], "exclude": [] -} \ No newline at end of file +} diff --git a/vscode4teaching-server/API.json b/vscode4teaching-server/API.json index edd2c3b4..efd6cdd2 100644 --- a/vscode4teaching-server/API.json +++ b/vscode4teaching-server/API.json @@ -2,17 +2,23 @@ "openapi": "3.0.3", "info": { "title": "VSCode4Teaching", - "description": "VSCode4Teaching REST API Documentation", + "description": "VSCode4Teaching REST API Documentation.", "contact": { "name": "VSCode4Teaching", "url": "https://github.com/codeurjc-students/2019-VSCode4Teaching" }, "license": { - "name": "Apache-2.0 LICENSE", + "name": "V4T License (Apache-2.0 LICENSE)", "url": "https://github.com/codeurjc-students/2019-VSCode4Teaching/blob/master/LICENSE" }, - "version": "2.0.2" + "version": "2.2.0" }, + "servers": [ + { + "url": "http://localhost:8080", + "description": "Inferred Url" + } + ], "tags": [ { "name": "comment-controller", @@ -33,10 +39,6 @@ { "name": "jwt-login-controller", "description": "JWT Login Controller" - }, - { - "name": "view-controller", - "description": "View Controller" } ], "paths": { @@ -985,10 +987,52 @@ "404": { "description": "Not Found" } - } + }, + "deprecated": true } }, "/api/exercises/{exerciseId}": { + "get": { + "tags": [ + "exercise-controller" + ], + "summary": "getExercise", + "operationId": "getExerciseUsingGET", + "parameters": [ + { + "name": "exerciseId", + "in": "path", + "description": "exerciseId", + "required": true, + "style": "simple", + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ExerciseCourseViewView" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + } + }, "put": { "tags": [ "exercise-controller" @@ -1266,13 +1310,76 @@ } } }, + "/api/v2/courses/{courseId}/exercises": { + "post": { + "tags": [ + "exercise-controller" + ], + "summary": "addExercises", + "operationId": "addExercisesUsingPOST", + "parameters": [ + { + "name": "courseId", + "in": "path", + "description": "courseId", + "required": true, + "style": "simple", + "schema": { + "minimum": 1, + "exclusiveMinimum": false, + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ExerciseDTO" + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ExerciseCourseViewView" + } + } + } + } + }, + "201": { + "description": "Created" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + } + } + }, "/api/exercises/{id}/files": { "get": { "tags": [ "exercise-files-controller" ], - "summary": "downloadExerciseFiles", - "operationId": "downloadExerciseFilesUsingGET", + "summary": "downloadFiles", + "operationId": "downloadFilesUsingGET", "parameters": [ { "name": "id", @@ -1284,6 +1391,16 @@ "type": "integer", "format": "int64" } + }, + { + "name": "resourceType", + "in": "path", + "description": "resourceType", + "required": true, + "style": "simple", + "schema": { + "type": "string" + } } ], "responses": { @@ -1305,8 +1422,8 @@ "tags": [ "exercise-files-controller" ], - "summary": "uploadZip", - "operationId": "uploadZipUsingPOST", + "summary": "uploadFiles", + "operationId": "uploadFilesUsingPOST", "parameters": [ { "name": "id", @@ -1318,6 +1435,16 @@ "type": "integer", "format": "int64" } + }, + { + "name": "type", + "in": "path", + "description": "type", + "required": true, + "style": "simple", + "schema": { + "type": "string" + } } ], "requestBody": { @@ -1365,13 +1492,13 @@ } } }, - "/api/exercises/{id}/files/template": { + "/api/exercises/{id}/files/{resourceType}": { "get": { "tags": [ "exercise-files-controller" ], - "summary": "getTemplate", - "operationId": "getTemplateUsingGET", + "summary": "downloadFiles", + "operationId": "downloadFilesUsingGET_1", "parameters": [ { "name": "id", @@ -1383,6 +1510,16 @@ "type": "integer", "format": "int64" } + }, + { + "name": "resourceType", + "in": "path", + "description": "resourceType", + "required": true, + "style": "simple", + "schema": { + "type": "string" + } } ], "responses": { @@ -1399,13 +1536,15 @@ "description": "Not Found" } } - }, + } + }, + "/api/exercises/{id}/files/{type}": { "post": { "tags": [ "exercise-files-controller" ], - "summary": "uploadTemplate", - "operationId": "uploadTemplateUsingPOST", + "summary": "uploadFiles", + "operationId": "uploadFilesUsingPOST_1", "parameters": [ { "name": "id", @@ -1417,6 +1556,16 @@ "type": "integer", "format": "int64" } + }, + { + "name": "type", + "in": "path", + "description": "type", + "required": true, + "style": "simple", + "schema": { + "type": "string" + } } ], "requestBody": { @@ -1862,96 +2011,6 @@ } } } - }, - "/": { - "get": { - "tags": [ - "view-controller" - ], - "summary": "redirect", - "operationId": "redirectUsingGET", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "type": "string" - } - } - } - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Forbidden" - }, - "404": { - "description": "Not Found" - } - } - } - }, - "/app/**/{path}": { - "get": { - "tags": [ - "view-controller" - ], - "summary": "serveAngularWebapp", - "operationId": "serveAngularWebappUsingGET", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "type": "string" - } - } - } - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Forbidden" - }, - "404": { - "description": "Not Found" - } - } - } - }, - "/{path}": { - "get": { - "tags": [ - "view-controller" - ], - "summary": "serveAngularWebapp", - "operationId": "serveAngularWebappUsingGET_1", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "type": "string" - } - } - } - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Forbidden" - }, - "404": { - "description": "Not Found" - } - } - } } }, "components": { @@ -2204,6 +2263,9 @@ "title": "ExerciseCourseViewView", "type": "object", "properties": { + "allowEditionAfterSolutionDownloaded": { + "type": "boolean" + }, "course": { "$ref": "#/components/schemas/CourseCourseViewView" }, @@ -2215,9 +2277,15 @@ "type": "integer", "format": "int64" }, + "includesTeacherSolution": { + "type": "boolean" + }, "name": { "type": "string" }, + "solutionIsPublic": { + "type": "boolean" + }, "updateDateTime": { "type": "string", "format": "date-time" @@ -2228,8 +2296,17 @@ "title": "ExerciseDTO", "type": "object", "properties": { + "allowEditionAfterSolutionDownloaded": { + "type": "boolean" + }, + "includesTeacherSolution": { + "type": "boolean" + }, "name": { "type": "string" + }, + "solutionIsPublic": { + "type": "boolean" } } }, @@ -2237,6 +2314,9 @@ "title": "ExerciseExercisesViewView", "type": "object", "properties": { + "allowEditionAfterSolutionDownloaded": { + "type": "boolean" + }, "createDateTime": { "type": "string", "format": "date-time" @@ -2245,9 +2325,15 @@ "type": "integer", "format": "int64" }, + "includesTeacherSolution": { + "type": "boolean" + }, "name": { "type": "string" }, + "solutionIsPublic": { + "type": "boolean" + }, "updateDateTime": { "type": "string", "format": "date-time" @@ -2306,6 +2392,9 @@ "title": "ExerciseGeneralViewView", "type": "object", "properties": { + "allowEditionAfterSolutionDownloaded": { + "type": "boolean" + }, "course": { "$ref": "#/components/schemas/CourseGeneralViewView" }, @@ -2317,9 +2406,15 @@ "type": "integer", "format": "int64" }, + "includesTeacherSolution": { + "type": "boolean" + }, "name": { "type": "string" }, + "solutionIsPublic": { + "type": "boolean" + }, "updateDateTime": { "type": "string", "format": "date-time" @@ -2337,8 +2432,12 @@ } }, "status": { - "type": "integer", - "format": "int32" + "type": "string", + "enum": [ + "FINISHED", + "IN_PROGRESS", + "NOT_STARTED" + ] } } }, @@ -2365,8 +2464,12 @@ } }, "status": { - "type": "integer", - "format": "int32" + "type": "string", + "enum": [ + "FINISHED", + "IN_PROGRESS", + "NOT_STARTED" + ] }, "updateDateTime": { "type": "string", @@ -2550,7 +2653,7 @@ "type": "string" }, "username": { - "pattern": "^(?:(?!template).)+$", + "pattern": "^(?:(?!(template)|(solution)|(student)).)+$", "type": "string" } } diff --git a/vscode4teaching-server/README.md b/vscode4teaching-server/README.md index f9f181d0..d1e8ab7f 100644 --- a/vscode4teaching-server/README.md +++ b/vscode4teaching-server/README.md @@ -81,7 +81,7 @@ mvn test ### Arguments and environment variables It is possible to enter some arguments in the JAR run line or as environment variables in the [``.env``](vscode4teaching-server/docker/.env) file to modify the basic application settings. They are: -- ``server.port`` (default: 8080). It is used to modify the port on which the application is served. +- ``server.port`` (default: ``8080``). It is used to modify the port on which the application is served. - ``jwt.secret`` (default: ``vscode4teaching``. Secret used to generate JWT tokens. It is **IMPORTANT** to change this value in production to ensure security. - ``v4t.filedirectory`` (default: ``v4t-course``). This is the directory where all course and exercise files created and submitted are stored. - Configuration of the database: diff --git a/vscode4teaching-server/pom.xml b/vscode4teaching-server/pom.xml index 05a81d4d..f750727f 100644 --- a/vscode4teaching-server/pom.xml +++ b/vscode4teaching-server/pom.xml @@ -6,13 +6,13 @@ <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> - <version>2.5.14</version> + <version>2.7.5</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.vscode4teaching</groupId> <artifactId>vscode4teaching-server</artifactId> - <version>2.1.4</version> + <version>2.2.0</version> <name>VSCode 4 Teaching</name> <description>Server side of VSCode 4 Teaching extension.</description> @@ -52,6 +52,11 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> + <dependency> + <groupId>org.yaml</groupId> + <artifactId>snakeyaml</artifactId> + <version>1.33</version> + </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> @@ -93,7 +98,7 @@ <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> - <version>2.1.212</version> + <version>2.1.214</version> <scope>test</scope> </dependency> <dependency> diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/DatabaseFileInitializer.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/DatabaseFileInitializer.java index f55b183b..ecb00256 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/DatabaseFileInitializer.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/DatabaseFileInitializer.java @@ -1,24 +1,9 @@ package com.vscode4teaching.vscode4teachingserver; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.regex.Pattern; -import java.util.stream.Stream; - -import javax.transaction.Transactional; - import com.vscode4teaching.vscode4teachingserver.model.*; import com.vscode4teaching.vscode4teachingserver.model.repositories.CourseRepository; import com.vscode4teaching.vscode4teachingserver.model.repositories.ExerciseFileRepository; import com.vscode4teaching.vscode4teachingserver.model.repositories.ExerciseRepository; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -28,26 +13,35 @@ import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; +import javax.transaction.Transactional; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.stream.Stream; + @Component -@ConditionalOnProperty(value = "file.initialization", havingValue = "true", matchIfMissing = false) +@ConditionalOnProperty(value = "file.initialization", havingValue = "true") @Transactional @Order(1) public class DatabaseFileInitializer implements CommandLineRunner { + private static final Logger logger = LoggerFactory.getLogger(DatabaseFileInitializer.class); @Autowired private CourseRepository courseRepository; - @Autowired private ExerciseRepository exerciseRepository; - @Autowired private ExerciseFileRepository fileRepository; - @Value("${v4t.filedirectory}") private String rootPath; - private static final Logger logger = LoggerFactory.getLogger(DatabaseFileInitializer.class); - @Override public void run(String... args) throws IOException { if (Files.exists(Paths.get(rootPath))) { @@ -61,7 +55,7 @@ public void run(String... args) throws IOException { long course_id = Long.parseLong(courseParts[courseParts.length - 1]); Optional<Course> courseOpt = courseRepository.findById(course_id); // If not found build course name and try to find it - if (!courseOpt.isPresent()) { + if (courseOpt.isEmpty()) { List<String> coursePartsList = new ArrayList<>(Arrays.asList(courseParts)); coursePartsList.remove(courseParts[courseParts.length - 1]); String courseName = String.join(" ", coursePartsList); @@ -88,7 +82,7 @@ public void run(String... args) throws IOException { .filter(exercise -> exercise.getId().equals(exercise_id) && exercise.getName().equalsIgnoreCase(exerciseName)) .findFirst(); - if (!exerciseOpt.isPresent()) { + if (exerciseOpt.isEmpty()) { exerciseOpt = exerciseRepository.findByCourseAndNameIgnoreCase(course, exerciseName); if (exerciseOpt.isPresent()) { Path dir = Paths.get(rootPath + File.separator + parts[1] + File.separator + parts[2]); @@ -115,7 +109,7 @@ public void run(String... args) throws IOException { try { long userInfoId = Integer.parseInt(userParts[userParts.length - 1]); userInfoOpt = exercise.getUserInfo().stream().filter(eui -> eui.getId().equals(userInfoId)).findFirst(); - } catch(NumberFormatException nfe) { + } catch (NumberFormatException nfe) { logger.error("File initialization for exercise " + exercise_id + " and user " + parts[3] + " went wrong."); } if (userInfoOpt.isPresent()) { diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/DatabaseInitializer.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/DatabaseInitializer.java index 831d7821..d6725ee8 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/DatabaseInitializer.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/DatabaseInitializer.java @@ -1,19 +1,7 @@ package com.vscode4teaching.vscode4teachingserver; -import java.util.ArrayList; -import java.util.List; - -import com.vscode4teaching.vscode4teachingserver.model.Course; -import com.vscode4teaching.vscode4teachingserver.model.Exercise; -import com.vscode4teaching.vscode4teachingserver.model.ExerciseUserInfo; -import com.vscode4teaching.vscode4teachingserver.model.Role; -import com.vscode4teaching.vscode4teachingserver.model.User; -import com.vscode4teaching.vscode4teachingserver.model.repositories.CourseRepository; -import com.vscode4teaching.vscode4teachingserver.model.repositories.ExerciseRepository; -import com.vscode4teaching.vscode4teachingserver.model.repositories.ExerciseUserInfoRepository; -import com.vscode4teaching.vscode4teachingserver.model.repositories.RoleRepository; -import com.vscode4teaching.vscode4teachingserver.model.repositories.UserRepository; - +import com.vscode4teaching.vscode4teachingserver.model.*; +import com.vscode4teaching.vscode4teachingserver.model.repositories.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.CommandLineRunner; @@ -22,8 +10,11 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; +import java.util.ArrayList; +import java.util.List; + @Component -@ConditionalOnProperty(value = "data.initialization", havingValue = "true", matchIfMissing = false) +@ConditionalOnProperty(value = "data.initialization", havingValue = "true") @Order(0) public class DatabaseInitializer implements CommandLineRunner { @@ -66,7 +57,7 @@ private User saveUser(User user, boolean isTeacher) { } @Override - public void run(String... args) throws Exception { + public void run(String... args) { User teacher = new User("johndoe@teacher.com", "johndoe", passwordEncoder.encode("teacherpassword"), "John", "Doe"); @@ -100,14 +91,15 @@ public void run(String... args) throws Exception { for (Course course : courses) { for (int j = 1; j < 6; j++) { - Exercise exercise = new Exercise("Exercise " + j, course); + Exercise exercise = new Exercise("Exercise " + j); + exercise.setCourse(course); course.addExercise(exercise); Exercise savedExercise = exerciseRepository.save(exercise); for (User user : users) { ExerciseUserInfo eui = new ExerciseUserInfo(savedExercise, user); if (course.equals(courses.get(0)) && savedExercise.getName().equals("Exercise 1") && (user.equals(users.get(2)) || user.equals(users.get(3)))) { - eui.setStatus(1); + eui.setStatus(ExerciseStatus.FINISHED); } exerciseUserInfoRepository.save(eui); } diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/DatabaseSuperuserInitializer.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/DatabaseSuperuserInitializer.java index a74ac689..e1fda0f4 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/DatabaseSuperuserInitializer.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/DatabaseSuperuserInitializer.java @@ -4,7 +4,6 @@ import com.vscode4teaching.vscode4teachingserver.model.User; import com.vscode4teaching.vscode4teachingserver.model.repositories.RoleRepository; import com.vscode4teaching.vscode4teachingserver.model.repositories.UserRepository; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.CommandLineRunner; @@ -49,15 +48,15 @@ private void addRole(User user, String roleName) { user.addRole(role); } - private User saveUser(User user) { + private void saveUser(User user) { addRole(user, "ROLE_STUDENT"); addRole(user, "ROLE_TEACHER"); - return userRepository.save(user); + userRepository.save(user); } @Override - public void run(String... args) throws Exception { - if (!userRepository.findByEmail(email).isPresent()) { + public void run(String... args) { + if (userRepository.findByEmail(email).isEmpty()) { User superuser = new User(email, username, passwordEncoder.encode(password), name, lastname); saveUser(superuser); } diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/VS4TApplication.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/VS4TApplication.java index 0f5221ae..7b1ac5a5 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/VS4TApplication.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/VS4TApplication.java @@ -8,7 +8,6 @@ import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; -import springfox.documentation.service.VendorExtension; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; @@ -21,39 +20,39 @@ @SpringBootApplication public class VS4TApplication { - @Value("${v4t.version}") - public String v4tVersion; - - /** - * API Documentation configuration (OpenAPI 3.0.3 format) - */ - @Bean - public Docket apiDocumentation() { - return new Docket(DocumentationType.OAS_30) - .select() - .apis(RequestHandlerSelectors.basePackage("com.vscode4teaching.vscode4teachingserver")) - .paths(PathSelectors.ant("/api/**")) - .build() - .apiInfo(new ApiInfo( - "VSCode4Teaching", - "VSCode4Teaching REST API Documentation.", - v4tVersion, - "", - new Contact("VSCode4Teaching", "https://github.com/codeurjc-students/2019-VSCode4Teaching", ""), - "V4T License (Apache-2.0 LICENSE)", - "https://github.com/codeurjc-students/2019-VSCode4Teaching/blob/master/LICENSE", - new ArrayList<>() - ) - ); - } - - public static void main(String[] args) { - SpringApplication.run(VS4TApplication.class, args); - } - - @PostConstruct - public void timezoneConfiguration() { - TimeZone.setDefault(TimeZone.getTimeZone("UTC")); - } + @Value("${v4t.version}") + public String v4tVersion; + + public static void main(String[] args) { + SpringApplication.run(VS4TApplication.class, args); + } + + /** + * API Documentation configuration (OpenAPI 3.0.3 format) + */ + @Bean + public Docket apiDocumentation() { + return new Docket(DocumentationType.OAS_30) + .select() + .apis(RequestHandlerSelectors.basePackage("com.vscode4teaching.vscode4teachingserver")) + .paths(PathSelectors.ant("/api/**")) + .build() + .apiInfo(new ApiInfo( + "VSCode4Teaching", + "VSCode4Teaching REST API Documentation.", + v4tVersion, + "", + new Contact("VSCode4Teaching", "https://github.com/codeurjc-students/2019-VSCode4Teaching", ""), + "V4T License (Apache-2.0 LICENSE)", + "https://github.com/codeurjc-students/2019-VSCode4Teaching/blob/master/LICENSE", + new ArrayList<>() + ) + ); + } + + @PostConstruct + public void timezoneConfiguration() { + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + } } diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/CourseController.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/CourseController.java index 046d8650..c19a145f 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/CourseController.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/CourseController.java @@ -138,7 +138,7 @@ public ResponseEntity<String> getCode(@PathVariable Long courseId, HttpServletRe @GetMapping("/courses/code/{courseCode}") @JsonView(CourseViews.ExercisesView.class) public ResponseEntity<Course> getExercisesWithCode(HttpServletRequest request, @PathVariable String courseCode) - throws CourseNotFoundException, NotInCourseException, UserNotFoundException { + throws CourseNotFoundException, UserNotFoundException { logger.info("Request to GET '/api/courses/code/{}' (deprecated API endpoint)", courseCode); return ResponseEntity.ok(courseService.joinCourseWithSharingCode(courseCode, jwtTokenUtil.getUsernameFromToken(request))); } @@ -154,7 +154,7 @@ public ResponseEntity<Course> getCourseInformationBySharingCode(@PathVariable St @PutMapping("/courses/code/{courseCode}") @JsonView(CourseViews.ExercisesView.class) public ResponseEntity<Course> joinCourse(HttpServletRequest request, @PathVariable String courseCode) - throws CourseNotFoundException, NotInCourseException, UserNotFoundException { + throws CourseNotFoundException, UserNotFoundException { logger.info("Request to PUT '/api/courses/code/{}'", courseCode); return ResponseEntity.ok(courseService.joinCourseWithSharingCode(courseCode, jwtTokenUtil.getUsernameFromToken(request))); } diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/ExerciseController.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/ExerciseController.java index 0d1fd682..a3a72909 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/ExerciseController.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/ExerciseController.java @@ -74,19 +74,31 @@ public ResponseEntity<List<Exercise>> addExercises(HttpServletRequest request, @ ArrayList<Exercise> savedExercises = new ArrayList<>(); for (ExerciseDTO exerciseDTO : exercisesDTO) { Exercise exercise = new Exercise(exerciseDTO.name); + exercise.setIncludesTeacherSolution(exerciseDTO.includesTeacherSolution); + exercise.setSolutionIsPublic(exerciseDTO.solutionIsPublic); + exercise.setAllowEditionAfterSolutionDownloaded(exerciseDTO.allowEditionAfterSolutionDownloaded); savedExercises.add(courseService.addExerciseToCourse(courseId, exercise, jwtTokenUtil.getUsernameFromToken(request))); } return new ResponseEntity<>(savedExercises, HttpStatus.CREATED); } + @GetMapping("/exercises/{exerciseId}") + @JsonView(ExerciseViews.CourseView.class) + public ResponseEntity<Exercise> getExercise(@PathVariable Long exerciseId) throws ExerciseNotFoundException { + logger.info("Request to GET '/api/exercises/{}'", exerciseId); + return ResponseEntity.ok(courseService.getExercise(exerciseId)); + } + @PutMapping("/exercises/{exerciseId}") @JsonView(ExerciseViews.CourseView.class) public ResponseEntity<Exercise> updateExercise(HttpServletRequest request, @PathVariable @Min(1) Long exerciseId, @RequestBody ExerciseDTO exerciseDTO) throws ExerciseNotFoundException, NotInCourseException { logger.info("Request to PUT '/api/exercises/{}' with body '{}'", exerciseId, exerciseDTO); - Exercise exercise = new Exercise(exerciseDTO.getName()); - return ResponseEntity - .ok(courseService.editExercise(exerciseId, exercise, jwtTokenUtil.getUsernameFromToken(request))); + Exercise exercise = new Exercise(exerciseDTO.name); + exercise.setIncludesTeacherSolution(exerciseDTO.includesTeacherSolution); + exercise.setSolutionIsPublic(exerciseDTO.solutionIsPublic); + exercise.setAllowEditionAfterSolutionDownloaded(exerciseDTO.allowEditionAfterSolutionDownloaded); + return ResponseEntity.ok(courseService.editExercise(exerciseId, exercise, jwtTokenUtil.getUsernameFromToken(request))); } @DeleteMapping("/exercises/{exerciseId}") diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/ExerciseFilesController.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/ExerciseFilesController.java index 7e9c30a7..29e9dbe0 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/ExerciseFilesController.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/ExerciseFilesController.java @@ -21,10 +21,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -35,6 +32,8 @@ @RequestMapping("/api") public class ExerciseFilesController { private static final String templateFolderName = "template"; + private static final String solutionFolderName = "solution"; + private final ExerciseFilesService filesService; private final JWTTokenUtil jwtTokenUtil; @@ -45,79 +44,53 @@ public ExerciseFilesController(ExerciseFilesService filesService, JWTTokenUtil j this.jwtTokenUtil = jwtTokenUtil; } - @GetMapping(value = "/exercises/{id}/files", produces = "application/zip") - public void downloadExerciseFiles(@PathVariable Long id, HttpServletRequest request, HttpServletResponse response) - throws ExerciseNotFoundException, NotInCourseException, IOException, NoTemplateException { - logger.info("Request to GET '/api/exercises/{}/files'", id); - String username = jwtTokenUtil.getUsernameFromToken(request); - Map<Exercise, List<File>> filesMap = filesService.getExerciseFiles(id, username); - Optional<List<File>> optFiles = filesMap.values().stream().findFirst(); - List<File> files = optFiles.orElseGet(ArrayList::new); - String zipName = files.get(0).getParentFile().getName().equals(ExerciseFilesController.templateFolderName) - ? "template-" + id - : "exercise-" + id + "-" + username; - response.setStatus(HttpServletResponse.SC_OK); - String[] header = headerFilename(zipName + ".zip"); - response.addHeader(header[0], header[1]); - String fileSeparatorPattern = Pattern.quote(File.separator); - String separator = files.get(0).getAbsolutePath().split( - fileSeparatorPattern + ExerciseFilesController.templateFolderName + fileSeparatorPattern).length > 1 - ? ExerciseFilesController.templateFolderName - : "student_[0-9]*"; - exportToZip(response, files, separator); - } - @PostMapping("/exercises/{id}/files") - public ResponseEntity<List<UploadFileResponse>> uploadZip(@PathVariable Long id, - @RequestParam("file") MultipartFile zip, HttpServletRequest request) - throws NotInCourseException, IOException, ExerciseFinishedException, NotFoundException { - logger.info("Request to POST '/api/exercises/{}/files' with a MultipartFile (ZIP) as body", id); - String username = jwtTokenUtil.getUsernameFromToken(request); - Map<Exercise, List<File>> filesMap = filesService.saveExerciseFiles(id, zip, username); - Optional<List<File>> optFiles = filesMap.values().stream().findFirst(); - List<File> files = optFiles.isPresent() ? optFiles.get() : new ArrayList<>(); - List<UploadFileResponse> uploadResponse = new ArrayList<>(files.size()); - String pattern = "student_[0-9]*" + File.separator; - for (File file : files) { - String[] filePath = file.getCanonicalPath().split(pattern); - uploadResponse.add(new UploadFileResponse(filePath[filePath.length - 1], - file.toURI().toURL().openConnection().getContentType(), file.length())); - } - return ResponseEntity.ok(uploadResponse); - } + // GET endpoint - @PostMapping("/exercises/{id}/files/template") - public ResponseEntity<List<UploadFileResponse>> uploadTemplate(@PathVariable Long id, - @RequestParam("file") MultipartFile zip, HttpServletRequest request) - throws ExerciseNotFoundException, NotInCourseException, IOException { - logger.info("Request to POST '/api/exercises/{}/files/template' with a MultipartFile (ZIP) as body", id); + @GetMapping(value = {"/exercises/{id}/files", "/exercises/{id}/files/{resourceType:template|solution}"}, produces = "application/zip") + public void downloadFiles(@PathVariable Long id, @PathVariable(required = false) Optional<String> resourceType, + HttpServletRequest request, HttpServletResponse response) + throws ExerciseNotFoundException, NotInCourseException, IOException, NoTemplateException, NoSolutionException { + logger.info("Request to GET '/api/exercises/{}/files/{}'", id, resourceType); String username = jwtTokenUtil.getUsernameFromToken(request); - Map<Exercise, List<File>> filesMap = filesService.saveExerciseTemplate(id, zip, username); - Optional<List<File>> optFiles = filesMap.values().stream().findFirst(); - List<File> files = optFiles.isPresent() ? optFiles.get() : new ArrayList<>(); - List<UploadFileResponse> uploadResponse = new ArrayList<>(files.size()); - String fileSeparatorPattern = Pattern.quote(File.separator); - String pattern = fileSeparatorPattern + ExerciseFilesController.templateFolderName + fileSeparatorPattern; - for (File file : files) { - String[] filePath = file.getCanonicalPath().split(pattern); - uploadResponse.add(new UploadFileResponse(filePath[filePath.length - 1], - file.toURI().toURL().openConnection().getContentType(), file.length())); + + // Stage 1: All the information required to execute the process is obtained and file paths are returned from + // database using filesService specific methods. This process distinguishes between the different possible cases: + // - Downloading individual files for each student's exercise + // - Downloading an exercise template + // - Downloading a proposed solution for an exercise (if exists) + Map<Exercise, List<File>> filesMap; + String zipName; + String separator; + if (resourceType.isPresent() && resourceType.get().equals("template")) { + zipName = "template-" + id; + separator = ExerciseFilesController.templateFolderName; + filesMap = filesService.getExerciseTemplate(id, username); + } else if (resourceType.isPresent() && resourceType.get().equals("solution")) { + zipName = "solution-" + id; + separator = ExerciseFilesController.solutionFolderName; + filesMap = filesService.getExerciseSolution(id, username); + } else { + if (filesService.existsExerciseFilesForUser(id, username)) { + zipName = "exercise-" + id + "-" + username; + separator = "student_[0-9]*"; + } else { + zipName = "template-" + id; + separator = "template"; + } + // Order matters here: if checked existence of files after generating them, this method will fail + filesMap = filesService.getExerciseFiles(id, username); } - return ResponseEntity.ok(uploadResponse); - } - @GetMapping("/exercises/{id}/files/template") - public void getTemplate(@PathVariable Long id, HttpServletResponse response, HttpServletRequest request) - throws ExerciseNotFoundException, NotInCourseException, NoTemplateException, IOException { - logger.info("Request to GET '/api/exercises/{}/files/template'", id); - String username = jwtTokenUtil.getUsernameFromToken(request); - Map<Exercise, List<File>> filesMap = filesService.getExerciseTemplate(id, username); + // Stage 2: files from returned paths are zipped and sent as response to client (method exportToZip()) Optional<List<File>> optFiles = filesMap.values().stream().findFirst(); - List<File> files = optFiles.isPresent() ? optFiles.get() : new ArrayList<>(); + List<File> files = optFiles.orElseGet(ArrayList::new); + response.setStatus(HttpServletResponse.SC_OK); - String[] header = headerFilename("template-" + id + ".zip"); + String[] header = headerFilename(zipName + ".zip"); response.addHeader(header[0], header[1]); - exportToZip(response, files, ExerciseFilesController.templateFolderName); + + exportToZip(response, files, separator); } @GetMapping("/exercises/{id}/teachers/files") @@ -127,16 +100,24 @@ public void getAllStudentsFiles(@PathVariable Long id, HttpServletRequest reques String username = jwtTokenUtil.getUsernameFromToken(request); Map<Exercise, List<File>> filesMap = filesService.getAllStudentsFiles(id, username); Optional<List<File>> optFiles = filesMap.values().stream().findFirst(); - List<File> files = optFiles.isPresent() ? optFiles.get() : new ArrayList<>(); + List<File> files = optFiles.orElseGet(ArrayList::new); response.setStatus(HttpServletResponse.SC_OK); String[] header = headerFilename("exercise-" + id + "-files.zip"); response.addHeader(header[0], header[1]); Optional<Exercise> exOpt = filesMap.keySet().stream().findFirst(); - String exerciseDirectory = exOpt.isPresent() ? exOpt.get().getName().toLowerCase().replace(" ", "_") + "_" + id - : ""; + String exerciseDirectory = exOpt.map(exercise -> exercise.getName().toLowerCase().replace(" ", "_") + "_" + id).orElse(""); exportToZip(response, files, exerciseDirectory); } + @JsonView(FileViews.GeneralView.class) + @GetMapping("/users/{username}/exercises/{exerciseId}/files") + public ResponseEntity<List<ExerciseFile>> getFileInfoByOwnerAndExercise(@PathVariable String username, + @PathVariable Long exerciseId) throws NotFoundException { + logger.info("Request to GET '/api/users/{}/exercises/{}/files'", username, exerciseId); + List<ExerciseFile> files = filesService.getFileIdsByExerciseAndId(exerciseId, username); + return files.isEmpty() ? ResponseEntity.noContent().build() : ResponseEntity.ok(files); + } + private String[] headerFilename(String filename) { String[] headerElements = new String[2]; headerElements[0] = "Content-Disposition"; @@ -149,9 +130,8 @@ private void exportToZip(HttpServletResponse response, List<File> files, String ZipOutputStream zipOutputStream = new ZipOutputStream(response.getOutputStream()); for (File file : files) { try { - String pattern = parentDirectory + File.separator; - String[] filePath = file.getCanonicalPath().split(pattern); - String zipFilePath = filePath[filePath.length - 1].replace('\\', '/'); + String[] filePath = file.getCanonicalPath().split(parentDirectory); + String zipFilePath = filePath[filePath.length - 1].substring(1).replace('\\', '/'); zipOutputStream.putNextEntry(new ZipEntry(zipFilePath)); FileInputStream fileInputStream = new FileInputStream(file); @@ -162,18 +142,49 @@ private void exportToZip(HttpServletResponse response, List<File> files, String } finally { zipOutputStream.closeEntry(); } - - } zipOutputStream.close(); } - @JsonView(FileViews.GeneralView.class) - @GetMapping("/users/{username}/exercises/{exerciseId}/files") - public ResponseEntity<List<ExerciseFile>> getFileInfoByOwnerAndExercise(@PathVariable String username, - @PathVariable Long exerciseId) throws ExerciseNotFoundException { - logger.info("Request to GET '/api/users/{}/exercises/{}/files'", username, exerciseId); - List<ExerciseFile> files = filesService.getFileIdsByExerciseAndOwner(exerciseId, username); - return files.isEmpty() ? ResponseEntity.noContent().build() : ResponseEntity.ok(files); + + // POST endpoint + + @PostMapping(value = {"/exercises/{id}/files", "/exercises/{id}/files/{type:template|solution}"}) + public ResponseEntity<List<UploadFileResponse>> uploadFiles(@PathVariable Long id, @PathVariable(required = false) String type, + @RequestParam("file") MultipartFile zip, HttpServletRequest request) + throws NotFoundException, NotInCourseException, IOException, ExerciseFinishedException { + logger.info("Request to POST '/api/exercises/{}/files/{}' with a MultipartFile (ZIP) as body", id, type); + + // Stage 1: All the information necessary to execute the process is obtained and files are saved using + // filesService specific methods. This process distinguishes between the different possible cases: + // - Uploading of individual files for each student's exercise + // - Uploading of an exercise template. + // - Uploading of the proposed solution to an exercise (if existing). + String username = jwtTokenUtil.getUsernameFromToken(request); + + Map<Exercise, List<File>> filesMap; + String fileSeparatorPattern = Pattern.quote(File.separator); + String pattern; + if (Objects.equals(type, "template")) { + filesMap = filesService.saveExerciseTemplate(id, zip, username); + pattern = fileSeparatorPattern + ExerciseFilesController.templateFolderName + fileSeparatorPattern; + } else if (Objects.equals(type, "solution")) { + filesMap = filesService.saveExerciseSolution(id, zip, username); + pattern = fileSeparatorPattern + ExerciseFilesController.solutionFolderName + fileSeparatorPattern; + } else { + filesMap = filesService.saveExerciseFiles(id, zip, username); + pattern = fileSeparatorPattern + "student_[0-9]*" + fileSeparatorPattern; + } + + // Stage 2: saved files in previous stage are now collected and response is prepared and sent. + Optional<List<File>> optFiles = filesMap.values().stream().findFirst(); + List<File> files = optFiles.orElseGet(ArrayList::new); + List<UploadFileResponse> uploadResponse = new ArrayList<>(files.size()); + for (File file : files) { + String[] filePath = file.getCanonicalPath().split(pattern); + uploadResponse.add(new UploadFileResponse(filePath[filePath.length - 1], + file.toURI().toURL().openConnection().getContentType(), file.length())); + } + return ResponseEntity.ok(uploadResponse); } -} \ No newline at end of file +} diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/JWTLoginController.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/JWTLoginController.java index 828a5bae..22db4992 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/JWTLoginController.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/JWTLoginController.java @@ -80,8 +80,8 @@ public ResponseEntity<User> saveTeacher(@Valid @RequestBody UserDTO userDto) { String encodedPassword = bCryptPasswordEncoder.encode(userDto.getPassword()); User user = new User(userDto.getEmail(), userDto.getUsername(), encodedPassword, userDto.getName(), userDto.getLastName()); - User saveduser = userDetailsService.save(user, true); - return new ResponseEntity<>(saveduser, HttpStatus.CREATED); + User savedUser = userDetailsService.save(user, true); + return new ResponseEntity<>(savedUser, HttpStatus.CREATED); } @PostMapping("/teachers/invitation") diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/ViewController.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/ViewController.java index 7f542f09..28a7268f 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/ViewController.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/ViewController.java @@ -12,7 +12,7 @@ public String redirect() { return "forward:/app"; } - @GetMapping({"/app/**/{path:[^\\.]*}", "/{path:app[^\\.]*}"}) + @GetMapping({"/app/**/{path:[^.]*}", "/{path:app[^.]*}"}) public String serveAngularWebapp() { return "forward:/index.html"; } diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/CommentDTO.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/CommentDTO.java index 551d8c83..5a474f26 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/CommentDTO.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/CommentDTO.java @@ -1,11 +1,11 @@ package com.vscode4teaching.vscode4teachingserver.controllers.dtos; -import javax.validation.constraints.NotEmpty; - import org.hibernate.validator.constraints.Length; +import javax.validation.constraints.NotEmpty; + public class CommentDTO { - + @NotEmpty(message = "Comment author should not be empty") @Length(min = 1, message = "Comment author should not be empty") private String author; @@ -29,5 +29,5 @@ public String getBody() { public void setBody(String body) { this.body = body; } - -} \ No newline at end of file + +} diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/CommentThreadDTO.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/CommentThreadDTO.java index 8a69467d..63b89657 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/CommentThreadDTO.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/CommentThreadDTO.java @@ -1,17 +1,16 @@ package com.vscode4teaching.vscode4teachingserver.controllers.dtos; +import javax.validation.constraints.Min; import java.util.ArrayList; import java.util.List; -import javax.validation.constraints.Min; - public class CommentThreadDTO { private List<CommentDTO> comments = new ArrayList<>(); @Min(0) private Long line; private String lineText; - + public List<CommentDTO> getComments() { return comments; } @@ -28,13 +27,13 @@ public void setLine(Long line) { this.line = line; } - public String getLineText() { - return lineText; - } + public String getLineText() { + return lineText; + } + + public void setLineText(String lineText) { + this.lineText = lineText; + } - public void setLineText(String lineText) { - this.lineText = lineText; - } - } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/CourseDTO.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/CourseDTO.java index c0ac3848..75e7d2cf 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/CourseDTO.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/CourseDTO.java @@ -1,9 +1,9 @@ package com.vscode4teaching.vscode4teachingserver.controllers.dtos; -import javax.validation.constraints.NotEmpty; - import org.hibernate.validator.constraints.Length; +import javax.validation.constraints.NotEmpty; + public class CourseDTO { @NotEmpty @Length(min = 10, max = 100, message = "Course name should be between 10 and 100 characters") @@ -17,5 +17,5 @@ public void setName(String name) { this.name = name; } - + } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/ExerciseDTO.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/ExerciseDTO.java index 7e5414dd..3c7ace4a 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/ExerciseDTO.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/ExerciseDTO.java @@ -1,19 +1,17 @@ package com.vscode4teaching.vscode4teachingserver.controllers.dtos; -import javax.validation.constraints.NotEmpty; - import org.hibernate.validator.constraints.Length; +import javax.validation.constraints.NotEmpty; + public class ExerciseDTO { @NotEmpty(message = "Name cannot be empty") @Length(min = 3, max = 100, message = "Exercise name should contain between 3 and 100 characters") public String name; - public String getName() { - return name; - } + public boolean includesTeacherSolution; + + public boolean solutionIsPublic; - public void setName(String name) { - this.name = name; - } + public boolean allowEditionAfterSolutionDownloaded; } diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/ExerciseUserInfoDTO.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/ExerciseUserInfoDTO.java index b88ad85a..682fdeba 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/ExerciseUserInfoDTO.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/ExerciseUserInfoDTO.java @@ -1,24 +1,26 @@ package com.vscode4teaching.vscode4teachingserver.controllers.dtos; +import com.vscode4teaching.vscode4teachingserver.model.ExerciseStatus; + import java.util.List; public class ExerciseUserInfoDTO { - private int status; + private ExerciseStatus status; private List<String> modifiedFiles; public boolean isFinished() { - return status == 1; + return status == ExerciseStatus.FINISHED; } public boolean isStarted() { - return status > 0; + return status != ExerciseStatus.NOT_STARTED; } - public int getStatus() { + public ExerciseStatus getStatus() { return status; } - public void setStatus(int status) { + public void setStatus(ExerciseStatus status) { this.status = status; } @@ -30,4 +32,4 @@ public void setModifiedFiles(List<String> modifiedFiles) { this.modifiedFiles = modifiedFiles; } -} \ No newline at end of file +} diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/JWTRequest.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/JWTRequest.java index b62d63a8..803d1288 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/JWTRequest.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/JWTRequest.java @@ -4,26 +4,26 @@ public class JWTRequest implements Serializable { - private static final long serialVersionUID = 45654645884777L; + private static final long serialVersionUID = 45654645884777L; - private String username; - private String password; + private String username; + private String password; - public String getUsername() { - return username; - } + public String getUsername() { + return username; + } - public void setUsername(String username) { - this.username = username; - } + public void setUsername(String username) { + this.username = username; + } - public String getPassword() { - return password; - } + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } - public void setPassword(String password) { - this.password = password; - } - } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/JWTResponse.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/JWTResponse.java index 47437b11..518f40d3 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/JWTResponse.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/JWTResponse.java @@ -4,22 +4,22 @@ public class JWTResponse implements Serializable { - private static final long serialVersionUID = -89479191651681891L; + private static final long serialVersionUID = -89479191651681891L; - private String jwtToken; + private String jwtToken; - public JWTResponse() { - } + public JWTResponse() { + } - public JWTResponse(String jwtToken) { - this.jwtToken = jwtToken; - } + public JWTResponse(String jwtToken) { + this.jwtToken = jwtToken; + } - public String getJwtToken() { - return jwtToken; - } + public String getJwtToken() { + return jwtToken; + } - public void setJwtToken(String jwtToken) { - this.jwtToken = jwtToken; - } + public void setJwtToken(String jwtToken) { + this.jwtToken = jwtToken; + } } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/UploadFileResponse.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/UploadFileResponse.java index 9a8417eb..2821194e 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/UploadFileResponse.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/UploadFileResponse.java @@ -35,5 +35,5 @@ public void setSize(long size) { this.size = size; } - + } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/UserDTO.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/UserDTO.java index 73e42c47..f147224c 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/UserDTO.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/UserDTO.java @@ -1,23 +1,23 @@ package com.vscode4teaching.vscode4teachingserver.controllers.dtos; +import org.hibernate.validator.constraints.Length; + import javax.validation.constraints.Email; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.Pattern; -import org.hibernate.validator.constraints.Length; - public class UserDTO { @Email(message = "Please provide a valid email") @NotEmpty(message = "Please provide an email") private String email; - @Length(min = 4, max = 50, message = "Your username must have between 4 and 50 characters") - @Pattern(regexp = "^(?:(?!template).)+$", message = "Username is not valid") + @Length(min = 4, max = 50, message = "Username must have between 4 and 50 characters") + @Pattern(regexp = "^(?:(?!(template)|(solution)|(student)).)+$", message = "Username is not valid (cannot contain the words \"template\", \"solution\" or \"student\")") private String username; @NotEmpty(message = "Please provide a password") - @Length(min = 8, message = "Your password must have at least 8 characters") + @Length(min = 8, message = "Password must have at least 8 characters") private String password; @NotEmpty(message = "Please provide your name") diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/exceptioncontrol/ValidationErrorResponse.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/exceptioncontrol/ValidationErrorResponse.java index 364c5512..e0657b8a 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/exceptioncontrol/ValidationErrorResponse.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/exceptioncontrol/ValidationErrorResponse.java @@ -5,6 +5,21 @@ public class ValidationErrorResponse { private List<ErrorDetail> errors; + public ValidationErrorResponse(List<ErrorDetail> errors) { + this.errors = errors; + } + + public ValidationErrorResponse() { + } + + public List<ErrorDetail> getErrors() { + return errors; + } + + public void setErrors(List<ErrorDetail> errors) { + this.errors = errors; + } + public static class ErrorDetail { private String fieldName; private String message; @@ -31,19 +46,4 @@ public void setMessage(String message) { } } - public ValidationErrorResponse(List<ErrorDetail> errors) { - this.errors = errors; - } - - public ValidationErrorResponse() { - } - - public List<ErrorDetail> getErrors() { - return errors; - } - - public void setErrors(List<ErrorDetail> errors) { - this.errors = errors; - } - } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/Comment.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/Comment.java index f09dc207..4be9f0fc 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/Comment.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/Comment.java @@ -1,21 +1,15 @@ package com.vscode4teaching.vscode4teachingserver.model; -import java.time.LocalDateTime; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.ManyToOne; -import javax.validation.constraints.NotEmpty; - import com.fasterxml.jackson.annotation.JsonView; import com.vscode4teaching.vscode4teachingserver.model.views.CommentViews; - import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; import org.hibernate.validator.constraints.Length; +import javax.persistence.*; +import javax.validation.constraints.NotEmpty; +import java.time.LocalDateTime; + @Entity public class Comment { @Id @@ -44,7 +38,7 @@ public class Comment { @UpdateTimestamp @JsonView(CommentViews.GeneralView.class) private LocalDateTime updateDateTime; - + public Comment(CommentThread thread, String body, String author) { this.thread = thread; this.body = body; @@ -52,7 +46,7 @@ public Comment(CommentThread thread, String body, String author) { } public Comment() { - + } public Long getId() { @@ -103,5 +97,5 @@ public void setUpdateDateTime(LocalDateTime updateDateTime) { this.updateDateTime = updateDateTime; } - + } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/CommentThread.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/CommentThread.java index 8094c31d..338a7a27 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/CommentThread.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/CommentThread.java @@ -1,24 +1,16 @@ package com.vscode4teaching.vscode4teachingserver.model; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -import javax.persistence.CascadeType; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.ManyToOne; -import javax.persistence.OneToMany; -import javax.validation.constraints.Min; - import com.fasterxml.jackson.annotation.JsonView; import com.vscode4teaching.vscode4teachingserver.model.views.CommentThreadViews; - import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; +import javax.persistence.*; +import javax.validation.constraints.Min; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + @Entity public class CommentThread { @Id diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/Course.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/Course.java index 4830f87d..b77353ce 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/Course.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/Course.java @@ -14,19 +14,16 @@ @Entity public class Course { + @JsonView(CourseViews.CodeView.class) + private final String uuid = UUID.randomUUID().toString(); @Id @GeneratedValue(strategy = GenerationType.AUTO) @JsonView(CourseViews.GeneralView.class) private Long id; - @JsonView(CourseViews.GeneralView.class) @NotEmpty(message = "Name cannot be null") @Length(min = 10, max = 100, message = "Course name should be between 10 and 100 characters") private String name; - - @JsonView(CourseViews.CodeView.class) - private final String uuid = UUID.randomUUID().toString(); - @OneToMany(mappedBy = "course", cascade = CascadeType.REMOVE, orphanRemoval = true) @JsonView(CourseViews.ExercisesView.class) private List<Exercise> exercises = new ArrayList<>(); @@ -129,7 +126,7 @@ public String getUuid() { public Set<User> getTeachers() { Set<User> teachers = new HashSet<>(); teachers.add(this.creator); - teachers.addAll(this.usersInCourse.stream().filter(u -> u.isTeacher()).collect(Collectors.toSet())); + teachers.addAll(this.usersInCourse.stream().filter(User::isTeacher).collect(Collectors.toSet())); return teachers; } } diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/Exercise.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/Exercise.java index 0e99ace9..e75b702b 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/Exercise.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/Exercise.java @@ -1,62 +1,58 @@ package com.vscode4teaching.vscode4teachingserver.model; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; -import java.util.stream.Collectors; - -import javax.persistence.CascadeType; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.ManyToOne; -import javax.persistence.OneToMany; -import javax.persistence.Table; -import javax.validation.Valid; -import javax.validation.constraints.NotEmpty; - import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonView; import com.vscode4teaching.vscode4teachingserver.model.views.ExerciseViews; - import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; import org.hibernate.validator.constraints.Length; +import javax.persistence.*; +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + @Entity -@Table(name="exercise") +@Table(name = "exercise") public class Exercise { + @JsonView(ExerciseViews.CodeView.class) + private final String uuid = UUID.randomUUID().toString(); @Id @GeneratedValue(strategy = GenerationType.AUTO) @JsonView(ExerciseViews.GeneralView.class) private Long id; - @JsonView(ExerciseViews.GeneralView.class) @NotEmpty(message = "Name cannot be empty") @Length(min = 3, max = 100, message = "Exercise name should contain between 3 and 100 characters") private String name; - @ManyToOne @JsonView(ExerciseViews.CourseView.class) private Course course; - @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @JsonIgnore private List<ExerciseFile> template = new ArrayList<>(); - + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + @JsonIgnore + private List<ExerciseFile> solution = new ArrayList<>(); @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @JsonIgnore private List<ExerciseFile> userFiles = new ArrayList<>(); - - @OneToMany(mappedBy="exercise", cascade = CascadeType.ALL, orphanRemoval = true) + @OneToMany(mappedBy = "exercise", cascade = CascadeType.ALL, orphanRemoval = true) @JsonIgnore private List<ExerciseUserInfo> userInfo = new ArrayList<>(); + @JsonView(ExerciseViews.GeneralView.class) + private boolean includesTeacherSolution; - @JsonView(ExerciseViews.CodeView.class) - private String uuid = UUID.randomUUID().toString(); + @JsonView(ExerciseViews.GeneralView.class) + private boolean solutionIsPublic; + + @JsonView(ExerciseViews.GeneralView.class) + private boolean allowEditionAfterSolutionDownloaded; @CreationTimestamp @JsonView(ExerciseViews.GeneralView.class) @@ -66,24 +62,13 @@ public class Exercise { @JsonView(ExerciseViews.GeneralView.class) private LocalDateTime updateDateTime; - public Exercise( - @NotEmpty(message = "Name cannot be empty") @Length(min = 3, max = 100, message = "Exercise name contain be between 3 and 100 characters") String name) { - this.name = name; - } - - public Exercise( - @NotEmpty(message = "Name cannot be empty") @Length(min = 3, max = 100, message = "Exercise name should contain between 3 and 100 characters") String name, - @Valid Course course, @Valid List<ExerciseFile> template) { - this.name = name; - this.course = course; - this.template = template; - } - public Exercise( @NotEmpty(message = "Name cannot be empty") @Length(min = 3, max = 100, message = "Exercise name should contain between 3 and 100 characters") String name, - @Valid Course course, @Valid ExerciseFile... template) { + @Valid ExerciseFile... template) { this.name = name; - this.course = course; + this.solutionIsPublic = false; + this.includesTeacherSolution = false; + this.allowEditionAfterSolutionDownloaded = false; this.template = Arrays.asList(template); } @@ -138,6 +123,42 @@ public void addFileToTemplate(@Valid ExerciseFile templateFile) { this.template.add(templateFile); } + public List<ExerciseFile> getSolution() { + return solution; + } + + public void setSolution(@Valid List<ExerciseFile> solution) { + this.solution = solution; + } + + public void addFileToSolution(@Valid ExerciseFile solutionFile) { + this.solution.add(solutionFile); + } + + public boolean includesTeacherSolution() { + return includesTeacherSolution; + } + + public void setIncludesTeacherSolution(boolean includesTeacherSolution) { + this.includesTeacherSolution = includesTeacherSolution; + } + + public boolean solutionIsPublic() { + return solutionIsPublic; + } + + public void setSolutionIsPublic(boolean solutionIsPublic) { + this.solutionIsPublic = solutionIsPublic; + } + + public boolean isEditionAfterSolutionDownloadedAllowed() { + return allowEditionAfterSolutionDownloaded; + } + + public void setAllowEditionAfterSolutionDownloaded(boolean allowEditionAfterSolutionDownloaded) { + this.allowEditionAfterSolutionDownloaded = allowEditionAfterSolutionDownloaded; + } + public LocalDateTime getCreateDateTime() { return createDateTime; } @@ -173,5 +194,5 @@ public List<ExerciseUserInfo> getUserInfo() { public void setUserInfo(List<ExerciseUserInfo> userInfo) { this.userInfo = userInfo; } - + } diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/ExerciseFile.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/ExerciseFile.java index 09238257..12836309 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/ExerciseFile.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/ExerciseFile.java @@ -1,27 +1,17 @@ package com.vscode4teaching.vscode4teachingserver.model; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -import javax.persistence.CascadeType; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.ManyToOne; -import javax.persistence.OneToMany; -import javax.persistence.Table; - import com.fasterxml.jackson.annotation.JsonView; import com.vscode4teaching.vscode4teachingserver.model.views.FileViews; - import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; +import javax.persistence.*; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + @Entity -@Table(name="file") +@Table(name = "file") public class ExerciseFile { @Id diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/ExerciseStatus.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/ExerciseStatus.java new file mode 100644 index 00000000..7cb9469e --- /dev/null +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/ExerciseStatus.java @@ -0,0 +1,24 @@ +package com.vscode4teaching.vscode4teachingserver.model; + +public enum ExerciseStatus { + NOT_STARTED(0, "Not started"), + IN_PROGRESS(2, "In progress"), + FINISHED(1, "Finished"); + + private final int code; + private final String asString; + + ExerciseStatus(int code, String asString) { + this.code = code; + this.asString = asString; + } + + public int getCode() { + return code; + } + + @Override + public String toString() { + return asString; + } +} diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/ExerciseUserInfo.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/ExerciseUserInfo.java index dde193ee..acab63d7 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/ExerciseUserInfo.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/ExerciseUserInfo.java @@ -1,27 +1,19 @@ package com.vscode4teaching.vscode4teachingserver.model; +import com.fasterxml.jackson.annotation.JsonView; +import com.vscode4teaching.vscode4teachingserver.model.views.ExerciseUserInfoViews; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import javax.persistence.*; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.Collection; import java.util.HashSet; import java.util.Set; -import javax.persistence.ElementCollection; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.ManyToOne; -import javax.persistence.Table; - -import com.fasterxml.jackson.annotation.JsonView; -import com.vscode4teaching.vscode4teachingserver.model.views.ExerciseUserInfoViews; - -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - @Entity -@Table(name="user_info") +@Table(name = "user_info") public class ExerciseUserInfo { @Id @@ -38,7 +30,7 @@ public class ExerciseUserInfo { private User user; @JsonView(ExerciseUserInfoViews.GeneralView.class) - private int status = 0; + private ExerciseStatus status = ExerciseStatus.NOT_STARTED; @CreationTimestamp @JsonView(ExerciseUserInfoViews.GeneralView.class) @@ -88,11 +80,11 @@ public void setUser(User user) { this.updateDateTime = LocalDateTime.now(ZoneOffset.UTC); } - public int getStatus() { + public ExerciseStatus getStatus() { return status; } - public void setStatus(int status) { + public void setStatus(ExerciseStatus status) { this.status = status; this.updateDateTime = LocalDateTime.now(ZoneOffset.UTC); } diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/Role.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/Role.java index 4e9341fb..a3ed1bd7 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/Role.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/Role.java @@ -1,15 +1,15 @@ package com.vscode4teaching.vscode4teachingserver.model; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonView; +import com.vscode4teaching.vscode4teachingserver.model.views.RoleViews; + import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.validation.constraints.NotEmpty; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonView; -import com.vscode4teaching.vscode4teachingserver.model.views.RoleViews; - @Entity public class Role { @Id @@ -44,5 +44,5 @@ public void setRoleName(String roleName) { this.roleName = roleName; } - + } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/User.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/User.java index 45d396b9..c01c0d48 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/User.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/User.java @@ -1,29 +1,21 @@ package com.vscode4teaching.vscode4teachingserver.model; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.ManyToMany; -import javax.validation.constraints.Email; -import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.Pattern; - import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonView; import com.vscode4teaching.vscode4teachingserver.model.views.UserViews; - import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; import org.hibernate.validator.constraints.Length; +import javax.persistence.*; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Pattern; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + @Entity public class User { @Id diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/CommentRepository.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/CommentRepository.java index e03c1de3..e8472c13 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/CommentRepository.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/CommentRepository.java @@ -1,7 +1,6 @@ package com.vscode4teaching.vscode4teachingserver.model.repositories; import com.vscode4teaching.vscode4teachingserver.model.Comment; - import org.springframework.data.jpa.repository.JpaRepository; public interface CommentRepository extends JpaRepository<Comment, Long> { diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/CommentThreadRepository.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/CommentThreadRepository.java index ecab4e59..18aad977 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/CommentThreadRepository.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/CommentThreadRepository.java @@ -1,11 +1,10 @@ package com.vscode4teaching.vscode4teachingserver.model.repositories; -import java.util.Optional; - import com.vscode4teaching.vscode4teachingserver.model.CommentThread; - import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface CommentThreadRepository extends JpaRepository<CommentThread, Long> { - public Optional<CommentThread> findByFile_Id(Long fileId); + Optional<CommentThread> findByFile_Id(Long fileId); } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/CourseRepository.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/CourseRepository.java index 4eac3b90..de05b8cf 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/CourseRepository.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/CourseRepository.java @@ -1,11 +1,10 @@ package com.vscode4teaching.vscode4teachingserver.model.repositories; -import java.util.Optional; - import com.vscode4teaching.vscode4teachingserver.model.Course; - import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface CourseRepository extends JpaRepository<Course, Long> { Optional<Course> findById(Long courseId); diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/ExerciseFileRepository.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/ExerciseFileRepository.java index 61465c00..56d10648 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/ExerciseFileRepository.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/ExerciseFileRepository.java @@ -1,13 +1,12 @@ package com.vscode4teaching.vscode4teachingserver.model.repositories; -import java.util.Optional; - import com.vscode4teaching.vscode4teachingserver.model.ExerciseFile; - import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface ExerciseFileRepository extends JpaRepository<ExerciseFile, Long> { - Optional<ExerciseFile> findByPath(String path); + Optional<ExerciseFile> findByPath(String path); } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/ExerciseRepository.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/ExerciseRepository.java index 0280f648..521aa125 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/ExerciseRepository.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/ExerciseRepository.java @@ -1,12 +1,11 @@ package com.vscode4teaching.vscode4teachingserver.model.repositories; -import java.util.Optional; - import com.vscode4teaching.vscode4teachingserver.model.Course; import com.vscode4teaching.vscode4teachingserver.model.Exercise; - import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface ExerciseRepository extends JpaRepository<Exercise, Long> { Optional<Exercise> findByCourseAndNameIgnoreCase(Course course, String name); } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/ExerciseUserInfoRepository.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/ExerciseUserInfoRepository.java index f68f4a10..2aa0443d 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/ExerciseUserInfoRepository.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/ExerciseUserInfoRepository.java @@ -1,16 +1,15 @@ package com.vscode4teaching.vscode4teachingserver.model.repositories; -import java.util.List; -import java.util.Optional; - import com.vscode4teaching.vscode4teachingserver.model.ExerciseUserInfo; - import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; +import java.util.Optional; + public interface ExerciseUserInfoRepository extends JpaRepository<ExerciseUserInfo, Long> { - public Optional<ExerciseUserInfo> findByExercise_IdAndUser_Username(Long exerciseId, String username); + Optional<ExerciseUserInfo> findByExercise_IdAndUser_Username(Long exerciseId, String username); - public List<ExerciseUserInfo> deleteByExercise_IdInAndUser_IdIn(List<Long> exerciseIds, List<Long> userIds); + void deleteByExercise_IdInAndUser_IdIn(List<Long> exerciseIds, List<Long> userIds); - public List<ExerciseUserInfo> findByExercise_Id(Long exerciseId); + List<ExerciseUserInfo> findByExercise_Id(Long exerciseId); } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/RoleRepository.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/RoleRepository.java index b26948ba..85433d38 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/RoleRepository.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/RoleRepository.java @@ -1,7 +1,6 @@ package com.vscode4teaching.vscode4teachingserver.model.repositories; import com.vscode4teaching.vscode4teachingserver.model.Role; - import org.springframework.data.jpa.repository.JpaRepository; public interface RoleRepository extends JpaRepository<Role, Long> { diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/UserRepository.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/UserRepository.java index 170d3764..9932657a 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/UserRepository.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/UserRepository.java @@ -1,12 +1,12 @@ package com.vscode4teaching.vscode4teachingserver.model.repositories; -import java.util.Optional; - import com.vscode4teaching.vscode4teachingserver.model.User; - import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByUsername(String username); + Optional<User> findByEmail(String email); } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/CommentThreadViews.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/CommentThreadViews.java index 5c5e2ccd..9f709556 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/CommentThreadViews.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/CommentThreadViews.java @@ -4,12 +4,12 @@ public class CommentThreadViews { private CommentThreadViews() { } - public static interface GeneralView { + public interface GeneralView { } - public static interface FileView extends FileViews.GeneralView, GeneralView { + public interface FileView extends FileViews.GeneralView, GeneralView { } - public static interface CommentView extends CommentViews.GeneralView, GeneralView { + public interface CommentView extends CommentViews.GeneralView, GeneralView { } } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/CommentViews.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/CommentViews.java index f36794b1..012bbe44 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/CommentViews.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/CommentViews.java @@ -4,10 +4,10 @@ public class CommentViews { private CommentViews() { } - public static interface GeneralView { + public interface GeneralView { } - public static interface ThreadView extends GeneralView { - + public interface ThreadView extends GeneralView { + } } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/CourseViews.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/CourseViews.java index 6c251ef0..5606c381 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/CourseViews.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/CourseViews.java @@ -4,18 +4,18 @@ public class CourseViews { private CourseViews() { } - public static interface GeneralView extends UserViews.GeneralView { + public interface GeneralView extends UserViews.GeneralView { } - public static interface ExercisesView extends GeneralView, ExerciseViews.GeneralView { + public interface ExercisesView extends GeneralView, ExerciseViews.GeneralView { } - public static interface UsersView extends GeneralView { + public interface UsersView extends GeneralView { } - public static interface CreatorView extends GeneralView { + public interface CreatorView extends GeneralView { } - public static interface CodeView extends GeneralView { + public interface CodeView extends GeneralView { } } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/ExerciseUserInfoViews.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/ExerciseUserInfoViews.java index 7c039a4f..5160cbf9 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/ExerciseUserInfoViews.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/ExerciseUserInfoViews.java @@ -4,6 +4,6 @@ public class ExerciseUserInfoViews { private ExerciseUserInfoViews() { } - public static interface GeneralView extends ExerciseViews.CourseView { + public interface GeneralView extends ExerciseViews.CourseView { } } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/ExerciseViews.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/ExerciseViews.java index 4fb667c8..84290964 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/ExerciseViews.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/ExerciseViews.java @@ -4,15 +4,15 @@ public class ExerciseViews { private ExerciseViews() { } - public static interface GeneralView { + public interface GeneralView { } - public static interface CourseView extends GeneralView, CourseViews.CreatorView { + public interface CourseView extends GeneralView, CourseViews.CreatorView { } - public static interface FileView extends GeneralView, FileViews.GeneralView { + public interface FileView extends GeneralView, FileViews.GeneralView { } - public static interface CodeView extends GeneralView { + public interface CodeView extends GeneralView { } } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/FileViews.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/FileViews.java index 69bb928e..669ea699 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/FileViews.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/FileViews.java @@ -4,13 +4,13 @@ public class FileViews { private FileViews() { } - public static interface GeneralView { + public interface GeneralView { } - public static interface OwnerView extends GeneralView { + public interface OwnerView extends GeneralView { } - - public static interface CommentView extends GeneralView, CommentThreadViews.CommentView { + + public interface CommentView extends GeneralView, CommentThreadViews.CommentView { } } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/RoleViews.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/RoleViews.java index e794f56d..b322829a 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/RoleViews.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/RoleViews.java @@ -4,6 +4,6 @@ public class RoleViews { private RoleViews() { } - public static interface GeneralView { + public interface GeneralView { } } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/UserViews.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/UserViews.java index 49b08880..5675ffd0 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/UserViews.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/views/UserViews.java @@ -4,14 +4,14 @@ public class UserViews { private UserViews() { } - public static interface GeneralView extends RoleViews.GeneralView { + public interface GeneralView extends RoleViews.GeneralView { } - public static interface CourseView extends CourseViews.GeneralView { - + public interface CourseView extends CourseViews.GeneralView { + } - public static interface EmailView extends GeneralView { - + public interface EmailView extends GeneralView { + } } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/security/SecurityConfig.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/security/SecurityConfig.java index 346873bc..432708c3 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/security/SecurityConfig.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/security/SecurityConfig.java @@ -2,7 +2,6 @@ import com.vscode4teaching.vscode4teachingserver.security.jwt.JWTAuthenticationEntryPoint; import com.vscode4teaching.vscode4teachingserver.security.jwt.JWTRequestFilter; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -44,20 +43,23 @@ protected void configure(HttpSecurity http) throws Exception { final String teacherRole = "TEACHER"; final String studentRole = "STUDENT"; http.authorizeRequests() + // Specific endpoints for every user (logged in or not) .antMatchers(HttpMethod.GET, "/api/courses", "/api/csrf", "/api/courses/code/*", "/api/v2/courses/code/*", "/api/courses/*/creator") .permitAll() .antMatchers(HttpMethod.POST, "/api/login", "/api/register", "/api/teachers/register", "/api/teachers/invitation") .permitAll() - .antMatchers(HttpMethod.POST, "/api/exercises/*/teachers/**", "/api/courses", "/api/courses/*/exercises", "/api/v2/courses/*/exercises", "/api/courses/*/users") + + // Specific endpoints for teachers + .antMatchers(HttpMethod.GET, "/api/exercises/*/info/teacher") + .hasAnyRole(teacherRole) + .antMatchers(HttpMethod.POST, "/api/courses", "/api/courses/*/exercises", "/api/courses/*/users", "/api/exercises/*/teachers/**", "/api/exercises/*/files/template", "/api/exercises/*/files/solution", "/api/v2/courses/*/exercises") .hasAnyRole(teacherRole) .antMatchers(HttpMethod.PUT, "/api/courses/*", "/api/courses/*/exercises/*", "/api/exercises/*") .hasAnyRole(teacherRole) .antMatchers(HttpMethod.DELETE, "/api/courses/*", "/api/courses/*/exercises/*", "/api/exercises/*") .hasAnyRole(teacherRole) - .antMatchers(HttpMethod.POST, "/api/exercises/*/files/template") - .hasAnyRole(teacherRole) - .antMatchers(HttpMethod.GET, "/api/exercises/*/info/teacher") - .hasAnyRole(teacherRole) + + // Every other endpoint in /api requires student role .antMatchers("/api/**") .hasAnyRole(studentRole) .anyRequest().permitAll().and() diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/security/jwt/JWTAuthenticationEntryPoint.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/security/jwt/JWTAuthenticationEntryPoint.java index d7e419d1..f0a0bfac 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/security/jwt/JWTAuthenticationEntryPoint.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/security/jwt/JWTAuthenticationEntryPoint.java @@ -1,23 +1,21 @@ package com.vscode4teaching.vscode4teachingserver.security.jwt; -import java.io.IOException; -import java.io.Serializable; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.Serializable; + @Component public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable { private static final long serialVersionUID = -6484984648949471L; @Override public void commence(HttpServletRequest request, HttpServletResponse response, - AuthenticationException authException) throws IOException, ServletException { + AuthenticationException authException) throws IOException { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage()); } diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/security/jwt/JWTRequestFilter.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/security/jwt/JWTRequestFilter.java index d7aece4b..6136a714 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/security/jwt/JWTRequestFilter.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/security/jwt/JWTRequestFilter.java @@ -1,13 +1,7 @@ package com.vscode4teaching.vscode4teachingserver.security.jwt; -import java.io.IOException; -import java.util.Arrays; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import com.vscode4teaching.vscode4teachingserver.servicesimpl.JWTUserDetailsService; +import io.jsonwebtoken.ExpiredJwtException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; @@ -15,7 +9,12 @@ import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; -import io.jsonwebtoken.ExpiredJwtException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; @Component public class JWTRequestFilter extends OncePerRequestFilter { diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/security/jwt/JWTTokenUtil.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/security/jwt/JWTTokenUtil.java index f8898181..6b415dd9 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/security/jwt/JWTTokenUtil.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/security/jwt/JWTTokenUtil.java @@ -1,21 +1,19 @@ package com.vscode4teaching.vscode4teachingserver.security.jwt; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; import java.io.Serializable; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.function.Function; -import javax.servlet.http.HttpServletRequest; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Component; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; - @Component public class JWTTokenUtil implements Serializable { private static final long serialVersionUID = 4564116581L; diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/CommentService.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/CommentService.java index cd22cc06..28d0078b 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/CommentService.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/CommentService.java @@ -1,24 +1,25 @@ package com.vscode4teaching.vscode4teachingserver.services; -import java.util.List; - -import javax.validation.Valid; -import javax.validation.constraints.Min; - import com.vscode4teaching.vscode4teachingserver.model.CommentThread; import com.vscode4teaching.vscode4teachingserver.model.ExerciseFile; import com.vscode4teaching.vscode4teachingserver.services.exceptions.CommentNotFoundException; import com.vscode4teaching.vscode4teachingserver.services.exceptions.ExerciseNotFoundException; import com.vscode4teaching.vscode4teachingserver.services.exceptions.FileNotFoundException; - import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; +import javax.validation.Valid; +import javax.validation.constraints.Min; +import java.util.List; + @Service @Validated public interface CommentService { - public CommentThread saveCommentThread(@Min(1) Long fileId, @Valid CommentThread commentThread) throws FileNotFoundException; - public List<CommentThread> getCommentThreadsByFile(@Min(1) Long fileId) throws FileNotFoundException; - public List<ExerciseFile> getFilesWithCommentsByUser(Long exerciseId, String username) throws ExerciseNotFoundException; - public CommentThread updateCommentThreadLine(@Min(1) Long commentThreadId, @Min(0) Long line, String lineText) throws CommentNotFoundException; + CommentThread saveCommentThread(@Min(1) Long fileId, @Valid CommentThread commentThread) throws FileNotFoundException; + + List<CommentThread> getCommentThreadsByFile(@Min(1) Long fileId) throws FileNotFoundException; + + List<ExerciseFile> getFilesWithCommentsByUser(Long exerciseId, String username) throws ExerciseNotFoundException; + + CommentThread updateCommentThreadLine(@Min(1) Long commentThreadId, @Min(0) Long line, String lineText) throws CommentNotFoundException; } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/CourseService.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/CourseService.java index f866ea0e..d3b6337a 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/CourseService.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/CourseService.java @@ -1,75 +1,70 @@ package com.vscode4teaching.vscode4teachingserver.services; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import javax.validation.Valid; -import javax.validation.constraints.Min; - import com.vscode4teaching.vscode4teachingserver.model.Course; import com.vscode4teaching.vscode4teachingserver.model.Exercise; import com.vscode4teaching.vscode4teachingserver.model.User; -import com.vscode4teaching.vscode4teachingserver.services.exceptions.CantRemoveCreatorException; -import com.vscode4teaching.vscode4teachingserver.services.exceptions.CourseNotFoundException; -import com.vscode4teaching.vscode4teachingserver.services.exceptions.ExerciseNotFoundException; -import com.vscode4teaching.vscode4teachingserver.services.exceptions.NotCreatorException; -import com.vscode4teaching.vscode4teachingserver.services.exceptions.NotInCourseException; -import com.vscode4teaching.vscode4teachingserver.services.exceptions.TeacherNotFoundException; -import com.vscode4teaching.vscode4teachingserver.services.exceptions.UserNotFoundException; - +import com.vscode4teaching.vscode4teachingserver.services.exceptions.*; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; +import javax.validation.Valid; +import javax.validation.constraints.Min; +import java.util.List; +import java.util.Optional; +import java.util.Set; + @Service @Validated public interface CourseService { - public List<Course> getAllCourses(); + List<Course> getAllCourses(); - public Optional<Course> getCourseById(Long courseId); + Optional<Course> getCourseById(Long courseId); - public Course registerNewCourse(@Valid Course course, String requestUsername) throws TeacherNotFoundException; + Course registerNewCourse(@Valid Course course, String requestUsername) throws TeacherNotFoundException; - public User getCreator(@Min(1) Long courseId) throws CourseNotFoundException; + User getCreator(@Min(1) Long courseId) throws CourseNotFoundException; - public Exercise addExerciseToCourse(@Min(1) Long courseId, @Valid Exercise exercise, String requestUsername) + Exercise addExerciseToCourse(@Min(1) Long courseId, @Valid Exercise exercise, String requestUsername) throws CourseNotFoundException, NotInCourseException; - public Course editCourse(@Min(1) Long courseId, @Valid Course courseData, String requestUsername) + Course editCourse(@Min(1) Long courseId, @Valid Course courseData, String requestUsername) throws CourseNotFoundException, NotInCourseException; - public void deleteCourse(@Min(1) Long courseId, String requestUsername) + void deleteCourse(@Min(1) Long courseId, String requestUsername) throws CourseNotFoundException, NotInCourseException, NotCreatorException; - public List<Exercise> getExercises(@Min(1) Long courseId, String requestUsername) + List<Exercise> getExercises(@Min(1) Long courseId, String requestUsername) throws CourseNotFoundException, NotInCourseException; - public Course joinCourseWithSharingCode(String uuid, String requestUsername) - throws CourseNotFoundException, NotInCourseException, UserNotFoundException; + Course joinCourseWithSharingCode(String uuid, String requestUsername) + throws CourseNotFoundException, UserNotFoundException; - public Course getCourseInformationWithSharingCode(String uuid) + Course getCourseInformationWithSharingCode(String uuid) throws CourseNotFoundException; - public Exercise editExercise(@Min(1) Long exerciseId, @Valid Exercise exerciseData, String requestUsername) + Exercise getExercise(@Min(1) Long exerciseId) + throws ExerciseNotFoundException; + + Exercise editExercise(@Min(1) Long exerciseId, @Valid Exercise exerciseData, String requestUsername) throws NotInCourseException, ExerciseNotFoundException; - public void deleteExercise(@Min(1) Long exerciseId, String requestUsername) + void deleteExercise(@Min(1) Long exerciseId, String requestUsername) throws NotInCourseException, ExerciseNotFoundException; - public List<Course> getUserCourses(@Min(1) Long userId) throws UserNotFoundException; + List<Course> getUserCourses(@Min(1) Long userId) throws UserNotFoundException; - public Set<User> getUsersInCourse(@Min(1) Long courseId, String requestUsername) + Set<User> getUsersInCourse(@Min(1) Long courseId, String requestUsername) throws CourseNotFoundException, NotInCourseException; - public Course addUsersToCourse(@Min(1) Long courseId, Long[] userIds, String requestUsername) + Course addUsersToCourse(@Min(1) Long courseId, Long[] userIds, String requestUsername) throws UserNotFoundException, CourseNotFoundException, NotInCourseException; - public Course removeUsersFromCourse(@Min(1) Long courseId, Long[] userIds, String requestUsername) + Course removeUsersFromCourse(@Min(1) Long courseId, Long[] userIds, String requestUsername) throws UserNotFoundException, CourseNotFoundException, NotInCourseException, CantRemoveCreatorException; - public String getCourseCode(Long courseId, String requestUsername) - throws UserNotFoundException, CourseNotFoundException, NotInCourseException; + String getCourseCode(Long courseId, String requestUsername) + throws CourseNotFoundException, NotInCourseException; - public String getExerciseCode(Long exerciseId, String requestUsername) - throws UserNotFoundException, ExerciseNotFoundException, NotInCourseException; + String getExerciseCode(Long exerciseId, String requestUsername) + throws ExerciseNotFoundException, NotInCourseException; } diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/ExerciseFilesService.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/ExerciseFilesService.java index e6689e4f..0a10ace3 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/ExerciseFilesService.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/ExerciseFilesService.java @@ -1,43 +1,45 @@ package com.vscode4teaching.vscode4teachingserver.services; -import java.io.File; -import java.io.IOException; -import java.util.List; -import java.util.Map; - -import javax.validation.constraints.Min; - import com.vscode4teaching.vscode4teachingserver.model.Exercise; import com.vscode4teaching.vscode4teachingserver.model.ExerciseFile; -import com.vscode4teaching.vscode4teachingserver.services.exceptions.ExerciseFinishedException; -import com.vscode4teaching.vscode4teachingserver.services.exceptions.ExerciseNotFoundException; -import com.vscode4teaching.vscode4teachingserver.services.exceptions.NoTemplateException; -import com.vscode4teaching.vscode4teachingserver.services.exceptions.NotFoundException; -import com.vscode4teaching.vscode4teachingserver.services.exceptions.NotInCourseException; - +import com.vscode4teaching.vscode4teachingserver.services.exceptions.*; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import org.springframework.web.multipart.MultipartFile; +import javax.validation.constraints.Min; +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; + @Service @Validated public interface ExerciseFilesService { - public Map<Exercise, List<File>> getExerciseFiles(@Min(1) Long exerciseId, String requestUsername) + Boolean existsExerciseFilesForUser(@Min(1) Long exerciseId, String requestUsername) + throws ExerciseNotFoundException, NotInCourseException; + + Map<Exercise, List<File>> getExerciseFiles(@Min(1) Long exerciseId, String requestUsername) throws ExerciseNotFoundException, NotInCourseException, NoTemplateException; - public Map<Exercise, List<File>> saveExerciseFiles(@Min(1) Long exerciseId, MultipartFile zip, - String requestUsername) + Map<Exercise, List<File>> saveExerciseFiles(@Min(1) Long exerciseId, MultipartFile zip, String requestUsername) throws NotInCourseException, IOException, ExerciseFinishedException, NotFoundException; - public Map<Exercise, List<File>> saveExerciseTemplate(@Min(1) Long exerciseId, MultipartFile zip, - String requestUsername) throws ExerciseNotFoundException, NotInCourseException, IOException; + Map<Exercise, List<File>> saveExerciseTemplate(@Min(1) Long exerciseId, MultipartFile zip, String requestUsername) + throws ExerciseNotFoundException, NotInCourseException, IOException; - public Map<Exercise, List<File>> getExerciseTemplate(@Min(1) Long exerciseId, String requestUsername) + Map<Exercise, List<File>> saveExerciseSolution(@Min(1) Long exerciseId, MultipartFile zip, String requestUsername) + throws ExerciseNotFoundException, NotInCourseException, IOException; + + Map<Exercise, List<File>> getExerciseTemplate(@Min(1) Long exerciseId, String requestUsername) throws ExerciseNotFoundException, NotInCourseException, NoTemplateException; - public Map<Exercise, List<File>> getAllStudentsFiles(@Min(1) Long exerciseId, String requestUsername) + Map<Exercise, List<File>> getExerciseSolution(@Min(1) Long exerciseId, String requestUsername) + throws ExerciseNotFoundException, NotInCourseException, NoSolutionException; + + Map<Exercise, List<File>> getAllStudentsFiles(@Min(1) Long exerciseId, String requestUsername) throws ExerciseNotFoundException, NotInCourseException; - public List<ExerciseFile> getFileIdsByExerciseAndOwner(@Min(1) Long exerciseId, String ownerUsername) - throws ExerciseNotFoundException; -} \ No newline at end of file + List<ExerciseFile> getFileIdsByExerciseAndId(@Min(1) Long exerciseId, String ownerUsername) + throws NotFoundException; +} diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/ExerciseInfoService.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/ExerciseInfoService.java index 0d86f35f..82677afe 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/ExerciseInfoService.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/ExerciseInfoService.java @@ -1,27 +1,29 @@ package com.vscode4teaching.vscode4teachingserver.services; -import java.util.List; - -import javax.validation.constraints.Min; -import javax.validation.constraints.NotEmpty; - +import com.vscode4teaching.vscode4teachingserver.model.ExerciseStatus; import com.vscode4teaching.vscode4teachingserver.model.ExerciseUserInfo; import com.vscode4teaching.vscode4teachingserver.services.exceptions.ExerciseNotFoundException; import com.vscode4teaching.vscode4teachingserver.services.exceptions.NotFoundException; import com.vscode4teaching.vscode4teachingserver.services.exceptions.NotInCourseException; - import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotEmpty; +import java.util.List; + @Service @Validated public interface ExerciseInfoService { - public ExerciseUserInfo getExerciseUserInfo(@Min(0) Long exerciseId, @NotEmpty String username) + ExerciseUserInfo getExerciseUserInfo(@Min(0) Long exerciseId, @NotEmpty String username) throws NotFoundException; - public ExerciseUserInfo updateExerciseUserInfo(@Min(0) Long exerciseId, @NotEmpty String username, int status, List<String> modifiedFiles) + ExerciseUserInfo getExerciseUserInfo(@Min(1) Long euiId) throws NotFoundException; + + ExerciseUserInfo updateExerciseUserInfo(@Min(0) Long exerciseId, @NotEmpty String username, ExerciseStatus status, List<String> modifiedFiles) throws NotFoundException; - public List<ExerciseUserInfo> getAllStudentExerciseUserInfo(@Min(0) Long exerciseId, String requestUsername) + List<ExerciseUserInfo> getAllStudentExerciseUserInfo(@Min(0) Long exerciseId, String requestUsername) throws ExerciseNotFoundException, NotInCourseException; + } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/exceptions/CommentNotFoundException.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/exceptions/CommentNotFoundException.java index c3f4bbc3..01265b0d 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/exceptions/CommentNotFoundException.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/exceptions/CommentNotFoundException.java @@ -2,8 +2,8 @@ public class CommentNotFoundException extends NotFoundException { - private static final long serialVersionUID = 2343242342111898L; - + private static final long serialVersionUID = 2343242342111898L; + public CommentNotFoundException(String msg) { super(msg); } diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/exceptions/MissingPropertyException.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/exceptions/MissingPropertyException.java index d4bca70e..e05ce512 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/exceptions/MissingPropertyException.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/exceptions/MissingPropertyException.java @@ -8,5 +8,5 @@ public class MissingPropertyException extends Exception { public MissingPropertyException(String... missingProperties) { super("The following keys are missing: " + Arrays.toString(missingProperties)); } - + } diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/exceptions/NoSolutionException.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/exceptions/NoSolutionException.java new file mode 100644 index 00000000..0e192395 --- /dev/null +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/exceptions/NoSolutionException.java @@ -0,0 +1,9 @@ +package com.vscode4teaching.vscode4teachingserver.services.exceptions; + +public class NoSolutionException extends Exception { + private static final long serialVersionUID = 7424646476876367677L; + + public NoSolutionException(Long exerciseId) { + super("No solution found for exercise: " + exerciseId); + } +} \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/exceptions/NotInCourseException.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/exceptions/NotInCourseException.java index b62c3506..4e613c2a 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/exceptions/NotInCourseException.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/exceptions/NotInCourseException.java @@ -2,9 +2,9 @@ public class NotInCourseException extends Exception { - private static final long serialVersionUID = 456748914891L; + private static final long serialVersionUID = 456748914891L; public NotInCourseException(String string) { super(string); - } + } } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/websockets/SocketHandler.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/websockets/SocketHandler.java index 4d58702d..4cd9a1de 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/websockets/SocketHandler.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/websockets/SocketHandler.java @@ -5,7 +5,6 @@ import com.vscode4teaching.vscode4teachingserver.services.exceptions.EmptyJSONObjectException; import com.vscode4teaching.vscode4teachingserver.services.exceptions.EmptyURIException; import com.vscode4teaching.vscode4teachingserver.services.exceptions.MissingPropertyException; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @@ -25,12 +24,12 @@ @Component public class SocketHandler extends TextWebSocketHandler { - private final List<WebSocketSession> sessions = new CopyOnWriteArrayList<>(); private static final Logger logger = LoggerFactory.getLogger(SocketHandler.class); + private final List<WebSocketSession> sessions = new CopyOnWriteArrayList<>(); @Override public void handleTextMessage(WebSocketSession session, TextMessage message) - throws InterruptedException, IOException, EmptyURIException, EmptyJSONObjectException, MissingPropertyException { + throws EmptyURIException, EmptyJSONObjectException, MissingPropertyException { URI uri = session.getUri(); if (uri == null) { @@ -75,7 +74,7 @@ public void afterConnectionEstablished(WebSocketSession session) throws Exceptio } @Override - public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { sessions.remove(session); if (session.getPrincipal() == null) { logger.info("Closed Websocket connection with unidentified user"); @@ -88,14 +87,14 @@ public void refreshExerciseDashboards(Set<User> teachers) { logger.info("Exercise user info updated, sending updates to teachers " + teachers.toString() + "..."); for (User teacher : teachers) { sessions.stream() - .filter(t -> t.isOpen() && Objects.requireNonNull(t.getPrincipal()).getName().equals(teacher.getUsername())) - .forEach(t -> { - try { - t.sendMessage(new TextMessage("{\"handle\":\"refresh\"}")); - } catch (IOException e) { - logger.error("Error sending websocket message: " + e.getMessage()); - } - }); + .filter(t -> t.isOpen() && Objects.requireNonNull(t.getPrincipal()).getName().equals(teacher.getUsername())) + .forEach(t -> { + try { + t.sendMessage(new TextMessage("{\"handle\":\"refresh\"}")); + } catch (IOException e) { + logger.error("Error sending websocket message: " + e.getMessage()); + } + }); } logger.info("Updates sent to teachers"); } diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/websockets/WebSocketConfig.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/websockets/WebSocketConfig.java index 86bf3bb3..276c675f 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/websockets/WebSocketConfig.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/websockets/WebSocketConfig.java @@ -11,9 +11,9 @@ public class WebSocketConfig implements WebSocketConfigurer { private final SocketHandler socketHandler; - public WebSocketConfig(SocketHandler socketHandler) { - this.socketHandler = socketHandler; - } + public WebSocketConfig(SocketHandler socketHandler) { + this.socketHandler = socketHandler; + } public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(socketHandler, "/liveshare", "dashboard-refresh"); diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/CommentServiceImpl.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/CommentServiceImpl.java index 8e5b2aad..b793f0a1 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/CommentServiceImpl.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/CommentServiceImpl.java @@ -1,6 +1,5 @@ package com.vscode4teaching.vscode4teachingserver.servicesimpl; -import com.vscode4teaching.vscode4teachingserver.model.Comment; import com.vscode4teaching.vscode4teachingserver.model.CommentThread; import com.vscode4teaching.vscode4teachingserver.model.Exercise; import com.vscode4teaching.vscode4teachingserver.model.ExerciseFile; @@ -48,9 +47,7 @@ public CommentThread saveCommentThread(Long fileId, CommentThread commentThread) file.addCommentThread(commentThread); commentThread.setFile(file); commentThreadRepository.save(commentThread); - for (Comment comment : commentThread.getComments()) { - commentRepository.save(comment); - } + commentRepository.saveAll(commentThread.getComments()); ExerciseFile savedFile = exerciseFileRepository.save(file); return savedFile.getComments().get(savedFile.getComments().size() - 1); } diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/CourseServiceImpl.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/CourseServiceImpl.java index 5854a470..9f4a3250 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/CourseServiceImpl.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/CourseServiceImpl.java @@ -118,7 +118,7 @@ public List<Exercise> getExercises(Long courseId, String requestUsername) @Override public Course joinCourseWithSharingCode(String uuid, String requestUsername) - throws CourseNotFoundException, NotInCourseException, UserNotFoundException { + throws CourseNotFoundException, UserNotFoundException { Course course = this.courseRepo.findByUuid(uuid).orElseThrow(() -> new CourseNotFoundException(uuid)); User user = userRepo.findByUsername(requestUsername) .orElseThrow(() -> new UserNotFoundException(requestUsername)); @@ -137,6 +137,11 @@ public Course getCourseInformationWithSharingCode(String uuid) return this.courseRepo.findByUuid(uuid).orElseThrow(() -> new CourseNotFoundException(uuid)); } + @Override + public Exercise getExercise(Long exerciseId) throws ExerciseNotFoundException { + return this.exerciseRepo.findById(exerciseId).orElseThrow(() -> new ExerciseNotFoundException(exerciseId)); + } + @Override public Exercise editExercise(Long exerciseId, Exercise exerciseData, String requestUsername) throws ExerciseNotFoundException, NotInCourseException { @@ -144,6 +149,9 @@ public Exercise editExercise(Long exerciseId, Exercise exerciseData, String requ .orElseThrow(() -> new ExerciseNotFoundException(exerciseId)); ExceptionUtil.throwExceptionIfNotInCourse(exercise.getCourse(), requestUsername, true); exercise.setName(exerciseData.getName()); + exercise.setIncludesTeacherSolution(exerciseData.includesTeacherSolution()); + exercise.setSolutionIsPublic(exerciseData.solutionIsPublic()); + exercise.setAllowEditionAfterSolutionDownloaded(exerciseData.isEditionAfterSolutionDownloadedAllowed()); return exerciseRepo.save(exercise); } @@ -203,7 +211,7 @@ public Course removeUsersFromCourse(@Min(1) Long courseId, Long[] userIds, Strin } course.removeUserFromCourse(user); } - List<Long> exerciseIds = course.getExercises().stream().map(e -> e.getId()).collect(Collectors.toList()); + List<Long> exerciseIds = course.getExercises().stream().map(Exercise::getId).collect(Collectors.toList()); this.exerciseUserInfoRepo.deleteByExercise_IdInAndUser_IdIn(exerciseIds, Arrays.asList(userIds)); this.websocketHandler.refreshExerciseDashboards(course.getTeachers()); return this.courseRepo.save(course); @@ -211,7 +219,7 @@ public Course removeUsersFromCourse(@Min(1) Long courseId, Long[] userIds, Strin @Override public String getCourseCode(Long courseId, String requestUsername) - throws UserNotFoundException, CourseNotFoundException, NotInCourseException { + throws CourseNotFoundException, NotInCourseException { Course course = this.courseRepo.findById(courseId).orElseThrow(() -> new CourseNotFoundException(courseId)); ExceptionUtil.throwExceptionIfNotInCourse(course, requestUsername, true); return course.getUuid(); @@ -219,7 +227,7 @@ public String getCourseCode(Long courseId, String requestUsername) @Override public String getExerciseCode(Long exerciseId, String requestUsername) - throws UserNotFoundException, ExerciseNotFoundException, NotInCourseException { + throws ExerciseNotFoundException, NotInCourseException { Exercise exercise = this.exerciseRepo.findById(exerciseId) .orElseThrow(() -> new ExerciseNotFoundException(exerciseId)); ExceptionUtil.throwExceptionIfNotInCourse(exercise.getCourse(), requestUsername, true); diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/ExerciseFilesServiceImpl.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/ExerciseFilesServiceImpl.java index 5c836b45..1bd99bd1 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/ExerciseFilesServiceImpl.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/ExerciseFilesServiceImpl.java @@ -6,6 +6,7 @@ import com.vscode4teaching.vscode4teachingserver.model.repositories.ExerciseUserInfoRepository; import com.vscode4teaching.vscode4teachingserver.model.repositories.UserRepository; import com.vscode4teaching.vscode4teachingserver.services.ExerciseFilesService; +import com.vscode4teaching.vscode4teachingserver.services.ExerciseInfoService; import com.vscode4teaching.vscode4teachingserver.services.exceptions.*; import org.hibernate.exception.ConstraintViolationException; import org.slf4j.Logger; @@ -36,17 +37,33 @@ public class ExerciseFilesServiceImpl implements ExerciseFilesService { private final ExerciseFileRepository fileRepository; private final ExerciseUserInfoRepository exerciseUserInfoRepository; private final UserRepository userRepository; + + private final ExerciseInfoService exerciseInfoService; + private final JWTUserDetailsService userService; private final Logger logger = LoggerFactory.getLogger(ExerciseFilesServiceImpl.class); @Value("${v4t.filedirectory}") private String rootPath; public ExerciseFilesServiceImpl(ExerciseRepository exerciseRepository, ExerciseFileRepository fileRepository, - ExerciseUserInfoRepository exerciseUserInfoRepository, UserRepository userRepository) { + ExerciseUserInfoRepository exerciseUserInfoRepository, UserRepository userRepository, + ExerciseInfoService exerciseInfoService, JWTUserDetailsService userService) { this.exerciseRepository = exerciseRepository; this.fileRepository = fileRepository; this.exerciseUserInfoRepository = exerciseUserInfoRepository; this.userRepository = userRepository; + this.exerciseInfoService = exerciseInfoService; + this.userService = userService; + } + + @Override + public Boolean existsExerciseFilesForUser(@Min(1) Long exerciseId, String requestUsername) + throws ExerciseNotFoundException, NotInCourseException { + logger.info("Called ExerciseFilesServiceImpl.existsExerciseFilesForUser({}, {})", exerciseId, requestUsername); + Exercise exercise = exerciseRepository.findById(exerciseId) + .orElseThrow(() -> new ExerciseNotFoundException(exerciseId)); + ExceptionUtil.throwExceptionIfNotInCourse(exercise.getCourse(), requestUsername, false); + return !exercise.getFilesByOwner(requestUsername).isEmpty(); } @Override @@ -74,8 +91,7 @@ public Map<Exercise, List<File>> getExerciseFiles(@Min(1) Long exerciseId, Strin } @Override - public Map<Exercise, List<File>> saveExerciseFiles(@Min(1) Long exerciseId, MultipartFile file, - String requestUsername) + public Map<Exercise, List<File>> saveExerciseFiles(@Min(1) Long exerciseId, MultipartFile file, String requestUsername) throws NotFoundException, NotInCourseException, IOException, ExerciseFinishedException { logger.info("Called ExerciseFilesServiceImpl.saveExerciseFiles({}, (file), {})", exerciseId, requestUsername); ExerciseUserInfo eui = exerciseUserInfoRepository @@ -83,44 +99,69 @@ public Map<Exercise, List<File>> saveExerciseFiles(@Min(1) Long exerciseId, Mult .orElseThrow(() -> new NotFoundException( "Exercise user info not found for user: " + requestUsername + ". Exercise: " + exerciseId)); - if (eui.getStatus() == 1) { + if (eui.getStatus() == ExerciseStatus.FINISHED) { throw new ExerciseFinishedException(exerciseId); } - return saveFiles(exerciseId, file, requestUsername, eui); + return saveFiles(file, exerciseId, requestUsername, eui, false, false); } @Override - public Map<Exercise, List<File>> saveExerciseTemplate(@Min(1) Long exerciseId, MultipartFile file, - String requestUsername) throws ExerciseNotFoundException, NotInCourseException, IOException { - return saveFiles(exerciseId, file, requestUsername, null); + public Map<Exercise, List<File>> saveExerciseTemplate(@Min(1) Long exerciseId, MultipartFile file, String requestUsername) + throws ExerciseNotFoundException, NotInCourseException, IOException { + return saveFiles(file, exerciseId, requestUsername, null, true, false); } - private Map<Exercise, List<File>> saveFiles(Long exerciseId, MultipartFile file, String requestUsername, - ExerciseUserInfo eui) throws ExerciseNotFoundException, NotInCourseException, IOException { - logger.info("Called ExerciseFilesServiceImpl.saveFiles({}, (file), {}, {})", exerciseId, requestUsername, eui); - Exercise exercise = exerciseRepository.findById(exerciseId) - .orElseThrow(() -> new ExerciseNotFoundException(exerciseId)); + @Override + public Map<Exercise, List<File>> saveExerciseSolution(@Min(1) Long exerciseId, MultipartFile file, String requestUsername) + throws ExerciseNotFoundException, NotInCourseException, IOException { + return saveFiles(file, exerciseId, requestUsername, null, false, true); + } + + private Map<Exercise, List<File>> saveFiles(MultipartFile zippedFile, Long exerciseId, String requestUsername, + ExerciseUserInfo eui, boolean isTemplate, boolean isSolution) + throws ExerciseNotFoundException, NotInCourseException, IOException { + logger.info("Called ExerciseFilesServiceImpl.saveFiles({}, (file), {}, {}, {}, {})", exerciseId, requestUsername, eui, isTemplate, isSolution); + + // Stage 1: all the information necessary to execute the process is obtained from function parameters. + Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(() -> new ExerciseNotFoundException(exerciseId)); Course course = exercise.getCourse(); - User user = userRepository.findByUsername(requestUsername) - .orElseThrow(() -> new NotInCourseException("User not in course: " + requestUsername)); + User user = userRepository.findByUsername(requestUsername).orElseThrow(() -> new NotInCourseException("User not in course: " + requestUsername)); ExceptionUtil.throwExceptionIfNotInCourse(course, requestUsername, (eui == null)); - // eui is null if file is a template, otherwise it'll be a normal eui - String lastFolderPath = (eui == null) ? "template" : "student_" + eui.getId(); - // For example, for root path "v4t_courses", a course "Course 1" with id 34, an exercise "Exercise 1" with id 77 - // and a user "john.doe" the final directory path would be - // v4t_courses/course_1_34/exercise_1_77/john.doe - Path targetDirectory = Paths.get(rootPath + File.separator + course.getName().toLowerCase().replace(" ", "_") - + "_" + course.getId() + File.separator + exercise.getName().toLowerCase().replace(" ", "_") + "_" - + exercise.getId() + File.separator + lastFolderPath).toAbsolutePath().normalize(); - if (!Files.exists(targetDirectory)) { - Files.createDirectories(targetDirectory); + + // Stage 2: the path where the provided resources will be saved is dynamically generated. + // To do this, it is necessary to detect what type of upload is being performed: + // - Upload of an answer to an exercise (created by a student) -> eui parameter is not null and booleans are false. + // - Upload of an exercise template -> eui is null, isTemplate is true and isSolution is false. + // - Upload of an exercise solution (proposed by teacher) -> eui is null, isTemplate is false and isSolution is true. + // According to this information, the necessary directories are generated. + // The first part of the generated path is always the same: root path (specified in application properties) + course + exercise. + StringBuilder destinationPath = new StringBuilder() + .append(rootPath) + .append(File.separator) + .append(course.getName().toLowerCase().replace(" ", "_")).append("_").append(course.getId()) // Course (name + _ + i) + .append(File.separator) + .append(exercise.getName().toLowerCase().replace(" ", "_")).append("_").append(exercise.getId()) // Exercise (name + _ + i) + .append(File.separator); + if (eui != null) { + destinationPath.append("student_").append(eui.getId()); + } else if (isTemplate) { + destinationPath.append("template"); + } else if (isSolution) { + destinationPath.append("solution"); } + Path destinationAbsolutePath = Paths.get(destinationPath.toString()).toAbsolutePath().normalize(); + if (!Files.exists(destinationAbsolutePath)) { + Files.createDirectories(destinationAbsolutePath); + } + + // Stage 3: the ZIP file sent is decompressed and inserted into the directory generated in previous stage. + // The new information available on the added exercise is also persisted during this process. byte[] buffer = new byte[1024]; - ZipInputStream zis = new ZipInputStream(file.getInputStream()); + ZipInputStream zis = new ZipInputStream(zippedFile.getInputStream()); ZipEntry zipEntry = zis.getNextEntry(); List<File> files = new ArrayList<>(); while (zipEntry != null) { - File destFile = newFile(targetDirectory.toFile(), zipEntry); + File destFile = newFile(destinationAbsolutePath.toFile(), zipEntry); if (zipEntry.isDirectory()) { if (!destFile.isDirectory() && !destFile.mkdirs()) { throw new IOException("Failed to create directory " + destFile); @@ -141,13 +182,16 @@ private Map<Exercise, List<File>> saveFiles(Long exerciseId, MultipartFile file, ExerciseFile exFile = new ExerciseFile(destFile.getCanonicalPath()); try { if (fileRepository.findByPath(destFile.getCanonicalPath()).isEmpty()) { - if (eui == null) { - ExerciseFile savedFile = fileRepository.save(exFile); - exercise.addFileToTemplate(savedFile); - } else { + if (eui != null) { exFile.setOwner(user); ExerciseFile savedFile = fileRepository.save(exFile); exercise.addUserFile(savedFile); + } else if (isTemplate) { + ExerciseFile savedFile = fileRepository.save(exFile); + exercise.addFileToTemplate(savedFile); + } else if (isSolution) { + ExerciseFile savedFile = fileRepository.save(exFile); + exercise.addFileToSolution(savedFile); } } } catch (ConstraintViolationException ex) { @@ -194,6 +238,24 @@ public Map<Exercise, List<File>> getExerciseTemplate(@Min(1) Long exerciseId, St } } + @Override + public Map<Exercise, List<File>> getExerciseSolution(@Min(1) Long exerciseId, String requestUsername) + throws ExerciseNotFoundException, NotInCourseException, NoSolutionException { + logger.info("Called ExerciseFilesServiceImpl.getExerciseSolution({}, {})", exerciseId, requestUsername); + Exercise exercise = exerciseRepository.findById(exerciseId) + .orElseThrow(() -> new ExerciseNotFoundException(exerciseId)); + ExceptionUtil.throwExceptionIfNotInCourse(exercise.getCourse(), requestUsername, false); + List<ExerciseFile> solution = exercise.getSolution(); + if (exercise.getSolution().isEmpty()) { + throw new NoSolutionException(exerciseId); + } else { + Map<Exercise, List<File>> filesMap = new HashMap<>(); + filesMap.put(exercise, + solution.stream().map(file -> Paths.get(file.getPath()).toFile()).collect(Collectors.toList())); + return filesMap; + } + } + @Override public Map<Exercise, List<File>> getAllStudentsFiles(@Min(1) Long exerciseId, String requestUsername) throws ExerciseNotFoundException, NotInCourseException { @@ -209,14 +271,25 @@ public Map<Exercise, List<File>> getAllStudentsFiles(@Min(1) Long exerciseId, St } @Override - public List<ExerciseFile> getFileIdsByExerciseAndOwner(@Min(1) Long exerciseId, String ownerUsername) - throws ExerciseNotFoundException { - logger.info("Called ExerciseFilesServiceImpl.getFileIdsByExerciseAndOwner({}, {})", exerciseId, ownerUsername); + public List<ExerciseFile> getFileIdsByExerciseAndId(@Min(1) Long exerciseId, String id) + throws NotFoundException { + logger.info("Called ExerciseFilesServiceImpl.getFileIdsByExerciseAndId({}, {})", exerciseId, id); Exercise ex = exerciseRepository.findById(exerciseId) .orElseThrow(() -> new ExerciseNotFoundException(exerciseId)); - List<ExerciseFile> files = ex.getFilesByOwner(ownerUsername); + + // This identificator can be: + // - A student username (if called from getSingleStudentExerciseFiles() from extension) + // - A EUI identificator (like "student_XX", if called from getMultipleStudentExerciseFiles() from extension) + User user; + if (id.startsWith("student_")) { + Long parsedEuiId = Long.parseLong(id.split("student_")[1]); + user = exerciseInfoService.getExerciseUserInfo(parsedEuiId).getUser(); + } else { + user = userService.findByUsername(id); + } + + List<ExerciseFile> files = ex.getFilesByOwner(user.getUsername()); if (!files.isEmpty()) { - String username = files.get(0).getOwner().getUsername(); List<ExerciseFile> copyFiles = new ArrayList<>(files); // Change paths to be relative to student's folder (named "student_{number}") diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/ExerciseInfoServiceImpl.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/ExerciseInfoServiceImpl.java index 840454fc..6993dd2b 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/ExerciseInfoServiceImpl.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/ExerciseInfoServiceImpl.java @@ -1,6 +1,7 @@ package com.vscode4teaching.vscode4teachingserver.servicesimpl; import com.vscode4teaching.vscode4teachingserver.model.Course; +import com.vscode4teaching.vscode4teachingserver.model.ExerciseStatus; import com.vscode4teaching.vscode4teachingserver.model.ExerciseUserInfo; import com.vscode4teaching.vscode4teachingserver.model.repositories.ExerciseUserInfoRepository; import com.vscode4teaching.vscode4teachingserver.services.ExerciseInfoService; @@ -14,8 +15,8 @@ import javax.validation.constraints.Min; import javax.validation.constraints.NotEmpty; -import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; @Service @@ -34,16 +35,18 @@ public ExerciseInfoServiceImpl(ExerciseUserInfoRepository exerciseUserInfoReposi public ExerciseUserInfo getExerciseUserInfo(@Min(0) Long exerciseId, @NotEmpty String username) throws NotFoundException { logger.info("Called ExerciseInfoServiceImpl.getExerciseUserInfo({}, {})", exerciseId, username); - ExerciseUserInfo eui = this.getAndCheckExerciseUserInfo(exerciseId, username); - //Changing status from not accessed to accessed but not finished - if (eui.getStatus() == 0) { - eui = updateExerciseUserInfo(exerciseId, username, 2, new ArrayList<>()); - } - return eui; + return this.getAndCheckExerciseUserInfo(exerciseId, username); + } + + @Override + public ExerciseUserInfo getExerciseUserInfo(@Min(1) Long euiId) throws NotFoundException { + logger.info("Called ExerciseInfoServiceImpl.getExerciseUserInfo({})", euiId); + Optional<ExerciseUserInfo> eui = exerciseUserInfoRepository.findById(euiId); + return eui.orElseThrow(() -> new NotFoundException(euiId.toString())); } @Override - public ExerciseUserInfo updateExerciseUserInfo(@Min(0) Long exerciseId, @NotEmpty String username, int status, List<String> modifiedFiles) + public ExerciseUserInfo updateExerciseUserInfo(@Min(0) Long exerciseId, @NotEmpty String username, ExerciseStatus status, List<String> modifiedFiles) throws NotFoundException { logger.info("Called ExerciseInfoServiceImpl.updateExerciseUserInfo({}, {}, {}, {})", exerciseId, username, status, modifiedFiles); ExerciseUserInfo eui = this.getAndCheckExerciseUserInfo(exerciseId, username); @@ -74,4 +77,4 @@ public List<ExerciseUserInfo> getAllStudentExerciseUserInfo(@Min(0) Long exercis euis = euis.stream().filter(eui -> !eui.getUser().isTeacher()).collect(Collectors.toList()); return euis; } -} +} \ No newline at end of file diff --git a/vscode4teaching-server/src/main/resources/application.properties b/vscode4teaching-server/src/main/resources/application.properties index 8b66c81c..89d3fd3b 100644 --- a/vscode4teaching-server/src/main/resources/application.properties +++ b/vscode4teaching-server/src/main/resources/application.properties @@ -1,11 +1,15 @@ # DB Configuration -spring.datasource.url=jdbc:mysql://localhost:3306/vsc4teach?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC +spring.datasource.url=jdbc:mysql://localhost:3306/vsc4teach?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&autoReconnect=true spring.datasource.username=root spring.datasource.password= spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect spring.jpa.properties.hibernate.dialect.storage_engine=innodb +# Temporary adaptations to upgrade to Spring Boot 2.7.X (from 2.5.14) +spring.mvc.pathmatch.matching-strategy=ant_path_matcher +spring.main.allow-circular-references=true + # Multipart Files configuration spring.servlet.multipart.max-file-size=1GB spring.servlet.multipart.max-request-size=2GB @@ -24,7 +28,7 @@ spring.jpa.properties.hibernate.jdbc.time_zone=UTC # IMPORTANT: change in production jwt.secret=vscode4teaching -# Initialization Configuration: inicialization of demo data and files +# Initialization Configuration: initialization of demo data and files data.initialization=true file.initialization=true diff --git a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/CommentControllerTests.java b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/CommentControllerTests.java index 3a042f80..a496de07 100644 --- a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/CommentControllerTests.java +++ b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/CommentControllerTests.java @@ -1,24 +1,12 @@ package com.vscode4teaching.vscode4teachingserver.controllertests; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.vscode4teaching.vscode4teachingserver.controllers.dtos.JWTRequest; import com.vscode4teaching.vscode4teachingserver.controllers.dtos.JWTResponse; -import com.vscode4teaching.vscode4teachingserver.model.Comment; -import com.vscode4teaching.vscode4teachingserver.model.CommentThread; -import com.vscode4teaching.vscode4teachingserver.model.Exercise; -import com.vscode4teaching.vscode4teachingserver.model.ExerciseFile; -import com.vscode4teaching.vscode4teachingserver.model.User; +import com.vscode4teaching.vscode4teachingserver.model.*; import com.vscode4teaching.vscode4teachingserver.model.views.CommentThreadViews; import com.vscode4teaching.vscode4teachingserver.model.views.FileViews; import com.vscode4teaching.vscode4teachingserver.services.CommentService; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -30,16 +18,14 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import java.util.ArrayList; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @TestPropertySource(locations = "classpath:test.properties") @@ -70,24 +56,24 @@ public void login() throws Exception { } @Test - public void saveCommentThread() throws JsonProcessingException, Exception { + public void saveCommentThread() throws Exception { ExerciseFile demoFile = new ExerciseFile("testPath"); - demoFile.setId(1l); - CommentThread commentThread = new CommentThread(demoFile, 0l, "Test line"); - CommentThread expectedCommentThread = new CommentThread(demoFile, 0l, "Test line"); - expectedCommentThread.setId(2l); + demoFile.setId(1L); + CommentThread commentThread = new CommentThread(demoFile, 0L, "Test line"); + CommentThread expectedCommentThread = new CommentThread(demoFile, 0L, "Test line"); + expectedCommentThread.setId(2L); Comment c1 = new Comment(commentThread, "Test 1", "johndoe"); Comment c2 = new Comment(commentThread, "Test 2", "johndoe"); commentThread.addComment(c1); commentThread.addComment(c2); Comment expectedC1 = new Comment(expectedCommentThread, "Test 1", "johndoe"); - expectedC1.setId(3l); + expectedC1.setId(3L); Comment expectedC2 = new Comment(expectedCommentThread, "Test 2", "johndoe"); - expectedC2.setId(4l); + expectedC2.setId(4L); expectedCommentThread.addComment(expectedC1); expectedCommentThread.addComment(expectedC2); ExerciseFile expectedFile = new ExerciseFile("testPath"); - expectedFile.setId(1l); + expectedFile.setId(1L); expectedFile.addCommentThread(expectedCommentThread); expectedCommentThread.setFile(expectedFile); when(commentService.saveCommentThread(anyLong(), any(CommentThread.class))).thenReturn(expectedCommentThread); @@ -103,7 +89,7 @@ public void saveCommentThread() throws JsonProcessingException, Exception { ArgumentCaptor<CommentThread> commentCaptor = ArgumentCaptor.forClass(CommentThread.class); verify(commentService, times(1)).saveCommentThread(anyLong(), commentCaptor.capture()); CommentThread capturedCommentThread = commentCaptor.getValue(); - assertThat(capturedCommentThread.getLine()).isEqualTo(0l); + assertThat(capturedCommentThread.getLine()).isEqualTo(0L); List<Comment> capturedComments = capturedCommentThread.getComments(); assertThat(capturedComments.get(0).getBody()).isEqualTo(expectedC1.getBody()); assertThat(capturedComments.get(0).getAuthor()).isEqualTo(expectedC1.getAuthor()); @@ -119,22 +105,22 @@ public void saveCommentThread() throws JsonProcessingException, Exception { @Test public void getCommentThreads() throws Exception { ExerciseFile demoFile = new ExerciseFile("testPath"); - demoFile.setId(1l); - CommentThread commentThread = new CommentThread(demoFile, 0l, "Test line"); - CommentThread expectedCommentThread = new CommentThread(demoFile, 0l, "Test line"); - expectedCommentThread.setId(2l); + demoFile.setId(1L); + CommentThread commentThread = new CommentThread(demoFile, 0L, "Test line"); + CommentThread expectedCommentThread = new CommentThread(demoFile, 0L, "Test line"); + expectedCommentThread.setId(2L); Comment c1 = new Comment(commentThread, "Test 1", "johndoe"); Comment c2 = new Comment(commentThread, "Test 2", "johndoe"); commentThread.addComment(c1); commentThread.addComment(c2); Comment expectedC1 = new Comment(expectedCommentThread, "Test 1", "johndoe"); - expectedC1.setId(3l); + expectedC1.setId(3L); Comment expectedC2 = new Comment(expectedCommentThread, "Test 2", "johndoe"); - expectedC2.setId(4l); + expectedC2.setId(4L); expectedCommentThread.addComment(expectedC1); expectedCommentThread.addComment(expectedC2); ExerciseFile expectedFile = new ExerciseFile("testPath"); - expectedFile.setId(1l); + expectedFile.setId(1L); expectedFile.addCommentThread(expectedCommentThread); expectedCommentThread.setFile(expectedFile); List<CommentThread> expectedCommentThreadList = new ArrayList<>(); @@ -156,33 +142,33 @@ public void getCommentThreads() throws Exception { @Test public void getCommentsByUser() throws Exception { User user = new User("johndoe@johndoe.com", "johndoe", "johndoe", "johndoe", "johndoe"); - user.setId(10000l); + user.setId(10000L); ExerciseFile demoFile = new ExerciseFile("testPath"); - demoFile.setId(1l); - CommentThread commentThread = new CommentThread(demoFile, 0l, "Test line"); - CommentThread expectedCommentThread = new CommentThread(demoFile, 0l, "Test line"); - expectedCommentThread.setId(2l); + demoFile.setId(1L); + CommentThread commentThread = new CommentThread(demoFile, 0L, "Test line"); + CommentThread expectedCommentThread = new CommentThread(demoFile, 0L, "Test line"); + expectedCommentThread.setId(2L); Comment c1 = new Comment(commentThread, "Test 1", "johndoe"); Comment c2 = new Comment(commentThread, "Test 2", "johndoe"); commentThread.addComment(c1); commentThread.addComment(c2); Comment expectedC1 = new Comment(expectedCommentThread, "Test 1", "johndoe"); - expectedC1.setId(3l); + expectedC1.setId(3L); Comment expectedC2 = new Comment(expectedCommentThread, "Test 2", "johndoe"); - expectedC2.setId(4l); + expectedC2.setId(4L); expectedCommentThread.addComment(expectedC1); expectedCommentThread.addComment(expectedC2); ExerciseFile expectedFile = new ExerciseFile("testPath"); - expectedFile.setId(1l); + expectedFile.setId(1L); expectedFile.addCommentThread(expectedCommentThread); expectedCommentThread.setFile(expectedFile); List<CommentThread> expectedCommentThreadList = new ArrayList<>(); expectedCommentThreadList.add(expectedCommentThread); Exercise ex = new Exercise("Test ex"); - ex.setId(1000l); + ex.setId(1000L); ex.addUserFile(expectedFile); ExerciseFile expectedFile2 = new ExerciseFile("testPath2"); - expectedFile2.setId(555l); + expectedFile2.setId(555L); expectedFile2.setOwner(new User("johndoe2@johndoe.com", "johndoe2", "johndoe2", "johndoe2", "johndoe2")); ex.addUserFile(expectedFile2); List<ExerciseFile> expectedFiles = new ArrayList<>(); @@ -202,25 +188,25 @@ public void getCommentsByUser() throws Exception { } @Test - public void updateCommentThreadLines() throws JsonProcessingException, Exception { + public void updateCommentThreadLines() throws Exception { ExerciseFile demoFile = new ExerciseFile("testPath"); - demoFile.setId(1l); - CommentThread commentThread = new CommentThread(demoFile, 0l, "Test line"); - commentThread.setId(2l); - CommentThread expectedCommentThread = new CommentThread(demoFile, 5l, "Test line 5"); - expectedCommentThread.setId(2l); + demoFile.setId(1L); + CommentThread commentThread = new CommentThread(demoFile, 0L, "Test line"); + commentThread.setId(2L); + CommentThread expectedCommentThread = new CommentThread(demoFile, 5L, "Test line 5"); + expectedCommentThread.setId(2L); Comment c1 = new Comment(commentThread, "Test 1", "johndoe"); Comment c2 = new Comment(commentThread, "Test 2", "johndoe"); commentThread.addComment(c1); commentThread.addComment(c2); Comment expectedC1 = new Comment(expectedCommentThread, "Test 1", "johndoe"); - expectedC1.setId(3l); + expectedC1.setId(3L); Comment expectedC2 = new Comment(expectedCommentThread, "Test 2", "johndoe"); - expectedC2.setId(4l); + expectedC2.setId(4L); expectedCommentThread.addComment(expectedC1); expectedCommentThread.addComment(expectedC2); ExerciseFile expectedFile = new ExerciseFile("testPath"); - expectedFile.setId(1l); + expectedFile.setId(1L); expectedFile.addCommentThread(expectedCommentThread); expectedCommentThread.setFile(expectedFile); when(commentService.updateCommentThreadLine(anyLong(), anyLong(), any(String.class))).thenReturn(expectedCommentThread); @@ -230,7 +216,7 @@ public void updateCommentThreadLines() throws JsonProcessingException, Exception put("/api/comments/2/lines").contentType("application/json") .header("Authorization", "Bearer " + jwtToken.getJwtToken()).with(csrf()) .content(objectMapper.writerWithView(CommentThreadViews.GeneralView.class) - .writeValueAsString(expectedCommentThread))) + .writeValueAsString(expectedCommentThread))) .andExpect(status().isOk()).andReturn(); verify(commentService, times(1)).updateCommentThreadLine(anyLong(), anyLong(), any(String.class)); diff --git a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/CourseControllerTests.java b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/CourseControllerTests.java index 3cc7a70e..baf93d45 100644 --- a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/CourseControllerTests.java +++ b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/CourseControllerTests.java @@ -1,24 +1,5 @@ package com.vscode4teaching.vscode4teachingserver.controllertests; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - import com.fasterxml.jackson.databind.ObjectMapper; import com.vscode4teaching.vscode4teachingserver.controllers.dtos.CourseDTO; import com.vscode4teaching.vscode4teachingserver.controllers.dtos.JWTRequest; @@ -30,7 +11,6 @@ import com.vscode4teaching.vscode4teachingserver.model.views.CourseViews; import com.vscode4teaching.vscode4teachingserver.model.views.UserViews; import com.vscode4teaching.vscode4teachingserver.services.CourseService; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -45,21 +25,26 @@ import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import java.util.*; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + @SpringBootTest @TestPropertySource(locations = "classpath:test.properties") @AutoConfigureMockMvc public class CourseControllerTests { + private static final Logger logger = LoggerFactory.getLogger(CourseControllerTests.class); @Autowired private MockMvc mockMvc; - @Autowired private ObjectMapper objectMapper; - @MockBean private CourseService courseService; - private JWTResponse jwtToken; - private static final Logger logger = LoggerFactory.getLogger(CourseControllerTests.class); @BeforeEach public void login() throws Exception { @@ -73,6 +58,27 @@ public void login() throws Exception { jwtToken = objectMapper.readValue(loginResult.getResponse().getContentAsString(), JWTResponse.class); } + @Test + public void getCourse() throws Exception { + logger.info("Test getCourse() begins."); + + Course course = new Course("Spring Boot Course"); + + when(courseService.getCourseById(1L)).thenReturn(Optional.of(course)); + + MvcResult mvcResult = mockMvc.perform(get("/api/courses/1").contentType("application/json").with(csrf()) + .header("Authorization", "Bearer " + jwtToken.getJwtToken())) + .andDo(MockMvcResultHandlers.print()).andExpect(status().isOk()).andReturn(); + + verify(courseService, times(1)).getCourseById(1L); + String actualResponseBody = mvcResult.getResponse().getContentAsString(); + String expectedResponseBody = objectMapper.writerWithView(CourseViews.CreatorView.class) + .writeValueAsString(course); + assertThat(expectedResponseBody).isEqualToIgnoringWhitespace(actualResponseBody); + + logger.info("Test getCourse() ends."); + } + @Test public void getAllCourses_withContent() throws Exception { logger.info("Test getAllCourses_withContent() begins."); @@ -144,7 +150,7 @@ public void postCourse_valid() throws Exception { CourseDTO course = new CourseDTO(); course.setName("Spring Boot Course"); Course expectedCourse = new Course("Spring Boot Course"); - expectedCourse.setId(1l); + expectedCourse.setId(1L); when(courseService.registerNewCourse(any(Course.class), anyString())).thenReturn(expectedCourse); MvcResult mvcResult = mockMvc @@ -228,8 +234,8 @@ public void getUserCourses_valid() throws Exception { courses.add(c2); User user = new User("johndoe@john.com", "johndoejr", "password", "John", "Doe"); user.setCourses(courses); - user.setId(4l); - when(courseService.getUserCourses(4l)).thenReturn(courses); + user.setId(4L); + when(courseService.getUserCourses(4L)).thenReturn(courses); MvcResult mvcResult = mockMvc .perform(get("/api/users/4/courses").contentType("application/json").with(csrf()) @@ -250,15 +256,15 @@ public void addUserToCourse_valid() throws Exception { logger.info("Test addUserToCourse_valid() begins."); User newUser1 = new User("johndoejr@gmail.com", "johndoejr", "pass", "John", "Doe Jr"); - newUser1.setId(1l); + newUser1.setId(1L); User newUser2 = new User("johndoejr2@gmail.com", "johndoejr2", "pass", "John", "Doe Jr 2"); - newUser2.setId(5l); + newUser2.setId(5L); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); Role teacherRole = new Role("ROLE_TEACHER"); - teacherRole.setId(3l); + teacherRole.setId(3L); User teacher = new User("johndoe@gmail.com", "johndoe", "pass", "John", "Doe "); - teacher.setId(4l); + teacher.setId(4L); teacher.addRole(studentRole); teacher.addRole(teacherRole); newUser1.addRole(studentRole); @@ -269,10 +275,10 @@ public void addUserToCourse_valid() throws Exception { expectedUsers.add(teacher); UserRequest request = new UserRequest(); - Long[] ids = { 1l, 5l }; + Long[] ids = {1L, 5L}; request.setIds(ids); Course expectedCourse = new Course("Spring Boot Course"); - expectedCourse.setId(1l); + expectedCourse.setId(1L); expectedCourse.addUserInCourse(teacher); expectedCourse.addUserInCourse(newUser1); expectedCourse.addUserInCourse(newUser2); @@ -298,15 +304,15 @@ public void getUsersInCourse_valid() throws Exception { logger.info("Test getUsersInCourse_valid() begins."); User newUser1 = new User("johndoejr@gmail.com", "johndoejr", "pass", "John", "Doe Jr"); - newUser1.setId(1l); + newUser1.setId(1L); User newUser2 = new User("johndoejr2@gmail.com", "johndoejr2", "pass", "John", "Doe Jr 2"); - newUser2.setId(5l); + newUser2.setId(5L); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); Role teacherRole = new Role("ROLE_TEACHER"); - teacherRole.setId(3l); + teacherRole.setId(3L); User teacher = new User("johndoe@gmail.com", "johndoe", "pass", "John", "Doe "); - teacher.setId(4l); + teacher.setId(4L); teacher.addRole(studentRole); teacher.addRole(teacherRole); newUser1.addRole(studentRole); @@ -336,15 +342,15 @@ public void removeUsersFromCourse_valid() throws Exception { logger.info("Test removeUsersFromCourse_valid() begins."); User newUser1 = new User("johndoejr@gmail.com", "johndoejr", "pass", "John", "Doe Jr"); - newUser1.setId(1l); + newUser1.setId(1L); User newUser2 = new User("johndoejr2@gmail.com", "johndoejr2", "pass", "John", "Doe Jr 2"); - newUser2.setId(5l); + newUser2.setId(5L); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); Role teacherRole = new Role("ROLE_TEACHER"); - teacherRole.setId(3l); + teacherRole.setId(3L); User teacher = new User("johndoe@gmail.com", "johndoe", "pass", "John", "Doe "); - teacher.setId(4l); + teacher.setId(4L); teacher.addRole(studentRole); teacher.addRole(teacherRole); newUser1.addRole(studentRole); @@ -355,10 +361,10 @@ public void removeUsersFromCourse_valid() throws Exception { expectedUsers.add(teacher); UserRequest request = new UserRequest(); - Long[] ids = { 1l, 5l }; + Long[] ids = {1L, 5L}; request.setIds(ids); Course expectedCourse = new Course("Spring Boot Course"); - expectedCourse.setId(1l); + expectedCourse.setId(1L); expectedCourse.addUserInCourse(teacher); when(courseService.removeUsersFromCourse(anyLong(), any(Long[].class), anyString())).thenReturn(expectedCourse); String requestString = objectMapper.writeValueAsString(request); @@ -380,22 +386,66 @@ public void removeUsersFromCourse_valid() throws Exception { @Test public void getCode_valid() throws Exception { Course c0 = new Course("Spring Boot Course"); - c0.setId(1l); + c0.setId(1L); String code = c0.getUuid(); User user = new User("johndoe@john.com", "johndoejr", "password", "John", "Doe"); user.addCourse(c0); - user.setId(4l); + user.setId(4L); - when(courseService.getCourseCode(1l, "johndoe")).thenReturn(code); + when(courseService.getCourseCode(1L, "johndoe")).thenReturn(code); MvcResult mvcResult = mockMvc .perform(get("/api/courses/1/code").contentType("application/json").with(csrf()).header("Authorization", "Bearer " + jwtToken.getJwtToken())) .andDo(MockMvcResultHandlers.print()).andExpect(status().isOk()).andReturn(); - verify(courseService, times(1)).getCourseCode(1l, "johndoe"); + verify(courseService, times(1)).getCourseCode(1L, "johndoe"); + String actualResponseBody = mvcResult.getResponse().getContentAsString(); + assertThat(code).isEqualToIgnoringWhitespace(actualResponseBody); + } + + @Test + public void getCourseInformationBySharingCode_valid() throws Exception { + Course course = new Course("Spring Boot Course"); + course.setId(1L); + String courseUuid = course.getUuid(); + User user = new User("johndoe@john.com", "johndoejr", "password", "John", "Doe"); + user.addCourse(course); + user.setId(4L); + + when(courseService.getCourseInformationWithSharingCode(courseUuid)).thenReturn(course); + + MvcResult mvcResult = mockMvc + .perform(get("/api/v2/courses/code/" + courseUuid).contentType("application/json").with(csrf()) + .header("Authorization", "Bearer " + jwtToken.getJwtToken())) + .andDo(MockMvcResultHandlers.print()).andExpect(status().isOk()).andReturn(); + + verify(courseService, times(1)).getCourseInformationWithSharingCode(courseUuid); + + String actualResponseBody = mvcResult.getResponse().getContentAsString(); + String expectedResponseBody = objectMapper.writerWithView(CourseViews.CreatorView.class) + .writeValueAsString(course); + assertThat(expectedResponseBody).isEqualToIgnoringWhitespace(actualResponseBody); + } + + @Test + public void joinCourse_valid() throws Exception { + Course course = new Course("Spring Boot Course"); + course.setId(1L); + String courseUuid = course.getUuid(); + + when(courseService.joinCourseWithSharingCode(courseUuid, "johndoe")).thenReturn(course); + + MvcResult mvcResult = mockMvc + .perform(put("/api/courses/code/" + courseUuid).contentType("application/json").with(csrf()) + .header("Authorization", "Bearer " + jwtToken.getJwtToken())) + .andDo(MockMvcResultHandlers.print()).andExpect(status().isOk()).andReturn(); + + verify(courseService, times(1)).joinCourseWithSharingCode(courseUuid, "johndoe"); + String actualResponseBody = mvcResult.getResponse().getContentAsString(); - String expectedResponseBody = code; + String expectedResponseBody = objectMapper.writerWithView(CourseViews.ExercisesView.class) + .writeValueAsString(course); assertThat(expectedResponseBody).isEqualToIgnoringWhitespace(actualResponseBody); } } \ No newline at end of file diff --git a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/ExerciseControllerTests.java b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/ExerciseControllerTests.java index ea41289a..e32d87d6 100644 --- a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/ExerciseControllerTests.java +++ b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/ExerciseControllerTests.java @@ -1,44 +1,16 @@ package com.vscode4teaching.vscode4teachingserver.controllertests; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.AdditionalAnswers.returnsElementsOf; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.vscode4teaching.vscode4teachingserver.controllers.dtos.ExerciseDTO; import com.vscode4teaching.vscode4teachingserver.controllers.dtos.ExerciseUserInfoDTO; import com.vscode4teaching.vscode4teachingserver.controllers.dtos.JWTRequest; import com.vscode4teaching.vscode4teachingserver.controllers.dtos.JWTResponse; -import com.vscode4teaching.vscode4teachingserver.model.Course; -import com.vscode4teaching.vscode4teachingserver.model.Exercise; -import com.vscode4teaching.vscode4teachingserver.model.ExerciseUserInfo; -import com.vscode4teaching.vscode4teachingserver.model.Role; -import com.vscode4teaching.vscode4teachingserver.model.User; +import com.vscode4teaching.vscode4teachingserver.model.*; import com.vscode4teaching.vscode4teachingserver.model.views.ExerciseUserInfoViews; import com.vscode4teaching.vscode4teachingserver.model.views.ExerciseViews; import com.vscode4teaching.vscode4teachingserver.services.CourseService; import com.vscode4teaching.vscode4teachingserver.services.ExerciseInfoService; - import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.slf4j.Logger; @@ -52,24 +24,31 @@ import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.AdditionalAnswers.returnsElementsOf; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + @SpringBootTest @TestPropertySource(locations = "classpath:test.properties") @AutoConfigureMockMvc public class ExerciseControllerTests { + private final Logger logger = LoggerFactory.getLogger(ExerciseControllerTests.class); @Autowired private MockMvc mockMvc; - @Autowired private ObjectMapper objectMapper; - @MockBean private CourseService courseService; - @MockBean private ExerciseInfoService exerciseInfoService; - private JWTResponse jwtToken; - private final Logger logger = LoggerFactory.getLogger(ExerciseControllerTests.class); @BeforeEach public void login() throws Exception { @@ -95,7 +74,7 @@ public void addExercise_valid() throws Exception { expectedExercise.setId(2L); expectedExercise.setCourse(course); ExerciseDTO exerciseDTO = new ExerciseDTO(); - exerciseDTO.setName("Spring Boot Exercise 1"); + exerciseDTO.name = "Spring Boot Exercise 1"; when(courseService.addExerciseToCourse(any(Long.class), any(Exercise.class), anyString())) .thenReturn(expectedExercise); @@ -144,9 +123,9 @@ public void addMultipleExercises_valid() throws Exception { course.setId(courseId); List<ExerciseDTO> exercisesList = new ArrayList<>(); List<Exercise> expectedExercises = new ArrayList<>(); - for (int i = 1; i <= number; i++){ + for (int i = 1; i <= number; i++) { ExerciseDTO dto = new ExerciseDTO(); - dto.setName("Exercise " + i); + dto.name = "Exercise " + i; Exercise exercise = new Exercise(); exercise.setName("Exercise " + i); exercise.setId((long) (1 + i)); @@ -172,6 +151,34 @@ public void addMultipleExercises_valid() throws Exception { logger.info("Test addMultipleExercises_valid() ends."); } + @Test + public void getExercise_valid() throws Exception { + logger.info("Test getExercise_valid() begins."); + + Course course = new Course("Spring Boot Course"); + Long courseId = 1L; + course.setId(courseId); + Exercise exercise = new Exercise(); + exercise.setName("Spring Boot Exercise 1"); + exercise.setId(2L); + exercise.setCourse(course); + course.addExercise(exercise); + when(courseService.getExercise(anyLong())).thenReturn(exercise); + + MvcResult mvcResult = mockMvc + .perform(get("/api/exercises/{exerciseId}", courseId).contentType("application/json").with(csrf()) + .header("Authorization", "Bearer " + jwtToken.getJwtToken())) + .andExpect(status().isOk()).andReturn(); + + verify(courseService, times(1)).getExercise(anyLong()); + String actualResponseBody = mvcResult.getResponse().getContentAsString(); + String expectedResponseBody = objectMapper.writerWithView(ExerciseViews.CourseView.class) + .writeValueAsString(exercise); + assertThat(expectedResponseBody).isEqualToIgnoringWhitespace(actualResponseBody); + + logger.info("Test getExercise_valid() ends."); + } + @Test public void getExercises_valid() throws Exception { logger.info("Test getExercises_valid() begins."); @@ -215,7 +222,7 @@ public void editExercise_valid() throws Exception { expectedExercise.setName("Spring Boot Exercise 1 v2"); expectedExercise.setId(1L); expectedExercise.setCourse(new Course("Spring Boot Course")); - exercise.setName("Spring Boot Exercise 1 v2"); + exercise.name = "Spring Boot Exercise 1 v2"; when(courseService.editExercise(anyLong(), any(Exercise.class), anyString())).thenReturn(expectedExercise); MvcResult mvcResult = mockMvc .perform(put("/api/exercises/1").contentType("application/json").with(csrf()) @@ -300,13 +307,13 @@ public void updateExerciseUserInfo_valid() throws Exception { User user = new User("johndoe@john.com", "johndoe", "password", "John", "Doe"); user.setId(4L); ExerciseUserInfoDTO euiDTO = new ExerciseUserInfoDTO(); - euiDTO.setStatus(1); + euiDTO.setStatus(ExerciseStatus.FINISHED); ArrayList<String> euiModifiedFiles = new ArrayList<>(); euiModifiedFiles.add("/sample"); euiDTO.setModifiedFiles(euiModifiedFiles); ExerciseUserInfo updatedEui = new ExerciseUserInfo(ex, user); - updatedEui.setStatus(1); - when(exerciseInfoService.updateExerciseUserInfo(anyLong(), anyString(), anyInt(), anyList())).thenReturn(updatedEui); + updatedEui.setStatus(ExerciseStatus.FINISHED); + when(exerciseInfoService.updateExerciseUserInfo(anyLong(), anyString(), any(ExerciseStatus.class), anyList())).thenReturn(updatedEui); MvcResult mvcResult = mockMvc .perform(put("/api/exercises/1/info").contentType("application/json").with(csrf()) @@ -324,8 +331,9 @@ public void updateExerciseUserInfo_valid() throws Exception { public void getAllStudentExerciseUserInfo_valid() throws Exception { // Set up courses and exercises Course course = new Course("Spring Boot Course"); - Exercise exercise = new Exercise("Exercise 1", course); + Exercise exercise = new Exercise("Exercise 1"); exercise.setId(10L); + exercise.setCourse(course); // Set up users Role studentRole = new Role("ROLE_STUDENT"); studentRole.setId(2L); @@ -340,7 +348,7 @@ public void getAllStudentExerciseUserInfo_valid() throws Exception { // Set up EUIs ExerciseUserInfo euiStudent1 = new ExerciseUserInfo(exercise, student1); ExerciseUserInfo euiStudent2 = new ExerciseUserInfo(exercise, student2); - euiStudent2.setStatus(1); + euiStudent2.setStatus(ExerciseStatus.FINISHED); List<ExerciseUserInfo> expectedList = new ArrayList<>(2); expectedList.add(euiStudent1); expectedList.add(euiStudent2); diff --git a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/ExerciseFilesControllerTests.java b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/ExerciseFilesControllerTests.java index 663b4a1d..64b8ea09 100644 --- a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/ExerciseFilesControllerTests.java +++ b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/ExerciseFilesControllerTests.java @@ -1,32 +1,5 @@ package com.vscode4teaching.vscode4teachingserver.controllertests; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.zip.ZipInputStream; - import com.fasterxml.jackson.databind.ObjectMapper; import com.vscode4teaching.vscode4teachingserver.controllers.dtos.JWTRequest; import com.vscode4teaching.vscode4teachingserver.controllers.dtos.JWTResponse; @@ -35,7 +8,6 @@ import com.vscode4teaching.vscode4teachingserver.model.ExerciseFile; import com.vscode4teaching.vscode4teachingserver.model.views.FileViews; import com.vscode4teaching.vscode4teachingserver.services.ExerciseFilesService; - import org.apache.tomcat.util.http.fileupload.FileUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -54,21 +26,34 @@ import org.springframework.test.web.servlet.MvcResult; import org.springframework.web.multipart.MultipartFile; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.*; +import java.util.zip.ZipInputStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + @SpringBootTest @TestPropertySource(locations = "classpath:test.properties") @AutoConfigureMockMvc public class ExerciseFilesControllerTests { + private static final Logger logger = LoggerFactory.getLogger(ExerciseFilesControllerTests.class); @Autowired private MockMvc mockMvc; - @Autowired private ObjectMapper objectMapper; - @MockBean private ExerciseFilesService filesService; - private JWTResponse jwtToken; - private static final Logger logger = LoggerFactory.getLogger(ExerciseFilesControllerTests.class); @BeforeEach public void login() throws Exception { @@ -106,7 +91,8 @@ public void downloadFilesFromExercise_exercise() throws Exception { } Map<Exercise, List<File>> returnMap = new HashMap<>(); returnMap.put(exercise, files); - when(filesService.getExerciseFiles(1l, "johndoe")).thenReturn(returnMap); + when(filesService.getExerciseFiles(1L, "johndoe")).thenReturn(returnMap); + when(filesService.existsExerciseFilesForUser(1L, "johndoe")).thenReturn(true); MvcResult result = mockMvc .perform(get("/api/exercises/1/files").contentType("application/zip").with(csrf()) @@ -126,7 +112,7 @@ public void downloadFilesFromExercise_exercise() throws Exception { @Test public void downloadFilesFromExercise_template() throws Exception { Exercise exercise = new Exercise("Exercise 1"); - exercise.setId(1l); + exercise.setId(1L); List<File> files = new ArrayList<>(); files.add(new File("v4t-course-test/spring-boot-course/exercise_1_1/template/ej1.txt")); files.add(new File("v4t-course-test/spring-boot-course/exercise_1_1/template/ej2.txt")); @@ -136,7 +122,7 @@ public void downloadFilesFromExercise_template() throws Exception { } Map<Exercise, List<File>> returnMap = new HashMap<>(); returnMap.put(exercise, files); - when(filesService.getExerciseFiles(1l, "johndoe")).thenReturn(returnMap); + when(filesService.getExerciseFiles(1L, "johndoe")).thenReturn(returnMap); MvcResult result = mockMvc .perform(get("/api/exercises/1/files").contentType("application/zip").with(csrf()) @@ -153,10 +139,40 @@ public void downloadFilesFromExercise_template() throws Exception { verify(filesService, times(1)).getExerciseFiles(anyLong(), anyString()); } + @Test + public void downloadFilesFromExercise_solution() throws Exception { + Exercise exercise = new Exercise("Exercise 1"); + exercise.setId(1L); + List<File> files = new ArrayList<>(); + files.add(new File("v4t-course-test/spring-boot-course/exercise_1_1/solution/ej1.txt")); + files.add(new File("v4t-course-test/spring-boot-course/exercise_1_1/solution/ej2.txt")); + for (File file : files) { + file.getParentFile().mkdirs(); + file.createNewFile(); + } + Map<Exercise, List<File>> returnMap = new HashMap<>(); + returnMap.put(exercise, files); + when(filesService.getExerciseSolution(1L, "johndoe")).thenReturn(returnMap); + + MvcResult result = mockMvc + .perform(get("/api/exercises/1/files/solution").contentType("application/zip").with(csrf()) + .header("Authorization", "Bearer " + jwtToken.getJwtToken())) + .andExpect(status().isOk()).andReturn(); + logger.info(result.toString()); + + assertThat(result.getResponse().getHeader("Content-Disposition")) + .isEqualTo("attachment; filename=\"solution-1.zip\""); + byte[] zipContent = result.getResponse().getContentAsByteArray(); + ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipContent)); + assertThat(zis.getNextEntry().getName()).isEqualTo("ej1.txt"); + assertThat(zis.getNextEntry().getName()).isEqualTo("ej2.txt"); + verify(filesService, times(1)).getExerciseSolution(anyLong(), anyString()); + } + @Test public void uploadFile() throws Exception { Exercise exercise = new Exercise("Exercise 1"); - exercise.setId(1l); + exercise.setId(1L); byte[] mock = null; MockMultipartFile mockMultiFile1 = new MockMultipartFile("file", "exs.zip", "application/zip", mock); Files.createDirectories(Paths.get("v4t-course-test/spring_boot_course_2/exercise_1_1/student_13/ex3")); @@ -181,9 +197,9 @@ public void uploadFile() throws Exception { .header("Authorization", "Bearer " + jwtToken.getJwtToken())).andExpect(status().isOk()).andReturn(); List<UploadFileResponse> expectedResponse = new ArrayList<>(); - expectedResponse.add(new UploadFileResponse("ex1.html", "text/html", 23l)); - expectedResponse.add(new UploadFileResponse("ex2.html", "text/html", 23l)); - expectedResponse.add(new UploadFileResponse("ex3" + File.separator + "ex3.html", "text/html", 23l)); + expectedResponse.add(new UploadFileResponse("ex1.html", "text/html", 23L)); + expectedResponse.add(new UploadFileResponse("ex2.html", "text/html", 23L)); + expectedResponse.add(new UploadFileResponse("ex3" + File.separator + "ex3.html", "text/html", 23L)); assertThat(result.getResponse().getContentAsString()) .isEqualToIgnoringWhitespace(objectMapper.writeValueAsString(expectedResponse)); @@ -210,7 +226,7 @@ public void uploadFile_noMultipart() throws Exception { @Test public void uploadTemplate() throws Exception { Exercise exercise = new Exercise("Exercise 1"); - exercise.setId(1l); + exercise.setId(1L); byte[] mock = null; MockMultipartFile mockMultiFile1 = new MockMultipartFile("file", "exs.zip", "application/zip", mock); Files.createDirectories(Paths.get("v4t-course-test/spring_boot_course_2/exercise_1_1/template/ex3")); @@ -237,9 +253,9 @@ public void uploadTemplate() throws Exception { .andExpect(status().isOk()).andReturn(); List<UploadFileResponse> expectedResponse = new ArrayList<>(); - expectedResponse.add(new UploadFileResponse("ex1.html", "text/html", 23l)); - expectedResponse.add(new UploadFileResponse("ex2.html", "text/html", 23l)); - expectedResponse.add(new UploadFileResponse("ex3" + File.separator + "ex3.html", "text/html", 23l)); + expectedResponse.add(new UploadFileResponse("ex1.html", "text/html", 23L)); + expectedResponse.add(new UploadFileResponse("ex2.html", "text/html", 23L)); + expectedResponse.add(new UploadFileResponse("ex3" + File.separator + "ex3.html", "text/html", 23L)); assertThat(result.getResponse().getContentAsString()) .isEqualToIgnoringWhitespace(objectMapper.writeValueAsString(expectedResponse)); @@ -248,10 +264,51 @@ public void uploadTemplate() throws Exception { verify(filesService, times(1)).saveExerciseTemplate(anyLong(), any(MultipartFile.class), anyString()); } + @Test + public void uploadSolution() throws Exception { + Exercise exercise = new Exercise("Exercise 1"); + exercise.setId(1L); + byte[] mock = null; + MockMultipartFile mockMultiFile1 = new MockMultipartFile("file", "exs.zip", "application/zip", mock); + Files.createDirectories(Paths.get("v4t-course-test/spring_boot_course_2/exercise_1_1/solution/ex3")); + Path path1 = Paths.get("src/test/java/com/vscode4teaching/vscode4teachingserver/files/ex1.html"); + Path path1Copy = Paths.get("v4t-course-test/spring_boot_course_2/exercise_1_1/solution/ex1.html"); + Files.copy(path1, path1Copy, StandardCopyOption.REPLACE_EXISTING); + Path path2 = Paths.get("src/test/java/com/vscode4teaching/vscode4teachingserver/files/ex2.html"); + Path path2Copy = Paths.get("v4t-course-test/spring_boot_course_2/exercise_1_1/solution/ex2.html"); + Files.copy(path2, path2Copy, StandardCopyOption.REPLACE_EXISTING); + Path path3 = Paths.get("src/test/java/com/vscode4teaching/vscode4teachingserver/files/ex3/ex3.html"); + Path path3Copy = Paths.get("v4t-course-test/spring_boot_course_2/exercise_1_1/solution/ex3/ex3.html"); + Files.copy(path3, path3Copy, StandardCopyOption.REPLACE_EXISTING); + + File mockFile1 = new File("v4t-course-test/spring_boot_course_2/exercise_1_1/solution/", "ex1.html"); + File mockFile2 = new File("v4t-course-test/spring_boot_course_2/exercise_1_1/solution/", "ex2.html"); + File mockFile3 = new File("v4t-course-test/spring_boot_course_2/exercise_1_1/solution/", "ex3/ex3.html"); + Map<Exercise, List<File>> returnMap = new HashMap<>(); + returnMap.put(exercise, Arrays.asList(mockFile1, mockFile2, mockFile3)); + when(filesService.saveExerciseSolution(anyLong(), any(MultipartFile.class), anyString())).thenReturn(returnMap); + + MvcResult result = mockMvc + .perform(multipart("/api/exercises/1/files/solution").file(mockMultiFile1).with(csrf()) + .header("Authorization", "Bearer " + jwtToken.getJwtToken())) + .andExpect(status().isOk()).andReturn(); + + List<UploadFileResponse> expectedResponse = new ArrayList<>(); + expectedResponse.add(new UploadFileResponse("ex1.html", "text/html", 23L)); + expectedResponse.add(new UploadFileResponse("ex2.html", "text/html", 23L)); + expectedResponse.add(new UploadFileResponse("ex3" + File.separator + "ex3.html", "text/html", 23L)); + + assertThat(result.getResponse().getContentAsString()) + .isEqualToIgnoringWhitespace(objectMapper.writeValueAsString(expectedResponse)); + + logger.info(result.getResponse().getContentAsString()); + verify(filesService, times(1)).saveExerciseSolution(anyLong(), any(MultipartFile.class), anyString()); + } + @Test public void getTemplate() throws Exception { Exercise exercise = new Exercise("Exercise 1"); - exercise.setId(1l); + exercise.setId(1L); List<File> files = new ArrayList<>(); files.add(new File("v4t-course-test/spring-boot-course/exercise_1_1/template/ej1.txt")); files.add(new File("v4t-course-test/spring-boot-course/exercise_1_1/template/ej2.txt")); @@ -261,7 +318,7 @@ public void getTemplate() throws Exception { } Map<Exercise, List<File>> returnMap = new HashMap<>(); returnMap.put(exercise, files); - when(filesService.getExerciseTemplate(1l, "johndoe")).thenReturn(returnMap); + when(filesService.getExerciseTemplate(1L, "johndoe")).thenReturn(returnMap); MvcResult result = mockMvc .perform(get("/api/exercises/1/files/template").contentType("application/zip").with(csrf()) @@ -281,7 +338,7 @@ public void getTemplate() throws Exception { @Test public void getAllStudentFiles() throws Exception { Exercise exercise = new Exercise("Exercise 1"); - exercise.setId(1l); + exercise.setId(1L); List<File> files = new ArrayList<>(); files.add(new File("v4t-course-test/spring-boot-course/exercise_1_1/student_13/ej1.txt")); files.add(new File("v4t-course-test/spring-boot-course/exercise_1_1/student_13/ej2.txt")); @@ -295,7 +352,7 @@ public void getAllStudentFiles() throws Exception { } Map<Exercise, List<File>> returnMap = new HashMap<>(); returnMap.put(exercise, files); - when(filesService.getAllStudentsFiles(1l, "johndoe")).thenReturn(returnMap); + when(filesService.getAllStudentsFiles(1L, "johndoe")).thenReturn(returnMap); MvcResult result = mockMvc .perform(get("/api/exercises/1/teachers/files").contentType("application/zip").with(csrf()) @@ -317,10 +374,10 @@ public void getAllStudentFiles() throws Exception { } @Test - @EnabledOnOs({ OS.WINDOWS }) + @EnabledOnOs({OS.WINDOWS}) public void getAllStudentFilesWindows() throws Exception { Exercise exercise = new Exercise("Exercise 1"); - exercise.setId(1l); + exercise.setId(1L); List<File> files = new ArrayList<>(); files.add(new File("v4t-course-test\\spring-boot-course\\exercise_1_1\\student_13\\ej1.txt")); files.add(new File("v4t-course-test\\spring-boot-course\\exercise_1_1\\student_13\\ej2.txt")); @@ -334,7 +391,7 @@ public void getAllStudentFilesWindows() throws Exception { } Map<Exercise, List<File>> returnMap = new HashMap<>(); returnMap.put(exercise, files); - when(filesService.getAllStudentsFiles(1l, "johndoe")).thenReturn(returnMap); + when(filesService.getAllStudentsFiles(1L, "johndoe")).thenReturn(returnMap); MvcResult result = mockMvc .perform(get("/api/exercises/1/teachers/files").contentType("application/zip").with(csrf()) @@ -360,14 +417,14 @@ public void getFileInfo() throws Exception { ExerciseFile ex1 = new ExerciseFile("test1"); List<ExerciseFile> exFiles = new ArrayList<>(); exFiles.add(ex1); - when(filesService.getFileIdsByExerciseAndOwner(anyLong(), any(String.class))).thenReturn(exFiles); + when(filesService.getFileIdsByExerciseAndId(anyLong(), any(String.class))).thenReturn(exFiles); MvcResult mvcResult = mockMvc .perform(get("/api/users/johndoejr1/exercises/2/files").contentType("application/json") .header("Authorization", "Bearer " + jwtToken.getJwtToken()).with(csrf())) .andExpect(status().isOk()).andReturn(); - verify(filesService, times(1)).getFileIdsByExerciseAndOwner(anyLong(), any(String.class)); + verify(filesService, times(1)).getFileIdsByExerciseAndId(anyLong(), any(String.class)); String actualResponseBody = mvcResult.getResponse().getContentAsString(); String expectedResponseBody = objectMapper.writerWithView(FileViews.GeneralView.class) .writeValueAsString(exFiles); diff --git a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/JWTLoginControllerTests.java b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/JWTLoginControllerTests.java index d24ef252..a0ddb125 100644 --- a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/JWTLoginControllerTests.java +++ b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/JWTLoginControllerTests.java @@ -36,7 +36,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.equalTo; -import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -90,11 +89,11 @@ public void login() throws Exception { @Test public void register() throws Exception { Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); com.vscode4teaching.vscode4teachingserver.model.User expectedUser = new com.vscode4teaching.vscode4teachingserver.model.User( "johndoe@gmail.com", "johndoe", bEncoder.encode("password"), "John", "Doe"); - expectedUser.setId(1l); - expectedUser.setRoles(Arrays.asList(studentRole)); + expectedUser.setId(1L); + expectedUser.setRoles(List.of(studentRole)); UserDTO userDTO = new UserDTO(); userDTO.setEmail("johndoe@gmail.com"); userDTO.setUsername("johndoe"); @@ -117,12 +116,12 @@ public void register() throws Exception { @WithMockUser(roles = {"STUDENT", "TEACHER"}) public void registerTeacher() throws Exception { Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); Role teacherRole = new Role("ROLE_TEACHER"); - teacherRole.setId(3l); + teacherRole.setId(3L); com.vscode4teaching.vscode4teachingserver.model.User expectedUser = new com.vscode4teaching.vscode4teachingserver.model.User( "johndoe@gmail.com", "johndoe", bEncoder.encode("password"), "John", "Doe"); - expectedUser.setId(1l); + expectedUser.setId(1L); expectedUser.setRoles(Arrays.asList(studentRole, teacherRole)); UserDTO userDTO = new UserDTO(); userDTO.setEmail("johndoe@gmail.com"); @@ -158,16 +157,16 @@ public void registerTeacher() throws Exception { public void getCurrentUser() throws Exception { Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); Role teacherRole = new Role("ROLE_TEACHER"); - teacherRole.setId(3l); + teacherRole.setId(3L); com.vscode4teaching.vscode4teachingserver.model.User expectedUser = new com.vscode4teaching.vscode4teachingserver.model.User( "johndoe@gmail.com", "johndoe", bEncoder.encode("password"), "John", "Doe"); - expectedUser.setId(1l); + expectedUser.setId(1L); expectedUser.setRoles(Arrays.asList(studentRole, teacherRole)); Course course = new Course("Spring Boot Course"); course.addUserInCourse(expectedUser); - course.setId(4l); + course.setId(4L); expectedUser.addCourse(course); when(userService.findByUsername("johndoe")).thenReturn(expectedUser); when(jwtTokenUtil.getUsernameFromToken(any(HttpServletRequest.class))).thenReturn("johndoe"); diff --git a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/utils/MockAuthentication.java b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/utils/MockAuthentication.java index 3efa711a..05b86fed 100644 --- a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/utils/MockAuthentication.java +++ b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/utils/MockAuthentication.java @@ -1,10 +1,10 @@ package com.vscode4teaching.vscode4teachingserver.controllertests.utils; -import java.util.Collection; - import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; +import java.util.Collection; + public class MockAuthentication implements Authentication { private static final long serialVersionUID = 946834943413654L; diff --git a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/integrationtests/IntegrationTests.java b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/integrationtests/IntegrationTests.java index d7838fce..8c080825 100644 --- a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/integrationtests/IntegrationTests.java +++ b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/integrationtests/IntegrationTests.java @@ -5,7 +5,6 @@ import com.vscode4teaching.vscode4teachingserver.controllers.dtos.JWTRequest; import com.vscode4teaching.vscode4teachingserver.controllers.dtos.JWTResponse; import com.vscode4teaching.vscode4teachingserver.model.Course; - import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -14,10 +13,10 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; -import static org.assertj.core.api.Assertions.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; // Databse is initialized in class DatabaseInitializer @SpringBootTest @@ -67,7 +66,7 @@ public void expectError() throws Exception { JWTResponse.class); MvcResult mvcErrorResult = mockMvc.perform(post("/api/courses").contentType("application/json").with(csrf()) - .header("Authorization", "Bearer " + jwtToken.getJwtToken())).andExpect(status().isBadRequest()) + .header("Authorization", "Bearer " + jwtToken.getJwtToken())).andExpect(status().isBadRequest()) .andReturn(); CourseDTO course = new CourseDTO(); diff --git a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/CommentServiceImplTests.java b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/CommentServiceImplTests.java index a4186d1b..795b9659 100644 --- a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/CommentServiceImplTests.java +++ b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/CommentServiceImplTests.java @@ -1,19 +1,6 @@ package com.vscode4teaching.vscode4teachingserver.servicetests; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.File; -import java.util.List; -import java.util.Optional; - -import com.vscode4teaching.vscode4teachingserver.model.Comment; -import com.vscode4teaching.vscode4teachingserver.model.CommentThread; -import com.vscode4teaching.vscode4teachingserver.model.Exercise; -import com.vscode4teaching.vscode4teachingserver.model.ExerciseFile; -import com.vscode4teaching.vscode4teachingserver.model.User; +import com.vscode4teaching.vscode4teachingserver.model.*; import com.vscode4teaching.vscode4teachingserver.model.repositories.CommentRepository; import com.vscode4teaching.vscode4teachingserver.model.repositories.CommentThreadRepository; import com.vscode4teaching.vscode4teachingserver.model.repositories.ExerciseFileRepository; @@ -22,7 +9,6 @@ import com.vscode4teaching.vscode4teachingserver.services.exceptions.ExerciseNotFoundException; import com.vscode4teaching.vscode4teachingserver.services.exceptions.FileNotFoundException; import com.vscode4teaching.vscode4teachingserver.servicesimpl.CommentServiceImpl; - import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -32,58 +18,58 @@ import org.slf4j.LoggerFactory; import org.springframework.test.context.TestPropertySource; +import java.io.File; +import java.util.List; +import java.util.Optional; + import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @TestPropertySource(locations = "classpath:test.properties") public class CommentServiceImplTests { + private static final Logger logger = LoggerFactory.getLogger(CommentServiceImplTests.class); @Mock private ExerciseFileRepository exerciseFileRepository; - @Mock private CommentThreadRepository commentThreadRepository; - @Mock private CommentRepository commentRepository; - @Mock private ExerciseRepository exerciseRepository; - @InjectMocks private CommentServiceImpl commentServiceImpl; - private static final Logger logger = LoggerFactory.getLogger(CommentServiceImplTests.class); - @Test public void saveCommentThread() throws FileNotFoundException { logger.info("Start saveCommentThread"); ExerciseFile demoFile = new ExerciseFile("testPath"); - demoFile.setId(1l); - CommentThread commentThread = new CommentThread(demoFile, 0l, "Test line"); - CommentThread expectedCommentThread = new CommentThread(demoFile, 0l, "Test line"); - expectedCommentThread.setId(2l); + demoFile.setId(1L); + CommentThread commentThread = new CommentThread(demoFile, 0L, "Test line"); + CommentThread expectedCommentThread = new CommentThread(demoFile, 0L, "Test line"); + expectedCommentThread.setId(2L); Comment c1 = new Comment(commentThread, "Test 1", "johndoe"); Comment c2 = new Comment(commentThread, "Test 2", "johndoe"); commentThread.addComment(c1); commentThread.addComment(c2); Comment expectedC1 = new Comment(expectedCommentThread, "Test 1", "johndoe"); - expectedC1.setId(3l); + expectedC1.setId(3L); Comment expectedC2 = new Comment(expectedCommentThread, "Test 2", "johndoe"); - expectedC2.setId(4l); + expectedC2.setId(4L); expectedCommentThread.addComment(expectedC1); expectedCommentThread.addComment(expectedC2); ExerciseFile expectedFile = new ExerciseFile("testPath"); - expectedFile.setId(1l); + expectedFile.setId(1L); expectedFile.addCommentThread(expectedCommentThread); - when(exerciseFileRepository.findById(1l)).thenReturn(Optional.of(demoFile)); + when(exerciseFileRepository.findById(1L)).thenReturn(Optional.of(demoFile)); when(exerciseFileRepository.save(any(ExerciseFile.class))).thenReturn(expectedFile); when(commentThreadRepository.save(commentThread)).thenReturn(expectedCommentThread); - when(commentRepository.save(any(Comment.class))).thenReturn(null); + when(commentRepository.saveAll(anyList())).thenReturn(null); - CommentThread savedCommentThread = commentServiceImpl.saveCommentThread(1l, commentThread); + CommentThread savedCommentThread = commentServiceImpl.saveCommentThread(1L, commentThread); - assertThat(savedCommentThread.getId()).isEqualTo(2l); + assertThat(savedCommentThread.getId()).isEqualTo(2L); assertThat(savedCommentThread.getLine()).isEqualTo(0); List<Comment> commentList = savedCommentThread.getComments(); assertThat(commentList.size()).isEqualTo(2); @@ -92,7 +78,7 @@ public void saveCommentThread() throws FileNotFoundException { assertThat(commentList.get(i).getBody()).isEqualTo("Test " + (i + 1)); assertThat(commentList.get(i).getThread()).isEqualTo(expectedCommentThread); } - verify(exerciseFileRepository, times(1)).findById(1l); + verify(exerciseFileRepository, times(1)).findById(1L); verify(exerciseFileRepository, times(1)).save(any(ExerciseFile.class)); logger.info("End saveCommentThread"); @@ -101,23 +87,23 @@ public void saveCommentThread() throws FileNotFoundException { @Test public void getCommentThreadByFile() throws FileNotFoundException { ExerciseFile demoFile = new ExerciseFile("testPath"); - demoFile.setId(1l); - CommentThread expectedCommentThread = new CommentThread(demoFile, 0l, "Test line"); - expectedCommentThread.setId(2l); + demoFile.setId(1L); + CommentThread expectedCommentThread = new CommentThread(demoFile, 0L, "Test line"); + expectedCommentThread.setId(2L); Comment expectedC1 = new Comment(expectedCommentThread, "Test 1", "johndoe"); - expectedC1.setId(3l); + expectedC1.setId(3L); Comment expectedC2 = new Comment(expectedCommentThread, "Test 2", "johndoe"); - expectedC2.setId(4l); + expectedC2.setId(4L); expectedCommentThread.addComment(expectedC1); expectedCommentThread.addComment(expectedC2); ExerciseFile expectedFile = new ExerciseFile("testPath"); - expectedFile.setId(1l); + expectedFile.setId(1L); expectedFile.addCommentThread(expectedCommentThread); - when(exerciseFileRepository.findById(1l)).thenReturn(Optional.of(expectedFile)); + when(exerciseFileRepository.findById(1L)).thenReturn(Optional.of(expectedFile)); - List<CommentThread> savedCommentThread = commentServiceImpl.getCommentThreadsByFile(1l); + List<CommentThread> savedCommentThread = commentServiceImpl.getCommentThreadsByFile(1L); - assertThat(savedCommentThread.get(0).getId()).isEqualTo(2l); + assertThat(savedCommentThread.get(0).getId()).isEqualTo(2L); assertThat(savedCommentThread.get(0).getLine()).isEqualTo(0); List<Comment> commentList = savedCommentThread.get(0).getComments(); assertThat(commentList.size()).isEqualTo(2); @@ -126,39 +112,39 @@ public void getCommentThreadByFile() throws FileNotFoundException { assertThat(commentList.get(i).getBody()).isEqualTo("Test " + (i + 1)); assertThat(commentList.get(i).getThread()).isEqualTo(expectedCommentThread); } - verify(exerciseFileRepository, times(1)).findById(1l); + verify(exerciseFileRepository, times(1)).findById(1L); } @Test public void getFilesWithCommentsByUser() throws ExerciseNotFoundException { ExerciseFile demoFile = new ExerciseFile("johndoe" + File.separator + "testPath"); demoFile.setOwner(new User("johndoe@johndoe.com", "johndoe", "johndoe", "johndoe", "johndoe")); - demoFile.setId(1l); - CommentThread expectedCommentThread = new CommentThread(demoFile, 0l, "Test line"); - expectedCommentThread.setId(2l); + demoFile.setId(1L); + CommentThread expectedCommentThread = new CommentThread(demoFile, 0L, "Test line"); + expectedCommentThread.setId(2L); Comment expectedC1 = new Comment(expectedCommentThread, "Test 1", "johndoe"); - expectedC1.setId(3l); + expectedC1.setId(3L); Comment expectedC2 = new Comment(expectedCommentThread, "Test 2", "johndoe"); - expectedC2.setId(4l); + expectedC2.setId(4L); expectedCommentThread.addComment(expectedC1); expectedCommentThread.addComment(expectedC2); ExerciseFile expectedFile = new ExerciseFile("johndoe" + File.separator + "testPath"); - expectedFile.setId(1l); + expectedFile.setId(1L); expectedFile.addCommentThread(expectedCommentThread); expectedFile.setOwner(new User("johndoe@johndoe.com", "johndoe", "johndoe", "johndoe", "johndoe")); Exercise ex = new Exercise("Test ex"); - ex.setId(1000l); + ex.setId(1000L); ex.addUserFile(expectedFile); ExerciseFile expectedFile2 = new ExerciseFile("johndoe2" + File.separator + "testPath2"); - expectedFile2.setId(555l); + expectedFile2.setId(555L); expectedFile2.setOwner(new User("johndoe2@johndoe.com", "johndoe2", "johndoe2", "johndoe2", "johndoe2")); ex.addUserFile(expectedFile2); - when(exerciseRepository.findById(1000l)).thenReturn(Optional.of(ex)); + when(exerciseRepository.findById(1000L)).thenReturn(Optional.of(ex)); - List<ExerciseFile> johndoeFiles = commentServiceImpl.getFilesWithCommentsByUser(1000l, "johndoe"); - List<ExerciseFile> johndoe2Files = commentServiceImpl.getFilesWithCommentsByUser(1000l, "johndoe2"); + List<ExerciseFile> johndoeFiles = commentServiceImpl.getFilesWithCommentsByUser(1000L, "johndoe"); + List<ExerciseFile> johndoe2Files = commentServiceImpl.getFilesWithCommentsByUser(1000L, "johndoe2"); - verify(exerciseRepository, times(2)).findById(1000l); + verify(exerciseRepository, times(2)).findById(1000L); assertThat(johndoeFiles.size()).isEqualTo(1); assertThat(johndoeFiles.get(0).getComments().size()).isEqualTo(1); assertThat(johndoeFiles.get(0).getComments().get(0).getComments().size()).isEqualTo(2); @@ -170,29 +156,29 @@ public void getFilesWithCommentsByUser() throws ExerciseNotFoundException { public void updateThreadLine() throws CommentNotFoundException { ExerciseFile demoFile = new ExerciseFile("johndoe" + File.separator + "testPath"); demoFile.setOwner(new User("johndoe@johndoe.com", "johndoe", "johndoe", "johndoe", "johndoe")); - demoFile.setId(1l); - CommentThread commentThread = new CommentThread(demoFile, 0l, "Test line"); - commentThread.setId(2l); + demoFile.setId(1L); + CommentThread commentThread = new CommentThread(demoFile, 0L, "Test line"); + commentThread.setId(2L); Comment expectedC1 = new Comment(commentThread, "Test 1", "johndoe"); - expectedC1.setId(3l); + expectedC1.setId(3L); Comment expectedC2 = new Comment(commentThread, "Test 2", "johndoe"); - expectedC2.setId(4l); + expectedC2.setId(4L); commentThread.addComment(expectedC1); commentThread.addComment(expectedC2); - CommentThread expectedCommentThread = new CommentThread(demoFile, 5l, "Test line 5"); - commentThread.setId(2l); + CommentThread expectedCommentThread = new CommentThread(demoFile, 5L, "Test line 5"); + commentThread.setId(2L); expectedCommentThread.addComment(expectedC1); expectedCommentThread.addComment(expectedC2); - when(commentThreadRepository.findById(2l)).thenReturn(Optional.of(commentThread)); + when(commentThreadRepository.findById(2L)).thenReturn(Optional.of(commentThread)); when(commentThreadRepository.save(commentThread)).thenReturn(expectedCommentThread); - CommentThread savedCommentThread = commentServiceImpl.updateCommentThreadLine(2l, 5l, "Test line 5"); + CommentThread savedCommentThread = commentServiceImpl.updateCommentThreadLine(2L, 5L, "Test line 5"); - verify(commentThreadRepository, times(1)).findById(2l); + verify(commentThreadRepository, times(1)).findById(2L); verify(commentThreadRepository, times(1)).save(commentThread); - assertThat(savedCommentThread.getLine()).isEqualTo(5l); + assertThat(savedCommentThread.getLine()).isEqualTo(5L); assertThat(savedCommentThread.getLineText()).isEqualToIgnoringWhitespace("Test line 5"); - assertThat(commentThread.getLine()).isEqualTo(5l); + assertThat(commentThread.getLine()).isEqualTo(5L); assertThat(commentThread.getLineText()).isEqualToIgnoringWhitespace("Test line 5"); } diff --git a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/CourseServiceImplTests.java b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/CourseServiceImplTests.java index 7bf5cdfb..c0261510 100644 --- a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/CourseServiceImplTests.java +++ b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/CourseServiceImplTests.java @@ -1,39 +1,13 @@ package com.vscode4teaching.vscode4teachingserver.servicetests; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.AdditionalAnswers.returnsFirstArg; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import com.vscode4teaching.vscode4teachingserver.model.Course; -import com.vscode4teaching.vscode4teachingserver.model.Exercise; -import com.vscode4teaching.vscode4teachingserver.model.ExerciseUserInfo; -import com.vscode4teaching.vscode4teachingserver.model.Role; -import com.vscode4teaching.vscode4teachingserver.model.User; +import com.vscode4teaching.vscode4teachingserver.model.*; import com.vscode4teaching.vscode4teachingserver.model.repositories.CourseRepository; import com.vscode4teaching.vscode4teachingserver.model.repositories.ExerciseRepository; import com.vscode4teaching.vscode4teachingserver.model.repositories.ExerciseUserInfoRepository; import com.vscode4teaching.vscode4teachingserver.model.repositories.UserRepository; -import com.vscode4teaching.vscode4teachingserver.services.exceptions.CourseNotFoundException; -import com.vscode4teaching.vscode4teachingserver.services.exceptions.ExerciseNotFoundException; -import com.vscode4teaching.vscode4teachingserver.services.exceptions.NotCreatorException; -import com.vscode4teaching.vscode4teachingserver.services.exceptions.NotInCourseException; -import com.vscode4teaching.vscode4teachingserver.services.exceptions.TeacherNotFoundException; -import com.vscode4teaching.vscode4teachingserver.services.exceptions.UserNotFoundException; -import com.vscode4teaching.vscode4teachingserver.servicesimpl.CourseServiceImpl; +import com.vscode4teaching.vscode4teachingserver.services.exceptions.*; import com.vscode4teaching.vscode4teachingserver.services.websockets.SocketHandler; - +import com.vscode4teaching.vscode4teachingserver.servicesimpl.CourseServiceImpl; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -43,49 +17,53 @@ import org.slf4j.LoggerFactory; import org.springframework.test.context.TestPropertySource; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.AdditionalAnswers.returnsFirstArg; +import static org.mockito.Mockito.*; + @ExtendWith(MockitoExtension.class) @TestPropertySource(locations = "classpath:test.properties") public class CourseServiceImplTests { + private static final Logger logger = LoggerFactory.getLogger(CourseServiceImplTests.class); @Mock private CourseRepository courseRepository; - @Mock private UserRepository userRepository; - @Mock private ExerciseRepository exerciseRepository; - @Mock private ExerciseUserInfoRepository exerciseUserInfoRepository; - @Mock private SocketHandler websocketHandler; - @InjectMocks private CourseServiceImpl courseServiceImpl; - private static final Logger logger = LoggerFactory.getLogger(CourseServiceImplTests.class); - @Test - public void registerNewCourse_valid() throws TeacherNotFoundException, NotInCourseException { + public void registerNewCourse_valid() throws TeacherNotFoundException { logger.info("Test registerNewCourse_valid() begins."); User user = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); Optional<User> userOpt = Optional.of(user); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); Role teacherRole = new Role("ROLE_TEACHER"); - teacherRole.setId(3l); + teacherRole.setId(3L); Course course = new Course("Spring Boot Course"); Course expectedCourse = new Course("Spring Boot Course"); - expectedCourse.setId(1l); + expectedCourse.setId(1L); when(courseRepository.save(course)).thenReturn(expectedCourse); when(userRepository.findByUsername(anyString())).thenReturn(userOpt); Course savedCourse = courseServiceImpl.registerNewCourse(course, "johndoe"); logger.info("Course to save: {}.\n Course saved: {}", course, savedCourse); - assertThat(savedCourse.getId()).isEqualTo(1l); + assertThat(savedCourse.getId()).isEqualTo(1L); assertThat(savedCourse.getName()).isEqualTo(course.getName()); assertThat(savedCourse.getExercises()).isNotNull(); assertThat(savedCourse.getExercises()).isEmpty(); @@ -123,12 +101,12 @@ public void addExerciseToCourse_valid() throws Exception { logger.info("Test addExerciseToCourse_valid() begins."); Course course = new Course("Spring Boot Course"); - Long courseTestId = 1l; + Long courseTestId = 1L; User teacher = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); Role teacherRole = new Role("ROLE_TEACHER"); - teacherRole.setId(3l); + teacherRole.setId(3L); teacher.addRole(studentRole); teacher.addRole(teacherRole); teacher.addCourse(course); @@ -156,7 +134,7 @@ public void addExerciseToCourse_valid() throws Exception { @Test public void addExerciseToCourse_exception() { logger.info("Test addExerciseToCourse_exception() begins."); - Long courseTestId = 1l; + Long courseTestId = 1L; Optional<Course> courseOpt = Optional.empty(); Exercise exercise = new Exercise(); exercise.setName("Unit testing in Spring Boot"); @@ -173,24 +151,24 @@ public void editCourse_valid() throws Exception { User teacher = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); Role teacherRole = new Role("ROLE_TEACHER"); - teacherRole.setId(3l); + teacherRole.setId(3L); Course oldCourse = new Course("Spring Boot Course"); Course courseData = new Course("New Spring Boot Course"); Course expectedCourse = new Course("New Spring Boot Course"); - expectedCourse.setId(1l); + expectedCourse.setId(1L); teacher.addRole(studentRole); teacher.addRole(teacherRole); teacher.addCourse(oldCourse); oldCourse.addUserInCourse(teacher); Optional<Course> oldCourseOpt = Optional.of(oldCourse); when(courseRepository.save(any(Course.class))).thenReturn(expectedCourse); - when(courseRepository.findById(1l)).thenReturn(oldCourseOpt); + when(courseRepository.findById(1L)).thenReturn(oldCourseOpt); - Course savedCourse = courseServiceImpl.editCourse(1l, courseData, "johndoe"); + Course savedCourse = courseServiceImpl.editCourse(1L, courseData, "johndoe"); - assertThat(savedCourse.getId()).isEqualTo(1l); + assertThat(savedCourse.getId()).isEqualTo(1L); assertThat(savedCourse.getName()).isEqualTo("New Spring Boot Course"); verify(courseRepository, times(1)).save(any(Course.class)); verify(courseRepository, times(1)).findById(anyLong()); @@ -201,13 +179,13 @@ public void editCourse_valid() throws Exception { @Test public void deleteCourse_valid() throws CourseNotFoundException, NotInCourseException, NotCreatorException { Course course = new Course("Spring Boot Course"); - Long courseTestId = 1l; - course.setId(1l); + Long courseTestId = 1L; + course.setId(1L); User teacher = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); Role teacherRole = new Role("ROLE_TEACHER"); - teacherRole.setId(3l); + teacherRole.setId(3L); teacher.addRole(studentRole); teacher.addRole(teacherRole); teacher.addCourse(course); @@ -228,16 +206,16 @@ public void deleteCourse_valid() throws CourseNotFoundException, NotInCourseExce @Test public void getExercises_valid() throws CourseNotFoundException, NotInCourseException { Course course = new Course("Spring Boot Course"); - Long courseTestId = 1l; + Long courseTestId = 1L; course.setId(courseTestId); User teacher = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); User student = new User("johndoejr2@gmail.com", "johndoe2", "pass", "John", "Doe 2"); Role studentRole = new Role("ROLE_STUDENT"); Role teacherRole = new Role("ROLE_TEACHER"); - studentRole.setId(2l); - teacherRole.setId(3l); - teacher.setId(4l); - student.setId(5l); + studentRole.setId(2L); + teacherRole.setId(3L); + teacher.setId(4L); + student.setId(5L); teacher.addRole(studentRole); teacher.addRole(teacherRole); student.addRole(studentRole); @@ -247,7 +225,7 @@ public void getExercises_valid() throws CourseNotFoundException, NotInCourseExce course.addUserInCourse(student); Exercise exercise = new Exercise(); exercise.setName("Spring Boot Exercise 1"); - exercise.setId(6l); + exercise.setId(6L); exercise.setCourse(course); course.addExercise(exercise); Optional<Course> courseOpt = Optional.of(course); @@ -264,14 +242,14 @@ public void getExercises_valid() throws CourseNotFoundException, NotInCourseExce @Test public void editExercise_valid() throws ExerciseNotFoundException, NotInCourseException { Course course = new Course("Spring Boot Course"); - Long courseTestId = 1l; + Long courseTestId = 1L; course.setId(courseTestId); User teacher = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); Role studentRole = new Role("ROLE_STUDENT"); Role teacherRole = new Role("ROLE_TEACHER"); - studentRole.setId(2l); - teacherRole.setId(3l); - teacher.setId(4l); + studentRole.setId(2L); + teacherRole.setId(3L); + teacher.setId(4L); teacher.addRole(studentRole); teacher.addRole(teacherRole); teacher.addCourse(course); @@ -282,16 +260,16 @@ public void editExercise_valid() throws ExerciseNotFoundException, NotInCourseEx exerciseData.setName("Spring Boot Exercise 2"); Exercise newExercise = new Exercise(); newExercise.setName("Spring Boot Exercise 2"); - oldExercise.setId(5l); - newExercise.setId(5l); + oldExercise.setId(5L); + newExercise.setId(5L); oldExercise.setCourse(course); course.addExercise(oldExercise); newExercise.setCourse(course); Optional<Exercise> exerciseOpt = Optional.of(oldExercise); - when(exerciseRepository.findById(5l)).thenReturn(exerciseOpt); + when(exerciseRepository.findById(5L)).thenReturn(exerciseOpt); when(exerciseRepository.save(any(Exercise.class))).thenReturn(newExercise); - Exercise savedExercise = courseServiceImpl.editExercise(5l, exerciseData, "johndoe"); + Exercise savedExercise = courseServiceImpl.editExercise(5L, exerciseData, "johndoe"); assertThat(savedExercise.getName()).isEqualTo("Spring Boot Exercise 2"); assertThat(savedExercise.getCourse()).isEqualTo(course); @@ -303,18 +281,18 @@ public void editExercise_valid() throws ExerciseNotFoundException, NotInCourseEx public void deleteExercise_valid() throws CourseNotFoundException, NotInCourseException, ExerciseNotFoundException { User teacher = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); Role teacherRole = new Role("ROLE_TEACHER"); - teacherRole.setId(3l); + teacherRole.setId(3L); Course course = new Course("Spring Boot Course"); - course.setId(1l); + course.setId(1L); teacher.addRole(studentRole); teacher.addRole(teacherRole); teacher.addCourse(course); course.addUserInCourse(teacher); Exercise exercise = new Exercise(); exercise.setName("Spring Boot Exercise 1"); - exercise.setId(6l); + exercise.setId(6L); exercise.setCourse(course); course.addExercise(exercise); Optional<Exercise> exerciseOpt = Optional.of(exercise); @@ -322,26 +300,85 @@ public void deleteExercise_valid() throws CourseNotFoundException, NotInCourseEx courseWithoutExercises.addUserInCourse(teacher); Optional<Course> courseOpt = Optional.of(courseWithoutExercises); doNothing().when(exerciseRepository).delete(any(Exercise.class)); - when(exerciseRepository.findById(6l)).thenReturn(exerciseOpt); - when(courseRepository.findById(1l)).thenReturn(courseOpt); + when(exerciseRepository.findById(6L)).thenReturn(exerciseOpt); + when(courseRepository.findById(1L)).thenReturn(courseOpt); - courseServiceImpl.deleteExercise(6l, "johndoe"); - List<Exercise> savedExercises = courseServiceImpl.getExercises(1l, "johndoe"); + courseServiceImpl.deleteExercise(6L, "johndoe"); + List<Exercise> savedExercises = courseServiceImpl.getExercises(1L, "johndoe"); assertThat(savedExercises).isEmpty(); - verify(exerciseRepository, times(1)).findById(6l); + verify(exerciseRepository, times(1)).findById(6L); + } + + @Test + public void joinCourseWithSharingCode_valid() throws CourseNotFoundException, UserNotFoundException { + logger.info("Test joinCourseWithSharingCode_valid() begins."); + Course course = new Course("Spring Boot Course"); + course.setId(1L); + Exercise exercise = new Exercise("Spring Boot Exercise 1"); + exercise.setId(2L); + exercise.setCourse(course); + course.addExercise(exercise); + User user = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); + user.setId(3L); + ExerciseUserInfo eui = new ExerciseUserInfo(exercise, user); + eui.setId(4L); + + when(courseRepository.findByUuid(course.getUuid())).thenReturn(Optional.of(course)); + when(userRepository.findByUsername(user.getUsername())).thenReturn(Optional.of(user)); + when(exerciseUserInfoRepository.save(any(ExerciseUserInfo.class))).thenReturn(eui); + when(courseRepository.save(any(Course.class))).thenReturn(course); + + Course returnedCourse = courseServiceImpl.joinCourseWithSharingCode(course.getUuid(), user.getUsername()); + + assertThat(returnedCourse).isEqualTo(course); + logger.info("Test joinCourseWithSharingCode_valid() ends."); + } + + @Test + public void getCourseInformationWithSharingCode_valid() throws CourseNotFoundException { + logger.info("Test getCourseInformationWithSharingCode_valid() begins."); + + Course expectedCourse = new Course("Spring Boot Course"); + + when(courseRepository.findByUuid(expectedCourse.getUuid())).thenReturn(Optional.of(expectedCourse)); + + Course actualCourse = courseServiceImpl.getCourseInformationWithSharingCode(expectedCourse.getUuid()); + + verify(courseRepository, times(1)).findByUuid(expectedCourse.getUuid()); + assertThat(actualCourse).isEqualTo(expectedCourse); + + logger.info("Test getCourseInformationWithSharingCode_valid() ends."); + } + + @Test + public void getExercise_valid() throws ExerciseNotFoundException { + Course course = new Course("Spring Boot Course"); + Long courseTestId = 1L; + course.setId(courseTestId); + Exercise expectedExercise = new Exercise(); + Long expectedExerciseId = 2L; + expectedExercise.setName("Spring Boot Exercise 1"); + expectedExercise.setId(expectedExerciseId); + expectedExercise.setCourse(course); + course.addExercise(expectedExercise); + when(exerciseRepository.findById(expectedExerciseId)).thenReturn(Optional.of(expectedExercise)); + + Exercise actualExercise = courseServiceImpl.getExercise(expectedExerciseId); + + assertThat(actualExercise).isEqualTo(expectedExercise); } @Test public void getUserCourses() throws UserNotFoundException { User teacher = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); Role teacherRole = new Role("ROLE_TEACHER"); - teacherRole.setId(3l); + teacherRole.setId(3L); Course course = new Course("Spring Boot Course"); - teacher.setId(4l); - course.setId(1l); + teacher.setId(4L); + course.setId(1L); teacher.addRole(studentRole); teacher.addRole(teacherRole); teacher.addCourse(course); @@ -349,7 +386,7 @@ public void getUserCourses() throws UserNotFoundException { Optional<User> userOpt = Optional.of(teacher); when(userRepository.findById(anyLong())).thenReturn(userOpt); - List<Course> courses = courseServiceImpl.getUserCourses(1l); + List<Course> courses = courseServiceImpl.getUserCourses(1L); assertThat(courses).contains(course); verify(userRepository, times(1)).findById(anyLong()); @@ -358,23 +395,24 @@ public void getUserCourses() throws UserNotFoundException { @Test public void addUserToCourse() throws UserNotFoundException, CourseNotFoundException, NotInCourseException { User newUser1 = new User("johndoejr@gmail.com", "johndoejr", "pass", "John", "Doe Jr"); - newUser1.setId(1l); + newUser1.setId(1L); User newUser2 = new User("johndoejr2@gmail.com", "johndoejr2", "pass", "John", "Doe Jr 2"); - newUser2.setId(5l); + newUser2.setId(5L); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); Role teacherRole = new Role("ROLE_TEACHER"); - teacherRole.setId(3l); + teacherRole.setId(3L); User teacher = new User("johndoe@gmail.com", "johndoe", "pass", "John", "Doe "); - teacher.setId(4l); + teacher.setId(4L); teacher.addRole(studentRole); teacher.addRole(teacherRole); newUser1.addRole(studentRole); newUser2.addRole(studentRole); Course course = new Course("Spring Boot Course"); - course.setId(5l); + course.setId(5L); course.addUserInCourse(teacher); - Exercise ex = new Exercise("Exercise 1", course); + Exercise ex = new Exercise("Exercise 1"); + ex.setCourse(course); course.addExercise(ex); Optional<User> userOpt1 = Optional.of(newUser1); Optional<User> userOpt2 = Optional.of(newUser2); @@ -387,8 +425,8 @@ public void addUserToCourse() throws UserNotFoundException, CourseNotFoundExcept when(courseRepository.save(any(Course.class))).thenReturn(expectedSavedCourse); when(exerciseUserInfoRepository.save(any(ExerciseUserInfo.class))).then(returnsFirstArg()); - Long[] ids = { 1l, 5l }; - Course savedCourse = courseServiceImpl.addUsersToCourse(5l, ids, "johndoe"); + Long[] ids = {1L, 5L}; + Course savedCourse = courseServiceImpl.addUsersToCourse(5L, ids, "johndoe"); assertThat(course.getUsersInCourse()).contains(newUser1); assertThat(course.getUsersInCourse()).contains(newUser2); @@ -402,21 +440,21 @@ public void addUserToCourse() throws UserNotFoundException, CourseNotFoundExcept @Test public void getUsersInCourse() throws CourseNotFoundException, NotInCourseException { User newUser1 = new User("johndoejr@gmail.com", "johndoejr", "pass", "John", "Doe Jr"); - newUser1.setId(1l); + newUser1.setId(1L); User newUser2 = new User("johndoejr2@gmail.com", "johndoejr2", "pass", "John", "Doe Jr 2"); - newUser2.setId(5l); + newUser2.setId(5L); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); Role teacherRole = new Role("ROLE_TEACHER"); - teacherRole.setId(3l); + teacherRole.setId(3L); User teacher = new User("johndoe@gmail.com", "johndoe", "pass", "John", "Doe "); - teacher.setId(4l); + teacher.setId(4L); teacher.addRole(studentRole); teacher.addRole(teacherRole); newUser1.addRole(studentRole); newUser2.addRole(studentRole); Course course = new Course("Spring Boot Course"); - course.setId(5l); + course.setId(5L); course.addUserInCourse(teacher); Optional<Course> courseOpt = Optional.of(course); course.addUserInCourse(teacher); @@ -424,7 +462,7 @@ public void getUsersInCourse() throws CourseNotFoundException, NotInCourseExcept course.addUserInCourse(newUser2); when(courseRepository.findById(anyLong())).thenReturn(courseOpt); - Set<User> users = courseServiceImpl.getUsersInCourse(4l, "johndoe"); + Set<User> users = courseServiceImpl.getUsersInCourse(4L, "johndoe"); assertThat(course.getUsersInCourse()).contains(newUser1); assertThat(course.getUsersInCourse()).contains(newUser2); @@ -435,21 +473,21 @@ public void getUsersInCourse() throws CourseNotFoundException, NotInCourseExcept @Test public void removeUsersFromCourse() throws Exception { User newUser1 = new User("johndoejr@gmail.com", "johndoejr", "pass", "John", "Doe Jr"); - newUser1.setId(1l); + newUser1.setId(1L); User newUser2 = new User("johndoejr2@gmail.com", "johndoejr2", "pass", "John", "Doe Jr 2"); - newUser2.setId(5l); + newUser2.setId(5L); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); Role teacherRole = new Role("ROLE_TEACHER"); - teacherRole.setId(3l); + teacherRole.setId(3L); User teacher = new User("johndoe@gmail.com", "johndoe", "pass", "John", "Doe "); - teacher.setId(4l); + teacher.setId(4L); teacher.addRole(studentRole); teacher.addRole(teacherRole); newUser1.addRole(studentRole); newUser2.addRole(studentRole); Course course = new Course("Spring Boot Course"); - course.setId(5l); + course.setId(5L); course.addUserInCourse(teacher); course.addUserInCourse(newUser1); course.addUserInCourse(newUser2); @@ -463,8 +501,8 @@ public void removeUsersFromCourse() throws Exception { when(courseRepository.findById(anyLong())).thenReturn(courseOpt); when(courseRepository.save(any(Course.class))).thenReturn(expectedSavedCourse); - Long[] ids = { 1l, 5l }; - Course savedCourse = courseServiceImpl.removeUsersFromCourse(5l, ids, "johndoe"); + Long[] ids = {1L, 5L}; + Course savedCourse = courseServiceImpl.removeUsersFromCourse(5L, ids, "johndoe"); assertThat(course.getUsersInCourse()).doesNotContain(newUser1); assertThat(course.getUsersInCourse()).doesNotContain(newUser2); @@ -478,20 +516,20 @@ public void removeUsersFromCourse() throws Exception { public void getCreator() throws CourseNotFoundException { logger.info("Test getCreator() begins."); User user = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); - user.setId(4l); + user.setId(4L); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); Role teacherRole = new Role("ROLE_TEACHER"); - teacherRole.setId(3l); + teacherRole.setId(3L); user.addRole(studentRole); user.addRole(teacherRole); Course course = new Course("Spring Boot Course"); - course.setId(1l); + course.setId(1L); course.setCreator(user); Optional<Course> courseOpt = Optional.of(course); when(courseRepository.findById(anyLong())).thenReturn(courseOpt); - User creatorFound = courseServiceImpl.getCreator(1l); + User creatorFound = courseServiceImpl.getCreator(1L); assertThat(creatorFound.getUsername()).isEqualTo("johndoe"); assertThat(course.getCreator()).isEqualTo(user); @@ -501,16 +539,16 @@ public void getCreator() throws CourseNotFoundException { @Test public void getCourseCode_valid() - throws CourseNotFoundException, NotInCourseException, NotCreatorException, UserNotFoundException { + throws CourseNotFoundException, NotInCourseException, UserNotFoundException { Course course = new Course("Spring Boot Course"); String courseCode = course.getUuid(); - Long courseTestId = 1l; - course.setId(1l); + Long courseTestId = 1L; + course.setId(1L); User teacher = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); Role teacherRole = new Role("ROLE_TEACHER"); - teacherRole.setId(3l); + teacherRole.setId(3L); teacher.addRole(studentRole); teacher.addRole(teacherRole); teacher.addCourse(course); @@ -529,14 +567,14 @@ public void getCourseCode_valid() @Test public void getExerciseCode_valid() - throws CourseNotFoundException, NotInCourseException, ExerciseNotFoundException, UserNotFoundException { + throws NotInCourseException, ExerciseNotFoundException, UserNotFoundException { User teacher = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); Role teacherRole = new Role("ROLE_TEACHER"); - teacherRole.setId(3l); + teacherRole.setId(3L); Course course = new Course("Spring Boot Course"); - course.setId(1l); + course.setId(1L); teacher.addRole(studentRole); teacher.addRole(teacherRole); teacher.addCourse(course); @@ -544,15 +582,15 @@ public void getExerciseCode_valid() Exercise exercise = new Exercise(); String exCode = exercise.getUuid(); exercise.setName("Spring Boot Exercise 1"); - exercise.setId(6l); + exercise.setId(6L); exercise.setCourse(course); course.addExercise(exercise); Optional<Exercise> exerciseOpt = Optional.of(exercise); - when(exerciseRepository.findById(6l)).thenReturn(exerciseOpt); + when(exerciseRepository.findById(6L)).thenReturn(exerciseOpt); - String exCodeByService = courseServiceImpl.getExerciseCode(6l, "johndoe"); + String exCodeByService = courseServiceImpl.getExerciseCode(6L, "johndoe"); assertThat(exCodeByService).isEqualTo(exCode); - verify(exerciseRepository, times(1)).findById(6l); + verify(exerciseRepository, times(1)).findById(6L); } } \ No newline at end of file diff --git a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/ExerciseFilesServiceImplTests.java b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/ExerciseFilesServiceImplTests.java index 8231f13e..33cc0e48 100644 --- a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/ExerciseFilesServiceImplTests.java +++ b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/ExerciseFilesServiceImplTests.java @@ -1,42 +1,13 @@ package com.vscode4teaching.vscode4teachingserver.servicetests; -import static org.assertj.core.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.AdditionalAnswers.returnsFirstArg; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; - -import com.vscode4teaching.vscode4teachingserver.model.Course; -import com.vscode4teaching.vscode4teachingserver.model.Exercise; -import com.vscode4teaching.vscode4teachingserver.model.ExerciseFile; -import com.vscode4teaching.vscode4teachingserver.model.ExerciseUserInfo; -import com.vscode4teaching.vscode4teachingserver.model.Role; -import com.vscode4teaching.vscode4teachingserver.model.User; +import com.vscode4teaching.vscode4teachingserver.model.*; import com.vscode4teaching.vscode4teachingserver.model.repositories.ExerciseFileRepository; import com.vscode4teaching.vscode4teachingserver.model.repositories.ExerciseRepository; import com.vscode4teaching.vscode4teachingserver.model.repositories.ExerciseUserInfoRepository; import com.vscode4teaching.vscode4teachingserver.model.repositories.UserRepository; -import com.vscode4teaching.vscode4teachingserver.services.exceptions.ExerciseFinishedException; -import com.vscode4teaching.vscode4teachingserver.services.exceptions.ExerciseNotFoundException; -import com.vscode4teaching.vscode4teachingserver.services.exceptions.NoTemplateException; -import com.vscode4teaching.vscode4teachingserver.services.exceptions.NotInCourseException; +import com.vscode4teaching.vscode4teachingserver.services.exceptions.*; import com.vscode4teaching.vscode4teachingserver.servicesimpl.ExerciseFilesServiceImpl; - +import com.vscode4teaching.vscode4teachingserver.servicesimpl.JWTUserDetailsService; import org.apache.tomcat.util.http.fileupload.FileUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -51,27 +22,35 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.web.multipart.MultipartFile; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.AdditionalAnswers.returnsFirstArg; +import static org.mockito.Mockito.*; + @ExtendWith(MockitoExtension.class) @TestPropertySource(locations = "classpath:test.properties") public class ExerciseFilesServiceImplTests { + private static final Logger logger = LoggerFactory.getLogger(ExerciseFilesServiceImplTests.class); @Mock private ExerciseRepository exerciseRepository; - @Mock private UserRepository userRepository; - @Mock private ExerciseFileRepository fileRepository; - @Mock private ExerciseUserInfoRepository exerciseUserInfoRepository; - @InjectMocks private ExerciseFilesServiceImpl filesService; - - private static final Logger logger = LoggerFactory.getLogger(ExerciseFilesServiceImplTests.class); - + @Mock + private JWTUserDetailsService userService; private Set<String> pathsSaved = new HashSet<>(); @BeforeEach @@ -90,19 +69,50 @@ public void cleanup() { } } + @Test + public void existsExerciseFilesForUser() throws NotInCourseException, ExerciseNotFoundException { + Course course = new Course("Spring Boot Course"); + course.setId(1L); + Exercise exercise = new Exercise("Spring Boot Exercise 1"); + Long exerciseId = 2L; + exercise.setId(exerciseId); + exercise.setCourse(course); + course.addExercise(exercise); + User user = new User("johndoejr@gmail.com", "johndoejr", "studentpassword", "John", "Doe Jr"); + user.addCourse(course); + course.addUserInCourse(user); + when(exerciseRepository.findById(exerciseId)).thenReturn(Optional.of(exercise)); + + Boolean actualExists = filesService.existsExerciseFilesForUser(exerciseId, user.getUsername()); + + assertThat(actualExists).isFalse(); + + + // Now files are added + ExerciseFile file1 = new ExerciseFile("file1", user); + ExerciseFile file2 = new ExerciseFile("file2", user); + ExerciseFile file3 = new ExerciseFile("file3", user); + List<ExerciseFile> expectedExerciseFiles = List.of(file1, file2, file3); + exercise.setUserFiles(expectedExerciseFiles); + + Boolean actualExists2 = filesService.existsExerciseFilesForUser(exerciseId, user.getUsername()); + + assertThat(actualExists2).isTrue(); + } + @Test public void getExerciseFiles_withTemplate() throws Exception { User student = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); - student.setId(3l); + student.setId(3L); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); student.addRole(studentRole); Course course = new Course("Spring Boot Course"); - course.setId(4l); + course.setId(4L); course.addUserInCourse(student); Exercise exercise = new Exercise(); exercise.setName("Exercise 1"); - exercise.setId(1l); + exercise.setId(1L); course.addExercise(exercise); exercise.setCourse(course); ExerciseFile file1 = new ExerciseFile("v4t-course-test/spring-boot-course/exercise_1_1/template/ej1.txt"); @@ -112,7 +122,7 @@ public void getExerciseFiles_withTemplate() throws Exception { Optional<Exercise> exOpt = Optional.of(exercise); when(exerciseRepository.findById(anyLong())).thenReturn(exOpt); - Map<Exercise, List<File>> filesMap = filesService.getExerciseFiles(1l, "johndoe"); + Map<Exercise, List<File>> filesMap = filesService.getExerciseFiles(1L, "johndoe"); List<File> files = filesMap.values().stream().findFirst().get(); assertThat(files.size()).isEqualTo(2); @@ -121,22 +131,54 @@ public void getExerciseFiles_withTemplate() throws Exception { assertThat(files.get(1).getPath().replace("\\", "/")) .isEqualTo("v4t-course-test/spring-boot-course/exercise_1_1/template/ej2.txt"); verify(exerciseRepository, times(1)).findById(anyLong()); + } + + @Test + public void getExerciseSolution() throws Exception { + User student = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); + student.setId(3L); + Role studentRole = new Role("ROLE_STUDENT"); + studentRole.setId(2L); + student.addRole(studentRole); + Course course = new Course("Spring Boot Course"); + course.setId(4L); + course.addUserInCourse(student); + Exercise exercise = new Exercise(); + exercise.setName("Exercise 1"); + exercise.setId(1L); + course.addExercise(exercise); + exercise.setCourse(course); + ExerciseFile file1 = new ExerciseFile("v4t-course-test/spring-boot-course/exercise_1_1/solution/ej1.txt"); + ExerciseFile file2 = new ExerciseFile("v4t-course-test/spring-boot-course/exercise_1_1/solution/ej2.txt"); + exercise.addFileToSolution(file1); + exercise.addFileToSolution(file2); + Optional<Exercise> exOpt = Optional.of(exercise); + when(exerciseRepository.findById(anyLong())).thenReturn(exOpt); + Map<Exercise, List<File>> filesMap = filesService.getExerciseSolution(1L, "johndoe"); + List<File> files = filesMap.values().stream().findFirst().get(); + + assertThat(files.size()).isEqualTo(2); + assertThat(files.get(0).getPath().replace("\\", "/")) + .isEqualTo("v4t-course-test/spring-boot-course/exercise_1_1/solution/ej1.txt"); + assertThat(files.get(1).getPath().replace("\\", "/")) + .isEqualTo("v4t-course-test/spring-boot-course/exercise_1_1/solution/ej2.txt"); + verify(exerciseRepository, times(1)).findById(anyLong()); } @Test public void getExerciseFiles_withUserFiles() throws Exception { User student = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); - student.setId(3l); + student.setId(3L); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); student.addRole(studentRole); Course course = new Course("Spring Boot Course"); - course.setId(4l); + course.setId(4L); course.addUserInCourse(student); Exercise exercise = new Exercise(); exercise.setName("Exercise 1"); - exercise.setId(1l); + exercise.setId(1L); course.addExercise(exercise); exercise.setCourse(course); ExerciseFile file1 = new ExerciseFile("v4t-course-test/spring-boot-course/exercise_1_1/template/ej1.txt"); @@ -152,7 +194,7 @@ public void getExerciseFiles_withUserFiles() throws Exception { Optional<Exercise> exOpt = Optional.of(exercise); when(exerciseRepository.findById(anyLong())).thenReturn(exOpt); - Map<Exercise, List<File>> filesMap = filesService.getExerciseFiles(1l, "johndoe"); + Map<Exercise, List<File>> filesMap = filesService.getExerciseFiles(1L, "johndoe"); List<File> files = filesMap.values().stream().findFirst().get(); logger.info(files.get(0).getAbsolutePath()); @@ -166,24 +208,24 @@ public void getExerciseFiles_withUserFiles() throws Exception { } @Test - public void getExerciseFiles_noTemplate() throws Exception { + public void getExerciseFiles_noTemplate() { User student = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); - student.setId(3l); + student.setId(3L); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); student.addRole(studentRole); Course course = new Course("Spring Boot Course"); - course.setId(4l); + course.setId(4L); course.addUserInCourse(student); Exercise exercise = new Exercise(); exercise.setName("Exercise 1"); - exercise.setId(1l); + exercise.setId(1L); course.addExercise(exercise); exercise.setCourse(course); Optional<Exercise> exOpt = Optional.of(exercise); when(exerciseRepository.findById(anyLong())).thenReturn(exOpt); - assertThrows(NoTemplateException.class, () -> filesService.getExerciseFiles(1l, "johndoe")); + assertThrows(NoTemplateException.class, () -> filesService.getExerciseFiles(1L, "johndoe")); verify(exerciseRepository, times(1)).findById(anyLong()); } @@ -193,7 +235,7 @@ private List<File> runSaveExerciseFiles() throws Exception { MultipartFile mockFile = new MockMultipartFile("file", file.getName(), "application/zip", new FileInputStream(file)); - Map<Exercise, List<File>> filesMap = filesService.saveExerciseFiles(1l, mockFile, "johndoe"); + Map<Exercise, List<File>> filesMap = filesService.saveExerciseFiles(1L, mockFile, "johndoe"); return filesMap.values().stream().findFirst().get(); } @@ -233,16 +275,16 @@ private void exerciseUserInfoAsserts(ExerciseUserInfo eui) { private ExerciseUserInfo setupSaveExerciseFiles() { User student = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); - student.setId(3l); + student.setId(3L); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); student.addRole(studentRole); Course course = new Course("Spring Boot Course"); - course.setId(4l); + course.setId(4L); course.addUserInCourse(student); Exercise exercise = new Exercise(); exercise.setName("Exercise 1"); - exercise.setId(1l); + exercise.setId(1L); course.addExercise(exercise); exercise.setCourse(course); ExerciseUserInfo eui = new ExerciseUserInfo(exercise, student); @@ -309,20 +351,20 @@ public void saveExerciseFilesIgnoreDuplicates() throws Exception { @Test public void saveExerciseFilesFinishedError() throws Exception { User student = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); - student.setId(3l); + student.setId(3L); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); student.addRole(studentRole); Course course = new Course("Spring Boot Course"); - course.setId(4l); + course.setId(4L); course.addUserInCourse(student); Exercise exercise = new Exercise(); exercise.setName("Exercise 1"); - exercise.setId(1l); + exercise.setId(1L); course.addExercise(exercise); exercise.setCourse(course); ExerciseUserInfo eui = new ExerciseUserInfo(exercise, student); - eui.setStatus(1); + eui.setStatus(ExerciseStatus.FINISHED); when(exerciseUserInfoRepository.findByExercise_IdAndUser_Username(anyLong(), anyString())) .thenReturn(Optional.of(eui)); // Get files @@ -331,7 +373,7 @@ public void saveExerciseFilesFinishedError() throws Exception { new FileInputStream(file)); ExerciseFinishedException e = assertThrows(ExerciseFinishedException.class, - () -> filesService.saveExerciseFiles(1l, mockFile, "johndoe")); + () -> filesService.saveExerciseFiles(1L, mockFile, "johndoe")); assertThat(e.getMessage()).isEqualToIgnoringWhitespace("Exercise is marked as finished: 1"); verify(exerciseUserInfoRepository, times(1)).findByExercise_IdAndUser_Username(anyLong(), anyString()); @@ -340,19 +382,19 @@ public void saveExerciseFilesFinishedError() throws Exception { @Test public void saveExerciseTemplate() throws Exception { User teacher = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); - teacher.setId(3l); + teacher.setId(3L); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); Role teacherRole = new Role("ROLE_TEACHER"); - studentRole.setId(10l); + studentRole.setId(10L); teacher.addRole(studentRole); teacher.addRole(teacherRole); Course course = new Course("Spring Boot Course"); - course.setId(4l); + course.setId(4L); course.addUserInCourse(teacher); Exercise exercise = new Exercise(); exercise.setName("Exercise 1"); - exercise.setId(1l); + exercise.setId(1L); course.addExercise(exercise); exercise.setCourse(course); when(exerciseRepository.findById(anyLong())).thenReturn(Optional.of(exercise)); @@ -364,7 +406,7 @@ public void saveExerciseTemplate() throws Exception { MultipartFile mockFile = new MockMultipartFile("file", file.getName(), "application/zip", new FileInputStream(file)); - Map<Exercise, List<File>> filesMap = filesService.saveExerciseTemplate(1l, mockFile, "johndoe"); + Map<Exercise, List<File>> filesMap = filesService.saveExerciseTemplate(1L, mockFile, "johndoe"); List<File> savedFiles = filesMap.values().stream().findFirst().get(); assertThat(Files.exists(Paths.get("null/"))).isTrue(); @@ -396,19 +438,78 @@ public void saveExerciseTemplate() throws Exception { verify(exerciseRepository, times(1)).save(any(Exercise.class)); } + @Test + public void saveExerciseSolution() throws Exception { + User teacher = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); + teacher.setId(3L); + Role studentRole = new Role("ROLE_STUDENT"); + studentRole.setId(2L); + Role teacherRole = new Role("ROLE_TEACHER"); + studentRole.setId(10L); + teacher.addRole(studentRole); + teacher.addRole(teacherRole); + Course course = new Course("Spring Boot Course"); + course.setId(4L); + course.addUserInCourse(teacher); + Exercise exercise = new Exercise(); + exercise.setName("Exercise 1"); + exercise.setId(1L); + course.addExercise(exercise); + exercise.setCourse(course); + when(exerciseRepository.findById(anyLong())).thenReturn(Optional.of(exercise)); + when(userRepository.findByUsername(anyString())).thenReturn(Optional.of(teacher)); + when(fileRepository.save(any(ExerciseFile.class))).then(returnsFirstArg()); + when(exerciseRepository.save(any(Exercise.class))).then(returnsFirstArg()); + // Get files + File file = Paths.get("src/test/java/com/vscode4teaching/vscode4teachingserver/files", "exs.zip").toFile(); + MultipartFile mockFile = new MockMultipartFile("file", file.getName(), "application/zip", + new FileInputStream(file)); + + Map<Exercise, List<File>> filesMap = filesService.saveExerciseSolution(1L, mockFile, "johndoe"); + List<File> savedFiles = filesMap.values().stream().findFirst().get(); + + assertThat(Files.exists(Paths.get("null/"))).isTrue(); + assertThat(Files.exists(Paths.get("null/spring_boot_course_4/exercise_1_1/"))).isTrue(); + assertThat(Files.exists(Paths.get("null/spring_boot_course_4/exercise_1_1/solution"))).isTrue(); + assertThat(Files.exists(Paths.get("null/spring_boot_course_4/exercise_1_1/solution/ex1.html"))).isTrue(); + assertThat(Files.exists(Paths.get("null/spring_boot_course_4/exercise_1_1/solution/ex2.html"))).isTrue(); + assertThat(Files.exists(Paths.get("null/spring_boot_course_4/exercise_1_1/solution/ex3/ex3.html"))).isTrue(); + assertThat(Files.readAllLines(Paths.get("null/spring_boot_course_4/exercise_1_1/solution/ex1.html"))) + .contains("<html>Exercise 1</html>"); + assertThat(Files.readAllLines(Paths.get("null/spring_boot_course_4/exercise_1_1/solution/ex2.html"))) + .contains("<html>Exercise 2</html>"); + assertThat(Files.readAllLines(Paths.get("null/spring_boot_course_4/exercise_1_1/solution/ex3/ex3.html"))) + .contains("<html>Exercise 3</html>"); + assertThat(exercise.getSolution()).hasSize(3); + assertThat(exercise.getSolution().get(0).getPath()).isEqualToIgnoringCase( + Paths.get("null/spring_boot_course_4/exercise_1_1/solution/ex1.html").toAbsolutePath().toString()); + assertThat(exercise.getSolution().get(1).getPath()).isEqualToIgnoringCase( + Paths.get("null/spring_boot_course_4/exercise_1_1/solution/ex2.html").toAbsolutePath().toString()); + assertThat(exercise.getSolution().get(2).getPath()).isEqualToIgnoringCase( + Paths.get("null/spring_boot_course_4/exercise_1_1/solution/ex3/ex3.html").toAbsolutePath().toString()); + assertThat(savedFiles.size()).isEqualTo(3); + assertThat(savedFiles.get(0).getAbsolutePath()).isEqualToIgnoringCase(exercise.getSolution().get(0).getPath()); + assertThat(savedFiles.get(1).getAbsolutePath()).isEqualToIgnoringCase(exercise.getSolution().get(1).getPath()); + assertThat(savedFiles.get(2).getAbsolutePath()).isEqualToIgnoringCase(exercise.getSolution().get(2).getPath()); + verify(exerciseRepository, times(1)).findById(anyLong()); + verify(userRepository, times(1)).findByUsername(anyString()); + verify(fileRepository, times(3)).save(any(ExerciseFile.class)); + verify(exerciseRepository, times(1)).save(any(Exercise.class)); + } + @Test public void getTemplate() throws Exception { User student = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); - student.setId(3l); + student.setId(3L); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); student.addRole(studentRole); Course course = new Course("Spring Boot Course"); - course.setId(4l); + course.setId(4L); course.addUserInCourse(student); Exercise exercise = new Exercise(); exercise.setName("Exercise 1"); - exercise.setId(1l); + exercise.setId(1L); course.addExercise(exercise); exercise.setCourse(course); ExerciseFile file1 = new ExerciseFile("v4t-course-test/spring-boot-course/exercise_1_1/template/ej1.txt"); @@ -418,7 +519,7 @@ public void getTemplate() throws Exception { Optional<Exercise> exOpt = Optional.of(exercise); when(exerciseRepository.findById(anyLong())).thenReturn(exOpt); - Map<Exercise, List<File>> filesMap = filesService.getExerciseTemplate(1l, "johndoe"); + Map<Exercise, List<File>> filesMap = filesService.getExerciseTemplate(1L, "johndoe"); List<File> files = filesMap.values().stream().findFirst().get(); assertThat(files.size()).isEqualTo(2); @@ -432,31 +533,31 @@ public void getTemplate() throws Exception { @Test public void getAllStudentExercises() throws ExerciseNotFoundException, NotInCourseException { User teacher = new User("johndoe@gmail.com", "johndoe", "pass", "John", "Doe"); - teacher.setId(3l); + teacher.setId(3L); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); Role teacherRole = new Role("ROLE_TEACHER"); - teacherRole.setId(10l); + teacherRole.setId(10L); teacher.addRole(studentRole); teacher.addRole(teacherRole); User student1 = new User("johndoejr1@gmail.com", "johndoejr1", "pass", "John", "Doe Jr 1"); - student1.setId(11l); + student1.setId(11L); student1.addRole(studentRole); User student2 = new User("johndoejr2@gmail.com", "johndoejr2", "pass", "John", "Doe Jr 2"); - student2.setId(12l); + student2.setId(12L); student2.addRole(studentRole); User student3 = new User("johndoejr3@gmail.com", "johndoejr3", "pass", "John", "Doe Jr 3"); - student3.setId(13l); + student3.setId(13L); student3.addRole(studentRole); Course course = new Course("Spring Boot Course"); - course.setId(4l); + course.setId(4L); course.addUserInCourse(teacher); course.addUserInCourse(student1); course.addUserInCourse(student2); course.addUserInCourse(student3); Exercise exercise = new Exercise(); exercise.setName("Exercise 1"); - exercise.setId(1l); + exercise.setId(1L); course.addExercise(exercise); exercise.setCourse(course); ExerciseFile file1 = new ExerciseFile("v4t-course-test/spring-boot-course/exercise_1_1/template/ej1.txt"); @@ -496,7 +597,7 @@ public void getAllStudentExercises() throws ExerciseNotFoundException, NotInCour Optional<Exercise> exOpt = Optional.of(exercise); when(exerciseRepository.findById(anyLong())).thenReturn(exOpt); - Map<Exercise, List<File>> filesMap = filesService.getAllStudentsFiles(1l, "johndoe"); + Map<Exercise, List<File>> filesMap = filesService.getAllStudentsFiles(1L, "johndoe"); List<File> files = filesMap.values().stream().findFirst().get(); assertThat(files.size()).isEqualTo(6); @@ -516,54 +617,55 @@ public void getAllStudentExercises() throws ExerciseNotFoundException, NotInCour } @Test - public void getFileIds() throws ExerciseNotFoundException { + public void getFileIds() throws NotFoundException { User teacher = new User("johndoe@gmail.com", "johndoe", "pass", "John", "Doe"); - teacher.setId(3l); + teacher.setId(3L); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); Role teacherRole = new Role("ROLE_TEACHER"); - teacherRole.setId(10l); + teacherRole.setId(10L); teacher.addRole(studentRole); teacher.addRole(teacherRole); User student1 = new User("johndoejr1@gmail.com", "johndoejr1", "pass", "John", "Doe Jr 1"); - student1.setId(11l); + student1.setId(11L); student1.addRole(studentRole); User student2 = new User("johndoejr2@gmail.com", "johndoejr2", "pass", "John", "Doe Jr 2"); - student2.setId(12l); + student2.setId(12L); student2.addRole(studentRole); User student3 = new User("johndoejr3@gmail.com", "johndoejr3", "pass", "John", "Doe Jr 3"); - student3.setId(13l); + student3.setId(13L); student3.addRole(studentRole); Course course = new Course("Spring Boot Course"); - course.setId(4l); + course.setId(4L); course.addUserInCourse(teacher); course.addUserInCourse(student1); course.addUserInCourse(student2); course.addUserInCourse(student3); Exercise exercise = new Exercise(); exercise.setName("Exercise 1"); - exercise.setId(1l); + exercise.setId(1L); course.addExercise(exercise); exercise.setCourse(course); ExerciseFile ex1 = new ExerciseFile("student_11" + File.separator + "test1"); - ex1.setId(101l); + ex1.setId(101L); ex1.setOwner(student1); ExerciseFile ex2 = new ExerciseFile("student_12" + File.separator + "test2"); - ex2.setId(102l); + ex2.setId(102L); ex2.setOwner(student2); ExerciseFile ex3 = new ExerciseFile("student_13" + File.separator + "test3"); - ex3.setId(103l); + ex3.setId(103L); ex3.setOwner(student3); exercise.addUserFile(ex1); exercise.addUserFile(ex2); exercise.addUserFile(ex3); when(exerciseRepository.findById(anyLong())).thenReturn(Optional.of(exercise)); + when(userService.findByUsername("johndoejr1")).thenReturn(student1); - List<ExerciseFile> files = filesService.getFileIdsByExerciseAndOwner(1l, "johndoejr1"); + List<ExerciseFile> files = filesService.getFileIdsByExerciseAndId(1L, "johndoejr1"); verify(exerciseRepository, times(1)).findById(anyLong()); assertThat(files.size()).isEqualTo(1); - assertThat(files.get(0).getId()).isEqualTo(101l); + assertThat(files.get(0).getId()).isEqualTo(101L); assertThat(files.get(0).getPath()).isEqualTo("test1"); } } \ No newline at end of file diff --git a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/ExerciseInfoServiceImplTests.java b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/ExerciseInfoServiceImplTests.java index d157e409..6a7f7ee1 100644 --- a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/ExerciseInfoServiceImplTests.java +++ b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/ExerciseInfoServiceImplTests.java @@ -1,27 +1,12 @@ package com.vscode4teaching.vscode4teachingserver.servicetests; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import com.vscode4teaching.vscode4teachingserver.model.Course; -import com.vscode4teaching.vscode4teachingserver.model.Exercise; -import com.vscode4teaching.vscode4teachingserver.model.ExerciseUserInfo; -import com.vscode4teaching.vscode4teachingserver.model.Role; -import com.vscode4teaching.vscode4teachingserver.model.User; +import com.vscode4teaching.vscode4teachingserver.model.*; import com.vscode4teaching.vscode4teachingserver.model.repositories.ExerciseUserInfoRepository; import com.vscode4teaching.vscode4teachingserver.services.exceptions.ExerciseNotFoundException; import com.vscode4teaching.vscode4teachingserver.services.exceptions.NotFoundException; import com.vscode4teaching.vscode4teachingserver.services.exceptions.NotInCourseException; import com.vscode4teaching.vscode4teachingserver.services.websockets.SocketHandler; import com.vscode4teaching.vscode4teachingserver.servicesimpl.ExerciseInfoServiceImpl; - import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -29,10 +14,12 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.context.TestPropertySource; +import java.util.*; + import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.AdditionalAnswers.returnsFirstArg; -import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @TestPropertySource(locations = "classpath:test.properties") @@ -47,16 +34,17 @@ public class ExerciseInfoServiceImplTests { private ExerciseInfoServiceImpl exerciseInfoService; @Test - public void getExerciseUserInfo_existing() throws NotFoundException { + public void getExerciseUserInfo_withExerciseIdAndUsername() throws NotFoundException { Course course = new Course("Spring Boot Course"); - Exercise exercise = new Exercise("Exercise 1", course); - Long exerciseId = 2l; + Exercise exercise = new Exercise("Exercise 1"); + Long exerciseId = 2L; exercise.setId(exerciseId); + exercise.setCourse(course); String username = "johndoe"; Role studentRole = new Role("ROLE_STUDENT"); User user = new User("johndoe@john.com", username, "johndoeuser", "John", "Doe", studentRole); ExerciseUserInfo eui = new ExerciseUserInfo(exercise, user); - eui.setStatus(1); + eui.setStatus(ExerciseStatus.FINISHED); Optional<ExerciseUserInfo> euiOpt = Optional.of(eui); when(exerciseUserInfoRepository.findByExercise_IdAndUser_Username(exerciseId, username)).thenReturn(euiOpt); @@ -64,13 +52,35 @@ public void getExerciseUserInfo_existing() throws NotFoundException { assertThat(savedEui.getExercise()).isEqualTo(exercise); assertThat(savedEui.getUser()).isEqualTo(user); - assertThat(savedEui.getStatus() == 1).isTrue(); + assertThat(savedEui.getStatus() == ExerciseStatus.FINISHED).isTrue(); verify(exerciseUserInfoRepository, times(1)).findByExercise_IdAndUser_Username(exerciseId, username); } @Test - public void getExerciseUserInfo_exception() throws NotFoundException { - Long exerciseId = 2l; + public void getExerciseUserInfo_withEuiId() throws NotFoundException { + Course course = new Course("Spring Boot Course"); + Exercise exercise = new Exercise("Exercise 1"); + exercise.setId(2L); + exercise.setCourse(course); + Role studentRole = new Role("ROLE_STUDENT"); + User user = new User("johndoe@john.com", "johndoe", "johndoeuser", "John", "Doe", studentRole); + ExerciseUserInfo eui = new ExerciseUserInfo(exercise, user); + Long euiId = 3L; + eui.setId(euiId); + eui.setStatus(ExerciseStatus.FINISHED); + when(exerciseUserInfoRepository.findById(euiId)).thenReturn(Optional.of(eui)); + + ExerciseUserInfo savedEui = exerciseInfoService.getExerciseUserInfo(euiId); + + assertThat(savedEui.getExercise()).isEqualTo(exercise); + assertThat(savedEui.getUser()).isEqualTo(user); + assertThat(savedEui.getStatus() == ExerciseStatus.FINISHED).isTrue(); + verify(exerciseUserInfoRepository, times(1)).findById(euiId); + } + + @Test + public void getExerciseUserInfo_exception() { + Long exerciseId = 2L; String username = "johndoe"; when(exerciseUserInfoRepository.findByExercise_IdAndUser_Username(exerciseId, username)) .thenReturn(Optional.empty()); @@ -82,14 +92,15 @@ public void getExerciseUserInfo_exception() throws NotFoundException { @Test public void updateExerciseUserInfo_valid() throws NotFoundException { Course course = new Course("Spring Boot Course"); - Exercise exercise = new Exercise("Exercise 1", course); - Long exerciseId = 2l; + Exercise exercise = new Exercise("Exercise 1"); + Long exerciseId = 2L; exercise.setId(exerciseId); + exercise.setCourse(course); String username = "johndoe"; Role studentRole = new Role("ROLE_STUDENT"); User user = new User("johndoe@john.com", username, "johndoeuser", "John", "Doe", studentRole); ExerciseUserInfo eui = new ExerciseUserInfo(exercise, user); - eui.setStatus(0); + eui.setStatus(ExerciseStatus.NOT_STARTED); Set<String> euiOldModifiedFiles = new HashSet<>(); euiOldModifiedFiles.add("/old/file.txt"); eui.setModifiedFiles(euiOldModifiedFiles); @@ -106,11 +117,11 @@ public void updateExerciseUserInfo_valid() throws NotFoundException { teacherSet.add(creator); when(exerciseUserInfoRepository.findByExercise_IdAndUser_Username(exerciseId, username)).thenReturn(euiOpt); when(exerciseUserInfoRepository.save(any(ExerciseUserInfo.class))).then(returnsFirstArg()); - ExerciseUserInfo savedEui = exerciseInfoService.updateExerciseUserInfo(exerciseId, username, 1, euiNewModifiedFiles); + ExerciseUserInfo savedEui = exerciseInfoService.updateExerciseUserInfo(exerciseId, username, ExerciseStatus.FINISHED, euiNewModifiedFiles); assertThat(savedEui.getExercise()).isEqualTo(exercise); assertThat(savedEui.getUser()).isEqualTo(user); - assertThat(savedEui.getStatus() == 1).isTrue(); + assertThat(savedEui.getStatus() == ExerciseStatus.FINISHED).isTrue(); assertThat(savedEui.getModifiedFiles()).size().isEqualTo(2); assertThat(savedEui.getModifiedFiles()).contains("/modified/file.txt"); assertThat(savedEui.getModifiedFiles()).contains("/old/file.txt"); @@ -122,25 +133,26 @@ public void updateExerciseUserInfo_valid() throws NotFoundException { @Test public void updateExerciseUserInfo_valid_no_file() throws NotFoundException { Course course = new Course("Spring Boot Course"); - Exercise exercise = new Exercise("Exercise 1", course); - Long exerciseId = 2l; + Exercise exercise = new Exercise("Exercise 1"); + Long exerciseId = 2L; exercise.setId(exerciseId); + exercise.setCourse(course); String username = "johndoe"; Role studentRole = new Role("ROLE_STUDENT"); User user = new User("johndoe@john.com", username, "johndoeuser", "John", "Doe", studentRole); ExerciseUserInfo eui = new ExerciseUserInfo(exercise, user); - eui.setStatus(0); + eui.setStatus(ExerciseStatus.NOT_STARTED); Set<String> euiOldModifiedFiles = new HashSet<>(); euiOldModifiedFiles.add("/old/file.txt"); eui.setModifiedFiles(euiOldModifiedFiles); Optional<ExerciseUserInfo> euiOpt = Optional.of(eui); when(exerciseUserInfoRepository.findByExercise_IdAndUser_Username(exerciseId, username)).thenReturn(euiOpt); when(exerciseUserInfoRepository.save(any(ExerciseUserInfo.class))).then(returnsFirstArg()); - ExerciseUserInfo savedEui = exerciseInfoService.updateExerciseUserInfo(exerciseId, username, 0, null); + ExerciseUserInfo savedEui = exerciseInfoService.updateExerciseUserInfo(exerciseId, username, ExerciseStatus.NOT_STARTED, null); assertThat(savedEui.getExercise()).isEqualTo(exercise); assertThat(savedEui.getUser()).isEqualTo(user); - assertThat(savedEui.getStatus() == 0).isTrue(); + assertThat(savedEui.getStatus() == ExerciseStatus.NOT_STARTED).isTrue(); assertThat(savedEui.getModifiedFiles()).size().isEqualTo(1); assertThat(savedEui.getModifiedFiles()).contains("/old/file.txt"); verify(exerciseUserInfoRepository, times(1)).findByExercise_IdAndUser_Username(exerciseId, username); @@ -151,14 +163,15 @@ public void updateExerciseUserInfo_valid_no_file() throws NotFoundException { public void getStudentUserInfo() throws ExerciseNotFoundException, NotInCourseException { // Set up courses and exercises Course course = new Course("Spring Boot Course"); - Exercise exercise = new Exercise("Exercise 1", course); - exercise.setId(10l); + Exercise exercise = new Exercise("Exercise 1"); + exercise.setId(10L); + exercise.setCourse(course); // Set up users User teacher = new User("johndoe@gmail.com", "johndoe", "pass", "John", "Doe"); Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); Role teacherRole = new Role("ROLE_TEACHER"); - teacherRole.setId(3l); + teacherRole.setId(3L); teacher.addRole(studentRole); teacher.addRole(teacherRole); teacher.addCourse(course); @@ -175,21 +188,21 @@ public void getStudentUserInfo() throws ExerciseNotFoundException, NotInCourseEx ExerciseUserInfo euiTeacher = new ExerciseUserInfo(exercise, teacher); ExerciseUserInfo euiStudent1 = new ExerciseUserInfo(exercise, student1); ExerciseUserInfo euiStudent2 = new ExerciseUserInfo(exercise, student2); - euiStudent2.setStatus(1); + euiStudent2.setStatus(ExerciseStatus.FINISHED); List<ExerciseUserInfo> expectedList = new ArrayList<>(3); expectedList.add(euiTeacher); expectedList.add(euiStudent1); expectedList.add(euiStudent2); - when(exerciseUserInfoRepository.findByExercise_Id(10l)).thenReturn(expectedList); + when(exerciseUserInfoRepository.findByExercise_Id(10L)).thenReturn(expectedList); - List<ExerciseUserInfo> returnedEuis = exerciseInfoService.getAllStudentExerciseUserInfo(10l, "johndoe"); + List<ExerciseUserInfo> returnedEuis = exerciseInfoService.getAllStudentExerciseUserInfo(10L, "johndoe"); - verify(exerciseUserInfoRepository, times(1)).findByExercise_Id(10l); + verify(exerciseUserInfoRepository, times(1)).findByExercise_Id(10L); assertThat(returnedEuis).hasSize(2); assertThat(returnedEuis.contains(euiTeacher)).isFalse(); assertThat(returnedEuis.get(0)).isEqualTo(euiStudent1); assertThat(returnedEuis.get(1)).isEqualTo(euiStudent2); - assertThat(returnedEuis.get(0).getStatus() == 1).isFalse(); - assertThat(returnedEuis.get(1).getStatus() == 1).isTrue(); + assertThat(returnedEuis.get(0).getStatus() == ExerciseStatus.FINISHED).isFalse(); + assertThat(returnedEuis.get(1).getStatus() == ExerciseStatus.FINISHED).isTrue(); } } \ No newline at end of file diff --git a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/UserServiceImplTests.java b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/UserServiceImplTests.java index 7420e022..11544540 100644 --- a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/UserServiceImplTests.java +++ b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/UserServiceImplTests.java @@ -1,17 +1,11 @@ package com.vscode4teaching.vscode4teachingserver.servicetests; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.when; - -import java.util.Optional; - import com.vscode4teaching.vscode4teachingserver.model.Role; import com.vscode4teaching.vscode4teachingserver.model.User; import com.vscode4teaching.vscode4teachingserver.model.repositories.RoleRepository; import com.vscode4teaching.vscode4teachingserver.model.repositories.UserRepository; +import com.vscode4teaching.vscode4teachingserver.services.exceptions.NotFoundException; import com.vscode4teaching.vscode4teachingserver.servicesimpl.JWTUserDetailsService; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -22,8 +16,14 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.test.context.TestPropertySource; -import static org.assertj.core.api.Assertions.*; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @TestPropertySource(locations = "classpath:test.properties") @@ -45,11 +45,31 @@ public class UserServiceImplTests { @BeforeEach public void setup() { user = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); - user.setId(1l); + user.setId(1L); studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); + studentRole.setId(2L); teacherRole = new Role("ROLE_TEACHER"); - teacherRole.setId(3l); + teacherRole.setId(3L); + } + + @Test + public void findByUsername() throws NotFoundException { + when(userRepository.findByUsername(anyString())).thenReturn(Optional.of(user)); + User expectedUser = userService.findByUsername("johndoe"); + assertThat(user).isEqualTo(expectedUser); + } + + @Test + public void findAll() { + User user1 = new User("johndoejr@gmail.com", "johndoejr", "studentpassword", "John", "Doe Jr"); + User user2 = new User("johndoejr2@gmail.com", "johndoejr2", "studentpassword2", "John", "Doe Jr 2"); + User user3 = new User("johndoejr3@gmail.com", "johndoejr3", "studentpassword3", "John", "Doe Jr 3"); + List<User> expectedUsers = List.of(user1, user2, user3); + when(userRepository.findAll()).thenReturn(expectedUsers); + + List<User> actualUsers = userService.findAll(); + + assertThat(actualUsers).isEqualTo(expectedUsers); } @Test diff --git a/vscode4teaching-webapp/karma.conf.js b/vscode4teaching-webapp/karma.conf.js deleted file mode 100644 index f789085a..00000000 --- a/vscode4teaching-webapp/karma.conf.js +++ /dev/null @@ -1,50 +0,0 @@ -// Karma configuration file, see link for more information -// https://karma-runner.github.io/1.0/config/configuration-file.html - -module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('karma-coverage'), - require('@angular-devkit/build-angular/plugins/karma') - ], - client: { - jasmine: { - // you can add configuration options for Jasmine here - // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html - // for example, you can disable the random execution with `random: false` - // or set a specific seed with `seed: 4321` - }, - clearContext: false // leave Jasmine Spec Runner output visible in browser - }, - jasmineHtmlReporter: { - suppressAll: true // removes the duplicated traces - }, - coverageReporter: { - dir: require('path').join(__dirname, './coverage/vscode4teaching-webapp'), - subdir: '.', - reporters: [ - { type: 'html' }, - { type: 'text-summary' } - ] - }, - reporters: ['progress', 'kjhtml'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['ChromeHeadlessCI'], - customLaunchers: { - ChromeHeadlessCI: { - base: 'ChromeHeadless', - flags: ['--no-sandbox'] - } - }, - singleRun: false, - restartOnFileChange: true - }); -}; diff --git a/vscode4teaching-webapp/package-lock.json b/vscode4teaching-webapp/package-lock.json index 1bbc7e53..a1fa9116 100644 --- a/vscode4teaching-webapp/package-lock.json +++ b/vscode4teaching-webapp/package-lock.json @@ -1,43 +1,42 @@ { "name": "vscode4teaching-webapp", - "version": "2.1.4", + "version": "2.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "vscode4teaching-webapp", - "version": "2.1.4", - "dependencies": { - "@angular/animations": "^14.1.0", - "@angular/common": "^14.1.0", - "@angular/compiler": "^14.1.0", - "@angular/core": "^14.1.0", - "@angular/forms": "^14.1.0", - "@angular/platform-browser": "^14.1.0", - "@angular/platform-browser-dynamic": "^14.1.0", - "@angular/router": "^14.1.0", - "@fortawesome/fontawesome-free": "^6.1.2", - "bootstrap": "^5.2.0", + "version": "2.2.0", + "dependencies": { + "@angular/animations": "^14.2.7", + "@angular/common": "^14.2.7", + "@angular/compiler": "^14.2.7", + "@angular/core": "^14.2.7", + "@angular/forms": "^14.2.7", + "@angular/platform-browser": "^14.2.7", + "@angular/platform-browser-dynamic": "^14.2.7", + "@angular/router": "^14.2.7", + "@fortawesome/fontawesome-free": "^6.2.0", + "bootstrap": "^5.2.2", "ngx-logger": "^5.0.11", - "rxjs": "^7.5.6", + "rxjs": "^7.5.7", "tslib": "^2.3.0", - "zone.js": "^0.11.7" + "zone.js": "^0.11.8" }, "devDependencies": { - "@angular-devkit/build-angular": "^14.1.0", - "@angular/cli": "^14.1.0", - "@angular/compiler-cli": "^14.1.0", - "@types/jasmine": "~3.10.0", + "@angular-devkit/build-angular": "^14.2.6", + "@angular/cli": "^14.2.6", + "@angular/compiler-cli": "^14.2.7", "@types/node": "^12.20.55", - "jasmine-core": "~3.10.0", - "karma": "^6.3.18", - "karma-chrome-launcher": "^3.1.1", - "karma-coverage": "~2.1.0", - "karma-jasmine": "^5.0.0", - "karma-jasmine-html-reporter": "~1.7.0", - "typescript": "^4.7.4" + "typescript": "^4.8.4" } }, + "node_modules/@adobe/css-tools": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.0.1.tgz", + "integrity": "sha512-+u76oB43nOHrF4DDWRLWDCtci7f3QJoEBigemIdIeTi1ODqjx6Tad9NCVnPRwewWlKkVab5PlK8DCtPTyX7S8g==", + "dev": true + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -52,12 +51,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1401.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1401.0.tgz", - "integrity": "sha512-dHgP2/5EXkJpdf6Y1QHQX2RP8xTli/CFZH3uNnTh+EuAib/kwu+Z6K3UttZWB5VGhAF1u/xf97Vly/UkXvjKAg==", + "version": "0.1402.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1402.6.tgz", + "integrity": "sha512-qTmPBD7fBXBtlSapGLUEcJvRuL/O556zCFFpH3kSlzPNTYxi2falBjGY+4aG+078RXT1vVZtFsvRTart6VbhAg==", "dev": true, "dependencies": { - "@angular-devkit/core": "14.1.0", + "@angular-devkit/core": "14.2.6", "rxjs": "6.6.7" }, "engines": { @@ -85,35 +84,35 @@ "dev": true }, "node_modules/@angular-devkit/build-angular": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-14.1.0.tgz", - "integrity": "sha512-AtecSuDEPLYd3p7uFVKpoA0XNcq+NvVYFJK8h90BG+IRZtzEm7ZJeYdohXVeVfTO5GvpNFN1XoHxR5rxiXeBhg==", + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-14.2.6.tgz", + "integrity": "sha512-XtaUwb3aZ8S0vl0y9bmbdFOH0KQCQ778twFH+ZfHW2BcPYtQz2Cy2rcVKXBQ850RyC0GxgMPfco6OGQndPpizg==", "dev": true, "dependencies": { "@ampproject/remapping": "2.2.0", - "@angular-devkit/architect": "0.1401.0", - "@angular-devkit/build-webpack": "0.1401.0", - "@angular-devkit/core": "14.1.0", - "@babel/core": "7.18.6", - "@babel/generator": "7.18.7", + "@angular-devkit/architect": "0.1402.6", + "@angular-devkit/build-webpack": "0.1402.6", + "@angular-devkit/core": "14.2.6", + "@babel/core": "7.18.10", + "@babel/generator": "7.18.12", "@babel/helper-annotate-as-pure": "7.18.6", - "@babel/plugin-proposal-async-generator-functions": "7.18.6", + "@babel/plugin-proposal-async-generator-functions": "7.18.10", "@babel/plugin-transform-async-to-generator": "7.18.6", - "@babel/plugin-transform-runtime": "7.18.6", - "@babel/preset-env": "7.18.6", - "@babel/runtime": "7.18.6", - "@babel/template": "7.18.6", + "@babel/plugin-transform-runtime": "7.18.10", + "@babel/preset-env": "7.18.10", + "@babel/runtime": "7.18.9", + "@babel/template": "7.18.10", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "14.1.0", + "@ngtools/webpack": "14.2.6", "ansi-colors": "4.1.3", "babel-loader": "8.2.5", "babel-plugin-istanbul": "6.1.1", "browserslist": "^4.9.1", - "cacache": "16.1.1", + "cacache": "16.1.2", "copy-webpack-plugin": "11.0.0", "critters": "0.0.16", "css-loader": "6.7.1", - "esbuild-wasm": "0.14.49", + "esbuild-wasm": "0.15.5", "glob": "8.0.3", "https-proxy-agent": "5.0.1", "inquirer": "8.2.4", @@ -129,27 +128,27 @@ "ora": "5.4.1", "parse5-html-rewriting-stream": "6.0.1", "piscina": "3.2.0", - "postcss": "8.4.14", - "postcss-import": "14.1.0", + "postcss": "8.4.16", + "postcss-import": "15.0.0", "postcss-loader": "7.0.1", - "postcss-preset-env": "7.7.2", + "postcss-preset-env": "7.8.0", "regenerator-runtime": "0.13.9", "resolve-url-loader": "5.0.0", "rxjs": "6.6.7", - "sass": "1.53.0", + "sass": "1.54.4", "sass-loader": "13.0.2", "semver": "7.3.7", "source-map-loader": "4.0.0", "source-map-support": "0.5.21", - "stylus": "0.58.1", + "stylus": "0.59.0", "stylus-loader": "7.0.0", "terser": "5.14.2", "text-table": "0.2.0", "tree-kill": "1.2.2", "tslib": "2.4.0", - "webpack": "5.73.0", + "webpack": "5.74.0", "webpack-dev-middleware": "5.3.3", - "webpack-dev-server": "4.9.3", + "webpack-dev-server": "4.11.0", "webpack-merge": "5.8.0", "webpack-subresource-integrity": "5.1.0" }, @@ -159,7 +158,7 @@ "yarn": ">= 1.13.0" }, "optionalDependencies": { - "esbuild": "0.14.49" + "esbuild": "0.15.5" }, "peerDependencies": { "@angular/compiler-cli": "^14.0.0", @@ -169,7 +168,7 @@ "ng-packagr": "^14.0.0", "protractor": "^7.0.0", "tailwindcss": "^2.0.0 || ^3.0.0", - "typescript": ">=4.6.2 <4.8" + "typescript": ">=4.6.2 <4.9" }, "peerDependenciesMeta": { "@angular/localize": { @@ -211,12 +210,12 @@ "dev": true }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1401.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1401.0.tgz", - "integrity": "sha512-jKfnHal09mVnEapmNrAHXL/00LfafmfEUtlOPzQMgGJL7MWCeMcFthsbcOnGuzUerbiiquRk/KmLTERYjH+ZrQ==", + "version": "0.1402.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1402.6.tgz", + "integrity": "sha512-gKsDxQ9pze0N1qDM0kdM4FfwpkjSOb0bQzqjZi7wTfrh/WGIQMCjG9CRwWT+Z289ZKaTpcQDPsDtOSo5QpKNDg==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1401.0", + "@angular-devkit/architect": "0.1402.6", "rxjs": "6.6.7" }, "engines": { @@ -248,9 +247,9 @@ "dev": true }, "node_modules/@angular-devkit/core": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.1.0.tgz", - "integrity": "sha512-Y2d/+nFmjjY4eatc3cwdDDAnpnhG3KTX2OVW7dXSUxW3eY5e3vdMlVUbFiKwvwAshlrJy85Y6RMvZSBN4VrpnA==", + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.2.6.tgz", + "integrity": "sha512-qtRSdRm/h7C3ya04PJTDgQXV6mM8Y4RakANX1GTSXetCf9AVSxg74NJX76DWUgiHT4JiPYnJgJU6Hr/L0H6JOQ==", "dev": true, "dependencies": { "ajv": "8.11.0", @@ -292,12 +291,12 @@ "dev": true }, "node_modules/@angular-devkit/schematics": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-14.1.0.tgz", - "integrity": "sha512-5QC01k9eznuQSiqxijKhVkAEmA8sioYuLhBzyffaPszSySH8kPMNxhAc8zJhBTNLumbS6iDaGkSqTQl5Kv9fOw==", + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-14.2.6.tgz", + "integrity": "sha512-mSFtc4M49mWrYsgJx/P6bA6SzXb8SeZqmppKRMoEQxiXI1bwFdGLNWzAmzEsGvS96h/nPIaOfcX5cKJSp++4FA==", "dev": true, "dependencies": { - "@angular-devkit/core": "14.1.0", + "@angular-devkit/core": "14.2.6", "jsonc-parser": "3.1.0", "magic-string": "0.26.2", "ora": "5.4.1", @@ -328,9 +327,9 @@ "dev": true }, "node_modules/@angular/animations": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-14.1.0.tgz", - "integrity": "sha512-OhEXi1u/M4QyltDCxSqo7YzF7ELgNDWNqbbM7vtWIcrc4c+Yiu1GXhW/GQRosF3WAuQVfdQzEI0VTeNoo98Kvw==", + "version": "14.2.7", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-14.2.7.tgz", + "integrity": "sha512-4vI22Pa56FkE7ydxZwEd7RHwIjfyE5MnbgB2fWEQ3obnul8GnQT7OHWiPgzV57SDqOCWZyWdLm9xuOnZVdypxQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -338,19 +337,19 @@ "node": "^14.15.0 || >=16.10.0" }, "peerDependencies": { - "@angular/core": "14.1.0" + "@angular/core": "14.2.7" } }, "node_modules/@angular/cli": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-14.1.0.tgz", - "integrity": "sha512-W/t2PkGHu9r87po1ZXQRYU81VtjzNMuGsP5tmoW1pGuibK7Kj+25G+jrXK/WADTi+pjTMXHNXYn8PlMNAIrZ/w==", + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-14.2.6.tgz", + "integrity": "sha512-8tXpe3htfZY8a+Am4nluVcztMFD5wnx4edGEDkkOiqkrUzbCtX4AyEBjUFldsYKZXbRFU46xEfM6jBnLOjxDZQ==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1401.0", - "@angular-devkit/core": "14.1.0", - "@angular-devkit/schematics": "14.1.0", - "@schematics/angular": "14.1.0", + "@angular-devkit/architect": "0.1402.6", + "@angular-devkit/core": "14.2.6", + "@angular-devkit/schematics": "14.2.6", + "@schematics/angular": "14.2.6", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "debug": "4.3.4", @@ -361,7 +360,7 @@ "npm-pick-manifest": "7.0.1", "open": "8.4.0", "ora": "5.4.1", - "pacote": "13.6.1", + "pacote": "13.6.2", "resolve": "1.22.1", "semver": "7.3.7", "symbol-observable": "4.0.0", @@ -378,9 +377,9 @@ } }, "node_modules/@angular/common": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-14.1.0.tgz", - "integrity": "sha512-leethDtLbA3qySaOEBUto602DF0qH1maK9u2zHncrUFOpnHAYUEd7N9MFMdIYASurTnwOSglEoIDCML94qzImQ==", + "version": "14.2.7", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-14.2.7.tgz", + "integrity": "sha512-vfydeB8urLzhRnZev/1Zm87k9jWlNfhSTk09yUnqvzcORfd3xOkcei0qc1xdIHCTEMyTREC+umsYHDmlEpZsVw==", "dependencies": { "tslib": "^2.3.0" }, @@ -388,14 +387,14 @@ "node": "^14.15.0 || >=16.10.0" }, "peerDependencies": { - "@angular/core": "14.1.0", + "@angular/core": "14.2.7", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-14.1.0.tgz", - "integrity": "sha512-aLbtpFDF3fp/DOEsWSdpszmoNZAb0To/zoKhHVmEReuUKkMtlPNd3+e6wkR2vrvR/cWgbKwdb7RQ1IQtGDu74A==", + "version": "14.2.7", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-14.2.7.tgz", + "integrity": "sha512-2I8hZVM/tfUi06B6VuWgf5hWu0tgNlMCEZ1Ed4NEDkqJj+gs2l6kNVUf+FxI6hHMZTFkJPXOPx3pI5Hea5CxEQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -403,7 +402,7 @@ "node": "^14.15.0 || >=16.10.0" }, "peerDependencies": { - "@angular/core": "14.1.0" + "@angular/core": "14.2.7" }, "peerDependenciesMeta": { "@angular/core": { @@ -412,9 +411,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-14.1.0.tgz", - "integrity": "sha512-llJkDnv0+riTdRPdOJv/FToz4X9ZO1URnalW+tIe2RyfOzkEqM+VLD/x+3cVgnsaFKuoPxIjZEkMoppGwVB4kg==", + "version": "14.2.7", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-14.2.7.tgz", + "integrity": "sha512-q6AQo91jc+Pd1PnWWxJq07IXr1yipq0MW3Uok5akEctbTsw4AT5y9wfPj6g/o/CkAz0kbL55QrmtyIWN5LMGdg==", "dev": true, "dependencies": { "@babel/core": "^7.17.2", @@ -437,14 +436,14 @@ "node": "^14.15.0 || >=16.10.0" }, "peerDependencies": { - "@angular/compiler": "14.1.0", - "typescript": ">=4.6.2 <4.8" + "@angular/compiler": "14.2.7", + "typescript": ">=4.6.2 <4.9" } }, "node_modules/@angular/core": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-14.1.0.tgz", - "integrity": "sha512-3quEsHmQifJOQ2oij5K+cjGjmhsKsyZI1+OTHWNZ6IXeuYviZv4U/Cui9fUJ1RN3CZxH3NzWB3gB/5qYFQfOgg==", + "version": "14.2.7", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-14.2.7.tgz", + "integrity": "sha512-9u2eeKS90YPh2b0pK5LKFSxKfLIzHnzkIKQFh6bEPGj43Fl2v8CwiVJu1CAKo1Or4qBY8zspSowM6S1kgGwfeg==", "dependencies": { "tslib": "^2.3.0" }, @@ -457,9 +456,9 @@ } }, "node_modules/@angular/forms": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-14.1.0.tgz", - "integrity": "sha512-y7VQ2t+/ASEjzt8zXg4y5b03lMSPHmnhy4XzjDT14ZFrALaSxyhkSqoBfAksPkTeKmsFMnP/VgLboRsE8TLs0Q==", + "version": "14.2.7", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-14.2.7.tgz", + "integrity": "sha512-tEhCIE4mzlD2S0bYE+4e4RG7lOIeRZjjcKw0nlpwE0AneM03gzFlQvbHLDi3qeu8Kc7XkF6C3FJQI/Q7nMiARg==", "dependencies": { "tslib": "^2.3.0" }, @@ -467,16 +466,16 @@ "node": "^14.15.0 || >=16.10.0" }, "peerDependencies": { - "@angular/common": "14.1.0", - "@angular/core": "14.1.0", - "@angular/platform-browser": "14.1.0", + "@angular/common": "14.2.7", + "@angular/core": "14.2.7", + "@angular/platform-browser": "14.2.7", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/platform-browser": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-14.1.0.tgz", - "integrity": "sha512-axNXUSqxsP0QSdNskd1pFo2uMo1UNoFaSAB02eDWwLkWQ1pWel+T78HiQY2bNeI3elgzjwPTT4vCCDQKNVTNig==", + "version": "14.2.7", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-14.2.7.tgz", + "integrity": "sha512-Hcn64kppozH5WlX/rkZoCGZyFFkLs0a4+rWen6uaZVxDWbas/PqR/a2LQerdS0Rn65/x+0l0w23u8TN0PnQPVA==", "dependencies": { "tslib": "^2.3.0" }, @@ -484,9 +483,9 @@ "node": "^14.15.0 || >=16.10.0" }, "peerDependencies": { - "@angular/animations": "14.1.0", - "@angular/common": "14.1.0", - "@angular/core": "14.1.0" + "@angular/animations": "14.2.7", + "@angular/common": "14.2.7", + "@angular/core": "14.2.7" }, "peerDependenciesMeta": { "@angular/animations": { @@ -495,9 +494,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-14.1.0.tgz", - "integrity": "sha512-0Lxz3HJ9qTOyMTp5Qud2tycP7wqe+tnHOSUqDywrbNRozTKGX0z3i+l0KMku3BtUbuMi3tJomqV914/dtbCvIw==", + "version": "14.2.7", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-14.2.7.tgz", + "integrity": "sha512-P3c4fXH0+RDlL9uzuU5Xea5OQ3ozmoWawFtf4faH7fD3deHlNmrwROBXAlHkYRjbGcAiuxpEx6NjBGdmYWPaKg==", "dependencies": { "tslib": "^2.3.0" }, @@ -505,16 +504,16 @@ "node": "^14.15.0 || >=16.10.0" }, "peerDependencies": { - "@angular/common": "14.1.0", - "@angular/compiler": "14.1.0", - "@angular/core": "14.1.0", - "@angular/platform-browser": "14.1.0" + "@angular/common": "14.2.7", + "@angular/compiler": "14.2.7", + "@angular/core": "14.2.7", + "@angular/platform-browser": "14.2.7" } }, "node_modules/@angular/router": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-14.1.0.tgz", - "integrity": "sha512-WBC1E+d9RS8vy57zJ6LVtWT3AM12mEHY7SCMBRJNBcrmBYJwojxeV8IVkUoW4Ds910gG/w3LjIN0eNHg5qRtNA==", + "version": "14.2.7", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-14.2.7.tgz", + "integrity": "sha512-ZWdXXv0sCXVxWdHmCPDAy/TFT5v9JMlPp18Mmi9J8X3KeL/h6YVTWeJ0YMAOchv8D8UL02HiijnVyUp8rv5Qvw==", "dependencies": { "tslib": "^2.3.0" }, @@ -522,9 +521,9 @@ "node": "^14.15.0 || >=16.10.0" }, "peerDependencies": { - "@angular/common": "14.1.0", - "@angular/core": "14.1.0", - "@angular/platform-browser": "14.1.0", + "@angular/common": "14.2.7", + "@angular/core": "14.2.7", + "@angular/platform-browser": "14.2.7", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -556,21 +555,21 @@ } }, "node_modules/@babel/core": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.6.tgz", - "integrity": "sha512-cQbWBpxcbbs/IUredIPkHiAGULLV8iwgNRMFzvbhEXISp4f3rUUXE5+TIw6KwUWUR3DwyI6gmBRnmAtYaWehwQ==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.10.tgz", + "integrity": "sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.18.6", - "@babel/helper-compilation-targets": "^7.18.6", - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helpers": "^7.18.6", - "@babel/parser": "^7.18.6", - "@babel/template": "^7.18.6", - "@babel/traverse": "^7.18.6", - "@babel/types": "^7.18.6", + "@babel/generator": "^7.18.10", + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-module-transforms": "^7.18.9", + "@babel/helpers": "^7.18.9", + "@babel/parser": "^7.18.10", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.18.10", + "@babel/types": "^7.18.10", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -595,12 +594,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.18.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.7.tgz", - "integrity": "sha512-shck+7VLlY72a2w9c3zYWuE1pwOKEiQHV7GTUbSnhyl5eu3i04t30tBY82ZRWrDfo3gkakCFtevExnxbkf2a3A==", + "version": "7.18.12", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.12.tgz", + "integrity": "sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg==", "dev": true, "dependencies": { - "@babel/types": "^7.18.7", + "@babel/types": "^7.18.10", "@jridgewell/gen-mapping": "^0.3.2", "jsesc": "^2.5.1" }, @@ -675,9 +674,9 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.9.tgz", - "integrity": "sha512-WvypNAYaVh23QcjpMR24CwZY2Nz6hqdOcFdPbNpV56hL5H6KiFheO7Xm1aPdlLQ7d5emYZX7VZwPp9x3z+2opw==", + "version": "7.18.13", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.13.tgz", + "integrity": "sha512-hDvXp+QYxSRL+23mpAlSGxHMDyIGChm0/AwTfTAAK5Ufe40nCsyNdaYCGuK91phn/fVu9kqayImRDkvNAgdrsA==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.18.6", @@ -917,6 +916,15 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz", + "integrity": "sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", @@ -936,29 +944,29 @@ } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.18.9.tgz", - "integrity": "sha512-cG2ru3TRAL6a60tfQflpEfs4ldiPwF6YW3zfJiRgmoFVIaC1vGnBBgatfec+ZUziPHkHSaXAuEck3Cdkf3eRpQ==", + "version": "7.18.11", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.18.11.tgz", + "integrity": "sha512-oBUlbv+rjZLh2Ks9SKi4aL7eKaAXBWleHzU89mP0G6BMUlRxSckk9tSIkgDGydhgFxHuGSlBQZfnaD47oBEB7w==", "dev": true, "dependencies": { "@babel/helper-function-name": "^7.18.9", - "@babel/template": "^7.18.6", - "@babel/traverse": "^7.18.9", - "@babel/types": "^7.18.9" + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.18.11", + "@babel/types": "^7.18.10" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.18.6.tgz", - "integrity": "sha512-vzSiiqbQOghPngUYt/zWGvK3LAsPhz55vc9XNN0xAl2gV4ieShI2OQli5duxWHD+72PZPTKAcfcZDE1Cwc5zsQ==", + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.18.9.tgz", + "integrity": "sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ==", "dev": true, "dependencies": { "@babel/template": "^7.18.6", - "@babel/traverse": "^7.18.6", - "@babel/types": "^7.18.6" + "@babel/traverse": "^7.18.9", + "@babel/types": "^7.18.9" }, "engines": { "node": ">=6.9.0" @@ -979,9 +987,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.9.tgz", - "integrity": "sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg==", + "version": "7.18.13", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.13.tgz", + "integrity": "sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -1023,14 +1031,14 @@ } }, "node_modules/@babel/plugin-proposal-async-generator-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.18.6.tgz", - "integrity": "sha512-WAz4R9bvozx4qwf74M+sfqPMKfSqwM0phxPTR6iJIi8robgzXwkEgmeJG1gEKhm6sDqT/U9aV3lfcqybIpev8w==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.18.10.tgz", + "integrity": "sha512-1mFuY2TOsR1hxbjCo4QL+qlIjV07p4H4EUYw2J/WCqsvFV6V9X9z9YhXbWndc/4fw+hYGlDT7egYxliMp5O6Ew==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-remap-async-to-generator": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-remap-async-to-generator": "^7.18.9", "@babel/plugin-syntax-async-generators": "^7.8.4" }, "engines": { @@ -1563,9 +1571,9 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.18.9.tgz", - "integrity": "sha512-p5VCYNddPLkZTq4XymQIaIfZNJwT9YsjkPOhkVEqt6QIpQFZVM9IltqqYpOEkJoN1DPznmxUDyZ5CTZs/ZCuHA==", + "version": "7.18.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.18.13.tgz", + "integrity": "sha512-TodpQ29XekIsex2A+YJPj5ax2plkGa8YYY6mFjCohk/IG9IY42Rtuj1FuDeemfg2ipxIFLzPeA83SIBnlhSIow==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.18.9" @@ -1865,16 +1873,16 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.18.6.tgz", - "integrity": "sha512-8uRHk9ZmRSnWqUgyae249EJZ94b0yAGLBIqzZzl+0iEdbno55Pmlt/32JZsHwXD9k/uZj18Aqqk35wBX4CBTXA==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.18.10.tgz", + "integrity": "sha512-q5mMeYAdfEbpBAgzl7tBre/la3LeCxmDO1+wMXRdPWbcoMjR3GiXlCLk7JBZVVye0bqTGNMbt0yYVXX1B1jEWQ==", "dev": true, "dependencies": { "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "babel-plugin-polyfill-corejs2": "^0.3.1", - "babel-plugin-polyfill-corejs3": "^0.5.2", - "babel-plugin-polyfill-regenerator": "^0.3.1", + "@babel/helper-plugin-utils": "^7.18.9", + "babel-plugin-polyfill-corejs2": "^0.3.2", + "babel-plugin-polyfill-corejs3": "^0.5.3", + "babel-plugin-polyfill-regenerator": "^0.4.0", "semver": "^6.3.0" }, "engines": { @@ -1970,12 +1978,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.6.tgz", - "integrity": "sha512-XNRwQUXYMP7VLuy54cr/KS/WeL3AZeORhrmeZ7iewgu+X2eBqmpaLI/hzqr9ZxCeUoq0ASK4GUzSM0BDhZkLFw==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", + "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.18.9" }, "engines": { "node": ">=6.9.0" @@ -2001,29 +2009,29 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.18.6.tgz", - "integrity": "sha512-WrthhuIIYKrEFAwttYzgRNQ5hULGmwTj+D6l7Zdfsv5M7IWV/OZbUfbeL++Qrzx1nVJwWROIFhCHRYQV4xbPNw==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.18.10.tgz", + "integrity": "sha512-wVxs1yjFdW3Z/XkNfXKoblxoHgbtUF7/l3PvvP4m02Qz9TZ6uZGxRVYjSQeR87oQmHco9zWitW5J82DJ7sCjvA==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.18.6", - "@babel/helper-compilation-targets": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", + "@babel/compat-data": "^7.18.8", + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9", "@babel/helper-validator-option": "^7.18.6", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.6", - "@babel/plugin-proposal-async-generator-functions": "^7.18.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-async-generator-functions": "^7.18.10", "@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/plugin-proposal-class-static-block": "^7.18.6", "@babel/plugin-proposal-dynamic-import": "^7.18.6", - "@babel/plugin-proposal-export-namespace-from": "^7.18.6", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", "@babel/plugin-proposal-json-strings": "^7.18.6", - "@babel/plugin-proposal-logical-assignment-operators": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", "@babel/plugin-proposal-numeric-separator": "^7.18.6", - "@babel/plugin-proposal-object-rest-spread": "^7.18.6", + "@babel/plugin-proposal-object-rest-spread": "^7.18.9", "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", - "@babel/plugin-proposal-optional-chaining": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.18.9", "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.18.6", "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", @@ -2045,40 +2053,40 @@ "@babel/plugin-transform-arrow-functions": "^7.18.6", "@babel/plugin-transform-async-to-generator": "^7.18.6", "@babel/plugin-transform-block-scoped-functions": "^7.18.6", - "@babel/plugin-transform-block-scoping": "^7.18.6", - "@babel/plugin-transform-classes": "^7.18.6", - "@babel/plugin-transform-computed-properties": "^7.18.6", - "@babel/plugin-transform-destructuring": "^7.18.6", + "@babel/plugin-transform-block-scoping": "^7.18.9", + "@babel/plugin-transform-classes": "^7.18.9", + "@babel/plugin-transform-computed-properties": "^7.18.9", + "@babel/plugin-transform-destructuring": "^7.18.9", "@babel/plugin-transform-dotall-regex": "^7.18.6", - "@babel/plugin-transform-duplicate-keys": "^7.18.6", + "@babel/plugin-transform-duplicate-keys": "^7.18.9", "@babel/plugin-transform-exponentiation-operator": "^7.18.6", - "@babel/plugin-transform-for-of": "^7.18.6", - "@babel/plugin-transform-function-name": "^7.18.6", - "@babel/plugin-transform-literals": "^7.18.6", + "@babel/plugin-transform-for-of": "^7.18.8", + "@babel/plugin-transform-function-name": "^7.18.9", + "@babel/plugin-transform-literals": "^7.18.9", "@babel/plugin-transform-member-expression-literals": "^7.18.6", "@babel/plugin-transform-modules-amd": "^7.18.6", "@babel/plugin-transform-modules-commonjs": "^7.18.6", - "@babel/plugin-transform-modules-systemjs": "^7.18.6", + "@babel/plugin-transform-modules-systemjs": "^7.18.9", "@babel/plugin-transform-modules-umd": "^7.18.6", "@babel/plugin-transform-named-capturing-groups-regex": "^7.18.6", "@babel/plugin-transform-new-target": "^7.18.6", "@babel/plugin-transform-object-super": "^7.18.6", - "@babel/plugin-transform-parameters": "^7.18.6", + "@babel/plugin-transform-parameters": "^7.18.8", "@babel/plugin-transform-property-literals": "^7.18.6", "@babel/plugin-transform-regenerator": "^7.18.6", "@babel/plugin-transform-reserved-words": "^7.18.6", "@babel/plugin-transform-shorthand-properties": "^7.18.6", - "@babel/plugin-transform-spread": "^7.18.6", + "@babel/plugin-transform-spread": "^7.18.9", "@babel/plugin-transform-sticky-regex": "^7.18.6", - "@babel/plugin-transform-template-literals": "^7.18.6", - "@babel/plugin-transform-typeof-symbol": "^7.18.6", - "@babel/plugin-transform-unicode-escapes": "^7.18.6", + "@babel/plugin-transform-template-literals": "^7.18.9", + "@babel/plugin-transform-typeof-symbol": "^7.18.9", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", "@babel/plugin-transform-unicode-regex": "^7.18.6", "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.18.6", - "babel-plugin-polyfill-corejs2": "^0.3.1", - "babel-plugin-polyfill-corejs3": "^0.5.2", - "babel-plugin-polyfill-regenerator": "^0.3.1", + "@babel/types": "^7.18.10", + "babel-plugin-polyfill-corejs2": "^0.3.2", + "babel-plugin-polyfill-corejs3": "^0.5.3", + "babel-plugin-polyfill-regenerator": "^0.4.0", "core-js-compat": "^3.22.1", "semver": "^6.3.0" }, @@ -2115,9 +2123,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.6.tgz", - "integrity": "sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==", + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz", + "integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==", "dev": true, "dependencies": { "regenerator-runtime": "^0.13.4" @@ -2127,33 +2135,33 @@ } }, "node_modules/@babel/template": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.6.tgz", - "integrity": "sha512-JoDWzPe+wgBsTTgdnIma3iHNFC7YVJoPssVBDjiHfNlyt4YcunDtcDOUmfVDfCK5MfdsaIoX9PkijPhjH3nYUw==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", + "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.6", - "@babel/types": "^7.18.6" + "@babel/parser": "^7.18.10", + "@babel/types": "^7.18.10" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.9.tgz", - "integrity": "sha512-LcPAnujXGwBgv3/WHv01pHtb2tihcyW1XuL9wd7jqh1Z8AQkTd+QVjMrMijrln0T7ED3UXLIy36P9Ao7W75rYg==", + "version": "7.18.13", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.13.tgz", + "integrity": "sha512-N6kt9X1jRMLPxxxPYWi7tgvJRH/rtoU+dbKAPDM44RFHiMH8igdsaSBgFeskhSl/kLWLDUvIh1RXCrTmg0/zvA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.18.9", + "@babel/generator": "^7.18.13", "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-function-name": "^7.18.9", "@babel/helper-hoist-variables": "^7.18.6", "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.18.9", - "@babel/types": "^7.18.9", + "@babel/parser": "^7.18.13", + "@babel/types": "^7.18.13", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -2162,12 +2170,12 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.9.tgz", - "integrity": "sha512-wt5Naw6lJrL1/SGkipMiFxJjtyczUWTP38deiP1PO60HsBjDeKk08CGC3S8iVuvf0FmTdgKwU1KIXzSKL1G0Ug==", + "version": "7.18.13", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.13.tgz", + "integrity": "sha512-CkPg8ySSPuHTYPJYo7IRALdqyjM9HCbt/3uOBEFbzyGVP6Mn8bwFPB0jX6982JVNBlYzM1nnPkfjuXSOPtQeEQ==", "dev": true, "dependencies": { - "@babel/types": "^7.18.9", + "@babel/types": "^7.18.13", "@jridgewell/gen-mapping": "^0.3.2", "jsesc": "^2.5.1" }, @@ -2190,11 +2198,12 @@ } }, "node_modules/@babel/types": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.9.tgz", - "integrity": "sha512-WwMLAg2MvJmt/rKEVQBBhIVffMmnilX4oe0sRe7iPOHIGsqpruFHHdrfj4O1CMMtgMtCU4oPafZjDPCRgO57Wg==", + "version": "7.18.13", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.13.tgz", + "integrity": "sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ==", "dev": true, "dependencies": { + "@babel/helper-string-parser": "^7.18.10", "@babel/helper-validator-identifier": "^7.18.6", "to-fast-properties": "^2.0.0" }, @@ -2207,6 +2216,8 @@ "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=0.1.90" } @@ -2329,6 +2340,25 @@ "postcss": "^8.2" } }, + "node_modules/@csstools/postcss-nested-calc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz", + "integrity": "sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, "node_modules/@csstools/postcss-normalize-display-values": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz", @@ -2402,6 +2432,25 @@ "postcss": "^8.2" } }, + "node_modules/@csstools/postcss-text-decoration-shorthand": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz", + "integrity": "sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, "node_modules/@csstools/postcss-trigonometric-functions": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz", @@ -2463,10 +2512,26 @@ "node": ">=10.0.0" } }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.5.tgz", + "integrity": "sha512-UHkDFCfSGTuXq08oQltXxSZmH1TXyWsL+4QhZDWvvLl6mEJQqk3u7/wq1LjhrrAXYIllaTtRSzUXl4Olkf2J8A==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@fortawesome/fontawesome-free": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.1.2.tgz", - "integrity": "sha512-XwWADtfdSN73/udaFm+1mnGIj/ShDZNFMe/PRoqv3FhQ4GNI2PUN70yFTPsjq65Lw2C9i4TG5/hTbxXIXVCiqQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.2.0.tgz", + "integrity": "sha512-CNR7qRIfCwWHNN7FnKUniva94edPdyQzil/zCwk3v6k4R6rR2Fr8i4s3PM7n/lyfPA6Zfko9z5WDzFxG9SW1uQ==", "hasInstallScript": true, "engines": { "node": ">=6" @@ -2581,9 +2646,9 @@ "dev": true }, "node_modules/@ngtools/webpack": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.1.0.tgz", - "integrity": "sha512-d4U6ymDCXckVgfjYEv1Wjzd78ZSm0NKgq8mN6FdlrCupg02LPIODjeKyNr4c4zwMAOJeHkVNEZ+USoDEK3XSsw==", + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.6.tgz", + "integrity": "sha512-HdfoHLGPzyP135BOlvTQcpeWisVfiH0u40YNTBVK3QAsrLnY17e2QG5BWBOrVYipRu1975cZtTC9rPjcCY8aLQ==", "dev": true, "engines": { "node": "^14.15.0 || >=16.10.0", @@ -2592,7 +2657,7 @@ }, "peerDependencies": { "@angular/compiler-cli": "^14.0.0", - "typescript": ">=4.6.2 <4.8", + "typescript": ">=4.6.2 <4.9", "webpack": "^5.54.0" } }, @@ -2632,9 +2697,9 @@ } }, "node_modules/@npmcli/fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.1.tgz", - "integrity": "sha512-1Q0uzx6c/NVNGszePbr5Gc2riSU1zLpNlo/1YWntH+eaPmMgBssAW0qXofCVkpdj3ce4swZtlDYQu+NKiYcptg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", "dev": true, "dependencies": { "@gar/promisify": "^1.1.3", @@ -2645,9 +2710,9 @@ } }, "node_modules/@npmcli/git": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-3.0.1.tgz", - "integrity": "sha512-UU85F/T+F1oVn3IsB/L6k9zXIMpXBuUBE25QDH0SsURwT6IOBqkC7M16uqo2vVZIyji3X1K4XH9luip7YekH1A==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-3.0.2.tgz", + "integrity": "sha512-CAcd08y3DWBJqJDpfuVL0uijlq5oaXaOJEKHKc4wqrjd00gkvTZB+nFuLn+doOOKddaQS9JfqtNoFCO2LCvA3w==", "dev": true, "dependencies": { "@npmcli/promise-spawn": "^3.0.0", @@ -2664,21 +2729,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/@npmcli/git/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@npmcli/installed-package-contents": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-1.0.7.tgz", @@ -2696,9 +2746,9 @@ } }, "node_modules/@npmcli/move-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.0.tgz", - "integrity": "sha512-UR6D5f4KEGWJV6BGPH3Qb2EtgH+t+1XQ1Tt85c7qicN6cezzuHPdZwwAxqZr4JLtnQu0LZsTza/5gmNmSl8XLg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", "dev": true, "dependencies": { "mkdirp": "^1.0.4", @@ -2730,9 +2780,9 @@ } }, "node_modules/@npmcli/run-script": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-4.1.7.tgz", - "integrity": "sha512-WXr/MyM4tpKA4BotB81NccGAv8B48lNH0gRoILucbcAhTQXLCoi6HflMV3KdXubIqvP9SuLsFn68Z7r4jl+ppw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-4.2.1.tgz", + "integrity": "sha512-7dqywvVudPSrRCW5nTHpHgeWnbBtz8cFkOuKrecm6ih+oO9ciydhWt6OF7HlqupRRmB8Q/gECVdB9LMfToJbRg==", "dev": true, "dependencies": { "@npmcli/node-gyp": "^2.0.0", @@ -2745,25 +2795,10 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/@npmcli/run-script/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@popperjs/core": { - "version": "2.11.5", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz", - "integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==", + "version": "2.11.6", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", + "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", "peer": true, "funding": { "type": "opencollective", @@ -2771,13 +2806,13 @@ } }, "node_modules/@schematics/angular": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-14.1.0.tgz", - "integrity": "sha512-lhqNZzA+iT3XwlwRU757mhYmd5WE9XB2OKFhosvvszou2zuNUJMDPR9P01ZVNCOa2fScOeCMg2q3ZDgGTBl96Q==", + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-14.2.6.tgz", + "integrity": "sha512-oeyMAQr3Q9nvAX+5FRgXcTMX9lqqenElBmAuwfqqdB0qD1jmkJ8TpWRuvYVA/931njpIwhfyLrzmzeNnJb23Sg==", "dev": true, "dependencies": { - "@angular-devkit/core": "14.1.0", - "@angular-devkit/schematics": "14.1.0", + "@angular-devkit/core": "14.2.6", + "@angular-devkit/schematics": "14.2.6", "jsonc-parser": "3.1.0" }, "engines": { @@ -2818,7 +2853,9 @@ "version": "1.2.11", "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz", "integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/@types/connect": { "version": "3.4.35", @@ -2843,13 +2880,17 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/@types/cors": { "version": "2.8.12", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/@types/eslint": { "version": "8.4.5", @@ -2878,9 +2919,9 @@ "dev": true }, "node_modules/@types/express": { - "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", - "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", + "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==", "dev": true, "dependencies": { "@types/body-parser": "*", @@ -2890,9 +2931,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.30", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.30.tgz", - "integrity": "sha512-gstzbTWro2/nFed1WXtf+TtrpwxH7Ggs4RLYTLbeVgIkUQOI3WG/JKjgeOU1zXDvezllupjrf8OPIdvTbIaVOQ==", + "version": "4.17.31", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", + "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", "dev": true, "dependencies": { "@types/node": "*", @@ -2909,12 +2950,6 @@ "@types/node": "*" } }, - "node_modules/@types/jasmine": { - "version": "3.10.6", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.10.6.tgz", - "integrity": "sha512-twY9adK/vz72oWxCWxzXaxoDtF9TpfEEsxvbc1ibjF3gMD/RThSuSud/GKUTR3aJnfbivAbC/vLqhY+gdWCHfA==", - "dev": true - }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -2922,9 +2957,9 @@ "dev": true }, "node_modules/@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", "dev": true }, "node_modules/@types/node": { @@ -2967,12 +3002,12 @@ } }, "node_modules/@types/serve-static": { - "version": "1.13.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", - "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", "dev": true, "dependencies": { - "@types/mime": "^1", + "@types/mime": "*", "@types/node": "*" } }, @@ -3428,22 +3463,10 @@ "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", "dev": true }, - "node_modules/atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true, - "bin": { - "atob": "bin/atob.js" - }, - "engines": { - "node": ">= 4.5.0" - } - }, "node_modules/autoprefixer": { - "version": "10.4.7", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.7.tgz", - "integrity": "sha512-ypHju4Y2Oav95SipEcCcI5J7CGPuvz8oat7sUtYj3ClK44bldfvtvcxK6IEK++7rqB7YchDGzweZIBG+SD0ZAA==", + "version": "10.4.8", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.8.tgz", + "integrity": "sha512-75Jr6Q/XpTqEf6D2ltS5uMewJIx5irCU1oBYJrWjFenq/m12WRRrz6g15L1EIoYvPLXTbEry7rDOwrcYNj77xw==", "dev": true, "funding": [ { @@ -3456,8 +3479,8 @@ } ], "dependencies": { - "browserslist": "^4.20.3", - "caniuse-lite": "^1.0.30001335", + "browserslist": "^4.21.3", + "caniuse-lite": "^1.0.30001373", "fraction.js": "^4.2.0", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", @@ -3568,12 +3591,12 @@ } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz", - "integrity": "sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.0.tgz", + "integrity": "sha512-RW1cnryiADFeHmfLS+WW/G431p1PsW5qdRdz0SDRi7TKcUgc7Oh/uXkT7MZ/+tGsT1BkczEAmD5XjUyJ5SWDTw==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.1" + "@babel/helper-define-polyfill-provider": "^0.3.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0" @@ -3610,6 +3633,8 @@ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": "^4.5.0 || >= 5.9" } @@ -3650,9 +3675,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", - "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", "dev": true, "dependencies": { "bytes": "3.1.2", @@ -3663,7 +3688,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.10.3", + "qs": "6.11.0", "raw-body": "2.5.1", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -3689,9 +3714,9 @@ "dev": true }, "node_modules/bonjour-service": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.0.13.tgz", - "integrity": "sha512-LWKRU/7EqDUC9CTAQtuZl5HzBALoCYwtLhffW3et7vZMwv3bWLpJf8bRYlMD5OCcDpTfnPgNCV4yo9ZIaJGMiA==", + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.0.14.tgz", + "integrity": "sha512-HIMbgLnk1Vqvs6B4Wq5ep7mxvj9sGz5d1JJyDNSGNIdA/w2MCz6GTjWTdjqOJV1bEPj+6IkxDvWNFKEBxNt4kQ==", "dev": true, "dependencies": { "array-flatten": "^2.1.2", @@ -3707,9 +3732,9 @@ "dev": true }, "node_modules/bootstrap": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.0.tgz", - "integrity": "sha512-qlnS9GL6YZE6Wnef46GxGv1UpGGzAwO0aPL1yOjzDIJpeApeMvqV24iL+pjr2kU4dduoBA9fINKWKgMToobx9A==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.2.tgz", + "integrity": "sha512-dEtzMTV71n6Fhmbg4fYJzQsw1N29hJKO1js5ackCgIpDcGid2ETMGC6zwSYw09v05Y+oRdQ9loC54zB1La3hHQ==", "funding": [ { "type": "github", @@ -3721,7 +3746,7 @@ } ], "peerDependencies": { - "@popperjs/core": "^2.11.5" + "@popperjs/core": "^2.11.6" } }, "node_modules/brace-expansion": { @@ -3746,9 +3771,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.2.tgz", - "integrity": "sha512-MonuOgAtUB46uP5CezYbRaYKBNt2LxP0yX+Pmj4LkcDFGkn9Cbpi83d9sCjwQDErXsIJSzY5oKGDbgOlF/LPAA==", + "version": "4.21.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz", + "integrity": "sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==", "dev": true, "funding": [ { @@ -3761,10 +3786,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001366", - "electron-to-chromium": "^1.4.188", + "caniuse-lite": "^1.0.30001370", + "electron-to-chromium": "^1.4.202", "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.4" + "update-browserslist-db": "^1.0.5" }, "bin": { "browserslist": "cli.js" @@ -3822,9 +3847,9 @@ } }, "node_modules/cacache": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.1.tgz", - "integrity": "sha512-VDKN+LHyCQXaaYZ7rA/qtkURU+/yYhviUdvqEv2LT6QPZU8jpyzEkEVAcKlKLt5dJ5BRp11ym8lo3NKLluEPLg==", + "version": "16.1.2", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.2.tgz", + "integrity": "sha512-Xx+xPlfCZIUHagysjjOAje9nRo8pRDczQCcXb4J2O0BLtH+xeVue6ba4y1kfJfQMAnM2mkcoMIAyOctlaRGWYA==", "dev": true, "dependencies": { "@npmcli/fs": "^2.1.0", @@ -3882,9 +3907,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001366", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001366.tgz", - "integrity": "sha512-yy7XLWCubDobokgzudpkKux8e0UOOnLHE6mlNJBzT3lZJz6s5atSEzjoL+fsCPkI0G8MP5uVdDx1ur/fXEWkZA==", + "version": "1.0.30001383", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001383.tgz", + "integrity": "sha512-swMpEoTp5vDoGBZsYZX7L7nXHe6dsHxi9o6/LKf/f0LukVtnrxly5GVb/fWdCDTqi/yw6Km6tiJ0pmBacm0gbg==", "dev": true, "funding": [ { @@ -4084,7 +4109,9 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/compressible": { "version": "2.0.18", @@ -4151,6 +4178,8 @@ "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "debug": "2.6.9", "finalhandler": "1.1.2", @@ -4175,6 +4204,8 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "ms": "2.0.0" } @@ -4183,7 +4214,9 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/console-control-strings": { "version": "1.1.0", @@ -4246,6 +4279,8 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -4324,12 +4359,12 @@ } }, "node_modules/core-js-compat": { - "version": "3.24.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.24.0.tgz", - "integrity": "sha512-F+2E63X3ff/nj8uIrf8Rf24UDGIz7p838+xjEp+Bx3y8OWXj+VTPPZNCtdqovPaS9o7Tka5mCH01Zn5vOd6UQg==", + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.25.0.tgz", + "integrity": "sha512-extKQM0g8/3GjFx9US12FAgx8KJawB7RCQ5y8ipYLbmfzEzmFRWdDjIlxDx82g7ygcNG85qMVUSRyABouELdow==", "dev": true, "dependencies": { - "browserslist": "^4.21.2", + "browserslist": "^4.21.3", "semver": "7.0.0" }, "funding": { @@ -4357,6 +4392,8 @@ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "object-assign": "^4", "vary": "^1" @@ -4479,32 +4516,6 @@ "node": ">= 8" } }, - "node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", - "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", - "dev": true, - "dependencies": { - "inherits": "^2.0.4", - "source-map": "^0.6.1", - "source-map-resolve": "^0.6.0" - } - }, "node_modules/css-blank-pseudo": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz", @@ -4610,19 +4621,10 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/css/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/cssdb": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-6.6.3.tgz", - "integrity": "sha512-7GDvDSmE+20+WcSMhP17Q1EVWUrLlbxxpMDqG731n8P99JhnQZHR9YvtjPvEHfjFUjvQJvdpKCjlKOX+xe4UVA==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.0.1.tgz", + "integrity": "sha512-pT3nzyGM78poCKLAEy2zWIVX2hikq6dIrjuZzLV98MumBg+xMTNYfHx7paUlfiRTgg91O/vR889CIf+qiv79Rw==", "dev": true, "funding": { "type": "opencollective", @@ -4645,13 +4647,17 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/date-format": { "version": "4.0.11", "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.11.tgz", "integrity": "sha512-VS20KRyorrbMCQmpdl2hg5KaOUsda1RbnsJg461FfrcyCUg+pkd0b40BSW4niQyTheww4DBXQnS7HwSrKkipLw==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=4.0" } @@ -4673,15 +4679,6 @@ } } }, - "node_modules/decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, "node_modules/default-gateway": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", @@ -4772,7 +4769,9 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/dir-glob": { "version": "3.0.1", @@ -4809,6 +4808,8 @@ "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "custom-event": "~1.0.0", "ent": "~2.2.0", @@ -4878,9 +4879,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.191", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.191.tgz", - "integrity": "sha512-MeEaiuoSFh4G+rrN+Ilm1KJr8pTTZloeLurcZ+PRcthvdK1gWThje+E6baL7/7LoNctrzCncavAG/j/vpES9jg==", + "version": "1.4.233", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.233.tgz", + "integrity": "sha512-ejwIKXTg1wqbmkcRJh9Ur3hFGHFDZDw1POzdsVrB2WZjgRuRMHIQQKNpe64N/qh3ZtH2otEoRoS+s6arAAuAAw==", "dev": true }, "node_modules/emoji-regex": { @@ -4935,6 +4936,8 @@ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz", "integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -4956,6 +4959,8 @@ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz", "integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=10.0.0" } @@ -4977,7 +4982,9 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/entities": { "version": "2.2.0", @@ -5032,9 +5039,9 @@ "dev": true }, "node_modules/esbuild": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.49.tgz", - "integrity": "sha512-/TlVHhOaq7Yz8N1OJrjqM3Auzo5wjvHFLk+T8pIue+fhnhIMpfAzsG6PLVMbFveVxqD2WOp3QHei+52IMUNmCw==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.5.tgz", + "integrity": "sha512-VSf6S1QVqvxfIsSKb3UKr3VhUCis7wgDbtF4Vd9z84UJr05/Sp2fRKmzC+CSPG/dNAPPJZ0BTBLTT1Fhd6N9Gg==", "dev": true, "hasInstallScript": true, "optional": true, @@ -5045,32 +5052,33 @@ "node": ">=12" }, "optionalDependencies": { - "esbuild-android-64": "0.14.49", - "esbuild-android-arm64": "0.14.49", - "esbuild-darwin-64": "0.14.49", - "esbuild-darwin-arm64": "0.14.49", - "esbuild-freebsd-64": "0.14.49", - "esbuild-freebsd-arm64": "0.14.49", - "esbuild-linux-32": "0.14.49", - "esbuild-linux-64": "0.14.49", - "esbuild-linux-arm": "0.14.49", - "esbuild-linux-arm64": "0.14.49", - "esbuild-linux-mips64le": "0.14.49", - "esbuild-linux-ppc64le": "0.14.49", - "esbuild-linux-riscv64": "0.14.49", - "esbuild-linux-s390x": "0.14.49", - "esbuild-netbsd-64": "0.14.49", - "esbuild-openbsd-64": "0.14.49", - "esbuild-sunos-64": "0.14.49", - "esbuild-windows-32": "0.14.49", - "esbuild-windows-64": "0.14.49", - "esbuild-windows-arm64": "0.14.49" + "@esbuild/linux-loong64": "0.15.5", + "esbuild-android-64": "0.15.5", + "esbuild-android-arm64": "0.15.5", + "esbuild-darwin-64": "0.15.5", + "esbuild-darwin-arm64": "0.15.5", + "esbuild-freebsd-64": "0.15.5", + "esbuild-freebsd-arm64": "0.15.5", + "esbuild-linux-32": "0.15.5", + "esbuild-linux-64": "0.15.5", + "esbuild-linux-arm": "0.15.5", + "esbuild-linux-arm64": "0.15.5", + "esbuild-linux-mips64le": "0.15.5", + "esbuild-linux-ppc64le": "0.15.5", + "esbuild-linux-riscv64": "0.15.5", + "esbuild-linux-s390x": "0.15.5", + "esbuild-netbsd-64": "0.15.5", + "esbuild-openbsd-64": "0.15.5", + "esbuild-sunos-64": "0.15.5", + "esbuild-windows-32": "0.15.5", + "esbuild-windows-64": "0.15.5", + "esbuild-windows-arm64": "0.15.5" } }, "node_modules/esbuild-android-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.49.tgz", - "integrity": "sha512-vYsdOTD+yi+kquhBiFWl3tyxnj2qZJsl4tAqwhT90ktUdnyTizgle7TjNx6Ar1bN7wcwWqZ9QInfdk2WVagSww==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.5.tgz", + "integrity": "sha512-dYPPkiGNskvZqmIK29OPxolyY3tp+c47+Fsc2WYSOVjEPWNCHNyqhtFqQadcXMJDQt8eN0NMDukbyQgFcHquXg==", "cpu": [ "x64" ], @@ -5084,9 +5092,9 @@ } }, "node_modules/esbuild-android-arm64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.49.tgz", - "integrity": "sha512-g2HGr/hjOXCgSsvQZ1nK4nW/ei8JUx04Li74qub9qWrStlysaVmadRyTVuW32FGIpLQyc5sUjjZopj49eGGM2g==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.5.tgz", + "integrity": "sha512-YyEkaQl08ze3cBzI/4Cm1S+rVh8HMOpCdq8B78JLbNFHhzi4NixVN93xDrHZLztlocEYqi45rHHCgA8kZFidFg==", "cpu": [ "arm64" ], @@ -5100,9 +5108,9 @@ } }, "node_modules/esbuild-darwin-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.49.tgz", - "integrity": "sha512-3rvqnBCtX9ywso5fCHixt2GBCUsogNp9DjGmvbBohh31Ces34BVzFltMSxJpacNki96+WIcX5s/vum+ckXiLYg==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.5.tgz", + "integrity": "sha512-Cr0iIqnWKx3ZTvDUAzG0H/u9dWjLE4c2gTtRLz4pqOBGjfjqdcZSfAObFzKTInLLSmD0ZV1I/mshhPoYSBMMCQ==", "cpu": [ "x64" ], @@ -5116,9 +5124,9 @@ } }, "node_modules/esbuild-darwin-arm64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.49.tgz", - "integrity": "sha512-XMaqDxO846srnGlUSJnwbijV29MTKUATmOLyQSfswbK/2X5Uv28M9tTLUJcKKxzoo9lnkYPsx2o8EJcTYwCs/A==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.5.tgz", + "integrity": "sha512-WIfQkocGtFrz7vCu44ypY5YmiFXpsxvz2xqwe688jFfSVCnUsCn2qkEVDo7gT8EpsLOz1J/OmqjExePL1dr1Kg==", "cpu": [ "arm64" ], @@ -5132,9 +5140,9 @@ } }, "node_modules/esbuild-freebsd-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.49.tgz", - "integrity": "sha512-NJ5Q6AjV879mOHFri+5lZLTp5XsO2hQ+KSJYLbfY9DgCu8s6/Zl2prWXVANYTeCDLlrIlNNYw8y34xqyLDKOmQ==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.5.tgz", + "integrity": "sha512-M5/EfzV2RsMd/wqwR18CELcenZ8+fFxQAAEO7TJKDmP3knhWSbD72ILzrXFMMwshlPAS1ShCZ90jsxkm+8FlaA==", "cpu": [ "x64" ], @@ -5148,9 +5156,9 @@ } }, "node_modules/esbuild-freebsd-arm64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.49.tgz", - "integrity": "sha512-lFLtgXnAc3eXYqj5koPlBZvEbBSOSUbWO3gyY/0+4lBdRqELyz4bAuamHvmvHW5swJYL7kngzIZw6kdu25KGOA==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.5.tgz", + "integrity": "sha512-2JQQ5Qs9J0440F/n/aUBNvY6lTo4XP/4lt1TwDfHuo0DY3w5++anw+jTjfouLzbJmFFiwmX7SmUhMnysocx96w==", "cpu": [ "arm64" ], @@ -5164,9 +5172,9 @@ } }, "node_modules/esbuild-linux-32": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.49.tgz", - "integrity": "sha512-zTTH4gr2Kb8u4QcOpTDVn7Z8q7QEIvFl/+vHrI3cF6XOJS7iEI1FWslTo3uofB2+mn6sIJEQD9PrNZKoAAMDiA==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.5.tgz", + "integrity": "sha512-gO9vNnIN0FTUGjvTFucIXtBSr1Woymmx/aHQtuU+2OllGU6YFLs99960UD4Dib1kFovVgs59MTXwpFdVoSMZoQ==", "cpu": [ "ia32" ], @@ -5180,9 +5188,9 @@ } }, "node_modules/esbuild-linux-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.49.tgz", - "integrity": "sha512-hYmzRIDzFfLrB5c1SknkxzM8LdEUOusp6M2TnuQZJLRtxTgyPnZZVtyMeCLki0wKgYPXkFsAVhi8vzo2mBNeTg==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.5.tgz", + "integrity": "sha512-ne0GFdNLsm4veXbTnYAWjbx3shpNKZJUd6XpNbKNUZaNllDZfYQt0/zRqOg0sc7O8GQ+PjSMv9IpIEULXVTVmg==", "cpu": [ "x64" ], @@ -5196,9 +5204,9 @@ } }, "node_modules/esbuild-linux-arm": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.49.tgz", - "integrity": "sha512-iE3e+ZVv1Qz1Sy0gifIsarJMQ89Rpm9mtLSRtG3AH0FPgAzQ5Z5oU6vYzhc/3gSPi2UxdCOfRhw2onXuFw/0lg==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.5.tgz", + "integrity": "sha512-wvAoHEN+gJ/22gnvhZnS/+2H14HyAxM07m59RSLn3iXrQsdS518jnEWRBnJz3fR6BJa+VUTo0NxYjGaNt7RA7Q==", "cpu": [ "arm" ], @@ -5212,9 +5220,9 @@ } }, "node_modules/esbuild-linux-arm64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.49.tgz", - "integrity": "sha512-KLQ+WpeuY+7bxukxLz5VgkAAVQxUv67Ft4DmHIPIW+2w3ObBPQhqNoeQUHxopoW/aiOn3m99NSmSV+bs4BSsdA==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.5.tgz", + "integrity": "sha512-7EgFyP2zjO065XTfdCxiXVEk+f83RQ1JsryN1X/VSX2li9rnHAt2swRbpoz5Vlrl6qjHrCmq5b6yxD13z6RheA==", "cpu": [ "arm64" ], @@ -5228,9 +5236,9 @@ } }, "node_modules/esbuild-linux-mips64le": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.49.tgz", - "integrity": "sha512-n+rGODfm8RSum5pFIqFQVQpYBw+AztL8s6o9kfx7tjfK0yIGF6tm5HlG6aRjodiiKkH2xAiIM+U4xtQVZYU4rA==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.5.tgz", + "integrity": "sha512-KdnSkHxWrJ6Y40ABu+ipTZeRhFtc8dowGyFsZY5prsmMSr1ZTG9zQawguN4/tunJ0wy3+kD54GaGwdcpwWAvZQ==", "cpu": [ "mips64el" ], @@ -5244,9 +5252,9 @@ } }, "node_modules/esbuild-linux-ppc64le": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.49.tgz", - "integrity": "sha512-WP9zR4HX6iCBmMFH+XHHng2LmdoIeUmBpL4aL2TR8ruzXyT4dWrJ5BSbT8iNo6THN8lod6GOmYDLq/dgZLalGw==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.5.tgz", + "integrity": "sha512-QdRHGeZ2ykl5P0KRmfGBZIHmqcwIsUKWmmpZTOq573jRWwmpfRmS7xOhmDHBj9pxv+6qRMH8tLr2fe+ZKQvCYw==", "cpu": [ "ppc64" ], @@ -5260,9 +5268,9 @@ } }, "node_modules/esbuild-linux-riscv64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.49.tgz", - "integrity": "sha512-h66ORBz+Dg+1KgLvzTVQEA1LX4XBd1SK0Fgbhhw4akpG/YkN8pS6OzYI/7SGENiN6ao5hETRDSkVcvU9NRtkMQ==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.5.tgz", + "integrity": "sha512-p+WE6RX+jNILsf+exR29DwgV6B73khEQV0qWUbzxaycxawZ8NE0wA6HnnTxbiw5f4Gx9sJDUBemh9v49lKOORA==", "cpu": [ "riscv64" ], @@ -5276,9 +5284,9 @@ } }, "node_modules/esbuild-linux-s390x": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.49.tgz", - "integrity": "sha512-DhrUoFVWD+XmKO1y7e4kNCqQHPs6twz6VV6Uezl/XHYGzM60rBewBF5jlZjG0nCk5W/Xy6y1xWeopkrhFFM0sQ==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.5.tgz", + "integrity": "sha512-J2ngOB4cNzmqLHh6TYMM/ips8aoZIuzxJnDdWutBw5482jGXiOzsPoEF4j2WJ2mGnm7FBCO4StGcwzOgic70JQ==", "cpu": [ "s390x" ], @@ -5292,9 +5300,9 @@ } }, "node_modules/esbuild-netbsd-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.49.tgz", - "integrity": "sha512-BXaUwFOfCy2T+hABtiPUIpWjAeWK9P8O41gR4Pg73hpzoygVGnj0nI3YK4SJhe52ELgtdgWP/ckIkbn2XaTxjQ==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.5.tgz", + "integrity": "sha512-MmKUYGDizYjFia0Rwt8oOgmiFH7zaYlsoQ3tIOfPxOqLssAsEgG0MUdRDm5lliqjiuoog8LyDu9srQk5YwWF3w==", "cpu": [ "x64" ], @@ -5308,9 +5316,9 @@ } }, "node_modules/esbuild-openbsd-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.49.tgz", - "integrity": "sha512-lP06UQeLDGmVPw9Rg437Btu6J9/BmyhdoefnQ4gDEJTtJvKtQaUcOQrhjTq455ouZN4EHFH1h28WOJVANK41kA==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.5.tgz", + "integrity": "sha512-2mMFfkLk3oPWfopA9Plj4hyhqHNuGyp5KQyTT9Rc8hFd8wAn5ZrbJg+gNcLMo2yzf8Uiu0RT6G9B15YN9WQyMA==", "cpu": [ "x64" ], @@ -5324,9 +5332,9 @@ } }, "node_modules/esbuild-sunos-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.49.tgz", - "integrity": "sha512-4c8Zowp+V3zIWje329BeLbGh6XI9c/rqARNaj5yPHdC61pHI9UNdDxT3rePPJeWcEZVKjkiAS6AP6kiITp7FSw==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.5.tgz", + "integrity": "sha512-2sIzhMUfLNoD+rdmV6AacilCHSxZIoGAU2oT7XmJ0lXcZWnCvCtObvO6D4puxX9YRE97GodciRGDLBaiC6x1SA==", "cpu": [ "x64" ], @@ -5340,9 +5348,9 @@ } }, "node_modules/esbuild-wasm": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.14.49.tgz", - "integrity": "sha512-5ddzZv8M3WI1fWZ5rEfK5cSA9swlWJcceKgqjKLLERC7FnlNW50kF7hxhpkyC0Z/4w7Xeyt3yUJ9QWNMDXLk2Q==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.15.5.tgz", + "integrity": "sha512-lTJOEKekN/4JI/eOEq0wLcx53co2N6vaT/XjBz46D1tvIVoUEyM0o2K6txW6gEotf31szFD/J1PbxmnbkGlK9A==", "dev": true, "bin": { "esbuild": "bin/esbuild" @@ -5352,9 +5360,9 @@ } }, "node_modules/esbuild-windows-32": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.49.tgz", - "integrity": "sha512-q7Rb+J9yHTeKr9QTPDYkqfkEj8/kcKz9lOabDuvEXpXuIcosWCJgo5Z7h/L4r7rbtTH4a8U2FGKb6s1eeOHmJA==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.5.tgz", + "integrity": "sha512-e+duNED9UBop7Vnlap6XKedA/53lIi12xv2ebeNS4gFmu7aKyTrok7DPIZyU5w/ftHD4MUDs5PJUkQPP9xJRzg==", "cpu": [ "ia32" ], @@ -5368,9 +5376,9 @@ } }, "node_modules/esbuild-windows-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.49.tgz", - "integrity": "sha512-+Cme7Ongv0UIUTniPqfTX6mJ8Deo7VXw9xN0yJEN1lQMHDppTNmKwAM3oGbD/Vqff+07K2gN0WfNkMohmG+dVw==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.5.tgz", + "integrity": "sha512-v+PjvNtSASHOjPDMIai9Yi+aP+Vwox+3WVdg2JB8N9aivJ7lyhp4NVU+J0MV2OkWFPnVO8AE/7xH+72ibUUEnw==", "cpu": [ "x64" ], @@ -5384,9 +5392,9 @@ } }, "node_modules/esbuild-windows-arm64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.49.tgz", - "integrity": "sha512-v+HYNAXzuANrCbbLFJ5nmO3m5y2PGZWLe3uloAkLt87aXiO2mZr3BTmacZdjwNkNEHuH3bNtN8cak+mzVjVPfA==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.5.tgz", + "integrity": "sha512-Yz8w/D8CUPYstvVQujByu6mlf48lKmXkq6bkeSZZxTA626efQOJb26aDGLzmFWx6eg/FwrXgt6SZs9V8Pwy/aA==", "cpu": [ "arm64" ], @@ -5542,14 +5550,14 @@ } }, "node_modules/express": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", - "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", "dev": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.0", + "body-parser": "1.20.1", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.5.0", @@ -5568,7 +5576,7 @@ "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", "proxy-addr": "~2.0.7", - "qs": "6.10.3", + "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.18.0", @@ -5664,7 +5672,9 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/external-editor": { "version": "3.1.0", @@ -5761,6 +5771,8 @@ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", @@ -5779,6 +5791,8 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "ms": "2.0.0" } @@ -5787,13 +5801,17 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/finalhandler/node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "ee-first": "1.1.1" }, @@ -5835,7 +5853,9 @@ "version": "3.2.6", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.6.tgz", "integrity": "sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/follow-redirects": { "version": "1.15.1", @@ -5893,6 +5913,8 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -6164,15 +6186,15 @@ "dev": true }, "node_modules/hosted-git-info": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.0.0.tgz", - "integrity": "sha512-rRnjWu0Bxj+nIfUOkz0695C0H6tRrN5iYIzYejb0tDEefe2AekHu/U5Kn9pEie5vsJqpNQU02az7TGSH3qpz4Q==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.1.0.tgz", + "integrity": "sha512-Ek+QmMEqZF8XrbFdwoDjSbm7rT23pCgEMOJmz6GPk/s4yH//RQfNPArhIxbguNxROq/+5lNBwCDHMhA903Kx1Q==", "dev": true, "dependencies": { "lru-cache": "^7.5.1" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/hpack.js": { @@ -6217,12 +6239,6 @@ "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", "dev": true }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, "node_modules/http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", @@ -6794,6 +6810,8 @@ "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">= 8.0.0" }, @@ -6850,83 +6868,6 @@ "semver": "bin/semver.js" } }, - "node_modules/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dev": true, - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/istanbul-reports": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", - "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", - "dev": true, - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jasmine-core": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.10.1.tgz", - "integrity": "sha512-ooZWSDVAdh79Rrj4/nnfklL3NQVra0BcuhcuWoAwwi+znLDoUeH87AFfeX8s+YeYi6xlv5nveRyaA1v7CintfA==", - "dev": true - }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -7031,6 +6972,8 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -7052,6 +6995,8 @@ "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.0.tgz", "integrity": "sha512-s8m7z0IF5g/bS5ONT7wsOavhW4i4aFkzD4u4wgzAQWT4HGUeWI3i21cK2Yz6jndMAeHETp5XuNsRoyGJZXVd4w==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -7085,110 +7030,6 @@ "node": ">= 10" } }, - "node_modules/karma-chrome-launcher": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.1.tgz", - "integrity": "sha512-hsIglcq1vtboGPAN+DGCISCFOxW+ZVnIqhDQcCMqqCp+4dmJ0Qpq5QAjkbA0X2L9Mi6OBkHi2Srrbmm7pUKkzQ==", - "dev": true, - "dependencies": { - "which": "^1.2.1" - } - }, - "node_modules/karma-coverage": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.1.1.tgz", - "integrity": "sha512-oxeOSBVK/jdZsiX03LhHQkO4eISSQb5GbHi6Nsw3Mw7G4u6yUgacBAftnO7q+emPBLMsrNbz1pGIrj+Jb3z17A==", - "dev": true, - "dependencies": { - "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-instrument": "^4.0.3", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.1", - "istanbul-reports": "^3.0.5", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/karma-coverage/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/karma-coverage/node_modules/istanbul-lib-instrument": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", - "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", - "dev": true, - "dependencies": { - "@babel/core": "^7.7.5", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/karma-coverage/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/karma-coverage/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/karma-jasmine": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", - "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", - "dev": true, - "dependencies": { - "jasmine-core": "^4.1.0" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "karma": "^6.0.0" - } - }, - "node_modules/karma-jasmine-html-reporter": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.7.0.tgz", - "integrity": "sha512-pzum1TL7j90DTE86eFt48/s12hqwQuiD+e5aXx2Dc9wDEn2LfGq6RoAxEZZjFiN0RDSCOnosEKRZWxbQ+iMpQQ==", - "dev": true, - "peerDependencies": { - "jasmine-core": ">=3.8", - "karma": ">=0.9", - "karma-jasmine": ">=1.1" - } - }, - "node_modules/karma-jasmine/node_modules/jasmine-core": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.2.0.tgz", - "integrity": "sha512-OcFpBrIhnbmb9wfI8cqPSJ50pv3Wg4/NSgoZIqHzIwO/2a9qivJWzv8hUvaREIMYYJBas6AvfXATFdVuzzCqVw==", - "dev": true - }, "node_modules/karma-source-map-support": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", @@ -7203,6 +7044,8 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7213,6 +7056,8 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -7233,6 +7078,8 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -7245,6 +7092,8 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -7257,6 +7106,8 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -7266,6 +7117,8 @@ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "rimraf": "^3.0.0" }, @@ -7278,6 +7131,8 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -7296,6 +7151,8 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=10" } @@ -7577,6 +7434,8 @@ "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.6.0.tgz", "integrity": "sha512-3v8R7fd45UB6THucSht6wN2/7AZEruQbXdjygPZcxt5TA/msO6si9CN5MefUuKXbYnJHTBnYcx4famwcyQd+sA==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "date-format": "^4.0.11", "debug": "^4.3.4", @@ -7589,9 +7448,9 @@ } }, "node_modules/lru-cache": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.13.1.tgz", - "integrity": "sha512-CHqbAq7NFlW3RSnoWXLJBxCWaZVBrfa9UEHId2M3AW8iEBurbqduNexEUCGc3SHc6iCYXNJCDi903LajSVAEPQ==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz", + "integrity": "sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ==", "dev": true, "engines": { "node": ">=12" @@ -7634,9 +7493,9 @@ } }, "node_modules/make-fetch-happen": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.0.tgz", - "integrity": "sha512-OnEfCLofQVJ5zgKwGk55GaqosqKjaR6khQlJY3dBAA+hM25Bc5CmX5rKUfVut+rYA3uidA7zb7AvcglU87rPRg==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", "dev": true, "dependencies": { "agentkeepalive": "^4.2.1", @@ -7729,6 +7588,8 @@ "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true, + "optional": true, + "peer": true, "bin": { "mime": "cli.js" }, @@ -7826,7 +7687,9 @@ "version": "1.2.6", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/minipass": { "version": "3.3.4", @@ -7853,9 +7716,9 @@ } }, "node_modules/minipass-fetch": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.0.tgz", - "integrity": "sha512-H9U4UVBGXEyyWJnqYDCLp1PwD8XIkJ4akNHp1aGVI+2Ym7wQMlxDKi4IB4JbmyU+pl9pEs/cVrK6cOuvmbK4Sg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", "dev": true, "dependencies": { "minipass": "^3.1.6", @@ -8154,21 +8017,6 @@ "node": "*" } }, - "node_modules/node-gyp/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/node-releases": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", @@ -8191,9 +8039,9 @@ } }, "node_modules/normalize-package-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-4.0.0.tgz", - "integrity": "sha512-m+GL22VXJKkKbw62ZaBBjv8u6IE3UI4Mh5QakIqs3fWiKe0Xyi6L97hakwZK41/LD4R/2ly71Bayx0NLMwLA/g==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-4.0.1.tgz", + "integrity": "sha512-EBk5QKKuocMJhB3BILuKhmaPjI8vNRSpIfO9woLC6NyHVkKKdVEdAO1mrT0ZfxNR1lKwCcTkuZfmGIFdizZ8Pg==", "dev": true, "dependencies": { "hosted-git-info": "^5.0.0", @@ -8202,7 +8050,7 @@ "validate-npm-package-license": "^3.0.4" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/normalize-path": { @@ -8266,15 +8114,15 @@ } }, "node_modules/npm-packlist": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-5.1.1.tgz", - "integrity": "sha512-UfpSvQ5YKwctmodvPPkK6Fwk603aoVsf8AEbmVKAEECrfvL8SSe1A2YIwrJ6xmTHAITKPwwZsWo7WwEbNk0kxw==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-5.1.3.tgz", + "integrity": "sha512-263/0NGrn32YFYi4J533qzrQ/krmmrWwhKkzwTuM4f/07ug51odoaNjUexxO4vxlzURHcmYMH1QjvHjsNDKLVg==", "dev": true, "dependencies": { "glob": "^8.0.1", "ignore-walk": "^5.0.1", - "npm-bundled": "^1.1.2", - "npm-normalize-package-bin": "^1.0.1" + "npm-bundled": "^2.0.0", + "npm-normalize-package-bin": "^2.0.0" }, "bin": { "npm-packlist": "bin/index.js" @@ -8283,6 +8131,27 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/npm-packlist/node_modules/npm-bundled": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-2.0.1.tgz", + "integrity": "sha512-gZLxXdjEzE/+mOstGDqR6b0EkhJ+kM6fxM6vUuckuctuVPh80Q6pw/rSZj9s4Gex9GxWtIicO1pc8DB9KZWudw==", + "dev": true, + "dependencies": { + "npm-normalize-package-bin": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm-packlist/node_modules/npm-normalize-package-bin": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-2.0.0.tgz", + "integrity": "sha512-awzfKUO7v0FscrSpRoogyNm0sajikhBWpU0QMrW09AMi9n1PoKU6WaIqUzuJSQnpciZZmJ/jMZ2Egfmb/9LiWQ==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/npm-pick-manifest": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-7.0.1.tgz", @@ -8299,9 +8168,9 @@ } }, "node_modules/npm-registry-fetch": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-13.3.0.tgz", - "integrity": "sha512-10LJQ/1+VhKrZjIuY9I/+gQTvumqqlgnsCufoXETHAPFTS3+M+Z5CFhZRDHGavmJ6rOye3UvNga88vl8n1r6gg==", + "version": "13.3.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-13.3.1.tgz", + "integrity": "sha512-eukJPi++DKRTjSBRcDZSDDsGqRK3ehbxfFUcgaRd0Yp6kRwOwh2WVn0r+8rMB4nnuzvAk6rQVzl6K5CkYOmnvw==", "dev": true, "dependencies": { "make-fetch-happen": "^10.0.6", @@ -8360,6 +8229,8 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -8383,14 +8254,14 @@ } }, "node_modules/object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", "object-keys": "^1.1.1" }, "engines": { @@ -8644,9 +8515,9 @@ } }, "node_modules/pacote": { - "version": "13.6.1", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-13.6.1.tgz", - "integrity": "sha512-L+2BI1ougAPsFjXRyBhcKmfT016NscRFLv6Pz5EiNf1CCFJFU0pSKKQwsZTyAQB+sTuUL4TyFyp6J1Ork3dOqw==", + "version": "13.6.2", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-13.6.2.tgz", + "integrity": "sha512-Gu8fU3GsvOPkak2CkbojR7vjs3k3P9cA6uazKTHdsdV0gpCEQq2opelnEv30KRQWgVzP5Vd/5umjcedma3MKtg==", "dev": true, "dependencies": { "@npmcli/git": "^3.0.0", @@ -8868,9 +8739,9 @@ } }, "node_modules/postcss": { - "version": "8.4.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", - "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "version": "8.4.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", + "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", "dev": true, "funding": [ { @@ -9168,9 +9039,9 @@ } }, "node_modules/postcss-import": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", - "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.0.0.tgz", + "integrity": "sha512-Y20shPQ07RitgBGv2zvkEAu9bqvrD77C9axhj/aA1BQj4czape2MdClCExvB27EwYEJdGgKZBpKanb0t1rK2Kg==", "dev": true, "dependencies": { "postcss-value-parser": "^4.0.0", @@ -9178,7 +9049,7 @@ "resolve": "^1.1.7" }, "engines": { - "node": ">=10.0.0" + "node": ">=14.0.0" }, "peerDependencies": { "postcss": "^8.0.0" @@ -9405,57 +9276,59 @@ } }, "node_modules/postcss-preset-env": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.7.2.tgz", - "integrity": "sha512-1q0ih7EDsZmCb/FMDRvosna7Gsbdx8CvYO5hYT120hcp2ZAuOHpSzibujZ4JpIUcAC02PG6b+eftxqjTFh5BNA==", - "dev": true, - "dependencies": { - "@csstools/postcss-cascade-layers": "^1.0.4", - "@csstools/postcss-color-function": "^1.1.0", - "@csstools/postcss-font-format-keywords": "^1.0.0", - "@csstools/postcss-hwb-function": "^1.0.1", - "@csstools/postcss-ic-unit": "^1.0.0", - "@csstools/postcss-is-pseudo-class": "^2.0.6", - "@csstools/postcss-normalize-display-values": "^1.0.0", - "@csstools/postcss-oklab-function": "^1.1.0", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.0.tgz", + "integrity": "sha512-leqiqLOellpLKfbHkD06E04P6d9ZQ24mat6hu4NSqun7WG0UhspHR5Myiv/510qouCjoo4+YJtNOqg5xHaFnCA==", + "dev": true, + "dependencies": { + "@csstools/postcss-cascade-layers": "^1.0.5", + "@csstools/postcss-color-function": "^1.1.1", + "@csstools/postcss-font-format-keywords": "^1.0.1", + "@csstools/postcss-hwb-function": "^1.0.2", + "@csstools/postcss-ic-unit": "^1.0.1", + "@csstools/postcss-is-pseudo-class": "^2.0.7", + "@csstools/postcss-nested-calc": "^1.0.0", + "@csstools/postcss-normalize-display-values": "^1.0.1", + "@csstools/postcss-oklab-function": "^1.1.1", "@csstools/postcss-progressive-custom-properties": "^1.3.0", - "@csstools/postcss-stepped-value-functions": "^1.0.0", - "@csstools/postcss-trigonometric-functions": "^1.0.1", - "@csstools/postcss-unset-value": "^1.0.1", - "autoprefixer": "^10.4.7", - "browserslist": "^4.21.0", + "@csstools/postcss-stepped-value-functions": "^1.0.1", + "@csstools/postcss-text-decoration-shorthand": "^1.0.0", + "@csstools/postcss-trigonometric-functions": "^1.0.2", + "@csstools/postcss-unset-value": "^1.0.2", + "autoprefixer": "^10.4.8", + "browserslist": "^4.21.3", "css-blank-pseudo": "^3.0.3", "css-has-pseudo": "^3.0.4", "css-prefers-color-scheme": "^6.0.3", - "cssdb": "^6.6.3", - "postcss-attribute-case-insensitive": "^5.0.1", + "cssdb": "^7.0.0", + "postcss-attribute-case-insensitive": "^5.0.2", "postcss-clamp": "^4.1.0", - "postcss-color-functional-notation": "^4.2.3", + "postcss-color-functional-notation": "^4.2.4", "postcss-color-hex-alpha": "^8.0.4", - "postcss-color-rebeccapurple": "^7.1.0", + "postcss-color-rebeccapurple": "^7.1.1", "postcss-custom-media": "^8.0.2", "postcss-custom-properties": "^12.1.8", "postcss-custom-selectors": "^6.0.3", - "postcss-dir-pseudo-class": "^6.0.4", - "postcss-double-position-gradients": "^3.1.1", + "postcss-dir-pseudo-class": "^6.0.5", + "postcss-double-position-gradients": "^3.1.2", "postcss-env-function": "^4.0.6", "postcss-focus-visible": "^6.0.4", "postcss-focus-within": "^5.0.4", "postcss-font-variant": "^5.0.0", - "postcss-gap-properties": "^3.0.3", - "postcss-image-set-function": "^4.0.6", + "postcss-gap-properties": "^3.0.5", + "postcss-image-set-function": "^4.0.7", "postcss-initial": "^4.0.1", - "postcss-lab-function": "^4.2.0", + "postcss-lab-function": "^4.2.1", "postcss-logical": "^5.0.4", "postcss-media-minmax": "^5.0.0", - "postcss-nesting": "^10.1.9", + "postcss-nesting": "^10.1.10", "postcss-opacity-percentage": "^1.1.2", - "postcss-overflow-shorthand": "^3.0.3", + "postcss-overflow-shorthand": "^3.0.4", "postcss-page-break": "^3.0.4", - "postcss-place": "^7.0.4", - "postcss-pseudo-class-any-link": "^7.1.5", + "postcss-place": "^7.0.5", + "postcss-pseudo-class-any-link": "^7.1.6", "postcss-replace-overflow-wrap": "^4.0.0", - "postcss-selector-not": "^6.0.0", + "postcss-selector-not": "^6.0.1", "postcss-value-parser": "^4.2.0" }, "engines": { @@ -9624,14 +9497,16 @@ "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=0.9" } }, "node_modules/qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "dev": true, "dependencies": { "side-channel": "^1.0.4" @@ -9706,15 +9581,15 @@ } }, "node_modules/read-package-json": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-5.0.1.tgz", - "integrity": "sha512-MALHuNgYWdGW3gKzuNMuYtcSSZbGQm94fAp16xt8VsYTLBjUSc55bLMKe6gzpWue0Tfi6CBgwCSdDAqutGDhMg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-5.0.2.tgz", + "integrity": "sha512-BSzugrt4kQ/Z0krro8zhTwV1Kd79ue25IhNN/VtHFy1mG/6Tluyi+msc0UpwaoQzxSHa28mntAjIZY6kEgfR9Q==", "dev": true, "dependencies": { "glob": "^8.0.1", "json-parse-even-better-errors": "^2.3.1", "normalize-package-data": "^4.0.0", - "npm-normalize-package-bin": "^1.0.1" + "npm-normalize-package-bin": "^2.0.0" }, "engines": { "node": "^12.13.0 || ^14.15.0 || >=16.0.0" @@ -9733,6 +9608,15 @@ "node": ">=10" } }, + "node_modules/read-package-json/node_modules/npm-normalize-package-bin": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-2.0.0.tgz", + "integrity": "sha512-awzfKUO7v0FscrSpRoogyNm0sajikhBWpU0QMrW09AMi9n1PoKU6WaIqUzuJSQnpciZZmJ/jMZ2Egfmb/9LiWQ==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -9973,7 +9857,9 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/rimraf": { "version": "3.0.2", @@ -10065,9 +9951,9 @@ } }, "node_modules/rxjs": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz", - "integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz", + "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==", "dependencies": { "tslib": "^2.1.0" } @@ -10085,9 +9971,9 @@ "dev": true }, "node_modules/sass": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.53.0.tgz", - "integrity": "sha512-zb/oMirbKhUgRQ0/GFz8TSAwRq2IlR29vOUJZOx0l8sV+CkHUfHa4u5nqrG+1VceZp7Jfj59SVW9ogdhTvJDcQ==", + "version": "1.54.4", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.54.4.tgz", + "integrity": "sha512-3tmF16yvnBwtlPrNBHw/H907j8MlOX8aTBnlNX1yrKx24RKcJGPyLhFUwkoKBKesR3unP93/2z14Ll8NicwQUA==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -10201,9 +10087,9 @@ "dev": true }, "node_modules/selfsigned": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.1.tgz", - "integrity": "sha512-LmME957M1zOsUhG+67rAjKfiWFox3SBxE/yymatMZsAx+oMrJ0YQ8AToOnyCm7xbeg2ep37IHLxdu0o2MavQOQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", + "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", "dev": true, "dependencies": { "node-forge": "^1" @@ -10490,6 +10376,8 @@ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.1.tgz", "integrity": "sha512-0y9pnIso5a9i+lJmsCdtmTTgJFFSvNQKDnPQRz28mGNnxbmqYg2QPtJTLFxhymFZhAIn50eHAKzJeiNaKr+yUQ==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", @@ -10506,13 +10394,17 @@ "version": "2.4.0", "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz", "integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/socket.io-parser": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.5.tgz", "integrity": "sha512-sNjbT9dX63nqUFIOv95tTVm6elyIU4RvB1m8dOeZt+IgWwcWklFDOdmGcfo3zSiRsnR/3pJkjY5lfoGqEe4Eig==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "@types/component-emitter": "^1.2.10", "component-emitter": "~1.3.0", @@ -10612,17 +10504,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-resolve": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", - "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", - "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", - "dev": true, - "dependencies": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0" - } - }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -10675,9 +10556,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz", - "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==", + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", + "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==", "dev": true }, "node_modules/spdy": { @@ -10742,6 +10623,8 @@ "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.1.tgz", "integrity": "sha512-iPhtd9unZ6zKdWgMeYGfSBuqCngyJy1B/GPi/lTpwGpa3bajuX30GjUVd0/Tn/Xhg0mr4DOSENozz9Y06qyonQ==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "date-format": "^4.0.10", "debug": "^4.3.4", @@ -10816,12 +10699,12 @@ } }, "node_modules/stylus": { - "version": "0.58.1", - "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.58.1.tgz", - "integrity": "sha512-AYiCHm5ogczdCPMfe9aeQa4NklB2gcf4D/IhzYPddJjTgPc+k4D/EVE0yfQbZD43MHP3lPy+8NZ9fcFxkrgs/w==", + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.59.0.tgz", + "integrity": "sha512-lQ9w/XIOH5ZHVNuNbWW8D822r+/wBSO/d6XvtyHLF7LW4KaCIDeVbvn5DF8fGCJAUCwVhVi/h6J0NUcnylUEjg==", "dev": true, "dependencies": { - "css": "^3.0.0", + "@adobe/css-tools": "^4.0.1", "debug": "^4.3.2", "glob": "^7.1.6", "sax": "~1.2.4", @@ -10832,6 +10715,9 @@ }, "engines": { "node": "*" + }, + "funding": { + "url": "https://opencollective.com/stylus" } }, "node_modules/stylus-loader": { @@ -11220,9 +11106,9 @@ "dev": true }, "node_modules/typescript": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -11247,6 +11133,8 @@ "url": "https://paypal.me/faisalman" } ], + "optional": true, + "peer": true, "engines": { "node": "*" } @@ -11314,6 +11202,8 @@ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">= 10.0.0" } @@ -11328,9 +11218,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.4.tgz", - "integrity": "sha512-jnmO2BEGUjsMOe/Fg9u0oczOe/ppIDZPebzccl1yDWGLFP16Pa1/RM5wEoKYPG2zstNcDuAStejyxsOuKINdGA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz", + "integrity": "sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==", "dev": true, "funding": [ { @@ -11427,6 +11317,8 @@ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -11463,9 +11355,9 @@ } }, "node_modules/webpack": { - "version": "5.73.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.73.0.tgz", - "integrity": "sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA==", + "version": "5.74.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.74.0.tgz", + "integrity": "sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", @@ -11473,11 +11365,11 @@ "@webassemblyjs/ast": "1.11.1", "@webassemblyjs/wasm-edit": "1.11.1", "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.4.1", + "acorn": "^8.7.1", "acorn-import-assertions": "^1.7.6", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.9.3", + "enhanced-resolve": "^5.10.0", "es-module-lexer": "^0.9.0", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -11490,7 +11382,7 @@ "schema-utils": "^3.1.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.3.1", + "watchpack": "^2.4.0", "webpack-sources": "^3.2.3" }, "bin": { @@ -11552,9 +11444,9 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.9.3", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.9.3.tgz", - "integrity": "sha512-3qp/eoboZG5/6QgiZ3llN8TUzkSpYg1Ko9khWX1h40MIEUNS2mDoIa8aXsPfskER+GbTvs/IJZ1QTBBhhuetSw==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.11.0.tgz", + "integrity": "sha512-L5S4Q2zT57SK7tazgzjMiSMBdsw+rGYIX27MgPgx7LDhWO0lViPrHKoLS7jo5In06PWYAhlYu3PbyoC6yAThbw==", "dev": true, "dependencies": { "@types/bonjour": "^3.5.9", @@ -11626,9 +11518,9 @@ } }, "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", - "integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.9.0.tgz", + "integrity": "sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==", "dev": true, "engines": { "node": ">=10.0.0" @@ -11762,15 +11654,18 @@ } }, "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "dependencies": { "isexe": "^2.0.0" }, "bin": { - "which": "bin/which" + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, "node_modules/wide-align": { @@ -11849,6 +11744,8 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -11917,15 +11814,21 @@ } }, "node_modules/zone.js": { - "version": "0.11.7", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.11.7.tgz", - "integrity": "sha512-e39K2EdK5JfA3FDuUTVRvPlYV4aBfnOOcGuILhQAT7nzeV12uSrLBzImUM9CDVoncDSX4brR/gwqu0heQ3BQ0g==", + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.11.8.tgz", + "integrity": "sha512-82bctBg2hKcEJ21humWIkXRlLBBmrc3nN7DFh5LGGhcyycO2S7FN8NmdvlcKaGFDNVL4/9kFLmwmInTavdJERA==", "dependencies": { "tslib": "^2.3.0" } } }, "dependencies": { + "@adobe/css-tools": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.0.1.tgz", + "integrity": "sha512-+u76oB43nOHrF4DDWRLWDCtci7f3QJoEBigemIdIeTi1ODqjx6Tad9NCVnPRwewWlKkVab5PlK8DCtPTyX7S8g==", + "dev": true + }, "@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -11937,12 +11840,12 @@ } }, "@angular-devkit/architect": { - "version": "0.1401.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1401.0.tgz", - "integrity": "sha512-dHgP2/5EXkJpdf6Y1QHQX2RP8xTli/CFZH3uNnTh+EuAib/kwu+Z6K3UttZWB5VGhAF1u/xf97Vly/UkXvjKAg==", + "version": "0.1402.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1402.6.tgz", + "integrity": "sha512-qTmPBD7fBXBtlSapGLUEcJvRuL/O556zCFFpH3kSlzPNTYxi2falBjGY+4aG+078RXT1vVZtFsvRTart6VbhAg==", "dev": true, "requires": { - "@angular-devkit/core": "14.1.0", + "@angular-devkit/core": "14.2.6", "rxjs": "6.6.7" }, "dependencies": { @@ -11964,36 +11867,36 @@ } }, "@angular-devkit/build-angular": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-14.1.0.tgz", - "integrity": "sha512-AtecSuDEPLYd3p7uFVKpoA0XNcq+NvVYFJK8h90BG+IRZtzEm7ZJeYdohXVeVfTO5GvpNFN1XoHxR5rxiXeBhg==", + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-14.2.6.tgz", + "integrity": "sha512-XtaUwb3aZ8S0vl0y9bmbdFOH0KQCQ778twFH+ZfHW2BcPYtQz2Cy2rcVKXBQ850RyC0GxgMPfco6OGQndPpizg==", "dev": true, "requires": { "@ampproject/remapping": "2.2.0", - "@angular-devkit/architect": "0.1401.0", - "@angular-devkit/build-webpack": "0.1401.0", - "@angular-devkit/core": "14.1.0", - "@babel/core": "7.18.6", - "@babel/generator": "7.18.7", + "@angular-devkit/architect": "0.1402.6", + "@angular-devkit/build-webpack": "0.1402.6", + "@angular-devkit/core": "14.2.6", + "@babel/core": "7.18.10", + "@babel/generator": "7.18.12", "@babel/helper-annotate-as-pure": "7.18.6", - "@babel/plugin-proposal-async-generator-functions": "7.18.6", + "@babel/plugin-proposal-async-generator-functions": "7.18.10", "@babel/plugin-transform-async-to-generator": "7.18.6", - "@babel/plugin-transform-runtime": "7.18.6", - "@babel/preset-env": "7.18.6", - "@babel/runtime": "7.18.6", - "@babel/template": "7.18.6", + "@babel/plugin-transform-runtime": "7.18.10", + "@babel/preset-env": "7.18.10", + "@babel/runtime": "7.18.9", + "@babel/template": "7.18.10", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "14.1.0", + "@ngtools/webpack": "14.2.6", "ansi-colors": "4.1.3", "babel-loader": "8.2.5", "babel-plugin-istanbul": "6.1.1", "browserslist": "^4.9.1", - "cacache": "16.1.1", + "cacache": "16.1.2", "copy-webpack-plugin": "11.0.0", "critters": "0.0.16", "css-loader": "6.7.1", - "esbuild": "0.14.49", - "esbuild-wasm": "0.14.49", + "esbuild": "0.15.5", + "esbuild-wasm": "0.15.5", "glob": "8.0.3", "https-proxy-agent": "5.0.1", "inquirer": "8.2.4", @@ -12009,27 +11912,27 @@ "ora": "5.4.1", "parse5-html-rewriting-stream": "6.0.1", "piscina": "3.2.0", - "postcss": "8.4.14", - "postcss-import": "14.1.0", + "postcss": "8.4.16", + "postcss-import": "15.0.0", "postcss-loader": "7.0.1", - "postcss-preset-env": "7.7.2", + "postcss-preset-env": "7.8.0", "regenerator-runtime": "0.13.9", "resolve-url-loader": "5.0.0", "rxjs": "6.6.7", - "sass": "1.53.0", + "sass": "1.54.4", "sass-loader": "13.0.2", "semver": "7.3.7", "source-map-loader": "4.0.0", "source-map-support": "0.5.21", - "stylus": "0.58.1", + "stylus": "0.59.0", "stylus-loader": "7.0.0", "terser": "5.14.2", "text-table": "0.2.0", "tree-kill": "1.2.2", "tslib": "2.4.0", - "webpack": "5.73.0", + "webpack": "5.74.0", "webpack-dev-middleware": "5.3.3", - "webpack-dev-server": "4.9.3", + "webpack-dev-server": "4.11.0", "webpack-merge": "5.8.0", "webpack-subresource-integrity": "5.1.0" }, @@ -12054,12 +11957,12 @@ } }, "@angular-devkit/build-webpack": { - "version": "0.1401.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1401.0.tgz", - "integrity": "sha512-jKfnHal09mVnEapmNrAHXL/00LfafmfEUtlOPzQMgGJL7MWCeMcFthsbcOnGuzUerbiiquRk/KmLTERYjH+ZrQ==", + "version": "0.1402.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1402.6.tgz", + "integrity": "sha512-gKsDxQ9pze0N1qDM0kdM4FfwpkjSOb0bQzqjZi7wTfrh/WGIQMCjG9CRwWT+Z289ZKaTpcQDPsDtOSo5QpKNDg==", "dev": true, "requires": { - "@angular-devkit/architect": "0.1401.0", + "@angular-devkit/architect": "0.1402.6", "rxjs": "6.6.7" }, "dependencies": { @@ -12081,9 +11984,9 @@ } }, "@angular-devkit/core": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.1.0.tgz", - "integrity": "sha512-Y2d/+nFmjjY4eatc3cwdDDAnpnhG3KTX2OVW7dXSUxW3eY5e3vdMlVUbFiKwvwAshlrJy85Y6RMvZSBN4VrpnA==", + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.2.6.tgz", + "integrity": "sha512-qtRSdRm/h7C3ya04PJTDgQXV6mM8Y4RakANX1GTSXetCf9AVSxg74NJX76DWUgiHT4JiPYnJgJU6Hr/L0H6JOQ==", "dev": true, "requires": { "ajv": "8.11.0", @@ -12111,12 +12014,12 @@ } }, "@angular-devkit/schematics": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-14.1.0.tgz", - "integrity": "sha512-5QC01k9eznuQSiqxijKhVkAEmA8sioYuLhBzyffaPszSySH8kPMNxhAc8zJhBTNLumbS6iDaGkSqTQl5Kv9fOw==", + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-14.2.6.tgz", + "integrity": "sha512-mSFtc4M49mWrYsgJx/P6bA6SzXb8SeZqmppKRMoEQxiXI1bwFdGLNWzAmzEsGvS96h/nPIaOfcX5cKJSp++4FA==", "dev": true, "requires": { - "@angular-devkit/core": "14.1.0", + "@angular-devkit/core": "14.2.6", "jsonc-parser": "3.1.0", "magic-string": "0.26.2", "ora": "5.4.1", @@ -12141,23 +12044,23 @@ } }, "@angular/animations": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-14.1.0.tgz", - "integrity": "sha512-OhEXi1u/M4QyltDCxSqo7YzF7ELgNDWNqbbM7vtWIcrc4c+Yiu1GXhW/GQRosF3WAuQVfdQzEI0VTeNoo98Kvw==", + "version": "14.2.7", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-14.2.7.tgz", + "integrity": "sha512-4vI22Pa56FkE7ydxZwEd7RHwIjfyE5MnbgB2fWEQ3obnul8GnQT7OHWiPgzV57SDqOCWZyWdLm9xuOnZVdypxQ==", "requires": { "tslib": "^2.3.0" } }, "@angular/cli": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-14.1.0.tgz", - "integrity": "sha512-W/t2PkGHu9r87po1ZXQRYU81VtjzNMuGsP5tmoW1pGuibK7Kj+25G+jrXK/WADTi+pjTMXHNXYn8PlMNAIrZ/w==", + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-14.2.6.tgz", + "integrity": "sha512-8tXpe3htfZY8a+Am4nluVcztMFD5wnx4edGEDkkOiqkrUzbCtX4AyEBjUFldsYKZXbRFU46xEfM6jBnLOjxDZQ==", "dev": true, "requires": { - "@angular-devkit/architect": "0.1401.0", - "@angular-devkit/core": "14.1.0", - "@angular-devkit/schematics": "14.1.0", - "@schematics/angular": "14.1.0", + "@angular-devkit/architect": "0.1402.6", + "@angular-devkit/core": "14.2.6", + "@angular-devkit/schematics": "14.2.6", + "@schematics/angular": "14.2.6", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "debug": "4.3.4", @@ -12168,7 +12071,7 @@ "npm-pick-manifest": "7.0.1", "open": "8.4.0", "ora": "5.4.1", - "pacote": "13.6.1", + "pacote": "13.6.2", "resolve": "1.22.1", "semver": "7.3.7", "symbol-observable": "4.0.0", @@ -12177,25 +12080,25 @@ } }, "@angular/common": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-14.1.0.tgz", - "integrity": "sha512-leethDtLbA3qySaOEBUto602DF0qH1maK9u2zHncrUFOpnHAYUEd7N9MFMdIYASurTnwOSglEoIDCML94qzImQ==", + "version": "14.2.7", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-14.2.7.tgz", + "integrity": "sha512-vfydeB8urLzhRnZev/1Zm87k9jWlNfhSTk09yUnqvzcORfd3xOkcei0qc1xdIHCTEMyTREC+umsYHDmlEpZsVw==", "requires": { "tslib": "^2.3.0" } }, "@angular/compiler": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-14.1.0.tgz", - "integrity": "sha512-aLbtpFDF3fp/DOEsWSdpszmoNZAb0To/zoKhHVmEReuUKkMtlPNd3+e6wkR2vrvR/cWgbKwdb7RQ1IQtGDu74A==", + "version": "14.2.7", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-14.2.7.tgz", + "integrity": "sha512-2I8hZVM/tfUi06B6VuWgf5hWu0tgNlMCEZ1Ed4NEDkqJj+gs2l6kNVUf+FxI6hHMZTFkJPXOPx3pI5Hea5CxEQ==", "requires": { "tslib": "^2.3.0" } }, "@angular/compiler-cli": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-14.1.0.tgz", - "integrity": "sha512-llJkDnv0+riTdRPdOJv/FToz4X9ZO1URnalW+tIe2RyfOzkEqM+VLD/x+3cVgnsaFKuoPxIjZEkMoppGwVB4kg==", + "version": "14.2.7", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-14.2.7.tgz", + "integrity": "sha512-q6AQo91jc+Pd1PnWWxJq07IXr1yipq0MW3Uok5akEctbTsw4AT5y9wfPj6g/o/CkAz0kbL55QrmtyIWN5LMGdg==", "dev": true, "requires": { "@babel/core": "^7.17.2", @@ -12211,41 +12114,41 @@ } }, "@angular/core": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-14.1.0.tgz", - "integrity": "sha512-3quEsHmQifJOQ2oij5K+cjGjmhsKsyZI1+OTHWNZ6IXeuYviZv4U/Cui9fUJ1RN3CZxH3NzWB3gB/5qYFQfOgg==", + "version": "14.2.7", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-14.2.7.tgz", + "integrity": "sha512-9u2eeKS90YPh2b0pK5LKFSxKfLIzHnzkIKQFh6bEPGj43Fl2v8CwiVJu1CAKo1Or4qBY8zspSowM6S1kgGwfeg==", "requires": { "tslib": "^2.3.0" } }, "@angular/forms": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-14.1.0.tgz", - "integrity": "sha512-y7VQ2t+/ASEjzt8zXg4y5b03lMSPHmnhy4XzjDT14ZFrALaSxyhkSqoBfAksPkTeKmsFMnP/VgLboRsE8TLs0Q==", + "version": "14.2.7", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-14.2.7.tgz", + "integrity": "sha512-tEhCIE4mzlD2S0bYE+4e4RG7lOIeRZjjcKw0nlpwE0AneM03gzFlQvbHLDi3qeu8Kc7XkF6C3FJQI/Q7nMiARg==", "requires": { "tslib": "^2.3.0" } }, "@angular/platform-browser": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-14.1.0.tgz", - "integrity": "sha512-axNXUSqxsP0QSdNskd1pFo2uMo1UNoFaSAB02eDWwLkWQ1pWel+T78HiQY2bNeI3elgzjwPTT4vCCDQKNVTNig==", + "version": "14.2.7", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-14.2.7.tgz", + "integrity": "sha512-Hcn64kppozH5WlX/rkZoCGZyFFkLs0a4+rWen6uaZVxDWbas/PqR/a2LQerdS0Rn65/x+0l0w23u8TN0PnQPVA==", "requires": { "tslib": "^2.3.0" } }, "@angular/platform-browser-dynamic": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-14.1.0.tgz", - "integrity": "sha512-0Lxz3HJ9qTOyMTp5Qud2tycP7wqe+tnHOSUqDywrbNRozTKGX0z3i+l0KMku3BtUbuMi3tJomqV914/dtbCvIw==", + "version": "14.2.7", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-14.2.7.tgz", + "integrity": "sha512-P3c4fXH0+RDlL9uzuU5Xea5OQ3ozmoWawFtf4faH7fD3deHlNmrwROBXAlHkYRjbGcAiuxpEx6NjBGdmYWPaKg==", "requires": { "tslib": "^2.3.0" } }, "@angular/router": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-14.1.0.tgz", - "integrity": "sha512-WBC1E+d9RS8vy57zJ6LVtWT3AM12mEHY7SCMBRJNBcrmBYJwojxeV8IVkUoW4Ds910gG/w3LjIN0eNHg5qRtNA==", + "version": "14.2.7", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-14.2.7.tgz", + "integrity": "sha512-ZWdXXv0sCXVxWdHmCPDAy/TFT5v9JMlPp18Mmi9J8X3KeL/h6YVTWeJ0YMAOchv8D8UL02HiijnVyUp8rv5Qvw==", "requires": { "tslib": "^2.3.0" } @@ -12272,21 +12175,21 @@ "dev": true }, "@babel/core": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.6.tgz", - "integrity": "sha512-cQbWBpxcbbs/IUredIPkHiAGULLV8iwgNRMFzvbhEXISp4f3rUUXE5+TIw6KwUWUR3DwyI6gmBRnmAtYaWehwQ==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.10.tgz", + "integrity": "sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==", "dev": true, "requires": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.18.6", - "@babel/helper-compilation-targets": "^7.18.6", - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helpers": "^7.18.6", - "@babel/parser": "^7.18.6", - "@babel/template": "^7.18.6", - "@babel/traverse": "^7.18.6", - "@babel/types": "^7.18.6", + "@babel/generator": "^7.18.10", + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-module-transforms": "^7.18.9", + "@babel/helpers": "^7.18.9", + "@babel/parser": "^7.18.10", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.18.10", + "@babel/types": "^7.18.10", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -12303,12 +12206,12 @@ } }, "@babel/generator": { - "version": "7.18.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.7.tgz", - "integrity": "sha512-shck+7VLlY72a2w9c3zYWuE1pwOKEiQHV7GTUbSnhyl5eu3i04t30tBY82ZRWrDfo3gkakCFtevExnxbkf2a3A==", + "version": "7.18.12", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.12.tgz", + "integrity": "sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg==", "dev": true, "requires": { - "@babel/types": "^7.18.7", + "@babel/types": "^7.18.10", "@jridgewell/gen-mapping": "^0.3.2", "jsesc": "^2.5.1" }, @@ -12366,9 +12269,9 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.9.tgz", - "integrity": "sha512-WvypNAYaVh23QcjpMR24CwZY2Nz6hqdOcFdPbNpV56hL5H6KiFheO7Xm1aPdlLQ7d5emYZX7VZwPp9x3z+2opw==", + "version": "7.18.13", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.13.tgz", + "integrity": "sha512-hDvXp+QYxSRL+23mpAlSGxHMDyIGChm0/AwTfTAAK5Ufe40nCsyNdaYCGuK91phn/fVu9kqayImRDkvNAgdrsA==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.18.6", @@ -12547,7 +12450,13 @@ "@babel/types": "^7.18.6" } }, - "@babel/helper-validator-identifier": { + "@babel/helper-string-parser": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz", + "integrity": "sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==", + "dev": true + }, + "@babel/helper-validator-identifier": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==", @@ -12560,26 +12469,26 @@ "dev": true }, "@babel/helper-wrap-function": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.18.9.tgz", - "integrity": "sha512-cG2ru3TRAL6a60tfQflpEfs4ldiPwF6YW3zfJiRgmoFVIaC1vGnBBgatfec+ZUziPHkHSaXAuEck3Cdkf3eRpQ==", + "version": "7.18.11", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.18.11.tgz", + "integrity": "sha512-oBUlbv+rjZLh2Ks9SKi4aL7eKaAXBWleHzU89mP0G6BMUlRxSckk9tSIkgDGydhgFxHuGSlBQZfnaD47oBEB7w==", "dev": true, "requires": { "@babel/helper-function-name": "^7.18.9", - "@babel/template": "^7.18.6", - "@babel/traverse": "^7.18.9", - "@babel/types": "^7.18.9" + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.18.11", + "@babel/types": "^7.18.10" } }, "@babel/helpers": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.18.6.tgz", - "integrity": "sha512-vzSiiqbQOghPngUYt/zWGvK3LAsPhz55vc9XNN0xAl2gV4ieShI2OQli5duxWHD+72PZPTKAcfcZDE1Cwc5zsQ==", + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.18.9.tgz", + "integrity": "sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ==", "dev": true, "requires": { "@babel/template": "^7.18.6", - "@babel/traverse": "^7.18.6", - "@babel/types": "^7.18.6" + "@babel/traverse": "^7.18.9", + "@babel/types": "^7.18.9" } }, "@babel/highlight": { @@ -12594,9 +12503,9 @@ } }, "@babel/parser": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.9.tgz", - "integrity": "sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg==", + "version": "7.18.13", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.13.tgz", + "integrity": "sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg==", "dev": true }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { @@ -12620,14 +12529,14 @@ } }, "@babel/plugin-proposal-async-generator-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.18.6.tgz", - "integrity": "sha512-WAz4R9bvozx4qwf74M+sfqPMKfSqwM0phxPTR6iJIi8robgzXwkEgmeJG1gEKhm6sDqT/U9aV3lfcqybIpev8w==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.18.10.tgz", + "integrity": "sha512-1mFuY2TOsR1hxbjCo4QL+qlIjV07p4H4EUYw2J/WCqsvFV6V9X9z9YhXbWndc/4fw+hYGlDT7egYxliMp5O6Ew==", "dev": true, "requires": { - "@babel/helper-environment-visitor": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-remap-async-to-generator": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-remap-async-to-generator": "^7.18.9", "@babel/plugin-syntax-async-generators": "^7.8.4" } }, @@ -12977,9 +12886,9 @@ } }, "@babel/plugin-transform-destructuring": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.18.9.tgz", - "integrity": "sha512-p5VCYNddPLkZTq4XymQIaIfZNJwT9YsjkPOhkVEqt6QIpQFZVM9IltqqYpOEkJoN1DPznmxUDyZ5CTZs/ZCuHA==", + "version": "7.18.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.18.13.tgz", + "integrity": "sha512-TodpQ29XekIsex2A+YJPj5ax2plkGa8YYY6mFjCohk/IG9IY42Rtuj1FuDeemfg2ipxIFLzPeA83SIBnlhSIow==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.18.9" @@ -13165,16 +13074,16 @@ } }, "@babel/plugin-transform-runtime": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.18.6.tgz", - "integrity": "sha512-8uRHk9ZmRSnWqUgyae249EJZ94b0yAGLBIqzZzl+0iEdbno55Pmlt/32JZsHwXD9k/uZj18Aqqk35wBX4CBTXA==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.18.10.tgz", + "integrity": "sha512-q5mMeYAdfEbpBAgzl7tBre/la3LeCxmDO1+wMXRdPWbcoMjR3GiXlCLk7JBZVVye0bqTGNMbt0yYVXX1B1jEWQ==", "dev": true, "requires": { "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "babel-plugin-polyfill-corejs2": "^0.3.1", - "babel-plugin-polyfill-corejs3": "^0.5.2", - "babel-plugin-polyfill-regenerator": "^0.3.1", + "@babel/helper-plugin-utils": "^7.18.9", + "babel-plugin-polyfill-corejs2": "^0.3.2", + "babel-plugin-polyfill-corejs3": "^0.5.3", + "babel-plugin-polyfill-regenerator": "^0.4.0", "semver": "^6.3.0" }, "dependencies": { @@ -13233,12 +13142,12 @@ } }, "@babel/plugin-transform-unicode-escapes": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.6.tgz", - "integrity": "sha512-XNRwQUXYMP7VLuy54cr/KS/WeL3AZeORhrmeZ7iewgu+X2eBqmpaLI/hzqr9ZxCeUoq0ASK4GUzSM0BDhZkLFw==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", + "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.18.9" } }, "@babel/plugin-transform-unicode-regex": { @@ -13252,29 +13161,29 @@ } }, "@babel/preset-env": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.18.6.tgz", - "integrity": "sha512-WrthhuIIYKrEFAwttYzgRNQ5hULGmwTj+D6l7Zdfsv5M7IWV/OZbUfbeL++Qrzx1nVJwWROIFhCHRYQV4xbPNw==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.18.10.tgz", + "integrity": "sha512-wVxs1yjFdW3Z/XkNfXKoblxoHgbtUF7/l3PvvP4m02Qz9TZ6uZGxRVYjSQeR87oQmHco9zWitW5J82DJ7sCjvA==", "dev": true, "requires": { - "@babel/compat-data": "^7.18.6", - "@babel/helper-compilation-targets": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", + "@babel/compat-data": "^7.18.8", + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9", "@babel/helper-validator-option": "^7.18.6", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.6", - "@babel/plugin-proposal-async-generator-functions": "^7.18.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-async-generator-functions": "^7.18.10", "@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/plugin-proposal-class-static-block": "^7.18.6", "@babel/plugin-proposal-dynamic-import": "^7.18.6", - "@babel/plugin-proposal-export-namespace-from": "^7.18.6", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", "@babel/plugin-proposal-json-strings": "^7.18.6", - "@babel/plugin-proposal-logical-assignment-operators": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", "@babel/plugin-proposal-numeric-separator": "^7.18.6", - "@babel/plugin-proposal-object-rest-spread": "^7.18.6", + "@babel/plugin-proposal-object-rest-spread": "^7.18.9", "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", - "@babel/plugin-proposal-optional-chaining": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.18.9", "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.18.6", "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", @@ -13296,40 +13205,40 @@ "@babel/plugin-transform-arrow-functions": "^7.18.6", "@babel/plugin-transform-async-to-generator": "^7.18.6", "@babel/plugin-transform-block-scoped-functions": "^7.18.6", - "@babel/plugin-transform-block-scoping": "^7.18.6", - "@babel/plugin-transform-classes": "^7.18.6", - "@babel/plugin-transform-computed-properties": "^7.18.6", - "@babel/plugin-transform-destructuring": "^7.18.6", + "@babel/plugin-transform-block-scoping": "^7.18.9", + "@babel/plugin-transform-classes": "^7.18.9", + "@babel/plugin-transform-computed-properties": "^7.18.9", + "@babel/plugin-transform-destructuring": "^7.18.9", "@babel/plugin-transform-dotall-regex": "^7.18.6", - "@babel/plugin-transform-duplicate-keys": "^7.18.6", + "@babel/plugin-transform-duplicate-keys": "^7.18.9", "@babel/plugin-transform-exponentiation-operator": "^7.18.6", - "@babel/plugin-transform-for-of": "^7.18.6", - "@babel/plugin-transform-function-name": "^7.18.6", - "@babel/plugin-transform-literals": "^7.18.6", + "@babel/plugin-transform-for-of": "^7.18.8", + "@babel/plugin-transform-function-name": "^7.18.9", + "@babel/plugin-transform-literals": "^7.18.9", "@babel/plugin-transform-member-expression-literals": "^7.18.6", "@babel/plugin-transform-modules-amd": "^7.18.6", "@babel/plugin-transform-modules-commonjs": "^7.18.6", - "@babel/plugin-transform-modules-systemjs": "^7.18.6", + "@babel/plugin-transform-modules-systemjs": "^7.18.9", "@babel/plugin-transform-modules-umd": "^7.18.6", "@babel/plugin-transform-named-capturing-groups-regex": "^7.18.6", "@babel/plugin-transform-new-target": "^7.18.6", "@babel/plugin-transform-object-super": "^7.18.6", - "@babel/plugin-transform-parameters": "^7.18.6", + "@babel/plugin-transform-parameters": "^7.18.8", "@babel/plugin-transform-property-literals": "^7.18.6", "@babel/plugin-transform-regenerator": "^7.18.6", "@babel/plugin-transform-reserved-words": "^7.18.6", "@babel/plugin-transform-shorthand-properties": "^7.18.6", - "@babel/plugin-transform-spread": "^7.18.6", + "@babel/plugin-transform-spread": "^7.18.9", "@babel/plugin-transform-sticky-regex": "^7.18.6", - "@babel/plugin-transform-template-literals": "^7.18.6", - "@babel/plugin-transform-typeof-symbol": "^7.18.6", - "@babel/plugin-transform-unicode-escapes": "^7.18.6", + "@babel/plugin-transform-template-literals": "^7.18.9", + "@babel/plugin-transform-typeof-symbol": "^7.18.9", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", "@babel/plugin-transform-unicode-regex": "^7.18.6", "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.18.6", - "babel-plugin-polyfill-corejs2": "^0.3.1", - "babel-plugin-polyfill-corejs3": "^0.5.2", - "babel-plugin-polyfill-regenerator": "^0.3.1", + "@babel/types": "^7.18.10", + "babel-plugin-polyfill-corejs2": "^0.3.2", + "babel-plugin-polyfill-corejs3": "^0.5.3", + "babel-plugin-polyfill-regenerator": "^0.4.0", "core-js-compat": "^3.22.1", "semver": "^6.3.0" }, @@ -13356,50 +13265,50 @@ } }, "@babel/runtime": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.6.tgz", - "integrity": "sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==", + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz", + "integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==", "dev": true, "requires": { "regenerator-runtime": "^0.13.4" } }, "@babel/template": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.6.tgz", - "integrity": "sha512-JoDWzPe+wgBsTTgdnIma3iHNFC7YVJoPssVBDjiHfNlyt4YcunDtcDOUmfVDfCK5MfdsaIoX9PkijPhjH3nYUw==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", + "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", "dev": true, "requires": { "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.6", - "@babel/types": "^7.18.6" + "@babel/parser": "^7.18.10", + "@babel/types": "^7.18.10" } }, "@babel/traverse": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.9.tgz", - "integrity": "sha512-LcPAnujXGwBgv3/WHv01pHtb2tihcyW1XuL9wd7jqh1Z8AQkTd+QVjMrMijrln0T7ED3UXLIy36P9Ao7W75rYg==", + "version": "7.18.13", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.13.tgz", + "integrity": "sha512-N6kt9X1jRMLPxxxPYWi7tgvJRH/rtoU+dbKAPDM44RFHiMH8igdsaSBgFeskhSl/kLWLDUvIh1RXCrTmg0/zvA==", "dev": true, "requires": { "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.18.9", + "@babel/generator": "^7.18.13", "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-function-name": "^7.18.9", "@babel/helper-hoist-variables": "^7.18.6", "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.18.9", - "@babel/types": "^7.18.9", + "@babel/parser": "^7.18.13", + "@babel/types": "^7.18.13", "debug": "^4.1.0", "globals": "^11.1.0" }, "dependencies": { "@babel/generator": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.9.tgz", - "integrity": "sha512-wt5Naw6lJrL1/SGkipMiFxJjtyczUWTP38deiP1PO60HsBjDeKk08CGC3S8iVuvf0FmTdgKwU1KIXzSKL1G0Ug==", + "version": "7.18.13", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.13.tgz", + "integrity": "sha512-CkPg8ySSPuHTYPJYo7IRALdqyjM9HCbt/3uOBEFbzyGVP6Mn8bwFPB0jX6982JVNBlYzM1nnPkfjuXSOPtQeEQ==", "dev": true, "requires": { - "@babel/types": "^7.18.9", + "@babel/types": "^7.18.13", "@jridgewell/gen-mapping": "^0.3.2", "jsesc": "^2.5.1" } @@ -13418,11 +13327,12 @@ } }, "@babel/types": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.9.tgz", - "integrity": "sha512-WwMLAg2MvJmt/rKEVQBBhIVffMmnilX4oe0sRe7iPOHIGsqpruFHHdrfj4O1CMMtgMtCU4oPafZjDPCRgO57Wg==", + "version": "7.18.13", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.13.tgz", + "integrity": "sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ==", "dev": true, "requires": { + "@babel/helper-string-parser": "^7.18.10", "@babel/helper-validator-identifier": "^7.18.6", "to-fast-properties": "^2.0.0" } @@ -13431,7 +13341,9 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "@csstools/postcss-cascade-layers": { "version": "1.0.5", @@ -13491,6 +13403,15 @@ "postcss-selector-parser": "^6.0.10" } }, + "@csstools/postcss-nested-calc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz", + "integrity": "sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, "@csstools/postcss-normalize-display-values": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz", @@ -13528,6 +13449,15 @@ "postcss-value-parser": "^4.2.0" } }, + "@csstools/postcss-text-decoration-shorthand": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz", + "integrity": "sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, "@csstools/postcss-trigonometric-functions": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz", @@ -13557,10 +13487,17 @@ "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "dev": true }, + "@esbuild/linux-loong64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.5.tgz", + "integrity": "sha512-UHkDFCfSGTuXq08oQltXxSZmH1TXyWsL+4QhZDWvvLl6mEJQqk3u7/wq1LjhrrAXYIllaTtRSzUXl4Olkf2J8A==", + "dev": true, + "optional": true + }, "@fortawesome/fontawesome-free": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.1.2.tgz", - "integrity": "sha512-XwWADtfdSN73/udaFm+1mnGIj/ShDZNFMe/PRoqv3FhQ4GNI2PUN70yFTPsjq65Lw2C9i4TG5/hTbxXIXVCiqQ==" + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.2.0.tgz", + "integrity": "sha512-CNR7qRIfCwWHNN7FnKUniva94edPdyQzil/zCwk3v6k4R6rR2Fr8i4s3PM7n/lyfPA6Zfko9z5WDzFxG9SW1uQ==" }, "@gar/promisify": { "version": "1.1.3", @@ -13655,9 +13592,9 @@ "dev": true }, "@ngtools/webpack": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.1.0.tgz", - "integrity": "sha512-d4U6ymDCXckVgfjYEv1Wjzd78ZSm0NKgq8mN6FdlrCupg02LPIODjeKyNr4c4zwMAOJeHkVNEZ+USoDEK3XSsw==", + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.6.tgz", + "integrity": "sha512-HdfoHLGPzyP135BOlvTQcpeWisVfiH0u40YNTBVK3QAsrLnY17e2QG5BWBOrVYipRu1975cZtTC9rPjcCY8aLQ==", "dev": true, "requires": {} }, @@ -13688,9 +13625,9 @@ } }, "@npmcli/fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.1.tgz", - "integrity": "sha512-1Q0uzx6c/NVNGszePbr5Gc2riSU1zLpNlo/1YWntH+eaPmMgBssAW0qXofCVkpdj3ce4swZtlDYQu+NKiYcptg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", "dev": true, "requires": { "@gar/promisify": "^1.1.3", @@ -13698,9 +13635,9 @@ } }, "@npmcli/git": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-3.0.1.tgz", - "integrity": "sha512-UU85F/T+F1oVn3IsB/L6k9zXIMpXBuUBE25QDH0SsURwT6IOBqkC7M16uqo2vVZIyji3X1K4XH9luip7YekH1A==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-3.0.2.tgz", + "integrity": "sha512-CAcd08y3DWBJqJDpfuVL0uijlq5oaXaOJEKHKc4wqrjd00gkvTZB+nFuLn+doOOKddaQS9JfqtNoFCO2LCvA3w==", "dev": true, "requires": { "@npmcli/promise-spawn": "^3.0.0", @@ -13712,17 +13649,6 @@ "promise-retry": "^2.0.1", "semver": "^7.3.5", "which": "^2.0.2" - }, - "dependencies": { - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } } }, "@npmcli/installed-package-contents": { @@ -13736,9 +13662,9 @@ } }, "@npmcli/move-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.0.tgz", - "integrity": "sha512-UR6D5f4KEGWJV6BGPH3Qb2EtgH+t+1XQ1Tt85c7qicN6cezzuHPdZwwAxqZr4JLtnQu0LZsTza/5gmNmSl8XLg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", "dev": true, "requires": { "mkdirp": "^1.0.4", @@ -13761,9 +13687,9 @@ } }, "@npmcli/run-script": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-4.1.7.tgz", - "integrity": "sha512-WXr/MyM4tpKA4BotB81NccGAv8B48lNH0gRoILucbcAhTQXLCoi6HflMV3KdXubIqvP9SuLsFn68Z7r4jl+ppw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-4.2.1.tgz", + "integrity": "sha512-7dqywvVudPSrRCW5nTHpHgeWnbBtz8cFkOuKrecm6ih+oO9ciydhWt6OF7HlqupRRmB8Q/gECVdB9LMfToJbRg==", "dev": true, "requires": { "@npmcli/node-gyp": "^2.0.0", @@ -13771,33 +13697,22 @@ "node-gyp": "^9.0.0", "read-package-json-fast": "^2.0.3", "which": "^2.0.2" - }, - "dependencies": { - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } } }, "@popperjs/core": { - "version": "2.11.5", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz", - "integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==", + "version": "2.11.6", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", + "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", "peer": true }, "@schematics/angular": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-14.1.0.tgz", - "integrity": "sha512-lhqNZzA+iT3XwlwRU757mhYmd5WE9XB2OKFhosvvszou2zuNUJMDPR9P01ZVNCOa2fScOeCMg2q3ZDgGTBl96Q==", + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-14.2.6.tgz", + "integrity": "sha512-oeyMAQr3Q9nvAX+5FRgXcTMX9lqqenElBmAuwfqqdB0qD1jmkJ8TpWRuvYVA/931njpIwhfyLrzmzeNnJb23Sg==", "dev": true, "requires": { - "@angular-devkit/core": "14.1.0", - "@angular-devkit/schematics": "14.1.0", + "@angular-devkit/core": "14.2.6", + "@angular-devkit/schematics": "14.2.6", "jsonc-parser": "3.1.0" } }, @@ -13830,7 +13745,9 @@ "version": "1.2.11", "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz", "integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "@types/connect": { "version": "3.4.35", @@ -13855,13 +13772,17 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "@types/cors": { "version": "2.8.12", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "@types/eslint": { "version": "8.4.5", @@ -13890,9 +13811,9 @@ "dev": true }, "@types/express": { - "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", - "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", + "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==", "dev": true, "requires": { "@types/body-parser": "*", @@ -13902,9 +13823,9 @@ } }, "@types/express-serve-static-core": { - "version": "4.17.30", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.30.tgz", - "integrity": "sha512-gstzbTWro2/nFed1WXtf+TtrpwxH7Ggs4RLYTLbeVgIkUQOI3WG/JKjgeOU1zXDvezllupjrf8OPIdvTbIaVOQ==", + "version": "4.17.31", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", + "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", "dev": true, "requires": { "@types/node": "*", @@ -13921,12 +13842,6 @@ "@types/node": "*" } }, - "@types/jasmine": { - "version": "3.10.6", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.10.6.tgz", - "integrity": "sha512-twY9adK/vz72oWxCWxzXaxoDtF9TpfEEsxvbc1ibjF3gMD/RThSuSud/GKUTR3aJnfbivAbC/vLqhY+gdWCHfA==", - "dev": true - }, "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -13934,9 +13849,9 @@ "dev": true }, "@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", "dev": true }, "@types/node": { @@ -13979,12 +13894,12 @@ } }, "@types/serve-static": { - "version": "1.13.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", - "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", "dev": true, "requires": { - "@types/mime": "^1", + "@types/mime": "*", "@types/node": "*" } }, @@ -14373,20 +14288,14 @@ "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", "dev": true }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, "autoprefixer": { - "version": "10.4.7", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.7.tgz", - "integrity": "sha512-ypHju4Y2Oav95SipEcCcI5J7CGPuvz8oat7sUtYj3ClK44bldfvtvcxK6IEK++7rqB7YchDGzweZIBG+SD0ZAA==", + "version": "10.4.8", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.8.tgz", + "integrity": "sha512-75Jr6Q/XpTqEf6D2ltS5uMewJIx5irCU1oBYJrWjFenq/m12WRRrz6g15L1EIoYvPLXTbEry7rDOwrcYNj77xw==", "dev": true, "requires": { - "browserslist": "^4.20.3", - "caniuse-lite": "^1.0.30001335", + "browserslist": "^4.21.3", + "caniuse-lite": "^1.0.30001373", "fraction.js": "^4.2.0", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", @@ -14470,12 +14379,12 @@ } }, "babel-plugin-polyfill-regenerator": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz", - "integrity": "sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.0.tgz", + "integrity": "sha512-RW1cnryiADFeHmfLS+WW/G431p1PsW5qdRdz0SDRi7TKcUgc7Oh/uXkT7MZ/+tGsT1BkczEAmD5XjUyJ5SWDTw==", "dev": true, "requires": { - "@babel/helper-define-polyfill-provider": "^0.3.1" + "@babel/helper-define-polyfill-provider": "^0.3.2" } }, "balanced-match": { @@ -14494,7 +14403,9 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "batch": { "version": "0.6.1", @@ -14526,9 +14437,9 @@ } }, "body-parser": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", - "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", "dev": true, "requires": { "bytes": "3.1.2", @@ -14539,7 +14450,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.10.3", + "qs": "6.11.0", "raw-body": "2.5.1", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -14563,9 +14474,9 @@ } }, "bonjour-service": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.0.13.tgz", - "integrity": "sha512-LWKRU/7EqDUC9CTAQtuZl5HzBALoCYwtLhffW3et7vZMwv3bWLpJf8bRYlMD5OCcDpTfnPgNCV4yo9ZIaJGMiA==", + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.0.14.tgz", + "integrity": "sha512-HIMbgLnk1Vqvs6B4Wq5ep7mxvj9sGz5d1JJyDNSGNIdA/w2MCz6GTjWTdjqOJV1bEPj+6IkxDvWNFKEBxNt4kQ==", "dev": true, "requires": { "array-flatten": "^2.1.2", @@ -14581,9 +14492,9 @@ "dev": true }, "bootstrap": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.0.tgz", - "integrity": "sha512-qlnS9GL6YZE6Wnef46GxGv1UpGGzAwO0aPL1yOjzDIJpeApeMvqV24iL+pjr2kU4dduoBA9fINKWKgMToobx9A==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.2.tgz", + "integrity": "sha512-dEtzMTV71n6Fhmbg4fYJzQsw1N29hJKO1js5ackCgIpDcGid2ETMGC6zwSYw09v05Y+oRdQ9loC54zB1La3hHQ==", "requires": {} }, "brace-expansion": { @@ -14605,15 +14516,15 @@ } }, "browserslist": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.2.tgz", - "integrity": "sha512-MonuOgAtUB46uP5CezYbRaYKBNt2LxP0yX+Pmj4LkcDFGkn9Cbpi83d9sCjwQDErXsIJSzY5oKGDbgOlF/LPAA==", + "version": "4.21.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz", + "integrity": "sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001366", - "electron-to-chromium": "^1.4.188", + "caniuse-lite": "^1.0.30001370", + "electron-to-chromium": "^1.4.202", "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.4" + "update-browserslist-db": "^1.0.5" } }, "buffer": { @@ -14648,9 +14559,9 @@ "dev": true }, "cacache": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.1.tgz", - "integrity": "sha512-VDKN+LHyCQXaaYZ7rA/qtkURU+/yYhviUdvqEv2LT6QPZU8jpyzEkEVAcKlKLt5dJ5BRp11ym8lo3NKLluEPLg==", + "version": "16.1.2", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.2.tgz", + "integrity": "sha512-Xx+xPlfCZIUHagysjjOAje9nRo8pRDczQCcXb4J2O0BLtH+xeVue6ba4y1kfJfQMAnM2mkcoMIAyOctlaRGWYA==", "dev": true, "requires": { "@npmcli/fs": "^2.1.0", @@ -14696,9 +14607,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001366", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001366.tgz", - "integrity": "sha512-yy7XLWCubDobokgzudpkKux8e0UOOnLHE6mlNJBzT3lZJz6s5atSEzjoL+fsCPkI0G8MP5uVdDx1ur/fXEWkZA==", + "version": "1.0.30001383", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001383.tgz", + "integrity": "sha512-swMpEoTp5vDoGBZsYZX7L7nXHe6dsHxi9o6/LKf/f0LukVtnrxly5GVb/fWdCDTqi/yw6Km6tiJ0pmBacm0gbg==", "dev": true }, "chalk": { @@ -14844,7 +14755,9 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "compressible": { "version": "2.0.18", @@ -14904,6 +14817,8 @@ "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", "dev": true, + "optional": true, + "peer": true, "requires": { "debug": "2.6.9", "finalhandler": "1.1.2", @@ -14916,6 +14831,8 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "optional": true, + "peer": true, "requires": { "ms": "2.0.0" } @@ -14924,7 +14841,9 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "dev": true, + "optional": true, + "peer": true } } }, @@ -14976,7 +14895,9 @@ "version": "0.4.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "cookie-signature": { "version": "1.0.6", @@ -15031,12 +14952,12 @@ } }, "core-js-compat": { - "version": "3.24.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.24.0.tgz", - "integrity": "sha512-F+2E63X3ff/nj8uIrf8Rf24UDGIz7p838+xjEp+Bx3y8OWXj+VTPPZNCtdqovPaS9o7Tka5mCH01Zn5vOd6UQg==", + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.25.0.tgz", + "integrity": "sha512-extKQM0g8/3GjFx9US12FAgx8KJawB7RCQ5y8ipYLbmfzEzmFRWdDjIlxDx82g7ygcNG85qMVUSRyABouELdow==", "dev": true, "requires": { - "browserslist": "^4.21.2", + "browserslist": "^4.21.3", "semver": "7.0.0" }, "dependencies": { @@ -15059,6 +14980,8 @@ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "dev": true, + "optional": true, + "peer": true, "requires": { "object-assign": "^4", "vary": "^1" @@ -15151,36 +15074,6 @@ "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" - }, - "dependencies": { - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "css": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", - "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", - "dev": true, - "requires": { - "inherits": "^2.0.4", - "source-map": "^0.6.1", - "source-map-resolve": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } } }, "css-blank-pseudo": { @@ -15244,9 +15137,9 @@ "dev": true }, "cssdb": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-6.6.3.tgz", - "integrity": "sha512-7GDvDSmE+20+WcSMhP17Q1EVWUrLlbxxpMDqG731n8P99JhnQZHR9YvtjPvEHfjFUjvQJvdpKCjlKOX+xe4UVA==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.0.1.tgz", + "integrity": "sha512-pT3nzyGM78poCKLAEy2zWIVX2hikq6dIrjuZzLV98MumBg+xMTNYfHx7paUlfiRTgg91O/vR889CIf+qiv79Rw==", "dev": true }, "cssesc": { @@ -15259,13 +15152,17 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "date-format": { "version": "4.0.11", "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.11.tgz", "integrity": "sha512-VS20KRyorrbMCQmpdl2hg5KaOUsda1RbnsJg461FfrcyCUg+pkd0b40BSW4niQyTheww4DBXQnS7HwSrKkipLw==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "debug": { "version": "4.3.4", @@ -15276,12 +15173,6 @@ "ms": "2.1.2" } }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", - "dev": true - }, "default-gateway": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", @@ -15350,7 +15241,9 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "dir-glob": { "version": "3.0.1", @@ -15381,6 +15274,8 @@ "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", "dev": true, + "optional": true, + "peer": true, "requires": { "custom-event": "~1.0.0", "ent": "~2.2.0", @@ -15432,9 +15327,9 @@ "dev": true }, "electron-to-chromium": { - "version": "1.4.191", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.191.tgz", - "integrity": "sha512-MeEaiuoSFh4G+rrN+Ilm1KJr8pTTZloeLurcZ+PRcthvdK1gWThje+E6baL7/7LoNctrzCncavAG/j/vpES9jg==", + "version": "1.4.233", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.233.tgz", + "integrity": "sha512-ejwIKXTg1wqbmkcRJh9Ur3hFGHFDZDw1POzdsVrB2WZjgRuRMHIQQKNpe64N/qh3ZtH2otEoRoS+s6arAAuAAw==", "dev": true }, "emoji-regex": { @@ -15482,6 +15377,8 @@ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz", "integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==", "dev": true, + "optional": true, + "peer": true, "requires": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -15499,7 +15396,9 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz", "integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "enhanced-resolve": { "version": "5.10.0", @@ -15515,7 +15414,9 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "entities": { "version": "2.2.0", @@ -15561,177 +15462,178 @@ "dev": true }, "esbuild": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.49.tgz", - "integrity": "sha512-/TlVHhOaq7Yz8N1OJrjqM3Auzo5wjvHFLk+T8pIue+fhnhIMpfAzsG6PLVMbFveVxqD2WOp3QHei+52IMUNmCw==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.5.tgz", + "integrity": "sha512-VSf6S1QVqvxfIsSKb3UKr3VhUCis7wgDbtF4Vd9z84UJr05/Sp2fRKmzC+CSPG/dNAPPJZ0BTBLTT1Fhd6N9Gg==", "dev": true, "optional": true, "requires": { - "esbuild-android-64": "0.14.49", - "esbuild-android-arm64": "0.14.49", - "esbuild-darwin-64": "0.14.49", - "esbuild-darwin-arm64": "0.14.49", - "esbuild-freebsd-64": "0.14.49", - "esbuild-freebsd-arm64": "0.14.49", - "esbuild-linux-32": "0.14.49", - "esbuild-linux-64": "0.14.49", - "esbuild-linux-arm": "0.14.49", - "esbuild-linux-arm64": "0.14.49", - "esbuild-linux-mips64le": "0.14.49", - "esbuild-linux-ppc64le": "0.14.49", - "esbuild-linux-riscv64": "0.14.49", - "esbuild-linux-s390x": "0.14.49", - "esbuild-netbsd-64": "0.14.49", - "esbuild-openbsd-64": "0.14.49", - "esbuild-sunos-64": "0.14.49", - "esbuild-windows-32": "0.14.49", - "esbuild-windows-64": "0.14.49", - "esbuild-windows-arm64": "0.14.49" + "@esbuild/linux-loong64": "0.15.5", + "esbuild-android-64": "0.15.5", + "esbuild-android-arm64": "0.15.5", + "esbuild-darwin-64": "0.15.5", + "esbuild-darwin-arm64": "0.15.5", + "esbuild-freebsd-64": "0.15.5", + "esbuild-freebsd-arm64": "0.15.5", + "esbuild-linux-32": "0.15.5", + "esbuild-linux-64": "0.15.5", + "esbuild-linux-arm": "0.15.5", + "esbuild-linux-arm64": "0.15.5", + "esbuild-linux-mips64le": "0.15.5", + "esbuild-linux-ppc64le": "0.15.5", + "esbuild-linux-riscv64": "0.15.5", + "esbuild-linux-s390x": "0.15.5", + "esbuild-netbsd-64": "0.15.5", + "esbuild-openbsd-64": "0.15.5", + "esbuild-sunos-64": "0.15.5", + "esbuild-windows-32": "0.15.5", + "esbuild-windows-64": "0.15.5", + "esbuild-windows-arm64": "0.15.5" } }, "esbuild-android-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.49.tgz", - "integrity": "sha512-vYsdOTD+yi+kquhBiFWl3tyxnj2qZJsl4tAqwhT90ktUdnyTizgle7TjNx6Ar1bN7wcwWqZ9QInfdk2WVagSww==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.5.tgz", + "integrity": "sha512-dYPPkiGNskvZqmIK29OPxolyY3tp+c47+Fsc2WYSOVjEPWNCHNyqhtFqQadcXMJDQt8eN0NMDukbyQgFcHquXg==", "dev": true, "optional": true }, "esbuild-android-arm64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.49.tgz", - "integrity": "sha512-g2HGr/hjOXCgSsvQZ1nK4nW/ei8JUx04Li74qub9qWrStlysaVmadRyTVuW32FGIpLQyc5sUjjZopj49eGGM2g==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.5.tgz", + "integrity": "sha512-YyEkaQl08ze3cBzI/4Cm1S+rVh8HMOpCdq8B78JLbNFHhzi4NixVN93xDrHZLztlocEYqi45rHHCgA8kZFidFg==", "dev": true, "optional": true }, "esbuild-darwin-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.49.tgz", - "integrity": "sha512-3rvqnBCtX9ywso5fCHixt2GBCUsogNp9DjGmvbBohh31Ces34BVzFltMSxJpacNki96+WIcX5s/vum+ckXiLYg==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.5.tgz", + "integrity": "sha512-Cr0iIqnWKx3ZTvDUAzG0H/u9dWjLE4c2gTtRLz4pqOBGjfjqdcZSfAObFzKTInLLSmD0ZV1I/mshhPoYSBMMCQ==", "dev": true, "optional": true }, "esbuild-darwin-arm64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.49.tgz", - "integrity": "sha512-XMaqDxO846srnGlUSJnwbijV29MTKUATmOLyQSfswbK/2X5Uv28M9tTLUJcKKxzoo9lnkYPsx2o8EJcTYwCs/A==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.5.tgz", + "integrity": "sha512-WIfQkocGtFrz7vCu44ypY5YmiFXpsxvz2xqwe688jFfSVCnUsCn2qkEVDo7gT8EpsLOz1J/OmqjExePL1dr1Kg==", "dev": true, "optional": true }, "esbuild-freebsd-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.49.tgz", - "integrity": "sha512-NJ5Q6AjV879mOHFri+5lZLTp5XsO2hQ+KSJYLbfY9DgCu8s6/Zl2prWXVANYTeCDLlrIlNNYw8y34xqyLDKOmQ==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.5.tgz", + "integrity": "sha512-M5/EfzV2RsMd/wqwR18CELcenZ8+fFxQAAEO7TJKDmP3knhWSbD72ILzrXFMMwshlPAS1ShCZ90jsxkm+8FlaA==", "dev": true, "optional": true }, "esbuild-freebsd-arm64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.49.tgz", - "integrity": "sha512-lFLtgXnAc3eXYqj5koPlBZvEbBSOSUbWO3gyY/0+4lBdRqELyz4bAuamHvmvHW5swJYL7kngzIZw6kdu25KGOA==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.5.tgz", + "integrity": "sha512-2JQQ5Qs9J0440F/n/aUBNvY6lTo4XP/4lt1TwDfHuo0DY3w5++anw+jTjfouLzbJmFFiwmX7SmUhMnysocx96w==", "dev": true, "optional": true }, "esbuild-linux-32": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.49.tgz", - "integrity": "sha512-zTTH4gr2Kb8u4QcOpTDVn7Z8q7QEIvFl/+vHrI3cF6XOJS7iEI1FWslTo3uofB2+mn6sIJEQD9PrNZKoAAMDiA==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.5.tgz", + "integrity": "sha512-gO9vNnIN0FTUGjvTFucIXtBSr1Woymmx/aHQtuU+2OllGU6YFLs99960UD4Dib1kFovVgs59MTXwpFdVoSMZoQ==", "dev": true, "optional": true }, "esbuild-linux-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.49.tgz", - "integrity": "sha512-hYmzRIDzFfLrB5c1SknkxzM8LdEUOusp6M2TnuQZJLRtxTgyPnZZVtyMeCLki0wKgYPXkFsAVhi8vzo2mBNeTg==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.5.tgz", + "integrity": "sha512-ne0GFdNLsm4veXbTnYAWjbx3shpNKZJUd6XpNbKNUZaNllDZfYQt0/zRqOg0sc7O8GQ+PjSMv9IpIEULXVTVmg==", "dev": true, "optional": true }, "esbuild-linux-arm": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.49.tgz", - "integrity": "sha512-iE3e+ZVv1Qz1Sy0gifIsarJMQ89Rpm9mtLSRtG3AH0FPgAzQ5Z5oU6vYzhc/3gSPi2UxdCOfRhw2onXuFw/0lg==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.5.tgz", + "integrity": "sha512-wvAoHEN+gJ/22gnvhZnS/+2H14HyAxM07m59RSLn3iXrQsdS518jnEWRBnJz3fR6BJa+VUTo0NxYjGaNt7RA7Q==", "dev": true, "optional": true }, "esbuild-linux-arm64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.49.tgz", - "integrity": "sha512-KLQ+WpeuY+7bxukxLz5VgkAAVQxUv67Ft4DmHIPIW+2w3ObBPQhqNoeQUHxopoW/aiOn3m99NSmSV+bs4BSsdA==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.5.tgz", + "integrity": "sha512-7EgFyP2zjO065XTfdCxiXVEk+f83RQ1JsryN1X/VSX2li9rnHAt2swRbpoz5Vlrl6qjHrCmq5b6yxD13z6RheA==", "dev": true, "optional": true }, "esbuild-linux-mips64le": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.49.tgz", - "integrity": "sha512-n+rGODfm8RSum5pFIqFQVQpYBw+AztL8s6o9kfx7tjfK0yIGF6tm5HlG6aRjodiiKkH2xAiIM+U4xtQVZYU4rA==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.5.tgz", + "integrity": "sha512-KdnSkHxWrJ6Y40ABu+ipTZeRhFtc8dowGyFsZY5prsmMSr1ZTG9zQawguN4/tunJ0wy3+kD54GaGwdcpwWAvZQ==", "dev": true, "optional": true }, "esbuild-linux-ppc64le": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.49.tgz", - "integrity": "sha512-WP9zR4HX6iCBmMFH+XHHng2LmdoIeUmBpL4aL2TR8ruzXyT4dWrJ5BSbT8iNo6THN8lod6GOmYDLq/dgZLalGw==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.5.tgz", + "integrity": "sha512-QdRHGeZ2ykl5P0KRmfGBZIHmqcwIsUKWmmpZTOq573jRWwmpfRmS7xOhmDHBj9pxv+6qRMH8tLr2fe+ZKQvCYw==", "dev": true, "optional": true }, "esbuild-linux-riscv64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.49.tgz", - "integrity": "sha512-h66ORBz+Dg+1KgLvzTVQEA1LX4XBd1SK0Fgbhhw4akpG/YkN8pS6OzYI/7SGENiN6ao5hETRDSkVcvU9NRtkMQ==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.5.tgz", + "integrity": "sha512-p+WE6RX+jNILsf+exR29DwgV6B73khEQV0qWUbzxaycxawZ8NE0wA6HnnTxbiw5f4Gx9sJDUBemh9v49lKOORA==", "dev": true, "optional": true }, "esbuild-linux-s390x": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.49.tgz", - "integrity": "sha512-DhrUoFVWD+XmKO1y7e4kNCqQHPs6twz6VV6Uezl/XHYGzM60rBewBF5jlZjG0nCk5W/Xy6y1xWeopkrhFFM0sQ==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.5.tgz", + "integrity": "sha512-J2ngOB4cNzmqLHh6TYMM/ips8aoZIuzxJnDdWutBw5482jGXiOzsPoEF4j2WJ2mGnm7FBCO4StGcwzOgic70JQ==", "dev": true, "optional": true }, "esbuild-netbsd-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.49.tgz", - "integrity": "sha512-BXaUwFOfCy2T+hABtiPUIpWjAeWK9P8O41gR4Pg73hpzoygVGnj0nI3YK4SJhe52ELgtdgWP/ckIkbn2XaTxjQ==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.5.tgz", + "integrity": "sha512-MmKUYGDizYjFia0Rwt8oOgmiFH7zaYlsoQ3tIOfPxOqLssAsEgG0MUdRDm5lliqjiuoog8LyDu9srQk5YwWF3w==", "dev": true, "optional": true }, "esbuild-openbsd-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.49.tgz", - "integrity": "sha512-lP06UQeLDGmVPw9Rg437Btu6J9/BmyhdoefnQ4gDEJTtJvKtQaUcOQrhjTq455ouZN4EHFH1h28WOJVANK41kA==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.5.tgz", + "integrity": "sha512-2mMFfkLk3oPWfopA9Plj4hyhqHNuGyp5KQyTT9Rc8hFd8wAn5ZrbJg+gNcLMo2yzf8Uiu0RT6G9B15YN9WQyMA==", "dev": true, "optional": true }, "esbuild-sunos-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.49.tgz", - "integrity": "sha512-4c8Zowp+V3zIWje329BeLbGh6XI9c/rqARNaj5yPHdC61pHI9UNdDxT3rePPJeWcEZVKjkiAS6AP6kiITp7FSw==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.5.tgz", + "integrity": "sha512-2sIzhMUfLNoD+rdmV6AacilCHSxZIoGAU2oT7XmJ0lXcZWnCvCtObvO6D4puxX9YRE97GodciRGDLBaiC6x1SA==", "dev": true, "optional": true }, "esbuild-wasm": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.14.49.tgz", - "integrity": "sha512-5ddzZv8M3WI1fWZ5rEfK5cSA9swlWJcceKgqjKLLERC7FnlNW50kF7hxhpkyC0Z/4w7Xeyt3yUJ9QWNMDXLk2Q==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.15.5.tgz", + "integrity": "sha512-lTJOEKekN/4JI/eOEq0wLcx53co2N6vaT/XjBz46D1tvIVoUEyM0o2K6txW6gEotf31szFD/J1PbxmnbkGlK9A==", "dev": true }, "esbuild-windows-32": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.49.tgz", - "integrity": "sha512-q7Rb+J9yHTeKr9QTPDYkqfkEj8/kcKz9lOabDuvEXpXuIcosWCJgo5Z7h/L4r7rbtTH4a8U2FGKb6s1eeOHmJA==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.5.tgz", + "integrity": "sha512-e+duNED9UBop7Vnlap6XKedA/53lIi12xv2ebeNS4gFmu7aKyTrok7DPIZyU5w/ftHD4MUDs5PJUkQPP9xJRzg==", "dev": true, "optional": true }, "esbuild-windows-64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.49.tgz", - "integrity": "sha512-+Cme7Ongv0UIUTniPqfTX6mJ8Deo7VXw9xN0yJEN1lQMHDppTNmKwAM3oGbD/Vqff+07K2gN0WfNkMohmG+dVw==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.5.tgz", + "integrity": "sha512-v+PjvNtSASHOjPDMIai9Yi+aP+Vwox+3WVdg2JB8N9aivJ7lyhp4NVU+J0MV2OkWFPnVO8AE/7xH+72ibUUEnw==", "dev": true, "optional": true }, "esbuild-windows-arm64": { - "version": "0.14.49", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.49.tgz", - "integrity": "sha512-v+HYNAXzuANrCbbLFJ5nmO3m5y2PGZWLe3uloAkLt87aXiO2mZr3BTmacZdjwNkNEHuH3bNtN8cak+mzVjVPfA==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.5.tgz", + "integrity": "sha512-Yz8w/D8CUPYstvVQujByu6mlf48lKmXkq6bkeSZZxTA626efQOJb26aDGLzmFWx6eg/FwrXgt6SZs9V8Pwy/aA==", "dev": true, "optional": true }, @@ -15840,14 +15742,14 @@ } }, "express": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", - "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", "dev": true, "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.0", + "body-parser": "1.20.1", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.5.0", @@ -15866,7 +15768,7 @@ "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", "proxy-addr": "~2.0.7", - "qs": "6.10.3", + "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.18.0", @@ -15938,7 +15840,9 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "external-editor": { "version": "3.1.0", @@ -16017,6 +15921,8 @@ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", "dev": true, + "optional": true, + "peer": true, "requires": { "debug": "2.6.9", "encodeurl": "~1.0.2", @@ -16032,6 +15938,8 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "optional": true, + "peer": true, "requires": { "ms": "2.0.0" } @@ -16040,13 +15948,17 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", "dev": true, + "optional": true, + "peer": true, "requires": { "ee-first": "1.1.1" } @@ -16078,7 +15990,9 @@ "version": "3.2.6", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.6.tgz", "integrity": "sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "follow-redirects": { "version": "1.15.1", @@ -16109,6 +16023,8 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, + "optional": true, + "peer": true, "requires": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -16313,9 +16229,9 @@ "dev": true }, "hosted-git-info": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.0.0.tgz", - "integrity": "sha512-rRnjWu0Bxj+nIfUOkz0695C0H6tRrN5iYIzYejb0tDEefe2AekHu/U5Kn9pEie5vsJqpNQU02az7TGSH3qpz4Q==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.1.0.tgz", + "integrity": "sha512-Ek+QmMEqZF8XrbFdwoDjSbm7rT23pCgEMOJmz6GPk/s4yH//RQfNPArhIxbguNxROq/+5lNBwCDHMhA903Kx1Q==", "dev": true, "requires": { "lru-cache": "^7.5.1" @@ -16365,12 +16281,6 @@ "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", "dev": true }, - "html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, "http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", @@ -16785,7 +16695,9 @@ "version": "4.0.10", "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "isexe": { "version": "2.0.0", @@ -16826,69 +16738,6 @@ } } }, - "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "istanbul-reports": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", - "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", - "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, - "jasmine-core": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.10.1.tgz", - "integrity": "sha512-ooZWSDVAdh79Rrj4/nnfklL3NQVra0BcuhcuWoAwwi+znLDoUeH87AFfeX8s+YeYi6xlv5nveRyaA1v7CintfA==", - "dev": true - }, "jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -16968,6 +16817,8 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, + "optional": true, + "peer": true, "requires": { "graceful-fs": "^4.1.6", "universalify": "^2.0.0" @@ -16984,6 +16835,8 @@ "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.0.tgz", "integrity": "sha512-s8m7z0IF5g/bS5ONT7wsOavhW4i4aFkzD4u4wgzAQWT4HGUeWI3i21cK2Yz6jndMAeHETp5XuNsRoyGJZXVd4w==", "dev": true, + "optional": true, + "peer": true, "requires": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -17016,6 +16869,8 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "optional": true, + "peer": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -17026,6 +16881,8 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, + "optional": true, + "peer": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -17040,6 +16897,8 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "optional": true, + "peer": true, "requires": { "brace-expansion": "^1.1.7" } @@ -17049,6 +16908,8 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, + "optional": true, + "peer": true, "requires": { "minimist": "^1.2.6" } @@ -17057,13 +16918,17 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", "dev": true, + "optional": true, + "peer": true, "requires": { "rimraf": "^3.0.0" } @@ -17073,6 +16938,8 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, + "optional": true, + "peer": true, "requires": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -17087,96 +16954,12 @@ "version": "20.2.9", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true - } - } - }, - "karma-chrome-launcher": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.1.tgz", - "integrity": "sha512-hsIglcq1vtboGPAN+DGCISCFOxW+ZVnIqhDQcCMqqCp+4dmJ0Qpq5QAjkbA0X2L9Mi6OBkHi2Srrbmm7pUKkzQ==", - "dev": true, - "requires": { - "which": "^1.2.1" - } - }, - "karma-coverage": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.1.1.tgz", - "integrity": "sha512-oxeOSBVK/jdZsiX03LhHQkO4eISSQb5GbHi6Nsw3Mw7G4u6yUgacBAftnO7q+emPBLMsrNbz1pGIrj+Jb3z17A==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-instrument": "^4.0.3", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.1", - "istanbul-reports": "^3.0.5", - "minimatch": "^3.0.4" - }, - "dependencies": { - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "istanbul-lib-instrument": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", - "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", - "dev": true, - "requires": { - "@babel/core": "^7.7.5", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "karma-jasmine": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", - "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", - "dev": true, - "requires": { - "jasmine-core": "^4.1.0" - }, - "dependencies": { - "jasmine-core": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.2.0.tgz", - "integrity": "sha512-OcFpBrIhnbmb9wfI8cqPSJ50pv3Wg4/NSgoZIqHzIwO/2a9qivJWzv8hUvaREIMYYJBas6AvfXATFdVuzzCqVw==", - "dev": true + "optional": true, + "peer": true } } }, - "karma-jasmine-html-reporter": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.7.0.tgz", - "integrity": "sha512-pzum1TL7j90DTE86eFt48/s12hqwQuiD+e5aXx2Dc9wDEn2LfGq6RoAxEZZjFiN0RDSCOnosEKRZWxbQ+iMpQQ==", - "dev": true, - "requires": {} - }, "karma-source-map-support": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", @@ -17380,6 +17163,8 @@ "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.6.0.tgz", "integrity": "sha512-3v8R7fd45UB6THucSht6wN2/7AZEruQbXdjygPZcxt5TA/msO6si9CN5MefUuKXbYnJHTBnYcx4famwcyQd+sA==", "dev": true, + "optional": true, + "peer": true, "requires": { "date-format": "^4.0.11", "debug": "^4.3.4", @@ -17389,9 +17174,9 @@ } }, "lru-cache": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.13.1.tgz", - "integrity": "sha512-CHqbAq7NFlW3RSnoWXLJBxCWaZVBrfa9UEHId2M3AW8iEBurbqduNexEUCGc3SHc6iCYXNJCDi903LajSVAEPQ==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz", + "integrity": "sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ==", "dev": true }, "magic-string": { @@ -17421,9 +17206,9 @@ } }, "make-fetch-happen": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.0.tgz", - "integrity": "sha512-OnEfCLofQVJ5zgKwGk55GaqosqKjaR6khQlJY3dBAA+hM25Bc5CmX5rKUfVut+rYA3uidA7zb7AvcglU87rPRg==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", "dev": true, "requires": { "agentkeepalive": "^4.2.1", @@ -17497,7 +17282,9 @@ "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "mime-db": { "version": "1.52.0", @@ -17562,7 +17349,9 @@ "version": "1.2.6", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "minipass": { "version": "3.3.4", @@ -17583,9 +17372,9 @@ } }, "minipass-fetch": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.0.tgz", - "integrity": "sha512-H9U4UVBGXEyyWJnqYDCLp1PwD8XIkJ4akNHp1aGVI+2Ym7wQMlxDKi4IB4JbmyU+pl9pEs/cVrK6cOuvmbK4Sg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", "dev": true, "requires": { "encoding": "^0.1.13", @@ -17804,15 +17593,6 @@ "requires": { "brace-expansion": "^1.1.7" } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } } } }, @@ -17839,9 +17619,9 @@ } }, "normalize-package-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-4.0.0.tgz", - "integrity": "sha512-m+GL22VXJKkKbw62ZaBBjv8u6IE3UI4Mh5QakIqs3fWiKe0Xyi6L97hakwZK41/LD4R/2ly71Bayx0NLMwLA/g==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-4.0.1.tgz", + "integrity": "sha512-EBk5QKKuocMJhB3BILuKhmaPjI8vNRSpIfO9woLC6NyHVkKKdVEdAO1mrT0ZfxNR1lKwCcTkuZfmGIFdizZ8Pg==", "dev": true, "requires": { "hosted-git-info": "^5.0.0", @@ -17899,15 +17679,32 @@ } }, "npm-packlist": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-5.1.1.tgz", - "integrity": "sha512-UfpSvQ5YKwctmodvPPkK6Fwk603aoVsf8AEbmVKAEECrfvL8SSe1A2YIwrJ6xmTHAITKPwwZsWo7WwEbNk0kxw==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-5.1.3.tgz", + "integrity": "sha512-263/0NGrn32YFYi4J533qzrQ/krmmrWwhKkzwTuM4f/07ug51odoaNjUexxO4vxlzURHcmYMH1QjvHjsNDKLVg==", "dev": true, "requires": { "glob": "^8.0.1", "ignore-walk": "^5.0.1", - "npm-bundled": "^1.1.2", - "npm-normalize-package-bin": "^1.0.1" + "npm-bundled": "^2.0.0", + "npm-normalize-package-bin": "^2.0.0" + }, + "dependencies": { + "npm-bundled": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-2.0.1.tgz", + "integrity": "sha512-gZLxXdjEzE/+mOstGDqR6b0EkhJ+kM6fxM6vUuckuctuVPh80Q6pw/rSZj9s4Gex9GxWtIicO1pc8DB9KZWudw==", + "dev": true, + "requires": { + "npm-normalize-package-bin": "^2.0.0" + } + }, + "npm-normalize-package-bin": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-2.0.0.tgz", + "integrity": "sha512-awzfKUO7v0FscrSpRoogyNm0sajikhBWpU0QMrW09AMi9n1PoKU6WaIqUzuJSQnpciZZmJ/jMZ2Egfmb/9LiWQ==", + "dev": true + } } }, "npm-pick-manifest": { @@ -17923,9 +17720,9 @@ } }, "npm-registry-fetch": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-13.3.0.tgz", - "integrity": "sha512-10LJQ/1+VhKrZjIuY9I/+gQTvumqqlgnsCufoXETHAPFTS3+M+Z5CFhZRDHGavmJ6rOye3UvNga88vl8n1r6gg==", + "version": "13.3.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-13.3.1.tgz", + "integrity": "sha512-eukJPi++DKRTjSBRcDZSDDsGqRK3ehbxfFUcgaRd0Yp6kRwOwh2WVn0r+8rMB4nnuzvAk6rQVzl6K5CkYOmnvw==", "dev": true, "requires": { "make-fetch-happen": "^10.0.6", @@ -17971,7 +17768,9 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "object-inspect": { "version": "1.12.2", @@ -17986,14 +17785,14 @@ "dev": true }, "object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", "dev": true, "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", "object-keys": "^1.1.1" } }, @@ -18173,9 +17972,9 @@ "dev": true }, "pacote": { - "version": "13.6.1", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-13.6.1.tgz", - "integrity": "sha512-L+2BI1ougAPsFjXRyBhcKmfT016NscRFLv6Pz5EiNf1CCFJFU0pSKKQwsZTyAQB+sTuUL4TyFyp6J1Ork3dOqw==", + "version": "13.6.2", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-13.6.2.tgz", + "integrity": "sha512-Gu8fU3GsvOPkak2CkbojR7vjs3k3P9cA6uazKTHdsdV0gpCEQq2opelnEv30KRQWgVzP5Vd/5umjcedma3MKtg==", "dev": true, "requires": { "@npmcli/git": "^3.0.0", @@ -18350,9 +18149,9 @@ } }, "postcss": { - "version": "8.4.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", - "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "version": "8.4.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", + "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", "dev": true, "requires": { "nanoid": "^3.3.4", @@ -18502,9 +18301,9 @@ } }, "postcss-import": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", - "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.0.0.tgz", + "integrity": "sha512-Y20shPQ07RitgBGv2zvkEAu9bqvrD77C9axhj/aA1BQj4czape2MdClCExvB27EwYEJdGgKZBpKanb0t1rK2Kg==", "dev": true, "requires": { "postcss-value-parser": "^4.0.0", @@ -18632,57 +18431,59 @@ } }, "postcss-preset-env": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.7.2.tgz", - "integrity": "sha512-1q0ih7EDsZmCb/FMDRvosna7Gsbdx8CvYO5hYT120hcp2ZAuOHpSzibujZ4JpIUcAC02PG6b+eftxqjTFh5BNA==", - "dev": true, - "requires": { - "@csstools/postcss-cascade-layers": "^1.0.4", - "@csstools/postcss-color-function": "^1.1.0", - "@csstools/postcss-font-format-keywords": "^1.0.0", - "@csstools/postcss-hwb-function": "^1.0.1", - "@csstools/postcss-ic-unit": "^1.0.0", - "@csstools/postcss-is-pseudo-class": "^2.0.6", - "@csstools/postcss-normalize-display-values": "^1.0.0", - "@csstools/postcss-oklab-function": "^1.1.0", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.0.tgz", + "integrity": "sha512-leqiqLOellpLKfbHkD06E04P6d9ZQ24mat6hu4NSqun7WG0UhspHR5Myiv/510qouCjoo4+YJtNOqg5xHaFnCA==", + "dev": true, + "requires": { + "@csstools/postcss-cascade-layers": "^1.0.5", + "@csstools/postcss-color-function": "^1.1.1", + "@csstools/postcss-font-format-keywords": "^1.0.1", + "@csstools/postcss-hwb-function": "^1.0.2", + "@csstools/postcss-ic-unit": "^1.0.1", + "@csstools/postcss-is-pseudo-class": "^2.0.7", + "@csstools/postcss-nested-calc": "^1.0.0", + "@csstools/postcss-normalize-display-values": "^1.0.1", + "@csstools/postcss-oklab-function": "^1.1.1", "@csstools/postcss-progressive-custom-properties": "^1.3.0", - "@csstools/postcss-stepped-value-functions": "^1.0.0", - "@csstools/postcss-trigonometric-functions": "^1.0.1", - "@csstools/postcss-unset-value": "^1.0.1", - "autoprefixer": "^10.4.7", - "browserslist": "^4.21.0", + "@csstools/postcss-stepped-value-functions": "^1.0.1", + "@csstools/postcss-text-decoration-shorthand": "^1.0.0", + "@csstools/postcss-trigonometric-functions": "^1.0.2", + "@csstools/postcss-unset-value": "^1.0.2", + "autoprefixer": "^10.4.8", + "browserslist": "^4.21.3", "css-blank-pseudo": "^3.0.3", "css-has-pseudo": "^3.0.4", "css-prefers-color-scheme": "^6.0.3", - "cssdb": "^6.6.3", - "postcss-attribute-case-insensitive": "^5.0.1", + "cssdb": "^7.0.0", + "postcss-attribute-case-insensitive": "^5.0.2", "postcss-clamp": "^4.1.0", - "postcss-color-functional-notation": "^4.2.3", + "postcss-color-functional-notation": "^4.2.4", "postcss-color-hex-alpha": "^8.0.4", - "postcss-color-rebeccapurple": "^7.1.0", + "postcss-color-rebeccapurple": "^7.1.1", "postcss-custom-media": "^8.0.2", "postcss-custom-properties": "^12.1.8", "postcss-custom-selectors": "^6.0.3", - "postcss-dir-pseudo-class": "^6.0.4", - "postcss-double-position-gradients": "^3.1.1", + "postcss-dir-pseudo-class": "^6.0.5", + "postcss-double-position-gradients": "^3.1.2", "postcss-env-function": "^4.0.6", "postcss-focus-visible": "^6.0.4", "postcss-focus-within": "^5.0.4", "postcss-font-variant": "^5.0.0", - "postcss-gap-properties": "^3.0.3", - "postcss-image-set-function": "^4.0.6", + "postcss-gap-properties": "^3.0.5", + "postcss-image-set-function": "^4.0.7", "postcss-initial": "^4.0.1", - "postcss-lab-function": "^4.2.0", + "postcss-lab-function": "^4.2.1", "postcss-logical": "^5.0.4", "postcss-media-minmax": "^5.0.0", - "postcss-nesting": "^10.1.9", + "postcss-nesting": "^10.1.10", "postcss-opacity-percentage": "^1.1.2", - "postcss-overflow-shorthand": "^3.0.3", + "postcss-overflow-shorthand": "^3.0.4", "postcss-page-break": "^3.0.4", - "postcss-place": "^7.0.4", - "postcss-pseudo-class-any-link": "^7.1.5", + "postcss-place": "^7.0.5", + "postcss-pseudo-class-any-link": "^7.1.6", "postcss-replace-overflow-wrap": "^4.0.0", - "postcss-selector-not": "^6.0.0", + "postcss-selector-not": "^6.0.1", "postcss-value-parser": "^4.2.0" } }, @@ -18796,12 +18597,14 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "dev": true, "requires": { "side-channel": "^1.0.4" @@ -18850,15 +18653,23 @@ } }, "read-package-json": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-5.0.1.tgz", - "integrity": "sha512-MALHuNgYWdGW3gKzuNMuYtcSSZbGQm94fAp16xt8VsYTLBjUSc55bLMKe6gzpWue0Tfi6CBgwCSdDAqutGDhMg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-5.0.2.tgz", + "integrity": "sha512-BSzugrt4kQ/Z0krro8zhTwV1Kd79ue25IhNN/VtHFy1mG/6Tluyi+msc0UpwaoQzxSHa28mntAjIZY6kEgfR9Q==", "dev": true, "requires": { "glob": "^8.0.1", "json-parse-even-better-errors": "^2.3.1", "normalize-package-data": "^4.0.0", - "npm-normalize-package-bin": "^1.0.1" + "npm-normalize-package-bin": "^2.0.0" + }, + "dependencies": { + "npm-normalize-package-bin": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-2.0.0.tgz", + "integrity": "sha512-awzfKUO7v0FscrSpRoogyNm0sajikhBWpU0QMrW09AMi9n1PoKU6WaIqUzuJSQnpciZZmJ/jMZ2Egfmb/9LiWQ==", + "dev": true + } } }, "read-package-json-fast": { @@ -19063,7 +18874,9 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "rimraf": { "version": "3.0.2", @@ -19125,9 +18938,9 @@ } }, "rxjs": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz", - "integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz", + "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==", "requires": { "tslib": "^2.1.0" } @@ -19145,9 +18958,9 @@ "dev": true }, "sass": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.53.0.tgz", - "integrity": "sha512-zb/oMirbKhUgRQ0/GFz8TSAwRq2IlR29vOUJZOx0l8sV+CkHUfHa4u5nqrG+1VceZp7Jfj59SVW9ogdhTvJDcQ==", + "version": "1.54.4", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.54.4.tgz", + "integrity": "sha512-3tmF16yvnBwtlPrNBHw/H907j8MlOX8aTBnlNX1yrKx24RKcJGPyLhFUwkoKBKesR3unP93/2z14Ll8NicwQUA==", "dev": true, "requires": { "chokidar": ">=3.0.0 <4.0.0", @@ -19216,9 +19029,9 @@ "dev": true }, "selfsigned": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.1.tgz", - "integrity": "sha512-LmME957M1zOsUhG+67rAjKfiWFox3SBxE/yymatMZsAx+oMrJ0YQ8AToOnyCm7xbeg2ep37IHLxdu0o2MavQOQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", + "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", "dev": true, "requires": { "node-forge": "^1" @@ -19455,6 +19268,8 @@ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.1.tgz", "integrity": "sha512-0y9pnIso5a9i+lJmsCdtmTTgJFFSvNQKDnPQRz28mGNnxbmqYg2QPtJTLFxhymFZhAIn50eHAKzJeiNaKr+yUQ==", "dev": true, + "optional": true, + "peer": true, "requires": { "accepts": "~1.3.4", "base64id": "~2.0.0", @@ -19468,13 +19283,17 @@ "version": "2.4.0", "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz", "integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "socket.io-parser": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.5.tgz", "integrity": "sha512-sNjbT9dX63nqUFIOv95tTVm6elyIU4RvB1m8dOeZt+IgWwcWklFDOdmGcfo3zSiRsnR/3pJkjY5lfoGqEe4Eig==", "dev": true, + "optional": true, + "peer": true, "requires": { "@types/component-emitter": "^1.2.10", "component-emitter": "~1.3.0", @@ -19547,16 +19366,6 @@ } } }, - "source-map-resolve": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", - "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", - "dev": true, - "requires": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0" - } - }, "source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -19608,9 +19417,9 @@ } }, "spdx-license-ids": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz", - "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==", + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", + "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==", "dev": true }, "spdy": { @@ -19666,6 +19475,8 @@ "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.1.tgz", "integrity": "sha512-iPhtd9unZ6zKdWgMeYGfSBuqCngyJy1B/GPi/lTpwGpa3bajuX30GjUVd0/Tn/Xhg0mr4DOSENozz9Y06qyonQ==", "dev": true, + "optional": true, + "peer": true, "requires": { "date-format": "^4.0.10", "debug": "^4.3.4", @@ -19716,12 +19527,12 @@ "dev": true }, "stylus": { - "version": "0.58.1", - "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.58.1.tgz", - "integrity": "sha512-AYiCHm5ogczdCPMfe9aeQa4NklB2gcf4D/IhzYPddJjTgPc+k4D/EVE0yfQbZD43MHP3lPy+8NZ9fcFxkrgs/w==", + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.59.0.tgz", + "integrity": "sha512-lQ9w/XIOH5ZHVNuNbWW8D822r+/wBSO/d6XvtyHLF7LW4KaCIDeVbvn5DF8fGCJAUCwVhVi/h6J0NUcnylUEjg==", "dev": true, "requires": { - "css": "^3.0.0", + "@adobe/css-tools": "^4.0.1", "debug": "^4.3.2", "glob": "^7.1.6", "sax": "~1.2.4", @@ -20006,16 +19817,18 @@ "dev": true }, "typescript": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", "dev": true }, "ua-parser-js": { "version": "0.7.31", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz", "integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "unicode-canonical-property-names-ecmascript": { "version": "2.0.0", @@ -20067,7 +19880,9 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "unpipe": { "version": "1.0.0", @@ -20076,9 +19891,9 @@ "dev": true }, "update-browserslist-db": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.4.tgz", - "integrity": "sha512-jnmO2BEGUjsMOe/Fg9u0oczOe/ppIDZPebzccl1yDWGLFP16Pa1/RM5wEoKYPG2zstNcDuAStejyxsOuKINdGA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz", + "integrity": "sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==", "dev": true, "requires": { "escalade": "^3.1.1", @@ -20146,7 +19961,9 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "watchpack": { "version": "2.4.0", @@ -20177,9 +19994,9 @@ } }, "webpack": { - "version": "5.73.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.73.0.tgz", - "integrity": "sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA==", + "version": "5.74.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.74.0.tgz", + "integrity": "sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.3", @@ -20187,11 +20004,11 @@ "@webassemblyjs/ast": "1.11.1", "@webassemblyjs/wasm-edit": "1.11.1", "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.4.1", + "acorn": "^8.7.1", "acorn-import-assertions": "^1.7.6", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.9.3", + "enhanced-resolve": "^5.10.0", "es-module-lexer": "^0.9.0", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -20204,7 +20021,7 @@ "schema-utils": "^3.1.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.3.1", + "watchpack": "^2.4.0", "webpack-sources": "^3.2.3" }, "dependencies": { @@ -20274,9 +20091,9 @@ } }, "webpack-dev-server": { - "version": "4.9.3", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.9.3.tgz", - "integrity": "sha512-3qp/eoboZG5/6QgiZ3llN8TUzkSpYg1Ko9khWX1h40MIEUNS2mDoIa8aXsPfskER+GbTvs/IJZ1QTBBhhuetSw==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.11.0.tgz", + "integrity": "sha512-L5S4Q2zT57SK7tazgzjMiSMBdsw+rGYIX27MgPgx7LDhWO0lViPrHKoLS7jo5In06PWYAhlYu3PbyoC6yAThbw==", "dev": true, "requires": { "@types/bonjour": "^3.5.9", @@ -20323,9 +20140,9 @@ } }, "ws": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", - "integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.9.0.tgz", + "integrity": "sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==", "dev": true, "requires": {} } @@ -20374,9 +20191,9 @@ "dev": true }, "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "requires": { "isexe": "^2.0.0" @@ -20445,6 +20262,8 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", "dev": true, + "optional": true, + "peer": true, "requires": {} }, "y18n": { @@ -20487,9 +20306,9 @@ "dev": true }, "zone.js": { - "version": "0.11.7", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.11.7.tgz", - "integrity": "sha512-e39K2EdK5JfA3FDuUTVRvPlYV4aBfnOOcGuILhQAT7nzeV12uSrLBzImUM9CDVoncDSX4brR/gwqu0heQ3BQ0g==", + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.11.8.tgz", + "integrity": "sha512-82bctBg2hKcEJ21humWIkXRlLBBmrc3nN7DFh5LGGhcyycO2S7FN8NmdvlcKaGFDNVL4/9kFLmwmInTavdJERA==", "requires": { "tslib": "^2.3.0" } diff --git a/vscode4teaching-webapp/package.json b/vscode4teaching-webapp/package.json index d0e1e6fd..15c0c0ef 100644 --- a/vscode4teaching-webapp/package.json +++ b/vscode4teaching-webapp/package.json @@ -1,42 +1,34 @@ { "name": "vscode4teaching-webapp", - "version": "2.1.4", + "version": "2.2.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build --base-href=\"/app\"", - "watch": "ng build --watch --configuration development", - "test": "ng test" + "watch": "ng build --watch --configuration development" }, "private": true, "dependencies": { - "@angular/animations": "^14.1.0", - "@angular/common": "^14.1.0", - "@angular/compiler": "^14.1.0", - "@angular/core": "^14.1.0", - "@angular/forms": "^14.1.0", - "@angular/platform-browser": "^14.1.0", - "@angular/platform-browser-dynamic": "^14.1.0", - "@angular/router": "^14.1.0", - "@fortawesome/fontawesome-free": "^6.1.2", - "bootstrap": "^5.2.0", + "@angular/animations": "^14.2.7", + "@angular/common": "^14.2.7", + "@angular/compiler": "^14.2.7", + "@angular/core": "^14.2.7", + "@angular/forms": "^14.2.7", + "@angular/platform-browser": "^14.2.7", + "@angular/platform-browser-dynamic": "^14.2.7", + "@angular/router": "^14.2.7", + "@fortawesome/fontawesome-free": "^6.2.0", + "bootstrap": "^5.2.2", "ngx-logger": "^5.0.11", - "rxjs": "^7.5.6", + "rxjs": "^7.5.7", "tslib": "^2.3.0", - "zone.js": "^0.11.7" + "zone.js": "^0.11.8" }, "devDependencies": { - "@angular-devkit/build-angular": "^14.1.0", - "@angular/cli": "^14.1.0", - "@angular/compiler-cli": "^14.1.0", - "@types/jasmine": "~3.10.0", + "@angular-devkit/build-angular": "^14.2.6", + "@angular/cli": "^14.2.6", + "@angular/compiler-cli": "^14.2.7", "@types/node": "^12.20.55", - "jasmine-core": "~3.10.0", - "karma": "^6.3.18", - "karma-chrome-launcher": "^3.1.1", - "karma-coverage": "~2.1.0", - "karma-jasmine": "^5.0.0", - "karma-jasmine-html-reporter": "~1.7.0", - "typescript": "^4.7.4" + "typescript": "^4.8.4" } } diff --git a/vscode4teaching-webapp/src/app/components/index/index.component.html b/vscode4teaching-webapp/src/app/components/index/index.component.html index 22634611..102e8c85 100644 --- a/vscode4teaching-webapp/src/app/components/index/index.component.html +++ b/vscode4teaching-webapp/src/app/components/index/index.component.html @@ -52,8 +52,12 @@ <h3>Now you can do the exercise! When you save a file, its contents will be able <img class="demo-gif" src="/assets/img/gif4_doingexercise.gif" alt="Animated image of demo student editing an exercise" /> <h3>When you finish the exercise, press the Finish button:</h3> <img class="demo-gif" src="/assets/img/gif5_finishexercise.gif" alt="Animated image of a demo student marking an exercise as finished" /> - <h3>New exercise! Refresh the list of exercises in the course:</h3> - <img class="demo-gif" src="/assets/img/gif6_newexercise.gif" alt="Animated image of demo student refreshing exercise list" /> + <h3>If your teacher included a solution, you can download it:</h3> + <img class="demo-gif" src="/assets/img/gif6_downloadsolution.gif" alt="Animated image of a demo student downloading the teacher's solution to an exercise" /> + <h3>When downloaded, you can also check the differences between your proposal and your teacher's solution:</h3> + <img class="demo-gif" src="/assets/img/gif7_diffwithsolution.gif" alt="Animated image of a demo student viewing the differences between his or her exercise and the teacher's solution" /> + <h3>New exercise!</h3> + <img class="demo-gif" src="/assets/img/gif8_newexercise.gif" alt="Animated image of demo student refreshing exercise list" /> </ng-container> </section> -</div> +</div> \ No newline at end of file diff --git a/vscode4teaching-webapp/src/assets/img/gif1_signup.gif b/vscode4teaching-webapp/src/assets/img/gif1_signup.gif index 613a5cd2..ea64ca38 100644 Binary files a/vscode4teaching-webapp/src/assets/img/gif1_signup.gif and b/vscode4teaching-webapp/src/assets/img/gif1_signup.gif differ diff --git a/vscode4teaching-webapp/src/assets/img/gif2_login.gif b/vscode4teaching-webapp/src/assets/img/gif2_login.gif index 12499b85..0a6b5400 100644 Binary files a/vscode4teaching-webapp/src/assets/img/gif2_login.gif and b/vscode4teaching-webapp/src/assets/img/gif2_login.gif differ diff --git a/vscode4teaching-webapp/src/assets/img/gif3_joincourse.gif b/vscode4teaching-webapp/src/assets/img/gif3_joincourse.gif index d5fbbe32..2ec00620 100644 Binary files a/vscode4teaching-webapp/src/assets/img/gif3_joincourse.gif and b/vscode4teaching-webapp/src/assets/img/gif3_joincourse.gif differ diff --git a/vscode4teaching-webapp/src/assets/img/gif4_doingexercise.gif b/vscode4teaching-webapp/src/assets/img/gif4_doingexercise.gif index 4645f491..a7b44b16 100644 Binary files a/vscode4teaching-webapp/src/assets/img/gif4_doingexercise.gif and b/vscode4teaching-webapp/src/assets/img/gif4_doingexercise.gif differ diff --git a/vscode4teaching-webapp/src/assets/img/gif5_finishexercise.gif b/vscode4teaching-webapp/src/assets/img/gif5_finishexercise.gif index 8bf2610f..46b38a27 100644 Binary files a/vscode4teaching-webapp/src/assets/img/gif5_finishexercise.gif and b/vscode4teaching-webapp/src/assets/img/gif5_finishexercise.gif differ diff --git a/vscode4teaching-webapp/src/assets/img/gif6_downloadsolution.gif b/vscode4teaching-webapp/src/assets/img/gif6_downloadsolution.gif new file mode 100644 index 00000000..9d9fe178 Binary files /dev/null and b/vscode4teaching-webapp/src/assets/img/gif6_downloadsolution.gif differ diff --git a/vscode4teaching-webapp/src/assets/img/gif6_newexercise.gif b/vscode4teaching-webapp/src/assets/img/gif6_newexercise.gif deleted file mode 100644 index daab27ef..00000000 Binary files a/vscode4teaching-webapp/src/assets/img/gif6_newexercise.gif and /dev/null differ diff --git a/vscode4teaching-webapp/src/assets/img/gif7_diffwithsolution.gif b/vscode4teaching-webapp/src/assets/img/gif7_diffwithsolution.gif new file mode 100644 index 00000000..1f53181b Binary files /dev/null and b/vscode4teaching-webapp/src/assets/img/gif7_diffwithsolution.gif differ diff --git a/vscode4teaching-webapp/src/assets/img/gif8_newexercise.gif b/vscode4teaching-webapp/src/assets/img/gif8_newexercise.gif new file mode 100644 index 00000000..9da8964f Binary files /dev/null and b/vscode4teaching-webapp/src/assets/img/gif8_newexercise.gif differ