diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000000..0cd51c5fb97 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,25 @@ +name: MarkBind Action + +on: + push: + branches: + - master + +jobs: + build_and_deploy: + runs-on: ubuntu-latest + steps: + - name: Install Graphviz + run: sudo apt-get install graphviz + - name: Install Java + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + - name: Build & Deploy MarkBind site + uses: MarkBind/markbind-action@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + rootDirectory: './docs' + baseUrl: '/tp' # assuming your repo name is tp + version: '^5.2.0' diff --git a/.gitignore b/.gitignore index 284c4ca7cd9..46fb10c3260 100644 --- a/.gitignore +++ b/.gitignore @@ -14,10 +14,21 @@ src/main/resources/docs/ /preferences.json /*.log.* hs_err_pid[0-9]*.log +/addressbookdata/ # Test sandbox files src/test/data/sandbox/ +src/test/data/ExportCommandTest/addressbookdata/ +src/test/data/ExportCommandTest/filteredAddressBook.json # MacOS custom attributes files created by Finder .DS_Store docs/_site/ +docs/_markbind/logs/ + +# Markbind generated files +_markbind/ + +#VSCode files +.vscode/ +bin/ diff --git a/README.md b/README.md index 13f5c77403f..85dc43396b8 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,24 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) +[![CI Status](https://github.com/AY2324S2-CS2103T-T10-1/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2324S2-CS2103T-T10-1/tp/actions) +[![codecov](https://codecov.io/gh/AY2324S2-CS2103T-T10-1/tp/graph/badge.svg?token=6NGZ4VS4VC)](https://app.codecov.io/gh/AY2324S2-CS2103T-T10-1/tp) + + +## Avengers Assemble ![Ui](docs/images/Ui.png) -* This is **a sample project for Software Engineering (SE) students**.
- Example usages: - * as a starting point of a course project (as opposed to writing everything from scratch) - * as a case study -* The project simulates an ongoing software project for a desktop application (called _AddressBook_) used for managing contact details. - * It is **written in OOP fashion**. It provides a **reasonably well-written** code base **bigger** (around 6 KLoC) than what students usually write in beginner-level SE modules, without being overwhelmingly big. - * It comes with a **reasonable level of user and developer documentation**. -* It is named `AddressBook Level 3` (`AB3` for short) because it was initially created as a part of a series of `AddressBook` projects (`Level 1`, `Level 2`, `Level 3` ...). -* For the detailed documentation of this project, see the **[Address Book Product Website](https://se-education.org/addressbook-level3)**. -* This project is a **part of the se-education.org** initiative. If you would like to contribute code to this project, see [se-education.org](https://se-education.org#https://se-education.org/#contributing) for more info. +Avengers Assemble is a contact management app, meant for use with a Command Line Interface (CLI) while still maintaining the benefits of a Graphical User Interface (GUI). + +This application is designed for Head Tutors of the CS1101S Programming Methodology course to manage the contact details of students, tutors and other staff members. Its use cases may also be relevant to Head Tutors of other courses. + +### Project Links +* [Project Website](https://ay2324s2-cs2103t-t10-1.github.io/tp/) +* [User Guide](https://ay2324s2-cs2103t-t10-1.github.io/tp/UserGuide.html) +* [Developer Guide](https://ay2324s2-cs2103t-t10-1.github.io/tp/DeveloperGuide.html) +* [About Us](https://ay2324s2-cs2103t-t10-1.github.io/tp/AboutUs.html) + + +### Acknowledgements + +* This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). +* Libraries used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5), [OpenCSV](https://opencsv.sourceforge.net/) + diff --git a/build.gradle b/build.gradle index a2951cc709e..2eae589df69 100644 --- a/build.gradle +++ b/build.gradle @@ -58,15 +58,26 @@ dependencies { implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'linux' implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.7.0' + implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-csv', version: '2.7.4' implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.7.4' + implementation 'com.opencsv:opencsv:5.5.2' + + testImplementation('org.junit.platform:junit-platform-launcher:1.5.2') testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: jUnitVersion + testImplementation "org.mockito:mockito-core:3.+" + testImplementation group: 'org.powermock', name: 'powermock-api-mockito2', version: '2.0.9' + testImplementation group: 'org.powermock', name: 'powermock-module-junit4', version: '1.6.4' testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: jUnitVersion } shadowJar { - archiveFileName = 'addressbook.jar' + archiveFileName = 'avengersassemble.jar' +} + +run { + enableAssertions = true } defaultTasks 'clean', 'test' diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000000..1748e487fbd --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,23 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +_markbind/logs/ + +# Dependency directories +node_modules/ + +# Production build files (change if you output the build to a different directory) +_site/ + +# Env +.env +.env.local + +# IDE configs +.vscode/ +.idea/* +*.iml diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 1c9514e966a..1d248dc406f 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,59 +1,57 @@ --- -layout: page -title: About Us + layout: default.md + title: "About Us" --- +# About Us + We are a team based in the [School of Computing, National University of Singapore](http://www.comp.nus.edu.sg). You can reach us at the email `seer[at]comp.nus.edu.sg` ## Project team -### John Doe +### Johan Soo - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/delishad21)] -* Role: Project Advisor +* Role: Developer +* Responsibilities: Develop Code -### Jane Doe +### Ang Leng Khai - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/zer0legion)] -* Role: Team Lead -* Responsibilities: UI +* Role: Developer +* Responsibilities: Develop code -### Johnny Doe +### Loh Sze Han, Danielle - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](http://github.com/danielleloh)] [[portfolio](team/johndoe.md)] * Role: Developer -* Responsibilities: Data +* Responsibilities: Develop code -### Jean Doe +### Pughal Raj - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/Pughal77)] * Role: Developer -* Responsibilities: Dev Ops + Threading +* Responsibilities: Develop code -### James Doe +### Castillo James - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/jayllo-c)] * Role: Developer -* Responsibilities: UI +* Responsibilities: Develop Code diff --git a/docs/Configuration.md b/docs/Configuration.md index 13cf0faea16..32f6255f3b9 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -1,6 +1,8 @@ --- -layout: page -title: Configuration guide + layout: default.md + title: "Configuration guide" --- +# Configuration guide + Certain properties of the application can be controlled (e.g user preferences file location, logging level) through the configuration file (default: `config.json`). diff --git a/docs/DevOps.md b/docs/DevOps.md index d2fd91a6001..c725d7c67b7 100644 --- a/docs/DevOps.md +++ b/docs/DevOps.md @@ -1,12 +1,15 @@ --- -layout: page -title: DevOps guide + layout: default.md + title: "DevOps guide" + pageNav: 3 --- -* Table of Contents -{:toc} +# DevOps guide --------------------------------------------------------------------------------------------------------------------- + + + + ## Build automation @@ -73,7 +76,7 @@ Any warnings or errors will be printed out to the console. Here are the steps to create a new release. -1. Update the version number in [`MainApp.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/MainApp.java). +1. Update the version number in [`MainApp.java`](https://github.com/AY2324S2-CS2103T-T10-1/tp/tree/master/src/main/java/seedu/address/MainApp.java). 1. Generate a fat JAR file using Gradle (i.e., `gradlew shadowJar`). 1. Tag the repo with the version number. e.g. `v0.1` 1. [Create a new release using GitHub](https://help.github.com/articles/creating-releases/). Upload the JAR file you created. diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 1b56bb5d31b..3106535b761 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -1,34 +1,141 @@ --- -layout: page -title: Developer Guide +layout: default.md +title: "Developer Guide" --- -* Table of Contents -{:toc} + +# Avengers Assemble Developer Guide + + +## Table of Contents + +
+
    +
  1. Acknowledgements
  2. +
  3. Setting Up, Getting Started
  4. +
  5. Design +
      +
    1. Architecture
    2. +
    3. UI Component
    4. +
    5. Logic Component
    6. +
    7. Model Component
    8. +
    9. Storage Component
    10. +
    11. Common Classes
    12. +
    +
  6. +
  7. Implementation +
      +
    1. General Features +
        +
      1. Help Command
      2. +
      3. Clear Command
      4. +
      5. List Command
      6. +
      +
    2. Contact Management Features +
        +
      1. Add Person Command
      2. +
      3. Edit Person Command
      4. +
      5. Delete Person Command
      6. +
      7. Find Command
      8. +
      9. Delete Shown Command
      10. +
      11. Import Contacts Command
      12. +
      13. Copy Command
      14. +
      15. Export Command
      16. +
      17. Feature: Addition of Optional Fields (Matric)
      18. +
      19. Feature: Automatic Tagging of Persons
      20. +
      +
    3. +
    4. Exam Features +
        +
      1. Add Exam Command
      2. +
      3. Delete Exam Command
      4. +
      5. Sequence Diagrams Illustrating Exam Modification
      6. +
      7. Select Exam Command
      8. +
      9. Deselect Exam Command
      10. +
      11. Sequence Diagrams Illustrating Exam Selection
      12. +
      13. Considerations for Exam Features
      14. +
      +
    5. +
    6. Exam Score Features +
        +
      1. Add Score Command
      2. +
      3. Edit Score Command
      4. +
      5. Delete Score Command
      6. +
      7. Import Exam Scores Command
      8. +
      9. Score Statistics Feature
      10. +
      +
    7. +
    +
  8. +
  9. Planned Enhancements
  10. +
  11. Documentation, Logging, Testing, Configuration, Dev-ops
  12. +
  13. Appendix +
      +
    1. Appendix A: Product Scope
    2. +
    3. Appendix B: User Stories
    4. +
    5. Appendix C: Use Cases
    6. +
    7. Appendix D: Non-Functional Requirements
    8. +
    9. Appendix E: Glossary
    10. +
    11. Appendix F: Instructions for Manual Testing
    12. +
        +
      1. Launch and Shutdown
      2. +
      3. Saving Data
      4. +
      5. Getting Help
      6. +
      7. Clearing all Persons and Exams: clear
      8. +
      9. Importing persons: import
      10. +
      11. Adding a Person: add
      12. +
      13. Editing a Person: edit
      14. +
      15. Deleting a Person: delete
      16. +
      17. Deleting Shown Persons: deleteShown
      18. +
      19. Listing all Persons: list
      20. +
      21. Finding a Person: find
      22. +
      23. Copying Emails: copy
      24. +
      25. Exporting Data to a CSV File: export
      26. +
      27. Adding an Exam: addExam
      28. +
      29. Deleting an Exam: deleteExam
      30. +
      31. Selecting an Exam: selectExam
      32. +
      33. Deselecting an Exam: deselectExam
      34. +
      35. Importing Exam Scores: importExamScores
      36. +
      37. Adding a Persons's Exam Score: addScore
      38. +
      39. Editing a Person's Exam Score: editScore
      40. +
      41. Deleting a Person's Exam Score: deleteScore
      42. +
      43. Mean and Median of Exam Scores
      44. +
      +
    13. Appendix G: Effort
    14. +
    +
  14. -------------------------------------------------------------------------------------------------------------------- +
    ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). + +Features related to the creation and reading of CSV files were made possible through the use of the [OpenCSV](http://opencsv.sourceforge.net/) library. + +Our project made use of AI assistance from [GitHub Copilot](https://copilot.github.com/) to finish small snippets of code and to provide suggestions. -------------------------------------------------------------------------------------------------------------------- -## **Setting up, getting started** +
    + +## **Setting Up, Getting Started** Refer to the guide [_Setting up and getting started_](SettingUp.md). -------------------------------------------------------------------------------------------------------------------- -## **Design** +
    + +
    -
    +## **Design** -:bulb: **Tip:** The `.puml` files used to create diagrams in this document `docs/diagrams` folder. Refer to the [_PlantUML Tutorial_ at se-edu/guides](https://se-education.org/guides/tutorials/plantUml.html) to learn how to create and edit diagrams. -
    +
    ### Architecture - +

    The ***Architecture Diagram*** given above explains the high-level design of the App. @@ -36,7 +143,7 @@ Given below is a quick overview of main components and how they interact with ea **Main components of the architecture** -**`Main`** (consisting of classes [`Main`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/MainApp.java)) is in charge of the app launch and shut down. +**`Main`** (consisting of classes [`Main`](https://github.com/AY2324S2-CS2103T-T10-1/tp/tree/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/AY2324S2-CS2103T-T10-1/tp/tree/master/src/main/java/seedu/address/MainApp.java)) is in charge of the app launch and shut down. * At app launch, it initializes the other components in the correct sequence, and connects them up with each other. * At shut down, it shuts down the other components and invokes cleanup methods where necessary. @@ -49,334 +156,3309 @@ The bulk of the app's work is done by the following four components: [**`Commons`**](#common-classes) represents a collection of classes used by multiple other components. +
    + **How the architecture components interact with each other** The *Sequence Diagram* below shows how the components interact with each other for the scenario where the user issues the command `delete 1`. - +

    Each of the four main components (also shown in the diagram above), * defines its *API* in an `interface` with the same name as the Component. -* implements its functionality using a concrete `{Component Name}Manager` class (which follows the corresponding API `interface` mentioned in the previous point. +* implements its functionality using a concrete `{Component Name}Manager` class (which follows the corresponding API `interface` mentioned in the previous point.) For example, the `Logic` component defines its API in the `Logic.java` interface and implements its functionality using the `LogicManager.java` class which follows the `Logic` interface. Other components interact with a given component through its interface rather than the concrete class (reason: to prevent outside component's being coupled to the implementation of a component), as illustrated in the (partial) class diagram below. - +

    The sections below give more details of each component. -### UI component +
    -The **API** of this component is specified in [`Ui.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/Ui.java) +
    -![Structure of the UI Component](images/UiClassDiagram.png) +
    + +### UI Component + +The **API** of this component is specified in [`Ui.java`](https://github.com/AY2324S2-CS2103T-T10-1/tp/tree/master/src/main/java/seedu/address/ui/Ui.java) + +

    The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. -The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/resources/view/MainWindow.fxml) +The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/AY2324S2-CS2103T-T10-1/tp/tree/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/AY2324S2-CS2103T-T10-1/tp/tree/master/src/main/resources/view/MainWindow.fxml) + +The `CommandBox` takes in user input which is passed onto the `Logic` component for the user input to be parsed and executed. A `CommandResult` is returned after execution and the feedback is displayed to the user through the `ResultDisplay` component of the UI. + +For the updating of other components in the UI, after each command execution, `MainWindow` runs an update that calls the update method on `PersonListPanel`, `ExamListPanel` and `StatusBarFooter`. + +`PersonListPanel` and `ExamListPanel` update themselves by retrieving the `filteredPersonList` and `examList` from the `Model` component and updating the displayed lists accordingly. + +`StatusBarFooter` contains the mean and median feature, and it updates itself by retrieving `ScoreStatistics` +from the `Model` on update. + +
    -The `UI` component, +In summary, the `UI` component: * executes user commands using the `Logic` component. -* listens for changes to `Model` data so that the UI can be updated with the modified data. +* checks for changes to `Model` data so that the UI can be updated with the modified data. * keeps a reference to the `Logic` component, because the `UI` relies on the `Logic` to execute commands. -* depends on some classes in the `Model` component, as it displays `Person` object residing in the `Model`. +* depends on some classes in the `Model` component, as the `UI` updates based on items that are stored in `Model` -### Logic component +The sequence diagram below illustrates a more in-depth view of the interactions within the UI component -**API** : [`Logic.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/logic/Logic.java) +

    -Here's a (partial) class diagram of the `Logic` component: +#### Considerations For UI + +##### Dynamic UI Updates + +The UI is designed to update dynamically based on changes in the `Model`. We narrowed down to two design choices for updating the UI components. They are: + +1. **Update using listeners embedded into UI components** - This design choice would involve embedding listeners into the UI components that would listen for changes in the `Model` (e.g. adding a listener to filteredPersons in ExamListPanel). This would allow for a more loosely coupled system, but would involve more complex implementation which could get messy as the number of listeners increase. +2. **Update using a centralized update method** - This design choice involves having a centralized `update` method in the `MainWindow` that would call an `update` method in all other UI components after every command. This would involve a more tightly coupled system and may involve unnecessary updates, but would be easier to implement and maintain. + +We chose the second design choice as having a centralized update method would allow for easier maintenance, as there is a clear indicator of how UI components are updated from `MainWindow`. Adding extensions would also be more straightforward as future developers would know where to look for the update logic. + +With listeners, the update logic would be scattered across multiple UI component classes, making it much harder to search and add upon the update logic. + +One of our main goals was to make our codebase easy to understand and maintain, and we felt that the centralized update method would be more in line with this goal despite the slight increase in coupling and inefficiency. + +
    - +
    -The sequence diagram below illustrates the interactions within the `Logic` component, taking `execute("delete 1")` API call as an example. +
    -![Interactions Inside the Logic Component for the `delete 1` Command](images/DeleteSequenceDiagram.png) +### Logic Component -
    :information_source: **Note:** The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline continues till the end of diagram. -
    +**API** : [`Logic.java`](https://github.com/AY2324S2-CS2103T-T10-1/tp/tree/master/src/main/java/seedu/address/logic/Logic.java) + +Here's a (partial) class diagram of the `Logic` component: + +

    How the `Logic` component works: 1. When `Logic` is called upon to execute a command, it is passed to an `AddressBookParser` object which in turn creates a parser that matches the command (e.g., `DeleteCommandParser`) and uses it to parse the command. -1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `DeleteCommand`) which is executed by the `LogicManager`. -1. The command can communicate with the `Model` when it is executed (e.g. to delete a person).
    +2. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `DeleteCommand`) which is executed by the `LogicManager`. +3. The command can communicate with the `Model` when it is executed (e.g. to delete a person).
    Note that although this is shown as a single step in the diagram above (for simplicity), in the code it can take several interactions (between the command object and the `Model`) to achieve. -1. The result of the command execution is encapsulated as a `CommandResult` object which is returned back from `Logic`. +4. The result of the command execution is encapsulated as a `CommandResult` object which is returned back from `Logic`. + +
    Here are the other classes in `Logic` (omitted from the class diagram above) that are used for parsing a user command: - +

    How the parsing works: -* When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as a `Command` object. + +* When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCommandParser`) +* The `XYZCommandParser` uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as a `Command` object. * All `XYZCommandParser` classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. -### Model component -**API** : [`Model.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/model/Model.java) +
    + +#### Example of Parsing User Input: `delete` Command + +The sequence diagram below illustrates the interactions within the `Logic` component, taking a simple `execute("delete 1")` API call as an example. + +

    - +**Note:** The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline continues till the end of diagram. + + +The following is a more detailed explanation on how user input is parsed into a `Command` object (not mentioned above for simplicity). + +##### Using Argument Tokenizer and Argument Multimap +* After the `XYZCommandParser` is instantiated by the `AddressBookParser`, it uses the `ArgumentTokenizer` class to tokenize the user input string into the arguments. + * This is done through the `tokenize` method which returns an `ArgumentMultimap` object. +* The `ArgumentMultimap` class is then used to extract the relevant arguments. + +
    + +**Mandatory Arguments and Multiple Arguments** + +* For some commands, some arguments are mandatory and the `arePrefixesPresent` method is used to check if the arguments (i.e the corresponding prefixes) are present in the user input. If not, an exception is thrown. +* For some commands, multiple arguments under the same category (e.g. two name arguments for an AddCommand) are not allowed. The `ArgumentMultimap` class is used to check for undesirable multiple arguments using the `verifyNoDuplicatePrefixesFor` method. If multiple arguments are present, an exception is thrown. + +
    + +**Validation of Arguments** + +* Validation of each extracted argument is done using the methods defined in the `ParserUtil` class. This class contains methods to validate different arguments extracted by the `ArgumentMultimap` class based on the `VALIDATION_REGEX` defined in component classes (`Name.java`, `Score.java`, etc.). +* The parsed arguments are then used to create a `XYZCommand` object to be executed. + + + +**Note:** Some commands do not require any arguments (e.g., `help`, `clear`, `list`, `exit`). In such cases, the `XYZCommand` class is directly instantiated by the `AddressBookParser` class without the parsing of arguments. As such, any arguments passed to these commands are ignored. + + + +#### Considerations for Logic + +The `Logic` component is designed to be the central component that executes all user commands. +This design choice was made to ensure that all commands are executed in a consistent manner, and to prevent the duplication of command execution logic across different components. +By centralizing the command execution logic in the `Logic` component, we ensure that all commands are executed in the same way, regardless of the component that initiates the command execution. +This design choice also allows for easier maintenance and extensibility, as any changes to the command execution logic can be made in a single location. + +
    + +
    + +
    + +### Model Component +**API** : [`Model.java`](https://github.com/AY2324S2-CS2103T-T10-1/tp/master/src/main/java/seedu/address/model/Model.java) + +

    The `Model` component, -* stores the address book data i.e., all `Person` objects (which are contained in a `UniquePersonList` object). -* stores the currently 'selected' `Person` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. +* stores the address book data i.e., all `Person` objects (which are contained in a `UniquePersonList` object) and all `Exam` objects (which are contained in a `UniqueExamList` object). +* stores the currently filtered `Person` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI can update when the data in the list changes. +* stores the currently selected `Exam` which is exposed to outsiders as an unmodifiable `ObservableValue`. This is used in conjunction with the exam and exam score implementation, and also used to update the highlighted exam on the UI. +* stores `ScoreStatistics` for the currently selected `Exam`. This statistic is used in conjunction with the mean and median feature. It is also exposed to outsiders as an unmodifiable `ObservableValue` so that the UI can be bound to this value for updating. * stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects. * does not depend on any of the other three components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components) -
    :information_source: **Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
    + + +**Note:** An alternative (arguably, a more OOP) model is given below relating to the `Person` class. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
    However, we opted not to use this model. As much as possible, we tried to keep the attributes of `Person` unlinked to other classes to prevent complications in our saving, import and export functionalities. + +

    + +
    + +
    + +
    + +### Storage Component + +**API** : [`Storage.java`](https://github.com/AY2324S2-CS2103T-T10-1/tp/tree/master/src/main/java/seedu/address/storage/Storage.java) + +

    + +
    +
    + +#### Saving of Data + +The `Storage` component uses the `Jackson` library to convert objects to JSON format. The conversion methods are predefined in the `JsonAdapted` classes for their corresponding objects. + +The `Logic` class stores a `StorageManager` object that implements the methods in the `Storage` class. For **every** command that is executed, `Logic` uses `StorageManager` to save the updated `AddressBook` through the `saveAddressBook` method. + +The `StorageManager` class calls on the `JsonAddressBookStorage` class to convert all objects in the `AddressBook` to JSON formatting. The converted JSON objects are consolidated in the `JsonSerializableAddressBook` class and it is serialized to JSON format and saved using the `saveJsonToFile` method. + +The sequence diagram below illustrates how data is saved within the `Storage` component when the user issues a command. - +

    -
    +
    +
    +
    -### Storage component +#### Loading of Data -**API** : [`Storage.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/storage/Storage.java) +When the application is initialized, the `Storage` component reads the JSON objects from the save file and converts them back to objects that can be used to initialize the `Model` component. +This is done using the `readJsonFile` method of the `JsonUtil` class which utilizes the methods defined in the `JsonAdapted` classes to convert the saved JSON data back to objects that can be used by the `Model` component. - +The sequence diagram below illustrates how data is loaded within the `Storage` component when the application is initialized. -The `Storage` component, +

    + + +In summary, the `Storage` component: * can save both address book data and user preference data in JSON format, and read them back into corresponding objects. * inherits from both `AddressBookStorage` and `UserPrefStorage`, which means it can be treated as either one (if only the functionality of only one is needed). * depends on some classes in the `Model` component (because the `Storage` component's job is to save/retrieve objects that belong to the `Model`) -### Common classes +
    + +### Common Classes Classes used by multiple components are in the `seedu.addressbook.commons` package. +These classes provide utility functions that are used across different components such as +`CollectionUtil`, `StringUtil`, `JsonUtil` etc. It also contains app wide constants and exceptions. -------------------------------------------------------------------------------------------------------------------- +
    + +
    + ## **Implementation** -This section describes some noteworthy details on how certain features are implemented. +This section describes some noteworthy details on how certain features are implemented -### \[Proposed\] Undo/redo feature +
    -#### Proposed Implementation +### General Features -The proposed undo/redo mechanism is facilitated by `VersionedAddressBook`. It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. Additionally, it implements the following operations: +As these general features do not require any arguments, the `AddressBookParser` directly instantiates the corresponding command classes. -* `VersionedAddressBook#commit()` — Saves the current address book state in its history. -* `VersionedAddressBook#undo()` — Restores the previous address book state from its history. -* `VersionedAddressBook#redo()` — Restores a previously undone address book state from its history. +
    -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. +#### **Help Command** : `help` -Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. +The `help` command utilizes the `java.awt.Toolkit` class to copy the user guide link to the user's clipboard. -Step 1. The user launches the application for the first time. The `VersionedAddressBook` will be initialized with the initial address book state, and the `currentStatePointer` pointing to that single address book state. +##### Executing the Command -![UndoRedoState0](images/UndoRedoState0.png) +On execution of the `HelpCommand`, the `copyToClipboard` method is called which retrieves the system clipboard +through `Toolkit.getDefaultToolkit().getSystemClipboard()` and copies the user guide link to the clipboard by using +`setContents` method. -Step 2. The user executes `delete 5` command to delete the 5th person in the address book. The `delete` command calls `Model#commitAddressBook()`, causing the modified state of the address book after the `delete 5` command executes to be saved in the `addressBookStateList`, and the `currentStatePointer` is shifted to the newly inserted address book state. +##### Design Considerations -![UndoRedoState1](images/UndoRedoState1.png) +We designed the help command to copy the user guide link directly to the clipboard as we wanted our application to be +CLI optimized. This allows our target users to easily access the user guide without having to use their mouse +to navigate to the user guide link. -Step 3. The user executes `add n/David …​` to add a new person. The `add` command also calls `Model#commitAddressBook()`, causing another modified address book state to be saved into the `addressBookStateList`. +
    -![UndoRedoState2](images/UndoRedoState2.png) +#### **Clear Command** : `clear` -
    :information_source: **Note:** If a command fails its execution, it will not call `Model#commitAddressBook()`, so the address book state will not be saved into the `addressBookStateList`. +The `clear` command allows users to clear all persons and exams from the persons and exams list. -
    +##### Executing the Command -Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoAddressBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous address book state, and restores the address book to that state. +The `ClearCommand` simply sets the `AddressBook` in the `Model` component to a new `AddressBook` object, effectively clearing all persons and exams from the persons and exams list. -![UndoRedoState3](images/UndoRedoState3.png) +##### Design Considerations -
    :information_source: **Note:** If the `currentStatePointer` is at index 0, pointing to the initial AddressBook state, then there are no previous AddressBook states to restore. The `undo` command uses `Model#canUndoAddressBook()` to check if this is the case. If so, it will return an error to the user rather -than attempting to perform the undo. +We designed the `clear` command to clear all persons and exams from their respective lists to provide users with a quick and easy way to reset the application to its initial state. This is useful for users who want to start over or clear the application for a fresh start. -
    +
    -The following sequence diagram shows how an undo operation goes through the `Logic` component: +#### **List Command** : `list` -![UndoSequenceDiagram](images/UndoSequenceDiagram-Logic.png) +The `list` command allows users to list all persons in the persons list. -
    :information_source: **Note:** The lifeline for `UndoCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +##### Executing the Command -
    +The `ListCommand` retrieves the `filteredPersonList` from the `Model` component and returns a `CommandResult` object containing the list of persons to be displayed on the UI. -Similarly, how an undo operation goes through the `Model` component is shown below: +##### Design Considerations -![UndoSequenceDiagram](images/UndoSequenceDiagram-Model.png) +We designed the `list` command to list all persons in the persons list to provide users with a quick and easy way to view all persons in the persons list. This is useful to revert the UI back to the default view after a find command has been executed which filters the persons displayed on the UI. -The `redo` command does the opposite — it calls `Model#redoAddressBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the address book to that state. +
    -
    :information_source: **Note:** If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone AddressBook states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. +
    -
    +### Contact Management Features -Step 5. The user then decides to execute the command `list`. Commands that do not modify the address book, such as `list`, will usually not call `Model#commitAddressBook()`, `Model#undoAddressBook()` or `Model#redoAddressBook()`. Thus, the `addressBookStateList` remains unchanged. +All contacts are stored as `Person` objects in the `UniquePersonList` object under the `AddressBook` of the `Model` component. +There is an additional `filteredPersons` list stored in the `Model` component that stores the persons currently displayed in the `PersonListPanel` on the UI. This list is updated whenever the user issues a command that might change the persons displayed in the `PersonListPanel`. -![UndoRedoState4](images/UndoRedoState4.png) +
    -Step 6. The user executes `clear`, which calls `Model#commitAddressBook()`. Since the `currentStatePointer` is not pointing at the end of the `addressBookStateList`, all address book states after the `currentStatePointer` will be purged. Reason: It no longer makes sense to redo the `add n/David …​` command. This is the behavior that most modern desktop applications follow. +#### **Add Person Command** : `add` -![UndoRedoState5](images/UndoRedoState5.png) +The `add` command allows users to add a person to the persons list. -The following activity diagram summarizes what happens when a user executes a new command: +The user can specify the person's: +* name (`Name`), +* phone number (`Phone`), +* address (`Address`), +* email (`Email`), - +and optionally provide additional information such as their: +* matriculation number (`Matric`), +* reflection (`Reflection`), +* studio (`Studio`), +* and tags (`Tag`). -#### Design considerations: +
    -**Aspect: How undo & redo executes:** +##### Parsing User Input +The `AddCommandParser` class is responsible for parsing user input to extract the details of the person to be added. It uses the `ArgumentTokenizer` to tokenize the input string, extracting prefixes and their associated values. It ensures that all mandatory fields are present and that there are no duplicate prefixes in the user input. -* **Alternative 1 (current choice):** Saves the entire address book. - * Pros: Easy to implement. - * Cons: May have performance issues in terms of memory usage. +##### Executing the Command +The `AddCommand` class creates a new `Person` object with the parsed details. +The `Person` object is then added to the `UniquePersonList` through the `addPerson` method in the `Model` component. -* **Alternative 2:** Individual command knows how to undo/redo by - itself. - * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). - * Cons: We must ensure that the implementation of each individual command are correct. +
    -_{more aspects and alternatives to be added}_ +##### Sequence Diagram -### \[Proposed\] Data archiving +The sequence diagram below illustrates a more in-depth view of the interactions regarding the parsing of user input. +It takes an add command: `execute(add n|Dohn Joe p|98765432 a|123 e|dohn@gm.com m|A1234567X s|S1 r|R1)` as an example. -_{Explain here how the data archiving feature will be implemented}_ +

    + --------------------------------------------------------------------------------------------------------------------- +**Note:** The lifeline for `AddCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline continues till the end of diagram. + -## **Documentation, logging, testing, configuration, dev-ops** +The parsing is detailed as follows: +

    -* [Documentation guide](Documentation.md) -* [Testing guide](Testing.md) -* [Logging guide](Logging.md) -* [Configuration guide](Configuration.md) -* [DevOps guide](DevOps.md) +
    +
    --------------------------------------------------------------------------------------------------------------------- +##### Design Considerations -## **Appendix: Requirements** +**Use of `Email` Field as Unique Identifier**
    -### Product scope +We have chosen to use the `Email` field as a unique identifier. Due to the real-world implementation of email addresses, and specifically in NUS, email addresses are unique to each person. This allows for easy identification of persons and prevents the creation of duplicate persons with the same email address. -**Target user profile**: +This is opposed to using the `Name` field as a unique identifier, as an app with our proposed scale will likely be handling a large number of persons potentially having the same name. This would make it difficult to identify or keep track of persons with the same name. -* has a need to manage a significant number of contacts -* prefer desktop apps over other types -* can type fast -* prefers typing to mouse interactions -* is reasonably comfortable using CLI apps +**Compulsory and Non-compulsory Fields**
    -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app +We have chosen to make the following fields compulsory as they are essentials and most likely available to the head TA: +* `Name` +* `Email` +* `Phone` +* `Address` +The following fields are optional as they may not be available for all persons: +* `Matric` +* `Reflection` +* `Studio` +* `Tag` -### User stories +
    -Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` +
    -| Priority | As a …​ | I want to …​ | So that I can…​ | -| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | -| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App | -| `* * *` | user | add a new person | | -| `* * *` | user | delete a person | remove entries that I no longer need | -| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list | -| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident | -| `*` | user with many persons in the address book | sort persons by name | locate a person easily | +
    -*{More to be added}* +#### **Edit Person Command** : `edit` -### Use cases +The `edit` command allows a user to edit the details of an existing person. -(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise) +##### Parsing User Input -**Use case: Delete a person** +The `EditCommandParser` class is responsible for parsing user input to extract the index of the person to be edited and the new details of the person. +It uses the `ArgumentTokenizer` class to tokenize the user input string, extracting the index of the person to be edited and the new details of the person. It ensures that the index is valid and that there are no duplicate prefixes in the user input. -**MSS** +##### Executing the Command -1. User requests to list persons -2. AddressBook shows a list of persons -3. User requests to delete a specific person in the list -4. AddressBook deletes the person +The `EditCommand` first retrieves the person to be edited from the `Model` component. +This is done by first retrieving the `filteredPersonList` from the `Model` component using the `getFilteredPersonList` method +The person to be edited is then retrieved from the `filteredPersonList` using the index provided by the user. +The `EditCommand` then creates a new `Person` object with the new details provided by the user and the selected person's existing details. The `Person` object is then updated in the `UniquePersonList` through the `setPerson` method in the `Model` component. - Use case ends. +##### Activity Diagram + +The activity diagram below illustrates the workflow involved in executing the `edit` command. In practice, a `Reject` activity will result in a `CommandException` being thrown. + +

    + +
    + +
    + +
    + +#### **Delete Person Command** : `delete` + +The `delete` command allows a user to delete a person with the specified index. + +##### Parsing User Input + +The `DeleteCommandParser` class is responsible for parsing user input to extract the index of the person to be deleted. It uses the `ArgumentTokenizer` to tokenize the input string, extracting the index of the person to be deleted and ensuring that the index is valid. + +##### Executing the Command + +The `DeleteCommand` class first retrieves the person to be deleted from the `Model` component. This is done by first retrieving the `filteredPersonList` from the `Model` component using the `getFilteredPersonList` method. The person to be deleted is then retrieved from the `filteredPersonList` using the index provided by the user. The `DeleteCommand` then deletes the person from the `UniquePersonList` through the `deletePerson` method in the `Model` component. + +##### Sequence Diagram + +For more details on the implementation of the `delete` command, refer to the [Delete Command Sequence Diagram](#example-of-parsing-user-input-delete-command). + +##### Design Considerations + +We have chosen to implement the `delete` command to accept the index of the person to be deleted to maximize convenience for the user. The numbering of the lists will be displayed to the user, making indexing very intuitive. + +
    + +
    + +
    + +#### **Find Command** : `find` + +The `find` command lets users search for persons by substring matching. The user can select any parameter to search under: +`NAME`, `EMAIL`, `TAG`, `MATRIC`, `REFLECTION`, `STUDIO`, and `TAGS` can all be used. E.g. to search for all persons under studio `S2`, the user can use `find s|s2`. + +The user can also use two other prefixes: `lt` and `mt` to search for persons with scores less than or more than a certain value respectively. +E.g. `find mt|50` will return all persons with scores more than 50. + +The `find` feature makes use of the predicate classes `PersonDetailPredicate` and `ExamPredicate`, as well as the method `updateFilteredPersonList` +to update the model to show only persons that fulfill the criteria that the user has keyed in. + +##### Parsing User Input + +The `FindCommandParser` class is responsible for parsing user input to extract search criteria. It uses the `ArgumentTokenizer` to tokenize the input string, +extracting prefixes and their associated values. Next, the method `verifyNoDuplicatePrefixesFor` ensures that there are no duplicate prefixes in the user input. +Following that, the `extractPrefixForFindCommand` method ensures that only one valid, non-empty prefix is provided in the input. +After which, the `extractValidKeyword` method ensures that the keyword provided in the input is valid in the case that the prefix is `mt|` or `lt|`, +since these two prefixes specifically require a numerical value as the keyword instead of a string value. + +##### Executing the Command + +The `FindCommand` class is responsible for executing the command for filtering the list in the application. + +Using the prefix and keyword from parsing user input, a `FindCommand` is created. the `execute` method is then called by the `LogicManager`. + +**Creating Predicate** + + + +**Note:** The `PersonDetailPredicate` and `ExamPredicate` classes implement the `Predicate` interface to filter contacts based on the search criteria. +A brief overview of the two classes is given below: +* `PersonDetailPredicate` takes a prefix and keyword as parameters, allowing it to filter contacts based on specific details like name, phone number, etc. +* `ExamPredicate` takes a prefix, a keyword, and an exam as parameters, allowing it to filter contacts based on exam scores of a specific exam. + + + +The find command first checks if an exam is required by checking if the prefix is `mt|` or `lt|`. +If an exam is required, the `selectedExam` is retrieved from the `model` and passed to the `ExamPredicate` constructor along with the prefix and keyword. +Otherwise, the `PersonDetailPredicate` class is created with the prefix and keyword. + +**Updating Filtered Person List**
    + +The `ModelManager` class implements the `Model` interface and manages the application's data. It maintains a `filteredPersons` list, +which is a filtered list of contacts based on the applied predicate. The `updateFilteredPersonList` method implemented in `ModelManager` +updates the filtered list based on the predicate provided. + +When the `FindCommand` is executed, the `updateFilteredPersonList` method is called with either the `PersonDetailPredicate` or `ExamPredicate` as a parameter. +This updates the `filteredPersons` list to show only persons that fulfill the conditions set in the `test` method in either of the predicates. + +
    + +**User Interface Interaction**
    + +After the `filteredPersons` list is updated, the user interface is updated such that the `PersonListPanel` now shows persons that fulfill the predicate generated by the original user input. + +The following sequence diagram illustrates the `find` command with the user input `find n|Alice`. +

    + +The next sequence diagram details the creation of the predicate, as well as the updating of the `filteredPersons` list in the `Model` component. +

    + +
    + +The following activity Diagram illustrates the user execution of the `find` command. +

    + +The next activity diagram is an expansion of the previous diagram, detailing the case where the user searches for contacts based on exam scores. +

    + +##### Design Considerations + +**User Interface Consistency**
    + +The choice of implementing the command to use prefixes to determine the filter criteria ensures consistency with other commands in the application. +As this command follows a similar structure to all other commands, it is easier for users to learn and use the application. + +**Flexibility in Search Criteria**
    + +By allowing users to specify search criteria using different prefixes (name, phone, email, etc.), the implementation offers flexibility. +Users can search for contacts based on various details, enhancing the usability of the feature. -**Extensions** +In the context of our potential users, +we considered that users would likely have to sometimes filter students by their classes, or filter people by their roles (student, tutor, professor). +So we opted to implement this feature with the flexibility of using all prefixes to account for all these potential use cases. -* 2a. The list is empty. +Furthermore, with consideration that our potential users will interact with exam scores, we wanted to integrate the find functionality +to search for contacts based on exam scores. Hence, we decided to introduce the `mt|` and `lt|` prefixes to allow users to search for contacts based on exam scores. - Use case ends. +
    -* 3a. The given index is invalid. +**Two Predicate Classes**
    - * 3a1. AddressBook shows an error message. +The implementation of two predicate classes, `PersonDetailPredicate` and `ExamPredicate`, allows for a clear separation of concerns. - Use case resumes at step 2. +The `PersonDetailPredicate` class is responsible for filtering contacts based on details like name, phone number, etc., +while the `ExamPredicate` class is responsible for filtering contacts based on exam scores. -*{More to be added}* +The alternative would be to have a single predicate class that handles all filtering, but this would make this supposed class more complex and harder to maintain. -### Non-Functional Requirements +**Predicate-based Filtering**
    -1. Should work on any _mainstream OS_ as long as it has Java `11` or above installed. -2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. -3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. +As the `Model` class was built prior to the implementation of this feature, we did our best to re-use available methods +instead of unnecessarily reprogramming already existing logic. Hence, we decided to craft the command around the idea of a +custom predicate as the `Model` class already had a `updateFilteredPersonList` method implemented that would filter persons using a predicate. -*{More to be added}* +**Extensibility**
    -### Glossary +This design allows for easy extension to accommodate future enhancements or additional search criteria. +New prefixes can be added to support additional search criteria without significant changes as we merely need to update our `Predicate` logic. +This ensures that the implementation remains adaptable to evolving requirements and we can upgrade and improve the feature whenever required. -* **Mainstream OS**: Windows, Linux, Unix, MacOS -* **Private contact detail**: A contact detail that is not meant to be shared with others +
    + +
    + +
    + +#### **Delete Shown Command** : `deleteShown` + +The `deleteShown` command relies on the `filteredPersons` list in the `Model` component to delete the persons currently displayed in the `PersonListPanel`. + +##### Executing the Command + +The `deleteShown` command first retrieves the `filteredPersons` list from the `Model` component using the `getFilteredPersonList` method. The `deleteShown` command then iterates through the `filteredPersons` list and deletes all currently shown `Persons` from the `UniquePersonList`. + +If the currently filtered list does is not showing between 0 and the total number of existing persons, the `deleteShown` command will throw a `CommandException`. + +##### Updating Filtered Person List + +After deleting all persons currently displayed in the `PersonListPanel`, the `filteredPersons` list in the `Model` component is updated to show all remaining persons in the persons list. + +The following activity diagram illustrates the workflow of the execution of the `deleteShown` command: + +

    + +##### Design Considerations + +**Reliance on `find` Command**
    + +Similarly to the `copy` command, the `deleteShown` command is designed to be used with the find command, which filters the persons displayed in the `PersonListPanel`. Consequently, the flexibility of the `deleteShown` command relies heavily on the implementation of the `find` command. Due to this dependency, any changes to the `find` command may affect the functionality of the `deleteShown` command. + +
    + +
    + +
    + +#### **Import Contacts Command** : `import` + +The `import` command allows users to import contacts from a CSV file. Users can specify the file path of the CSV file to +import contacts from and with the validation and checking of the CSV rows, person objects can be added to the persons list in the application. + +##### Parsing User Input + +The `ImportCommandParser` class is responsible for parsing user input to extract the file path of the CSV file to be imported. It uses the `ArgumentTokenizer` to tokenize the input string, extracting the file path of the CSV file to be imported. + +##### Executing the Command + +The `ImportCommand` class first makes use `OpenCSV` library which parses the CSV file into a `List`, with each `String[]` +representing a row in the CSV file. The `List` is further parsed row by row by the `readCsvFile` method, which +returns a `Pair`. The key of the returned `Pair` is a `personsData` list containing the `Person` objects successfully parsed from the CSV file and the value is an error report containing all the errors that occurred during the process of reading from the CSV file. + +The `ImportCommand` then iterates through the `personsData` list and adds each `Person` object to the `Model` component +through repeated use of the `AddCommand`. Errors that occur during this process are also added to the error report. + +In summary, The import process is done in the following steps: +1. `ImportCommand` reads the CSV file with the given file path. +2. The CSV file is parsed and each row is converted into an `AddCommand` +3. The `AddCommand` is then executed passing the same model as import command. +4. The `AddCommand` then adds the person to the model. + +**Handling duplicate persons**
    + +Duplicate records in the imported CSV file is handled by `AddCommand`, which will check if the person already exists in the model. If the person already exists, the `AddCommand` throws a `CommandException` which is caught by the `ImportCommand` and added to an error report. + +**Handling invalid CSV files**
    + +Invalid files are handled by `ImportCommand`, with the help of `ImportCommandParser` and `CsvUtil`. `ImportCommandParser` will check if is a CSV file. +`CsvUtil` will check if the CSV file is valid and will return a list of persons and an error report. The error report will be displayed to the user if there are any errors. + +Overall, the conditions checked are: +- The file exists +- The file is a CSV file +- **The first row of the file is the header row**. In which all compulsory fields for creating a persons object + (ie `name`, `email`, `address`, `phone`)are present. Optional headers will be read if present. Headers in the CSV that are not a field in `Person` will be ignored. + +If the file is not valid, an error message will be returned. + +**Handling duplicate headers in the CSV file** + +Handled by CsvUtil. The first occurrence of the header will be used and the rest will be ignored. + +
    + +The sequence diagrams below illustrates the interactions within the `Logic` component when the user issues the command `import`. + +**Parsing** + +

    + +**Execution** + +

    + +
    + +**Reference Diagram for each addCommand in importCommand** + +

    + +
    +
    + +##### Design Considerations + +**Usage of `AddCommand`**
    + +The main concern in the increased coupling between `ImportCommand` and `AddCommand`. However, we established that this coupling was actually a good thing, as the incorporation of the `AddCommand` allowed us to reuse the validation and error handling that was already implemented in the `AddCommand`. Furthermore, should we ever need to change the validation and error handling in the `AddCommand`, the `ImportCommand` would automatically inherit these changes. By making `AddCommand` the gate in which all persons are added to the model, we ensure that all persons added to the model are validated and handled in the same way. + +
    + +
    + +
    + +#### **Copy Command** : `copy` + +The `copy` command enables users to quickly copy the email addresses of the persons currently displayed to them in the +`PersonListPanel`. The copied emails are stored in the users' clipboard and can be pasted into an email client. +This feature is useful when users need to send emails to a group of persons. + +The copy command is a child of the `command` class and relies on the `filteredPersons` list in the `Model` component, +as well as the `java.awt` package to copy the emails of all currently displayed persons to the users' clipboard. + +##### Parsing User Input + +The `CopyCommand` class is instantiated directly by the `AddressBookParser` class when the user inputs the `copy` command. +This is because the `copy` command does not require any additional arguments from the user. + +##### Executing the Command + +The `CopyCommand` class is responsible for executing the command for obtaining the emails of the filtered persons and copying them to the clipboard. +It iterates through the `filteredPersons` list in the `Model` component and extracts the email addresses of each person. +The email addresses are then concatenated into a single string, separated by commas, and copied to the clipboard using the `java.awt` package. + +**User Interface Interaction**
    + +After the `CopyCommand` is executed, the `UI` component updates the `ResultDisplay` to show a message indicating that the emails have been copied to the clipboard. + +The following activity diagram summarizes the steps involved in executing the `copy` command: +

    + +##### Considerations + +**Reliance on `find` Command**
    + +The `copy` command is designed to be used with the find command, which filters the persons displayed in the `PersonListPanel`. +Consequently, the flexibility of the `copy` command relies heavily on the implementation of the `find` command. +Due to this dependency, any changes to the `find` command may affect the functionality of the `copy` command. + +
    + +##### Extensibility + +Due to the simplicity of the `copy` command, there are limited opportunities for extending its functionality. +However, future enhancements could include the ability to copy other details of persons, such as phone numbers or addresses. + +##### Alternative Implementations + +**Alternative 1: Copying emails of all persons** + +Copies the emails of all persons in the persons list, regardless of whether they are currently displayed in the `PersonListPanel`. +However, this approach may lead to users copying a large number of emails unintentionally, which could be overwhelming. +Furthermore, it may not be clear to users which emails are being copied. + +**Alternative 2: Copying emails into a file** + +Instead of copying the emails to the clipboard, the emails could be saved into a file. +This approach would allow users to access the emails at a later time and would prevent the loss of copied emails if the clipboard is cleared. +However, it may be less convenient for users who want to paste the emails directly into an email client. + +
    + +
    + +
    + +#### **Export Command** : `export` + +The `export` command allows users to export the details of each person currently displayed in the `PersonListPanel` to a CSV file. The CSV file is generated in the file `./addressbookdata/avengersassemble.csv`. + +The user can first use the `find` feature to filter out the relevant persons, which will be displayed in the `PersonListPanel`. +The `export` feature also relies on the Jackson Dataformat CSV module and the Jackson Databind module to write the details of persons to the CSV file `./addressbookdata/avengersassemble.csv`. + +##### Parsing User Input + +The `export` command does not require any additional arguments from the user. Hence, an `ExportCommandParser` class is not required. +`AddressBookParser` directly creates an `ExportCommand` object. + +##### Executing the Command + +**Data Retrieval**
    +* The `execute` method retrieves the `filteredPersons` list in `Model` by calling the `getFilteredPersonList()` method in `Model`. + This list stores the relevant persons currently displayed in the `PersonListPanel`. + It then creates a temporary `AddressBook` object and iterates through the `filteredPersons` list to add each person from the list into the `AddressBook`. + The data is then written to a JSON file named `filteredaddressbook.json` with the `writeToJsonFile` method in `ExportCommand`. + +* The `execute` method also retrieves the address book file path by calling the `getAddressBookFilePath()` method in `Model` (this `AddressBook` stores information of **all** persons and exams). + This file path is retrieved to obtain information on the examinations added in the application + +The sequence diagram illustrates the interactions between the `Logic` and `Model` components when data is being retrieved from `Model` when `export` is executed: + +

    + +

    + +
    + +**JSON File Handling**
    + +The contents of both the JSON files retrieved in the above section is read with the `readJsonFile()` method in `ExportCommand` and returned as JSON trees, `filteredJsonTree` and `unfilteredJsonTree`. +This method uses Jackson's `ObjectMapper`. + +* From the `filteredJsonTree`, the persons array is extracted using the `readPersonsArray()` method in `ExportCommand` to obtain the filtered persons and their data. +* From the `unfilteredJsonTree`, the exams array is extracted using the `readExamsArray()` method in `ExportCommand` to obtain the exams. + +
    + +**CSV Conversion**
    + +A CSV file, `avengersassemble.csv`, to write the data to, is created. +Its directory is also created using the `createCsvDirectory()` method in `ExportCommand` if the directory does not exist. +The CSV schema is dynamically built based on the structure of the JSON array using the `buildCsvSchema()` method in `CsvUtil`. This method relies on the Jackson Dataformat CSV module to build the CSV schema. +The CSV schema and JSON data are used to write to the CSV file using Jackson's `CsvMapper`. + +The following sequence diagram shows the interactions within the different classes in the JSON file handling section and CSV conversion section when the `export` command is executed: + +

    + +

    + +
    +
    + +
    + +##### Design Considerations + +**Obtaining Exam Names from `exams` Array in the Address Book File Path**
    +* **Alternative 1 (current choice):** Obtaining exam names from `exams` array in the address book file path. + * A person in the JSON file will only contain the exam details if they have a score for that exam. + Therefore, with this choice, if no one in the filtered person's JSON file contains any score for a specific exam, the exam name will still be exported. + * By adopting this alternative, users will be informed about the existence of an exam even if none of the persons in the filtered list have a score for that exam. +

    +* **Alternative 2:** Obtaining exam names directly from `persons` array in the filtered person's JSON file. + * This choice will only export an exam if someone in the filtered persons list has a score for that exam. + +
    + +**Adding `Exam:` to Exam Names in the CSV Column Headings**
    +Since users have the flexibility to determine the names of exams added, there's a possibility of adding an exam with the same name as a field (e.g. `reflection`). +This could lead to confusion when mapping the CSV schema and JSON data. +Therefore, appending `Exam:` to the beginning of exam names in the CSV column headings can help mitigate this potential confusion. + +
    + +
    + +
    + +#### **Feature: Addition of Optional Fields (Matric)** + +The optional `Matric` field enables the user to store the matriculation number of a person. The field is stored as a `Matric` in the `Person` object. + +Note: The optional `Studio` and `Reflection` fields are similarly implemented. + +##### Implementation Details + +The `Matric` class is a simple wrapper class that ensures it is valid according to NUS matriculation number format and is not empty. +The `Matric` field is used by the `add` and `edit` commands. + +##### Parsing User Input: `add` + +For the `add` command, as opposed to the `name` and other fields, the parser does not check if a prefix for `Matric` is present. This is because we define the `Matric` field to be optional as contacts (e.g. professors) do not need to have a matriculation number. + +The parser also verifies that there are no duplicate prefixes for `Matric` in a single `add` command. A new Person is then created with the `Matric` field set to the parsed `Matric` object. + +If there is no `Matric` field present, the `Matric` field of the new `Person` object is set to `null`. + +##### Parsing User Input: `edit` + +For the `edit` command, the parser will add or update the `Matric` field of the person being edited. + +
    + +
    + +#### **Feature: Automatic Tagging of Persons** + +A `student` tag is automatically added during the parsing of the `add` command based on the presence of the `Matric` field of the person being added. + +##### Implementation Details + +During the parsing of the `add` command, the parser will check if the `Matric` field is present, indicating that they are a student. +The parser also generates `Tag` objects based on the user input. The existing tags are updated with the new automatically generated tag. + +The activity diagram is as follows: +

    + +
    -------------------------------------------------------------------------------------------------------------------- -## **Appendix: Instructions for manual testing** +
    -Given below are instructions to test the app manually. +
    -
    :information_source: **Note:** These instructions only provide a starting point for testers to work on; -testers are expected to do more *exploratory* testing. +### **Exam Features** + +There are 4 main commands that are used to interact with the exam feature: `addExam`, `deleteExam`, `selectExam` and `deselectExam`. + +All exams are stored in the `UniqueExamList` object in `AddressBook` of the `Model` component. The `Model` component also stores the currently selected exam in the `selectedExam` field. + +
    + +
    + +#### **Add Exam Command** : `addExam` + +The `addExam` command allows users to add an exam to the application. +The user can specify the name of the exam and the maximum score of the exam. +The exam is then added and stored in the `UniqueExamList`. + +##### Parsing User Input + +The `AddExamCommandParser` is responsible for parsing user input to extract the `name` and the `maxScore` of the exam. +It uses the `ArgumentTokenizer` to tokenize the input string, extracting `name` and `maxScore`. +It ensures that `name` and `maxScore` are valid and present in the user input, and that there are no duplicate prefixes in the user input. +The `name` and `maxScore` are then used to instantiate an `AddExamCommand`. + +##### Executing the Command + +The `AddExamCommand` class creates a new `Exam` object with the parsed arguments +It adds the `Exam` to the `UniqueExamList` through the `addExam` method in the `Model` component. +If the exam already exists in the list, a `CommandException` is thrown. + +
    + +
    + +#### **Delete Exam Command** : `deleteExam` + +The `deleteExam` command allows users to delete an exam from the application. +The user can specify the index of the exam to be deleted. +The exam is then removed from the `UniqueExamList`. + +##### Parsing User Input + +The `DeleteExamParser` is responsible for parsing user input to extract the `index` of the exam to be deleted. +It uses the `ArgumentTokenizer` to tokenize the input string, extracting the `index`. +It ensures that the `index` is valid and present in the user input, and that there are no other prefixes in the user input. +The `index` is used to instantiate a `DeleteExamCommand`. + +##### Executing the Command + +The `DeleteExamCommand` uses the index to delete the exam from the `UniqueExamList` in the `Model` component. +It first retrieves the `UniqueExamList` by using the `getExamList` method in the `Model` component. +It then retrieves the exam from the `UniqueExamList` using the user provided index. +If the index is greater than the size of the list, a `CommandException` is thrown. +Using the retrieved exam, it then deletes the exam from the `UniqueExamList` through the `deleteExam` method in the `Model` component. + +
    + +
    + +
    + +#### **Sequence Diagrams Illustrating Exam Modification** + +The following two sequence diagram illustrates the interactions between the Logic and Model when an exam is modified. This diagram uses the `addExam` command as an example. + +**Parsing** + +

    + +**Execution** + +

    + +Note: `deleteExam` follows a similar structure, differing in the arguments parsed and the methods called on the `Model` component (e.g. deleting from `UniqueExamList` instead of adding to it). + +
    + +
    + +
    + +#### **Select Exam Command** : `selectExam` + +The `selectExam` command allows users to select an exam from the `UniqueExamList`. +The selection of exams is heavily used in conjunction with our exam score features. + +##### Parsing User Input + +The `SelectExamCommandParser` is responsible for parsing user input to extract the `index` of the exam to be selected. +It uses the `ArgumentTokenizer` to tokenize the input string, extracting the `index`. +It ensures that the `index` is valid and present in the user input, and that there are no other prefixes in the user input. + +##### Executing the Command + +The `SelectExamCommand` uses the index to select an exam from the `UniqueExamList` in the `Model` component. +It first retrieves the `UniqueExamList` by using the `getExamList` method in the `Model` component. +It then retrieves the exam from the `UniqueExamList` using the user provided index. +If the index is greater than the size of the list, a `CommandException` is thrown. +Using the retrieved exam, it then sets the `selectedExam` field in the `Model` component using the `selectExam` method. + +
    + +
    + +#### **Deselect Exam Command** : `deselectExam` + +The `deselectExam` command allows users to deselect the currently selected exam. + +##### Parsing User Input + +The `deselectExam` command does not take any arguments from the user. +Hence, a `DeselectExamCommandParser` is not required. `AddressBookParser` directly creates a `DeselectExamCommand` object. + +##### Executing the Command + +The `DeselectExamCommand` uses the `deselectExam` method in the `Model` component to deselect the currently selected exam. +It sets the `selectedExam` field in the `Model` component to `null`. +If there is no exam selected, a `CommandException` is thrown. + +
    + +
    + +
    + +#### **Sequence Diagrams Illustrating Exam Selection** + +The following sequence diagram illustrates the interactions between the Logic and Model when the `SelectExamCommand` is executed. + +

    + +Notes: +- The `ObservableList` object is what is returned when retrieving the `UniqueExamList`. This prevents unwanted modifications to the `UniqueExamList` when retrieving the selected exam. +- `deselectExam` follows a similar structure as the diagram above, differing in the arguments parsed and the methods called on the `Model` component (i.e. calling `deselectExam` on `Model` instead of `selectExam`). + +
    + +
    + +#### **Considerations for Exam Features** + +##### Using a Selection System for Exams + +We decided to implement a selection system for exams to complement the exam score feature. The application would only display the scores of the selected exam, making it easier for users to manage and view the scores. + +Our alternative design was to display the scores of all exams at once on every person. However, this alternative design would have made the UI cluttered and less user-friendly. The selection system allows users to focus on the scores of a specific exam, making it easier to view and manage the scores. + +##### Using Index for Exam Selection +We were initially torn between the selection of exams using the exam name or the index. We eventually settled on using the index as it is easier for users to type and remember short numeric codes rather than potentially long and complex exam names which are more prone to typographical errors. + +##### Allowing Deselection of Exams +We decided to allow users to deselect exams as the exam scores and score statistics are displayed based on the selected exam. Deselecting the exam allows users to get rid of the displayed scores and statistics when they are no longer needed. + +##### Extensibility +The design of the exam feature allows for easy extension to accommodate future enhancements or additional functionalities. Methods for managing exams are implemented in the `Model` component, and the updating of UI for Exams is abstracted into the UI component, Making it easy to add new commands or features related to exams. + +
    + +-------------------------------------------------------------------------------------------------------------------- + +
    + +### **Exam Score Features** + +There are 3 main commands that are used to interact with exam scores of each person: `addScore`, `editScore` and `deleteScore`. + +
    + +
    + +#### **Add Score Command** : `addScore` + +The `addScore` command allows users to add a score for an exam to a person displayed in the application. +The user should select the exam they want to add a score for, then specify the index of the person they want to add a score for, and the score they want to add. +The score is then stored in a hashmap named `scores` within the `Person` object in the `Model` component. +This hashmap maps the selected exam (an `Exam` object) to the specified score (a `Score` object). + +##### Parsing User Input + +The `AddScoreCommandParser` is responsible for parsing the user input to extract the index of the person in the displayed list to add a score to, and the score to add. +It uses the `ArgumentTokenizer` to tokenize the input string, extracting the `index` and `score`. +It also ensures that the `index` and `score` input value is valid, and that there are no duplicate prefixes in the user input. +The `index` and `score` is then used in instantiating the `AddScoreCommand` by the `AddScoreCommandParser`. + +The following sequence diagram illustrates the parsing of an `addScore` command with the user input `addScore 1 s|100`: + +

    + +

    + + +Note: The parsing of an `editScore` command follows a similar structure, differing in the object instantiated at the end of the `parse` method. +`EditScoreCommandParser` instantiates an `EditScoreCommand` object. + + +##### Executing the Command + +The `execute` method in `AddScoreCommand` retrieves the `filteredPersons` list in `Model`, and validates the target index against the list of filtered persons to ensure it is not out of bounds. +It then fetches the person to add the score for based on the target index. +It also retrieves the currently selected exam from the `Model`, and validates that the score to be added is not more than the maximum score of the selected exam. +It adds the score to the person's existing `scores` hashmap using the `addExamScoreToPerson` method in the `Model`. + +
    + +The following sequence diagram illustrates the execution of an `addScore` command: + +

    + +

    + + +Note: The execution of an `editScore` command follows a similar structure to the execution of an `addScore` command. + + +
    + +
    + +#### **Edit Score Command** : `editScore` + +The `editScore` command allows users to edit a score for an exam of a person displayed in the application. +The user should select the exam they want to edit the score for, then specify the index of the person they want to edit the score for, and the new score they want to edit to. +The updated score is then stored in a hashmap named `scores` within the `Person` object in the `Model` component. +This hashmap maps the selected exam (an `Exam` object) to the updated specified score (a `Score` object). + +##### Parsing User Input + +The `EditScoreCommandParser` is responsible for parsing the user input to extract the index of the person in the displayed list to edit the score for, and the new score to edit to. +It uses the `ArgumentTokenizer` to tokenize the input string, extracting the `index` and `score`. +It also ensures that the `index` and `score` input value is valid, and that there are no duplicate prefixes in the user input. +The `index` and `score` is then used in instantiating the `EditScoreCommand` by the `EditScoreCommandParser`. + +##### Executing the Command + +The `execute` method in `EditScoreCommand` retrieves the `filteredPersons` list in `Model`, and validates the target index against the list of filtered persons to ensure it is not out of bounds. +It then fetches the person to edit the score for based on the target index. +It also retrieves the currently selected exam from the `Model`, and validates that the score to be added is not more than the maximum score of the selected exam. +It updates the score for the selected exam in the person's existing `scores` hashmap using the `addExamScoreToPerson` method in `Model`. + +
    + +
    + +
    + +#### **Delete Score Command** : `deleteScore` + +The `deleteScore` command allows users to delete a score for an exam from a person displayed in the application. +The user should select the exam they want to delete the score for, then specify the index of the person they want to delete the score for. +The key-value pair (exam-score) is removed from the `scores` hashmap within the `Person` object. +This operation removes both the selected exam (key) and the score (value), effectively deleting the score from `Person`. + +##### Parsing User Input + +The `DeleteScoreCommandParser` is responsible for parsing the user input to extract the index of the person in the displayed list to delete the score for. +It uses the `ParserUtil` to parse the input string, extracting the `index`. +It also ensures that the `index` is valid, and that there are no duplicate prefixes (i.e. there is only one `index` value) in the user input. +The `index` is then used in instantiating the `DeleteScoreCommand` by the `DeleteScoreCommandParser`. + +The following sequence diagram illustrates the parsing of an `deleteScore` command with the user input `deleteScore 1`: + +

    + +

    + +##### Executing the Command + +The `execute` method in `DeleteScoreCommand` retrieves the `filteredPersons` list in `Model`, and validates the target index against the list of filtered persons to ensure it is not out of bounds. +It then fetches the person to delete the score for based on the target index. +It also retrieves the currently selected exam from the `Model`. +It removes the score for the selected exam in the person's existing `scores` hashmap using the `removeExamScoreFromPerson` method in `Model`. + +

    + +

    + +
    + +
    + +
    + +#### **Import Exam Scores Command** : `importExamScores` + +The `importExamScores` command lets users import exam scores corresponding to existing exams and persons from a CSV file. + +##### Parsing User Input + +The `ImportExamScoresParser` class is responsible for parsing the user input. It uses the `ArgumentTokenizer` to tokenize the input string, extracting the file path of the CSV file to be imported. + +##### Executing the Command + +**Parsing CSV File**
    + +The `ImportExamScoresCommand` class reads the CSV file with the given file path. +The CSV file is parsed with the `OpenCSV` library and a `List` is created, with each `String[]` representing a row in the CSV file. + +**File Validation**
    + +After parsing, a mapping of `Exam` objects to an inner mapping of an `email` string to a `Double` score is created. This mapping is used to validate the data in the CSV file. +If the **file** is invalid, an error message is returned. + +The validation workflow for the **file** is as follows: + +

    + +
    + +If the file is valid, any invalid entries will be ignored, with the rest being successfully processed. + +A **column** will be ignored if: +1. The column header is not the `email` column, but does not start with `Exam:`. +2. The column header's name does not correspond to an existing `Exam` object. (i.e. Anything after `Exam:` is not an existing exam name.) + +
    + +A **row** will be ignored if: +1. The `email` value does not correspond to an existing `Person`. + +
    + +A **cell** will be ignored if: +1. The `Double` representing the score for an existing `Person` and `Exam` is not a valid `Score`. + +
    + +**Value Validation**
    + +For every valid row: + +The `Double` is parsed into a `Score` object. + +The `Model` object is then used to: +* Get the `Exam` object corresponding to the exam name in the row; +* Get the `Person` object corresponding to the email in the row; +* And finally add the `Score` object to the correct `Person` for the correct `Exam`. + +
    + +##### Concrete Examples of Validation + +For concrete examples of the validation process, [refer to the manual testing section of the `importExamScores` command](#importing-exam-scores-importexamscores). + + +
    + +
    + +
    + +#### **Score Statistics Feature** + +The exam statistics feature allows users to view the mean and median scores of the selected exam. The statistics are displayed in the `StatusBarFooter` element of the UI on the right side. + +The statistics are automatically updated whenever the selected exam is changed or when there are potential modifications to the scores of the selected exam. + +When there are no scores for the selected exam, the statistics are displayed as `No scores available`. When no exam is selected, the statistics are not displayed at all. + +##### Storage of Exam Statistics + +The `ScoreStatistics` class is used to store the mean and median scores of the selected exam. The `Model` component stores the `ScoreStatistics` object for the currently selected exam as a `SimpleObjectProperty`. + +##### Updating of Exam Statistics + +the `ModelManager` class implements a `updateSelectedExamStatistics` and `getSelectedExamStatistics` method to update the statistics. + +`updateSelectedExamStatistics` is called whenever the selected exam is changed or when there are potential modifications to the scores of the selected exam (deletion of a Person, adding of Score, etc.). This ensures that the `selectedExamStatistics` object is always kept up-to-date with the scores of the selected exam. + +The sequence diagram below illustrates the interactions within the `Model` component when the score statistics are updated using the `selectExam` command as an example. + +

    + +
    + +##### User Interface Interaction + +The `StatusBarFooter` element of the UI is initialized with an `ObservableValue` object. This object is bound to the `selectedExamStatistics` object in the `Model` component and is retrieved using the `getSelectedExamStatistics` method. + +Whenever a command is executed, the `StatusBarFooter` retrieves the updated statistics and displays them on the right side of the footer which can be seen at the bottom of the UI. + +##### Considerations for Exam Statistics Command + +**Storage of Exam Statistics**
    + +There were considerations to just avoid the storage of the statistics and calculate them on the fly whenever needed. However, this would have been inefficient as the statistics would have to be recalculated every time the selected exam is changed or when there are potential modifications to the scores of the selected exam. By storing the statistics, we can limit recalculations to only when necessary. + +Furthermore, storing the statistics allows us to maintain the code structure of our UI component, which is designed to observe and retrieve data from the `Model` component. If the statistics were to be calculated on the fly, the UI component would have to either calculate the statistics itself or request the `Model` component to calculate the statistics, which would have complicated the code structure by introducing more dependencies between the UI and Model components. + +**Using `ScoreStatistics` Class**
    + +The `ScoreStatistics` class was used to store the mean and median scores of the selected exam. This class was chosen as it provides a clean and structured way to store the statistics. The class also provides extensibility, as additional statistics can easily be added in the future by extending the class. + +
    + +-------------------------------------------------------------------------------------------------------------------- + +
    + +
    + +## **Planned Enhancements** + +
    + +#### Enhance Input Validation for `find` Command + +Currently, the `find` command only validates the `lt` and `mt` prefixes, where other prefixes are not validated. This means that users may search for persons with fields that do not exist to begin with, which is guaranteed to return no results. + +##### Planned Implementation + +We plan to enhance the `find` command to validate all prefixes other than `lt` and `mt`. This will ensure that users are not able to search for persons with fields that do not exist in the `Person` object. + +However, we need to be careful about overzealous input validation where users may still want to search for fields using incomplete parts of a field, and hence we have to balance these two considerations. + +For example, an extreme case will be to search for persons with the `Name` field with `~`, which is disallowed to begin with as `~` is not a valid character for a name. We plan to inform the user outright that the search is invalid and will not return any results. + +
    + +#### Update UI to Wrap Text + +Currently, the `ResultDisplay` box does not wrap text, which means that long lines of text will extend beyond the width of the box. This results in the need for two scroll bars, a horizontal one for the result box and a vertical one for the currently shown list of persons. This is not ideal as it makes the UI less optimized for the target audience, who prefer using a CLI-optimized application and prefer not to use mouse controls to scroll through scroll boxes. + +##### Planned Implementation + +We plan to modify the `ResultDisplay` box to wrap text so that there is no longer a need for the horizontal scroll bar in the `ResultDisplay` box. + +In the case where the wrapped text still exceeds the height of the `ResultDisplay` box, we plan to enable it to dynamically adjust its height as needed. + +
    + +#### Primary Key: Use Both `Matric` and `Email` + +Currently, only `Email` is used as a unique identifier for `Person` objects. However, this means that two `Person` objects can have different `Email`s but the same `Matric` number. This clashes with the real-life constraint that NUS students, in particular CS1101S students, are put under, where Matriculation numbers are supposed to be unique for each student. Our planned enhancement hence aims to better reflect real-life constraints. + +##### Planned Implementation + +Currently, the `hasPerson` method in the `Model` class checks for the existence of a `Person` object based on the `Email` field. We plan to modify this method to check for the existence of a `Person` object based on both the `Email` and `Matric` fields. This will ensure that two `Person` objects cannot have the same `Matric` number. + +However, more checking needs to be done to ensure persons cannot have different overall unique identifiers, but the same `Email` or `Matric` field. (E.g. two persons cannot have the same `Email` but different `Matric` numbers.) + +Additionally, some persons such as staff members and course instructors may not have a `Matric` field. Hence, careful consideration needs to be made to implement this new method of checking for unique identifiers. + +
    + +#### UX: Make Sample Data Tags More Relevant and Helpful to the User + +Currently, the sample data tags are not very helpful to the user, having tags like `friends`, `neighbors` and `family`. This may pose confusion to users about the context of the application, which is the head TA's management of persons related to CS1101S. + +##### Planned Implementation + +Remove all `Tag` objects that are in the sample data that border on irrelevancy. This can be done by modifying the `SampleDataUtil` class to not add these tags to the sample data. + +Retain all other relevant `Tag` objects like `colleagues` and `student` to better reflect the context of the application. + +
    + +-------------------------------------------------------------------------------------------------------------------- +
    + +## **Documentation, Logging, Testing, Configuration, Dev-Ops** + +* [Documentation guide](Documentation.md) +* [Testing guide](Testing.md) +* [Logging guide](Logging.md) +* [Configuration guide](Configuration.md) +* [DevOps guide](DevOps.md) + +-------------------------------------------------------------------------------------------------------------------- + +
    + +
    + +## **Appendix** + +
    + +### Appendix A: Product Scope + +**Target user profile**: + +**Name:** Sarah Johnson, +**Age:** 23, +**Occupation:** Head Tutor for CS1101S + +* Head tutor for CS1101S course +* Has a need to manage various aspects of course administration +* Has a need to schedule classes +* Has a need to coordinate with teaching assistants +* Has a need to effectively communicate with students +* Has a need to manage a significant number of persons +* Prefer desktop apps over other types +* Can type fast +* Prefers typing to mouse interactions +* Is reasonably comfortable using CLI apps + +
    + +**Value proposition**: + +* Manage persons faster than a typical mouse/GUI driven app +* Centralized platform to store and manage person details for all relevant individuals involved in course administration +* Able to store and manage exam scores for all students in the course +* Easier access to information through organizing relevant persons into different subgroups +* Able to set up the application through different data-loading options +* Able to assist with management of large scale communication + +
    + +**Problem scope**: + +* The CS1101S Head Tutor will face challenges in effectively organizing and managing contact information within the department due to the large scale the course has to operate on. Existing methods, such as paper-based lists or basic digital spreadsheets, lack the necessary functionality to efficiently handle the diverse needs of proper contact management. There is a need for a user-friendly and offline-capable address book solution tailored specifically to the needs of a single user. This address book system should provide features such as easy contact entry and editing, quick search functionality, customizable categorization options, and the ability to add notes for each contact. Additionally, it should operate offline without requiring an internet connection and should not rely on complex database management systems. +* While Avengers Assemble will greatly improve contact management and organization for the CS1101S Head Tutor, it will not address broader departmental communication or collaboration needs beyond individual contact management since the application is designed to be a single-user system. It will not facilitate communication between users or provide collaboration tools for group projects or tasks. Additionally, the address book system will not handle complex data analysis or reporting functions beyond basic contact information management. Finally, while the system will provide offline functionality, it will not offer real-time synchronization with online databases or cloud storage solutions. + +
    + +
    + +
    + +### Appendix B: User Stories + +Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` + +#### General + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PriorityAs a …​I want to …​So that I can…​
    * * *potential user exploring the appsee the app populated with sample dataimmediately see an example of the app in use
    * * *new usersee usage instructionsrefer to instructions when I forget how to use the App
    * * *new usereasily clear the example datastart using the app with real-life data
    * *experienced useruse the application offlineupdate and interact with it anywhere
    + +
    + +#### For Contact Management + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PriorityAs a …​I want to …​So that I can…​
    * * *head tutor using the appimport persons from a CSV fileeasily add a large number of persons to the application
    * * *new usersave the data I input into the appdon't lose the information I've entered
    * * *useradd a new personmake minor additions to the persons in the application
    * * *userupdate and edit person detailskeep my persons list accurate
    * * *userdelete a personremove entries that I no longer need
    * * *userdelete a specific group of entriesremove multiple entries that I no longer need more efficiently
    * * *userview all saved contactsoversee the data stored within my app
    * * *userfind a person through their particularslocate details of persons without having to go through the entire list
    * * *head tutor using the appcategorize my persons into groupsmanage different groups of students effectively
    * * *head tutor using the appcopy email addresses of a groupeffectively communicate with target groups
    * * *head tutor using the appexport the details of persons to a CSVeasily share the details of a group with others
    + +
    + +
    + +#### For Exam and Score Management + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PriorityAs a …​I want to …​So that I can…​
    * * *head tutor using the appimport assessment scores from a CSV fileeasily add a large number of scores to the application
    * * *head tutor using the appadd exams to the appkeep track of student performance
    * * *head tutor using the appdelete exams from the appremove exams that are no longer relevant
    * * *head tutor using the appview scores for a specific examanalyze student scores
    * * *head tutor using the appadd scores to the appkeep track of student performance
    * * *head tutor using the appedit scores in the appcorrect errors in the scores
    * * *head tutor using the appdelete scores from the appremove scores that are no longer relevant
    * * *head tutor using the appexport scores to a CSV fileeasily share the scores with others
    * * *head tutor using the appview statistics of scoresanalyze student performance
    + +-------------------------------------------------------------------------------------------------------------------- + +
    + +
    + +### Appendix C: Use Cases + +(For all use cases below, the **System** is the `AvengersAssemble` and the **Actor** is the `user`, unless specified otherwise) + +##### Use Case: UC01 — Getting Help + +**MSS:** + +1. User requests help information. +2. AvengersAssemble copies the link to the user guide to the user's clipboard. +3. User pastes the link into a browser to access the user guide. + + Use case ends. + +##### Use Case: UC02 — Clearing Sample Data + +**MSS:** + +1. User requests to clear the sample data. +2. AvengersAssemble clears the sample data. +3. AvengersAssemble displays a message indicating that the sample data has been cleared. + + Use case ends. + +##### Use Case: UC03 — Importing Person Details from a CSV File + +**MSS:** + +1. User requests to import person details from a CSV file. +2. AvengersAssemble imports the person details from the CSV file. +3. AvengersAssemble displays a message indicating that the person details have been imported. + + Use case ends. + +**Extensions:** + +* 1a. The file to be imported is not a CSV file. + + * 1a1. AvengersAssemble displays an error message indicating that the file type is not recognized and should be a CSV file. + + Use case ends. + +* 1b. AvengersAssemble cannot find the file to be imported. + + * 1b1. AvengersAssemble displays a message indicating that the file is not recognized. + + Use case ends. + +
    + +##### Use Case: UC04 — Adding a Person + +**MSS:** + +1. User requests to add a new person and inputs details for the new person. +2. AvengersAssemble saves the new person's information. +3. AvengersAssemble confirms the addition of the new person. + + Use case ends. + +**Extensions:** + +* 1a. User does not input all compulsory parameters along with the person. + + * 1a1. AvengersAssemble prompts the user on the proper usage of the command. + + Step 1a1 is repeated until the data entered is correct. + + Use case resumes at step 2. + +* 1b. User tries to add a person with an existing email address. + + * 1b1. AvengersAssemble displays an error message informing the user that the email address already exists. + + Step 1b1 is repeated until a valid email address is entered. + + Use case resumes at step 2. + +##### Use Case: UC05 — Editing a Person's Details + +**MSS:** + +1. User requests to edit a specific person with updated details. +2. AvengersAssemble saves the updated details. +3. AvengersAssemble confirms the successful update. + + Use case ends. + +**Extensions:** + +* 1a. User does not input enough parameters along with the person. + + * 1a1. AvengersAssemble prompts the user on the proper usage of the command. + + Step 1a1 is repeated until the data entered is correct. + + Use case resumes at step 2. + +* 1b. The selected person does not exist. + + * 1b1. AvengersAssemble displays an error message indicating that the person does not exist. + + Use case ends. + +##### Use Case: UC06 — Deleting a Person + +**MSS:** + +1. User !!requests to list persons (UC08)!! +2. AvengersAssemble shows a list of persons +3. User requests to delete a specific person in the list +4. AvengersAssemble deletes the person + + Use case ends. + +**Extensions:** + +* 2a. The list is empty. + + Use case ends. + +* 3a. The given index is invalid. + + * 3a1. AvengersAssemble shows an error message. + + Use case resumes at step 2. + +##### Use Case: UC07 — Deleting All Shown Persons + +**MSS:** + +1. User !!requests to find group of persons (UC09)!! by desired requirements +2. User requests to delete all listed persons. +3. AvengersAssemble deletes all listed persons. +4. AvengersAssemble displays a message to confirm that all listed persons have been deleted. + + Use case ends. + +**Extensions:** + +* 2a. No persons are listed. + + * 2a1. AvengersAssemble displays a message indicating that there is no persons to delete. + + Use case ends. + +* 2b. User has a filtered view that contains all existing persons. + + * 2b1. AvengersAssemble displays a message indicating that all persons cannot be deleted at once. + + Use case ends. + +
    + +##### Use Case: UC08 — Listing All Persons + +**MSS:** + +1. User requests to list persons. +2. AvengersAssemble shows the list of persons. +3. User views the list of persons. + + Use case ends. + +**Extensions:** + +* 2a. The list is empty. + + * 2a1. AvengersAssemble displays a message indicating that the list is empty. + + Use case ends. + +##### Use Case: UC09 — Finding Persons + +**MSS:** + +1. User requests to find a specific group of persons matching the search criteria. +2. AvengersAssemble displays a list of persons matching the criteria. + + Use case ends. + +**Extensions:** + +* 1a. No persons match the search criteria. + + * 1a1. AvengersAssemble displays a message indicating that no persons match the search criteria. + + Use case ends. + +
    + +##### Use Case: UC10 — Copying Email Addresses + +**MSS:** + +1. User requests to copy emails of currently displayed persons. +2. AvengersAssemble copies the emails of currently displayed persons +into user's clipboard. +3. AvengersAssemble notifies the user that emails have been copied. +4. User can paste emails when composing emails. + + Use case ends. + +**Extensions:** + +* 2a. No persons currently displayed. + + * 2a1. AvengersAssemble displays a message indicating that no persons are currently displayed. + + Use case ends. + +##### Use Case: UC11 — Exporting Persons to CSV + +**MSS:** + +1. User !!requests to filter persons (UC09)!! by desired requirements +2. User requests to export all listed persons and details to a CSV file. +3. AvengersAssemble exports the persons to a CSV file. +4. AvengersAssemble displays a message to confirm that all listed persons have been exported to a CSV file. + + Use case ends. + +**Extensions:** + +* 2a. No persons are listed. + + * 2a2. AvengersAssemble displays a message indicating that there is no persons to export. + + Use case ends. + + +
    + +##### Use Case: UC12 — Importing Exam Results from a CSV File + +**MSS:** + +1. User requests to import exam results from a CSV file. +2. AvengersAssemble displays a message that all exam results have been imported. + + Use case ends. + +**Extensions:** + +* 2a. AvengersAssemble cannot find the file specified. + + * 2a1. AvengersAssemble displays a message indicating that the file is not recognized. + + Use case ends. + +* 2b. The file to be imported is not a CSV file. + + * 2b1. AvengersAssemble displays an error message indicating that the file type is not recognized and should be a CSV file + + Use case ends. + +* 2c. There are duplicate entries in the CSV file. + + * 2c1. AvengersAssemble displays a message indicating that there are duplicate entries in the CSV file, and only the first instance has been kept. + + Use case ends. + +* 2d. The CSV file contains invalid entries. + + * 2d1. AvengersAssemble displays a message indicating that there are invalid entries in the CSV file, and all other valid entries have been imported. + + Use case ends. + +
    + +##### Use Case: UC13 — Adding an Exam + +**MSS:** + +1. User requests to add an exam. +2. AvengersAssemble displays a message that the exam has been added. + + Use case ends. + +**Extensions:** + +* 1a. User does not input all compulsory parameters along with the exam. + + * 1a1. AvengersAssemble prompts the user on the proper usage of the command. + + Step 1a1 is repeated until the data entered is correct. + + Use case resumes at step 2. + +* 1b. User tries to add an exam with an existing name. + + * 1b1. AvengersAssemble displays an error message informing the user that the exam name already exists. + + Step 1b1 is repeated until a valid exam name is entered. + + Use case resumes at step 2. + +* 1c. User tries to add an exam with an invalid score. + + * 1c1. AvengersAssemble displays an error message informing the user that the score is invalid. + + Step 1c1 is repeated until a valid score is entered. + + Use case resumes at step 2. + +* 1d. User tries to add an exam with an invalid name. + + * 1d1. AvengersAssemble displays an error message informing the user that the name is invalid. + + Step 1d1 is repeated until a valid name is entered. + + Use case resumes at step 2. + +
    + +##### Use Case: UC14 — Deleting an Exam + +**MSS:** + +1. User requests to delete an exam. +2. AvengersAssemble displays a message that the exam has been deleted. + + Use case ends. + +**Extensions:** + +* 1a. The exam does not exist. + + * 1a1. AvengersAssemble displays an error message indicating that the exam does not exist. + + Use case ends. + +##### Use Case: UC15 — Selecting an Exam + +**MSS:** + +1. User requests to select an exam. +2. AvengersAssemble displays the scores of the selected exam. + + Use case ends. + +**Extensions:** + +* 1a. The exam does not exist. + + * 1a1. AvengersAssemble displays an error message indicating that the exam does not exist. + + Use case ends. + +##### Use Case: UC16 — Deselecting an Exam + +**MSS:** + +1. User requests to deselect an exam. +2. AvengersAssemble displays the persons without the scores of the selected exam. + + Use case ends. + +**Extensions:** + +* 1a. The exam does not exist. + + * 1a1. AvengersAssemble displays an error message indicating that the exam does not exist. + + Use case ends. + +
    + +##### Use Case: UC17 — Adding Scores to a Student for an Exam + +**MSS:** + +1. User !!requests to select an exam (UC15)!! to add scores to. +2. User requests to add scores to a student for the selected exam. +3. AvengersAssemble displays a message that the scores have been added. + + Use case ends. + +**Extensions:** + +* 2a. The student does not exist. + + * 2a1. AvengersAssemble displays an error message indicating that the student does not exist. + + Use case ends. + +* 2b. The student already has a score for the exam. + + * 2b1. AvengersAssemble displays an error message indicating that the student already has a score for the exam. + + Use case ends. + +##### Use Case: UC18 — Editing Scores for a Student for an Exam + +**MSS:** + +1. User !!requests to select an exam (UC15)!! to edit scores for. +2. User requests to edit scores for a student for the selected exam. +3. AvengersAssemble displays a message that the scores have been edited. + + Use case ends. + +**Extensions:** + +* 2a. The student does not exist. + + * 2a1. AvengersAssemble displays an error message indicating that the student does not exist. + + Use case ends. + +* 2b. The student does not have a score for the exam. + + * 2b1. AvengersAssemble displays an error message indicating that the student does not have a score for the exam. + + Use case ends. + +* 2c. The score is invalid. + + * 2c1. AvengersAssemble displays an error message indicating that the score is invalid. + + Use case ends. + +##### Use Case: UC19 — Deleting Scores for a Student for an Exam + +**MSS:** + +1. User !!requests to select an exam (UC15)!! to delete scores for. +2. User requests to delete scores for a student for the selected exam. +3. AvengersAssemble displays a message that the scores have been deleted. + + Use case ends. + +**Extensions:** + +* 2a. The student does not exist. + + * 2a1. AvengersAssemble displays an error message indicating that the student does not exist. + + Use case ends. + +* 2b. The student does not have a score for the exam. + + * 2b1. AvengersAssemble displays an error message indicating that the student does not have a score for the exam. + + Use case ends. + +##### Use Case: UC20 — Viewing Statistics of Scores + +**MSS:** + +1. User !!requests to select an exam (UC15)!! to view statistics of scores for. +2. AvengersAssemble displays the statistics of scores for the selected exam. + + Use case ends. + +**Extensions:** + +* 2a. There are no scores for the exam. + + * 2a1. AvengersAssemble does not display any statistics. + + Use case ends. + +##### Use Case: UC21 — Exit Application + +**MSS:** + +1. User requests to exit the application. +2. AvengersAssemble exits the application. + + Use case ends. + +-------------------------------------------------------------------------------------------------------------------- + +
    + +
    + +### Appendix D: Non-Functional Requirements + +1. Should work on any _mainstream OS_ as long as it has `Java 11` or above installed. +2. Should be able to hold up to 2000 persons without a noticeable sluggishness in performance for typical usage. +3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. +4. A user should be able to import up to 2000 persons from an external source without a noticeable sluggishness in performance for typical usage. +5. The application should provide comprehensive documentation and help resources to assist users in understanding how to use the software effectively. + +-------------------------------------------------------------------------------------------------------------------- + +
    + +### Appendix E: Glossary + +* **OS** : Operating System +* **Mainstream OS**: Windows, Linux, MacOS +* **CLI**: Command Line Interface +* **CSV**: Comma Separated Values - a file format used to store tabular data +* **MSS**: Main Success Scenario +* **UI**: User Interface +* **GUI**: Graphical User Interface +* **API**: Application Programming Interface - used to define how the components of this software interact with each other +* **Matric**: Matriculation number of a student + +-------------------------------------------------------------------------------------------------------------------- + +
    + + +
    + +### Appendix F: Instructions for Manual Testing + +Given below are instructions to test the app manually. + + + +**Note:** These instructions only provide a starting point for testers to work on; +testers are expected to do more *exploratory* testing. + + + +
    + +#### Launch and Shutdown + +1. Initial launch. + * **Test case:** Launching the app for the first time.
    + 1. Download the jar file and copy into an empty folder. + 2. Open Terminal and type the following: + ```bash + java -jar avengersassemble.jar + ``` + **Expected:** Shows the GUI with a set of sample persons. The window size may not be optimal. +

    +2. Saving window preferences. + * **Prerequisites:** + * Launch the app. + * Resize the window. + * Close the app. + * **Test case:** Launch the app.
    + **Expected:** The most recent window size and location is retained. +

    + +3. Shutdown. + * **Test case:** `exit`
    + **Expected:** The GUI closes and the application exits. +

    + +
    + +
    + +#### Saving Data + +1. Saving of data. + * **Prerequisites:** + * The app is a clean state. +

    + * **Test case:** Launch and exit the app.
    + **Expected:** A new `data/avengersassemble.json` file is created. This is the storage file. +

    + +1. Dealing with missing or corrupted data files. + + * **Prerequisites:** + * There is an existing storage file in the default location. +

    + * **Test case:** Delete the storage file, then launch and exit the app.
    + **Expected:** A new `data/avengersassemble.json` file populated with sample data is created. +

    + * **Test case:** Corrupt the `data/avengersassemble.json` file by adding random text to it.
    + **Expected:** The app should ignore the corrupted file and create a new empty `data/avengersassemble.json` file when launched and interacted with. +

    + +
    + +#### Getting Help: `help` + +**Command:** `help`
    +**More information on usage:** Getting Help + +1. Getting more information on the usage of the app. + + * **Test case:** `help`
    + **Expected:** Link to the user guide is copied to the clipboard. Status message shows that the link has been copied. + The link should be accessible from a browser. + +
    + +
    + +#### Clearing All Persons and Exams: `clear` + +**Command:** `clear`
    +**More information on usage:** Clearing All Entries + +1. Clearing all contact information from the app. + + * **Prerequisites:** + * Ensure that there is at least one person and exam in the app. +

    + * **Test case:** `clear`
    + **Expected:** All persons and exams are deleted from the list. Status message shows that all persons and exams have been + deleted from the app. + +
    + +
    + +
    + +#### Importing Persons: `import` + +**Command:** `import`
    +**More information on usage:** Importing Persons + +The import command requires the use of an external CSV file. The test cases below assume that the tests are run on a Windows system, and that the CSV file is located at the path `C:\path\to\file.csv`. Please modify the filepath accordingly based on where your file is stored and your operating system. + + +Note: On Window systems, you can right-click the file and copy the file path, remember to remove the double quotes.
    On MacOS, you can drag the file into the terminal to get the file path.
    On Linux, you can use the pwd command to get the current directory and append the file name to it. +
    + +1. Importing data from a CSV file + + * **Prerequisites** + * There is a file at `C:\path\to\file.csv` with the following content: + ``` + name,email,address,phone + Alice,alice@gmail.com,wonderland,123 + ``` + * Initially, the persons list is empty. + +
    + + * **Test case:** `import i|C:\path\to\file.csv`
    + **Expected:** The person with the following details is added: + * Name: `Alice` + * Email: `alice@gmail.com` + * Address: `wonderland` + * Phone: `123` + +
    + +2. Importing data from a CSV File that does not exist + + * **Prerequisites** + * No CSV file at the path `C:\path\to\file.csv` + +
    + + * **Test case:** `import i|C:\path\to\file.csv`
    + **Expected:** Error message shown in the error report. No change in list of persons. + +
    + +3. Importing data from a file that is not a CSV file + + * **Prerequisites** + * There is a file at the path `C:\path\to\file.txt` + +
    + + * **Test case:** `import i|C:\path\to\file.txt`
    + **Expected:** Error message shown in the error report. No change in list of persons. + +
    + +
    + +4. Importing data from a CSV File with duplicate compulsory headers in header row + + * **Prerequisites** + * A CSV file with duplicate compulsory headers (e.g. 2 header columns named 'name') at the path `C:\path\to\file.csv` with the following content: + ``` + name,email,address,phone,name + Alice,alice@gmail.com,wonderland,123,bob + ``` + * Initially, the persons list is empty. + +
    + + * **Test case:** `import i|C:\path\to\file.csv`
    + **Expected:** First occurrence of the header is used. Columns with duplicate headers are ignored. The person with the following details is added: + * Name: `Alice` + * Email: `alice@gmail.com` + * Address: `wonderland` + * Phone: `123` + +
    + +5. Importing data from a CSV file with missing compulsory headers in header row + + * **Prerequisites** + * A CSV file with missing compulsory headers at the path `C:\path\to\file.csv` with the following content (missing the `name` header): + ``` + email,address,phone + alice@gmail.com,wonderland,123 + ``` + +
    + + * **Test case:** `import i|C:\path\to\file.csv`
    + * **Expected:** Error message shown that `name` header is missing in the error report. No change in list of persons. + +
    + +6. Importing data from a CSV file with missing compulsory values in a row + + * **Prerequisites** + * A CSV file with missing compulsory values in a row at the path `C:\path\to\file.csv` with the following content: + ``` + name,email,address,phone + Alice,,wonderland,123 + Bob,bob@gmail.com,town,123 + ``` + * **Initially, the persons list is empty.** + +
    + + * **Test case:** `import i|C:\path\to\file.csv`
    + * **Expected:** Error message in the results in the display indicating that import has failed with errors. Only one person with the following details is added: + * Name: `Bob` + * Email: `bob@gmail.com` + * Address: `town` + * Phone: `123` + +
    + +
    + +7. Importing data from a CSV file with extra headers in header row + + * **Prerequisites** + * A CSV file with extra headers in header row at the path `C:\path\to\file.csv` with the following content: + ``` + name,email,address,phone,extra + Alice,alice@gmail.com,123,123,extra + ``` + * Initially, the persons list is empty. + +
    + + * **Test case:** `import i|C:\path\to\file.csv`
    + * **Expected:** Only the compulsory headers are read. Optional headers are read if present. Extra headers are ignored. The person with the following details is added: + * Name: `Alice` + * Email: `alice@gmail.com` + * Address: `123` + * Phone: `123` + +
    + +8. Importing data from a CSV file with unequal number of values in a row as the number of headers + + * **Prerequisites** + * A CSV file with unequal number of values in a row as the number of headers at the path `C:\path\to\file.csv` with the following content: + ``` + name,email,address,phone + Alice,alice@gmail.com,wonderland,123,123 + Bob,bob@gmail.com,town,123 + ``` + * Initially, the persons list is empty. + +
    + + * **Test case:** `import i|C:\path\to\file.csv`
    + **Expected:** Error message in the results in the display indicating that import has failed with errors. Only one person with the following details is added: + * Name: `Bob` + * Email: `bob@gmail.com` + * Address: `town` + * Phone: `123` + +
    + +9. Importing data from an empty CSV file + + * **Prerequisites** + * An empty CSV file at the path `C:\path\to\file.csv` + +
    + + * **Test case:** `import i|C:\path\to\file.csv`
    + * **Expected:** A message that no person is imported is shown. No change in list of persons. + +
    + +
    + +
    + +#### Adding a Person: `add` + +**Command:** `add`
    +**More information on usage:** Adding a Person + +1. Adding a person with all fields. + + * **Prerequisites:** + * No persons in the list. +

    + * **Test case:** `add n|Alice p|98765432 a|Hall e|e09123456@u.nus.edu m|A1234567X r|R2 s|S1 t|excelling`
    + **Expected:** A person with the following fields is added to the list: + * Name: `Alice` + * Phone: `98765432` + * Address: `Hall` + * Email: `e09123456@u.nus.edu` + * Matric: `A1234567X` + * Reflection: `R2` + * Studio: `S1` + * Tags: `excelling`, `student` + + + + **Note:** If a `Matric` number is provided, the person is automatically tagged as a `student`. + + + + * **Test case (missing `Address` and `Phone` fields):** `add n|Alice e|e09123456@u.nus.edu`
    + **Expected:** An error message is shown indicating that the `Address` and `Phone` fields are missing. +

    + + +2. Adding a person with repeated prefixes. + + * **Prerequisites:** + * No persons in the list. +

    + * **Test case (repeating `Name` field):** `add n|Ali n|Ali p|98765432 a|Hall e|test@test.com m|A1234567X`
    + **Expected:** An error message is shown indicating that the `Name` field is repeated. +

    + + +3. Adding a person whose `Email` already exists. + + * **Prerequisites:** + * A person with email `e1234567@u.nus.edu` already exists in the list. +

    + * **Test case (`Email` already exists):** `add n|Alice p|987 a|Hall e|e1234567@u.nus.edu`
    + **Expected:** An error message is shown indicating that the email already exists. +

    + +
    + +4. Adding a person with only compulsory fields. + + * **Prerequisites:** + * No persons in the list. +

    + * **Test case:** `add n|Alice p|98765432 a|Hall e|e09123456@u.nus.edu`
    + **Expected:** A person with the following fields is added to the list: + * Name: `Alice` + * Phone: `98765432` + * Address: `Hall` + * Email: `e09123456@u.nus.edu` +

    + +5. Adding a person with matriculation number + + * **Prerequisites:** + * No persons in the list. +

    + * **Test case:** `add n|Alice p|98765432 a|Hall e|alice@example.com m|A1234567X`
    + **Expected:** A person with the following fields is added to the list: + * Name: `Alice` + * Phone: `98765432` + * Address: `Hall` + * Email: `alice@example.com` + * Matric: `A1234567X` + * Tags: `student` +

    + Note that the `student` tag is automatically added to the new person. +

    + * **Test case:** `add n|Alice p|98765432 a|Hall e|alice@example.com`
    + **Expected:** A person with the following fields is added to the list: + * Name: `Alice` + * Phone: `98765432` + * Address: `Hall` + * Email: `alice@example.com` +

    + Note that there is no automatic tagging. +

    + +
    + +
    + +#### Editing a Person: `edit` + +**Command:** `edit`
    +**More information on usage:** Editing a Person + +1. Editing a person with all fields. + + * **Prerequisites:** + * Start with the provided sample data. +

    + * **Test case:** `edit 1 n|new name p|123 a|new home e|newemail@eg.com m|A0000000X r|R1 s|S1 t|tag1 t|tag2`
    + **Expected:** The first person’s details are updated with all the new values. +

    + * **Other successful test cases to try:** Include a combination of updating some fields and not updating others.
    + **Expected:** Similar to above. +

    + + +2. Editing a person with repeated prefixes. + + * **Prerequisites:** + * Start with the provided sample data. +

    + * **Test case (repeated `n|` prefix):** `edit 1 n|new name n|new name 2 p|123 a|new address`
    + **Expected:** An error message is shown indicating that the `Name` field is repeated. +

    + * **Other incorrect `edit` commands to try:** Commands with repeated `p|`, `a|`, `e|`, `m|`, `r|`, `s|`, `t|` prefixes.
    + **Expected:** Similar to previous. +

    + +3. Editing a Person's `Email` to an Existing `Email`. + + * **Prerequisites:** + * Start with the provided sample data. Note the emails of the first and second person. +

    + * **Test case:** `edit 1 e|berniceyu@example.com`
    + **Expected:** An error message is shown indicating that the email already exists. +

    + +
    + +
    + +#### Deleting a Person: `delete` + +**Command:** `delete`
    +**More information on usage:** Deleting a Person + +1. Deleting a person while all persons are being shown. + + * **Prerequisites:** + * List all persons using the `list` command. Multiple persons in the list. +

    + * **Test case:** `delete 1`
    + **Expected:** First person is deleted from the list. Details of the deleted person shown in the status message. +

    + * **Test case (invalid index):** `delete 0`
    + **Expected:** No person is deleted. Error details shown in the status message. +

    + * **Other incorrect `delete` commands to try:** `delete`, `delete x`, `...` (where x is larger than the list size)
    + **Expected:** Similar to previous. +

    + +2. Deleting a person while some persons are being shown. + + * **Prerequisites:** + * Filter persons using the `find` command. Multiple but not all persons in the list. +

    + * **Test case**: `delete 1`
    + **Expected:** First person in the filtered list is deleted. Details of the deleted person shown in the status message. +

    + * **Test case**: `delete 0`
    + **Expected:** No person is deleted. Error details shown in the status message. +

    + * **Other incorrect `delete` commands to try:** `delete`, `delete x`
    + **Expected:** Similar to previous. +

    + +3. Deleting a person while no persons are being shown. + + * **Prerequisites:** + * Filter persons using the `find` command such that there are no persons in the list, or delete all persons with `clear`. +

    + * **Test case**: `delete 1`
    + **Expected:** No person is deleted. Error details shown in the status message. +

    + +
    + +
    + +#### Deleting Shown Persons: `deleteShown` + +**Command:** `deleteShown`
    +**More information on usage:** Deleting Filtered Persons + + +1. Deleting a proper subset of all persons. + + * **Prerequisites:** + * Filter persons using the `find` command such that there are multiple, but not all, persons in the list. +

    + * **Test case:** `deleteShown`
    + **Expected:** All persons currently shown are deleted, and the list is updated to show all remaining persons. +

    + * **Other successful test cases:** `deleteShown x`
    + **Expected:** Similar to previous, as extraneous parameters for single-word commands are treated as typos and ignored. +

    + +2. Deleting all persons. + + * **Prerequisites:** + * Filter persons using the `find` command such that all persons are shown, or list all persons with `list`. +

    + * **Test case:** `deleteShown`
    + **Expected:** An error is shown indicating that all persons cannot be deleted at once. +

    + * **Other incorrect `deleteShown` commands to try:** `deleteShown x`
    + **Expected:** Similar to previous. +

    + +
    + +#### Listing All Persons: `list` + +**Command:** `list`
    +**More information on usage:** Listing All Persons + +1. Starting with sample data. + + * **Prerequisites:** + * Start with the provided sample data. +

    + * **Test case:** `list`
    + **Expected:** All persons are shown in the list. +

    + * **Other successful test cases:** `list x`
    + **Expected:** Similar to previous, as extraneous parameters for single-word commands are treated as typos and ignored. +

    + +2. Starting with a filtered list. + + * **Prerequisites:** + * Filter persons using the `find` command such that there are multiple, but not all, persons in the list. +

    + * **Test case:** `list`
    + **Expected:** All persons in the overall list are shown. +

    + +
    + +
    + +#### Finding a Person: `find` + +**Command:** `find`
    +**More information on usage:** Filtering Persons + +1. Finding persons by contact details. + + * **Prerequisites:** + * Ensure that there are multiple persons in the app. +

    + * **Test case:** `find n|Alice`
    + **Expected:** Persons with the name "Alice" are shown. Status message shows the number of persons found. +

    + * **Test case:** `find e|alice`
    + **Expected:** Persons with emails that contain the word "alice" are shown. Status message shows the number of persons found. +

    + * **Test case:** `find p|123`
    + **Expected:** Persons with phone numbers that contain the digits "123" are shown. Status message shows the number of persons found. +

    + * **Test case:** `find a|Ang Mo Kio`
    + **Expected:** Persons with addresses that contain the word "Ang Mo Kio" are shown. Status message shows the number of persons found. +

    + * **Test case:** `find t|student`
    + **Expected:** Persons with the tag "student" are shown. Status message shows the number of persons found. +

    + * **Test case:** `find m|A123`
    + **Expected:** Persons with matriculation numbers containing "A123" are shown. Status message shows the number of persons found. +

    + * **Test case:** `find r|R01`
    + **Expected:** Persons with the reflection "R01" are shown. Status message shows the number of persons found. +

    + * **Test case:** `find s|S01`
    + **Expected:** Persons with the studio "S01" are shown. Status message shows the number of persons found. +

    + +
    + +2. Finding persons by score. + + * **Prerequisites:** + * Ensure that there are multiple persons in the app. + * Ensure that at least one exam is added using the `addExam` command. For this example, we shall add a new exam with name `test exam` and maximum score `100`. + * Ensure an exam is selected using the `selectExam` command. For this example, we shall select the `test exam`. +

    + * **Test case:** `find lt|50`
    + **Expected:** Persons with scores less than 50 are shown. Status message shows the number of persons found. +

    + * **Test case:** `find mt|50`
    + **Expected:** Persons with scores more than 50 are shown. Status message shows the number of persons found. +

    + * **Test case:** `find lt|-1`
    + **Expected:** An error message is shown indicating that the `score` provided is invalid. +

    + * **Test case:** `find mt|101`
    + **Expected:** An error message is shown indicating that the `score` provided is greater than the maximum score of the selected exam. +

    + +3. Finding persons by multiple prefixes. + + * **Prerequisites:** + * Ensure that there are multiple persons in the app. +

    + * **Test case (multiple unique prefixes):** `find n|Alice e|Alice`
    + **Expected:** An error message is shown indicating that the format of the command is incorrect. +

    + * **Similar incorrect test cases to try:** Any combination of two or more different prefixes
    + **Expected:** Similar to previous. +

    + * **Test case (multiple duplicate prefixes):** `find n|Alice n|Bob`
    + **Expected:** An error message is shown indicating that the prefix `n` is duplicated. +

    + * **Similar incorrect test cases to try:** Duplicated `p|`, `a|`, `e|`, `m|`, `r|`, `s|`, `t|`, `mt|`, `lt|` prefixes.
    + **Expected:** Similar to previous. + +
    + +
    + +
    + +#### Copying Emails: `copy` + +**Command:** `copy`
    +**More information on usage:** Copying Emails + +1. Copying the emails of all persons. + + * **Prerequisites:** + * Ensure that there are multiple persons in the app. + * Ensure all persons are displayed using the `list` command. +

    + * **Test case:** `copy`
    + **Expected:** All emails are copied to the clipboard. Status message shows the number of emails copied. +

    + +2. Copying the emails of a specific group. + + * **Prerequisites:** + * Ensure that there are multiple persons in the app. + * Filter the person list using the `find` command. +

    + * **Test case:** `copy`
    + **Expected:** All emails of the currently displayed persons are copied to the clipboard. Status message shows the number of emails copied. + +
    + +
    + +
    + +#### Exporting Data to a CSV File: `export` + +**Command:** `export`
    +**More information on usage:** Exporting Data to a CSV File + +1. Exporting data while all persons are displayed. + + * **Prerequisites:** + * Start with the provided sample data. + * List all persons using the `list` command. +

    + * **Test case:** `export`
    + **Expected:** A file named `addressbookdata` containing `avengersassemble.csv` is created in the same directory where the JAR file of the Avengers Assemble is located. All currently displayed persons and their details are exported to the CSV file. +

    + +2. Exporting data while person list is filtered. + + * **Prerequisites:** + * Start with the provided sample data. + * Filter the person list using the `find` command. +

    + * **Test case:** Similar to previous.
    + **Expected:** Similar to previous. +

    + +3. Exporting data with exams and exam scores added. + + * **Prerequisites:** + * Start with the provided sample data. + * Add an exam using the `addExam` command. For this example, we shall add an exam with name `Test Exam`. + * List all persons using the `list` command. +

    + * **Test case:** `export`
    + **Expected:** A file named `addressbookdata` containing `avengersassemble.csv` is created in the same directory where the JAR file of the Avengers Assemble is located. All currently displayed persons and their details are exported to the CSV file. A column with column heading `Exam:Test Exam` is present in the same CSV file, but no values are present in that column. +

    + * **Test case:** Add exam scores to persons in displayed list using `addScore`, then `export`
    + **Expected:** A file named `addressbookdata` containing `avengersassemble.csv` is created in the same directory where the JAR file of the Avengers Assemble is located. All currently displayed persons and their details are exported to the CSV file. A column with column heading `Exam:Test Exam` is present in the same CSV file, with corresponding exam scores for each person included in that column. + +
    + +
    + +
    + +#### Adding an Exam: `addExam` + +**Command:** `addExam`
    +**More information on usage:** Adding an Exam + +1. Adding an exam with valid data + + * **Prerequisites:** + * No exams in the exams list. +

    + * **Test case:** `addExam n|Midterm s|100`
    + **Expected:** New exam is added to the exams list. Status message shows the exam added. +

    + * **Other test cases to try:** `addExam n|Final s|100`
    + **Expected:** New exam is added to the exams list. Status message shows the exam added. +

    + +2. Adding an exam that already exists + + * **Prerequisites:** + * An exam of name: Final, Score: 100 exists in the exams list. +

    + * **Test case:** `addExam n|Final s|100`
    + **Expected:** Error message shown in the error report. No change in the exams list. +

    + +3. Adding an exam with missing fields + + * **Prerequisites:** + * No exams in the exams list. +

    + * **Test case (missing score):** `addExam n|Final`
    + **Expected:** Error message shown in the error report. No change in the exams list. + +
    + +
    + +#### Deleting an Exam: `deleteExam` + +**Command:** `deleteExam`
    +**More information on usage:** Deleting an Exam + +1. Deleting an exam + + * **Prerequisites:** + * Exactly one exam in the exams list. Hence, exam has an index of 1. +

    + * **Test case:** `deleteExam 1`
    + **Expected:** First exam is deleted from the exams list. Status message shows the exam deleted. +

    + * **Test case (index out of bounds):** `deleteExam 2`
    + **Expected:** No exam is deleted. Error message shown. No change in the exams list. +

    + * **Test case (no index):** `deleteExam`
    + **Expected:** No exam is deleted. Error message shown. No change in the exams list. + +
    + +
    + +
    + +#### Selecting an Exam: `selectExam` + +**Command:** `selectExam`
    +**More information on usage:** Selecting an Exam + +1. Selecting an exam + + * **Prerequisites:** + * Exactly one exam in the exams list. Hence, exam has an index of 1. +

    + * **Test case:** `selectExam 1`
    + **Expected:** First exam is selected. Status message shows the exam selected. +

    + * **Test case:** `selectExam 0`
    + **Expected:** No exam is selected. Error message shown. No change in the exams list. +

    + * **Test case (index out of bounds):** `selectExam 2`
    + **Expected:** No exam is selected. Error message shown. No change in the exams list. +

    + * **Test case (no index):** `selectExam`
    + **Expected:** No exam is selected. Error message shown. No change in the exams list. + +
    + +
    + +#### Deselecting an Exam: `deselectExam` + +**Command:** `deselectExam`
    +**More information on usage:** Deselecting an Exam + +1. Deselecting an exam + + * **Prerequisites:** + * An exam has been selected. +

    + * **Test case:** `deselectExam`
    + **Expected:** Selected exam is deselected. Status message shows the exam deselected. +

    + * **Test case (no exam selected):** `deselectExam`
    + **Expected:** No exam is deselected. Error message shown. No change in the exams list. + +
    + +
    + +
    + +#### Importing Exam Scores: `importExamScores` + +**Command:** `importExamScores`
    +**More information on usage:** Importing Exam Scores + +1. Importing exam scores from a CSV file. + + * **Prerequisites:** + * Add an `Exam` to the sample data: `addExam n|Midterm s|100`. + * Create a CSV file with the following content:
    + Contents of `/path/to/file.csv`: + + ``` + email,Exam:Midterm + alexyeoh@example.com,50 + ``` + * **Test case:** `importExamScores i|/path/to/file.csv`
    + **Expected:** The person with the email of `alexyeoh@example.com` now has a `Midterm` score of `50`. +

    + +2. Importing an invalid file. + + * **Prerequisites:** + * Start with sample data and the `Midterm` exam. + * Create a file named `invalid.json`. +

    + * **Test case:** `importExamScores i|invalid.json`
    + **Expected:** An error message is shown indicating that the file is not a CSV file. +

    + +3. Importing a CSV file with incorrect formatting. + + * **Prerequisites:** + * Start with sample data and the `Midterm` exam. + * Create a CSV file with the following content:
    + Contents of `/path/to/file.csv`: + ``` + email,Exam:Midterm,email + alexyeoh@example.com,50,alexyeoh@example.com + ``` + * **Test case:** `importExamScores i|/path/to/file.csv`
    + **Expected:** An error message is shown indicating that the email header should exist only in the first column. +

    + * **Other incorrect `importExamScores` commands to try:** CSV files where email is not the first header.
    + **Expected:** Similar to previous. +

    + +
    + +4. Importing a CSV file with duplicate entries. + + * **Prerequisites:** + * Start with sample data and the `Midterm` exam. + * Create a CSV file with the following content:
    + Contents of `/path/to/file.csv`: + + ``` + email,Exam:Midterm,Exam:Midterm + alexyeoh@example.com,50,60 + ``` + * **Test case:** `importExamScores i|/path/to/file.csv`
    + **Expected:** A message is shown indicating that there are duplicate entries in the CSV file, and only the first instance has been kept. The `Midterm` score for the person with the email of `alexyeoh@example.com` is `50`. +

    + +5. Importing a CSV File with invalid entries. + + * **Prerequisites:** + * Start with sample data and the `Midterm` exam. + * Create a CSV file with the following content:
    + Contents of `/path/to/file.csv`: + + ``` + email,Exam:Midterm,Exam:Finals + alexyeoh@example.com,101,50 + berniceyu@example.com,50,60 + nonexistent@example.com,100,100 + ``` + * **Test case:** `importExamScores i|/path/to/file.csv`
    + **Expected:** A message is shown indicating that there are invalid entries in the CSV file, and all other valid entries have been imported. The errors shown are as follows: + + * The score for `alexyeoh@example.com` for the `Midterm` exam is invalid. + * The person with the email `nonexistent@example.com` does not exist in the given list. + * The `Finals` exam does not exist. + Note that the `Midterm` score for the person with the email of `berniceyu@example.com` is `50`. +

    + + * **Other incorrect `importExamScores` commands to try:** CSV files with a mix of invalid scores, nonexistent emails, and nonexistent exams.
    + **Expected:** Similar to previous. +

    + +
    + +
    + +#### Adding a Persons's Exam Score: `addScore` + +**Command:** `addScore`
    +**More information on usage:** Adding an Exam Score + +1. Adding a score to a person while all persons are displayed. + + * **Prerequisites:** + * Ensure at least one exam is added using the `addExam` command. For this example, we shall add a new exam with name `test exam` and maximum score `100`. + * Ensure an exam is selected using the `selectExam` command. For this example, we shall select `test exam` from above. + * Ensure all persons are displayed using the `list` command. +

    + * **Test case:** `addScore 1 s|100`
    + **Expected:** A score of `100` is added to the first person in the list of displayed persons. The score and the name of the corresponding person will be shown in the status message. +

    + * **Test case:** `addScore 2 s|50.25`
    + **Expected:** A score of `50.25` is added to the second person in the list of displayed persons.The score and the name of the corresponding person will be shown in the status message. +

    + * **Test case (invalid index input):** `addScore 0 s|100`
    + **Expected:** No score is added to any persons. Error details are shown in the status message. +

    + * **Test case (no index input):** `addScore s|100`
    + **Expected:** No score is added to any persons. Error details are shown in the status message. +

    + * **Test case (no score input):** `addScore 3 s|`
    + **Expected:** No score is added to any persons. Error details are shown in the status message. +

    + * **Test case (score larger than maximum score is input):** `addScore 3 s|101`
    + **Expected:** No score is added to any persons. Error details are shown in the status message. +

    + * **Test case (negative score input):** `addScore 3 s|-50`
    + **Expected:** No score is added to any persons. Error details are shown in the status message. +

    + * **Test case (person already contains a score):** `addScore 1 s|50.25`
    + **Expected:** No score is added to any persons. Error details are shown in the status message. +

    + * **Other incorrect `addScore` commands to try:** `addScore`, `addScore INDEX s|100` (where `INDEX` is larger than the list size), `addScore 3 s|SCORE` (where `SCORE` is non-numeric, is less than 0, more than the maximum score of the selected exams, and/or has more than 2 digits in its fractional part)
    + **Expected:** No score is added to any persons. Error details are shown in the status message. +

    + +
    + +2. Adding a score to a person while person list is filtered. + + * **Prerequisites:** + * Ensure at least one exam is added using the `addExam` command. For this example, we shall add a new exam with name `test exam` and maximum score `100`. + * Ensure an exam is selected using the `selectExam` command. For this example, we shall select `test exam` from above. + * Filter the person list using the `find` command. +

    + * **Test case:** Similar to previous.
    + **Expected:** Similar to previous. + +
    + +
    + +
    + +#### Editing a Person's Exam Score: `editScore` + +**Command:** `editScore`
    +**More information on usage:** Editing an Exam Score + +1. Editing a score of a person while all persons are displayed. + + * **Prerequisites:** + * Ensure at least one exam is added using the `addExam` command. For this example, we shall add a new exam with name `test exam` and maximum score `100`. + * Ensure an exam is selected using the `selectExam` command. For this example, we shall select `test exam` from above. + * Ensure all persons are displayed using the `list` command. + * Ensure that one person in the list has a score for the selected exam using the `addScore` command. For this example, we shall add a score of `100` to the first person in the list. +

    + * **Test case:** `editScore 1 s|90`
    + **Expected:** The score of `100` is edited to `90` for the first person in the list of displayed persons. The score and the details of the corresponding person will be shown in the status message. +

    + * **Test case (invalid index input):** `editScore 0 s|90`
    + **Expected:** No person's score is edited. Error details are shown in the status message. +

    + * **Test case (no index input):** `editScore s|90`
    + **Expected:** No person's score is edited. Error details are shown in the status message. +

    + * **Test case (no score input):** `editScore 1 s|`
    + **Expected:** No person's score is edited. Error details are shown in the status message. +

    + * **Test case (score larger than maximum score is input):** `editScore 1 s|101`
    + **Expected:** No score is added to any persons. Error details are shown in the status message. +

    + * **Test case (person does not contain any score):** `editScore 2 s|90`
    + **Expected:** No score is added to any persons. Error details are shown in the status message. +

    + * **Other incorrect `editScore` commands to try:** `editScore`, `editScore INDEX s|90` (where `INDEX` is larger than the list size), `editScore 1 s|SCORE` (where `SCORE` is non-numeric, is less than 0, more than the maximum score of the selected exam, and/or has more than 2 digits in its fractional part)
    + **Expected:** No score is added to any persons. Error details are shown in the status message. +

    + +2. Editing a score of a person while person list is filtered. + + * **Prerequisites:** + * Ensure at least one exam is added using the `addExam` command. For this example, we shall add a new exam with name `test exam` and maximum score `100`. + * Ensure an exam is selected using the `selectExam` command. For this example, we shall select `test exam` from above. + * Filter the person list using the `find` command. + * Ensure that one person in the list has a score for the selected exam using the `addScore` command. For this example, we shall add a score of `100` to the first person in the list. +

    + * **Test case:** Similar to previous.
    + **Expected:** Similar to previous. + +
    + +
    + +#### Deleting a Person's Exam Score: `deleteScore` + +**Command:** `deleteScore`
    +**More information on usage:** Deleting an Exam Score + +1. Deleting a score of a person while all persons are displayed. + + * **Prerequisites:** + * Ensure at least one exam is added using the `addExam` command. For this example, we shall add a new exam with name `test exam` and maximum score `100`. + * Ensure an exam is selected using the `selectExam` command. For this example, we shall select `test exam` from above. + * Ensure all persons are displayed using the `list` command. + * Ensure that one person in the list has a score for the selected exam using the `addScore` command. For this example, we shall add a score of `100` to the first person in the list. +

    + * **Test case:** `deleteScore`
    + **Expected:** The score of `100` is deleted from the first person in the list of displayed persons. The details of the corresponding person will be shown in the status message. +

    + * **Test case (invalid index input):** `deleteScore 0`
    + **Expected:** No person's score is deleted. Error details are shown in the status message. +

    + * **Test case (person does not contain any score):** `deleteScore 2`
    + **Expected:** No person's score is deleted. Error details are shown in the status message. +

    + * **Other incorrect `deleteScore` commands to try:** `deleteScore`, `deleteScore INDEX` (where `INDEX` is larger than the list size)
    + **Expected:** No person's score is deleted. Error details are shown in the status message. +

    + +2. Deleting a score of a person while person list is filtered. + + * **Prerequisites:** + * Ensure at least one exam is added using the `addExam` command. For this example, we shall add a new exam with name `test exam` and maximum score `100`. + * Ensure an exam is selected using the `selectExam` command. For this example, we shall select `test exam` from above. + * Filter the person list using the `find` command. + * Ensure that one person in the list has a score for the selected exam using the `addScore` command. For this example, we shall add a score of `100` to the first person in the list. +

    + * **Test case:** Similar to previous.
    + **Expected:** Similar to previous. + +
    + +
    + +
    + +#### Mean and Median of Exam Scores + +**More information on usage:** Mean and Median of Exam Scores + +1. Mean and median of exam scores while all persons are displayed. + + * **Prerequisites:** + * Ensure at least one exam is added using the `addExam` command. For this example, we shall add a new exam with name `test exam` and maximum score `100`. + * Ensure an exam is selected using the `selectExam` command. For this example, we shall select `test exam` from above. + * Ensure all persons are displayed using the `list` command. +

    + * **Initially, no scores added to any persons in the list**
    + **Expected:** "No scores available" is displayed at the bottom, right corner of the GUI. +

    + * **Use `addScore` to add a score of `50` to the first person in the list**
    + **Expected:** A mean score of `50` and a median score of `50` is displayed at the bottom, right corner of the GUI. +

    + * **Use `addScore` to add a score of `25` to the second person in the list and a score of `27.7` to the third person in the list**
    + **Expected:** the calculated mean value of the three scores (rounded to two decimal places), `50`, `25` and `27.7`, and the median of the three scores, are displayed at the bottom, right corner of the GUI. + +
    + +-------------------------------------------------------------------------------------------------------------------- + +
    + +
    + +### Appendix G: Effort + +This section aims to showcase the effort put into Avengers Assemble by our team. +We will highlight the difficulty level, challenges faced, and effort required in this project. + +
    + +#### Difficulty Level + +On top of the `Person` entity originally implemented by AB3, Avengers Assemble also incorporates an additional entity of +`Exam`, with `Score` serving as a connection between the two entities. +With this additional entity added, considerations had to be made regarding the implementation of +different features, interactions between each entity, and the management and storage of these +entities. The consideration of these factors turned out to be more challenging than initially anticipated. -
    +Moreover, in addition to enhancing the original features of AB3 to cater to our target users, Avengers Assemble also introduces +many new commands to improve the usability of our application, as well as to handle the diverse behaviors and interactions +of `Person` and `Exam`. This required a significant amount of effort to ensure that the new features were +implemented correctly and seamlessly integrated with the existing features. -### Launch and shutdown +Compared to the individual project, the group project was lower in intensity for each of us in terms of lines of code, +but the coordination and communication required to ensure that the features were implemented correctly and +seamlessly integrated with the existing features added a layer of complexity to the project. -1. Initial launch +
    - 1. Download the jar file and copy into an empty folder +#### Effort Required - 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. +##### Enhancements to Existing Features -1. Saving window preferences +**Addition of New Fields to Persons**
    +New fields such as recitation, studio, matriculation number, was added to persons to align with the context of our application. - 1. Resize the window to an optimum size. Move the window to a different location. Close the window. +**Find**
    +Our team improved on the existing `find` command of AB3 to allow for more flexibility. With the new improvements, users +can now find not only based on the name field of persons, but also specify their search based on other fields such as +`email` and `recitation`. With the addition of the exam score features, we also adapted our `find` command to allow users +to filter out persons less than or more than a specified score, revamping the way `find` is used and handled. - 1. Re-launch the app by double-clicking the jar file.
    - Expected: The most recent window size and location is retained. +**Automatic Tagging of Persons**
    +In the context of our application, it is mainly used to store students', instructors' and teaching assistants' contacts. +Hence, on top of the original behavior of the tag feature, we adapted it to automatically tag contacts with a +matriculation number as students. -1. _{ more test cases …​ }_ +**User Interface**
    +Enhancements were made to the user interface to improve the user experience. The structure of the user interface was +modified to accommodate the new features, and the theme of the application was changed to follow the theme of the +course that we were developing the application for. Furthermore, the logic for the updating of user interface was also +modified to a more developer-friendly approach which would allow developers to understand and modify the user interface +more easily. -### Deleting a person +
    -1. Deleting a person while all persons are being shown +##### New Features - 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. +**Copy**
    +Our team introduced a new copy command which allows for users to copy the email addresses of the currently displayed persons. +This is to cater to the context of our application, assisting head tutors with the task of making mass announcements. - 1. Test case: `delete 1`
    - Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. +**Import and Export**
    +To facilitate the handling and managing of large amounts of information, our group introduced the import and export feature to +allow for flexible data movement externally and internally. These features required extensive effort due to how bug prone +they were. This is elaborated upon in the challenges section below. - 1. Test case: `delete 0`
    - Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. +**Exams and Exams Scores**
    +The implementation of the exam and exam score features was the most significant addition to our application, requiring adjustments to existing features and the +introduction of many new commands to handle and manage the addition of exams and exam features. This feature was the most +complex and required the most effort to implement, as it involved the introduction of a new entity, `Exam`, and the management +of scores for each person for each exam. This is further elaborated upon in the challenges section below. - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
    - Expected: Similar to previous. +
    -1. _{ more test cases …​ }_ +#### Challenges Faced -### Saving data +##### Understanding the Existing Codebase +One of the challenges we faced was understanding the existing codebase of AB3. We had to familiarize ourselves with the +structure of the codebase, the interactions between the different classes, and the existing features of AB3. This required +us to spend time reading through the code, discussing the existing features, and identifying potential areas where +conflicts might arise when adding new features. We also had to consider how to integrate our new features using the existing +structure in AB3, and how to ensure that the new features did not conflict with the existing features. -1. Dealing with missing/corrupted data files +##### Considerations for New Entity `Exam` and its Interactions with `Person` +Our team wanted to implement a feature that would allow users to manage and store exam scores for each person. +It was clear from the start that this would require the introduction of a new entity, `Exam`, to store information about +each exam. However, we found that there was a challenge in determining how to connect the `Person` entity with the `Exam` entity, and how to +manage and store the scores for each person for each exam. This required careful consideration and planning to ensure that +the interactions between the two entities were seamless and intuitive for users. We also had to consider how to handle the +storage of these entities and how to manage the data effectively. - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ +**Limited User Interface Space for Score Interaction**
    -1. _{ more test cases …​ }_ +One of the greatest challenges was designing a user-friendly interface for score +interaction within the limited screen space. We had to devise intuitive methods for users to view, input and manage the scores +of various exams, without overwhelming the interface. This proved to be a greater challenge than initially anticipated, +as we had to consider trade-offs between functionality and user experience. Lowering the complexity of the interface +would result in an interface that is easier to read, but might not provide all the necessary information at a glance and +require more user interactions to access the information. On the other hand, a more complex interface would provide more +information at a glance, but might overwhelm users with too much information. Striking a balance between these two +trade-offs was a challenge that required extended discussions and iterations before arriving at a solution that we were +satisfied with: the selection system for exams. + +
    + +**Implementation of Exam and Exam Score Features**
    + +After coming to a consensus in regard to the user interface, implementation for exam features seemed straightforward. +However, it turned out to be a lot more complex to implement than initially anticipated. Our exam features consisted +of many sub-features which included the management of exams, the management of scores, the storage of scores in persons, +and the importing of scores. As we were working in a collaborative environment, we had to consider how to distribute +the workload in a manner that would prevent conflicts. +This required early discussions of the structure of the exam and score features, and how they would interact with the existing features of AB3. +We drafted up diagrams to visualize the interactions between each feature. This helped us to identify potential conflicts +early on and resolve them through distributing the workload effectively and meeting regularly to discuss progress and issues. + +**Data Management for Exams and Scores**
    + +Handling the data for exams and scores was another challenge that we faced. We had to consider how to store the data for +each exam, how to store the scores for each person for each exam, and how to manage the data effectively. +The storage for exams was relatively straightforward, as we could create an additional list in the `AddressBook` class to store +the exams. However, the storage for scores was more complex. We had to decide whether to store all the exam score data +in corresponding `Exam` objects, or store each persons' exam scores in their corresponding `Person` objects. + +There was once again another trade-off to consider: storing all exam score data in the `Exam` objects would make it +easier to implement exam operations, but would require more complex interactions between the score and person objects. +On the other hand, storing each person's score in their corresponding `Person` object would make it easier to implement +operations on persons, but would require more complex interactions for exam management. We had to consider the pros and cons +of each approach, before deciding on the latter approach, as we concluded that our application was more person-centric. + +##### Bug Fixing and Testing +A significant challenge we faced was the identification and resolution of bugs. Unit tests for our own features were +relatively straightforward to implement, but we found that identifying edge cases proved to be tricky. Certain features +were also a lot more bug prone than others, such as the import features, which required extensive testing to ensure that +all potential errors were caught and proper error messages were displayed. We also had to ensure that the application +handled these errors gracefully and did not crash when these errors occurred. + +
    + +#### Achievements +Overall, our group successfully implemented the planned features while addressing bugs and managing potential feature flaws. +Despite initial hesitations about implementing significant new features like exams and exam scores, we overcame the challenge and achieved our goals. + +
    diff --git a/docs/Documentation.md b/docs/Documentation.md index 3e68ea364e7..082e652d947 100644 --- a/docs/Documentation.md +++ b/docs/Documentation.md @@ -1,29 +1,21 @@ --- -layout: page -title: Documentation guide + layout: default.md + title: "Documentation guide" + pageNav: 3 --- -**Setting up and maintaining the project website:** - -* We use [**Jekyll**](https://jekyllrb.com/) to manage documentation. -* The `docs/` folder is used for documentation. -* To learn how set it up and maintain the project website, follow the guide [_[se-edu/guides] **Using Jekyll for project documentation**_](https://se-education.org/guides/tutorials/jekyll.html). -* Note these points when adapting the documentation to a different project/product: - * The 'Site-wide settings' section of the page linked above has information on how to update site-wide elements such as the top navigation bar. - * :bulb: In addition to updating content files, you might have to update the config files `docs\_config.yml` and `docs\_sass\minima\_base.scss` (which contains a reference to `AB-3` that comes into play when converting documentation pages to PDF format). -* If you are using Intellij for editing documentation files, you can consider enabling 'soft wrapping' for `*.md` files, as explained in [_[se-edu/guides] **Intellij IDEA: Useful settings**_](https://se-education.org/guides/tutorials/intellijUsefulSettings.html#enabling-soft-wrapping) +# Documentation Guide +* We use [**MarkBind**](https://markbind.org/) to manage documentation. +* The `docs/` folder contains the source files for the documentation website. +* To learn how set it up and maintain the project website, follow the guide [[se-edu/guides] Working with Forked MarkBind sites](https://se-education.org/guides/tutorials/markbind-forked-sites.html) for project documentation. **Style guidance:** * Follow the [**_Google developer documentation style guide_**](https://developers.google.com/style). +* Also relevant is the [_se-edu/guides **Markdown coding standard**_](https://se-education.org/guides/conventions/markdown.html). -* Also relevant is the [_[se-edu/guides] **Markdown coding standard**_](https://se-education.org/guides/conventions/markdown.html) - -**Diagrams:** - -* See the [_[se-edu/guides] **Using PlantUML**_](https://se-education.org/guides/tutorials/plantUml.html) -**Converting a document to the PDF format:** +**Converting to PDF** -* See the guide [_[se-edu/guides] **Saving web documents as PDF files**_](https://se-education.org/guides/tutorials/savingPdf.html) +* See the guide [_se-edu/guides **Saving web documents as PDF files**_](https://se-education.org/guides/tutorials/savingPdf.html). diff --git a/docs/Gemfile b/docs/Gemfile deleted file mode 100644 index c8385d85874..00000000000 --- a/docs/Gemfile +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -source "https://rubygems.org" - -git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } - -gem 'jekyll' -gem 'github-pages', group: :jekyll_plugins -gem 'wdm', '~> 0.1.0' if Gem.win_platform? -gem 'webrick' diff --git a/docs/Logging.md b/docs/Logging.md index 5e4fb9bc217..589644ad5c6 100644 --- a/docs/Logging.md +++ b/docs/Logging.md @@ -1,8 +1,10 @@ --- -layout: page -title: Logging guide + layout: default.md + title: "Logging guide" --- +# Logging guide + * We are using `java.util.logging` package for logging. * The `LogsCenter` class is used to manage the logging levels and logging destinations. * The `Logger` for a class can be obtained using `LogsCenter.getLogger(Class)` which will log messages according to the specified logging level. diff --git a/docs/SettingUp.md b/docs/SettingUp.md index 275445bd551..03df0295bd2 100644 --- a/docs/SettingUp.md +++ b/docs/SettingUp.md @@ -1,27 +1,32 @@ --- -layout: page -title: Setting up and getting started + layout: default.md + title: "Setting up and getting started" + pageNav: 3 --- -* Table of Contents -{:toc} +# Setting up and getting started + + -------------------------------------------------------------------------------------------------------------------- ## Setting up the project in your computer -
    :exclamation: **Caution:** + +**Caution:** Follow the steps in the following guide precisely. Things will not work out if you deviate in some steps. -
    + First, **fork** this repo, and **clone** the fork into your computer. If you plan to use Intellij IDEA (highly recommended): 1. **Configure the JDK**: Follow the guide [_[se-edu/guides] IDEA: Configuring the JDK_](https://se-education.org/guides/tutorials/intellijJdk.html) to to ensure Intellij is configured to use **JDK 11**. -1. **Import the project as a Gradle project**: Follow the guide [_[se-edu/guides] IDEA: Importing a Gradle project_](https://se-education.org/guides/tutorials/intellijImportGradleProject.html) to import the project into IDEA.
    - :exclamation: Note: Importing a Gradle project is slightly different from importing a normal Java project. +1. **Import the project as a Gradle project**: Follow the guide [_[se-edu/guides] IDEA: Importing a Gradle project_](https://se-education.org/guides/tutorials/intellijImportGradleProject.html) to import the project into IDEA. + + Note: Importing a Gradle project is slightly different from importing a normal Java project. + 1. **Verify the setup**: 1. Run the `seedu.address.Main` and try a few commands. 1. [Run the tests](Testing.md) to ensure they all pass. @@ -34,10 +39,11 @@ If you plan to use Intellij IDEA (highly recommended): If using IDEA, follow the guide [_[se-edu/guides] IDEA: Configuring the code style_](https://se-education.org/guides/tutorials/intellijCodeStyle.html) to set up IDEA's coding style to match ours. -
    :bulb: **Tip:** + + **Tip:** Optionally, you can follow the guide [_[se-edu/guides] Using Checkstyle_](https://se-education.org/guides/tutorials/checkstyle.html) to find how to use the CheckStyle within IDEA e.g., to report problems _as_ you write code. -
    + 1. **Set up CI** diff --git a/docs/Testing.md b/docs/Testing.md index 8a99e82438a..78ddc57e670 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -1,12 +1,15 @@ --- -layout: page -title: Testing guide + layout: default.md + title: "Testing guide" + pageNav: 3 --- -* Table of Contents -{:toc} +# Testing guide --------------------------------------------------------------------------------------------------------------------- + + + + ## Running tests @@ -19,8 +22,10 @@ There are two ways to run tests. * **Method 2: Using Gradle** * Open a console and run the command `gradlew clean test` (Mac/Linux: `./gradlew clean test`) -
    :link: **Link**: Read [this Gradle Tutorial from the se-edu/guides](https://se-education.org/guides/tutorials/gradle.html) to learn more about using Gradle. -
    + + +**Link**: Read [this Gradle Tutorial from the se-edu/guides](https://se-education.org/guides/tutorials/gradle.html) to learn more about using Gradle. + -------------------------------------------------------------------------------------------------------------------- diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 7abd1984218..91a9bcf6430 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,198 +1,1217 @@ --- -layout: page -title: User Guide + layout: default.md + title: "User Guide" --- -AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized for use via a Command Line Interface** (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, AB3 can get your contact management tasks done faster than traditional GUI apps. +# Avengers Assemble User Guide -* Table of Contents -{:toc} +Avengers Assemble (AA) is a **desktop app for managing contacts**, meant for use with a Command Line Interface (CLI) +while still having the benefits of a Graphical User Interface (GUI). + +The application is designed for **Head Tutors** of the NUS CS1101S Programming Methodology course, who intend to simplify their administrative tasks relating to contact management between students, other teaching assistants, and course instructors. + +This user guide provides a comprehensive overview of the Avengers Assemble's features and functionalities, and aims to guide you through its setup and usage. We will walk you through each feature in a structured manner: + +1. Installation, +2. Basic commands like adding and editing, and +3. Advanced commands like filtering and exporting of data. + +By following this guide, you will be able to gain a thorough understanding of Avengers Assemble and maximize its potential to streamline your administrative tasks. -------------------------------------------------------------------------------------------------------------------- -## Quick start + +## Table of Contents + +Click below to navigate the user guide: + + -1. Ensure you have Java `11` or above installed in your Computer. +
    -1. Download the latest `addressbook.jar` from [here](https://github.com/se-edu/addressbook-level3/releases). +-------------------------------------------------------------------------------------------------------------------- -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +
    -1. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar addressbook.jar` command to run the application.
    - A GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.
    - ![Ui](images/Ui.png) +## Quick Start -1. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
    - Some example commands you can try: +1. Ensure you have `Java 11` or above installed in your computer. - * `list` : Lists all contacts. +2. Download the latest `avengersassemble.jar` [here](https://github.com/AY2324S2-CS2103T-T10-1/tp/releases). - * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : Adds a contact named `John Doe` to the Address Book. +3. Copy the file to the folder you want to use as the _home folder_ for our application. - * `delete 3` : Deletes the 3rd contact shown in the current list. +4. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar avengersassemble.jar` command to run the application.
    + ```dtd + cd + java -jar avengersassemble.jar + ``` + You should see this when the app starts up. Note how it contains some sample data.
    - * `clear` : Deletes all contacts. +

    + image of avengers assemble's ui +

    - * `exit` : Exits the app. +5. Refer to the [features](#contact-management-features) below for details of each command. -1. Refer to the [Features](#features) below for details of each command. +
    -------------------------------------------------------------------------------------------------------------------- -## Features +
    + +
    + +## Outline of Application + +The image below shows the outline of the application, with the main sections highlighted: + +

    + outlined image of avengers assemble's ui + +

    + +The main sections are as follows: + +1. **Command Input Box**: + * This is where you can type your commands. + * Press `Enter` to execute the command. +2. **Results Box**: + * This area displays the results of the commands you have executed. + * It will also display any error messages if the command was not executed successfully. +3. **Persons Display Panel**: + * This area displays the list of persons that you have imported or saved into our application. + * Each persons is displayed with an index number, name, email, phone number, address, and tags. + * Each person is also displayed with their matriculation number, studio group, and reflection group if they have been added. + * If an exam is selected, the person's score for that exam will also be displayed if it exists. +4. **Exams Display Panel**: + * This area displays the list of exams that you have added into our application. + * Each exam is displayed with an index number, name, and maximum score. + * Selected exams will be highlighted in the list. + +-------------------------------------------------------------------------------------------------------------------- + +
    + +
    + +## Contact Management Features + +These features are designed to help you manage your contacts effectively and centralizes all contact information in one place. +You can import contacts from CSV files generated by external sources such as canvas and edurec, manage the contact details of each person in your contact list, and export relevant data as CSV files. + +### Legend +These boxes might offer you additional information of different types: + +>**Good to know:** +>Provides you with supporting information. -
    + -**:information_source: Notes about the command format:**
    +**Important:** +Provides you with more important information that you should know. +
    -* Words in `UPPER_CASE` are the parameters to be supplied by the user.
    - e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. + -* Items in square brackets are optional.
    - e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`. +**Tip:** +Provides you with tips to use our app more effectively. -* Items with `…`​ after them can be used multiple times including zero times.
    - e.g. `[t/TAG]…​` can be used as ` ` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. +
    + + + +**Caution:** +Provides you with warnings about potential issues you might face. + + + +
    + +
    + +### Getting Help : `help` + +Copies the link of our user guide to your clipboard. Paste it into a browser to view it. + +**Format:** `help` + +You will see this message when you have successfully copied the link. + +

    + image of help window +

    + +
    + +Before we proceed with the commands, here are some important points to note on their formatting. These points will also be repeated in the [command parameter summary](#command-parameter-summary) for you to refer to easily at any point in time. + + + +**Important:**
    + +* Words in `UPPER_CASE` are the parameters to be supplied by you. + > e.g. in `add n|NAME`, `NAME` is a parameter which can be used as `add n|John Doe`. + +* Prefixes encased with '[ ]' are optional. + > e.g. `n|NAME [t|TAG]` can be used as `n|John Doe t|friend` or as `n|John Doe`. + +* Prefixes with '…' after them can be used multiple times. + > e.g. `[t|TAG]…​` can be used as ` ` (i.e. 0 times), `t|friend` (i.e 1 time), `t|friend t|family` etc. * Parameters can be in any order.
    - e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. + > e.g. if the command specifies `n|NAME p|PHONE_NUMBER`, `p|PHONE_NUMBER n|NAME` is also acceptable. + +* Extraneous parameters for commands that do not take in parameters (such as `help` , `list`, `exit`, `copy`, `export` and `clear`) will be ignored.
    + > e.g. if the command specifies `help 123`, it will be interpreted as `help`. + +
    -* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
    - e.g. if the command specifies `help 123`, it will be interpreted as `help`. + +**Caution:**
    * If you are using a PDF version of this document, be careful when copying and pasting commands that span multiple lines as space characters surrounding line-breaks may be omitted when copied over to the application. -
    -### Viewing help : `help` + + +
    -Shows a message explaning how to access the help page. +
    -![help message](images/helpMessage.png) +### Clearing All Entries : `clear` -Format: `help` +Deletes **all** data from the application. This includes all contact information and examinations. +**Format:** `clear` -### Adding a person: `add` + -Adds a person to the address book. +**Caution:**
    +Using clear will delete all data in an irreversible manner. Be sure to back up your relevant data by using the `export` command before using this command. -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +
    + +
    + +
    + +
    + +### Importing Persons from a CSV File : `import` + +Imports all persons and their details from a CSV file of your specification. + +**Format:** `import i|FILEPATH` + + + +**Important:**
    + +* The file path should be **absolute**. +* This command will only import persons' particulars. To import exam scores, take a look at [`importExamScores`](#importing-exam-scores-from-a-csv-file--importexamscores). +* All compulsory parameters **must** be present in the column headings (i.e. the first row) of the CSV file you are importing i.e. `name`, `email`, `phone` and `address`. +* Only values under accepted column headings are read i.e. `name`, `email`, `phone`, `address`, `tags`, `matric`, `reflection` and `studio` +* The number of headers must match the number of cells for each row. +* Invalid rows will be skipped and will not be imported! + +
    + +**Example:** `import i|/Users/johansoo/Desktop/AvengersAssemble/exam_data.csv`
    +imports the data from the CSV file located at `/Users/johansoo/Desktop/AvengersAssemble/exam_data.csv` + +You will see this message once you successfully imported the data, with the app showing the details of the imported persons: + +

    + image of successful import +

    + +For more details on the input parameter, [click here](#command-parameter-summary). + +
    + +
    + +
    + +### Adding a Person : `add` + +Adds a person to your contact list. The person's details are now stored in the application. + +**Format:** `add n|NAME p|PHONE_NUMBER e|EMAIL a|ADDRESS [t|TAG]… [m|MATRICULATION_NUMBER] [s|STUDIO] [r|REFLECTION]​` + + + +**Important:** + +Each person should have a unique email address. Avengers Assemble does not allow for duplicate email addresses to be added. + + + + + +**Tip:** -
    :bulb: **Tip:** A person can have any number of tags (including 0) -
    -Examples: -* `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` -* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal` +
    + +> **Note:** +> For your convenience, a `student` tag will automatically be added to a contact if they are added with a matriculation number. +> You are free to edit or remove the tags after the person is added with the [`edit`](#editing-a-person--edit) command. +> For example, a student TA can be added with the `student` tag, and then the `TA` tag can be added to indicate that they are a TA. + +**Example:** +`add n|John Doe p|98765432 e|johnd@example.com a|John street, block 123, #01-01 m|A1234567Z s|S1 r|R2`
    +adds a contact John Doe with the respective phone number, email and physical addresses, matriculation number, studio group and reflection group. + +You will see this message once you have successfully added a person, indicating their details: + +

    + image showing the successful addition of persons +

    -### Listing all persons : `list` +For more details on each parameter, [click here](#command-parameter-summary). -Shows a list of all persons in the address book. +
    -Format: `list` +
    -### Editing a person : `edit` +
    -Edits an existing person in the address book. +### Editing a Person : `edit` -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +Edits the details of an existing person in your contact list. -* Edits the person at the specified `INDEX`. The index refers to the index number shown in the displayed person list. The index **must be a positive integer** 1, 2, 3, …​ +**Format:** `edit INDEX [n|NAME] [p|PHONE] [e|EMAIL] [a|ADDRESS] [t|TAG]… [m|MATRICULATION_NUMBER] [s|STUDIO] [r|REFLECTION]​` + + + +**Information:**
    +* The person at the specified `INDEX` will be edited. The index **must be a positive integer** (1, 2, 3, …)​. * At least one of the optional fields must be provided. -* Existing values will be updated to the input values. -* When editing tags, the existing tags of the person will be removed i.e adding of tags is not cumulative. -* You can remove all the person’s tags by typing `t/` without - specifying any tags after it. +* Existing values will be updated to the new values. +* Editing tags will replace all existing tags i.e. adding of tags is **not cumulative**. +* You can remove optional fields by typing `t|`, `m|`, `r|` or `s|` respectively without any values. -Examples: -* `edit 1 p/91234567 e/johndoe@example.com` Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively. -* `edit 2 n/Betsy Crower t/` Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. +
    -### Locating persons by name: `find` + -Finds persons whose names contain any of the given keywords. +**Important:** -Format: `find KEYWORD [MORE_KEYWORDS]` +Updating a matriculation number, studio, or reflection field will not automatically update the tags of the person. You will need to manually update the tags if necessary. -* The search is case-insensitive. e.g `hans` will match `Hans` -* The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans` -* Only the name is searched. -* Only full words will be matched e.g. `Han` will not match `Hans` -* Persons matching at least one keyword will be returned (i.e. `OR` search). - e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` + -Examples: -* `find John` returns `john` and `John Doe` -* `find alex david` returns `Alex Yeoh`, `David Li`
    - ![result for 'find alex david'](images/findAlexDavidResult.png) +**Examples:** -### Deleting a person : `delete` +1. `edit 2 n|Betsy Crower t|`: + * Edits the name of the second person to be `Betsy Crower` and clears all existing tags. -Deletes the specified person from the address book. +
    -Format: `delete INDEX` +2. `edit 1 p|91234567 e|johndoe@example.com`: + * Edits the phone number and email address of the first person to be `91234567` and `johndoe@example.com` respectively. -* Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. -* The index **must be a positive integer** 1, 2, 3, …​ +You will see this message once you have successfully edited a person, indicating their updated details: -Examples: -* `list` followed by `delete 2` deletes the 2nd person in the address book. -* `find Betsy` followed by `delete 1` deletes the 1st person in the results of the `find` command. +

    + image showing a successful edit +

    -### Clearing all entries : `clear` +For more details on each parameter, [click here](#command-parameter-summary). -Clears all entries from the address book. +
    -Format: `clear` +
    -### Exiting the program : `exit` +
    -Exits the program. +### Deleting a Person : `delete` -Format: `exit` +Deletes the specified person from your contact list. -### Saving the data +**Format:** `delete INDEX` -AddressBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. + -### Editing the data file +**Important:**
    -AddressBook data are saved automatically as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. +The person at the specified `INDEX` will be deleted. The index **must be a positive integer** (1, 2, 3, …)​ -
    :exclamation: **Caution:** -If your changes to the data file makes its format invalid, AddressBook will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it.
    -Furthermore, certain edits can cause the AddressBook to behave in unexpected ways (e.g., if a value entered is outside of the acceptable range). Therefore, edit the data file only if you are confident that you can update it correctly. -
    +
    + +**Examples**: + +1. `find n|Betsy` followed by `delete 1` deletes the first person in the results of the `find` command. +2. `list` followed by `delete 1` deletes the first person stored in the app. + +You will see this message once you successfully delete a person from your list, indicating the details of the deleted person: + +

    + image showing a successful delete +

    + +
    + +
    + +
    + +### Deleting Filtered Persons : `deleteShown` + +Deletes the current filtered list of persons. Requires a [`find`](#filtering-persons--find) command to be run first. + +**Format:** `deleteShown` + + + +**Information:** + +* Deletes all persons in the current filtered list of persons. +* The list of persons is filtered using the most recent [`find`](#filtering-persons--find) command. +* The remaining list of persons is shown after the [`list`](#listing-all-persons--list) command is executed. + + + +> **Note:** The application ignores any extraneous parameters as we assume they are typos. + +You will see this message once you have successfully deleted the shown persons from your list. + +

    + image of a successful deleteshown +

    + +
    + +
    + +
    + +### Listing All Persons : `list` + +Displays all the persons in your contact list. + +**Format:** `list` + +> **Note:** The application ignores any extraneous parameters as we assume they are typos. + +You will see this message once you have successfully listed all contacts, with the app showing all existing persons in the contact list: + +

    + image of a successful list +

    + +
    + +
    + +
    + +### Filtering Persons : `find` + +Filters your contacts based on specific criteria you set. + +**Format:** `find PREFIX|KEYWORD` + + + +**Information:**
    +* Use this command to search for persons using a specific aspect of their details, as specified by the prefix. +* The search will return any result that contains the keyword you have specified or matches the condition provided by the user. + > e.g. `find e|john` will find any person that contains `john` in their email.
    + > e.g. `find lt|50` will find any person who scored lower than 50 marks for the selected exam.
    + > e.g. `find mt|80.55` will find any person who scored more than 80.55 marks for the selected exam. +* The search is **case-insensitive**. +* Only one prefix can be used at a time. + +
    + + + +**Important: An exam must be selected for this command to work with the `lt|` and `mt|` prefixes! +You can use the [`selectExam`](#selecting-an-exam--selectexam) command to do so.** + + + +**Example:** `find n|John`
    +returns `john` and `John Doe` if they exist in your contact book. + +You will see this message once you have successfully found a person, with the app showing all persons that match your search criteria: + +

    + image of successful find +

    + +For more details on each parameter, [click here](#command-parameter-summary). + +
    + +
    + +
    + +### Copying Emails : `copy` + +Copies the emails of currently displayed persons into your clipboard. + +**Format:** `copy` + + + +**Information:**
    +* The emails copied into your clipboard will have semicolons separating them. +* Semicolons are used as delimiters to separate the emails when pasted into any email client. + +
    + + + +**Tip:**
    + +* Use [`list`](#listing-all-persons--list) or [`find`](#filtering-persons--find) to get the list of people you would like to email. +* The emails are copied into your clipboard such that you may easily broadcast emails to specific groups of people. + +
    + +> **Note:** The application ignores any extraneous parameters as we assume they are typos. + +You will see this message once you have successfully copied the emails of the persons shown to you, indicating that they have been copied to the clipboard: + +

    + image of successful copy +

    + +
    + +
    + +
    + +### Exporting Data to a CSV File : `export` + +Exports currently displayed persons and their details to a CSV file of your specification. + +**Format:** `export` + +> By default, the file will be stored in `addressbookdata/avengersassemble.csv`. + + + +**Tip:** + +You can specify the groups of contacts you want to export using the [`find`](#filtering-persons--find) or [`list`](#listing-all-persons--list) commands before you use this command. + + + + + +**Caution:** + +When performing an export, the current information will overwrite any existing CSV files with the same name. +If you want to preserve the exported data, you should rename it or save it in a separate location. + + + +You will see this message once you have successfully exported the data: + +

    + image of successful export +

    + +
    + +
    + +### Exiting the Program : `exit` + +Exits the program. The app will close automatically. + +**Format:** `exit` + +> **Note:** The application ignores any extraneous parameters as we assume they are typos. + +
    + +-------------------------------------------------------------------------------------------------------------------- + +
    + +
    + +## Exam Management Features + +These features are designed to help you manage exam scores for the persons in your contact list and consolidate all assessment data from canvas, edurec, and source academy. +You can import exam scores from CSV files generated by these platforms, manage the exam scores of each person in your contact list, and gather statistics on student performance. + +
    -### Archiving data files `[coming in v2.0]` +### Importing Exam Scores from a CSV File : `importExamScores` -_Details coming soon ..._ +Imports all exam results from a CSV file. + +**Format:** `importExamScores i|FILEPATH` + + + +**Information:**
    + +* The file path should be **absolute**. +* The `email` header **must** be the first column header. +* Exam names starting with `Exam:` e.g. if your exam is named `Midterms`, the column heading containing the scores for `Midterms` should be named `Exam:Midterms`. +* This command will only import scores if both the person and exam exists currently in Avengers Assemble. Use [`add`](#add) and [`addExam`](#addexam) to add persons and exams respectively. +* Duplicate exam headers in the CSV file will be ignored, with only the first occurrence being used. +* Each row following the header row corresponds to an individual's exam scores, + with the first column containing the person's email address and the subsequent columns + containing the scores for the exams as specified by the headers. +* Exam score values should be entered as decimal numbers within the valid score range (0 - maximum score), + rounded to a maximum of 2 decimal places. + +
    + + + +**Caution:**
    + +* Erroneous entries will be ignored, and the application will continue to import the rest of the data. +* This command will only import persons' exam scores. To import persons' particulars, take a look at [`import`](#importing-persons-from-a-csv-file--import) + +
    + +**Example:** `importExamScores i|/Users/johansoo/Desktop/AvengersAssemble/exam_data.csv`
    +imports exam results from the CSV file located at `/Users/johansoo/Desktop/AvengersAssemble/exam_data.csv`. + +You will see this message once you have successfully imported the exam results: + +

    + image of successful importExamScores +

    + +For more details on the parameter, [click here](#command-parameter-summary). + +
    + +
    + +### Adding an Exam : `addExam` + +Adds an exam into your exam list. + +**Format:** `addExam n|NAME s|MAX_SCORE` + + + +**Important:** + +Each exam should have a unique name. Avengers Assemble does not allow for exams with duplicate names to be added. + + + +**Example:** `addExam n|Midterm s|100`
    +Adds an exam with the name "Midterm" and a max score of "100" into your exam list. + +You will see this message once you successfully add an exam, including the details of the exam: + +

    + image of successful addExam +

    + +For more details on each parameter, [click here](#command-parameter-summary). + +
    + +
    + +
    + +### Deleting an Exam : `deleteExam` + +Removes an exam from your exam list. + +**Format:** `deleteExam INDEX` + + + +**Information:** + +* Deletes the exam at the specified `INDEX`. +* When an exam is deleted, all corresponding records of scores associated with that exam will also be deleted. +* If the currently selected exam is deleted, it will be deselected. + + + +**Example:** `deleteExam 3`
    +Removes the third exam displayed in Avengers Assemble. + +You will see this message once you have successfully deleted an exam, including the details of the exam you are deleting: + +

    + image of successful deleteExam +

    + +
    + +
    + +
    + +### Selecting an Exam : `selectExam` + +Selects an exam in your exam list. + +**Format:** `selectExam INDEX` + + + +**Information:** + +* Selects the exam at the specified `INDEX`. +* On selection, the exam will become highlighted on the user interface. +* Selecting an exam will display all scores of persons associated with that exam. + + + +**Example:** `selectExam 1`
    +Selects the first exam displayed on the exam list. + +You will see this message once you have successfully selected an exam, including the details of the exam: + +

    + image of successful selectExam +

    + +
    + +
    + +
    + +### Deselecting an Exam : `deselectExam` + +Deselects your currently selected exam. + +**Format:** `deselectExam` + +You will see this message once you have successfully deselected an exam: + +

    + image of successful deselectExam +

    + +
    + +
    + +
    + +### Adding an Exam Score : `addScore` + +Adds an exam score to a person at the specified index. + +**Format:** `addScore INDEX s|SCORE` + + + +**Important: An exam must be selected for this command to work! You can use the [`selectExam`](#selectexam) command to do so.** + + + + + +**Information:** + +* Adds an exam score to the person at the specific `INDEX`. +* The exam score added will correspond to the currently selected exam. +* The exam score added **cannot** be greater than the max score of the currently selected exam. +* The exam score will be displayed on the user interface only when the corresponding exam is selected. + + + +**Example:** `addScore 1 s|42`
    +Adds a score of 42 to the person currently displayed at index 1. + +You will see this message once you successfully add a score, including the name of the person you added the score for: + +

    + image of successful addScore +

    + +For more details on the parameter, [click here](#command-parameter-summary). + +
    + +
    + +
    + +### Editing an Exam Score : `editScore` + +Edits a specified person's exam score. + +**Format:** `editScore INDEX s|SCORE` + + + +**Information:** + +* Edits the exam score of the person at the specific `INDEX`. +* A person **must** have an exam score for the currently selected exam for this command to work. +* The exam score edited corresponds to the currently selected exam. +* The exam score **cannot** be edited to be greater than the max score of the currently selected exam. + + + + + +**Important:** + +An exam must be selected for this command to work! You can use the [`selectExam`](#selecting-an-exam--selectexam) command to do so. + + + +**Example:** `editScore 1 s|25`
    +Edits the score of the person currently displayed at index 1 to 25. + +You will see this message once you successfully edit a score, including some details of the person you added the score for: + +

    + image of successful editScore +

    + +For more details on the parameter, [click here](#command-parameter-summary). + +
    + +
    + +
    + +### Deleting an Exam Score : `deleteScore` + +Deletes a specified person's exam score. + +**Format:** `deleteScore INDEX` + + + +**Information:** + +* Deletes the exam score of the person at the specific `INDEX`. +* A person **must** have an exam score for the currently selected exam for this command to work. +* The exam score deleted corresponds to the currently selected exam. + + + + + +**Important:** + +An exam must be selected for this command to work! You can use the [`selectExam`](#selecting-an-exam--selectexam) command to do so. + + + +**Example:** `deleteScore 1`
    +deletes the score of the person currently displayed at index 1. + +You will see this message once you have successfully deleted a score, including some details of the person you added the score for: + +

    + image of successful deleteScore +

    + +
    + +
    + +
    + +### Mean and Median of Exam Scores + +You can view the mean and median of the scores of the exam currently selected at the bottom right of the application window. + + + +**Information:** + +* When an exam is selected, the mean and median will automatically show up on the right of the footer of the application. +* The mean and median is calculated based on the currently filtered list of persons. +* If a person has no score for the selected exam, he is completely excluded from the calculation of mean and median. + + + +When an exam is selected, the statistics will show on the bottom right of the application: + +

    + image of application window indicating mean and median scores +

    + +
    -------------------------------------------------------------------------------------------------------------------- +
    + +
    + +## Additional Information + +### Saving the Data + +All data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. + +### Editing the Data File + +All data are saved automatically as a JSON file located at `[JAR file location]/data/avengersassemble.json` by default. You can update data directly by editing that data file if you are an advanced user. + + + +**Caution:** + +If your changes to the data file makes its format invalid, Avengers Assemble will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it.
    +Furthermore, certain edits can cause the Avengers Assemble application to behave in unexpected ways (e.g., if a value entered is outside the acceptable range). Therefore, edit the data file only if you are confident that you can update it correctly. + +
    + +
    + +-------------------------------------------------------------------------------------------------------------------- + +
    + ## FAQ -**Q**: How do I transfer my data to another Computer?
    -**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous AddressBook home folder. +**Q**: How do I transfer my data to another computer?
    +**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous AA home folder. + +
    -------------------------------------------------------------------------------------------------------------------- -## Known issues +
    + +## Known Issues + +### Using Multiple Screens + +If you move the application to a secondary screen, and later switch to using only the primary screen, the GUI will open off-screen. The remedy is to delete the `preferences.json` file created by the application before running the application again. + +### Importing on MacOS + +On MacOS computers, due to privacy settings, the application may encounter difficulties accessing and importing CSV files from various locations. +If this issue occurs, transfer the CSV file you want to import to the same folder where the application's JAR file is located, then try again. -1. **When using multiple screens**, if you move the application to a secondary screen, and later switch to using only the primary screen, the GUI will open off-screen. The remedy is to delete the `preferences.json` file created by the application before running the application again. +### Exporting Data on Initial Launch + +Since the `export` function relies on data stored in your computer's hard disk, there might be some issues exporting it during the first launch of the application. +If you encounter this problem, you can resolve it by executing any other command (such as `list`) and then attempting the export again. + +
    + +-------------------------------------------------------------------------------------------------------------------- + +
    + +
    + +## Command Summary + +Below is a summary of the commands available in Avengers Assemble. Some examples are included for your convenience. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ActionFormat, Examples
    Helphelp
    Clearclear
    Importimport i|FILEPATH

    • e.g. import i|C:/Users/alk/Downloads/avengersassemble.csv
    Addadd n|NAME p|PHONE_NUMBER e|EMAIL a|ADDRESS [t|TAG]… [m|MATRICULATION_NUMBER] [s|STUDIO] [r|REFLECTION]

    • e.g. add n|James Ho p|22224444 e|jamesho@example.com a|123, Clementi Rd, 1234665 t|friend t|colleague m|A1234567X
    Editedit INDEX [n|NAME] [p|PHONE_NUMBER] [e|EMAIL] [a|ADDRESS] [t|TAG]… [m|MATRICULATION_NUMBER] [s|STUDIO] [r|REFLECTION]

    • e.g. edit 2 n|James Lee e|jameslee@example.com m|A1234567X
    Deletedelete INDEX

    • e.g. delete 3
    Delete Shown PersonsdeleteShown
    Listlist
    Findfind PREFIX|KEYWORD

    • e.g. find n|James
    Copycopy
    Export to CSVexport
    Exitexit
    Import Exam ScoresimportExamScores i|FILEPATH

    • e.g. importExamScores i|C:/Users/alk/Downloads/exam_scores.csv
    Add ExamaddExam n|NAME s|MAX_SCORE

    • e.g. addExam n|Midterm s|100
    Delete ExamdeleteExam INDEX

    • e.g. deleteExam 3
    Select ExamselectExam INDEX

    • e.g. selectExam 1
    Deselect ExamdeselectExam
    Add Exam ScoreaddScore INDEX s|SCORE

    • e.g. addScore 1 s|42
    Edit Exam ScoreeditScore INDEX s|SCORE

    • e.g. editScore 1 s|25
    Delete Exam ScoredeleteScore INDEX

    • e.g. deleteScore 1
    + +
    -------------------------------------------------------------------------------------------------------------------- -## Command summary - -Action | Format, Examples ---------|------------------ -**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​`
    e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` -**Clear** | `clear` -**Delete** | `delete INDEX`
    e.g., `delete 3` -**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…​`
    e.g.,`edit 2 n/James Lee e/jameslee@example.com` -**Find** | `find KEYWORD [MORE_KEYWORDS]`
    e.g., `find James Jake` -**List** | `list` -**Help** | `help` +
    + +
    + +## Command Parameter Summary + +Some commands require you to include parameters. These parameters are identified by prefixes. Here are a list of valid prefixes and what they each refer to. + + + +**Important:**
    + +* Words in `UPPER_CASE` are the parameters to be supplied by you.
    + > e.g. in `add n|NAME`, `NAME` is a parameter which can be used as `add n|John Doe`. + +* Prefixes encased with '[ ]' are optional. + > e.g. `n|NAME [t|TAG]` can be used as `n|John Doe t|friend` or as `n|John Doe`. + +* Prefixes with '…' after them can be used multiple times. + > e.g. `[t|TAG]…​` can be used as ` ` (i.e. 0 times), `t|friend` (i.e 1 time), `t|friend t|family` etc. + +* Parameters can be in any order.
    + > e.g. if the command specifies `n|NAME p|PHONE_NUMBER`, `p|PHONE_NUMBER n|NAME` is also acceptable. + +* Extraneous parameters for commands that do not take in parameters (such as `help` , `list`, `exit`, `copy`, `export` and `clear`) will be ignored.
    + > e.g. if the command specifies `help 123`, it will be interpreted as `help`. + +
    + + + +**Caution:**
    +* If you are using a PDF version of this document, be careful when copying and pasting commands that span multiple lines as space characters surrounding line-breaks may be omitted when copied over to the application. +* Note the same prefix may be used for different purposes such as in the case of `s|` for studios and for scores. In these cases, we ensure no command would have to use the same prefix for multiple purposes. + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PrefixWhat it Refers toMax. LengthConstraints
    n|Name80Should only contain alphanumeric characters, spaces, and the characters ,, -, ., /, ( and ).
    n|Exam Name30Should only contain alphanumeric characters and spaces.
    p|Phone Number30It can start with an optional + to indicate the country code, with the rest only containing numbers. It should be at least 3 digits long.
    e|Email100Format: local-part@domain
    Constraints for local part:
    • Should only contain alphanumeric characters, and the characters +, _, . and -.
    • Should not start or end with special characters.
    • Should not have two consecutive special characters.
    Constraints for domain:
    • Made up of domain labels separated by periods.
    • Must end with a domain label of at least 2 characters long.
    • Domain label should consist of alphanumeric characters separated only by singular hyphens, if any.
    a|Address100Can take any values.
    i|Path of CSV File to Import-Should be the absolute file path of the CSV file without any inverted commas.
    [m| ]Matriculation IDFixed at 9The first letter must be an uppercase 'A', followed by 7 numbers, and end with an uppercase letter.
    [r| ]Reflection Group4The first letter must be an uppercase 'R', followed by any number.
    [s| ]Studio Group4The first letter must be an uppercase 'S', followed by any number.
    [s| ]Score7 + 2 decimalsFor exam max scores: the input must be a positive integer.
    For persons' exam scores: the input must be an integer greater than or equal to zero.
    [t| ]…Tags100Should be alphanumeric, and should not contain spaces.
    [lt|]Less Than7 + 2 decimalsShould be a positive numerical value smaller than the currently selected exam's max score.
    [mt| ]More Than7 + 2 decimalsShould be a positive numerical value smaller than the currently selected exam's max score.
    + +
    diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index 6bd245d8f4e..00000000000 --- a/docs/_config.yml +++ /dev/null @@ -1,15 +0,0 @@ -title: "AB-3" -theme: minima - -header_pages: - - UserGuide.md - - DeveloperGuide.md - - AboutUs.md - -markdown: kramdown - -repository: "se-edu/addressbook-level3" -github_icon: "images/github-icon.png" - -plugins: - - jemoji diff --git a/docs/_data/projects.yml b/docs/_data/projects.yml deleted file mode 100644 index 8f3e50cb601..00000000000 --- a/docs/_data/projects.yml +++ /dev/null @@ -1,23 +0,0 @@ -- name: "AB-1" - url: https://se-edu.github.io/addressbook-level1 - -- name: "AB-2" - url: https://se-edu.github.io/addressbook-level2 - -- name: "AB-3" - url: https://se-edu.github.io/addressbook-level3 - -- name: "AB-4" - url: https://se-edu.github.io/addressbook-level4 - -- name: "Duke" - url: https://se-edu.github.io/duke - -- name: "Collate" - url: https://se-edu.github.io/collate - -- name: "Book" - url: https://se-edu.github.io/se-book - -- name: "Resources" - url: https://se-edu.github.io/resources diff --git a/docs/_includes/custom-head.html b/docs/_includes/custom-head.html deleted file mode 100644 index 8559a67ffad..00000000000 --- a/docs/_includes/custom-head.html +++ /dev/null @@ -1,6 +0,0 @@ -{% comment %} - Placeholder to allow defining custom head, in principle, you can add anything here, e.g. favicons: - - 1. Head over to https://realfavicongenerator.net/ to add your own favicons. - 2. Customize default _includes/custom-head.html in your source directory and insert the given code snippet. -{% endcomment %} diff --git a/docs/_includes/head.html b/docs/_includes/head.html deleted file mode 100644 index 83ac5326933..00000000000 --- a/docs/_includes/head.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - {%- include custom-head.html -%} - - {{page.title}} - - diff --git a/docs/_includes/header.html b/docs/_includes/header.html deleted file mode 100644 index 33badcd4f99..00000000000 --- a/docs/_includes/header.html +++ /dev/null @@ -1,36 +0,0 @@ - diff --git a/docs/_layouts/alt-page.html b/docs/_layouts/alt-page.html deleted file mode 100644 index 5dbc6ef245f..00000000000 --- a/docs/_layouts/alt-page.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -layout: default ---- -
    - -
    -

    {{ page.alt_title | escape }}

    -
    - -
    - {{ content }} -
    - -
    diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html deleted file mode 100644 index e092cd572e0..00000000000 --- a/docs/_layouts/default.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - {%- include head.html -%} - - - - {%- include header.html -%} - -
    -
    - {{ content }} -
    -
    - - - - diff --git a/docs/_layouts/page.html b/docs/_layouts/page.html deleted file mode 100644 index 01e4b2a93b8..00000000000 --- a/docs/_layouts/page.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -layout: default ---- -
    - -
    -

    {{ page.title | escape }}

    -
    - -
    - {{ content }} -
    - -
    diff --git a/docs/_markbind/layouts/default.md b/docs/_markbind/layouts/default.md new file mode 100644 index 00000000000..6cee4f6e911 --- /dev/null +++ b/docs/_markbind/layouts/default.md @@ -0,0 +1,77 @@ + + + + +
    + + Avengers Assemble +
  15. Home
  16. +
  17. User Guide
  18. +
  19. Developer Guide
  20. +
  21. About Us
  22. +
  23. :fab-github: +
  24. +
  25. + +
  26. +
    +
    + +
    + +
    + {{ content }} +
    + + +
    + +
    + +
    + [**Powered by** {{MarkBind}}, generated on {{timestamp}}] +
    +
    diff --git a/docs/_markbind/variables.json b/docs/_markbind/variables.json new file mode 100644 index 00000000000..9d89eb0358b --- /dev/null +++ b/docs/_markbind/variables.json @@ -0,0 +1,3 @@ +{ + "jsonVariableExample": "Your variables can be defined here as well" +} diff --git a/docs/_markbind/variables.md b/docs/_markbind/variables.md new file mode 100644 index 00000000000..89ae5318fa4 --- /dev/null +++ b/docs/_markbind/variables.md @@ -0,0 +1,4 @@ + +To inject this HTML segment in your markbind files, use {{ example }} where you want to place it. +More generally, surround the segment's id with double curly braces. + diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss deleted file mode 100644 index 0d3f6e80ced..00000000000 --- a/docs/_sass/minima/_base.scss +++ /dev/null @@ -1,295 +0,0 @@ -html { - font-size: $base-font-size; -} - -/** - * Reset some basic elements - */ -body, h1, h2, h3, h4, h5, h6, -p, blockquote, pre, hr, -dl, dd, ol, ul, figure { - margin: 0; - padding: 0; - -} - - - -/** - * Basic styling - */ -body { - font: $base-font-weight #{$base-font-size}/#{$base-line-height} $base-font-family; - color: $text-color; - background-color: $background-color; - -webkit-text-size-adjust: 100%; - -webkit-font-feature-settings: "kern" 1; - -moz-font-feature-settings: "kern" 1; - -o-font-feature-settings: "kern" 1; - font-feature-settings: "kern" 1; - font-kerning: normal; - display: flex; - min-height: 100vh; - flex-direction: column; - overflow-wrap: break-word; -} - - - -/** - * Set `margin-bottom` to maintain vertical rhythm - */ -h1, h2, h3, h4, h5, h6, -p, blockquote, pre, -ul, ol, dl, figure, -%vertical-rhythm { - margin-bottom: $spacing-unit / 2; -} - -hr { - margin-top: $spacing-unit; - margin-bottom: $spacing-unit; -} - -/** - * `main` element - */ -main { - display: block; /* Default value of `display` of `main` element is 'inline' in IE 11. */ -} - - - -/** - * Images - */ -img { - max-width: 100%; - vertical-align: middle; -} - - - -/** - * Figures - */ -figure > img { - display: block; -} - -figcaption { - font-size: $small-font-size; -} - - - -/** - * Lists - */ -ul, ol { - margin-left: $spacing-unit; -} - -li { - > ul, - > ol { - margin-bottom: 0; - } -} - - - -/** - * Headings - */ -h1, h2, h3, h4, h5, h6 { - font-weight: $base-font-weight; -} - - - -/** - * Links - */ -a { - color: $link-base-color; - text-decoration: none; - - &:visited { - color: $link-visited-color; - } - - &:hover { - color: $text-color; - text-decoration: underline; - } - - .social-media-list &:hover { - text-decoration: none; - - .username { - text-decoration: underline; - } - } -} - - -/** - * Blockquotes - */ -blockquote { - color: $brand-color; - border-left: 4px solid $brand-color-light; - padding-left: $spacing-unit / 2; - @include relative-font-size(1.125); - font-style: italic; - - > :last-child { - margin-bottom: 0; - } - - i, em { - font-style: normal; - } -} - - - -/** - * Code formatting - */ -pre, -code { - font-family: $code-font-family; - font-size: 0.9375em; - border: 1px solid $brand-color-light; - border-radius: 3px; - background-color: $code-background-color; -} - -code { - padding: 1px 5px; -} - -pre { - padding: 8px 12px; - overflow-x: auto; - - > code { - border: 0; - padding-right: 0; - padding-left: 0; - } -} - -.highlight { - border-radius: 3px; - background: $code-background-color; - @extend %vertical-rhythm; - - .highlighter-rouge & { - background: $code-background-color; - } -} - - - -/** - * Wrapper - */ -.wrapper { - max-width: calc(#{$content-width} - (#{$spacing-unit})); - margin-right: auto; - margin-left: auto; - padding-right: $spacing-unit / 2; - padding-left: $spacing-unit / 2; - @extend %clearfix; - - @media screen and (min-width: $on-large) { - max-width: calc(#{$content-width} - (#{$spacing-unit} * 2)); - padding-right: $spacing-unit; - padding-left: $spacing-unit; - } -} - - - -/** - * Clearfix - */ -%clearfix:after { - content: ""; - display: table; - clear: both; -} - - - -/** - * Icons - */ - -.orange { - color: #f66a0a; -} - -.grey { - color: #828282; -} - -/** - * Tables - */ -table { - margin-bottom: $spacing-unit; - width: 100%; - text-align: $table-text-align; - color: $table-text-color; - border-collapse: collapse; - border: 1px solid $table-border-color; - tr { - &:nth-child(even) { - background-color: $table-zebra-color; - } - } - th, td { - padding: ($spacing-unit / 3) ($spacing-unit / 2); - } - th { - background-color: $table-header-bg-color; - border: 1px solid $table-header-border; - } - td { - border: 1px solid $table-border-color; - } - - @include media-query($on-laptop) { - display: block; - overflow-x: auto; - -webkit-overflow-scrolling: touch; - -ms-overflow-style: -ms-autohiding-scrollbar; - } -} - -@media print { - /** - * Prevents page break from cutting through content when printing - */ - body { - display: block; - } - /** - * Replaces the top navigation menu with the project name when printing - */ - .site-header .wrapper { - display: none; - } - .site-header { - text-align: center; - } - .site-header:before { - content: "AB-3"; - font-size: 32px; - } -} - diff --git a/docs/_sass/minima/_layout.scss b/docs/_sass/minima/_layout.scss deleted file mode 100644 index ca99f981701..00000000000 --- a/docs/_sass/minima/_layout.scss +++ /dev/null @@ -1,263 +0,0 @@ -/** - * Site header - */ -.site-header { - border-top: 5px solid $brand-color-dark; - border-bottom: 1px solid $brand-color-light; - min-height: $spacing-unit * 1.865; - line-height: $base-line-height * $base-font-size * 2.25; - - // Positioning context for the mobile navigation icon - position: relative; -} - -.site-title { - @include relative-font-size(1.625); - font-weight: 300; - letter-spacing: -1px; - margin-bottom: 0; - float: left; - - @include media-query($on-palm) { - padding-right: 45px; - } - - &, - &:visited { - color: $brand-color-dark; - } -} - -.site-nav { - position: absolute; - top: 9px; - right: $spacing-unit / 2; - background-color: $background-color; - border: 1px solid $brand-color-light; - border-radius: 5px; - text-align: right; - - .nav-trigger { - display: none; - } - - .menu-icon { - float: right; - width: 36px; - height: 26px; - line-height: 0; - padding-top: 10px; - text-align: center; - - > svg path { - fill: $brand-color-dark; - } - } - - label[for="nav-trigger"] { - display: block; - float: right; - width: 36px; - height: 36px; - z-index: 2; - cursor: pointer; - } - - input ~ .trigger { - clear: both; - display: none; - } - - input:checked ~ .trigger { - display: block; - padding-bottom: 5px; - } - - .page-link { - color: $text-color; - line-height: $base-line-height; - display: block; - padding: 5px 10px; - - // Gaps between nav items, but not on the last one - &:not(:last-child) { - margin-right: 0; - } - margin-left: 20px; - } - - @media screen and (min-width: $on-medium) { - position: static; - float: right; - border: none; - background-color: inherit; - - label[for="nav-trigger"] { - display: none; - } - - .menu-icon { - display: none; - } - - input ~ .trigger { - display: block; - } - - .page-link { - display: inline; - padding: 0; - - &:not(:last-child) { - margin-right: 20px; - } - margin-left: auto; - } - } -} - - - -/** - * Page content - */ -.page-content { - padding: $spacing-unit 0; - flex: 1 0 auto; -} - -.page-heading { - @include relative-font-size(2); -} - -.post-list-heading { - @include relative-font-size(1.75); -} - -.post-list { - margin-left: 0; - list-style: none; - - > li { - margin-bottom: $spacing-unit; - } -} - -.post-meta { - font-size: $small-font-size; - color: $brand-color; -} - -.post-link { - display: block; - @include relative-font-size(1.5); -} - - - -/** - * Posts - */ -.post-header { - margin-bottom: $spacing-unit; -} - -.post-title, -.post-content h1 { - @include relative-font-size(2.625); - letter-spacing: -1px; - line-height: 1.15; - - @media screen and (min-width: $on-large) { - @include relative-font-size(2.625); - } -} - -.post-content { - margin-bottom: $spacing-unit; - - h1, h2, h3 { margin-top: $spacing-unit * 2 } - h4, h5, h6 { margin-top: $spacing-unit } - - h2 { - @include relative-font-size(1.75); - - @media screen and (min-width: $on-large) { - @include relative-font-size(2); - } - } - - h3 { - @include relative-font-size(1.375); - - @media screen and (min-width: $on-large) { - @include relative-font-size(1.625); - } - } - - h4 { - @include relative-font-size(1.25); - } - - h5 { - @include relative-font-size(1.125); - } - h6 { - @include relative-font-size(1.0625); - } -} - - -.social-media-list { - display: table; - margin: 0 auto; - li { - float: left; - margin: 5px 10px 5px 0; - &:last-of-type { margin-right: 0 } - a { - display: block; - padding: $spacing-unit / 4; - border: 1px solid $brand-color-light; - &:hover { border-color: darken($brand-color-light, 10%) } - } - } -} - - - -/** - * Pagination navbar - */ -.pagination { - margin-bottom: $spacing-unit; - @extend .social-media-list; - li { - a, div { - min-width: 41px; - text-align: center; - box-sizing: border-box; - } - div { - display: block; - padding: $spacing-unit / 4; - border: 1px solid transparent; - - &.pager-edge { - color: darken($brand-color-light, 5%); - border: 1px dashed; - } - } - } -} - - - -/** - * Grid helpers - */ -@media screen and (min-width: $on-large) { - .one-half { - width: calc(50% - (#{$spacing-unit} / 2)); - } -} diff --git a/docs/_sass/minima/custom-mixins.scss b/docs/_sass/minima/custom-mixins.scss deleted file mode 100644 index 9d4bedc1c67..00000000000 --- a/docs/_sass/minima/custom-mixins.scss +++ /dev/null @@ -1,21 +0,0 @@ -@mixin alert-variant($background, $border, $color) { - color: $color; - @include gradient-bg($background); - border-color: $border; - - .alert-link { - color: darken($color, 10%); - } -} - -@mixin gradient-bg($color, $foreground: null) { - @if $enable-gradients { - @if $foreground { - background-image: $foreground, linear-gradient(180deg, mix($body-bg, $color, 15%), $color); - } @else { - background-image: linear-gradient(180deg, mix($body-bg, $color, 15%), $color); - } - } @else { - background-color: $color; - } -} diff --git a/docs/_sass/minima/custom-styles.scss b/docs/_sass/minima/custom-styles.scss deleted file mode 100644 index 56b5d56b430..00000000000 --- a/docs/_sass/minima/custom-styles.scss +++ /dev/null @@ -1,34 +0,0 @@ -// Placeholder to allow defining custom styles that override everything else. -// (Use `_sass/minima/custom-variables.scss` to override variable defaults) -h2, h3, h4, h5, h6 { - color: #e46c0a; -} - -// Bootstrap style alerts -.alert { - position: relative; - padding: $alert-padding-y $alert-padding-x; - margin-bottom: $alert-margin-bottom; - border: $alert-border-width solid transparent; - border-radius : $alert-border-radius; -} - -// Headings for larger alerts -.alert-heading { - // Specified to prevent conflicts of changing $headings-color - color: inherit; -} - -// Provide class for links that match alerts -.alert-link { - font-weight: $alert-link-font-weight; -} - -// Generate contextual modifier classes for colorizing the alert. - -@each $color, $value in $theme-colors { - .alert-#{$color} { - @include alert-variant(color-level($value, $alert-bg-level), color-level($value, $alert-border-level), color-level($value, $alert-color-level)); - } -} - diff --git a/docs/_sass/minima/custom-variables.scss b/docs/_sass/minima/custom-variables.scss deleted file mode 100644 index a128970cbe7..00000000000 --- a/docs/_sass/minima/custom-variables.scss +++ /dev/null @@ -1,76 +0,0 @@ -// Placeholder to allow overriding predefined variables smoothly. - -//Bootstrap's default -$white: #fff !default; -$gray-100: #f8f9fa !default; -$gray-200: #e9ecef !default; -$gray-300: #dee2e6 !default; -$gray-400: #ced4da !default; -$gray-500: #adb5bd !default; -$gray-600: #6c757d !default; -$gray-700: #495057 !default; -$gray-800: #343a40 !default; -$gray-900: #212529 !default; -$black: #000 !default; -$blue: #0d6efd !default; -$indigo: #6610f2 !default; -$purple: #6f42c1 !default; -$pink: #d63384 !default; -$red: #dc3545 !default; -$orange: #fd7e14 !default; -$yellow: #ffc107 !default; -$green: #28a745 !default; -$teal: #20c997 !default; -$cyan: #17a2b8 !default; - -$primary: $blue !default; -$secondary: $gray-600 !default; -$success: $green !default; -$info: $cyan !default; -$warning: $yellow !default; -$danger: $red !default; -$light: $gray-100 !default; -$dark: $gray-800 !default; - -$theme-colors: ( - "primary": $primary, - "secondary": $secondary, - "success": $success, - "info": $info, - "warning": $warning, - "danger": $danger, - "light": $light, - "dark": $dark -) !default; - -$theme-color-interval: 8% !default; - -$body-bg: $white !default; -$body-color: $gray-900 !default; -$body-text-align: null !default; - -$enable-gradients: true; - -// Define alert colors, border radius, and padding. -$border-radius: .25rem !default; -$border-width: 1px !default; -$font-weight-bold: 700 !default; - -$alert-padding-y: .75rem !default; -$alert-padding-x: 1.25rem !default; -$alert-margin-bottom: 1rem !default; -$alert-border-radius: $border-radius !default; -$alert-link-font-weight: $font-weight-bold !default; -$alert-border-width: $border-width !default; - -$alert-bg-level: -10 !default; -$alert-border-level: -9 !default; -$alert-color-level: 6 !default; - -// Request a color level -// scss-docs-start color-level -@function color-level($color: $primary, $level: 0) { - $color-base: if($level > 0, $black, $white); - $level: abs($level); - @return mix($color-base, $color, $level * $theme-color-interval); -} diff --git a/docs/_sass/minima/initialize.scss b/docs/_sass/minima/initialize.scss deleted file mode 100644 index 30288811151..00000000000 --- a/docs/_sass/minima/initialize.scss +++ /dev/null @@ -1,51 +0,0 @@ -@charset "utf-8"; - -// Define defaults for each variable. - -$base-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Segoe UI Symbol", "Segoe UI Emoji", "Apple Color Emoji", Roboto, Helvetica, Arial, sans-serif !default; -$code-font-family: "Menlo", "Inconsolata", "Consolas", "Roboto Mono", "Ubuntu Mono", "Liberation Mono", "Courier New", monospace; -$base-font-size: 16px !default; -$base-font-weight: 400 !default; -$small-font-size: $base-font-size * 0.875 !default; -$base-line-height: 1.5 !default; - -$spacing-unit: 30px !default; - -$table-text-align: left !default; - -// Width of the content area -$content-width: 800px !default; - -$on-palm: 600px !default; -$on-laptop: 800px !default; - -$on-medium: $on-palm !default; -$on-large: $on-laptop !default; - -// Use media queries like this: -// @include media-query($on-palm) { -// .wrapper { -// padding-right: $spacing-unit / 2; -// padding-left: $spacing-unit / 2; -// } -// } -// Notice the following mixin uses max-width, in a deprecated, desktop-first -// approach, whereas media queries used elsewhere now use min-width. -@mixin media-query($device) { - @media screen and (max-width: $device) { - @content; - } -} - -@mixin relative-font-size($ratio) { - font-size: #{$ratio}rem; -} - -// Import pre-styling-overrides hook and style-partials. -@import - "minima/custom-variables", // Hook to override predefined variables. - "minima/custom-mixins", // Hook to add custom mixins. - "minima/base", // Defines element resets. - "minima/layout", // Defines structure and style based on CSS selectors. - "minima/custom-styles" // Hook to override existing styles. -; diff --git a/docs/_sass/minima/skins/classic.scss b/docs/_sass/minima/skins/classic.scss deleted file mode 100644 index 37ea9c5244c..00000000000 --- a/docs/_sass/minima/skins/classic.scss +++ /dev/null @@ -1,84 +0,0 @@ -@charset "utf-8"; - -$brand-color: #828282 !default; -$brand-color-light: lighten($brand-color, 40%) !default; -$brand-color-dark: darken($brand-color, 25%) !default; - -$text-color: #111 !default; -$background-color: #fdfdfd !default; -$code-background-color: #eef !default; - -$link-base-color: #2a7ae2 !default; -$link-visited-color: darken($link-base-color, 15%) !default; - -$table-text-color: lighten($text-color, 18%) !default; -$table-zebra-color: lighten($brand-color, 46%) !default; -$table-header-bg-color: lighten($brand-color, 43%) !default; -$table-header-border: lighten($brand-color, 36%) !default; -$table-border-color: $brand-color-light !default; - - -// Syntax highlighting styles should be adjusted appropriately for every "skin" -// ---------------------------------------------------------------------------- - -.highlight { - .c { color: #998; font-style: italic } // Comment - .err { color: #a61717; background-color: #e3d2d2 } // Error - .k { font-weight: bold } // Keyword - .o { font-weight: bold } // Operator - .cm { color: #998; font-style: italic } // Comment.Multiline - .cp { color: #999; font-weight: bold } // Comment.Preproc - .c1 { color: #998; font-style: italic } // Comment.Single - .cs { color: #999; font-weight: bold; font-style: italic } // Comment.Special - .gd { color: #000; background-color: #fdd } // Generic.Deleted - .gd .x { color: #000; background-color: #faa } // Generic.Deleted.Specific - .ge { font-style: italic } // Generic.Emph - .gr { color: #a00 } // Generic.Error - .gh { color: #999 } // Generic.Heading - .gi { color: #000; background-color: #dfd } // Generic.Inserted - .gi .x { color: #000; background-color: #afa } // Generic.Inserted.Specific - .go { color: #888 } // Generic.Output - .gp { color: #555 } // Generic.Prompt - .gs { font-weight: bold } // Generic.Strong - .gu { color: #aaa } // Generic.Subheading - .gt { color: #a00 } // Generic.Traceback - .kc { font-weight: bold } // Keyword.Constant - .kd { font-weight: bold } // Keyword.Declaration - .kp { font-weight: bold } // Keyword.Pseudo - .kr { font-weight: bold } // Keyword.Reserved - .kt { color: #458; font-weight: bold } // Keyword.Type - .m { color: #099 } // Literal.Number - .s { color: #d14 } // Literal.String - .na { color: #008080 } // Name.Attribute - .nb { color: #0086B3 } // Name.Builtin - .nc { color: #458; font-weight: bold } // Name.Class - .no { color: #008080 } // Name.Constant - .ni { color: #800080 } // Name.Entity - .ne { color: #900; font-weight: bold } // Name.Exception - .nf { color: #900; font-weight: bold } // Name.Function - .nn { color: #555 } // Name.Namespace - .nt { color: #000080 } // Name.Tag - .nv { color: #008080 } // Name.Variable - .ow { font-weight: bold } // Operator.Word - .w { color: #bbb } // Text.Whitespace - .mf { color: #099 } // Literal.Number.Float - .mh { color: #099 } // Literal.Number.Hex - .mi { color: #099 } // Literal.Number.Integer - .mo { color: #099 } // Literal.Number.Oct - .sb { color: #d14 } // Literal.String.Backtick - .sc { color: #d14 } // Literal.String.Char - .sd { color: #d14 } // Literal.String.Doc - .s2 { color: #d14 } // Literal.String.Double - .se { color: #d14 } // Literal.String.Escape - .sh { color: #d14 } // Literal.String.Heredoc - .si { color: #d14 } // Literal.String.Interpol - .sx { color: #d14 } // Literal.String.Other - .sr { color: #009926 } // Literal.String.Regex - .s1 { color: #d14 } // Literal.String.Single - .ss { color: #990073 } // Literal.String.Symbol - .bp { color: #999 } // Name.Builtin.Pseudo - .vc { color: #008080 } // Name.Variable.Class - .vg { color: #008080 } // Name.Variable.Global - .vi { color: #008080 } // Name.Variable.Instance - .il { color: #099 } // Literal.Number.Integer.Long -} diff --git a/docs/_sass/minima/skins/solarized-dark.scss b/docs/_sass/minima/skins/solarized-dark.scss deleted file mode 100644 index f3b1f387de0..00000000000 --- a/docs/_sass/minima/skins/solarized-dark.scss +++ /dev/null @@ -1,4 +0,0 @@ -@charset "utf-8"; - -$sol-is-dark: true; -@import "minima/skins/solarized"; diff --git a/docs/_sass/minima/skins/solarized.scss b/docs/_sass/minima/skins/solarized.scss deleted file mode 100644 index 982bd7f2990..00000000000 --- a/docs/_sass/minima/skins/solarized.scss +++ /dev/null @@ -1,133 +0,0 @@ -@charset "utf-8"; - -// Solarized skin -// ============== -// Created by Sander Voerman using the Solarized -// color scheme by Ethan Schoonover . - -// This style sheet implements two options for the minima.skin setting: -// "solarized" for light mode and "solarized-dark" for dark mode. -$sol-is-dark: false !default; - - -// Color scheme -// ------------ -// The inline comments show the canonical L*a*b values for each color. - -$sol-base03: #002b36; // 15 -12 -12 -$sol-base02: #073642; // 20 -12 -12 -$sol-base01: #586e75; // 45 -07 -07 -$sol-base00: #657b83; // 50 -07 -07 -$sol-base0: #839496; // 60 -06 -03 -$sol-base1: #93a1a1; // 65 -05 -02 -$sol-base2: #eee8d5; // 92 -00 10 -$sol-base3: #fdf6e3; // 97 00 10 -$sol-yellow: #b58900; // 60 10 65 -$sol-orange: #cb4b16; // 50 50 55 -$sol-red: #dc322f; // 50 65 45 -$sol-magenta: #d33682; // 50 65 -05 -$sol-violet: #6c71c4; // 50 15 -45 -$sol-blue: #268bd2; // 55 -10 -45 -$sol-cyan: #2aa198; // 60 -35 -05 -$sol-green: #859900; // 60 -20 65 - -$sol-mono3: $sol-base3; -$sol-mono2: $sol-base2; -$sol-mono1: $sol-base1; -$sol-mono00: $sol-base00; -$sol-mono01: $sol-base01; - -@if $sol-is-dark { - $sol-mono3: $sol-base03; - $sol-mono2: $sol-base02; - $sol-mono1: $sol-base01; - $sol-mono00: $sol-base0; - $sol-mono01: $sol-base1; -} - - -// Minima color variables -// ---------------------- - -$brand-color: $sol-mono1 !default; -$brand-color-light: mix($sol-mono1, $sol-mono3) !default; -$brand-color-dark: $sol-mono00 !default; - -$text-color: $sol-mono01 !default; -$background-color: $sol-mono3 !default; -$code-background-color: $sol-mono2 !default; - -$link-base-color: $sol-blue !default; -$link-visited-color: mix($sol-blue, $sol-mono00) !default; - -$table-text-color: $sol-mono00 !default; -$table-zebra-color: mix($sol-mono2, $sol-mono3) !default; -$table-header-bg-color: $sol-mono2 !default; -$table-header-border: $sol-mono1 !default; -$table-border-color: $sol-mono1 !default; - - -// Syntax highlighting styles -// -------------------------- - -.highlight { - .c { color: $sol-mono1; font-style: italic } // Comment - .err { color: $sol-red } // Error - .k { color: $sol-mono01; font-weight: bold } // Keyword - .o { color: $sol-mono01; font-weight: bold } // Operator - .cm { color: $sol-mono1; font-style: italic } // Comment.Multiline - .cp { color: $sol-mono1; font-weight: bold } // Comment.Preproc - .c1 { color: $sol-mono1; font-style: italic } // Comment.Single - .cs { color: $sol-mono1; font-weight: bold; font-style: italic } // Comment.Special - .gd { color: $sol-red } // Generic.Deleted - .gd .x { color: $sol-red } // Generic.Deleted.Specific - .ge { color: $sol-mono00; font-style: italic } // Generic.Emph - .gr { color: $sol-red } // Generic.Error - .gh { color: $sol-mono1 } // Generic.Heading - .gi { color: $sol-green } // Generic.Inserted - .gi .x { color: $sol-green } // Generic.Inserted.Specific - .go { color: $sol-mono00 } // Generic.Output - .gp { color: $sol-mono00 } // Generic.Prompt - .gs { color: $sol-mono01; font-weight: bold } // Generic.Strong - .gu { color: $sol-mono1 } // Generic.Subheading - .gt { color: $sol-red } // Generic.Traceback - .kc { color: $sol-mono01; font-weight: bold } // Keyword.Constant - .kd { color: $sol-mono01; font-weight: bold } // Keyword.Declaration - .kp { color: $sol-mono01; font-weight: bold } // Keyword.Pseudo - .kr { color: $sol-mono01; font-weight: bold } // Keyword.Reserved - .kt { color: $sol-violet; font-weight: bold } // Keyword.Type - .m { color: $sol-cyan } // Literal.Number - .s { color: $sol-magenta } // Literal.String - .na { color: $sol-cyan } // Name.Attribute - .nb { color: $sol-blue } // Name.Builtin - .nc { color: $sol-violet; font-weight: bold } // Name.Class - .no { color: $sol-cyan } // Name.Constant - .ni { color: $sol-violet } // Name.Entity - .ne { color: $sol-violet; font-weight: bold } // Name.Exception - .nf { color: $sol-blue; font-weight: bold } // Name.Function - .nn { color: $sol-mono00 } // Name.Namespace - .nt { color: $sol-blue } // Name.Tag - .nv { color: $sol-cyan } // Name.Variable - .ow { color: $sol-mono01; font-weight: bold } // Operator.Word - .w { color: $sol-mono1 } // Text.Whitespace - .mf { color: $sol-cyan } // Literal.Number.Float - .mh { color: $sol-cyan } // Literal.Number.Hex - .mi { color: $sol-cyan } // Literal.Number.Integer - .mo { color: $sol-cyan } // Literal.Number.Oct - .sb { color: $sol-magenta } // Literal.String.Backtick - .sc { color: $sol-magenta } // Literal.String.Char - .sd { color: $sol-magenta } // Literal.String.Doc - .s2 { color: $sol-magenta } // Literal.String.Double - .se { color: $sol-magenta } // Literal.String.Escape - .sh { color: $sol-magenta } // Literal.String.Heredoc - .si { color: $sol-magenta } // Literal.String.Interpol - .sx { color: $sol-magenta } // Literal.String.Other - .sr { color: $sol-green } // Literal.String.Regex - .s1 { color: $sol-magenta } // Literal.String.Single - .ss { color: $sol-magenta } // Literal.String.Symbol - .bp { color: $sol-mono1 } // Name.Builtin.Pseudo - .vc { color: $sol-cyan } // Name.Variable.Class - .vg { color: $sol-cyan } // Name.Variable.Global - .vi { color: $sol-cyan } // Name.Variable.Instance - .il { color: $sol-cyan } // Literal.Number.Integer.Long -} diff --git a/docs/assets/css/style.scss b/docs/assets/css/style.scss deleted file mode 100644 index b5ec6976efa..00000000000 --- a/docs/assets/css/style.scss +++ /dev/null @@ -1,12 +0,0 @@ ---- -# Only the main Sass file needs front matter (the dashes are enough) ---- - -@import - "minima/skins/{{ site.minima.skin | default: 'classic' }}", - "minima/initialize"; - -.icon { - height: 21px; - width: 21px -} diff --git a/docs/diagrams/AddCommandParsing.puml b/docs/diagrams/AddCommandParsing.puml new file mode 100644 index 00000000000..6d29c52f5b0 --- /dev/null +++ b/docs/diagrams/AddCommandParsing.puml @@ -0,0 +1,47 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":AddCommandParser" as AddCommandParser LOGIC_COLOR +participant "<>\nArgumentTokenizer" as ArgumentTokenizer LOGIC_COLOR +participant "multi:ArgumentMultimap" as ArgumentMultimap LOGIC_COLOR +participant "<>\nParserUtil" as ParserUtil LOGIC_COLOR +end box + +mainFrame sd parse fields +AddCommandParser -> ParserUtil : parseName(...) +activate ParserUtil +ParserUtil --> AddCommandParser +deactivate ParserUtil +AddCommandParser -> ParserUtil : parsePhone(...) +activate ParserUtil +ParserUtil --> AddCommandParser +deactivate ParserUtil +AddCommandParser -> ParserUtil : parseEmail(...) +activate ParserUtil +ParserUtil --> AddCommandParser +deactivate ParserUtil +AddCommandParser -> ParserUtil : parseAddress(...) +activate ParserUtil +ParserUtil --> AddCommandParser +deactivate ParserUtil +AddCommandParser -> ParserUtil : parseTags(...) +activate ParserUtil +ParserUtil --> AddCommandParser +deactivate ParserUtil + +' Callback +AddCommandParser -> AddCommandParser : handleOptionalMatric(...) +activate AddCommandParser +AddCommandParser --> AddCommandParser +deactivate AddCommandParser +AddCommandParser -> AddCommandParser : handleOptionalReflection(...) +activate AddCommandParser +AddCommandParser --> AddCommandParser +deactivate AddCommandParser +AddCommandParser -> AddCommandParser : handleOptionalStudio(...) +activate AddCommandParser +AddCommandParser --> AddCommandParser +deactivate AddCommandParser +@enduml diff --git a/docs/diagrams/AddExamExecutionSequenceDiagram.puml b/docs/diagrams/AddExamExecutionSequenceDiagram.puml new file mode 100644 index 00000000000..ca7310c5345 --- /dev/null +++ b/docs/diagrams/AddExamExecutionSequenceDiagram.puml @@ -0,0 +1,57 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":AddExamCommand" as AddExamCommand LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "m:Model" as Model MODEL_COLOR +participant ":AddressBook" as AddressBook MODEL_COLOR +participant ":UniqueExamList" as UniqueExamList MODEL_COLOR +end box + +[-> AddExamCommand : execute(m) +activate AddExamCommand + +AddExamCommand -> Model : hasExam(e) +activate Model + +Model -> AddressBook : hasExam(e) +activate AddressBook + +AddressBook -> UniqueExamList : contains(e) +activate UniqueExamList + +UniqueExamList --> AddressBook +deactivate UniqueExamList + +AddressBook --> Model +deactivate AddressBook + +Model --> AddExamCommand +deactivate Model + +AddExamCommand -> Model : addExam(e) +activate Model + +Model -> AddressBook : addExam(e) +activate AddressBook + +AddressBook -> UniqueExamList : add(e) +activate UniqueExamList + +UniqueExamList --> AddressBook +deactivate UniqueExamList + +AddressBook --> Model +deactivate AddressBook + +Model --> AddExamCommand +deactivate Model + +[<-- AddExamCommand : CommandResult +deactivate AddExamCommand + +@enduml diff --git a/docs/diagrams/AddExamParsingSequenceDiagram.puml b/docs/diagrams/AddExamParsingSequenceDiagram.puml new file mode 100644 index 00000000000..1f774f61e1e --- /dev/null +++ b/docs/diagrams/AddExamParsingSequenceDiagram.puml @@ -0,0 +1,61 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":AddExamCommandParser" as AddExamCommandParser LOGIC_COLOR +participant "<>\nArgumentTokenizer" as ArgumentTokenizer LOGIC_COLOR +participant "multi:ArgumentMultimap" as ArgumentMultimap LOGIC_COLOR +participant "a:AddExamCommand" as AddExamCommand LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "e:Exam" as Exam MODEL_COLOR +end box + +[-> AddExamCommandParser : parse(...) +activate AddExamCommandParser + +AddExamCommandParser -> ArgumentTokenizer : tokenize(...) +activate ArgumentTokenizer + +create ArgumentMultimap +ArgumentTokenizer -> ArgumentMultimap +activate ArgumentMultimap +ArgumentMultimap --> ArgumentTokenizer + +deactivate ArgumentMultimap + +ArgumentTokenizer --> AddExamCommandParser +deactivate ArgumentTokenizer + +AddExamCommandParser -> AddExamCommandParser : arePrefixesPresent(...) +activate AddExamCommandParser +AddExamCommandParser --> AddExamCommandParser +deactivate AddExamCommandParser + +AddExamCommandParser -> ArgumentMultimap : verifyNoDuplicatePrefixesFor(...) +activate ArgumentMultimap +ArgumentMultimap --> AddExamCommandParser + +deactivate ArgumentMultimap + +deactivate ArgumentTokenizer + +create Exam +AddExamCommandParser -> Exam +activate Exam +Exam --> AddExamCommandParser +deactivate Exam + +create AddExamCommand +AddExamCommandParser -> AddExamCommand : new AddExamCommand(e) +activate AddExamCommand + +AddExamCommand --> AddExamCommandParser +deactivate AddExamCommand + +[<-- AddExamCommandParser : e +deactivate AddExamCommandParser + +@enduml diff --git a/docs/diagrams/AddScoreExecutionSequenceDiagram.puml b/docs/diagrams/AddScoreExecutionSequenceDiagram.puml new file mode 100644 index 00000000000..b93c02d7033 --- /dev/null +++ b/docs/diagrams/AddScoreExecutionSequenceDiagram.puml @@ -0,0 +1,61 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":AddScoreCommand" as AddScoreCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "m:Model" as Model MODEL_COLOR +participant "personToEdit:Person" as Person MODEL_COLOR +participant "selectedExam:Exam" as Exam MODEL_COLOR +end box + +[-> AddScoreCommand : execute(m) +activate AddScoreCommand + +AddScoreCommand -> Model : getFilteredPersonList() +activate Model + +Model --> AddScoreCommand : list +deactivate Model + +create Person +AddScoreCommand -> Person : list.get() +activate Person + +Person --> AddScoreCommand +deactivate Person + +AddScoreCommand -> Model : getSelectedExam() +activate Model + +create Exam +Model -> Exam : getValue() +activate Exam + +Exam --> Model +deactivate Exam + +Model --> AddScoreCommand : selectedExam +deactivate Model + +AddScoreCommand -> Model : addExamScoreToPerson() +activate Model + +Model --> AddScoreCommand +deactivate Model + +create CommandResult +AddScoreCommand -> CommandResult +activate CommandResult + +CommandResult --> AddScoreCommand +deactivate CommandResult + +[<-- AddScoreCommand : r +deactivate AddScoreCommand + +@enduml diff --git a/docs/diagrams/AddScoreParsingSequenceDiagram.puml b/docs/diagrams/AddScoreParsingSequenceDiagram.puml new file mode 100644 index 00000000000..8a6050cfc33 --- /dev/null +++ b/docs/diagrams/AddScoreParsingSequenceDiagram.puml @@ -0,0 +1,79 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":AddScoreCommandParser" as AddScoreCommandParser LOGIC_COLOR +participant "<>\nArgumentTokenizer" as ArgumentTokenizer LOGIC_COLOR +participant "a:ArgumentMultimap" as ArgumentMultimap LOGIC_COLOR +participant "<>\nParserUtil" as ParserUtil LOGIC_COLOR +participant "c:AddScoreCommand" as AddScoreCommand LOGIC_COLOR +end box + +box Commons COMMONS_COLOR_T1 +participant "i:Index" as Index COMMONS_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "s:Score" as Score MODEL_COLOR +end box + +[-> AddScoreCommandParser : parse("1 s|100") +activate AddScoreCommandParser + +AddScoreCommandParser -> ArgumentTokenizer : tokenize("1 s|100", PREFIX_SCORE) +activate ArgumentTokenizer + +create ArgumentMultimap +ArgumentTokenizer -> ArgumentMultimap +activate ArgumentMultimap + +ArgumentMultimap --> ArgumentTokenizer +deactivate ArgumentMultimap + +ArgumentTokenizer --> AddScoreCommandParser : a +deactivate ArgumentTokenizer + +AddScoreCommandParser -> ArgumentMultimap : verifyNoDuplicatePrefixesFor(PREFIX_SCORE) +activate ArgumentMultimap + +ArgumentMultimap --> AddScoreCommandParser +deactivate ArgumentMultimap + +AddScoreCommandParser -> ParserUtil : parseIndex() +activate ParserUtil + +create Index +ParserUtil -> Index +activate Index + +Index --> ParserUtil +deactivate Index + +ParserUtil --> AddScoreCommandParser : i +deactivate ParserUtil + +AddScoreCommandParser -> ParserUtil : parseScore() +activate ParserUtil + +create Score +ParserUtil -> Score +activate Score + +Score --> ParserUtil +deactivate Score + +ParserUtil --> AddScoreCommandParser : s +deactivate ParserUtil + +create AddScoreCommand +AddScoreCommandParser -> AddScoreCommand : new AddScoreCommand(i, s) +activate AddScoreCommand + +AddScoreCommand --> AddScoreCommandParser +deactivate AddScoreCommand + +[<-- AddScoreCommandParser : c +deactivate AddScoreCommandParser + +@enduml diff --git a/docs/diagrams/AddSequenceDiagram.puml b/docs/diagrams/AddSequenceDiagram.puml new file mode 100644 index 00000000000..9873e946c1b --- /dev/null +++ b/docs/diagrams/AddSequenceDiagram.puml @@ -0,0 +1,95 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":AddCommandParser" as AddCommandParser LOGIC_COLOR +participant "<>\nArgumentTokenizer" as ArgumentTokenizer LOGIC_COLOR +participant "multi:ArgumentMultimap" as ArgumentMultimap LOGIC_COLOR +participant "<>\nParserUtil" as ParserUtil LOGIC_COLOR +participant "a:AddCommand" as AddCommand LOGIC_COLOR +end box + + + +box Model MODEL_COLOR_T1 +participant "p:Person" as Person MODEL_COLOR +end box + +[-> AddressBookParser : parseCommand(...) +activate AddressBookParser + +create AddCommandParser +AddressBookParser -> AddCommandParser +activate AddCommandParser + +AddCommandParser --> AddressBookParser +deactivate AddCommandParser + +AddressBookParser -> AddCommandParser : parse(...) +activate AddCommandParser + +'Tokenizer and ParserUtil dont have constructors, how? + +AddCommandParser -> ArgumentTokenizer : tokenize(...) +activate ArgumentTokenizer + +create ArgumentMultimap +ArgumentTokenizer -> ArgumentMultimap +activate ArgumentMultimap +ArgumentMultimap --> ArgumentTokenizer + +deactivate ArgumentMultimap + +ArgumentTokenizer --> AddCommandParser +deactivate ArgumentTokenizer + +AddCommandParser -> AddCommandParser : arePrefixesPresent(...) +activate AddCommandParser +AddCommandParser --> AddCommandParser +deactivate AddCommandParser + +AddCommandParser -> ArgumentMultimap : getPreamble() +activate ArgumentMultimap +ArgumentMultimap --> AddCommandParser +deactivate ArgumentMultimap + +AddCommandParser -> ArgumentMultimap : verifyNoDuplicatePrefixesFor(...) +activate ArgumentMultimap +ArgumentMultimap --> AddCommandParser + +deactivate ArgumentMultimap + +deactivate ArgumentTokenizer + +ref over AddCommandParser, ParserUtil : parse fields + +create Person +AddCommandParser -> Person +activate Person +Person --> AddCommandParser +deactivate Person + +create AddCommand +AddCommandParser -> AddCommand +activate AddCommand + +AddCommand --> AddCommandParser +deactivate AddCommand + +AddCommandParser --> AddressBookParser : a +deactivate AddCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +AddCommandParser -[hidden]-> AddressBookParser +destroy AddCommandParser +destroy ArgumentMultimap + +[<-- AddressBookParser : +deactivate AddressBookParser + +@enduml diff --git a/docs/diagrams/AutomaticTaggingActivityDiagram.puml b/docs/diagrams/AutomaticTaggingActivityDiagram.puml new file mode 100644 index 00000000000..8019702b3d0 --- /dev/null +++ b/docs/diagrams/AutomaticTaggingActivityDiagram.puml @@ -0,0 +1,13 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +start +:User adds a new person; + +if () then ([has Matric]) + :Add new "student" tag; +else ([else]) +endif +stop +@enduml diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml index 598474a5c82..4fbc44739e9 100644 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ b/docs/diagrams/BetterModelClassDiagram.puml @@ -6,6 +6,7 @@ skinparam classBackgroundColor MODEL_COLOR AddressBook *-right-> "1" UniquePersonList AddressBook *-right-> "1" UniqueTagList + UniqueTagList -[hidden]down- UniquePersonList UniqueTagList -[hidden]down- UniquePersonList @@ -18,4 +19,16 @@ Person *--> Name Person *--> Phone Person *--> Email Person *--> Address +Person *--> "0..1" Matric +Person *--> "0..1" Studio +Person *--> "0..1" Reflection +Person *--> "*" Score + +Name -[hidden]right-> Phone +Phone -[hidden]right-> Address +Address -[hidden]right-> Email +Email -[hidden]down-> Matric +Matric -[hidden]right-> Studio +Studio -[hidden]right-> Reflection + @enduml diff --git a/docs/diagrams/CopyImplementationActivityDiagram.puml b/docs/diagrams/CopyImplementationActivityDiagram.puml new file mode 100644 index 00000000000..297366b2184 --- /dev/null +++ b/docs/diagrams/CopyImplementationActivityDiagram.puml @@ -0,0 +1,19 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +start +:User executes copy command; +if () then ([persons are displayed]) + :Get emails of the displayed persons; + if () + :Copy emails to clipboard; + else ([no clipboard available]) + endif +else ([no persons displayed]) + :throw new CommandException("No person currently displayed"); +endif + +stop + +@enduml diff --git a/docs/diagrams/DeleteScoreExecutionSequenceDiagram.puml b/docs/diagrams/DeleteScoreExecutionSequenceDiagram.puml new file mode 100644 index 00000000000..d2a6d6d0869 --- /dev/null +++ b/docs/diagrams/DeleteScoreExecutionSequenceDiagram.puml @@ -0,0 +1,61 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":DeleteScoreCommand" as DeleteScoreCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "m:Model" as Model MODEL_COLOR +participant "selectedExam:Exam" as Exam MODEL_COLOR +participant "p:Person" as Person MODEL_COLOR +end box + +[-> DeleteScoreCommand : execute(m) +activate DeleteScoreCommand + +DeleteScoreCommand -> Model : getFilteredPersonList() +activate Model + +Model --> DeleteScoreCommand : list +deactivate Model + +DeleteScoreCommand -> Model : getSelectedExam() +activate Model + +create Exam +Model -> Exam : getValue() +activate Exam + +Exam --> Model +deactivate Exam + +Model --> DeleteScoreCommand : selectedExam +deactivate Model + +create Person +DeleteScoreCommand -> Person : list.get() +activate Person + +Person --> DeleteScoreCommand +deactivate Person + +DeleteScoreCommand -> Model : removeExamScoreFromPerson(p, selectedExam) +activate Model + +Model --> DeleteScoreCommand +deactivate Model + +create CommandResult +DeleteScoreCommand -> CommandResult +activate CommandResult + +CommandResult --> DeleteScoreCommand +deactivate CommandResult + +[<-- DeleteScoreCommand : r +deactivate DeleteScoreCommand + +@enduml diff --git a/docs/diagrams/DeleteScoreParsingSequenceDiagram.puml b/docs/diagrams/DeleteScoreParsingSequenceDiagram.puml new file mode 100644 index 00000000000..5cd7d95ca8d --- /dev/null +++ b/docs/diagrams/DeleteScoreParsingSequenceDiagram.puml @@ -0,0 +1,41 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":DeleteScoreCommandParser" as DeleteScoreCommandParser LOGIC_COLOR +participant "<>\nParserUtil" as ParserUtil LOGIC_COLOR +participant "d:DeleteScoreCommand" as DeleteScoreCommand LOGIC_COLOR +end box + +box Commons COMMONS_COLOR_T1 +participant "i:Index" as Index COMMONS_COLOR +end box + +[-> DeleteScoreCommandParser : parse("1") +activate DeleteScoreCommandParser + +DeleteScoreCommandParser -> ParserUtil : parseIndex("1") +activate ParserUtil + +create Index +ParserUtil -> Index +activate Index + +Index --> ParserUtil +deactivate Index + +ParserUtil --> DeleteScoreCommandParser : i +deactivate ParserUtil + +create DeleteScoreCommand +DeleteScoreCommandParser -> DeleteScoreCommand +activate DeleteScoreCommand + +DeleteScoreCommand --> DeleteScoreCommandParser +deactivate DeleteScoreCommand + +[<-- DeleteScoreCommandParser : d +deactivate DeleteScoreCommandParser + +@enduml diff --git a/docs/diagrams/DeleteShownActivityDiagram.puml b/docs/diagrams/DeleteShownActivityDiagram.puml new file mode 100644 index 00000000000..1dd3fa95ca6 --- /dev/null +++ b/docs/diagrams/DeleteShownActivityDiagram.puml @@ -0,0 +1,16 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +start +:User executes deleteShown command; +if () then ([filtered list equals list of all existing persons]) + :Indicate that all persons cannot be deleted; +else ([else]) +while () is ([there are persons left to delete]) + :Delete person; + endwhile; + -> [else]; +endif; +stop +@enduml diff --git a/docs/diagrams/EditCommandActivityDiagram.puml b/docs/diagrams/EditCommandActivityDiagram.puml new file mode 100644 index 00000000000..0c4883bb0ea --- /dev/null +++ b/docs/diagrams/EditCommandActivityDiagram.puml @@ -0,0 +1,29 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 + +start +:Edit command is parsed; + +if () then ([else]) + :Parse prefixes; + if () then ([else]) + :Execute Edit command; + if () is ([else]) then + if () is ([else]) then + :Edit person successfully; + else ([email is edited to a duplicate]) + :Reject; + endif + else ([no prefixes provided]) + :Reject; + endif + else ([repeated prefixes provided]) + :Reject; + endif +else ([invalid index]) + :Reject; +endif; +stop +@enduml diff --git a/docs/diagrams/ExportDataRetrievalSequenceDiagram.puml b/docs/diagrams/ExportDataRetrievalSequenceDiagram.puml new file mode 100644 index 00000000000..ea836df5b31 --- /dev/null +++ b/docs/diagrams/ExportDataRetrievalSequenceDiagram.puml @@ -0,0 +1,72 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":ExportCommand" as ExportCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Commons COMMONS_COLOR_T1 +participant "<>\nCsvUtil" as CsvUtil COMMONS_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "m:Model" as Model MODEL_COLOR +participant "a:AddressBook" as AddressBook MODEL_COLOR +end box + +[-> ExportCommand : execute(m) +activate ExportCommand + +ExportCommand -> Model : getFilteredPersonList() +activate Model + +Model --> ExportCommand : filteredPersons +deactivate Model + +create AddressBook +ExportCommand -> AddressBook : new AddressBook() +activate AddressBook + +AddressBook --> ExportCommand +deactivate AddressBook + +ExportCommand -> ExportCommand : addToAddressBook() +activate ExportCommand + +ExportCommand --> ExportCommand +deactivate ExportCommand + +ExportCommand -> ExportCommand : writeToJsonFile() +activate ExportCommand + +ExportCommand --> ExportCommand +deactivate ExportCommand + +ExportCommand -> Model : getAddressBookFilePath() +activate Model + +Model --> ExportCommand +deactivate Model + + +ref over ExportCommand, CsvUtil + JSON file handling and CSV conversion +end ref + +create CommandResult +ExportCommand -> CommandResult +activate CommandResult + +CommandResult --> ExportCommand +deactivate CommandResult + +[<-- ExportCommand : r +deactivate ExportCommand + +@enduml diff --git a/docs/diagrams/ExportSequenceDiagram.puml b/docs/diagrams/ExportSequenceDiagram.puml new file mode 100644 index 00000000000..6aac2ebb000 --- /dev/null +++ b/docs/diagrams/ExportSequenceDiagram.puml @@ -0,0 +1,51 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":ExportCommand" as ExportCommand LOGIC_COLOR +end box + +box Commons COMMONS_COLOR_T1 +participant "<>\nCsvUtil" as CsvUtil COMMONS_COLOR +end box + +mainframe **sd** JSON file handling and CSV conversion + +ExportCommand -> ExportCommand : readJsonFile() +activate ExportCommand + +ExportCommand --> ExportCommand +deactivate ExportCommand + +ExportCommand -> ExportCommand : readPersonsArray() +activate ExportCommand + +ExportCommand --> ExportCommand +deactivate ExportCommand + +ExportCommand -> ExportCommand : readExamsArray() +activate ExportCommand + +ExportCommand --> ExportCommand +deactivate ExportCommand + +ExportCommand -> ExportCommand : createCsvDirectory() +activate ExportCommand + +ExportCommand --> ExportCommand +deactivate ExportCommand + +ExportCommand -> CsvUtil : writeToCsvFile() +activate CsvUtil + +CsvUtil -> CsvUtil : buildCsvSchema() +activate CsvUtil + +CsvUtil --> CsvUtil +deactivate CsvUtil + +CsvUtil --> ExportCommand +deactivate CsvUtil + +@enduml diff --git a/docs/diagrams/FindImplementationActivityDiagram.puml b/docs/diagrams/FindImplementationActivityDiagram.puml new file mode 100644 index 00000000000..9410031d5d4 --- /dev/null +++ b/docs/diagrams/FindImplementationActivityDiagram.puml @@ -0,0 +1,41 @@ +@startuml +' rake symbol from https://forum.plantuml.net/195/is-there-any-support-for-subactivity-or-the-rake-symbol +sprite $rake [16x16/8] { +0000000000000000 +0000000jj0000000 +0000000jj0000000 +0005555jj5555000 +000jjeejjeejj000 +000jj00jj00jj000 +000jj00jj00jj000 +0000000000000000 +} + +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 + +title Activity: find command execution +start +:User executes find command; + +if () then ([command is in invalid format]) + :Show invalid command format as error message; + + +else ([else]) + if () then ([prefix is mt| or lt|]) +' if () then ([invalid score provided]) +' :Show invalid value as error message; +' stop +' else ([else]) +' endif + :find by score <$rake>; + else ([else]) + :Displays the students that match the search criteria; + endif +endif +stop + + +@enduml diff --git a/docs/diagrams/FindImplementationFindByScoreActivityDiagram.puml b/docs/diagrams/FindImplementationFindByScoreActivityDiagram.puml new file mode 100644 index 00000000000..58e19a82346 --- /dev/null +++ b/docs/diagrams/FindImplementationFindByScoreActivityDiagram.puml @@ -0,0 +1,21 @@ +@startuml + +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +title Activity: find by score +start + +if () then ([else]) + :Show no exam selected as error message; +else ([exam selected]) + if () then ([else]) + :Show score is invalid as error message; + else ([score is valid]) + :Displays the students that match the search criteria; + endif +endif +stop + + +@enduml diff --git a/docs/diagrams/FindImplementationPredicateCreationSequenceDiagram.puml b/docs/diagrams/FindImplementationPredicateCreationSequenceDiagram.puml new file mode 100644 index 00000000000..b8c8b8623d4 --- /dev/null +++ b/docs/diagrams/FindImplementationPredicateCreationSequenceDiagram.puml @@ -0,0 +1,58 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant "f:FindCommand" as FindCommand LOGIC_COLOR +participant "p:PersonDetailPredicate" as PersonDetailPredicate LOGIC_COLOR +participant "p:ExamPredicate" as ExamPredicate LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "m:Model" as Model MODEL_COLOR +end box + +mainframe **sd** create predicate and update the filtered person list + +' Create an alt frame to show the predicate creation +alt prefix is mt| or lt| + create ExamPredicate + FindCommand -> ExamPredicate : ExamPredicate(...) + activate ExamPredicate + + ExamPredicate --> FindCommand + deactivate ExamPredicate + + FindCommand -> Model : updateFilteredPersonList(p) + activate Model + + Model --> FindCommand + deactivate Model + ' Hidden arrow to position the destroy marker below the end of the activation bar. + Model -[hidden]-> ExamPredicate + destroy ExamPredicate + ' Hidden arrow to add space between the destroy marker and the border of the alt frame + Model -[hidden]-> ExamPredicate + +else else + create PersonDetailPredicate + FindCommand -> PersonDetailPredicate : PersonDetailPredicate(...) + activate PersonDetailPredicate + + PersonDetailPredicate --> FindCommand + deactivate PersonDetailPredicate + + FindCommand -> Model : updateFilteredPersonList(p) + activate Model + + Model --> FindCommand + deactivate Model + ' Hidden arrow to position the destroy marker below the end of the activation bar. + Model -[hidden]-> PersonDetailPredicate + destroy PersonDetailPredicate + ' Hidden arrow to add space between the destroy marker and the border of the alt frame + Model -[hidden]-> PersonDetailPredicate + +end + +@enduml diff --git a/docs/diagrams/FindImplementationSequenceDiagram.puml b/docs/diagrams/FindImplementationSequenceDiagram.puml new file mode 100644 index 00000000000..f883bdfbc5e --- /dev/null +++ b/docs/diagrams/FindImplementationSequenceDiagram.puml @@ -0,0 +1,71 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":FindCommandParser" as FindCommandParser LOGIC_COLOR +participant "f:FindCommand" as FindCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "m:Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("find n|Alice") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("find n|Alice") +activate AddressBookParser + +create FindCommandParser +AddressBookParser -> FindCommandParser +activate FindCommandParser + +FindCommandParser --> AddressBookParser +deactivate FindCommandParser + +AddressBookParser -> FindCommandParser : parse("n|Alice") +activate FindCommandParser + +create FindCommand +FindCommandParser -> FindCommand : FindCommand(...) +activate FindCommand + +FindCommand --> FindCommandParser : +deactivate FindCommand + +FindCommandParser --> AddressBookParser : f +deactivate FindCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +FindCommandParser -[hidden]-> AddressBookParser +destroy FindCommandParser + +AddressBookParser --> LogicManager : f +deactivate AddressBookParser + +LogicManager -> FindCommand : execute(m) +activate FindCommand + + +ref over FindCommand, Model : create predicate and update the filtered person list + +create CommandResult +FindCommand -> CommandResult +activate CommandResult + +CommandResult --> FindCommand +deactivate CommandResult + +FindCommand --> LogicManager : r +deactivate FindCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/ImportExamScoresFileActivityDiagram.puml b/docs/diagrams/ImportExamScoresFileActivityDiagram.puml new file mode 100644 index 00000000000..7e333552f0a --- /dev/null +++ b/docs/diagrams/ImportExamScoresFileActivityDiagram.puml @@ -0,0 +1,26 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 + +start +:Filepath input is parsed; +if () is ([file is a .csv file]) then + :importExamScores command is executed with the filepath; + if () is ([no exception is thrown]) then + :Read the rows of the CSV file; + if () is ([email header exists only in the first column]) then + :Start adding scores; + else ([else]) + :Reject; + endif + else ([else]) + :Reject; + endif +else ([else]) + :Reject; +endif +stop + + +@enduml diff --git a/docs/diagrams/ImportParserSequenceDiagram.puml b/docs/diagrams/ImportParserSequenceDiagram.puml new file mode 100644 index 00000000000..301b50066dd --- /dev/null +++ b/docs/diagrams/ImportParserSequenceDiagram.puml @@ -0,0 +1,48 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":ImportCommandParser" as ImportCommandParser LOGIC_COLOR +participant "<>\nArgumentTokenizer" as ArgumentTokenizer LOGIC_COLOR +participant "multi:ArgumentMultimap" as ArgumentMultimap LOGIC_COLOR +participant "<>\nParserUtil" as ParserUtil LOGIC_COLOR +end box + +-> ImportCommandParser : parse(...) +activate ImportCommandParser + +'Tokenizer and ParserUtil dont have constructors, how? + +ImportCommandParser -> ArgumentTokenizer : tokenize(...) +activate ArgumentTokenizer + +create ArgumentMultimap +ArgumentTokenizer -> ArgumentMultimap +activate ArgumentMultimap +ArgumentMultimap --> ArgumentTokenizer + +deactivate ArgumentMultimap + +ArgumentTokenizer --> ImportCommandParser +deactivate ArgumentTokenizer + +ImportCommandParser -> ImportCommandParser : isPrefixPresent(...) +activate ImportCommandParser +ImportCommandParser --> ImportCommandParser +deactivate ImportCommandParser + +ImportCommandParser -> ArgumentMultimap : getPreamble() +activate ArgumentMultimap +ArgumentMultimap --> ImportCommandParser +deactivate ArgumentMultimap + +ImportCommandParser -> ParserUtil : parseFilePath(...) +activate ParserUtil +ParserUtil --> ImportCommandParser : filePath +deactivate ParserUtil + +<-- ImportCommandParser : ImportCommand(filePath) +deactivate ImportCommandParser + +@enduml diff --git a/docs/diagrams/ImportSequenceDiagram.puml b/docs/diagrams/ImportSequenceDiagram.puml new file mode 100644 index 00000000000..1010023e5fb --- /dev/null +++ b/docs/diagrams/ImportSequenceDiagram.puml @@ -0,0 +1,75 @@ +›@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant "i:ImportCommand" as ImportCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +participant "a:AddCommandParser" as AddCommandParser LOGIC_COLOR +participant "a:AddCommand" as AddCommand LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "p:Person" as Person MODEL_COLOR +participant "m:Model" as Model MODEL_COLOR +end box + +box Commons COMMONS_COLOR_T1 +participant "<>\nCsvUtil" as CsvUtil COMMONS_COLOR +end box + + -> ImportCommand : execute(m) +activate ImportCommand + +ImportCommand -> CsvUtil : readCsvFile() +activate CsvUtil +CsvUtil --> ImportCommand : personsData +deactivate CsvUtil + +ImportCommand -> ImportCommand : addToModel(personsData) +activate ImportCommand + +loop personData in personsData + +ImportCommand -> ImportCommand : convertToAddCommandInput(personData) +activate ImportCommand +ImportCommand --> ImportCommand : addCommandInput +deactivate ImportCommand + +ImportCommand -> AddCommandParser : parse(addCommandInput) +activate AddCommandParser +create AddCommand +AddCommandParser -> AddCommand : new AddCommand() +activate AddCommand +AddCommand --> AddCommandParser +deactivate AddCommand +AddCommandParser --> ImportCommand +deactivate AddCommandParser + +ImportCommand -> AddCommand +activate AddCommand + +ref over AddCommand, Person, Model : Add Command Execution +AddCommand --> ImportCommand +deactivate AddCommand +AddCommand -[Hidden]-> ImportCommand +destroy AddCommand +end +ImportCommand --> ImportCommand +deactivate ImportCommand + +create CommandResult +ImportCommand -> CommandResult +activate CommandResult + +CommandResult --> ImportCommand: CommandResult +deactivate CommandResult + + <-- ImportCommand : CommandResult +deactivate ImportCommand + +@enduml diff --git a/docs/diagrams/ImportSequenceDiagramRef.puml b/docs/diagrams/ImportSequenceDiagramRef.puml new file mode 100644 index 00000000000..e09bfa47ea2 --- /dev/null +++ b/docs/diagrams/ImportSequenceDiagramRef.puml @@ -0,0 +1,37 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant "i:ImportCommand" as ImportCommand LOGIC_COLOR +participant "a:AddCommand" as AddCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "p:Person" as Person MODEL_COLOR +participant "m:Model" as Model MODEL_COLOR +end box + +mainFrame sd Add Command Execution +ImportCommand -> AddCommand : execute(m) +activate AddCommand + +AddCommand -> Model : addPerson(p) +activate Model + +Model --> AddCommand +deactivate Model + +create CommandResult +AddCommand -> CommandResult +activate CommandResult + +CommandResult --> AddCommand +deactivate CommandResult + +AddCommand --> ImportCommand : r +deactivate AddCommand +AddCommand -[hidden]-> ImportCommand +destroy AddCommand +@enduml diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 0de5673070d..7a549002119 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -13,12 +13,18 @@ Class ModelManager Class UserPrefs Class UniquePersonList +Class UniqueExamList Class Person Class Address Class Email Class Name Class Phone +Class Matric +Class Studio +Class Reflection Class Tag +Class Exam +Class Score Class I #FFFFFF } @@ -36,19 +42,30 @@ ModelManager -right-> "1" UserPrefs UserPrefs .up.|> ReadOnlyUserPrefs AddressBook *--> "1" UniquePersonList -UniquePersonList --> "~* all" Person -Person *--> Name +AddressBook *--> "1" UniqueExamList +UniquePersonList -[hidden]left-> UniqueExamList +UniquePersonList --> " ~* all" Person +UniqueExamList --> "*" Exam +Person *-l-> Name Person *--> Phone Person *--> Email Person *--> Address -Person *--> "*" Tag +Person *--> "0..1" Matric +Person *--> "0..1" Studio +Person *--> " 0..1" Reflection +Person *-r-> "*" Tag +Person *--> "*" Score Person -[hidden]up--> I UniquePersonList -[hidden]right-> I -Name -[hidden]right-> Phone +Name -[hidden]down-> Phone Phone -[hidden]right-> Address Address -[hidden]right-> Email +Email -[hidden]right-> Matric +Matric -[hidden]right-> Studio +Studio -[hidden]right-> Reflection +Reflection -[hidden]up-> Tag -ModelManager --> "~* filtered" Person +ModelManager --> "~* filtered " Person @enduml diff --git a/docs/diagrams/SelectExamSequenceDiagram.puml b/docs/diagrams/SelectExamSequenceDiagram.puml new file mode 100644 index 00000000000..9f3f0b36c27 --- /dev/null +++ b/docs/diagrams/SelectExamSequenceDiagram.puml @@ -0,0 +1,53 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 + +participant ":SelectExamCommand" as SelectExamCommand LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "m:Model" as Model MODEL_COLOR +participant ":AddressBook" as AddressBook MODEL_COLOR +participant "exams:UniqueExamList" as UniqueExamList MODEL_COLOR +participant ":ObservableList" as ObservableList MODEL_COLOR +end box + +[-> SelectExamCommand : execute(m) +activate SelectExamCommand + +SelectExamCommand -> Model : getExamList() +activate Model + +Model -> AddressBook : getExamList() +activate AddressBook + +AddressBook -> UniqueExamList : asUnmodifiableObservableList() +activate UniqueExamList + +UniqueExamList --> AddressBook : ObservableList +deactivate UniqueExamList + +AddressBook --> Model : ObservableList +deactivate AddressBook + +Model --> SelectExamCommand : ObservableList +deactivate Model + +SelectExamCommand -> ObservableList : get(targetIndex.getZeroBased()) +activate ObservableList + +ObservableList --> SelectExamCommand : exam +deactivate ObservableList + +SelectExamCommand -> Model : selectExam(exam) +activate Model + +Model --> SelectExamCommand : +deactivate Model + +[<-- SelectExamCommand : CommandResult +deactivate SelectExamCommand + +@enduml diff --git a/docs/diagrams/StatisticsSequenceDiagram.puml b/docs/diagrams/StatisticsSequenceDiagram.puml new file mode 100644 index 00000000000..307289639cd --- /dev/null +++ b/docs/diagrams/StatisticsSequenceDiagram.puml @@ -0,0 +1,41 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Model MODEL_COLOR_T1 +participant "m:ModelManager" as Model MODEL_COLOR +participant "selectedExam:SimpleObjectProperty" as selectedExam MODEL_COLOR +participant "selectedExamStatistics:SimpleObjectProperty" as selectedExamStatistics MODEL_COLOR +end box + +[-> Model : selectExam(e) +activate Model + +Model -> selectedExam : set(e) +activate selectedExam + +selectedExam --> Model +deactivate selectedExam + +Model -> Model : updateSelectedExamStatistics() +activate Model + +Model -> Model : calculateExamScoreStatistics(selectedExam) +activate Model + +Model --> Model : Statistics +deactivate Model + +Model -> selectedExamStatistics : set(Statistics) +activate selectedExamStatistics + +selectedExamStatistics --> Model +deactivate selectedExamStatistics + +Model --> Model +deactivate Model + +[<-- Model +deactivate Model + +@enduml diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index a821e06458c..c8f446f9d4b 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -20,6 +20,8 @@ Class JsonAddressBookStorage Class JsonSerializableAddressBook Class JsonAdaptedPerson Class JsonAdaptedTag +Class JsonAdaptedExam +Class JsonAdaptedExamScore } } @@ -39,5 +41,7 @@ JsonAddressBookStorage .up.|> AddressBookStorage JsonAddressBookStorage ..> JsonSerializableAddressBook JsonSerializableAddressBook --> "*" JsonAdaptedPerson JsonAdaptedPerson --> "*" JsonAdaptedTag +JsonAdaptedPerson --> "*" JsonAdaptedExamScore +JsonSerializableAddressBook --> "*" JsonAdaptedExam @enduml diff --git a/docs/diagrams/StorageLoadSequenceDiagram.puml b/docs/diagrams/StorageLoadSequenceDiagram.puml new file mode 100644 index 00000000000..6c205c9c272 --- /dev/null +++ b/docs/diagrams/StorageLoadSequenceDiagram.puml @@ -0,0 +1,47 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Main +participant ":MainApp" as MainApp #Grey +end box + +box Storage STORAGE_COLOR_T1 +participant ":StorageManager" as StorageManager STORAGE_COLOR +participant ":JsonAddressBookStorage" as JsonAddressBookStorage STORAGE_COLOR +participant "<>\nJsonUtil" as JsonUtil STORAGE_COLOR +end box + +[-> MainApp : init() +activate MainApp + +MainApp -> MainApp : initModelManager(...) +activate MainApp + +MainApp -> StorageManager : readAddressBook() +activate StorageManager + +StorageManager -> JsonAddressBookStorage : readAddressBook(filePath) +activate JsonAddressBookStorage + +JsonAddressBookStorage -> JsonUtil : readJsonFile(filePath) +activate JsonUtil + +JsonUtil --> JsonAddressBookStorage : Optional +deactivate JsonUtil + +JsonAddressBookStorage --> StorageManager : Optional +deactivate JsonAddressBookStorage + +StorageManager --> MainApp : Optional +deactivate StorageManager + +MainApp --> MainApp : model +deactivate MainApp + +[<-- MainApp +deactivate StorageManager + +deactivate MainApp + +@enduml diff --git a/docs/diagrams/StorageSequenceDiagram.puml b/docs/diagrams/StorageSequenceDiagram.puml new file mode 100644 index 00000000000..f9ac06205cb --- /dev/null +++ b/docs/diagrams/StorageSequenceDiagram.puml @@ -0,0 +1,49 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":Logic" as Logic LOGIC_COLOR +end box + +box Storage STORAGE_COLOR_T1 +participant ":StorageManager" as StorageManager STORAGE_COLOR +participant ":JsonAddressBookStorage" as JsonAddressBookStorage STORAGE_COLOR +participant "j:JsonSerializableAddressBook" as JsonSerializableAddressBook STORAGE_COLOR +participant "<>\nJsonUtil" as JsonUtil STORAGE_COLOR +end box + +[-> Logic : execute(userInput) +activate Logic + +Logic -> StorageManager : saveAddressBook(addressBook) +activate StorageManager + +StorageManager -> JsonAddressBookStorage : saveAddressBook(addressBook, filePath) +activate JsonAddressBookStorage + +create JsonSerializableAddressBook +JsonAddressBookStorage -> JsonSerializableAddressBook +activate JsonSerializableAddressBook + +JsonSerializableAddressBook --> JsonAddressBookStorage : j +deactivate JsonSerializableAddressBook + +JsonAddressBookStorage -> JsonUtil : saveJsonToFile(j, filePath) +activate JsonUtil + +JsonUtil --> JsonAddressBookStorage +deactivate JsonUtil + +JsonAddressBookStorage --> StorageManager +deactivate JsonAddressBookStorage +destroy JsonSerializableAddressBook + +StorageManager --> Logic +deactivate StorageManager + +[<-- Logic +deactivate Logic + + +@enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..94ca7b27725 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -6,13 +6,14 @@ skinparam classBackgroundColor UI_COLOR package UI <>{ Class "<>\nUi" as Ui -Class "{abstract}\nUiPart" as UiPart Class UiManager Class MainWindow Class HelpWindow Class ResultDisplay Class PersonListPanel +Class ExamListPanel Class PersonCard +Class ExamCard Class StatusBarFooter Class CommandBox } @@ -31,30 +32,22 @@ HiddenOutside ..> Ui UiManager .left.|> Ui UiManager -down-> "1" MainWindow MainWindow *-down-> "1" CommandBox -MainWindow *-down-> "1" ResultDisplay +MainWindow *-left-> "1" ResultDisplay MainWindow *-down-> "1" PersonListPanel MainWindow *-down-> "1" StatusBarFooter -MainWindow --> "0..1" HelpWindow +MainWindow *-down-> "1" ExamListPanel PersonListPanel -down-> "*" PersonCard +ExamListPanel -down-> "*" ExamCard +ExamListPanel -[hidden]down-> StatusBarFooter -MainWindow -left-|> UiPart +ResultDisplay -[hidden]down-> PersonListPanel -ResultDisplay --|> UiPart -CommandBox --|> UiPart -PersonListPanel --|> UiPart -PersonCard --|> UiPart -StatusBarFooter --|> UiPart -HelpWindow --|> UiPart +PersonCard .down.> Model +ExamCard .down.> Model +StatusBarFooter .down.> Model -PersonCard ..> Model UiManager -right-> Logic MainWindow -left-> Logic -PersonListPanel -[hidden]left- HelpWindow -HelpWindow -[hidden]left- CommandBox -CommandBox -[hidden]left- ResultDisplay -ResultDisplay -[hidden]left- StatusBarFooter - -MainWindow -[hidden]-|> UiPart @enduml diff --git a/docs/diagrams/UiSequenceDiagram.puml b/docs/diagrams/UiSequenceDiagram.puml new file mode 100644 index 00000000000..a63438a4f57 --- /dev/null +++ b/docs/diagrams/UiSequenceDiagram.puml @@ -0,0 +1,59 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box UI UI_COLOR_T1 +participant ":CommandBox" as CommandBox UI_COLOR +participant ":MainWindow" as MainWindow UI_COLOR +participant ":ResultDisplay" as ResultDisplay UI_COLOR +participant ":PersonListPanel" as PersonListPanel UI_COLOR +participant ":ExamListPanel" as ExamListPanel UI_COLOR +participant ":StatusBarFooter" as StatusBarFooter UI_COLOR +end box + +box Logic LOGIC_COLOR_T1 +participant ":Logic" as Logic LOGIC_COLOR +end box + +[-> CommandBox : userInput +activate CommandBox + +CommandBox -> MainWindow : execute(userInput) +activate MainWindow + +MainWindow -> Logic : execute(userInput) +activate Logic + +Logic --> MainWindow : commandResult +deactivate Logic + +MainWindow -> ResultDisplay : setFeedbackToUser(...) +activate ResultDisplay + +ResultDisplay --> MainWindow +deactivate ResultDisplay + +MainWindow -> PersonListPanel : update() +activate PersonListPanel + +PersonListPanel --> MainWindow +deactivate PersonListPanel + +MainWindow -> ExamListPanel : update() +activate ExamListPanel + +ExamListPanel --> MainWindow +deactivate ExamListPanel + +MainWindow -> StatusBarFooter : update() +activate StatusBarFooter + +StatusBarFooter --> MainWindow +deactivate StatusBarFooter + +MainWindow --> CommandBox +deactivate MainWindow + +[<-- CommandBox +deactivate CommandBox +@enduml diff --git a/docs/diagrams/style.puml b/docs/diagrams/style.puml index f7d7347ae84..e9ad648a00c 100644 --- a/docs/diagrams/style.puml +++ b/docs/diagrams/style.puml @@ -25,12 +25,18 @@ !define MODEL_COLOR_T3 #7B000E !define MODEL_COLOR_T4 #51000A -!define STORAGE_COLOR #A38300 -!define STORAGE_COLOR_T1 #FFE374 +!define STORAGE_COLOR #674EA7 +!define STORAGE_COLOR_T1 #8E7CC3 !define STORAGE_COLOR_T2 #EDC520 !define STORAGE_COLOR_T3 #806600 !define STORAGE_COLOR_T2 #544400 +!define COMMONS_COLOR #FF5733 +!define COMMONS_COLOR_T1 #FFAB89 +!define COMMONS_COLOR_T2 #E84108 +!define COMMONS_COLOR_T3 #C23500 +!define COMMONS_COLOR_T2 #8F2000 + !define USER_COLOR #000000 skinparam Package { @@ -58,7 +64,7 @@ skinparam Sequence { MessageAlign center BoxFontSize 15 BoxPadding 0 - BoxFontColor #FFFFFF + BoxFontColor #000000 FontName Arial } diff --git a/docs/images/AaLogo.png b/docs/images/AaLogo.png new file mode 100644 index 00000000000..55f711ef9f9 Binary files /dev/null and b/docs/images/AaLogo.png differ diff --git a/docs/images/ArchitectureDiagram.png b/docs/images/ArchitectureDiagram.png deleted file mode 100644 index cd540665053..00000000000 Binary files a/docs/images/ArchitectureDiagram.png and /dev/null differ diff --git a/docs/images/ArchitectureSequenceDiagram.png b/docs/images/ArchitectureSequenceDiagram.png deleted file mode 100644 index 37ad06a2803..00000000000 Binary files a/docs/images/ArchitectureSequenceDiagram.png and /dev/null differ diff --git a/docs/images/BetterModelClassDiagram.png b/docs/images/BetterModelClassDiagram.png deleted file mode 100644 index 02a42e35e76..00000000000 Binary files a/docs/images/BetterModelClassDiagram.png and /dev/null differ diff --git a/docs/images/CommitActivityDiagram.png b/docs/images/CommitActivityDiagram.png deleted file mode 100644 index 5b464126b35..00000000000 Binary files a/docs/images/CommitActivityDiagram.png and /dev/null differ diff --git a/docs/images/ComponentManagers.png b/docs/images/ComponentManagers.png deleted file mode 100644 index ae52a35718a..00000000000 Binary files a/docs/images/ComponentManagers.png and /dev/null differ diff --git a/docs/images/DeleteSequenceDiagram.png b/docs/images/DeleteSequenceDiagram.png deleted file mode 100644 index ac2ae217c51..00000000000 Binary files a/docs/images/DeleteSequenceDiagram.png and /dev/null differ diff --git a/docs/images/LogicClassDiagram.png b/docs/images/LogicClassDiagram.png deleted file mode 100644 index fe91c69efe7..00000000000 Binary files a/docs/images/LogicClassDiagram.png and /dev/null differ diff --git a/docs/images/LogicStorageDIP.png b/docs/images/LogicStorageDIP.png deleted file mode 100644 index 871157f5a9c..00000000000 Binary files a/docs/images/LogicStorageDIP.png and /dev/null differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png deleted file mode 100644 index a19fb1b4ac8..00000000000 Binary files a/docs/images/ModelClassDiagram.png and /dev/null differ diff --git a/docs/images/ParserClasses.png b/docs/images/ParserClasses.png deleted file mode 100644 index 2caeeb1a067..00000000000 Binary files a/docs/images/ParserClasses.png and /dev/null differ diff --git a/docs/images/SeEduLogo.png b/docs/images/SeEduLogo.png deleted file mode 100644 index 31ad50b6f88..00000000000 Binary files a/docs/images/SeEduLogo.png and /dev/null differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png deleted file mode 100644 index 18fa4d0d51f..00000000000 Binary files a/docs/images/StorageClassDiagram.png and /dev/null differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..ceeb6b6ce09 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/UiClassDiagram.png b/docs/images/UiClassDiagram.png deleted file mode 100644 index 11f06d68671..00000000000 Binary files a/docs/images/UiClassDiagram.png and /dev/null differ diff --git a/docs/images/UiOutline.png b/docs/images/UiOutline.png new file mode 100644 index 00000000000..5a2851ea1e3 Binary files /dev/null and b/docs/images/UiOutline.png differ diff --git a/docs/images/UndoRedoState0.png b/docs/images/UndoRedoState0.png deleted file mode 100644 index c5f91b58533..00000000000 Binary files a/docs/images/UndoRedoState0.png and /dev/null differ diff --git a/docs/images/UndoRedoState1.png b/docs/images/UndoRedoState1.png deleted file mode 100644 index 2d3ad09c047..00000000000 Binary files a/docs/images/UndoRedoState1.png and /dev/null differ diff --git a/docs/images/UndoRedoState2.png b/docs/images/UndoRedoState2.png deleted file mode 100644 index 20853694e03..00000000000 Binary files a/docs/images/UndoRedoState2.png and /dev/null differ diff --git a/docs/images/UndoRedoState3.png b/docs/images/UndoRedoState3.png deleted file mode 100644 index 1a9551b31be..00000000000 Binary files a/docs/images/UndoRedoState3.png and /dev/null differ diff --git a/docs/images/UndoRedoState4.png b/docs/images/UndoRedoState4.png deleted file mode 100644 index 46dfae78c94..00000000000 Binary files a/docs/images/UndoRedoState4.png and /dev/null differ diff --git a/docs/images/UndoRedoState5.png b/docs/images/UndoRedoState5.png deleted file mode 100644 index f45889b5fdf..00000000000 Binary files a/docs/images/UndoRedoState5.png and /dev/null differ diff --git a/docs/images/danielleloh.png b/docs/images/danielleloh.png new file mode 100644 index 00000000000..5e7d9bd8f5f Binary files /dev/null and b/docs/images/danielleloh.png differ diff --git a/docs/images/delishad21.png b/docs/images/delishad21.png new file mode 100644 index 00000000000..de98b97684a Binary files /dev/null and b/docs/images/delishad21.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png index b1f70470137..b292db6a2e8 100644 Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ diff --git a/docs/images/jayllo-c.png b/docs/images/jayllo-c.png new file mode 100644 index 00000000000..20588398a26 Binary files /dev/null and b/docs/images/jayllo-c.png differ diff --git a/docs/images/pughal77.png b/docs/images/pughal77.png new file mode 100644 index 00000000000..99b3253e877 Binary files /dev/null and b/docs/images/pughal77.png differ diff --git a/docs/images/success_images/Ui.png b/docs/images/success_images/Ui.png new file mode 100644 index 00000000000..ceeb6b6ce09 Binary files /dev/null and b/docs/images/success_images/Ui.png differ diff --git a/docs/images/success_images/addExam_success.png b/docs/images/success_images/addExam_success.png new file mode 100644 index 00000000000..6f0f0232165 Binary files /dev/null and b/docs/images/success_images/addExam_success.png differ diff --git a/docs/images/success_images/addScore_success.png b/docs/images/success_images/addScore_success.png new file mode 100644 index 00000000000..88442ff46ef Binary files /dev/null and b/docs/images/success_images/addScore_success.png differ diff --git a/docs/images/success_images/add_success.png b/docs/images/success_images/add_success.png new file mode 100644 index 00000000000..4858b4bf33e Binary files /dev/null and b/docs/images/success_images/add_success.png differ diff --git a/docs/images/success_images/clear_success.png b/docs/images/success_images/clear_success.png new file mode 100644 index 00000000000..332e7df4ba2 Binary files /dev/null and b/docs/images/success_images/clear_success.png differ diff --git a/docs/images/success_images/copy_success.png b/docs/images/success_images/copy_success.png new file mode 100644 index 00000000000..8a6061b5fa6 Binary files /dev/null and b/docs/images/success_images/copy_success.png differ diff --git a/docs/images/success_images/deleteExam_success.png b/docs/images/success_images/deleteExam_success.png new file mode 100644 index 00000000000..0b8d3945482 Binary files /dev/null and b/docs/images/success_images/deleteExam_success.png differ diff --git a/docs/images/success_images/deleteScore_success.png b/docs/images/success_images/deleteScore_success.png new file mode 100644 index 00000000000..f2e82d05d76 Binary files /dev/null and b/docs/images/success_images/deleteScore_success.png differ diff --git a/docs/images/success_images/deleteShown_success.png b/docs/images/success_images/deleteShown_success.png new file mode 100644 index 00000000000..5334a5028cf Binary files /dev/null and b/docs/images/success_images/deleteShown_success.png differ diff --git a/docs/images/success_images/delete_success.png b/docs/images/success_images/delete_success.png new file mode 100644 index 00000000000..cc6dbf6a36c Binary files /dev/null and b/docs/images/success_images/delete_success.png differ diff --git a/docs/images/success_images/deselectExam_success.png b/docs/images/success_images/deselectExam_success.png new file mode 100644 index 00000000000..35488bfd176 Binary files /dev/null and b/docs/images/success_images/deselectExam_success.png differ diff --git a/docs/images/success_images/editScore_success.png b/docs/images/success_images/editScore_success.png new file mode 100644 index 00000000000..7e2cb0ab726 Binary files /dev/null and b/docs/images/success_images/editScore_success.png differ diff --git a/docs/images/success_images/edit_success.png b/docs/images/success_images/edit_success.png new file mode 100644 index 00000000000..ed20aed2ee8 Binary files /dev/null and b/docs/images/success_images/edit_success.png differ diff --git a/docs/images/success_images/export_success.png b/docs/images/success_images/export_success.png new file mode 100644 index 00000000000..f99473fd3e8 Binary files /dev/null and b/docs/images/success_images/export_success.png differ diff --git a/docs/images/success_images/find_success.png b/docs/images/success_images/find_success.png new file mode 100644 index 00000000000..0d5249fd152 Binary files /dev/null and b/docs/images/success_images/find_success.png differ diff --git a/docs/images/success_images/help_success.png b/docs/images/success_images/help_success.png new file mode 100644 index 00000000000..1e25920dcff Binary files /dev/null and b/docs/images/success_images/help_success.png differ diff --git a/docs/images/success_images/importExam_success.png b/docs/images/success_images/importExam_success.png new file mode 100644 index 00000000000..1b6e09cf268 Binary files /dev/null and b/docs/images/success_images/importExam_success.png differ diff --git a/docs/images/success_images/import_success.png b/docs/images/success_images/import_success.png new file mode 100644 index 00000000000..c52deb1c7e6 Binary files /dev/null and b/docs/images/success_images/import_success.png differ diff --git a/docs/images/success_images/list_success.png b/docs/images/success_images/list_success.png new file mode 100644 index 00000000000..f4b9b788273 Binary files /dev/null and b/docs/images/success_images/list_success.png differ diff --git a/docs/images/success_images/mean_median_success.png b/docs/images/success_images/mean_median_success.png new file mode 100644 index 00000000000..049bea549be Binary files /dev/null and b/docs/images/success_images/mean_median_success.png differ diff --git a/docs/images/success_images/selectExam_success.png b/docs/images/success_images/selectExam_success.png new file mode 100644 index 00000000000..f173ebeb882 Binary files /dev/null and b/docs/images/success_images/selectExam_success.png differ diff --git a/docs/images/tracing/LogicSequenceDiagram.png b/docs/images/tracing/LogicSequenceDiagram.png deleted file mode 100644 index 25c8b66b9f1..00000000000 Binary files a/docs/images/tracing/LogicSequenceDiagram.png and /dev/null differ diff --git a/docs/images/zer0legion.png b/docs/images/zer0legion.png new file mode 100644 index 00000000000..0d9be79eb86 Binary files /dev/null and b/docs/images/zer0legion.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..f9db667c1a4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,19 +1,21 @@ --- -layout: page -title: AddressBook Level-3 + layout: default.md + title: "" --- -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) -[![codecov](https://codecov.io/gh/se-edu/addressbook-level3/branch/master/graph/badge.svg)](https://codecov.io/gh/se-edu/addressbook-level3) +# Avengers Assemble + +[![CI Status](https://github.com/AY2324S2-CS2103T-T10-1/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2324S2-CS2103T-T10-1/tp/actions) +[![codecov](https://codecov.io/gh/AY2324S2-CS2103T-T10-1/tp/graph/badge.svg?token=6NGZ4VS4VC)](https://app.codecov.io/gh/AY2324S2-CS2103T-T10-1/tp) ![Ui](images/Ui.png) -**AddressBook is a desktop application for managing your contact details.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). +**Avengers Assemble is a desktop application for managing your contact details.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). -* If you are interested in using AddressBook, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). -* If you are interested about developing AddressBook, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. +* If you are interested in using Avengers Assemble, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +* If you are interested about developing Avengers Assemble, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. **Acknowledgements** -* Libraries used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5) +* Libraries used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5), [OpenCSV](https://opencsv.sourceforge.net/) diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 00000000000..63a232e05dc --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,8587 @@ +{ + "name": "docs", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "docs", + "version": "1.0.0", + "devDependencies": { + "markbind-cli": "^5.1.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.2.tgz", + "integrity": "sha512-m5cPn3e2+FDCOgi1mz0RexTUvvQibBebOUlUlW0+YrMjDTPkiJ6VTKukA1GRsvRw+12KyJndNjj0O4AgTxm2Pg==", + "dev": true, + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/file-exists/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/@kwsites/file-exists/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "dev": true + }, + "node_modules/@markbind/core": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@markbind/core/-/core-5.1.0.tgz", + "integrity": "sha512-YAXjH+qCXnrBzpKIAJkayVLmyIUaG/8Dms3Gpd2VIufeZyW8w0diXdgKSsymjzodTMgghZMdxG3Qpng833ARPg==", + "dev": true, + "dependencies": { + "@fortawesome/fontawesome-free": "^6.4.0", + "@markbind/core-web": "5.1.0", + "@primer/octicons": "^15.0.1", + "@sindresorhus/slugify": "^0.9.1", + "@tlylt/markdown-it-imsize": "^3.0.0", + "bluebird": "^3.7.2", + "bootswatch": "5.1.3", + "cheerio": "^0.22.0", + "crypto-js": "^4.0.0", + "csv-parse": "^4.14.2", + "ensure-posix-path": "^1.1.1", + "fastmatter": "^2.1.1", + "fs-extra": "^9.0.1", + "gh-pages": "^2.1.1", + "highlight.js": "^10.4.1", + "htmlparser2": "^3.10.1", + "ignore": "^5.1.4", + "js-beautify": "1.14.3", + "katex": "^0.15.6", + "lodash": "^4.17.15", + "markdown-it": "^12.3.2", + "markdown-it-attrs": "^4.1.3", + "markdown-it-emoji": "^1.4.0", + "markdown-it-linkify-images": "^3.0.0", + "markdown-it-mark": "^3.0.0", + "markdown-it-regexp": "^0.4.0", + "markdown-it-sub": "^1.0.0", + "markdown-it-sup": "^1.0.0", + "markdown-it-table-of-contents": "^0.4.4", + "markdown-it-task-lists": "^2.1.1", + "markdown-it-texmath": "^1.0.0", + "markdown-it-video": "^0.6.3", + "material-icons": "^1.9.1", + "moment": "^2.29.4", + "nunjucks": "3.2.2", + "path-is-inside": "^1.0.2", + "simple-git": "^2.17.0", + "url-parse": "^1.5.10", + "uuid": "^8.3.1", + "vue": "2.6.14", + "vue-server-renderer": "2.6.14", + "vue-template-compiler": "2.6.14", + "walk-sync": "^2.0.2", + "winston": "^2.4.4" + } + }, + "node_modules/@markbind/core-web": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@markbind/core-web/-/core-web-5.1.0.tgz", + "integrity": "sha512-TRzz8ZCr25pylKvFxF/WwXDi4Gbtsb2OLXV61WyTFqVy03tFoEJ2mqncpbliI9DrfDdKWcm1YZPgDCedVkYjKA==", + "dev": true + }, + "node_modules/@primer/octicons": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/@primer/octicons/-/octicons-15.2.0.tgz", + "integrity": "sha512-4cHZzcZ3F/HQNL4EKSaFyVsW7XtITiJkTeB1JDDmRuP/XobyWyF9gWxuV9c+byUa8dOB5KNQn37iRvNrIehPUQ==", + "dev": true, + "dependencies": { + "object-assign": "^4.1.1" + } + }, + "node_modules/@sindresorhus/slugify": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-0.9.1.tgz", + "integrity": "sha512-b6heYM9dzZD13t2GOiEQTDE0qX+I1GyOotMwKh9VQqzuNiVdPVT8dM43fe9HNb/3ul+Qwd5oKSEDrDIfhq3bnQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5", + "lodash.deburr": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@tlylt/markdown-it-imsize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@tlylt/markdown-it-imsize/-/markdown-it-imsize-3.0.0.tgz", + "integrity": "sha512-6kTM+vRJTuN2UxNPyJ8yC+NHrzS+MxVHV+z+bDxSr/Fd7eTah2+otLKC2B17YI/1lQnSumA2qokPGuzsA98c6g==", + "dev": true + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "node_modules/a-sync-waterfall": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", + "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", + "dev": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/apache-crypt": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/apache-crypt/-/apache-crypt-1.2.5.tgz", + "integrity": "sha512-ICnYQH+DFVmw+S4Q0QY2XRXD8Ne8ewh8HgbuFH4K7022zCxgHM0Hz1xkRnUlEfAXNbwp1Cnhbedu60USIfDxvg==", + "dev": true, + "dependencies": { + "unix-crypt-td-js": "^1.1.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/apache-md5": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/apache-md5/-/apache-md5-1.1.7.tgz", + "integrity": "sha512-JtHjzZmJxtzfTSjsCyHgPR155HBe5WGyUyHTaEkfy46qhwCFKx1Epm6nAxgUG3WfUZP1dWhGqj9Z2NOBeZ+uBw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "dev": true, + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "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/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "dependencies": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/bootswatch": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bootswatch/-/bootswatch-5.1.3.tgz", + "integrity": "sha512-NmZFN6rOCoXWQ/PkzmD8FFWDe24kocX9OXWHNVaLxVVnpqpAzEbMFsf8bAfKwVtpNXibasZCzv09B5fLieAh2g==", + "dev": true + }, + "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/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "dependencies": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cheerio": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", + "integrity": "sha512-8/MzidM6G/TgRelkzDG13y3Y9LxBjCb+8yOEZ9+wwq5gVF2w2pV0wmHvjfT0RvuxGyR7UEuK36r+yYMbT4uKgA==", + "dev": true, + "dependencies": { + "css-select": "~1.2.0", + "dom-serializer": "~0.1.0", + "entities": "~1.1.1", + "htmlparser2": "^3.9.1", + "lodash.assignin": "^4.0.9", + "lodash.bind": "^4.1.4", + "lodash.defaults": "^4.0.1", + "lodash.filter": "^4.4.0", + "lodash.flatten": "^4.2.0", + "lodash.foreach": "^4.3.0", + "lodash.map": "^4.4.0", + "lodash.merge": "^4.4.0", + "lodash.pick": "^4.2.1", + "lodash.reduce": "^4.4.0", + "lodash.reject": "^4.4.0", + "lodash.some": "^4.4.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "dependencies": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", + "dev": true, + "dependencies": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/component-emitter": { + "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 + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==", + "dev": true + }, + "node_modules/css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha512-dUQOBoqdR7QwV90WysXPLXG5LO7nhYBgiWVfxF80DKPF8zx1t/pUd2FYy73emg3zrjtM6dzmYgbHKfV2rxiHQA==", + "dev": true, + "dependencies": { + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" + } + }, + "node_modules/css-what": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/csv-parse": { + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-4.16.3.tgz", + "integrity": "sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg==", + "dev": true + }, + "node_modules/cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "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/define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dom-serializer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "dev": true, + "dependencies": { + "domelementtype": "^1.3.0", + "entities": "^1.1.1" + } + }, + "node_modules/domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "node_modules/domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dev": true, + "dependencies": { + "domelementtype": "1" + } + }, + "node_modules/domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw==", + "dev": true, + "dependencies": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "node_modules/editorconfig": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", + "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", + "dev": true, + "dependencies": { + "commander": "^2.19.0", + "lru-cache": "^4.1.5", + "semver": "^5.6.0", + "sigmund": "^1.0.1" + }, + "bin": { + "editorconfig": "bin/editorconfig" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/email-addresses": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-3.1.0.tgz", + "integrity": "sha512-k0/r7GrWVL32kZlGwfPNgB2Y/mMXVTq/decgLczm/j34whdaspNrZO8CnXPf1laaHxI6ptUlsnAxN+UAPw+fzg==", + "dev": true + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ensure-posix-path": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ensure-posix-path/-/ensure-posix-path-1.1.1.tgz", + "integrity": "sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==", + "dev": true + }, + "node_modules/entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", + "dev": true, + "dependencies": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + } + }, + "node_modules/event-stream/node_modules/split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", + "dev": true, + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/event-stream/node_modules/stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", + "dev": true, + "dependencies": { + "duplexer": "~0.1.1" + } + }, + "node_modules/expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", + "dev": true, + "dependencies": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "dependencies": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "dev": true, + "engines": { + "node": "> 0.1.90" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, + "node_modules/fastmatter": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fastmatter/-/fastmatter-2.1.1.tgz", + "integrity": "sha512-NFrjZEPJZTexoJEuyM5J7n4uFaLf0dOI7Ok4b2IZXOYBqCp1Bh5RskANmQ2TuDsz3M35B1yL2AP/Rn+kp85KeA==", + "dev": true, + "dependencies": { + "js-yaml": "^3.13.0", + "split": "^1.0.1", + "stream-combiner": "^0.2.2", + "through2": "^3.0.1" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fecha": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", + "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==", + "dev": true + }, + "node_modules/figlet": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.5.2.tgz", + "integrity": "sha512-WOn21V8AhyE1QqVfPIVxe3tupJacq1xGkPTB4iagT6o+P2cAgEOOwIxMftr4+ZCTI6d551ij9j61DFr0nsP2uQ==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/file-stream-rotator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.4.1.tgz", + "integrity": "sha512-W3aa3QJEc8BS2MmdVpQiYLKHj3ijpto1gMDlsgCRSKfIUe6MwkcpODGPQ3vZfb0XvCeCqlu9CBQTN7oQri2TZQ==", + "dev": true, + "dependencies": { + "moment": "^2.11.2" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, + "node_modules/filename-reserved-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-1.0.0.tgz", + "integrity": "sha512-UZArj7+U+2reBBVCvVmRlyq9D7EYQdUtuNN+1iz7pF1jGcJ2L0TjiRCxsTZfj2xFbM4c25uGCUDpKTHA7L2TKg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/filenamify": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-1.2.1.tgz", + "integrity": "sha512-DKVP0WQcB7WaIMSwDETqImRej2fepPqvXQjaVib7LRZn9Rxn5UbvK2tYTqGf1A1DkIprQQkG4XSQXSOZp7Q3GQ==", + "dev": true, + "dependencies": { + "filename-reserved-regex": "^1.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/filenamify-url": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/filenamify-url/-/filenamify-url-1.0.0.tgz", + "integrity": "sha512-O9K9JcZeF5VdZWM1qR92NSv1WY2EofwudQayPx5dbnnFl9k0IcZha4eV/FGkjnBK+1irOQInij0yiooCHu/0Fg==", + "dev": true, + "dependencies": { + "filenamify": "^1.0.0", + "humanize-url": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", + "dev": true, + "dependencies": { + "map-cache": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", + "dev": true + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gh-pages": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-2.2.0.tgz", + "integrity": "sha512-c+yPkNOPMFGNisYg9r4qvsMIjVYikJv7ImFOhPIVPt0+AcRUamZ7zkGRLHz7FKB0xrlZ+ddSOJsZv9XAFVXLmA==", + "dev": true, + "dependencies": { + "async": "^2.6.1", + "commander": "^2.18.0", + "email-addresses": "^3.0.1", + "filenamify-url": "^1.0.0", + "fs-extra": "^8.1.0", + "globby": "^6.1.0" + }, + "bin": { + "gh-pages": "bin/gh-pages.js", + "gh-pages-clean": "bin/gh-pages-clean.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/gh-pages/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/gh-pages/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/gh-pages/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/gh-pages/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", + "dev": true, + "dependencies": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "dev": true, + "dependencies": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", + "dev": true + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "dev": true, + "dependencies": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "node_modules/http-auth": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/http-auth/-/http-auth-3.1.3.tgz", + "integrity": "sha512-Jbx0+ejo2IOx+cRUYAGS1z6RGc6JfYUNkysZM4u4Sfk1uLlGv814F7/PIjQQAuThLdAWxb74JMGd5J8zex1VQg==", + "dev": true, + "dependencies": { + "apache-crypt": "^1.1.2", + "apache-md5": "^1.0.6", + "bcryptjs": "^2.3.0", + "uuid": "^3.0.0" + }, + "engines": { + "node": ">=4.6.1" + } + }, + "node_modules/http-auth/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "node_modules/humanize-url": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/humanize-url/-/humanize-url-1.0.1.tgz", + "integrity": "sha512-RtgTzXCPVb/te+e82NDhAc5paj+DuKSratIGAr+v+HZK24eAQ8LMoBGYoL7N/O+9iEc33AKHg45dOMKw3DNldQ==", + "dev": true, + "dependencies": { + "normalize-url": "^1.0.0", + "strip-url-auth": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, + "node_modules/js-beautify": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.3.tgz", + "integrity": "sha512-f1ra8PHtOEu/70EBnmiUlV8nJePS58y9qKjl4JHfYWlFH6bo7ogZBz//FAZp7jDuXtYnGYKymZPlrg2I/9Zo4g==", + "dev": true, + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^0.15.3", + "glob": "^7.1.3", + "nopt": "^5.0.0" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/katex": { + "version": "0.15.6", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.15.6.tgz", + "integrity": "sha512-UpzJy4yrnqnhXvRPhjEuLA4lcPn6eRngixW7Q3TJErjg3Aw2PuLFBzTkdUb89UtumxjhHTqL3a5GDGETMSwgJA==", + "dev": true, + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "dependencies": { + "commander": "^8.0.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dev": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/live-server": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/live-server/-/live-server-1.2.1.tgz", + "integrity": "sha512-Yn2XCVjErTkqnM3FfTmM7/kWy3zP7+cEtC7x6u+wUzlQ+1UW3zEYbbyJrc0jNDwiMDZI0m4a0i3dxlGHVyXczw==", + "dev": true, + "dependencies": { + "chokidar": "^2.0.4", + "colors": "latest", + "connect": "^3.6.6", + "cors": "latest", + "event-stream": "3.3.4", + "faye-websocket": "0.11.x", + "http-auth": "3.1.x", + "morgan": "^1.9.1", + "object-assign": "latest", + "opn": "latest", + "proxy-middleware": "latest", + "send": "latest", + "serve-index": "^1.9.1" + }, + "bin": { + "live-server": "live-server.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "dependencies": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "node_modules/live-server/node_modules/anymatch/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "deprecated": "Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies", + "dev": true, + "dependencies": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + }, + "optionalDependencies": { + "fsevents": "^1.2.7" + } + }, + "node_modules/live-server/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "deprecated": "fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2.", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/live-server/node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "dev": true, + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/live-server/node_modules/glob-parent/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", + "dev": true, + "dependencies": { + "binary-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/live-server/node_modules/readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/live-server/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/live-server/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==", + "dev": true + }, + "node_modules/lodash.assignin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", + "integrity": "sha512-yX/rx6d/UTVh7sSVWVSIMjfnz95evAgDFdb1ZozC35I9mSFCkmzptOzevxjgbQUsc78NR44LVHWjsoMQXy9FDg==", + "dev": true + }, + "node_modules/lodash.bind": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", + "integrity": "sha512-lxdsn7xxlCymgLYo1gGvVrfHmkjDiyqVv62FAeF2i5ta72BipE1SLxw8hPEPLhD4/247Ijw07UQH7Hq/chT5LA==", + "dev": true + }, + "node_modules/lodash.deburr": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lodash.deburr/-/lodash.deburr-4.1.0.tgz", + "integrity": "sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==", + "dev": true + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true + }, + "node_modules/lodash.filter": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", + "integrity": "sha512-pXYUy7PR8BCLwX5mgJ/aNtyOvuJTdZAo9EQFUvMIYugqmJxnrYaANvTbgndOzHSCSR0wnlBBfRXJL5SbWxo3FQ==", + "dev": true + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true + }, + "node_modules/lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==", + "dev": true + }, + "node_modules/lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==", + "dev": true + }, + "node_modules/lodash.reduce": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw==", + "dev": true + }, + "node_modules/lodash.reject": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", + "integrity": "sha512-qkTuvgEzYdyhiJBx42YPzPo71R1aEr0z79kAv7Ixg8wPFEjgRgJdUsGMG3Hf3OYSF/kHI79XhNlt+5Ar6OzwxQ==", + "dev": true + }, + "node_modules/lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", + "dev": true + }, + "node_modules/lodash.template": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", + "dev": true, + "dependencies": { + "lodash._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "node_modules/lodash.templatesettings": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", + "dev": true, + "dependencies": { + "lodash._reinterpolate": "^3.0.0" + } + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "node_modules/logform": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-1.10.0.tgz", + "integrity": "sha512-em5ojIhU18fIMOw/333mD+ZLE2fis0EzXl1ZwHx4iQzmpQi6odNiY/t+ITNr33JZhT9/KEaH+UPIipr6a9EjWg==", + "dev": true, + "dependencies": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^2.3.3", + "ms": "^2.1.1", + "triple-beam": "^1.2.0" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", + "dev": true + }, + "node_modules/map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", + "dev": true, + "dependencies": { + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/markbind-cli": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/markbind-cli/-/markbind-cli-5.1.0.tgz", + "integrity": "sha512-6POI1Q++2aZa+Udk/oQ6LX1oNPbKUBDY0mN3Up7VOFeK+XYW51faxuCk2Q91JTBxYRKLNtshxf0y12kB4Cj9Qw==", + "dev": true, + "dependencies": { + "@markbind/core": "5.1.0", + "@markbind/core-web": "5.1.0", + "bluebird": "^3.7.2", + "chalk": "^3.0.0", + "cheerio": "^0.22.0", + "chokidar": "^3.3.0", + "colors": "1.4.0", + "commander": "^8.1.0", + "figlet": "^1.2.4", + "find-up": "^4.1.0", + "fs-extra": "^9.0.1", + "live-server": "1.2.1", + "lodash": "^4.17.15", + "url-parse": "^1.5.10", + "winston": "^2.4.4", + "winston-daily-rotate-file": "^3.10.0" + }, + "bin": { + "markbind": "index.js" + } + }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it-attrs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/markdown-it-attrs/-/markdown-it-attrs-4.1.6.tgz", + "integrity": "sha512-O7PDKZlN8RFMyDX13JnctQompwrrILuz2y43pW2GagcwpIIElkAdfeek+erHfxUOlXWPsjFeWmZ8ch1xtRLWpA==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "markdown-it": ">= 9.0.0" + } + }, + "node_modules/markdown-it-emoji": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz", + "integrity": "sha512-QCz3Hkd+r5gDYtS2xsFXmBYrgw6KuWcJZLCEkdfAuwzZbShCmCfta+hwAMq4NX/4xPzkSHduMKgMkkPUJxSXNg==", + "dev": true + }, + "node_modules/markdown-it-linkify-images": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-linkify-images/-/markdown-it-linkify-images-3.0.0.tgz", + "integrity": "sha512-Vs5yGJa5MWjFgytzgtn8c1U6RcStj3FZKhhx459U8dYbEE5FTWZ6mMRkYMiDlkFO0j4VCsQT1LT557bY0ETgtg==", + "dev": true, + "dependencies": { + "markdown-it": "^13.0.1" + } + }, + "node_modules/markdown-it-linkify-images/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/markdown-it-linkify-images/node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/markdown-it-linkify-images/node_modules/linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "dev": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/markdown-it-linkify-images/node_modules/markdown-it": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz", + "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it-mark": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/markdown-it-mark/-/markdown-it-mark-3.0.1.tgz", + "integrity": "sha512-HyxjAu6BRsdt6Xcv6TKVQnkz/E70TdGXEFHRYBGLncRE9lBFwDNLVtFojKxjJWgJ+5XxUwLaHXy+2sGBbDn+4A==", + "dev": true + }, + "node_modules/markdown-it-regexp": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/markdown-it-regexp/-/markdown-it-regexp-0.4.0.tgz", + "integrity": "sha512-0XQmr46K/rMKnI93Y3CLXsHj4jIioRETTAiVnJnjrZCEkGaDOmUxTbZj/aZ17G5NlRcVpWBYjqpwSlQ9lj+Kxw==", + "dev": true + }, + "node_modules/markdown-it-sub": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-sub/-/markdown-it-sub-1.0.0.tgz", + "integrity": "sha512-z2Rm/LzEE1wzwTSDrI+FlPEveAAbgdAdPhdWarq/ZGJrGW/uCQbKAnhoCsE4hAbc3SEym26+W2z/VQB0cQiA9Q==", + "dev": true + }, + "node_modules/markdown-it-sup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-sup/-/markdown-it-sup-1.0.0.tgz", + "integrity": "sha512-E32m0nV9iyhRR7CrhnzL5msqic7rL1juWre6TQNxsnApg7Uf+F97JOKxUijg5YwXz86lZ0mqfOnutoryyNdntQ==", + "dev": true + }, + "node_modules/markdown-it-table-of-contents": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.4.4.tgz", + "integrity": "sha512-TAIHTHPwa9+ltKvKPWulm/beozQU41Ab+FIefRaQV1NRnpzwcV9QOe6wXQS5WLivm5Q/nlo0rl6laGkMDZE7Gw==", + "dev": true, + "engines": { + "node": ">6.4.0" + } + }, + "node_modules/markdown-it-task-lists": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz", + "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==", + "dev": true + }, + "node_modules/markdown-it-texmath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-texmath/-/markdown-it-texmath-1.0.0.tgz", + "integrity": "sha512-4hhkiX8/gus+6e53PLCUmUrsa6ZWGgJW2XCW6O0ASvZUiezIK900ZicinTDtG3kAO2kon7oUA/ReWmpW2FByxg==", + "dev": true + }, + "node_modules/markdown-it-video": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/markdown-it-video/-/markdown-it-video-0.6.3.tgz", + "integrity": "sha512-T4th1kwy0OcvyWSN4u3rqPGxvbDclpucnVSSaH3ZacbGsAts964dxokx9s/I3GYsrDCJs4ogtEeEeVP18DQj0Q==", + "dev": true + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/matcher-collection": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/matcher-collection/-/matcher-collection-2.0.1.tgz", + "integrity": "sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ==", + "dev": true, + "dependencies": { + "@types/minimatch": "^3.0.3", + "minimatch": "^3.0.2" + }, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/material-icons": { + "version": "1.13.11", + "resolved": "https://registry.npmjs.org/material-icons/-/material-icons-1.13.11.tgz", + "integrity": "sha512-kp2oAdaqo/Zp6hpTZW01rOgDPWmxBUszSdDzkRm1idCjjNvdUMnqu8qu58cll6CObo+o0cydOiPLdoSugLm+mQ==", + "dev": true + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true + }, + "node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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/mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "dependencies": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dev": true, + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/nan": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz", + "integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==", + "dev": true, + "optional": true + }, + "node_modules/nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha512-A48My/mtCklowHBlI8Fq2jFWK4tX4lJ5E6ytFsSOq1fzpvT0SQSgKhSg7lN5c2uYFOrUAOQp6zhhJnpp1eMloQ==", + "dev": true, + "dependencies": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "dependencies": { + "boolbase": "~1.0.0" + } + }, + "node_modules/nunjucks": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.2.tgz", + "integrity": "sha512-KUi85OoF2NMygwODAy28Lh9qHmq5hO3rBlbkYoC8v377h4l8Pt5qFjILl0LWpMbOrZ18CzfVVUvIHUIrtED3sA==", + "dev": true, + "dependencies": { + "a-sync-waterfall": "^1.0.0", + "asap": "^2.0.3", + "commander": "^5.1.0" + }, + "bin": { + "nunjucks-precompile": "bin/precompile" + }, + "engines": { + "node": ">= 6.9.0" + }, + "optionalDependencies": { + "chokidar": "^3.3.0" + } + }, + "node_modules/nunjucks/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-assign": { + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", + "dev": true, + "dependencies": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-descriptor/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz", + "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", + "dev": true, + "dependencies": { + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/opn": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-6.0.0.tgz", + "integrity": "sha512-I9PKfIZC+e4RXZ/qr1RhgyCnGgYX0UEIlXgWnCOVACIvFgaC9rz6Won7xbdhoHrd8IIhV7YEpHjreNUNkqCGkQ==", + "deprecated": "The package has been renamed to `open`", + "dev": true, + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", + "dev": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "dev": true, + "dependencies": { + "through": "~2.3" + } + }, + "node_modules/picomatch": { + "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" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, + "node_modules/proxy-middleware": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.15.0.tgz", + "integrity": "sha512-EGCG8SeoIRVMhsqHQUdDigB2i7qU7fCsWASwn54+nPutYO8n4q6EiwMzyfWlC+dzRFExP+kvcnDFdBDHoZBU7Q==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true + }, + "node_modules/query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==", + "dev": true, + "dependencies": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "dependencies": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true + }, + "node_modules/repeat-element": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", + "deprecated": "https://github.com/lydell/resolve-url#deprecated", + "dev": true + }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "dev": true, + "dependencies": { + "ret": "~0.1.10" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz", + "integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/send/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", + "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==", + "dev": true + }, + "node_modules/simple-git": { + "version": "2.48.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.48.0.tgz", + "integrity": "sha512-z4qtrRuaAFJS4PUd0g+xy7aN4y+RvEt/QTJpR184lhJguBA1S/LsVlvE/CM95RsYMOFJG3NGGDjqFCzKU19S/A==", + "dev": true, + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.3.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/steveukx/" + } + }, + "node_modules/simple-git/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/simple-git/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "dependencies": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "dependencies": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "dependencies": { + "kind-of": "^3.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "dev": true, + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dev": true, + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "node_modules/source-map-url": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "deprecated": "See https://github.com/lydell/source-map-url#deprecated", + "dev": true + }, + "node_modules/split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "dependencies": { + "extend-shallow": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", + "dev": true, + "dependencies": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stream-combiner": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", + "integrity": "sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==", + "dev": true, + "dependencies": { + "duplexer": "~0.1.1", + "through": "~2.3.4" + } + }, + "node_modules/strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-url-auth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-url-auth/-/strip-url-auth-1.0.1.tgz", + "integrity": "sha512-++41PnXftlL3pvI6lpvhSEO+89g1kIJC4MYB5E6yH+WHa5InIqz51yGd1YOGd7VNSNdoEOfzTMqbAM/2PbgaHQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + }, + "node_modules/to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-object-path/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "dependencies": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==", + "dev": true + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true + }, + "node_modules/union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/union-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unix-crypt-td-js": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/unix-crypt-td-js/-/unix-crypt-td-js-1.1.4.tgz", + "integrity": "sha512-8rMeVYWSIyccIJscb9NdCfZKSRBKYTeVnwmiRYT2ulE3qd1RaDQ0xQDP+rI3ccIWbhu/zuo5cgN8z73belNZgw==", + "dev": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "dev": true, + "dependencies": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "dev": true, + "dependencies": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dev": true, + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "deprecated": "Please see https://github.com/lydell/urix#deprecated", + "dev": true + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vue": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz", + "integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==", + "dev": true + }, + "node_modules/vue-server-renderer": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/vue-server-renderer/-/vue-server-renderer-2.6.14.tgz", + "integrity": "sha512-HifYRa/LW7cKywg9gd4ZtvtRuBlstQBao5ZCWlg40fyB4OPoGfEXAzxb0emSLv4pBDOHYx0UjpqvxpiQFEuoLA==", + "dev": true, + "dependencies": { + "chalk": "^1.1.3", + "hash-sum": "^1.0.2", + "he": "^1.1.0", + "lodash.template": "^4.5.0", + "lodash.uniq": "^4.5.0", + "resolve": "^1.2.0", + "serialize-javascript": "^3.1.0", + "source-map": "0.5.6" + } + }, + "node_modules/vue-server-renderer/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vue-server-renderer/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vue-server-renderer/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/vue-template-compiler": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.14.tgz", + "integrity": "sha512-ODQS1SyMbjKoO1JBJZojSw6FE4qnh9rIpUZn2EUT86FKizx9uH5z6uXiIrm4/Nb/gwxTi/o17ZDEGWAXHvtC7g==", + "dev": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.1.0" + } + }, + "node_modules/walk-sync": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.2.0.tgz", + "integrity": "sha512-IC8sL7aB4/ZgFcGI2T1LczZeFWZ06b3zoHH7jBPyHxOtIIz1jppWHjjEXkOFvFojBVAK9pV7g47xOZ4LW3QLfg==", + "dev": true, + "dependencies": { + "@types/minimatch": "^3.0.3", + "ensure-posix-path": "^1.1.0", + "matcher-collection": "^2.0.0", + "minimatch": "^3.0.4" + }, + "engines": { + "node": "8.* || >= 10.*" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/winston": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.6.tgz", + "integrity": "sha512-J5Zu4p0tojLde8mIOyDSsmLmcP8I3Z6wtwpTDHx1+hGcdhxcJaAmG4CFtagkb+NiN1M9Ek4b42pzMWqfc9jm8w==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "colors": "1.0.x", + "cycle": "1.0.x", + "eyes": "0.1.x", + "isstream": "0.1.x", + "stack-trace": "0.0.x" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/winston-compat": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/winston-compat/-/winston-compat-0.1.5.tgz", + "integrity": "sha512-EPvPcHT604AV3Ji6d3+vX8ENKIml9VSxMRnPQ+cuK/FX6f3hvPP2hxyoeeCOCFvDrJEujalfcKWlWPvAnFyS9g==", + "dev": true, + "dependencies": { + "cycle": "~1.0.3", + "logform": "^1.6.0", + "triple-beam": "^1.2.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/winston-daily-rotate-file": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-3.10.0.tgz", + "integrity": "sha512-KO8CfbI2CvdR3PaFApEH02GPXiwJ+vbkF1mCkTlvRIoXFI8EFlf1ACcuaahXTEiDEKCii6cNe95gsL4ZkbnphA==", + "dev": true, + "dependencies": { + "file-stream-rotator": "^0.4.1", + "object-hash": "^1.3.0", + "semver": "^6.2.0", + "triple-beam": "^1.3.0", + "winston-compat": "^0.1.4", + "winston-transport": "^4.2.0" + }, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "winston": "^2 || ^3" + } + }, + "node_modules/winston-daily-rotate-file/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/winston-transport": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", + "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", + "dev": true, + "dependencies": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/winston-transport/node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "dev": true + }, + "node_modules/winston-transport/node_modules/logform": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz", + "integrity": "sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw==", + "dev": true, + "dependencies": { + "@colors/colors": "1.5.0", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + } + }, + "node_modules/winston-transport/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/winston/node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, + "node_modules/winston/node_modules/colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + } + }, + "dependencies": { + "@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true + }, + "@fortawesome/fontawesome-free": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.2.tgz", + "integrity": "sha512-m5cPn3e2+FDCOgi1mz0RexTUvvQibBebOUlUlW0+YrMjDTPkiJ6VTKukA1GRsvRw+12KyJndNjj0O4AgTxm2Pg==", + "dev": true + }, + "@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dev": true, + "requires": { + "debug": "^4.1.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" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "dev": true + }, + "@markbind/core": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@markbind/core/-/core-5.1.0.tgz", + "integrity": "sha512-YAXjH+qCXnrBzpKIAJkayVLmyIUaG/8Dms3Gpd2VIufeZyW8w0diXdgKSsymjzodTMgghZMdxG3Qpng833ARPg==", + "dev": true, + "requires": { + "@fortawesome/fontawesome-free": "^6.4.0", + "@markbind/core-web": "5.1.0", + "@primer/octicons": "^15.0.1", + "@sindresorhus/slugify": "^0.9.1", + "@tlylt/markdown-it-imsize": "^3.0.0", + "bluebird": "^3.7.2", + "bootswatch": "5.1.3", + "cheerio": "^0.22.0", + "crypto-js": "^4.0.0", + "csv-parse": "^4.14.2", + "ensure-posix-path": "^1.1.1", + "fastmatter": "^2.1.1", + "fs-extra": "^9.0.1", + "gh-pages": "^2.1.1", + "highlight.js": "^10.4.1", + "htmlparser2": "^3.10.1", + "ignore": "^5.1.4", + "js-beautify": "1.14.3", + "katex": "^0.15.6", + "lodash": "^4.17.15", + "markdown-it": "^12.3.2", + "markdown-it-attrs": "^4.1.3", + "markdown-it-emoji": "^1.4.0", + "markdown-it-linkify-images": "^3.0.0", + "markdown-it-mark": "^3.0.0", + "markdown-it-regexp": "^0.4.0", + "markdown-it-sub": "^1.0.0", + "markdown-it-sup": "^1.0.0", + "markdown-it-table-of-contents": "^0.4.4", + "markdown-it-task-lists": "^2.1.1", + "markdown-it-texmath": "^1.0.0", + "markdown-it-video": "^0.6.3", + "material-icons": "^1.9.1", + "moment": "^2.29.4", + "nunjucks": "3.2.2", + "path-is-inside": "^1.0.2", + "simple-git": "^2.17.0", + "url-parse": "^1.5.10", + "uuid": "^8.3.1", + "vue": "2.6.14", + "vue-server-renderer": "2.6.14", + "vue-template-compiler": "2.6.14", + "walk-sync": "^2.0.2", + "winston": "^2.4.4" + } + }, + "@markbind/core-web": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@markbind/core-web/-/core-web-5.1.0.tgz", + "integrity": "sha512-TRzz8ZCr25pylKvFxF/WwXDi4Gbtsb2OLXV61WyTFqVy03tFoEJ2mqncpbliI9DrfDdKWcm1YZPgDCedVkYjKA==", + "dev": true + }, + "@primer/octicons": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/@primer/octicons/-/octicons-15.2.0.tgz", + "integrity": "sha512-4cHZzcZ3F/HQNL4EKSaFyVsW7XtITiJkTeB1JDDmRuP/XobyWyF9gWxuV9c+byUa8dOB5KNQn37iRvNrIehPUQ==", + "dev": true, + "requires": { + "object-assign": "^4.1.1" + } + }, + "@sindresorhus/slugify": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-0.9.1.tgz", + "integrity": "sha512-b6heYM9dzZD13t2GOiEQTDE0qX+I1GyOotMwKh9VQqzuNiVdPVT8dM43fe9HNb/3ul+Qwd5oKSEDrDIfhq3bnQ==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "lodash.deburr": "^4.1.0" + } + }, + "@tlylt/markdown-it-imsize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@tlylt/markdown-it-imsize/-/markdown-it-imsize-3.0.0.tgz", + "integrity": "sha512-6kTM+vRJTuN2UxNPyJ8yC+NHrzS+MxVHV+z+bDxSr/Fd7eTah2+otLKC2B17YI/1lQnSumA2qokPGuzsA98c6g==", + "dev": true + }, + "@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "a-sync-waterfall": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", + "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", + "dev": true + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "apache-crypt": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/apache-crypt/-/apache-crypt-1.2.5.tgz", + "integrity": "sha512-ICnYQH+DFVmw+S4Q0QY2XRXD8Ne8ewh8HgbuFH4K7022zCxgHM0Hz1xkRnUlEfAXNbwp1Cnhbedu60USIfDxvg==", + "dev": true, + "requires": { + "unix-crypt-td-js": "^1.1.4" + } + }, + "apache-md5": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/apache-md5/-/apache-md5-1.1.7.tgz", + "integrity": "sha512-JtHjzZmJxtzfTSjsCyHgPR155HBe5WGyUyHTaEkfy46qhwCFKx1Epm6nAxgUG3WfUZP1dWhGqj9Z2NOBeZ+uBw==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "dev": true + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "dev": true + }, + "async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "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 + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + } + } + }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "bootswatch": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bootswatch/-/bootswatch-5.1.3.tgz", + "integrity": "sha512-NmZFN6rOCoXWQ/PkzmD8FFWDe24kocX9OXWHNVaLxVVnpqpAzEbMFsf8bAfKwVtpNXibasZCzv09B5fLieAh2g==", + "dev": true + }, + "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" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "cheerio": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", + "integrity": "sha512-8/MzidM6G/TgRelkzDG13y3Y9LxBjCb+8yOEZ9+wwq5gVF2w2pV0wmHvjfT0RvuxGyR7UEuK36r+yYMbT4uKgA==", + "dev": true, + "requires": { + "css-select": "~1.2.0", + "dom-serializer": "~0.1.0", + "entities": "~1.1.1", + "htmlparser2": "^3.9.1", + "lodash.assignin": "^4.0.9", + "lodash.bind": "^4.1.4", + "lodash.defaults": "^4.0.1", + "lodash.filter": "^4.4.0", + "lodash.flatten": "^4.2.0", + "lodash.foreach": "^4.3.0", + "lodash.map": "^4.4.0", + "lodash.merge": "^4.4.0", + "lodash.pick": "^4.2.1", + "lodash.reduce": "^4.4.0", + "lodash.reject": "^4.4.0", + "lodash.some": "^4.4.0" + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true + }, + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true + }, + "component-emitter": { + "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 + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "requires": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "requires": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", + "dev": true + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==", + "dev": true + }, + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha512-dUQOBoqdR7QwV90WysXPLXG5LO7nhYBgiWVfxF80DKPF8zx1t/pUd2FYy73emg3zrjtM6dzmYgbHKfV2rxiHQA==", + "dev": true, + "requires": { + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" + } + }, + "css-what": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", + "dev": true + }, + "csv-parse": { + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-4.16.3.tgz", + "integrity": "sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg==", + "dev": true + }, + "cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA==", + "dev": true + }, + "de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "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 + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + } + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true + }, + "dom-serializer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "dev": true, + "requires": { + "domelementtype": "^1.3.0", + "entities": "^1.1.1" + } + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dev": true, + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw==", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "editorconfig": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", + "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", + "dev": true, + "requires": { + "commander": "^2.19.0", + "lru-cache": "^4.1.5", + "semver": "^5.6.0", + "sigmund": "^1.0.1" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + } + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "email-addresses": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-3.1.0.tgz", + "integrity": "sha512-k0/r7GrWVL32kZlGwfPNgB2Y/mMXVTq/decgLczm/j34whdaspNrZO8CnXPf1laaHxI6ptUlsnAxN+UAPw+fzg==", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true + }, + "ensure-posix-path": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ensure-posix-path/-/ensure-posix-path-1.1.1.tgz", + "integrity": "sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==", + "dev": true + }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true + }, + "event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", + "dev": true, + "requires": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + }, + "dependencies": { + "split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", + "dev": true, + "requires": { + "through": "2" + } + }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", + "dev": true, + "requires": { + "duplexer": "~0.1.1" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + } + } + }, + "eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "dev": true + }, + "fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, + "fastmatter": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fastmatter/-/fastmatter-2.1.1.tgz", + "integrity": "sha512-NFrjZEPJZTexoJEuyM5J7n4uFaLf0dOI7Ok4b2IZXOYBqCp1Bh5RskANmQ2TuDsz3M35B1yL2AP/Rn+kp85KeA==", + "dev": true, + "requires": { + "js-yaml": "^3.13.0", + "split": "^1.0.1", + "stream-combiner": "^0.2.2", + "through2": "^3.0.1" + } + }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "fecha": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", + "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==", + "dev": true + }, + "figlet": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.5.2.tgz", + "integrity": "sha512-WOn21V8AhyE1QqVfPIVxe3tupJacq1xGkPTB4iagT6o+P2cAgEOOwIxMftr4+ZCTI6d551ij9j61DFr0nsP2uQ==", + "dev": true + }, + "file-stream-rotator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.4.1.tgz", + "integrity": "sha512-W3aa3QJEc8BS2MmdVpQiYLKHj3ijpto1gMDlsgCRSKfIUe6MwkcpODGPQ3vZfb0XvCeCqlu9CBQTN7oQri2TZQ==", + "dev": true, + "requires": { + "moment": "^2.11.2" + } + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, + "filename-reserved-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-1.0.0.tgz", + "integrity": "sha512-UZArj7+U+2reBBVCvVmRlyq9D7EYQdUtuNN+1iz7pF1jGcJ2L0TjiRCxsTZfj2xFbM4c25uGCUDpKTHA7L2TKg==", + "dev": true + }, + "filenamify": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-1.2.1.tgz", + "integrity": "sha512-DKVP0WQcB7WaIMSwDETqImRej2fepPqvXQjaVib7LRZn9Rxn5UbvK2tYTqGf1A1DkIprQQkG4XSQXSOZp7Q3GQ==", + "dev": true, + "requires": { + "filename-reserved-regex": "^1.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + } + }, + "filenamify-url": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/filenamify-url/-/filenamify-url-1.0.0.tgz", + "integrity": "sha512-O9K9JcZeF5VdZWM1qR92NSv1WY2EofwudQayPx5dbnnFl9k0IcZha4eV/FGkjnBK+1irOQInij0yiooCHu/0Fg==", + "dev": true, + "requires": { + "filenamify": "^1.0.0", + "humanize-url": "^1.0.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "dev": true + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true + }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", + "dev": true + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "dev": true + }, + "gh-pages": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-2.2.0.tgz", + "integrity": "sha512-c+yPkNOPMFGNisYg9r4qvsMIjVYikJv7ImFOhPIVPt0+AcRUamZ7zkGRLHz7FKB0xrlZ+ddSOJsZv9XAFVXLmA==", + "dev": true, + "requires": { + "async": "^2.6.1", + "commander": "^2.18.0", + "email-addresses": "^3.0.1", + "filenamify-url": "^1.0.0", + "fs-extra": "^8.1.0", + "globby": "^6.1.0" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + } + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "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 + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", + "dev": true + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true + }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "dev": true, + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "http-auth": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/http-auth/-/http-auth-3.1.3.tgz", + "integrity": "sha512-Jbx0+ejo2IOx+cRUYAGS1z6RGc6JfYUNkysZM4u4Sfk1uLlGv814F7/PIjQQAuThLdAWxb74JMGd5J8zex1VQg==", + "dev": true, + "requires": { + "apache-crypt": "^1.1.2", + "apache-md5": "^1.0.6", + "bcryptjs": "^2.3.0", + "uuid": "^3.0.0" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "dependencies": { + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true + } + } + }, + "http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "humanize-url": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/humanize-url/-/humanize-url-1.0.1.tgz", + "integrity": "sha512-RtgTzXCPVb/te+e82NDhAc5paj+DuKSratIGAr+v+HZK24eAQ8LMoBGYoL7N/O+9iEc33AKHg45dOMKw3DNldQ==", + "dev": true, + "requires": { + "normalize-url": "^1.0.0", + "strip-url-auth": "^1.0.0" + } + }, + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, + "js-beautify": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.3.tgz", + "integrity": "sha512-f1ra8PHtOEu/70EBnmiUlV8nJePS58y9qKjl4JHfYWlFH6bo7ogZBz//FAZp7jDuXtYnGYKymZPlrg2I/9Zo4g==", + "dev": true, + "requires": { + "config-chain": "^1.1.13", + "editorconfig": "^0.15.3", + "glob": "^7.1.3", + "nopt": "^5.0.0" + } + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "katex": { + "version": "0.15.6", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.15.6.tgz", + "integrity": "sha512-UpzJy4yrnqnhXvRPhjEuLA4lcPn6eRngixW7Q3TJErjg3Aw2PuLFBzTkdUb89UtumxjhHTqL3a5GDGETMSwgJA==", + "dev": true, + "requires": { + "commander": "^8.0.0" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dev": true, + "requires": { + "uc.micro": "^1.0.1" + } + }, + "live-server": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/live-server/-/live-server-1.2.1.tgz", + "integrity": "sha512-Yn2XCVjErTkqnM3FfTmM7/kWy3zP7+cEtC7x6u+wUzlQ+1UW3zEYbbyJrc0jNDwiMDZI0m4a0i3dxlGHVyXczw==", + "dev": true, + "requires": { + "chokidar": "^2.0.4", + "colors": "latest", + "connect": "^3.6.6", + "cors": "latest", + "event-stream": "3.3.4", + "faye-websocket": "0.11.x", + "http-auth": "3.1.x", + "morgan": "^1.9.1", + "object-assign": "latest", + "opn": "latest", + "proxy-middleware": "latest", + "send": "latest", + "serve-index": "^1.9.1" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + } + }, + "fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "dev": true, + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==", + "dev": true + }, + "lodash.assignin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", + "integrity": "sha512-yX/rx6d/UTVh7sSVWVSIMjfnz95evAgDFdb1ZozC35I9mSFCkmzptOzevxjgbQUsc78NR44LVHWjsoMQXy9FDg==", + "dev": true + }, + "lodash.bind": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", + "integrity": "sha512-lxdsn7xxlCymgLYo1gGvVrfHmkjDiyqVv62FAeF2i5ta72BipE1SLxw8hPEPLhD4/247Ijw07UQH7Hq/chT5LA==", + "dev": true + }, + "lodash.deburr": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lodash.deburr/-/lodash.deburr-4.1.0.tgz", + "integrity": "sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==", + "dev": true + }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true + }, + "lodash.filter": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", + "integrity": "sha512-pXYUy7PR8BCLwX5mgJ/aNtyOvuJTdZAo9EQFUvMIYugqmJxnrYaANvTbgndOzHSCSR0wnlBBfRXJL5SbWxo3FQ==", + "dev": true + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true + }, + "lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==", + "dev": true + }, + "lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==", + "dev": true + }, + "lodash.reduce": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw==", + "dev": true + }, + "lodash.reject": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", + "integrity": "sha512-qkTuvgEzYdyhiJBx42YPzPo71R1aEr0z79kAv7Ixg8wPFEjgRgJdUsGMG3Hf3OYSF/kHI79XhNlt+5Ar6OzwxQ==", + "dev": true + }, + "lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", + "dev": true + }, + "lodash.template": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", + "dev": true, + "requires": { + "lodash._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "lodash.templatesettings": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", + "dev": true, + "requires": { + "lodash._reinterpolate": "^3.0.0" + } + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "logform": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-1.10.0.tgz", + "integrity": "sha512-em5ojIhU18fIMOw/333mD+ZLE2fis0EzXl1ZwHx4iQzmpQi6odNiY/t+ITNr33JZhT9/KEaH+UPIipr6a9EjWg==", + "dev": true, + "requires": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^2.3.3", + "ms": "^2.1.1", + "triple-beam": "^1.2.0" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true + }, + "map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "markbind-cli": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/markbind-cli/-/markbind-cli-5.1.0.tgz", + "integrity": "sha512-6POI1Q++2aZa+Udk/oQ6LX1oNPbKUBDY0mN3Up7VOFeK+XYW51faxuCk2Q91JTBxYRKLNtshxf0y12kB4Cj9Qw==", + "dev": true, + "requires": { + "@markbind/core": "5.1.0", + "@markbind/core-web": "5.1.0", + "bluebird": "^3.7.2", + "chalk": "^3.0.0", + "cheerio": "^0.22.0", + "chokidar": "^3.3.0", + "colors": "1.4.0", + "commander": "^8.1.0", + "figlet": "^1.2.4", + "find-up": "^4.1.0", + "fs-extra": "^9.0.1", + "live-server": "1.2.1", + "lodash": "^4.17.15", + "url-parse": "^1.5.10", + "winston": "^2.4.4", + "winston-daily-rotate-file": "^3.10.0" + } + }, + "markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dev": true, + "requires": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true + } + } + }, + "markdown-it-attrs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/markdown-it-attrs/-/markdown-it-attrs-4.1.6.tgz", + "integrity": "sha512-O7PDKZlN8RFMyDX13JnctQompwrrILuz2y43pW2GagcwpIIElkAdfeek+erHfxUOlXWPsjFeWmZ8ch1xtRLWpA==", + "dev": true, + "requires": {} + }, + "markdown-it-emoji": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz", + "integrity": "sha512-QCz3Hkd+r5gDYtS2xsFXmBYrgw6KuWcJZLCEkdfAuwzZbShCmCfta+hwAMq4NX/4xPzkSHduMKgMkkPUJxSXNg==", + "dev": true + }, + "markdown-it-linkify-images": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-linkify-images/-/markdown-it-linkify-images-3.0.0.tgz", + "integrity": "sha512-Vs5yGJa5MWjFgytzgtn8c1U6RcStj3FZKhhx459U8dYbEE5FTWZ6mMRkYMiDlkFO0j4VCsQT1LT557bY0ETgtg==", + "dev": true, + "requires": { + "markdown-it": "^13.0.1" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "dev": true + }, + "linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "dev": true, + "requires": { + "uc.micro": "^1.0.1" + } + }, + "markdown-it": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz", + "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==", + "dev": true, + "requires": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + } + } + } + }, + "markdown-it-mark": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/markdown-it-mark/-/markdown-it-mark-3.0.1.tgz", + "integrity": "sha512-HyxjAu6BRsdt6Xcv6TKVQnkz/E70TdGXEFHRYBGLncRE9lBFwDNLVtFojKxjJWgJ+5XxUwLaHXy+2sGBbDn+4A==", + "dev": true + }, + "markdown-it-regexp": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/markdown-it-regexp/-/markdown-it-regexp-0.4.0.tgz", + "integrity": "sha512-0XQmr46K/rMKnI93Y3CLXsHj4jIioRETTAiVnJnjrZCEkGaDOmUxTbZj/aZ17G5NlRcVpWBYjqpwSlQ9lj+Kxw==", + "dev": true + }, + "markdown-it-sub": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-sub/-/markdown-it-sub-1.0.0.tgz", + "integrity": "sha512-z2Rm/LzEE1wzwTSDrI+FlPEveAAbgdAdPhdWarq/ZGJrGW/uCQbKAnhoCsE4hAbc3SEym26+W2z/VQB0cQiA9Q==", + "dev": true + }, + "markdown-it-sup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-sup/-/markdown-it-sup-1.0.0.tgz", + "integrity": "sha512-E32m0nV9iyhRR7CrhnzL5msqic7rL1juWre6TQNxsnApg7Uf+F97JOKxUijg5YwXz86lZ0mqfOnutoryyNdntQ==", + "dev": true + }, + "markdown-it-table-of-contents": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.4.4.tgz", + "integrity": "sha512-TAIHTHPwa9+ltKvKPWulm/beozQU41Ab+FIefRaQV1NRnpzwcV9QOe6wXQS5WLivm5Q/nlo0rl6laGkMDZE7Gw==", + "dev": true + }, + "markdown-it-task-lists": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz", + "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==", + "dev": true + }, + "markdown-it-texmath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-texmath/-/markdown-it-texmath-1.0.0.tgz", + "integrity": "sha512-4hhkiX8/gus+6e53PLCUmUrsa6ZWGgJW2XCW6O0ASvZUiezIK900ZicinTDtG3kAO2kon7oUA/ReWmpW2FByxg==", + "dev": true + }, + "markdown-it-video": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/markdown-it-video/-/markdown-it-video-0.6.3.tgz", + "integrity": "sha512-T4th1kwy0OcvyWSN4u3rqPGxvbDclpucnVSSaH3ZacbGsAts964dxokx9s/I3GYsrDCJs4ogtEeEeVP18DQj0Q==", + "dev": true + }, + "matcher-collection": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/matcher-collection/-/matcher-collection-2.0.1.tgz", + "integrity": "sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ==", + "dev": true, + "requires": { + "@types/minimatch": "^3.0.3", + "minimatch": "^3.0.2" + } + }, + "material-icons": { + "version": "1.13.11", + "resolved": "https://registry.npmjs.org/material-icons/-/material-icons-1.13.11.tgz", + "integrity": "sha512-kp2oAdaqo/Zp6hpTZW01rOgDPWmxBUszSdDzkRm1idCjjNvdUMnqu8qu58cll6CObo+o0cydOiPLdoSugLm+mQ==", + "dev": true + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.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" + } + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + } + }, + "moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "dev": true + }, + "morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dev": true, + "requires": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "nan": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz", + "integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==", + "dev": true, + "optional": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true + }, + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha512-A48My/mtCklowHBlI8Fq2jFWK4tX4lJ5E6ytFsSOq1fzpvT0SQSgKhSg7lN5c2uYFOrUAOQp6zhhJnpp1eMloQ==", + "dev": true, + "requires": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + } + }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "requires": { + "boolbase": "~1.0.0" + } + }, + "nunjucks": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.2.tgz", + "integrity": "sha512-KUi85OoF2NMygwODAy28Lh9qHmq5hO3rBlbkYoC8v377h4l8Pt5qFjILl0LWpMbOrZ18CzfVVUvIHUIrtED3sA==", + "dev": true, + "requires": { + "a-sync-waterfall": "^1.0.0", + "asap": "^2.0.3", + "chokidar": "^3.3.0", + "commander": "^5.1.0" + }, + "dependencies": { + "commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true + } + } + }, + "object-assign": { + "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 + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-hash": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz", + "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "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, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "opn": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-6.0.0.tgz", + "integrity": "sha512-I9PKfIZC+e4RXZ/qr1RhgyCnGgYX0UEIlXgWnCOVACIvFgaC9rz6Won7xbdhoHrd8IIhV7YEpHjreNUNkqCGkQ==", + "dev": true, + "requires": { + "is-wsl": "^1.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "dev": true, + "requires": { + "through": "~2.3" + } + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", + "dev": true + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, + "proxy-middleware": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.15.0.tgz", + "integrity": "sha512-EGCG8SeoIRVMhsqHQUdDigB2i7qU7fCsWASwn54+nPutYO8n4q6EiwMzyfWlC+dzRFExP+kvcnDFdBDHoZBU7Q==", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true + }, + "query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==", + "dev": true, + "requires": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true + }, + "repeat-element": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "resolve": { + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", + "dev": true + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safe-stable-stringify": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz", + "integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==", + "dev": true + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true + } + } + }, + "serialize-javascript": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", + "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + } + } + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + } + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==", + "dev": true + }, + "simple-git": { + "version": "2.48.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.48.0.tgz", + "integrity": "sha512-z4qtrRuaAFJS4PUd0g+xy7aN4y+RvEt/QTJpR184lhJguBA1S/LsVlvE/CM95RsYMOFJG3NGGDjqFCzKU19S/A==", + "dev": true, + "requires": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.3.2" + }, + "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" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-url": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "dev": true + }, + "split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "requires": { + "through": "2" + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true + }, + "stream-combiner": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", + "integrity": "sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==", + "dev": true, + "requires": { + "duplexer": "~0.1.1", + "through": "~2.3.4" + } + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", + "dev": true + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, + "strip-url-auth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-url-auth/-/strip-url-auth-1.0.1.tgz", + "integrity": "sha512-++41PnXftlL3pvI6lpvhSEO+89g1kIJC4MYB5E6yH+WHa5InIqz51yGd1YOGd7VNSNdoEOfzTMqbAM/2PbgaHQ==", + "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" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true + }, + "trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, + "triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==", + "dev": true + }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + } + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + }, + "unix-crypt-td-js": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/unix-crypt-td-js/-/unix-crypt-td-js-1.1.4.tgz", + "integrity": "sha512-8rMeVYWSIyccIJscb9NdCfZKSRBKYTeVnwmiRYT2ulE3qd1RaDQ0xQDP+rI3ccIWbhu/zuo5cgN8z73belNZgw==", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "dev": true + } + } + }, + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "dev": true + }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true + }, + "vue": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz", + "integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==", + "dev": true + }, + "vue-server-renderer": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/vue-server-renderer/-/vue-server-renderer-2.6.14.tgz", + "integrity": "sha512-HifYRa/LW7cKywg9gd4ZtvtRuBlstQBao5ZCWlg40fyB4OPoGfEXAzxb0emSLv4pBDOHYx0UjpqvxpiQFEuoLA==", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "hash-sum": "^1.0.2", + "he": "^1.1.0", + "lodash.template": "^4.5.0", + "lodash.uniq": "^4.5.0", + "resolve": "^1.2.0", + "serialize-javascript": "^3.1.0", + "source-map": "0.5.6" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true + } + } + }, + "vue-template-compiler": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.14.tgz", + "integrity": "sha512-ODQS1SyMbjKoO1JBJZojSw6FE4qnh9rIpUZn2EUT86FKizx9uH5z6uXiIrm4/Nb/gwxTi/o17ZDEGWAXHvtC7g==", + "dev": true, + "requires": { + "de-indent": "^1.0.2", + "he": "^1.1.0" + } + }, + "walk-sync": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.2.0.tgz", + "integrity": "sha512-IC8sL7aB4/ZgFcGI2T1LczZeFWZ06b3zoHH7jBPyHxOtIIz1jppWHjjEXkOFvFojBVAK9pV7g47xOZ4LW3QLfg==", + "dev": true, + "requires": { + "@types/minimatch": "^3.0.3", + "ensure-posix-path": "^1.1.0", + "matcher-collection": "^2.0.0", + "minimatch": "^3.0.4" + } + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true + }, + "winston": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.6.tgz", + "integrity": "sha512-J5Zu4p0tojLde8mIOyDSsmLmcP8I3Z6wtwpTDHx1+hGcdhxcJaAmG4CFtagkb+NiN1M9Ek4b42pzMWqfc9jm8w==", + "dev": true, + "requires": { + "async": "^3.2.3", + "colors": "1.0.x", + "cycle": "1.0.x", + "eyes": "0.1.x", + "isstream": "0.1.x", + "stack-trace": "0.0.x" + }, + "dependencies": { + "async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", + "dev": true + } + } + }, + "winston-compat": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/winston-compat/-/winston-compat-0.1.5.tgz", + "integrity": "sha512-EPvPcHT604AV3Ji6d3+vX8ENKIml9VSxMRnPQ+cuK/FX6f3hvPP2hxyoeeCOCFvDrJEujalfcKWlWPvAnFyS9g==", + "dev": true, + "requires": { + "cycle": "~1.0.3", + "logform": "^1.6.0", + "triple-beam": "^1.2.0" + } + }, + "winston-daily-rotate-file": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-3.10.0.tgz", + "integrity": "sha512-KO8CfbI2CvdR3PaFApEH02GPXiwJ+vbkF1mCkTlvRIoXFI8EFlf1ACcuaahXTEiDEKCii6cNe95gsL4ZkbnphA==", + "dev": true, + "requires": { + "file-stream-rotator": "^0.4.1", + "object-hash": "^1.3.0", + "semver": "^6.2.0", + "triple-beam": "^1.3.0", + "winston-compat": "^0.1.4", + "winston-transport": "^4.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "winston-transport": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", + "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", + "dev": true, + "requires": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + }, + "dependencies": { + "fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "dev": true + }, + "logform": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz", + "integrity": "sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw==", + "dev": true, + "requires": { + "@colors/colors": "1.5.0", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000000..aa7083fd8a7 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,14 @@ +{ + "name": "docs", + "version": "1.0.0", + "description": "AB-3 docs", + "scripts": { + "init": "markbind init", + "build": "markbind build", + "serve": "markbind serve", + "deploy": "markbind deploy" + }, + "devDependencies": { + "markbind-cli": "^5.1.0" + } +} diff --git a/docs/release_1.2/copy_1.png b/docs/release_1.2/copy_1.png new file mode 100644 index 00000000000..6d05939f17c Binary files /dev/null and b/docs/release_1.2/copy_1.png differ diff --git a/docs/release_1.2/copy_2.png b/docs/release_1.2/copy_2.png new file mode 100644 index 00000000000..1d3abc825eb Binary files /dev/null and b/docs/release_1.2/copy_2.png differ diff --git a/docs/release_1.2/export_1.png b/docs/release_1.2/export_1.png new file mode 100644 index 00000000000..3dcad09a7ab Binary files /dev/null and b/docs/release_1.2/export_1.png differ diff --git a/docs/release_1.2/export_2.png b/docs/release_1.2/export_2.png new file mode 100644 index 00000000000..05ac8ec31ba Binary files /dev/null and b/docs/release_1.2/export_2.png differ diff --git a/docs/release_1.2/export_csv_result.png b/docs/release_1.2/export_csv_result.png new file mode 100644 index 00000000000..090742bff86 Binary files /dev/null and b/docs/release_1.2/export_csv_result.png differ diff --git a/docs/release_1.2/find_1.png b/docs/release_1.2/find_1.png new file mode 100644 index 00000000000..efd9ab9c6c5 Binary files /dev/null and b/docs/release_1.2/find_1.png differ diff --git a/docs/release_1.2/find_2.png b/docs/release_1.2/find_2.png new file mode 100644 index 00000000000..2e8a18f53a3 Binary files /dev/null and b/docs/release_1.2/find_2.png differ diff --git a/docs/release_1.2/import_1.png b/docs/release_1.2/import_1.png new file mode 100644 index 00000000000..5a2eb8a1bd6 Binary files /dev/null and b/docs/release_1.2/import_1.png differ diff --git a/docs/release_1.2/release_notes_1.2.md b/docs/release_1.2/release_notes_1.2.md new file mode 100644 index 00000000000..44a2e0903dd --- /dev/null +++ b/docs/release_1.2/release_notes_1.2.md @@ -0,0 +1,50 @@ +# Avengers Assemble v1.2 Release Notes + +With our additions, we are confident that Avengers Assemble has a MVP. Although some features require refinement, we have implemented enough features with sufficient quality to ensure a working product for head TAs for CS1101S. +## What's New +### 1. Export Feature +Avengers Assemble now supports exporting data of up to 500 contacts to a CSV file! + +1. Simply type `export` to export a CSV file of the contacts. +Typing in the `export` command. + +2. Once this message shows, the CSV is available for external use by the head TA. +The CSV will be exported. + +3. An example of the exported CSV file. +The CSV file. + +### 2. Import Feature +Avengers Assemble now supports importing data of up to 500 contacts stored in a csv file + +1. Simply type `import i/` where filePath is the absolute path of your csv file. +2. Once this message shows, imported contacts will be reflected in addressBook +Import Success + +> Note: There might be some issues importing outside of IDEs for Mac users due to privacy setting issues. We recommend users testing on Mac to use the IDE to run the .jar file and import the csv file. + +### 3. Find Feature +Avengers Assemble now supports finding contacts by their various details, and not just their names! + +1. Simply type `find` to find a contact by their details, followed by the field you want to search up. For example, the screenshot below shows finding contacts with an address containing `30`. +Typing in the `find` command. +2. The result will show all contacts with an address containing `30`. +The result of the `find` command. + +### 4. Copy Feature +Avengers Assemble also supports copying the emails of any specific group of contacts in the list into your clipboard! + +1. Use the `find` command to search for the people whose emails you would like to copy. +2. Type `copy` to copy all the emails of currently displayed contacts into your clipboard. +Type in the copy command. +3. All emails will be copied into your clipboard! +The result of the `copy` command. + +### 5. New Fields Added +To further support head TAs keeping track of the various reflections and studios happening in the ever hectic CS1101S, Avengers Assemble now supports adding the reflection `r/` and studio `s/` fields to contacts! + +The new fields added. + +### 6. Bug Fixes +Previously, the existing codebase checked for duplicate contacts by their name. However, this was not sufficient as there could be multiple contacts with the same name which is especially likely for a large course like CS1101S. This has been fixed by checking for the duplicates by their email. + diff --git a/docs/release_1.2/screenshot_1.2.png b/docs/release_1.2/screenshot_1.2.png new file mode 100644 index 00000000000..9818b13c784 Binary files /dev/null and b/docs/release_1.2/screenshot_1.2.png differ diff --git a/docs/site.json b/docs/site.json new file mode 100644 index 00000000000..767943f836f --- /dev/null +++ b/docs/site.json @@ -0,0 +1,31 @@ +{ + "baseUrl": "", + "titlePrefix": "Avengers Assemble", + "titleSuffix": "Avengers Assemble", + "faviconPath": "images/AaLogo.png", + "style": { + "codeTheme": "light", + "bootstrapTheme": "bootswatch-zephyr" + }, + "ignore": [ + "_markbind/layouts/*", + "_markbind/logs/*", + "_site/*", + "site.json", + "*.md", + "*.njk", + ".git/*", + "node_modules/*" + ], + "pagesExclude": ["node_modules/*"], + "pages": [ + { + "glob": ["**/index.md", "**/*.md"] + } + ], + "deploy": { + "message": "Site Update." + }, + "timeZone": "Asia/Singapore" + +} diff --git a/docs/stylesheets/main.css b/docs/stylesheets/main.css new file mode 100644 index 00000000000..66254de5b57 --- /dev/null +++ b/docs/stylesheets/main.css @@ -0,0 +1,182 @@ +mark { + background-color: #ff0; + border-radius: 5px; + padding-top: 0; + padding-bottom: 0; +} + +.indented { + padding-left: 20px; +} + +.theme-card img { + width: 100%; +} + +/* Scrollbar */ + +.slim-scroll::-webkit-scrollbar { + width: 5px; +} + +.slim-scroll::-webkit-scrollbar-thumb { + background: #808080; + border-radius: 20px; +} + +.slim-scroll::-webkit-scrollbar-track { + background: transparent; + border-radius: 20px; +} + +.slim-scroll-blue::-webkit-scrollbar { + width: 5px; +} + +.slim-scroll-blue::-webkit-scrollbar-thumb { + background: #00b0ef; + border-radius: 20px; +} + +.slim-scroll-blue::-webkit-scrollbar-track { + background: transparent; + border-radius: 20px; +} + +/* Layout containers */ + +#flex-body { + display: flex; + flex: 1; + align-items: start; +} + +#content-wrapper { + flex: 1; + margin: 0 auto; + min-width: 0; + max-width: 1000px; + overflow-x: auto; + padding: 0.8rem 20px 0 20px; + transition: 0.4s; + -webkit-transition: 0.4s; +} + +#site-nav, +#page-nav { + display: flex; + flex-direction: column; + position: sticky; + top: var(--sticky-header-height); + flex: 0 0 auto; + max-width: 300px; + max-height: calc(100vh - var(--sticky-header-height)); + width: 300px; +} + +#site-nav { + border-right: 1px solid lightgrey; + padding-bottom: 20px; + z-index: 999; +} + +.site-nav-top { + margin: 0.8rem 0; + padding: 0 12px 12px 12px; +} + +.nav-component { + overflow-y: auto; +} + +#page-nav { + border-left: 1px solid lightgrey; +} + +@media screen and (max-width: 1299.98px) { + #page-nav { + display: none; + } +} + +/* Bootstrap medium(md) responsive breakpoint */ +@media screen and (max-width: 991.98px) { + #site-nav { + display: none; + } +} + +/* Bootstrap small(sm) responsive breakpoint */ +@media (max-width: 767.98px) { + .indented { + padding-left: 10px; + } + + #content-wrapper { + padding: 0 10px; + } +} + +/* Bootstrap extra small(xs) responsive breakpoint */ +@media screen and (max-width: 575.98px) { + #site-nav { + display: none; + } +} + +/* Hide site navigation when printing */ +@media print { + #site-nav { + display: none; + } + + #page-nav { + display: none; + } + + /* Reduce font size when printing */ + h1 { + font-size: 1.2rem !important; + } + h2 { + font-size: 1.0rem !important; + } + h3 { + font-size: 0.9rem !important; + } + h4 { + font-size: 0.8rem !important; + } + h5 { + font-size: 0.7rem !important; + } + body { + font-size: 0.65rem !important; + } + .btn { + font-size: 0.65rem !important; + } + img { + zoom: 0.8; /* might not work on some browsers */ + } +} + +h2 { + color: #e62e2e +} + +h3 { + color: #f78d22 +} + +h4 { + color: #38a0d4 +} + +h5 { + color: #009616 +} + +h6 { + color: #032b43 +} diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md index 773a07794e2..86aa7ebfc34 100644 --- a/docs/team/johndoe.md +++ b/docs/team/johndoe.md @@ -1,6 +1,6 @@ --- -layout: page -title: John Doe's Project Portfolio Page + layout: default.md + title: "John Doe's Project Portfolio Page" --- ### Project: AddressBook Level 3 diff --git a/docs/tutorials/AddRemark.md b/docs/tutorials/AddRemark.md index d98f38982e7..e63199e2af7 100644 --- a/docs/tutorials/AddRemark.md +++ b/docs/tutorials/AddRemark.md @@ -1,8 +1,11 @@ --- -layout: page -title: "Tutorial: Adding a command" + layout: default.md + title: "Tutorial: Adding a command" + pageNav: 3 --- +# Tutorial: Adding a command + Let's walk you through the implementation of a new command — `remark`. This command allows users of the AddressBook application to add optional remarks to people in their address book and edit it if required. The command should have the following format: @@ -22,7 +25,7 @@ For now, let’s keep `RemarkCommand` as simple as possible and print some outpu **`RemarkCommand.java`:** -``` java +```java package seedu.address.logic.commands; import seedu.address.model.Model; @@ -57,13 +60,13 @@ Run `Main#main` and try out your new `RemarkCommand`. If everything went well, y While we have successfully printed a message to `ResultDisplay`, the command does not do what it is supposed to do. Let’s change the command to throw a `CommandException` to accurately reflect that our command is still a work in progress. -![The relationship between RemarkCommand and Command](../images/add-remark/RemarkCommandClass.png) + Following the convention in other commands, we add relevant messages as constants and use them. **`RemarkCommand.java`:** -``` java +```java public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the remark of the person identified " + "by the index number used in the last person listing. " @@ -90,7 +93,7 @@ Let’s change `RemarkCommand` to parse input from the user. We start by modifying the constructor of `RemarkCommand` to accept an `Index` and a `String`. While we are at it, let’s change the error message to echo the values. While this is not a replacement for tests, it is an obvious way to tell if our code is functioning as intended. -``` java +```java import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; //... public class RemarkCommand extends Command { @@ -142,13 +145,13 @@ Now let’s move on to writing a parser that will extract the index and remark f Create a `RemarkCommandParser` class in the `seedu.address.logic.parser` package. The class must extend the `Parser` interface. -![The relationship between Parser and RemarkCommandParser](../images/add-remark/RemarkCommandParserClass.png) + Thankfully, `ArgumentTokenizer#tokenize()` makes it trivial to parse user input. Let’s take a look at the JavaDoc provided for the function to understand what it does. **`ArgumentTokenizer.java`:** -``` java +```java /** * Tokenizes an arguments string and returns an {@code ArgumentMultimap} * object that maps prefixes to their respective argument values. Only the @@ -166,7 +169,7 @@ We can tell `ArgumentTokenizer#tokenize()` to look out for our new prefix `r/` a **`ArgumentMultimap.java`:** -``` java +```java /** * Returns the last value of {@code prefix}. */ @@ -181,7 +184,7 @@ This appears to be what we need to get a String of the remark. But what about th **`DeleteCommandParser.java`:** -``` java +```java Index index = ParserUtil.parseIndex(args); return new DeleteCommand(index); ``` @@ -192,7 +195,7 @@ Now that we have the know-how to extract the data that we need from the user’s **`RemarkCommandParser.java`:** -``` java +```java public RemarkCommand parse(String args) throws ParseException { requireNonNull(args); ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, @@ -212,11 +215,11 @@ public RemarkCommand parse(String args) throws ParseException { } ``` -
    + -:information_source: Don’t forget to update `AddressBookParser` to use our new `RemarkCommandParser`! +Don’t forget to update `AddressBookParser` to use our new `RemarkCommandParser`! -
    + If you are stuck, check out the sample [here](https://github.com/se-edu/addressbook-level3/commit/dc6d5139d08f6403da0ec624ea32bd79a2ae0cbf#diff-8bf239e8e9529369b577701303ddd96af93178b4ed6735f91c2d8488b20c6b4a). @@ -244,7 +247,7 @@ Simply add the following to [`seedu.address.ui.PersonCard`](https://github.com/s **`PersonCard.java`:** -``` java +```java @FXML private Label remark; ``` @@ -276,11 +279,11 @@ We change the constructor of `Person` to take a `Remark`. We will also need to d Unfortunately, a change to `Person` will cause other commands to break, you will have to modify these commands to use the updated `Person`! -
    + -:bulb: Use the `Find Usages` feature in IntelliJ IDEA on the `Person` class to find these commands. +Use the `Find Usages` feature in IntelliJ IDEA on the `Person` class to find these commands. -
    + Refer to [this commit](https://github.com/se-edu/addressbook-level3/commit/ce998c37e65b92d35c91d28c7822cd139c2c0a5c) and check that you have got everything in order! @@ -291,11 +294,11 @@ AddressBook stores data by serializing `JsonAdaptedPerson` into `json` with the While the changes to code may be minimal, the test data will have to be updated as well. -
    + -:exclamation: You must delete AddressBook’s storage file located at `/data/addressbook.json` before running it! Not doing so will cause AddressBook to default to an empty address book! +You must delete AddressBook’s storage file located at `/data/avengersassemble.json` before running it! Not doing so will cause AddressBook to default to an empty address book! -
    + Check out [this commit](https://github.com/se-edu/addressbook-level3/commit/556cbd0e03ff224d7a68afba171ad2eb0ce56bbf) to see what the changes entail. @@ -308,7 +311,7 @@ Just add [this one line of code!](https://github.com/se-edu/addressbook-level3/c **`PersonCard.java`:** -``` java +```java public PersonCard(Person person, int displayedIndex) { //... remark.setText(person.getRemark().value); @@ -328,7 +331,7 @@ save it with `Model#setPerson()`. **`RemarkCommand.java`:** -``` java +```java //... public static final String MESSAGE_ADD_REMARK_SUCCESS = "Added remark to Person: %1$s"; public static final String MESSAGE_DELETE_REMARK_SUCCESS = "Removed remark from Person: %1$s"; diff --git a/docs/tutorials/RemovingFields.md b/docs/tutorials/RemovingFields.md index f29169bc924..c73bd379e5e 100644 --- a/docs/tutorials/RemovingFields.md +++ b/docs/tutorials/RemovingFields.md @@ -1,8 +1,11 @@ --- -layout: page -title: "Tutorial: Removing Fields" + layout: default.md + title: "Tutorial: Removing Fields" + pageNav: 3 --- +# Tutorial: Removing Fields + > Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away. > > — Antoine de Saint-Exupery @@ -10,17 +13,17 @@ title: "Tutorial: Removing Fields" When working on an existing code base, you will most likely find that some features that are no longer necessary. This tutorial aims to give you some practice on such a code 'removal' activity by removing the `address` field from `Person` class. -
    + **If you have done the [Add `remark` command tutorial](AddRemark.html) already**, you should know where the code had to be updated to add the field `remark`. From that experience, you can deduce where the code needs to be changed to _remove_ that field too. The removing of the `address` field can be done similarly.

    However, if you have no such prior knowledge, removing a field can take a quite a bit of detective work. This tutorial takes you through that process. **At least have a read even if you don't actually do the steps yourself.** -
    + -* Table of Contents -{:toc} + + ## Safely deleting `Address` @@ -50,10 +53,10 @@ Let’s try removing references to `Address` in `EditPersonDescriptor`. 1. Remove the usages of `address` and select `Do refactor` when you are done. -
    + - :bulb: **Tip:** Removing usages may result in errors. Exercise discretion and fix them. For example, removing the `address` field from the `Person` class will require you to modify its constructor. -
    + **Tip:** Removing usages may result in errors. Exercise discretion and fix them. For example, removing the `address` field from the `Person` class will require you to modify its constructor. + 1. Repeat the steps for the remaining usages of `Address` @@ -71,7 +74,7 @@ A quick look at the `PersonCard` class and its `fxml` file quickly reveals why i **`PersonCard.java`** -``` java +```java ... @FXML private Label address; diff --git a/docs/tutorials/TracingCode.md b/docs/tutorials/TracingCode.md index 4fb62a83ef6..2b1b0f2d6b7 100644 --- a/docs/tutorials/TracingCode.md +++ b/docs/tutorials/TracingCode.md @@ -1,26 +1,30 @@ --- -layout: page -title: "Tutorial: Tracing code" + layout: default.md + title: "Tutorial: Tracing code" + pageNav: 3 --- +# Tutorial: Tracing code + + > Indeed, the ratio of time spent reading versus writing is well over 10 to 1. We are constantly reading old code as part of the effort to write new code. …​\[Therefore,\] making it easy to read makes it easier to write. > > — Robert C. Martin Clean Code: A Handbook of Agile Software Craftsmanship When trying to understand an unfamiliar code base, one common strategy used is to trace some representative execution path through the code base. One easy way to trace an execution path is to use a debugger to step through the code. In this tutorial, you will be using the IntelliJ IDEA’s debugger to trace the execution path of a specific user command. -* Table of Contents -{:toc} + + ## Before we start Before we jump into the code, it is useful to get an idea of the overall structure and the high-level behavior of the application. This is provided in the 'Architecture' section of the developer guide. In particular, the architecture diagram (reproduced below), tells us that the App consists of several components. -![ArchitectureDiagram](../images/ArchitectureDiagram.png) + It also has a sequence diagram (reproduced below) that tells us how a command propagates through the App. - + Note how the diagram shows only the execution flows _between_ the main components. That is, it does not show details of the execution path *inside* each component. By hiding those details, the diagram aims to inform the reader about the overall execution path of a command without overwhelming the reader with too much details. In this tutorial, you aim to find those omitted details so that you get a more in-depth understanding of how the code works. @@ -37,16 +41,16 @@ As you know, the first step of debugging is to put in a breakpoint where you wan In our case, we would want to begin the tracing at the very point where the App start processing user input (i.e., somewhere in the UI component), and then trace through how the execution proceeds through the UI component. However, the execution path through a GUI is often somewhat obscure due to various *event-driven mechanisms* used by GUI frameworks, which happens to be the case here too. Therefore, let us put the breakpoint where the `UI` transfers control to the `Logic` component. - + According to the sequence diagram you saw earlier (and repeated above for reference), the `UI` component yields control to the `Logic` component through a method named `execute`. Searching through the code base for an `execute()` method that belongs to the `Logic` component yields a promising candidate in `seedu.address.logic.Logic`. -
    + -:bulb: **Intellij Tip:** The ['**Search Everywhere**' feature](https://www.jetbrains.com/help/idea/searching-everywhere.html) can be used here. In particular, the '**Find Symbol**' ('Symbol' here refers to methods, variables, classes etc.) variant of that feature is quite useful here as we are looking for a _method_ named `execute`, not simply the text `execute`. -
    +**Intellij Tip:** The ['**Search Everywhere**' feature](https://www.jetbrains.com/help/idea/searching-everywhere.html) can be used here. In particular, the '**Find Symbol**' ('Symbol' here refers to methods, variables, classes etc.) variant of that feature is quite useful here as we are looking for a _method_ named `execute`, not simply the text `execute`. + A quick look at the `seedu.address.logic.Logic` (an extract given below) confirms that this indeed might be what we’re looking for. @@ -67,14 +71,14 @@ public interface Logic { But apparently, this is an interface, not a concrete implementation. That should be fine because the [Architecture section of the Developer Guide](../DeveloperGuide.html#architecture) tells us that components interact through interfaces. Here's the relevant diagram: - + Next, let's find out which statement(s) in the `UI` code is calling this method, thus transferring control from the `UI` to the `Logic`. -
    + -:bulb: **Intellij Tip:** The ['**Find Usages**' feature](https://www.jetbrains.com/help/idea/find-highlight-usages.html#find-usages) can find from which parts of the code a class/method/variable is being used. -
    +**Intellij Tip:** The ['**Find Usages**' feature](https://www.jetbrains.com/help/idea/find-highlight-usages.html#find-usages) can find from which parts of the code a class/method/variable is being used. + ![`Find Usages` tool window. `Edit` \> `Find` \> `Find Usages`.](../images/tracing/FindUsages.png) @@ -87,10 +91,10 @@ Now let’s set the breakpoint. First, double-click the item to reach the corres Recall from the User Guide that the `edit` command has the format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` For this tutorial we will be issuing the command `edit 1 n/Alice Yeoh`. -
    + -:bulb: **Tip:** Over the course of the debugging session, you will encounter every major component in the application. Try to keep track of what happens inside the component and where the execution transfers to another component. -
    +**Tip:** Over the course of the debugging session, you will encounter every major component in the application. Try to keep track of what happens inside the component and where the execution transfers to another component. + 1. To start the debugging session, simply `Run` \> `Debug Main` @@ -110,7 +114,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ **LogicManager\#execute().** - ``` java + ```java @Override public CommandResult execute(String commandText) throws CommandException, ParseException { @@ -142,7 +146,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ ![StepOver](../images/tracing/StepOver.png) 1. _Step into_ the line where user input in parsed from a String to a Command, which should bring you to the `AddressBookParser#parseCommand()` method (partial code given below): - ``` java + ```java public Command parseCommand(String userInput) throws ParseException { ... final String commandWord = matcher.group("commandWord"); @@ -157,7 +161,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ 1. Stepping through the `switch` block, we end up at a call to `EditCommandParser().parse()` as expected (because the command we typed is an edit command). - ``` java + ```java ... case EditCommand.COMMAND_WORD: return new EditCommandParser().parse(arguments); @@ -166,8 +170,10 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ 1. Let’s see what `EditCommandParser#parse()` does by stepping into it. You might have to click the 'step into' button multiple times here because there are two method calls in that statement: `EditCommandParser()` and `parse()`. -
    :bulb: **Intellij Tip:** Sometimes, you might end up stepping into functions that are not of interest. Simply use the `step out` button to get out of them! -
    + + + **Intellij Tip:** Sometimes, you might end up stepping into functions that are not of interest. Simply use the `step out` button to get out of them! + 1. Stepping through the method shows that it calls `ArgumentTokenizer#tokenize()` and `ParserUtil#parseIndex()` to obtain the arguments and index required. @@ -175,17 +181,17 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ ![EditCommand](../images/tracing/EditCommand.png) 1. As you just traced through some code involved in parsing a command, you can take a look at this class diagram to see where the various parsing-related classes you encountered fit into the design of the `Logic` component. - + 1. Let’s continue stepping through until we return to `LogicManager#execute()`. The sequence diagram below shows the details of the execution path through the Logic component. Does the execution path you traced in the code so far match the diagram?
    - ![Tracing an `edit` command through the Logic component](../images/tracing/LogicSequenceDiagram.png) + 1. Now, step over until you read the statement that calls the `execute()` method of the `EditCommand` object received, and step into that `execute()` method (partial code given below): **`EditCommand#execute()`:** - ``` java + ```java @Override public CommandResult execute(Model model) throws CommandException { ... @@ -205,25 +211,28 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ * it uses the `updateFilteredPersonList` method to ask the `Model` to populate the 'filtered list' with _all_ persons.
    FYI, The 'filtered list' is the list of persons resulting from the most recent operation that will be shown to the user immediately after. For the `edit` command, we populate it with all the persons so that the user can see the edited person along with all other persons. If this was a `find` command, we would be setting that list to contain the search results instead.
    To provide some context, given below is the class diagram of the `Model` component. See if you can figure out where the 'filtered list' of persons is being tracked. -
    +
    * :bulb: This may be a good time to read through the [`Model` component section of the DG](../DeveloperGuide.html#model-component) 1. As you step through the rest of the statements in the `EditCommand#execute()` method, you'll see that it creates a `CommandResult` object (containing information about the result of the execution) and returns it.
    Advancing the debugger by one more step should take you back to the middle of the `LogicManager#execute()` method.
    1. Given that you have already seen quite a few classes in the `Logic` component in action, see if you can identify in this partial class diagram some of the classes you've encountered so far, and see how they fit into the class structure of the `Logic` component: - + + * :bulb: This may be a good time to read through the [`Logic` component section of the DG](../DeveloperGuide.html#logic-component) 1. Similar to before, you can step over/into statements in the `LogicManager#execute()` method to examine how the control is transferred to the `Storage` component and what happens inside that component. -
    :bulb: **Intellij Tip:** When trying to step into a statement such as `storage.saveAddressBook(model.getAddressBook())` which contains multiple method calls, Intellij will let you choose (by clicking) which one you want to step into. -
    + + + **Intellij Tip:** When trying to step into a statement such as `storage.saveAddressBook(model.getAddressBook())` which contains multiple method calls, Intellij will let you choose (by clicking) which one you want to step into. + -1. As you step through the code inside the `Storage` component, you will eventually arrive at the `JsonAddressBook#saveAddressBook()` method which calls the `JsonSerializableAddressBook` constructor, to create an object that can be _serialized_ (i.e., stored in storage medium) in JSON format. That constructor is given below (with added line breaks for easier readability): +1. As you step through the code inside the `Storage` component, you will eventually arrive at the `JsonAddressBook#saveAddressBook()` method which calls the `JsonSerializableAddressBook` constructor, to create an object that can be _serialized_ (i.e., stored in storage medium) in JSON format. That constructor is given below (with added line breaks for easier readability): **`JsonSerializableAddressBook` constructor:** - ``` java + ```java /** * Converts a given {@code ReadOnlyAddressBook} into this class for Jackson use. * @@ -243,7 +252,8 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ This is because regular Java objects need to go through an _adaptation_ for them to be suitable to be saved in JSON format. 1. While you are stepping through the classes in the `Storage` component, here is the component's class diagram to help you understand how those classes fit into the structure of the component.
    - + + * :bulb: This may be a good time to read through the [`Storage` component section of the DG](../DeveloperGuide.html#storage-component) 1. We can continue to step through until you reach the end of the `LogicManager#execute()` method and return to the `MainWindow#executeCommand()` method (the place where we put the original breakpoint). @@ -251,7 +261,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ 1. Stepping into `resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser());`, we end up in: **`ResultDisplay#setFeedbackToUser()`** - ``` java + ```java public void setFeedbackToUser(String feedbackToUser) { requireNonNull(feedbackToUser); resultDisplay.setText(feedbackToUser); diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 3d6bd06d5af..e645d997555 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -36,7 +36,7 @@ */ public class MainApp extends Application { - public static final Version VERSION = new Version(0, 2, 2, true); + public static final Version VERSION = new Version(1, 3, 0, false); private static final Logger logger = LogsCenter.getLogger(MainApp.class); diff --git a/src/main/java/seedu/address/commons/core/GuiSettings.java b/src/main/java/seedu/address/commons/core/GuiSettings.java index a97a86ee8d7..fa18f527fdd 100644 --- a/src/main/java/seedu/address/commons/core/GuiSettings.java +++ b/src/main/java/seedu/address/commons/core/GuiSettings.java @@ -12,8 +12,8 @@ */ public class GuiSettings implements Serializable { - private static final double DEFAULT_HEIGHT = 600; - private static final double DEFAULT_WIDTH = 740; + private static final double DEFAULT_HEIGHT = 1000; + private static final double DEFAULT_WIDTH = 1500; private final double windowWidth; private final double windowHeight; diff --git a/src/main/java/seedu/address/commons/exceptions/DataLoadingException.java b/src/main/java/seedu/address/commons/exceptions/DataLoadingException.java index 9904ba47afe..1f40c4bc423 100644 --- a/src/main/java/seedu/address/commons/exceptions/DataLoadingException.java +++ b/src/main/java/seedu/address/commons/exceptions/DataLoadingException.java @@ -7,5 +7,8 @@ public class DataLoadingException extends Exception { public DataLoadingException(Exception cause) { super(cause); } + public DataLoadingException(String message) { + super(message); + } } diff --git a/src/main/java/seedu/address/commons/util/CsvUtil.java b/src/main/java/seedu/address/commons/util/CsvUtil.java new file mode 100644 index 00000000000..0f01210046e --- /dev/null +++ b/src/main/java/seedu/address/commons/util/CsvUtil.java @@ -0,0 +1,270 @@ +package seedu.address.commons.util; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Queue; +import java.util.Set; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.dataformat.csv.CsvMapper; +import com.fasterxml.jackson.dataformat.csv.CsvSchema; +import com.opencsv.CSVParser; +import com.opencsv.CSVParserBuilder; +import com.opencsv.CSVReader; +import com.opencsv.CSVReaderBuilder; +import com.opencsv.exceptions.CsvException; + +import javafx.util.Pair; +import seedu.address.commons.exceptions.DataLoadingException; +import seedu.address.logic.commands.exceptions.CommandException; + +/** + * Utility class for processing CSV files. + * Acknowledgement: The methods in this class were made possible with the help of + * the [OpenCSV](http://opencsv.sourceforge.net/) library. + */ +public class CsvUtil { + + public static final String MESSAGE_ERROR_READING_FILE = "Error reading file: "; + + /** + * Reads a CSV file. + * Firstly, it checks if the compulsory parameters are present in the header of the csv file (the header + * is the first line of the csv file). + * Secondly, it only reads the columns that are in compulsoryParameters or optionalParameters. + * Values in other headers will be ignored. + * Thirdly, it checks if the number of values in each row is the same as the number of headers. + * If the check for compulsory parameters fails, the error report will contain the missing compulsory parameters. + * If the check for the number of values in each row fails, the error report will contain the rows that do not have + * the same number of values as the number of headers. + * It is crucial that a dataLoadingException is thrown in the + * caller of this method if the error report is not empty. + * @param filePath the path of the file + * @param compulsoryParameters + * @param optionalParameters + * @return + * A pair in which the first value is the data and the second is the error report. Data can be null if + * the file is not read successfully or if any compulsory parameters are missing in the header row of csv file. + * @throws IOException + */ + public static Pair>>, String> readCsvFile( + Path filePath, HashSet compulsoryParameters, HashSet optionalParameters) + throws IOException { + List> data = null; + Optional>> nullableData = Optional.empty(); + String errorMsgs = ""; + try { + CSVReader reader = new CSVReaderBuilder(new FileReader(filePath.toString())).build(); + Optional headers = Optional.ofNullable(reader.readNext()); // first line should be headers + if (headers.isPresent()) { + List headersList = List.of(headers.get()); + List columnsToSkip = columnsToSkip(headersList, compulsoryParameters, optionalParameters); + List rows = reader.readAll(); + Pair>, String> result = parseData(rows, headersList, columnsToSkip); + data = result.getKey(); + errorMsgs = result.getValue(); + nullableData = Optional.of(data); + } + + return new Pair<>(nullableData, errorMsgs); + } catch (DataLoadingException | CsvException e) { + return new Pair<>(nullableData, e.getMessage()); + } + } + + /** + * Returns a list of columns to skip in the CSV file. The columns to skip are the headers that are not in + * compulsoryParameters or optionalParameters. + * @param headers + * @param compulsoryParameters + * @param optionalParameters + * @return columnsToSkip + * @throws DataLoadingException + */ + public static List columnsToSkip( + List headers, HashSet compulsoryParameters, HashSet optionalParameters) + throws DataLoadingException { + // first check if the compulsory parameters are present in the header + checkCompulsoryParameters(compulsoryParameters, headers); + List columnsToSkip = new ArrayList<>(); + HashSet uniqueHeaders = new HashSet<>(); + for (int i = 0; i < headers.size(); i++) { + if ((!compulsoryParameters.contains(headers.get(i)) + && !optionalParameters.contains(headers.get(i))) + | uniqueHeaders.contains(headers.get(i))) { + columnsToSkip.add(i); + } else { + uniqueHeaders.add(headers.get(i)); + } + + } + + return columnsToSkip; + } + + /** + * Parses the data from the CSV file. + * @param rows + * @param headers + * @param columnsToSkip + * @return + */ + public static Pair>, String> parseData( + List rows, List headers, List columnsToSkip) { + List> data = new ArrayList<>(); + StringBuilder errorMsgs = new StringBuilder(); // to store which rows do not have the same size as the header + Queue columnsToSkipQueue; + for (int i = 0; i < rows.size(); i++) { + String[] row = rows.get(i); + Map map = new HashMap<>(); + columnsToSkipQueue = new LinkedList<>(columnsToSkip); + if (row.length != headers.size()) { + errorMsgs.append( + String.format("Row %d does not have the same number of values as the number of headers." + + "Given: %d, Expected: %d\n", i, row.length, headers.size())); + continue; + } + for (int j = 0; j < row.length; j++) { + if (!columnsToSkipQueue.isEmpty() && columnsToSkipQueue.peek() == j) { + columnsToSkipQueue.remove(); + continue; + } + map.put(headers.get(j), row[j]); + } + data.add(map); + } + + return new Pair<>(data, errorMsgs.toString()); + } + + /** + * Checks if the compulsory parameters are present in the header of the csv file. + * @param compulsoryParameters + * @param headers + * @throws DataLoadingException + */ + public static void checkCompulsoryParameters(HashSet compulsoryParameters, List headers) + throws DataLoadingException { + StringBuilder missingParameters = new StringBuilder("Missing compulsory header(s) in Csv file:"); + boolean hasMissingParameters = false; + for (String compulsoryParameter : compulsoryParameters) { + if (!headers.contains(compulsoryParameter)) { + hasMissingParameters = true; + missingParameters.append(String.format(" %s,", compulsoryParameter)); + } + } + if (hasMissingParameters) { + throw new DataLoadingException(missingParameters.toString()); + } + } + /** + * Reads all lines from a CSV file. + * @param filePath the path of the file + * @return a list of String arrays representing the lines of the CSV file + * @throws CommandException if an error occurs while reading the file + */ + public static List readAllLinesForImportExamScores(Path filePath) throws CommandException { + try (Reader reader = Files.newBufferedReader(filePath)) { + return readCsvForImportExamScores(reader); + } catch (IOException | CsvException exception) { + throw new CommandException(MESSAGE_ERROR_READING_FILE + filePath.toString()); + } + } + + /** + * Reads a CSV file. + * @param reader the reader to read the file + * @return a list of String arrays representing the lines of the CSV file + * @throws IOException if an error occurs while reading the file + * @throws CsvException if an error occurs while parsing the CSV file + */ + private static List readCsvForImportExamScores(Reader reader) throws IOException, CsvException { + CSVParser parser = new CSVParserBuilder().build(); + CSVReader csvReader = new CSVReaderBuilder(reader).withCSVParser(parser).build(); + List lst = csvReader.readAll(); + return lst; + } + + /** + * Builds the CSV schema from a JSON array. + * + * @param personsArray The JSON persons array to derive the schema from. + * @param examsArray The JSON exams array to derive the schema from. + * @return The CSV schema. + */ + public static CsvSchema buildCsvSchema(JsonNode personsArray, JsonNode examsArray) { + CsvSchema.Builder csvSchemaBuilder = CsvSchema.builder(); + JsonNode firstObject = personsArray.elements().next(); + Set addedColumns = new HashSet<>(); // Should addedColumns be Hashset? + + firstObject.fieldNames().forEachRemaining(fieldName -> { + if (!fieldName.equals("examScores")) { + csvSchemaBuilder.addColumn(fieldName); + addedColumns.add(fieldName); + } + }); + + if (examsArray != null && examsArray.size() != 0) { + for (JsonNode exam : examsArray) { + String examName = "Exam:" + exam.get("name").asText(); + if (!addedColumns.contains(examName)) { + csvSchemaBuilder.addColumn(examName); + addedColumns.add(examName); + } + } + } + + CsvSchema csvSchema = csvSchemaBuilder + .build() + .withHeader(); + return csvSchema; + } + + /** + * Writes JSON data to a CSV file. + * + * @param csvFile The CSV file to write to. + * @param personsArray The JSON person array to be written to CSV. + * @param examsArray The JSON exams array to be written to CSV. + * @throws IOException If an I/O error occurs during file writing. + */ + public static void writeToCsvFile(File csvFile, JsonNode personsArray, JsonNode examsArray) throws IOException { + CsvSchema csvSchema = buildCsvSchema(personsArray, examsArray); + CsvMapper csvMapper = new CsvMapper(); + + ArrayNode modifiedJsonTree = JsonNodeFactory.instance.arrayNode(); + for (JsonNode person : personsArray) { + ObjectNode modifiedJsonNode = ((ObjectNode) person).deepCopy(); + JsonNode examScores = modifiedJsonNode.get("examScores"); + modifiedJsonNode.remove("examScores"); + if (examScores != null && examScores.isArray()) { + examScores.forEach(exam -> { + String fieldName = "Exam:" + exam.get("examName").asText(); + Double score = exam.get("score").asDouble(); + ObjectNode scoreNode = JsonNodeFactory.instance.objectNode(); + scoreNode.put(fieldName, score); + modifiedJsonNode.setAll(scoreNode); + }); + } + modifiedJsonTree.add(modifiedJsonNode); + } + + csvMapper.writerFor(JsonNode.class) + .with(csvSchema) + .writeValue(csvFile, modifiedJsonTree); + } +} diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/address/commons/util/StringUtil.java index 61cc8c9a1cb..98ec933a8b2 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/seedu/address/commons/util/StringUtil.java @@ -38,6 +38,49 @@ public static boolean containsWordIgnoreCase(String sentence, String word) { .anyMatch(preppedWord::equalsIgnoreCase); } + /** + * Returns true if the {@code sentence} contains the {@code word}. + * Ignores case, and a full match is not required. + *
    examples:
    +     *       containsSubstringIgnoreCase("ABc def", "abc") == true
    +     *       containsSubstringIgnoreCase("ABc def", "DEF") == true
    +     *       containsSubstringIgnoreCase("ABc def", "AB") == true
    +     *       
    + * @param sentence cannot be null + * @param subString cannot be null, cannot be empty + */ + public static boolean containsSubstringIgnoreCase(String sentence, String subString) { + requireNonNull(sentence); + requireNonNull(subString); + + String preppedString = subString.trim(); + checkArgument(!preppedString.isEmpty(), "Substring parameter cannot be empty"); + + return sentence.toLowerCase().contains(preppedString.toLowerCase()); + } + + /** + * Returns true if the {@code sentence} is equal to the {@code word}. + * Ignores case. + *
    examples:
    +     *      equalsIgnoreCase("ABc def", "abc def") == true
    +     *      equalsIgnoreCase("ABc def", "DEF") == false
    +     *      equalsIgnoreCase("ABc def", "ABc de") == false
    +     *      
    + * @param sentence cannot be null + * @param word cannot be null + * @return true if the {@code sentence} is equal to the {@code word} + */ + public static boolean equalsIgnoreCase(String sentence, String word) { + requireNonNull(sentence); + requireNonNull(word); + + String preppedString = word.trim(); + checkArgument(!preppedString.isEmpty(), "Word parameter cannot be empty"); + + return sentence.toLowerCase().equals(preppedString.toLowerCase()); + } + /** * Returns a detailed message of the t, including the stack trace. */ @@ -65,4 +108,23 @@ public static boolean isNonZeroUnsignedInteger(String s) { return false; } } + + /** + * Returns true if {@code s} represents a non-negative unsigned double + * e.g. 0.0, 1.0, 2.0, 3.0, ..., {@code Double.MAX_VALUE}
    + * Will return false for any other non-null string input + * e.g. empty string, "-1.0", "+1.0", and " 2.0 " (untrimmed), + * "3.0 0.0" (contains whitespace), "1.0 a" (contains letters) + * @throws NullPointerException if {@code s} is null. + */ + public static boolean isNonNegativeUnsignedDouble(String s) { + requireNonNull(s); + + try { + double value = Double.parseDouble(s); + return value >= 0 && !s.startsWith("+"); // "+1.0" is successfully parsed by Double#parseDouble(String) + } catch (NumberFormatException nfe) { + return false; + } + } } diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 92cd8fa605a..d95ec680463 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -2,12 +2,15 @@ import java.nio.file.Path; +import javafx.beans.value.ObservableValue; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ScoreStatistics; +import seedu.address.model.exam.Exam; import seedu.address.model.person.Person; /** @@ -33,6 +36,19 @@ public interface Logic { /** Returns an unmodifiable view of the filtered list of persons */ ObservableList getFilteredPersonList(); + /** Returns an unmodifiable view of the list of exams */ + ObservableList getExamList(); + + /** + * Returns the selected exam in the exam list. + */ + ObservableValue getSelectedExam(); + + /** + * Returns the exam score statistics for the given exam. + */ + ObservableValue getSelectedExamStatistics(); + /** * Returns the user prefs' address book file path. */ diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 5aa3b91c7d0..b67a7d2937d 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -5,6 +5,7 @@ import java.nio.file.Path; import java.util.logging.Logger; +import javafx.beans.value.ObservableValue; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; @@ -15,6 +16,8 @@ import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ScoreStatistics; +import seedu.address.model.exam.Exam; import seedu.address.model.person.Person; import seedu.address.storage.Storage; @@ -71,6 +74,21 @@ public ObservableList getFilteredPersonList() { return model.getFilteredPersonList(); } + @Override + public ObservableList getExamList() { + return model.getExamList(); + } + + @Override + public ObservableValue getSelectedExam() { + return model.getSelectedExam(); + } + + @Override + public ObservableValue getSelectedExamStatistics() { + return model.getSelectedExamStatistics(); + } + @Override public Path getAddressBookFilePath() { return model.getAddressBookFilePath(); diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java index ecd32c31b53..badb42b23b4 100644 --- a/src/main/java/seedu/address/logic/Messages.java +++ b/src/main/java/seedu/address/logic/Messages.java @@ -5,6 +5,7 @@ import java.util.stream.Stream; import seedu.address.logic.parser.Prefix; +import seedu.address.model.exam.Exam; import seedu.address.model.person.Person; /** @@ -13,11 +14,15 @@ public class Messages { public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; + public static final String MESSAGE_EMPTY_PERSON_LIST = "No person currently displayed"; public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; + public static final String MESSAGE_INVALID_EXAM_DISPLAYED_INDEX = "The exam index provided is invalid"; public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; public static final String MESSAGE_DUPLICATE_FIELDS = "Multiple values specified for the following single-valued field(s): "; + public static final String MESSAGE_MISSING_COMPULSORY_PREFIXES = "Missing compulsory prefixes: "; + public static final String MESSAGE_NO_EXAM_SELECTED = "No exam selected. Please select an exam first."; /** * Returns an error message indicating the duplicate prefixes. @@ -43,9 +48,36 @@ public static String format(Person person) { .append(person.getEmail()) .append("; Address: ") .append(person.getAddress()) + .append("; Matriculation Number: ") + .append(person.getMatric()) + .append("; Reflection: ") + .append(person.getReflection()) + .append("; Studio: ") + .append(person.getStudio()) .append("; Tags: "); person.getTags().forEach(builder::append); + builder.append("; Scores: "); + person.getScores().forEach((exam, score) -> builder.append("[ ") + .append(exam.getName()).append(": ") + .append(score).append(" / ").append(exam.getMaxScore()) + .append(" ] ") + ); + builder.append(";"); return builder.toString(); } + /** + * Formats the {@code exam} for display to the user. + * @param exam + * @return + */ + + public static String format(Exam exam) { + final StringBuilder builder = new StringBuilder(); + builder.append("Exam Name: ") + .append(exam.getName()) + .append("; Maximum Score: ") + .append(exam.getMaxScore()); + return builder.toString(); + } } diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index 5d7185a9680..4e1764d63cb 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -3,8 +3,11 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MATRIC_NUMBER; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_REFLECTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_STUDIO; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import seedu.address.commons.util.ToStringBuilder; @@ -26,17 +29,21 @@ public class AddCommand extends Command { + PREFIX_PHONE + "PHONE " + PREFIX_EMAIL + "EMAIL " + PREFIX_ADDRESS + "ADDRESS " + + "[" + PREFIX_MATRIC_NUMBER + "MATRICULATION NUMBER] " + + "[" + PREFIX_REFLECTION + "REFLECTION] " + + "[" + PREFIX_STUDIO + "STUDIO] " + "[" + PREFIX_TAG + "TAG]...\n" + "Example: " + COMMAND_WORD + " " + PREFIX_NAME + "John Doe " + PREFIX_PHONE + "98765432 " + PREFIX_EMAIL + "johnd@example.com " + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " + + PREFIX_MATRIC_NUMBER + "A1234567X " + PREFIX_TAG + "friends " + PREFIX_TAG + "owesMoney"; public static final String MESSAGE_SUCCESS = "New person added: %1$s"; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; + public static final String MESSAGE_DUPLICATE_EMAIL = "This email already exists in the address book"; private final Person toAdd; @@ -53,7 +60,7 @@ public CommandResult execute(Model model) throws CommandException { requireNonNull(model); if (model.hasPerson(toAdd)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); + throw new CommandException(MESSAGE_DUPLICATE_EMAIL); } model.addPerson(toAdd); diff --git a/src/main/java/seedu/address/logic/commands/AddExamCommand.java b/src/main/java/seedu/address/logic/commands/AddExamCommand.java new file mode 100644 index 00000000000..662aa7b7184 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddExamCommand.java @@ -0,0 +1,71 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCORE; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.exam.Exam; + +/** + * Adds an exam to the address book. + */ +public class AddExamCommand extends Command { + + public static final String COMMAND_WORD = "addExam"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds an exam to the address book. " + + "Parameters: " + PREFIX_NAME + "EXAM_NAME " + PREFIX_SCORE + "MAXIMUM_SCORE\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_NAME + "Midterm " + PREFIX_SCORE + "100"; + + public static final String MESSAGE_SUCCESS = "New exam added: %1$s"; + public static final String MESSAGE_DUPLICATE_EXAM = "This exam already exists in the address book"; + + private final Exam toAdd; + + /** + * Creates an AddExamCommand to add the specified {@code Exam} + */ + public AddExamCommand(Exam exam) { + requireNonNull(exam); + toAdd = exam; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + if (model.hasExam(toAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_EXAM); + } + + model.addExam(toAdd); + return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toAdd))); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AddExamCommand)) { + return false; + } + + AddExamCommand otherAddExamCommand = (AddExamCommand) other; + return toAdd.equals(otherAddExamCommand.toAdd); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("toAdd", toAdd) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/AddScoreCommand.java b/src/main/java/seedu/address/logic/commands/AddScoreCommand.java new file mode 100644 index 00000000000..6e3961b16d0 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddScoreCommand.java @@ -0,0 +1,89 @@ +package seedu.address.logic.commands; + +import static seedu.address.commons.util.CollectionUtil.isAnyNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCORE; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.exam.Exam; +import seedu.address.model.person.Person; +import seedu.address.model.person.Score; + + + +/** + * Adds a score to a person in the address book. + */ +public class AddScoreCommand extends Command { + + public static final String COMMAND_WORD = "addScore"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Adds a score to the person identified by the index number used in the last person listing. " + + "Parameters: INDEX (must be a positive integer) " + + PREFIX_SCORE + "SCORE\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_SCORE + "85"; + + public static final String MESSAGE_ADD_SCORE_SUCCESS = "Added score %s for %s"; + public static final String MESSAGE_SCORE_EXISTS = "This person already has a score for this exam." + + " Use editScore instead."; + public static final String MESSAGE_SCORE_GREATER_THAN_MAX = + "Score for %s cannot be greater than the maximum score."; + + private final Index targetIndex; + private final Score score; + + /** + * Creates an AddScoreCommand to add the specified {@code Score} to the person at the specified {@code Index}. + */ + public AddScoreCommand(Index targetIndex, Score score) { + requireAllNonNull(targetIndex, score); + this.targetIndex = targetIndex; + this.score = score; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + List lastShownList = model.getFilteredPersonList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToEdit = lastShownList.get(targetIndex.getZeroBased()); + Map updatedScores = new HashMap<>(personToEdit.getScores()); + Exam selectedExam = model.getSelectedExam().getValue(); + + if (!isAnyNonNull(selectedExam)) { + throw new CommandException(Messages.MESSAGE_NO_EXAM_SELECTED); + } + + if (selectedExam.getMaxScore().getScore() < score.getScore()) { + throw new CommandException(String.format(MESSAGE_SCORE_GREATER_THAN_MAX, selectedExam.getName())); + } + + if (updatedScores.containsKey(selectedExam)) { + throw new CommandException(MESSAGE_SCORE_EXISTS); + } + + model.addExamScoreToPerson(personToEdit, selectedExam, score); + return new CommandResult(String.format("Added score %s for %s", score, personToEdit.getName())); + } + + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof AddScoreCommand + && targetIndex.equals(((AddScoreCommand) other).targetIndex) + && score.equals(((AddScoreCommand) other).score)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 9c86b1fa6e4..df6859ac706 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -11,13 +11,14 @@ public class ClearCommand extends Command { public static final String COMMAND_WORD = "clear"; - public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; + public static final String MESSAGE_SUCCESS = "All persons and exams have been cleared!"; @Override public CommandResult execute(Model model) { requireNonNull(model); model.setAddressBook(new AddressBook()); + model.deselectExam(); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/Command.java b/src/main/java/seedu/address/logic/commands/Command.java index 64f18992160..68b9f092d60 100644 --- a/src/main/java/seedu/address/logic/commands/Command.java +++ b/src/main/java/seedu/address/logic/commands/Command.java @@ -12,7 +12,7 @@ public abstract class Command { * Executes the command and returns the result message. * * @param model {@code Model} which the command should operate on. - * @return feedback message of the operation result for display + * @return feedback message of the operation result for display. * @throws CommandException If an error occurs during command execution. */ public abstract CommandResult execute(Model model) throws CommandException; diff --git a/src/main/java/seedu/address/logic/commands/CopyCommand.java b/src/main/java/seedu/address/logic/commands/CopyCommand.java new file mode 100644 index 00000000000..10a755c1693 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/CopyCommand.java @@ -0,0 +1,60 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.awt.GraphicsEnvironment; +import java.awt.Toolkit; +import java.awt.datatransfer.StringSelection; +import java.util.List; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + +/** + * Copies the emails of all persons in the address book to the clipboard. + */ +public class CopyCommand extends Command { + + public static final String COMMAND_WORD = "copy"; + public static final String MESSAGE_SUCCESS = "Copied emails of all listed persons to clipboard"; + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + if (lastShownList.isEmpty()) { + throw new CommandException("No person currently displayed"); + } + + StringSelection emails = getEmails(lastShownList); + // only copies to clipboard if the environment is not headless + if (!GraphicsEnvironment.isHeadless()) { + copyToClipboard(emails); + } + + return new CommandResult(MESSAGE_SUCCESS); + } + + /** + * Returns a StringSelection containing the emails of all persons in {@code lastShownList}. + * @param lastShownList list of persons to get emails from. + * @return StringSelection containing the emails of all persons in {@code lastShownList}. + */ + public StringSelection getEmails(List lastShownList) { + StringBuilder emails = new StringBuilder(); + + for (Person person : lastShownList) { + emails.append(person.getEmail().value).append("; "); + } + return new StringSelection(emails.toString().trim()); + } + + /** + * Copies the emails of all persons in {@code lastShownList} to the clipboard. + */ + private void copyToClipboard(StringSelection stringSelection) { + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(stringSelection, stringSelection); + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index 1135ac19b74..ffef8af319d 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -20,10 +20,10 @@ public class DeleteCommand extends Command { public static final String MESSAGE_USAGE = COMMAND_WORD + ": Deletes the person identified by the index number used in the displayed person list.\n" - + "Parameters: INDEX (must be a positive integer)\n" + + "Parameter: INDEX (must be a positive integer)\n" + "Example: " + COMMAND_WORD + " 1"; - public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; + public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted person: %1$s"; private final Index targetIndex; diff --git a/src/main/java/seedu/address/logic/commands/DeleteExamCommand.java b/src/main/java/seedu/address/logic/commands/DeleteExamCommand.java new file mode 100644 index 00000000000..c5c48697337 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteExamCommand.java @@ -0,0 +1,69 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.exam.Exam; + +/** + * Deletes an exam identified using it's displayed index from the address book. + */ +public class DeleteExamCommand extends Command { + + public static final String COMMAND_WORD = "deleteExam"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the exam identified by the index number used in the displayed exam list.\n" + + "Parameter: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_DELETE_EXAM_SUCCESS = "Deleted exam: %1$s"; + + private final Index targetIndex; + + public DeleteExamCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List examList = model.getExamList(); + + if (targetIndex.getZeroBased() >= examList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_EXAM_DISPLAYED_INDEX); + } + + Exam examToDelete = examList.get(targetIndex.getZeroBased()); + model.deleteExam(examToDelete); + return new CommandResult(String.format(MESSAGE_DELETE_EXAM_SUCCESS, Messages.format(examToDelete))); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof DeleteExamCommand)) { + return false; + } + + DeleteExamCommand otherDeleteExamCommand = (DeleteExamCommand) other; + return targetIndex.equals(otherDeleteExamCommand.targetIndex); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("targetIndex", targetIndex) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteScoreCommand.java b/src/main/java/seedu/address/logic/commands/DeleteScoreCommand.java new file mode 100644 index 00000000000..7a252234368 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteScoreCommand.java @@ -0,0 +1,86 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.isAnyNonNull; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.exam.Exam; +import seedu.address.model.person.Person; +import seedu.address.model.person.Score; + +/** + * Deletes a score of currently selected test for a person in the address book. + */ +public class DeleteScoreCommand extends Command { + + public static final String COMMAND_WORD = "deleteScore"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the score of the person identified by the index number used in the displayed person list. \n" + + "Parameter: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted score for %s(%s) for %s."; + public static final String MESSAGE_NO_EXAM_SCORE_TO_DELETE = "%s(%s) does not have a score to delete. "; + + private final Index index; + + /** + * Creates a DeleteScoreCommand object to delete the specified score to the person at the specified {@code Index}. + */ + public DeleteScoreCommand(Index index) { + requireNonNull(index); + this.index = index; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List filteredPersons = model.getFilteredPersonList(); + + if (index.getZeroBased() >= filteredPersons.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Exam selectedExam = model.getSelectedExam().getValue(); + if (!isAnyNonNull(selectedExam)) { + throw new CommandException(Messages.MESSAGE_NO_EXAM_SELECTED); + } + + Person personWithScoreToDelete = filteredPersons.get(index.getZeroBased()); + Map scores = new HashMap<>(personWithScoreToDelete.getScores()); + + if (!scores.containsKey(selectedExam)) { + throw new CommandException(String.format(MESSAGE_NO_EXAM_SCORE_TO_DELETE, + personWithScoreToDelete.getName(), + personWithScoreToDelete.getEmail())); + } + + model.removeExamScoreFromPerson(personWithScoreToDelete, selectedExam); + return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, + personWithScoreToDelete.getName(), + personWithScoreToDelete.getEmail(), + selectedExam.getName())); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof DeleteScoreCommand)) { + return false; + } + + DeleteScoreCommand otherDeleteScoreCommand = (DeleteScoreCommand) other; + return index.equals(otherDeleteScoreCommand.index); + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteShownCommand.java b/src/main/java/seedu/address/logic/commands/DeleteShownCommand.java new file mode 100644 index 00000000000..dd3ce71ca76 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteShownCommand.java @@ -0,0 +1,120 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import javafx.collections.ObservableList; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + +/** + * Deletes all persons in the shown list. + */ +public class DeleteShownCommand extends Command { + public static final String COMMAND_WORD = "deleteShown"; + + public static final String MESSAGE_SUCCESS = "Deleted %d shown person(s). Listing %d remaining person(s)."; + public static final String MESSAGE_NO_PERSONS = "No persons to delete."; + public static final String MESSAGE_NO_FILTER = + "For safety, all persons cannot be deleted. Use the 'clear' command instead."; + + /** + * Deletes all persons in the last shown list. + * @param model {@code Model} which the command should operate on. + * @return a command result in which the success message is displayed + * @throws CommandException if the list is empty + */ + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + ObservableList lastShownList = model.getFilteredPersonList(); + checkForEmptyList(lastShownList); + + ensureNotAllAreDeleted(model, lastShownList); + + int deletedCount = getFilteredSize(lastShownList); + int leftOver = getLeftOverSize(model, lastShownList); + + deletePersons(model, lastShownList); + + showRemainingPersons(model); + + return new CommandResult(String.format(MESSAGE_SUCCESS, + deletedCount, leftOver)); + } + + /** + * Checks if the list is empty. + * @param list the list + * @throws CommandException if the list is empty + */ + private static void checkForEmptyList(ObservableList list) throws CommandException { + if (list.isEmpty()) { + throw new CommandException(MESSAGE_NO_PERSONS); + } + } + + /** + * Shows the remaining persons in the address book. + * @param model the model + */ + private static void showRemainingPersons(Model model) { + model.updateFilteredPersonList(Model.PREDICATE_SHOW_ALL_PERSONS); + } + + /** + * Deletes all persons in the last shown list. + * @param model the model + * @param lastShownList the last shown list + */ + private static void deletePersons(Model model, ObservableList lastShownList) { + for (int i = lastShownList.size() - 1; i >= 0; i--) { + model.deletePerson(lastShownList.get(i)); + } + } + + /** + * Returns the number of persons in the filtered list. + * @param lastShownList the last shown list + * @return the number of persons in the filtered list + */ + private static int getFilteredSize(ObservableList lastShownList) { + return lastShownList.size(); + } + + /** + * Returns the total number of persons in the address book. + * @param model the model + * @return the total number of persons in the address book + */ + private static int getTotalSize(Model model) { + AddressBook addressBook = new AddressBook(model.getAddressBook()); + + return addressBook.getPersonList().size(); + } + + /** + * Returns the number of persons that will be left over after deletion. + * @param model the model + * @param lastShownList the last shown list + * @return the number of persons that will be left over after deletion + */ + private static int getLeftOverSize(Model model, ObservableList lastShownList) { + return getTotalSize(model) - getFilteredSize(lastShownList); + } + + /** + * Ensures that not all persons are deleted. + * @param model the model + * @param lastShownList the last shown list + * @throws CommandException if all persons are at risk of being deleted + */ + private static void ensureNotAllAreDeleted(Model model, ObservableList lastShownList) + throws CommandException { + if (getFilteredSize(lastShownList) == getTotalSize(model)) { + throw new CommandException(MESSAGE_NO_FILTER); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeselectExamCommand.java b/src/main/java/seedu/address/logic/commands/DeselectExamCommand.java new file mode 100644 index 00000000000..81d143692b4 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeselectExamCommand.java @@ -0,0 +1,30 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.exam.Exam; + +/** + * Deselects the currently selected exam. + */ +public class DeselectExamCommand extends Command { + + public static final String COMMAND_WORD = "deselectExam"; + + public static final String MESSAGE_SUCCESS = "%s deselected"; + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + if (model.getSelectedExam().getValue() == null) { + throw new CommandException(Messages.MESSAGE_NO_EXAM_SELECTED); + } + Exam examToDeselect = model.getSelectedExam().getValue(); + model.deselectExam(); + return new CommandResult(String.format(MESSAGE_SUCCESS, examToDeselect.getName())); + } + +} diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 4b581c7331e..24fcc56248a 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -3,14 +3,19 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MATRIC_NUMBER; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_REFLECTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_STUDIO; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -21,11 +26,16 @@ import seedu.address.logic.Messages; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; +import seedu.address.model.exam.Exam; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Score; +import seedu.address.model.student.Matric; +import seedu.address.model.student.Reflection; +import seedu.address.model.student.Studio; import seedu.address.model.tag.Tag; /** @@ -43,6 +53,9 @@ public class EditCommand extends Command { + "[" + PREFIX_PHONE + "PHONE] " + "[" + PREFIX_EMAIL + "EMAIL] " + "[" + PREFIX_ADDRESS + "ADDRESS] " + + "[" + PREFIX_MATRIC_NUMBER + "MATRICULATION NUMBER] " + + "[" + PREFIX_REFLECTION + "REFLECTION] " + + "[" + PREFIX_STUDIO + "STUDIO] " + "[" + PREFIX_TAG + "TAG]...\n" + "Example: " + COMMAND_WORD + " 1 " + PREFIX_PHONE + "91234567 " @@ -100,8 +113,15 @@ private static Person createEditedPerson(Person personToEdit, EditPersonDescript Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); - - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + Matric updatedMatric = editPersonDescriptor.getMatric().orElse(personToEdit.getMatric()); + Reflection updatedReflection = editPersonDescriptor.getReflection().orElse(personToEdit.getReflection()); + Studio updatedStudio = editPersonDescriptor.getStudio().orElse(personToEdit.getStudio()); + // Scores are not edited + Map scores = personToEdit.getScores(); + + return new Person(updatedName, updatedPhone, updatedEmail, + updatedAddress, updatedTags, updatedMatric, + updatedReflection, updatedStudio, scores); } @Override @@ -139,6 +159,12 @@ public static class EditPersonDescriptor { private Address address; private Set tags; + private Matric matric; + private Reflection reflection; + private Studio studio; + + private Map scores; + public EditPersonDescriptor() {} /** @@ -151,13 +177,17 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { setEmail(toCopy.email); setAddress(toCopy.address); setTags(toCopy.tags); + setMatric(toCopy.matric); + setReflection(toCopy.reflection); + setStudio(toCopy.studio); + setScores(toCopy.scores); } /** * Returns true if at least one field is edited. */ public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); + return CollectionUtil.isAnyNonNull(name, phone, email, address, tags, matric, reflection, studio); } public void setName(Name name) { @@ -209,6 +239,38 @@ public Optional> getTags() { return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); } + public void setScores(Map scores) { + this.scores = (scores != null) ? new HashMap<>(scores) : null; + } + + public Optional> getScores() { + return (scores != null) ? Optional.of(Collections.unmodifiableMap(scores)) : Optional.empty(); + } + + public void setMatric(Matric matric) { + this.matric = matric; + } + + public Optional getMatric() { + return Optional.ofNullable(matric); + } + + public void setReflection(Reflection reflection) { + this.reflection = reflection; + } + + public Optional getReflection() { + return Optional.ofNullable(reflection); + } + + public void setStudio(Studio studio) { + this.studio = studio; + } + + public Optional getStudio() { + return Optional.ofNullable(studio); + } + @Override public boolean equals(Object other) { if (other == this) { @@ -225,7 +287,11 @@ public boolean equals(Object other) { && Objects.equals(phone, otherEditPersonDescriptor.phone) && Objects.equals(email, otherEditPersonDescriptor.email) && Objects.equals(address, otherEditPersonDescriptor.address) - && Objects.equals(tags, otherEditPersonDescriptor.tags); + && Objects.equals(tags, otherEditPersonDescriptor.tags) + && Objects.equals(matric, otherEditPersonDescriptor.matric) + && Objects.equals(reflection, otherEditPersonDescriptor.reflection) + && Objects.equals(studio, otherEditPersonDescriptor.studio) + && Objects.equals(scores, otherEditPersonDescriptor.scores); } @Override @@ -236,7 +302,12 @@ public String toString() { .add("email", email) .add("address", address) .add("tags", tags) + .add("matriculation number", matric) + .add("reflection", reflection) + .add("studio", studio) + .add("scores", scores) .toString(); } + } } diff --git a/src/main/java/seedu/address/logic/commands/EditScoreCommand.java b/src/main/java/seedu/address/logic/commands/EditScoreCommand.java new file mode 100644 index 00000000000..b5da8fdf8e8 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/EditScoreCommand.java @@ -0,0 +1,93 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.isAnyNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCORE; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.exam.Exam; +import seedu.address.model.person.Person; +import seedu.address.model.person.Score; + +/** + * Edits a score of a person in the address book. + */ +public class EditScoreCommand extends Command { + + public static final String COMMAND_WORD = "editScore"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the score for the person identified by " + + "the index number used in the displayed person list. " + + "Existing values will be overwritten by the input value.\n" + + "Parameters: INDEX (must be a positive integer) " + + PREFIX_SCORE + "SCORE\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_SCORE + "17"; + + public static final String MESSAGE_EDIT_SCORE_SUCCESS = "Edited score for %s to %s for %s (%s)."; + public static final String MESSAGE_SCORE_GREATER_THAN_MAX = + "Score for %s cannot be greater than the maximum score."; + public static final String MESSAGE_NO_SCORE_TO_EDIT = "%s (%s) does not have a score to edit for %s."; + + private final Index index; + private final Score score; + + /** + * Creates an EditScoreCommand object to edit the specified {@code Score} of the person at + * the specified {@code Index}. + */ + public EditScoreCommand(Index index, Score score) { + requireNonNull(index); + requireNonNull(score); + + this.index = index; + this.score = score; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List filteredPersons = model.getFilteredPersonList(); + + if (index.getZeroBased() >= filteredPersons.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Exam selectedExam = model.getSelectedExam().getValue(); + if (!isAnyNonNull(selectedExam)) { + throw new CommandException(Messages.MESSAGE_NO_EXAM_SELECTED); + } + + if (selectedExam.getMaxScore().getScore() < score.getScore()) { + throw new CommandException(String.format(MESSAGE_SCORE_GREATER_THAN_MAX, selectedExam.getName())); + } + + Person personToEdit = filteredPersons.get(index.getZeroBased()); + + Map updatedScores = new HashMap<>(personToEdit.getScores()); + if (!updatedScores.containsKey(selectedExam)) { + throw new CommandException(String.format(MESSAGE_NO_SCORE_TO_EDIT, personToEdit.getName(), + personToEdit.getEmail(), + selectedExam.getName())); + } + + model.addExamScoreToPerson(personToEdit, selectedExam, score); + return new CommandResult(String.format(MESSAGE_EDIT_SCORE_SUCCESS, selectedExam.getName(), score, + personToEdit.getName(), personToEdit.getEmail())); + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof EditScoreCommand + && index.equals(((EditScoreCommand) other).index) + && score.equals(((EditScoreCommand) other).score)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java index 3dd85a8ba90..1592c5f9a3f 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java @@ -9,7 +9,7 @@ public class ExitCommand extends Command { public static final String COMMAND_WORD = "exit"; - public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Address Book as requested ..."; + public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Avengers Assemble as requested ..."; @Override public CommandResult execute(Model model) { diff --git a/src/main/java/seedu/address/logic/commands/ExportCommand.java b/src/main/java/seedu/address/logic/commands/ExportCommand.java new file mode 100644 index 00000000000..57c8b8425f5 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ExportCommand.java @@ -0,0 +1,223 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import javafx.collections.ObservableList; +import seedu.address.commons.util.CsvUtil; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.person.Person; +import seedu.address.storage.JsonAddressBookStorage; + +/** + * Exports the information of Persons listed in the interface of the addressbook to a CSV file. + */ +public class ExportCommand extends Command { + + public static final String COMMAND_WORD = "export"; + + public static final String MESSAGE_SUCCESS = "Exported all currently listed person(s)'s information to a " + + "CSV file. \n" + + "CSV file can be found in addressbookdata file."; + public static final String MESSAGE_NOTHING_TO_EXPORT_FAILURE = "Nothing to export."; + public static final String MESSAGE_WRITE_TO_JSON_FAILURE = "Could not write information in filtered list to " + + "JSON storage."; + public static final String MESSAGE_JSON_FILE_NOT_FOUND_FAILURE = "Cannot find JSON file to export."; + public static final String MESSAGE_PARSE_JSON_FILE_FAILURE = "Error parsing JSON data."; + public static final String MESSAGE_MAPPING_JSON_TO_CSV_FAILURE = "Error mapping JSON data to CSV schema."; + public static final String MESSAGE_EMPTY_JSON_FILE_FAILURE = "The JSON File is empty."; + public static final String MESSAGE_CREATE_CSV_DIRECTORY_FAILURE = "Could not create directory for CSV file."; + + private Path filteredJsonFilePath = Paths.get("data", "filteredaddressbook.json"); + private String csvFilePath = "./addressbookdata/avengersassemble.csv"; + + /** + * Gets the current CSV file path. + * + * @return The file path that the data is being exported to. + */ + public String getCsvFilePath() { + return this.csvFilePath; + } + + /** + * Updates the CSV file path if needed. + * + * @param filePath The file path to change to. + */ + public void updateCsvFilePath(String filePath) { + this.csvFilePath = filePath; + } + + /** + * Gets the current JSON file path in which the filtered lists of persons will be written to. + * + * @return The file path that the filtered persons list is being written to. + */ + public Path getFilteredJsonFilePath() { + return this.filteredJsonFilePath; + } + + /** + * Updates the filtered JSON file path in which the filtered lists of persons will be written to. + * + * @param filePath The JSON file path to be written to. + */ + public void updateFilteredJsonFilePath(Path filePath) { + this.filteredJsonFilePath = filePath; + } + + /** + * Checks if the list is empty and throws an exception if the list is empty. + * + * @param personList The list in which to check if it is empty. + * @throws CommandException If the list is empty. + */ + public void checkForEmptyList(ObservableList personList) throws CommandException { + if (personList.isEmpty()) { + throw new CommandException(MESSAGE_NOTHING_TO_EXPORT_FAILURE); + } + } + + /** + * Adds all persons in ObservableList to AddressBook. + * + * @param addressBook The address book where persons should be added into. + * @param personList List containing persons to be added into address book. + */ + public void addToAddressBook(AddressBook addressBook, ObservableList personList) { + for (Person person : personList) { + addressBook.addPerson(person); + } + } + + /** + * Writes the information in the address book to a JSON file. + * + * @param jsonAddressBookStorage The JSON file to write information into. + * @param addressBook The address book from which to read the information from. + * @throws CommandException If information cannot be written into the JSON file. + */ + public void writeToJsonFile(JsonAddressBookStorage jsonAddressBookStorage, AddressBook addressBook) + throws CommandException { + try { + jsonAddressBookStorage.saveAddressBook(addressBook); + } catch (IOException e) { + throw new CommandException(MESSAGE_WRITE_TO_JSON_FAILURE); + } + } + + /** + * Read the JSON file and returns the contents as a JSON tree. + * + * @param jsonFile The JSON file to be read and mapped to the CSV schema. + * @return The JSON tree representing the content of the JSON file. + * @throws CommandException If an error occurs while reading the JSON file and mapping it to the CSV schema. + */ + public JsonNode readJsonFile(File jsonFile) throws CommandException { + try { + JsonNode jsonTree = new ObjectMapper().readTree(jsonFile); + return jsonTree; + } catch (IOException e) { + if (e instanceof FileNotFoundException) { + throw new CommandException(MESSAGE_JSON_FILE_NOT_FOUND_FAILURE); + } else if (e instanceof JsonParseException) { + throw new CommandException(MESSAGE_PARSE_JSON_FILE_FAILURE); + } else if (e instanceof JsonMappingException) { + throw new CommandException(MESSAGE_MAPPING_JSON_TO_CSV_FAILURE); + } else { + throw new CommandException("Error: " + e.getMessage()); + } + } + } + + /** + * Reads the persons array in the JSON file and returns the contents as a JSON tree. + * + * @param jsonTree The JSON tree where the persons array should be read from. + * @return The JSON tree representing the content of the persons array in the release file. + * @throws CommandException If the persons array in the JSON file is empty. + */ + public JsonNode readPersonsArray(JsonNode jsonTree) throws CommandException { + JsonNode personsArray = jsonTree.get("persons"); + if (personsArray == null || personsArray.size() == 0) { + throw new CommandException(MESSAGE_EMPTY_JSON_FILE_FAILURE); + } + return personsArray; + } + + /** + * Reads the exams array in the JSON file and returns the contents as a JSON tree. + * + * @param jsonTree The JSON file where the exams array should be read from. + * @return The JSON tree representing the content of the exams array in the release file. + */ + public JsonNode readExamsArray(JsonNode jsonTree) { + JsonNode examsArray = jsonTree.get("exams"); + return examsArray; + } + + /** + * Creates the directory for the CSV file if it does not exist. + * + * @param csvFile The CSV file for which the directory needs to be created. + * @throws CommandException If directory fails to be created. + */ + public void createCsvDirectory(File csvFile) throws CommandException { + File csvParentDirectory = csvFile.getParentFile(); + if (!csvParentDirectory.exists()) { + boolean isCreated = csvParentDirectory.mkdir(); + if (!isCreated) { + throw new CommandException(MESSAGE_CREATE_CSV_DIRECTORY_FAILURE); + } + } + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + try { + ObservableList filteredPersonObservableList = model.getFilteredPersonList(); + checkForEmptyList(filteredPersonObservableList); + + AddressBook filteredPersonAddressBook = new AddressBook(); + addToAddressBook(filteredPersonAddressBook, filteredPersonObservableList); + + JsonAddressBookStorage jsonAddressBookStorage = new JsonAddressBookStorage(filteredJsonFilePath); + writeToJsonFile(jsonAddressBookStorage, filteredPersonAddressBook); + + File filteredJsonFile = filteredJsonFilePath.toFile(); + File unfilteredJsonFile = model.getAddressBookFilePath().toFile(); + + File csvFile = new File(csvFilePath); + + JsonNode filteredJsonTree = readJsonFile(filteredJsonFile); + JsonNode unfilteredJsonTree = readJsonFile(unfilteredJsonFile); + + JsonNode personsArray = readPersonsArray(filteredJsonTree); + JsonNode examsArray = readExamsArray(unfilteredJsonTree); + + createCsvDirectory(csvFile); + + CsvUtil.writeToCsvFile(csvFile, personsArray, examsArray); + + return new CommandResult(MESSAGE_SUCCESS); + + } catch (IOException e) { + throw new CommandException("Error: " + e.getMessage()); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index 72b9eddd3a7..378910fbb83 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -1,39 +1,114 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.isAnyNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LESS_THAN; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MATRIC_NUMBER; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MORE_THAN; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_REFLECTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_STUDIO; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import java.util.Arrays; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.Prefix; import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.exam.Exam; +import seedu.address.model.person.ExamPredicate; +import seedu.address.model.person.PersonDetailPredicate; /** * Finds and lists all persons in address book whose name contains any of the argument keywords. * Keyword matching is case insensitive. */ public class FindCommand extends Command { + public static final Prefix[] ACCEPTED_PREFIXES = {PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_ADDRESS, PREFIX_TAG, PREFIX_MATRIC_NUMBER, + PREFIX_REFLECTION, PREFIX_STUDIO, PREFIX_LESS_THAN, + PREFIX_MORE_THAN}; + public static final String COMMAND_WORD = "find"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " - + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" - + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" - + "Example: " + COMMAND_WORD + " alice bob charlie"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons with a specified aspect " + + "containing specified keyword (case-insensitive) or\n" + + "which meets the criteria with the specified value (positive decimal up to 2 decimal places)\n" + + "and displays them as a list with index numbers.\n" + + "The currently supported prefixes are: " + + Arrays.toString(ACCEPTED_PREFIXES) + "\n" + + "Parameters: PREFIX|KEYWORD\n" + + "Example: " + COMMAND_WORD + " " + PREFIX_NAME + "Alice"; + + public static final String MESSAGE_SCORE_GREATER_THAN_MAX = "Value cannot be greater than the maximum score."; + + private static final Logger logger = LogsCenter.getLogger(FindCommand.class); - private final NameContainsKeywordsPredicate predicate; + private final Prefix prefix; + private final String keyword; - public FindCommand(NameContainsKeywordsPredicate predicate) { - this.predicate = predicate; + /** + * Creates a FindCommand to find the specified Persons + */ + public FindCommand(Prefix prefix, String keyword) { + this.prefix = prefix; + this.keyword = keyword; } @Override - public CommandResult execute(Model model) { + public CommandResult execute(Model model) throws CommandException { requireNonNull(model); - model.updateFilteredPersonList(predicate); + logger.info("Executing find command"); + if (isExamRequired()) { + updateFilteredPersonListByScore(model); + } else { + updateFilteredPersonListByPersonDetail(model); + } + logger.info("Find command executed successfully"); return new CommandResult( String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); } + /** + * Updates the filtered person list by score. + * @param model Where the person list is stored. + * @throws CommandException + */ + private void updateFilteredPersonListByScore(Model model) throws CommandException { + Exam selectedExam = model.getSelectedExam().getValue(); + if (!isAnyNonNull(selectedExam)) { + throw new CommandException(Messages.MESSAGE_NO_EXAM_SELECTED); + } + if (selectedExam.getMaxScore().getScore() < Double.parseDouble(keyword)) { + throw new CommandException(MESSAGE_SCORE_GREATER_THAN_MAX); + } + model.updateFilteredPersonList(new ExamPredicate(prefix, keyword, selectedExam)); + } + + /** + * Updates the filtered person list by person detail. + * @param model Where the person list is stored. + */ + private void updateFilteredPersonListByPersonDetail(Model model) { + model.updateFilteredPersonList(new PersonDetailPredicate(prefix, keyword)); + } + + /** + * Checks if the prefix requires an exam to be selected. + * @return True if the prefix is PREFIX_LESSTHAN or PREFIX_GREATERTHAN. + */ + private boolean isExamRequired() { + return prefix.equals(PREFIX_LESS_THAN) || prefix.equals(PREFIX_MORE_THAN); + } + @Override public boolean equals(Object other) { if (other == this) { @@ -46,13 +121,15 @@ public boolean equals(Object other) { } FindCommand otherFindCommand = (FindCommand) other; - return predicate.equals(otherFindCommand.predicate); + return prefix.equals(otherFindCommand.prefix) + && keyword.equals(otherFindCommand.keyword); } @Override public String toString() { return new ToStringBuilder(this) - .add("predicate", predicate) + .add("prefix", prefix) + .add("keyword", keyword) .toString(); } } diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java index bf824f91bd0..aecec38a2d7 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/seedu/address/logic/commands/HelpCommand.java @@ -1,21 +1,34 @@ package seedu.address.logic.commands; +import java.awt.GraphicsEnvironment; +import java.awt.Toolkit; +import java.awt.datatransfer.StringSelection; + import seedu.address.model.Model; /** * Format full help instructions for every command for display. */ public class HelpCommand extends Command { - public static final String COMMAND_WORD = "help"; public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows program usage instructions.\n" + "Example: " + COMMAND_WORD; + public static final String SHOWING_HELP_MESSAGE = + "Copied user guide link to clipboard. Open the link in a browser to view it."; + private static final StringSelection USER_GUIDE_URL = + new StringSelection("https://ay2324s2-cs2103t-t10-1.github.io/tp/UserGuide.html"); - public static final String SHOWING_HELP_MESSAGE = "Opened help window."; + private void copyToClipboard(StringSelection stringSelection) { + if (GraphicsEnvironment.isHeadless()) { + return; + } + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(stringSelection, stringSelection); + } @Override public CommandResult execute(Model model) { - return new CommandResult(SHOWING_HELP_MESSAGE, true, false); + copyToClipboard(USER_GUIDE_URL); + return new CommandResult(SHOWING_HELP_MESSAGE, false, false); } } diff --git a/src/main/java/seedu/address/logic/commands/ImportCommand.java b/src/main/java/seedu/address/logic/commands/ImportCommand.java new file mode 100644 index 00000000000..1e293a2b6ef --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ImportCommand.java @@ -0,0 +1,222 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.commons.util.CsvUtil.readCsvFile; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_IMPORT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MATRIC_NUMBER; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_REFLECTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_STUDIO; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import javafx.util.Pair; +import seedu.address.commons.exceptions.DataLoadingException; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.AddCommandParser; +import seedu.address.logic.parser.Prefix; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.Model; + +/** + * Changes the of an existing person in the address book. + */ +public class ImportCommand extends Command { + + public static final String COMMAND_WORD = "import"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Imports persons from specified filepath." + + " Must be an absolute CSV file path\n" + + "Parameters: " + PREFIX_IMPORT + "FILEPATH\n" + + "Example: " + COMMAND_WORD + " " + PREFIX_IMPORT + "C:usr/lib/text.csv"; + private static final String MESSAGE_IMPORT_SUCCESS = "Imported persons successfully!\n"; + private final Path filePath; + + /** + * Represents the order of the data that should be parsed into the addCommandParser + */ + private final HashSet compulsoryParameters = + new HashSet<>(List.of(new String[]{"name", "phone", "email", "address"})); + private final HashSet optionalParameters = new HashSet<>( + List.of(new String[]{"matric", "reflection", "studio", "tags"})); + + private final AddCommandParser addCommandParser; + + private String errorMsgsFromReadingCsv = ""; + private String errorMsgsFromAddingPersons = ""; + + private int successfulImports = 0; + private int unsuccessfulImports = 0; + + /** + * Represents a mapping of String to prefix of the data that should be parsed into the addCommandParser. + */ + private final Map prefixMap = Map.of( + "name", PREFIX_NAME, + "phone", PREFIX_PHONE, + "email", PREFIX_EMAIL, + "address", PREFIX_ADDRESS, + "matric", PREFIX_MATRIC_NUMBER, + "reflection", PREFIX_REFLECTION, + "studio", PREFIX_STUDIO, + "tags", PREFIX_TAG + ); + + /** + * @param filePath absolute path of file (path starts from C:...) + */ + public ImportCommand(Path filePath) { + requireNonNull(filePath); + + this.filePath = filePath; + this.addCommandParser = new AddCommandParser(); + } + + /** + * Generates a report of the import process. + */ + private String generateReport() { + String importSuccessMsg = ( + !errorMsgsFromReadingCsv.isEmpty() | !errorMsgsFromAddingPersons.isEmpty() + ? "Import completed with errors\n" + : successfulImports == 0 + ? "No persons were imported\n" + : MESSAGE_IMPORT_SUCCESS); + + String reportForAddingPersons = "\n" + ( + errorMsgsFromAddingPersons.isEmpty() && successfulImports > 0 + ? "All valid persons have been added!\n" + : successfulImports == 0 + ? "No valid persons were found. Csv file is empty or error occurred reading from csv file\n" + : "Errors found in adding persons!\n") + + String.format("Successful imports: %d\n", successfulImports) + + String.format("Unsuccessful imports: %d\n", unsuccessfulImports) + + errorMsgsFromAddingPersons; + + String reportForReadingCsv = + errorMsgsFromReadingCsv.isEmpty() + ? "" + : "\nErrors found from reading csv!\n" + errorMsgsFromReadingCsv; + + return importSuccessMsg + reportForReadingCsv + reportForAddingPersons; + } + /** + * Generates an error report from adding persons. The index refers to the index of the person in personsData. + * @param e + * @param index + */ + private void generateErrorReportFromAddingPersons(Exception e, int index) { + errorMsgsFromAddingPersons += String.format("Person %s: ", index) + e.getMessage() + "\n"; + } + + private void generateErrorReportFromReadingCsv(DataLoadingException e) { + errorMsgsFromReadingCsv += e.getMessage() + "\n"; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + Pair importResults; + try { + Pair>>, String> result = + readCsvFile(filePath, compulsoryParameters, optionalParameters); + Optional>> personsData = result.getKey(); + if (personsData.isPresent()) { + importResults = addToModel(model, personsData.get()); + successfulImports = importResults.getKey(); + unsuccessfulImports = importResults.getValue(); + } + if (!result.getValue().isEmpty()) { + generateErrorReportFromReadingCsv(new DataLoadingException(result.getValue())); + } + } catch (IOException e) { + throw new CommandException(e.getMessage()); + } + + return new CommandResult(generateReport()); + } + + /** + * Adds the persons data to the model using a series of addCommands. + * @param model + * @param personsData + * @throws CommandException + */ + public Pair addToModel(Model model, List> personsData) { + requireAllNonNull(model, personsData); + int successfulImports = 0; + int unsuccessfulImports = 0; + for (Map personData : personsData) { + try { + String addCommandInput = convertToAddCommandInput(personData); + AddCommand addCommand = addCommandParser.parse(addCommandInput); + addCommand.execute(model); + successfulImports++; + } catch (ParseException | CommandException e) { + generateErrorReportFromAddingPersons(e, successfulImports + unsuccessfulImports); + unsuccessfulImports++; + } + } + return new Pair<>(successfulImports, unsuccessfulImports); + } + + /** + * Converts a map of person data to a string that can be parsed by the addCommandParser + * @param personData + * @return + */ + private String convertToAddCommandInput(Map personData) { + // Changed this method to private to prevent malicious use + StringBuilder sb = new StringBuilder(); + sb.append(" "); + for (Map.Entry entry : personData.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + if (value.isEmpty()) { + // skip empty values + continue; + } + if (key.equals("tags")) { + // tag is a special case, it can have multiple values + String tags = personData.get(key); + String[] tagArray = tags.split(";"); + for (String tag : tagArray) { + sb.append(prefixMap.get(key).getPrefix()); + sb.append(tag); + sb.append(" "); + } + } else { + sb.append(prefixMap.get(key).getPrefix()); + sb.append(personData.get(key)); + } + sb.append(" "); + } + return sb.toString(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ImportCommand)) { + return false; + } + + ImportCommand e = (ImportCommand) other; + return filePath.equals(e.filePath); + } + +} diff --git a/src/main/java/seedu/address/logic/commands/ImportExamScoresCommand.java b/src/main/java/seedu/address/logic/commands/ImportExamScoresCommand.java new file mode 100644 index 00000000000..39c8031b414 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ImportExamScoresCommand.java @@ -0,0 +1,308 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_IMPORT; + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; + +import javafx.collections.ObservableList; +import seedu.address.commons.util.CsvUtil; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.exam.Exam; +import seedu.address.model.person.Person; +import seedu.address.model.person.Score; + +/** + * Imports a CSV file containing details of existing exams into the application. + */ +public class ImportExamScoresCommand extends Command { + + public static final String COMMAND_WORD = "importExamScores"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Imports exam scores from specified filepath." + + " Must be an absolute CSV file path without any inverted commas. Parameter: " + + PREFIX_IMPORT + "FILEPATH\n" + + "Example: " + COMMAND_WORD + " " + PREFIX_IMPORT + "C:usr/lib/text.csv"; + public static final String MESSAGE_SCORE_NOT_NUMBER = "Score for %s is not a valid score"; + public static final String MESSAGE_PERSON_DOES_NOT_EXIST = "Person with the given email does not exist"; + public static final String MESSAGE_SUCCESS = "Imported exam scores from: %s"; + public static final String MESSAGE_DUPLICATE_EXAM = + "Duplicate exam header. Only the records of the first exam will be imported if present."; + public static final String EXAM_CSV_HEADER = "Exam:"; + public static final String PREFIX_ERROR_REPORT = + "\nBelow are the errors for any rows that were skipped:\n"; + public static final String MESSAGE_EXAM_DOES_NOT_EXIST = "Exam does not exist"; + public static final String MESSAGE_GRADE_TOO_HIGH = "Grade for %s exceeds maximum score"; + public static final String HEADER_EMAIL = "email"; + public static final String HEADER_EXAM = "exam"; + public static final String ERROR_EMAIL_FIRST_VALUE = + "Please ensure that the email column exists only in the first column in the CSV file, without duplicates."; + public static final String ERROR_WRONG_CSV_FORMAT = "Please ensure that the CSV file is in the correct format.\n" + + "The first row should contain the headers of the exams and the first column should contain the emails.\n" + + "There should be no empty cells in the CSV file."; + public static final String ERROR_BUILDER = "%s of %s: %s\n"; + private StringBuilder errorReport; + private Path filepath; + + /** + * Creates an ImportExamCommand to import exams from the specified file path. + * @param filePath the path of the file + */ + public ImportExamScoresCommand(Path filePath) { + this.errorReport = new StringBuilder(); + this.filepath = filePath; + } + + // Error report methods + + /** + * Adds an error to the error report. + * @param field the offending field + * @param subject the subject of the error + * @param error the error message + */ + void addToErrorReport(String field, String subject, String error) { + errorReport.append(String.format(ERROR_BUILDER, field, subject, error)); + } + + /** + * Generates the error report. + * @return the error report + */ + String generateErrorReport() { + String errorReportString = errorReport.toString(); + if (errorReportString.isBlank()) { + return ""; + } else { + return PREFIX_ERROR_REPORT + errorReportString; + } + } + + private void detectDuplicateExamHeaders(List lst) { + String[] examNames = getExamNames(lst); + HashSet uniqueExamNames = new HashSet<>(); + for (int i = 1; i < examNames.length; i++) { + if (!uniqueExamNames.add(examNames[i]) && isExam(examNames[i])) { + addToErrorReport( + HEADER_EXAM, getExamName(examNames[i]), MESSAGE_DUPLICATE_EXAM); + } + } + } + + // Exam mapping methods + + /** + * Reorganizes the parsed CSV data into a mapping of individual results from exams. + * @param lst the list of String arrays representing the lines of the CSV file + * @return a mapping of exam names to a mapping of student emails to their respective scores + */ + private HashMap> createExamsMapping(List lst) { + HashMap> map = new HashMap<>(); + + // Check if there is at least a header row within the CSV file. + if (hasHeader(lst)) { + detectDuplicateExamHeaders(lst); + createExamNameHeaders(lst, map); + updateExamResults(lst, map); + } + return map; + } + + private void createExamNameHeaders(List lst, HashMap> map) { + String[] examNames = getExamNames(lst); + + for (int i = 1; i < examNames.length; i++) { + map.put(examNames[i], new HashMap<>()); + } + } + + private void updateExamResults(List lst, HashMap> map) { + String[] examNames = getExamNames(lst); + + for (int i = 1; i < lst.size(); i++) { + String[] row = lst.get(i); + updateExamResult(map, examNames, row); + } + } + + private void updateExamResult(HashMap> map, String[] examNames, String[] row) { + if (hasEmail(row)) { + String email = row[0]; + addRows(map, examNames, row, email); + } + } + + private void addRows( + HashMap> map, String[] examNames, String[] row, String email) { + for (int j = 1; j < row.length; j++) { + addRow(map, examNames, row, email, j); + } + } + + private void addRow( + HashMap> map, String[] examNames, String[] row, String email, int j) { + if (Score.isValidScoreString(row[j])) { + map.get(examNames[j]).put(email, new Score(Double.parseDouble(row[j])).getScore()); + } else { + if (isExam(examNames[j])) { + addToErrorReport( + HEADER_EMAIL, email, String.format(MESSAGE_SCORE_NOT_NUMBER, getExamName(examNames[j]))); + } + } + } + + private void addScores(HashMap> headers, Model model) { + Object[] examNames = headers.keySet().toArray(); + + for (int i = 0; i < examNames.length; i++) { + ObservableList exams = model.getExamByName(examNames[i].toString().strip()); + if (exams.size() > 0) { + Exam exam = exams.get(0); + insertGrades(headers, model, exam, examNames[i]); + } else { + addToErrorReport(HEADER_EXAM, (String) examNames[i], MESSAGE_EXAM_DOES_NOT_EXIST); + } + } + } + + private void insertGrades( + HashMap> headers, Model model, Exam exam, Object examNames) { + HashMap grades = headers.get((String) examNames); + Object[] emails = grades.keySet().toArray(); + + for (int j = 0; j < emails.length; j++) { + String email = (String) emails[j]; + Double grade = grades.get(email); + + addScoreToPerson(model, email, exam, grade); + } + } + + private void addScoreToPerson(Model model, String email, Exam exam, Double grade) { + ObservableList persons = model.getPersonByEmail(email); + if (persons.size() > 0) { + Person person = persons.get(0); + if (grade <= exam.maxScore.value) { + model.addExamScoreToPerson(person, exam, new Score(grade)); + } else { + addToErrorReport(HEADER_EMAIL, email, String.format(MESSAGE_GRADE_TOO_HIGH, exam.getName())); + } + } else { + addToErrorReport(HEADER_EMAIL, email, MESSAGE_PERSON_DOES_NOT_EXIST); + } + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + List lst = CsvUtil.readAllLinesForImportExamScores(filepath); + System.out.println(lst); + if (!isEmailFirstValue(lst)) { + throw new CommandException(ERROR_EMAIL_FIRST_VALUE); + } + + try { + parseScoresFromRawCsv(model, lst); + } catch (NullPointerException | ArrayIndexOutOfBoundsException e) { + throw new CommandException(ERROR_WRONG_CSV_FORMAT); + } + + return new CommandResult( + String.format(MESSAGE_SUCCESS, filepath.toString()) + generateErrorReport()); + } + + private void parseScoresFromRawCsv(Model model, List lst) throws CommandException { + reverse(lst); + HashMap> headers = createExamsMapping(lst); + + HashMap> headersForExams = removeNonExams(headers); + addScores(headersForExams, model); + } + + // Trivial methods + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof ImportExamScoresCommand + && filepath.equals(((ImportExamScoresCommand) other).filepath)); + } + + private boolean hasHeader(List lst) { + return lst.size() > 0; + } + + private String[] getExamNames(List lst) { + return lst.get(0); + } + + private boolean hasEmail(String[] row) { + return row.length > 0; + } + + private boolean isExam(String examName) { + return examName.startsWith(EXAM_CSV_HEADER); + } + + private boolean isEmailHeader(String header) { + return header.equals(HEADER_EMAIL); + } + + // Utility methods + + private String getExamName(String examName) { + return examName.substring(EXAM_CSV_HEADER.length()).strip(); + } + + private HashMap> removeNonExams(HashMap> map) { + HashMap> newMap = new HashMap<>(); + for (String key : map.keySet()) { + if (isExam(key) || isEmailHeader(key)) { + newMap.put(getExamName(key), map.get(key)); + } + } + return newMap; + } + + private void reverse(List lst) { + for (int i = 0; i < lst.size(); i++) { + String[] row = lst.get(i); + reverseRowButKeepEmail(lst, i, row); + } + } + + private static void reverseRowButKeepEmail(List lst, int i, String[] row) { + String[] reversedRow = new String[row.length]; + if (row.length > 0) { + reversedRow[0] = row[0]; + } + if (row.length > 1) { + reverseElements(row, reversedRow); + } + lst.set(i, reversedRow); + } + + private static void reverseElements(String[] row, String[] reversedRow) { + for (int j = 1; j < row.length; j++) { + reversedRow[j] = row[row.length - j]; + } + } + + boolean isEmailFirstValue(List lst) { + if (!(lst.size() > 0 && lst.get(0).length > 0 && lst.get(0)[0].equals(HEADER_EMAIL))) { + return false; + } + for (int i = 0; i < lst.get(0).length; i++) { + if (i != 0 && lst.get(0)[i].equals(HEADER_EMAIL)) { + return false; + } + } + return true; + } + + +} diff --git a/src/main/java/seedu/address/logic/commands/SelectExamCommand.java b/src/main/java/seedu/address/logic/commands/SelectExamCommand.java new file mode 100644 index 00000000000..57259ff72b4 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SelectExamCommand.java @@ -0,0 +1,68 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.exam.Exam; + +/** + * Selects an exam identified using it's displayed index from the address book. + */ +public class SelectExamCommand extends Command { + + public static final String COMMAND_WORD = "selectExam"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Selects the exam identified by the index number used in the displayed exam list.\n" + + "Parameter: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_SELECT_EXAM_SUCCESS = "Selected exam: %1$s"; + + private final Index targetIndex; + + public SelectExamCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getExamList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_EXAM_DISPLAYED_INDEX); + } + + Exam examToSelect = lastShownList.get(targetIndex.getZeroBased()); + model.selectExam(examToSelect); + return new CommandResult(String.format(MESSAGE_SELECT_EXAM_SUCCESS, examToSelect)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof SelectExamCommand)) { + return false; + } + + SelectExamCommand otherSelectExamCommand = (SelectExamCommand) other; + return targetIndex.equals(otherSelectExamCommand.targetIndex); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("targetIndex", targetIndex) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 4ff1a97ed77..44f5cc5edf8 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -1,22 +1,36 @@ package seedu.address.logic.parser; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.Messages.MESSAGE_MISSING_COMPULSORY_PREFIXES; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MATRIC_NUMBER; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_REFLECTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_STUDIO; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; import seedu.address.logic.commands.AddCommand; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.exam.Exam; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Score; +import seedu.address.model.student.Matric; +import seedu.address.model.student.Reflection; +import seedu.address.model.student.Studio; import seedu.address.model.tag.Tag; /** @@ -31,25 +45,97 @@ public class AddCommandParser implements Parser { */ public AddCommand parse(String args) throws ParseException { ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize( + args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_ADDRESS, PREFIX_TAG, PREFIX_MATRIC_NUMBER, PREFIX_REFLECTION, PREFIX_STUDIO); - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) - || !argMultimap.getPreamble().isEmpty()) { + List missingCompulsoryPrefixes = findMissingCompulsoryPrefixes(argMultimap, PREFIX_NAME, PREFIX_PHONE, + PREFIX_EMAIL, PREFIX_ADDRESS); + + if (missingCompulsoryPrefixes.size() > 0) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + MESSAGE_MISSING_COMPULSORY_PREFIXES + + missingCompulsoryPrefixes.stream() + .map(Prefix::toString) + .collect(Collectors.joining(", ")))); + } + + if (!argMultimap.getPreamble().isEmpty()) { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); } - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, + PREFIX_MATRIC_NUMBER, PREFIX_REFLECTION, PREFIX_STUDIO); Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); + Matric matric = handleOptionalMatric(argMultimap); + Reflection reflection = handleOptionalReflection(argMultimap); + Studio studio = handleOptionalStudio(argMultimap); + Map scores = new HashMap<>(); + + // Update the tagList automatically Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); + tagList = autoTag(tagList, matric); - Person person = new Person(name, phone, email, address, tagList); + Person person = new Person(name, phone, email, address, tagList, matric, reflection, studio, scores); return new AddCommand(person); } + /** + * Automatically adds a tag to the tagList based on the presence of matric, reflection and studio. + * @param tagList the list of tags to be updated + * @param matric the matric number of the person + * @return + */ + private Set autoTag(Set tagList, Matric matric) { + boolean isMatricPresent = !Matric.isEmptyMatric(matric.matricNumber); + + Optional autoTag = createTag(isMatricPresent); + autoTag.ifPresent(tagList::add); + + return tagList; + } + + /** + * Creates a tag based on the presence of matric, reflection and studio. + * @param isMatricPresent boolean whether a Matric number is present. + * @return an Optional Tag based on the presence of matric, reflection and studio. + */ + private Optional createTag(boolean isMatricPresent) { + if (isMatricPresent) { + return Optional.of(new Tag("student")); + } + return Optional.empty(); + } + + private static Matric handleOptionalMatric(ArgumentMultimap argMultimap) throws ParseException { + if (!arePrefixesPresent(argMultimap, PREFIX_MATRIC_NUMBER)) { + return new Matric(""); + } else { + return ParserUtil.parseMatric(argMultimap.getValue(PREFIX_MATRIC_NUMBER).get()); + } + } + + private static Reflection handleOptionalReflection(ArgumentMultimap argMultimap) throws ParseException { + if (!arePrefixesPresent(argMultimap, PREFIX_REFLECTION)) { + return new Reflection(""); + } else { + return ParserUtil.parseReflection(argMultimap.getValue(PREFIX_REFLECTION).get()); + } + } + + private static Studio handleOptionalStudio(ArgumentMultimap argMultimap) throws ParseException { + if (!arePrefixesPresent(argMultimap, PREFIX_STUDIO)) { + return new Studio(""); + } else { + return ParserUtil.parseStudio(argMultimap.getValue(PREFIX_STUDIO).get()); + } + } + /** * Returns true if none of the prefixes contains empty {@code Optional} values in the given * {@code ArgumentMultimap}. @@ -58,4 +144,10 @@ private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Pre return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); } + private static List findMissingCompulsoryPrefixes(ArgumentMultimap argumentMultimap, + Prefix... compulsoryPrefixes) { + return Stream.of(compulsoryPrefixes).filter(prefix -> argumentMultimap.getValue(prefix).isEmpty()) + .collect(Collectors.toList()); + } + } diff --git a/src/main/java/seedu/address/logic/parser/AddExamCommandParser.java b/src/main/java/seedu/address/logic/parser/AddExamCommandParser.java new file mode 100644 index 00000000000..0e770d7e9f7 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddExamCommandParser.java @@ -0,0 +1,51 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCORE; + +import java.util.stream.Stream; + +import seedu.address.logic.commands.AddExamCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.exam.Exam; +import seedu.address.model.person.Score; + +/** + * Parses input arguments and creates a new AddExamCommand object + */ +public class AddExamCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddExamCommand + * and returns an AddExamCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddExamCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_SCORE); + + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_SCORE) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddExamCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_SCORE); + + String name = ParserUtil.parseExamName(argMultimap.getValue(PREFIX_NAME).get()); + Score maxScore = ParserUtil.parseExamScore(argMultimap.getValue(PREFIX_SCORE).get()); + + Exam exam = new Exam(name, maxScore); + + return new AddExamCommand(exam); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/AddScoreCommandParser.java b/src/main/java/seedu/address/logic/parser/AddScoreCommandParser.java new file mode 100644 index 00000000000..cee5028b4fc --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddScoreCommandParser.java @@ -0,0 +1,36 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCORE; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.AddScoreCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Score; + +/** + * Parses input arguments and creates a new AddScoreCommand object + */ +public class AddScoreCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddScoreCommand + * and returns an AddScoreCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddScoreCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_SCORE); + + if (argMultimap.getPreamble().isEmpty() || !argMultimap.getValue(PREFIX_SCORE).isPresent()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddScoreCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_SCORE); + + Index index = ParserUtil.parseIndex(argMultimap.getPreamble()); + Score score = ParserUtil.parseScore(argMultimap.getValue(PREFIX_SCORE).get()); + + return new AddScoreCommand(index, score); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 3149ee07e0b..dd54e15cd3f 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -9,14 +9,26 @@ import seedu.address.commons.core.LogsCenter; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddExamCommand; +import seedu.address.logic.commands.AddScoreCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CopyCommand; import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.DeleteExamCommand; +import seedu.address.logic.commands.DeleteScoreCommand; +import seedu.address.logic.commands.DeleteShownCommand; +import seedu.address.logic.commands.DeselectExamCommand; import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.EditScoreCommand; import seedu.address.logic.commands.ExitCommand; +import seedu.address.logic.commands.ExportCommand; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.ImportCommand; +import seedu.address.logic.commands.ImportExamScoresCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.SelectExamCommand; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -56,27 +68,63 @@ public Command parseCommand(String userInput) throws ParseException { case AddCommand.COMMAND_WORD: return new AddCommandParser().parse(arguments); + case AddExamCommand.COMMAND_WORD: + return new AddExamCommandParser().parse(arguments); + case EditCommand.COMMAND_WORD: return new EditCommandParser().parse(arguments); case DeleteCommand.COMMAND_WORD: return new DeleteCommandParser().parse(arguments); + case DeleteExamCommand.COMMAND_WORD: + return new DeleteExamCommandParser().parse(arguments); + + case DeleteScoreCommand.COMMAND_WORD: + return new DeleteScoreCommandParser().parse(arguments); + case ClearCommand.COMMAND_WORD: return new ClearCommand(); case FindCommand.COMMAND_WORD: return new FindCommandParser().parse(arguments); + case CopyCommand.COMMAND_WORD: + return new CopyCommand(); + + case ImportCommand.COMMAND_WORD: + return new ImportCommandParser().parse(arguments); + case ListCommand.COMMAND_WORD: return new ListCommand(); + case ExportCommand.COMMAND_WORD: + return new ExportCommand(); + case ExitCommand.COMMAND_WORD: return new ExitCommand(); case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case DeleteShownCommand.COMMAND_WORD: + return new DeleteShownCommand(); + + case SelectExamCommand.COMMAND_WORD: + return new SelectExamCommandParser().parse(arguments); + + case DeselectExamCommand.COMMAND_WORD: + return new DeselectExamCommand(); + + case AddScoreCommand.COMMAND_WORD: + return new AddScoreCommandParser().parse(arguments); + + case EditScoreCommand.COMMAND_WORD: + return new EditScoreCommandParser().parse(arguments); + + case ImportExamScoresCommand.COMMAND_WORD: + return new ImportExamScoresCommandParser().parse(arguments); + default: logger.finer("This user input caused a ParseException: " + userInput); throw new ParseException(MESSAGE_UNKNOWN_COMMAND); diff --git a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java b/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java index 21e26887a83..653d6dac928 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java +++ b/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java @@ -70,9 +70,20 @@ public void verifyNoDuplicatePrefixesFor(Prefix... prefixes) throws ParseExcepti Prefix[] duplicatedPrefixes = Stream.of(prefixes).distinct() .filter(prefix -> argMultimap.containsKey(prefix) && argMultimap.get(prefix).size() > 1) .toArray(Prefix[]::new); - if (duplicatedPrefixes.length > 0) { throw new ParseException(Messages.getErrorMessageForDuplicatePrefixes(duplicatedPrefixes)); } } + + /** + * Returns true if the ArgumentMultimap contains a single prefix-argument pair and an empty preamble. + */ + public boolean isSinglePrefix() { + return argMultimap.size() == 2 && containsPreamble() && getPreamble().isEmpty() + || argMultimap.size() == 1 && !containsPreamble(); + } + + private boolean containsPreamble() { + return argMultimap.containsKey(new Prefix("")); + } } diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..127aa296142 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -6,10 +6,21 @@ public class CliSyntax { /* Prefix definitions */ - public static final Prefix PREFIX_NAME = new Prefix("n/"); - public static final Prefix PREFIX_PHONE = new Prefix("p/"); - public static final Prefix PREFIX_EMAIL = new Prefix("e/"); - public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); - public static final Prefix PREFIX_TAG = new Prefix("t/"); + public static final Prefix PREFIX_NAME = new Prefix("n|"); + public static final Prefix PREFIX_PHONE = new Prefix("p|"); + public static final Prefix PREFIX_EMAIL = new Prefix("e|"); + public static final Prefix PREFIX_ADDRESS = new Prefix("a|"); + public static final Prefix PREFIX_TAG = new Prefix("t|"); + public static final Prefix PREFIX_MATRIC_NUMBER = new Prefix("m|"); + public static final Prefix PREFIX_REFLECTION = new Prefix("r|"); + public static final Prefix PREFIX_STUDIO = new Prefix("s|"); + + public static final Prefix PREFIX_IMPORT = new Prefix("i|"); + + public static final Prefix PREFIX_SCORE = new Prefix("s|"); + public static final Prefix PREFIX_LESS_THAN = new Prefix("lt|"); + public static final Prefix PREFIX_MORE_THAN = new Prefix("mt|"); + + public static final Prefix PREFIX_SPECIAL = new Prefix("|"); } diff --git a/src/main/java/seedu/address/logic/parser/DeleteExamCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteExamCommandParser.java new file mode 100644 index 00000000000..f241d06d5c2 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteExamCommandParser.java @@ -0,0 +1,29 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.DeleteExamCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new DeleteExamCommand object + */ +public class DeleteExamCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteExamCommand + * and returns a DeleteExamCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteExamCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteExamCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteExamCommand.MESSAGE_USAGE), pe); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/DeleteScoreCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteScoreCommandParser.java new file mode 100644 index 00000000000..ffa16f621fb --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteScoreCommandParser.java @@ -0,0 +1,28 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.DeleteScoreCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new DeleteScoreCommand object. + */ +public class DeleteScoreCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteScoreCommand + * and returns a DeleteScoreCommand object for execution. + * @throws if the user input does not conform the expected format. + */ + public DeleteScoreCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteScoreCommand(index); + } catch (ParseException e) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteScoreCommand.MESSAGE_USAGE), e); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 46b3309a78b..283f8e64d9f 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -4,8 +4,11 @@ import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MATRIC_NUMBER; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_REFLECTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_STUDIO; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import java.util.Collection; @@ -32,7 +35,9 @@ public class EditCommandParser implements Parser { public EditCommand parse(String args) throws ParseException { requireNonNull(args); ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize( + args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_ADDRESS, PREFIX_TAG, PREFIX_MATRIC_NUMBER, PREFIX_REFLECTION, PREFIX_STUDIO); Index index; @@ -42,7 +47,8 @@ public EditCommand parse(String args) throws ParseException { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); } - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); + argMultimap.verifyNoDuplicatePrefixesFor( + PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_MATRIC_NUMBER, PREFIX_STUDIO); EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); @@ -58,8 +64,22 @@ public EditCommand parse(String args) throws ParseException { if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); } + if (argMultimap.getValue(PREFIX_MATRIC_NUMBER).isPresent()) { + editPersonDescriptor.setMatric( + ParserUtil.parseMatricForEdit(argMultimap.getValue(PREFIX_MATRIC_NUMBER).get())); + } + if (argMultimap.getValue(PREFIX_REFLECTION).isPresent()) { + editPersonDescriptor.setReflection( + ParserUtil.parseReflectionForEdit(argMultimap.getValue(PREFIX_REFLECTION).get())); + } + if (argMultimap.getValue(PREFIX_STUDIO).isPresent()) { + editPersonDescriptor.setStudio(ParserUtil.parseStudioForEdit(argMultimap.getValue(PREFIX_STUDIO).get())); + } parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); + // Editcommand never changes scores so the value does not matter + editPersonDescriptor.setScores(null); + if (!editPersonDescriptor.isAnyFieldEdited()) { throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); } diff --git a/src/main/java/seedu/address/logic/parser/EditScoreCommandParser.java b/src/main/java/seedu/address/logic/parser/EditScoreCommandParser.java new file mode 100644 index 00000000000..3df34c71fea --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/EditScoreCommandParser.java @@ -0,0 +1,37 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCORE; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.EditScoreCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Score; + +/** + * Parses input arguments and creates a new EditScoreCommand object. + */ +public class EditScoreCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the EditScoreCommand + * and returns an EditScoreCommand object for execution. + * + * @return EditScoreCommand object with relevant parameters. + * @throws ParseException if the user input does not conform to the expected format. + */ + public EditScoreCommand parse(String args) throws ParseException { + ArgumentMultimap argumentMultimap = ArgumentTokenizer.tokenize(args, PREFIX_SCORE); + + if (argumentMultimap.getPreamble().isEmpty() || !argumentMultimap.getValue(PREFIX_SCORE).isPresent()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditScoreCommand.MESSAGE_USAGE)); + } + + argumentMultimap.verifyNoDuplicatePrefixesFor(PREFIX_SCORE); + + Index index = ParserUtil.parseIndex(argumentMultimap.getPreamble()); + Score score = ParserUtil.parseScore(argumentMultimap.getValue(PREFIX_SCORE).get()); + + return new EditScoreCommand(index, score); + } +} diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java index 2867bde857b..7e9db2b251e 100644 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/FindCommandParser.java @@ -1,12 +1,21 @@ package seedu.address.logic.parser; +import static java.util.Objects.requireNonNull; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; - -import java.util.Arrays; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LESS_THAN; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MATRIC_NUMBER; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MORE_THAN; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_REFLECTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_STUDIO; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.Score; /** * Parses input arguments and creates a new FindCommand object @@ -19,15 +28,49 @@ public class FindCommandParser implements Parser { * @throws ParseException if the user input does not conform the expected format */ public FindCommand parse(String args) throws ParseException { - String trimmedArgs = args.trim(); - if (trimmedArgs.isEmpty()) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); - } + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, FindCommand.ACCEPTED_PREFIXES); - String[] nameKeywords = trimmedArgs.split("\\s+"); + argMultimap.verifyNoDuplicatePrefixesFor( + PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_MATRIC_NUMBER, PREFIX_STUDIO, + PREFIX_REFLECTION, PREFIX_TAG, PREFIX_LESS_THAN, PREFIX_MORE_THAN); - return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); + Prefix prefix = extractPrefixForFindCommand(argMultimap); + String keyword = extractValidKeyword(argMultimap, prefix); + + return new FindCommand(prefix, keyword); } + /** + * Checks if the given {@code ArgumentMultimap} contains only one valid, non-empty prefix for the FindCommand + * and returns the found prefix. + * @throws ParseException if the user input does not conform to the expected format + */ + private Prefix extractPrefixForFindCommand(ArgumentMultimap argMultimap) throws ParseException { + if (argMultimap.isSinglePrefix()) { + for (Prefix prefix : FindCommand.ACCEPTED_PREFIXES) { + if (argMultimap.getValue(prefix).isPresent() + && !argMultimap.getValue(prefix).get().isEmpty()) { + return prefix; + } + } + } + + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); + } + + /** + * Checks if the value of the given {@code ArgumentMultimap} is a positive decimal number + * if the prefix is PREFIX_LESSTHAN or PREFIX_GREATERTHAN. + * @throws ParseException if the user input does not conform to the expected format + */ + private String extractValidKeyword(ArgumentMultimap argMultimap, Prefix prefix) throws ParseException { + if (prefix.equals(CliSyntax.PREFIX_LESS_THAN) || prefix.equals(CliSyntax.PREFIX_MORE_THAN)) { + if (!Score.isValidScoreString(argMultimap.getValue(prefix).get())) { + throw new ParseException(Score.MESSAGE_CONSTRAINTS); + } + } + return argMultimap.getValue(prefix).get(); + } } diff --git a/src/main/java/seedu/address/logic/parser/ImportCommandParser.java b/src/main/java/seedu/address/logic/parser/ImportCommandParser.java new file mode 100644 index 00000000000..1b4083be073 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ImportCommandParser.java @@ -0,0 +1,65 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_IMPORT; + +import java.nio.file.InvalidPathException; +import java.nio.file.Path; + +import seedu.address.logic.commands.ImportCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new {@code ImportCommand} object + */ +public class ImportCommandParser implements Parser { + + private static final String MESSAGE_NOT_CSV = "File %s is not a csv file \nImport command only accepts csv files"; + + private static final String MESSAGE_INVALID_PATH = "Invalid file path: %s"; + /** + * Parses the given {@code String} of arguments in the context of the {@code RemarkCommand} + * and returns a {@code RemarkCommand} object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ImportCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_IMPORT); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_IMPORT); + if (!isPrefixPresent( + argMultimap) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ImportCommand.MESSAGE_USAGE)); + } + Path path; + try { + path = ParserUtil.parseFilePath(argMultimap.getValue(PREFIX_IMPORT).orElse("")); + } catch (InvalidPathException e) { + throw new ParseException(String.format(MESSAGE_INVALID_PATH, e.getReason())); + } + + + if (!isCsvFile(path)) { + throw new ParseException(String.format(MESSAGE_NOT_CSV, path)); + } + return new ImportCommand(path); + } + + /** + * Returns true if the prefix contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean isPrefixPresent(ArgumentMultimap argumentMultimap) { + return argumentMultimap.getValue(CliSyntax.PREFIX_IMPORT).isPresent(); + } + + /** + * Returns true if the file is a CSV file. + * @param path the path of the file + * @return true if the file is a CSV file + */ + private static boolean isCsvFile(Path path) { + return path.toString().endsWith(".csv"); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ImportExamScoresCommandParser.java b/src/main/java/seedu/address/logic/parser/ImportExamScoresCommandParser.java new file mode 100644 index 00000000000..7360eff79ec --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ImportExamScoresCommandParser.java @@ -0,0 +1,60 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_IMPORT; + +import java.nio.file.InvalidPathException; +import java.nio.file.Path; + +import seedu.address.logic.commands.ImportExamScoresCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new {@code ImportExamCommand} object + */ +public class ImportExamScoresCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the {@code ImportExamCommand} + * @param args the arguments to be parsed + * @return a new {@code ImportExamCommand} object + * @throws ParseException if the user input does not conform the expected format + */ + public ImportExamScoresCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_IMPORT); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_IMPORT); + if (!isPrefixPresent( + argMultimap) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ImportExamScoresCommand.MESSAGE_USAGE)); + } + + try { + Path path = ParserUtil.parseFilePath(argMultimap.getValue(PREFIX_IMPORT).orElse("")); + if (!isCsvFile(path)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + ImportExamScoresCommand.MESSAGE_USAGE)); + } + return new ImportExamScoresCommand(path); + } catch (InvalidPathException e) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + ImportExamScoresCommand.MESSAGE_USAGE)); + } + } + + /** + * Returns true if the given file is a CSV file. + * @param path the path of the file + * @return true if the file is a CSV file + */ + private static boolean isCsvFile(Path path) { + return path.toString().endsWith(".csv"); + } + + private static boolean isPrefixPresent(ArgumentMultimap argumentMultimap) { + return argumentMultimap.getValue(CliSyntax.PREFIX_IMPORT).isPresent(); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..e6e974fef86 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -2,6 +2,8 @@ import static java.util.Objects.requireNonNull; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collection; import java.util.HashSet; import java.util.Set; @@ -9,10 +11,15 @@ import seedu.address.commons.core.index.Index; import seedu.address.commons.util.StringUtil; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.exam.Exam; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Phone; +import seedu.address.model.person.Score; +import seedu.address.model.student.Matric; +import seedu.address.model.student.Reflection; +import seedu.address.model.student.Studio; import seedu.address.model.tag.Tag; /** @@ -35,6 +42,95 @@ public static Index parseIndex(String oneBasedIndex) throws ParseException { return Index.fromOneBased(Integer.parseInt(trimmedIndex)); } + /** + * Parses {@code matric} into an {@code Matric} and returns it. Leading and trailing whitespaces will be trimmed. + * @param matric the matric number to be parsed + * @return the parsed matric number + * @throws ParseException if the specified matric number is invalid + */ + public static Matric parseMatric(String matric) throws ParseException { + String trimmedMatric = matric.trim(); + if (!Matric.isValidMatric(trimmedMatric)) { + throw new ParseException(Matric.MESSAGE_CONSTRAINTS); + } + return new Matric(trimmedMatric); + } + + /** + * Parses {@code matric} in the context of an Edit command where blank Matrics are accepted. + * @param matric the matric number to be parsed + * @return the parsed matric number + * @throws ParseException if the specified matric number is invalid + */ + public static Matric parseMatricForEdit(String matric) throws ParseException { + String trimmedMatric = matric.trim(); + if (!Matric.isValidConstructorParam(trimmedMatric)) { + throw new ParseException(Matric.MESSAGE_CONSTRAINTS); + } + return new Matric(trimmedMatric); + } + + /** + * Parses {@code reflection} into a {@code Reflection} and returns it. Leading and trailing whitespaces will be + * trimmed. + * @param reflection the reflection to be parsed + * @return the parsed reflection + * @throws ParseException if the specified reflection is invalid + */ + public static Reflection parseReflection(String reflection) throws ParseException { + requireNonNull(reflection); + String trimmedReflection = reflection.trim(); + if (!Reflection.isValidReflection(trimmedReflection)) { + throw new ParseException(Reflection.MESSAGE_CONSTRAINTS); + } + return new Reflection(trimmedReflection); + } + + /** + * Parses {@code reflection} in the context of an Edit command where blank Reflections are accepted. + * @param reflection the reflection to be parsed + * @return the parsed reflection + * @throws ParseException if the specified reflection is invalid + */ + public static Reflection parseReflectionForEdit(String reflection) throws ParseException { + requireNonNull(reflection); + String trimmedReflection = reflection.trim(); + if (!Reflection.isValidConstructorParam(trimmedReflection)) { + throw new ParseException(Reflection.MESSAGE_CONSTRAINTS); + } + return new Reflection(trimmedReflection); + } + + /** + * Parses {@code studio} into a {@code Studio} and returns it. Leading and trailing whitespaces will be trimmed. + * @param studio the studio to be parsed + * @return the parsed studio + * @throws ParseException if the specified studio is invalid + */ + public static Studio parseStudio(String studio) throws ParseException { + requireNonNull(studio); + String trimmedStudio = studio.trim(); + if (!Studio.isValidStudio(trimmedStudio)) { + throw new ParseException(Studio.MESSAGE_CONSTRAINTS); + } + return new Studio(trimmedStudio); + } + + /** + * Parses {@code studio} in the context of an Edit command where blank Studios are accepted. + * @param studio the studio to be parsed + * @return the parsed studio + * @throws ParseException if the specified studio is invalid + */ + public static Studio parseStudioForEdit(String studio) throws ParseException { + requireNonNull(studio); + String trimmedStudio = studio.trim(); + if (!Studio.isValidConstructorParam(trimmedStudio)) { + throw new ParseException(Studio.MESSAGE_CONSTRAINTS); + } + return new Studio(trimmedStudio); + } + /** * Parses a {@code String name} into a {@code Name}. * Leading and trailing whitespaces will be trimmed. @@ -121,4 +217,67 @@ public static Set parseTags(Collection tags) throws ParseException } return tagSet; } + + /** + * Parses a {@code String filePath} into a {@code filePath}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code filePath} is invalid. + */ + public static Path parseFilePath(String filePath) throws ParseException { + requireNonNull(filePath); + String trimmedTag = filePath.trim(); + // add error handling for what to do when invalid input is passed + return Paths.get(trimmedTag); + } + + /** + * Parses a {@code String score} into an {@code int}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code score} is invalid. + */ + public static Score parseScore(String score) throws ParseException { + requireNonNull(score); + String trimmedScore = score.trim(); + if (!Score.isValidScoreString(trimmedScore)) { + throw new ParseException(Score.MESSAGE_CONSTRAINTS); + } + + double parsedScore = Double.parseDouble(trimmedScore); + + return new Score(parsedScore); + } + + /** + * Parses a {@code String examScore} into an {@code int}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code examScore} is invalid. + */ + public static Score parseExamScore(String score) throws ParseException { + requireNonNull(score); + String trimmedScore = score.trim(); + if (!Exam.isValidExamScoreString(trimmedScore)) { + throw new ParseException(Exam.MESSAGE_CONSTRAINTS); + } + + double parsedScore = Double.parseDouble(trimmedScore); + return new Score(parsedScore); + } + + /** + * Parses a {@code String examName} into a {@code String}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code examName} is invalid. + */ + public static String parseExamName(String examName) throws ParseException { + requireNonNull(examName); + String trimmedExamName = examName.trim(); + if (!Exam.isValidExamName(trimmedExamName)) { + throw new ParseException(Exam.MESSAGE_CONSTRAINTS); + } + return trimmedExamName; + } } diff --git a/src/main/java/seedu/address/logic/parser/SelectExamCommandParser.java b/src/main/java/seedu/address/logic/parser/SelectExamCommandParser.java new file mode 100644 index 00000000000..035fcc4772f --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/SelectExamCommandParser.java @@ -0,0 +1,28 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.SelectExamCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new SelectExamCommand object + */ +public class SelectExamCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the SelectExamCommand + * and returns a SelectExamCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public SelectExamCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new SelectExamCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SelectExamCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 73397161e84..ad691e66b47 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -4,8 +4,11 @@ import java.util.List; +import javafx.collections.FXCollections; import javafx.collections.ObservableList; import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.exam.Exam; +import seedu.address.model.exam.UniqueExamList; import seedu.address.model.person.Person; import seedu.address.model.person.UniquePersonList; @@ -16,6 +19,7 @@ public class AddressBook implements ReadOnlyAddressBook { private final UniquePersonList persons; + private final UniqueExamList exams; /* * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication @@ -26,12 +30,13 @@ public class AddressBook implements ReadOnlyAddressBook { */ { persons = new UniquePersonList(); + exams = new UniqueExamList(); } public AddressBook() {} /** - * Creates an AddressBook using the Persons in the {@code toBeCopied} + * Creates an AddressBook using the Persons and Exams in the {@code toBeCopied} */ public AddressBook(ReadOnlyAddressBook toBeCopied) { this(); @@ -48,6 +53,14 @@ public void setPersons(List persons) { this.persons.setPersons(persons); } + /** + * Replaces the contents of the exam list with {@code exams}. + * {@code exams} must not contain duplicate exams. + */ + public void setExams(List exams) { + this.exams.setExams(exams); + } + /** * Resets the existing data of this {@code AddressBook} with {@code newData}. */ @@ -55,6 +68,7 @@ public void resetData(ReadOnlyAddressBook newData) { requireNonNull(newData); setPersons(newData.getPersonList()); + setExams(newData.getExamList()); } //// person-level operations @@ -94,12 +108,54 @@ public void removePerson(Person key) { persons.remove(key); } + //// exam-level operations + + /** + * Returns true if an exam with the same identity as {@code exam} exists in the address book. + */ + public boolean hasExam(Exam exam) { + requireNonNull(exam); + return exams.contains(exam); + } + + /** + * Adds an exam to the address book. + * The exam must not already exist in the address book. + */ + public void addExam(Exam e) { + exams.add(e); + } + + /** + * Removes {@code key} from this {@code AddressBook}. + * {@code key} must exist in the address book. + */ + public void removeExam(Exam key) { + exams.remove(key); + } + + + /** + * Returns the ObservableList containing the exam object with the given name, if it exists. + * @param examName The name of the exam to search for. + * @return An ObservableList containing the exam with the given name, if it exists. + */ + public ObservableList getExamByName(String examName) { + for (Exam exam : exams) { + if (exam.getName().equals(examName)) { + return FXCollections.observableArrayList(exam); + } + } + return FXCollections.observableArrayList(); + } + //// util methods @Override public String toString() { return new ToStringBuilder(this) .add("persons", persons) + .add("exams", exams) .toString(); } @@ -108,23 +164,41 @@ public ObservableList getPersonList() { return persons.asUnmodifiableObservableList(); } + @Override + public ObservableList getExamList() { + return exams.asUnmodifiableObservableList(); + } + + /** + * Returns the ObservableList containing the person object with the given email, if it exists. + * @param email The email of the person to search for. + * @return An ObservableList containing the person with the given email, if it exists. + */ + public ObservableList getPersonByEmail(String email) { + for (Person person : persons) { + if (person.getEmail().toString().equals(email)) { + return FXCollections.observableArrayList(person); + } + } + return FXCollections.observableArrayList(); + } + @Override public boolean equals(Object other) { if (other == this) { return true; } - // instanceof handles nulls if (!(other instanceof AddressBook)) { return false; } AddressBook otherAddressBook = (AddressBook) other; - return persons.equals(otherAddressBook.persons); + return persons.equals(otherAddressBook.persons) && exams.equals(otherAddressBook.exams); } @Override public int hashCode() { - return persons.hashCode(); + return persons.hashCode() + exams.hashCode(); } } diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..b05e0998a13 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -3,9 +3,12 @@ import java.nio.file.Path; import java.util.function.Predicate; +import javafx.beans.value.ObservableValue; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; +import seedu.address.model.exam.Exam; import seedu.address.model.person.Person; +import seedu.address.model.person.Score; /** * The API of the Model component. @@ -76,6 +79,30 @@ public interface Model { */ void setPerson(Person target, Person editedPerson); + /** + * Returns a read-only observable list of persons with the given email, if any. + * @param email The email to get the person by. + * @return A read-only observable list of persons with the given email. + */ + ObservableList getPersonByEmail(String email); + + /** + * Adds an exam score to the given person {@code target}, + * maintains immutability. A new person is set into the list with the exam score added + * @param target The person to add the exam score to. + * @param exam The exam to add the score to. + * @param score The score to add. + */ + void addExamScoreToPerson(Person target, Exam exam, Score score); + + /** + * Removes an exam from the given person {@code target}, + * maintains immutability. A new person is set into the list with the exam removed + * @param target The person to remove the exam from. + * @param exam The exam to remove. + */ + void removeExamScoreFromPerson(Person target, Exam exam); + /** Returns an unmodifiable view of the filtered person list */ ObservableList getFilteredPersonList(); @@ -84,4 +111,52 @@ public interface Model { * @throws NullPointerException if {@code predicate} is null. */ void updateFilteredPersonList(Predicate predicate); + + /** + * Returns true if an exam with the same identity as {@code exam} exists in the address book. + */ + boolean hasExam(Exam exam); + + /** + * Deletes the given exam. + * The exam must exist in the address book. + */ + void deleteExam(Exam target); + + /** + * Adds the given exam. + * {@code exam} must not already exist in the address book. + */ + void addExam(Exam exam); + + /** + * Selects the given exam. + * The exam must exist in the address book. + */ + void selectExam(Exam target); + + /** + * Deselects the selected exam. + */ + void deselectExam(); + + /** + * Returns the selected exam. + */ + ObservableValue getSelectedExam(); + + /** + * Returns the exam with the given name. + * @param examName The name of the exam to get. + * @return The exam with the given name. + */ + ObservableList getExamByName(String examName); + + /** Returns an unmodifiable view of the filtered exam list */ + ObservableList getExamList(); + + /** + * Returns the exam score statistics for the given exam. + */ + ObservableValue getSelectedExamStatistics(); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 57bc563fde6..0ad7aedd053 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -4,14 +4,22 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Objects; import java.util.function.Predicate; import java.util.logging.Logger; +import java.util.stream.Collectors; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.model.exam.Exam; import seedu.address.model.person.Person; +import seedu.address.model.person.Score; /** * Represents the in-memory model of the address book data. @@ -22,6 +30,8 @@ public class ModelManager implements Model { private final AddressBook addressBook; private final UserPrefs userPrefs; private final FilteredList filteredPersons; + private final SimpleObjectProperty selectedExam; + private final SimpleObjectProperty selectedExamStatistics; /** * Initializes a ModelManager with the given addressBook and userPrefs. @@ -34,6 +44,8 @@ public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs this.addressBook = new AddressBook(addressBook); this.userPrefs = new UserPrefs(userPrefs); filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + selectedExam = new SimpleObjectProperty<>(null); + selectedExamStatistics = new SimpleObjectProperty<>(null); } public ModelManager() { @@ -80,6 +92,7 @@ public void setAddressBookFilePath(Path addressBookFilePath) { @Override public void setAddressBook(ReadOnlyAddressBook addressBook) { this.addressBook.resetData(addressBook); + updateSelectedExamStatistics(); } @Override @@ -96,12 +109,14 @@ public boolean hasPerson(Person person) { @Override public void deletePerson(Person target) { addressBook.removePerson(target); + updateSelectedExamStatistics(); } @Override public void addPerson(Person person) { addressBook.addPerson(person); updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + updateSelectedExamStatistics(); } @Override @@ -109,6 +124,27 @@ public void setPerson(Person target, Person editedPerson) { requireAllNonNull(target, editedPerson); addressBook.setPerson(target, editedPerson); + updateSelectedExamStatistics(); + } + + @Override + public ObservableList getPersonByEmail(String email) { + return addressBook.getPersonByEmail(email); + } + + @Override + public void addExamScoreToPerson(Person person, Exam exam, Score score) { + Person newPerson = person.addExamScore(exam, score); + setPerson(person, newPerson); + updateSelectedExamStatistics(); + + } + + @Override + public void removeExamScoreFromPerson(Person person, Exam exam) { + Person newPerson = person.removeExam(exam); + setPerson(person, newPerson); + updateSelectedExamStatistics(); } //=========== Filtered Person List Accessors ============================================================= @@ -126,6 +162,7 @@ public ObservableList getFilteredPersonList() { public void updateFilteredPersonList(Predicate predicate) { requireNonNull(predicate); filteredPersons.setPredicate(predicate); + updateSelectedExamStatistics(); } @Override @@ -145,4 +182,104 @@ public boolean equals(Object other) { && filteredPersons.equals(otherModelManager.filteredPersons); } + //=========== Exam ================================================================================ + + @Override + public boolean hasExam(Exam exam) { + requireNonNull(exam); + return addressBook.hasExam(exam); + } + + @Override + public void deleteExam(Exam target) { + addressBook.removeExam(target); + for (Person person : addressBook.getPersonList()) { + if (person.hasExamScore(target)) { + removeExamScoreFromPerson(person, target); + } + } + if (selectedExam.getValue() != null && selectedExam.getValue().equals(target)) { + deselectExam(); + updateSelectedExamStatistics(); + } + } + + @Override + public void addExam(Exam exam) { + addressBook.addExam(exam); + } + + @Override + public ObservableList getExamList() { + return addressBook.getExamList(); + } + + @Override + public void selectExam(Exam target) { + requireNonNull(target); + selectedExam.set(target); + updateSelectedExamStatistics(); + } + + @Override + public void deselectExam() { + selectedExam.set(null); + updateSelectedExamStatistics(); + } + + /** + * Returns a view of the selected exam. (For updating UI purposes) + */ + @Override + public ObservableValue getSelectedExam() { + return selectedExam; + } + + @Override + public ObservableValue getSelectedExamStatistics() { + updateSelectedExamStatistics(); + return selectedExamStatistics; + } + + private void updateSelectedExamStatistics() { + if (selectedExam.getValue() == null) { + selectedExamStatistics.set(null); + return; + } + selectedExamStatistics.set(calculateExamScoreStatistics(selectedExam.getValue())); + } + + private ScoreStatistics calculateExamScoreStatistics(Exam exam) { + // Get all scores for the exam that exist in the filtered persons + List scores = filteredPersons.stream() + .map(person -> person.getScores().get(exam)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + if (scores.isEmpty()) { + return new ScoreStatistics(); + } + + double sum = scores.stream().mapToDouble(Score::getScore).sum(); + double mean = sum / scores.size(); //Division by zero is handled by the if statement above + double median = getMedian(scores); + + return new ScoreStatistics(mean, median); + } + + private double getMedian(List scores) { + Collections.sort(scores); + int size = scores.size(); + if (size % 2 == 0) { + return (scores.get(size / 2 - 1).getScore() + scores.get(size / 2).getScore()) / 2.0; + } else { + return scores.get(size / 2).getScore(); + } + } + + @Override + public ObservableList getExamByName(String examName) { + return addressBook.getExamByName(examName); + } + } diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java index 6ddc2cd9a29..12995aa1ebe 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java @@ -1,6 +1,7 @@ package seedu.address.model; import javafx.collections.ObservableList; +import seedu.address.model.exam.Exam; import seedu.address.model.person.Person; /** @@ -14,4 +15,10 @@ public interface ReadOnlyAddressBook { */ ObservableList getPersonList(); + /** + * Returns an unmodifiable view of the exams list. + * This list will not contain any duplicate persons. + */ + ObservableList getExamList(); + } diff --git a/src/main/java/seedu/address/model/ScoreStatistics.java b/src/main/java/seedu/address/model/ScoreStatistics.java new file mode 100644 index 00000000000..062afaa4987 --- /dev/null +++ b/src/main/java/seedu/address/model/ScoreStatistics.java @@ -0,0 +1,45 @@ +package seedu.address.model; + +import java.text.DecimalFormat; + +/** + * Represents the statistics of a list of scores. + */ +public class ScoreStatistics { + private final double mean; + private final double median; + + /** + * Constructs a {@code ScoreStatistics} object with the given statistics. + */ + public ScoreStatistics(double mean, double median) { + this.mean = mean; + this.median = median; + } + + /** + * Constructs a {@code ScoreStatistics} object with no scores available. + */ + public ScoreStatistics() { + this.mean = -1; + this.median = -1; + } + + public double getMean() { + return mean; + } + + public double getMedian() { + return median; + } + + @Override + public String toString() { + if (mean == -1) { + return "No scores available"; + } + DecimalFormat df = new DecimalFormat("0.##"); + return "Mean: " + df.format(mean) + + ", Median: " + df.format(median); + } +} diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/address/model/UserPrefs.java index 6be655fb4c7..2ab692fa80b 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/seedu/address/model/UserPrefs.java @@ -14,7 +14,7 @@ public class UserPrefs implements ReadOnlyUserPrefs { private GuiSettings guiSettings = new GuiSettings(); - private Path addressBookFilePath = Paths.get("data" , "addressbook.json"); + private Path addressBookFilePath = Paths.get("data" , "avengersassemble.json"); /** * Creates a {@code UserPrefs} with default values. diff --git a/src/main/java/seedu/address/model/exam/Exam.java b/src/main/java/seedu/address/model/exam/Exam.java new file mode 100644 index 00000000000..04b81bf1572 --- /dev/null +++ b/src/main/java/seedu/address/model/exam/Exam.java @@ -0,0 +1,107 @@ +package seedu.address.model.exam; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import seedu.address.model.person.Score; + +/** + * Represents an Exam in the address book. + * Guarantees: immutable; name is valid as declared in {@link #isValidName(String)} + */ +public class Exam { + + public static final String MESSAGE_CONSTRAINTS = "Names should only contain alphanumeric characters and spaces " + + "up to 30 characters, and it should not be blank. " + + Score.MESSAGE_CONSTRAINTS + " " + + "Exam Scores must also be greater than zero."; + + /* + * The first character of the name must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + * The rest of the string can contain any alphanumeric character and spaces. + */ + public static final String VALIDATION_REGEX_EXAM_NAME = "[\\p{Alnum}][\\p{Alnum} ]{0,29}"; + + public final String name; + public final Score maxScore; + /** + * Constructs an {@code Exam}. + * + * @param name A valid name. + * @param maxScore A valid max score. + */ + public Exam(String name, Score maxScore) { + requireNonNull(name); + checkArgument(isValidExamName(name), MESSAGE_CONSTRAINTS); + this.name = name; + this.maxScore = maxScore; + } + + public String getName() { + return name; + } + + public Score getMaxScore() { + return maxScore; + } + + /** + * Returns true if both exams have the same name. + * This defines a weaker notion of equality between two exams. + */ + public boolean isSameExam(Exam otherExam) { + if (otherExam == this) { + return true; + } + + return otherExam != null + && otherExam.getName().equals(getName()); + } + + /** + * Returns if a given string is a valid name. + */ + public static boolean isValidExamName(String test) { + return test.matches(VALIDATION_REGEX_EXAM_NAME); + } + + /** + * Returns true if a given string is a valid exam score. + */ + public static boolean isValidExamScoreString(String test) { + if (!test.matches(Score.VALIDATION_REGEX)) { + return false; + } + Double.parseDouble(test); + if (Double.parseDouble(test) <= 0) { + return false; + } + return true; + } + + @Override + public String toString() { + return name + ": " + maxScore; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Exam)) { + return false; + } + + Exam otherExam = (Exam) other; + return name.equals(otherExam.name) + && maxScore.equals(otherExam.maxScore); + } + + @Override + public int hashCode() { + return name.hashCode() + maxScore.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/exam/UniqueExamList.java b/src/main/java/seedu/address/model/exam/UniqueExamList.java new file mode 100644 index 00000000000..4da0e420db4 --- /dev/null +++ b/src/main/java/seedu/address/model/exam/UniqueExamList.java @@ -0,0 +1,138 @@ +package seedu.address.model.exam; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Iterator; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.exam.exceptions.DuplicateExamException; +import seedu.address.model.exam.exceptions.ExamNotFoundException; + +/** + * A list of exams that enforces uniqueness between its elements and does not allow nulls. + */ +public class UniqueExamList implements Iterable { + + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Returns true if the list contains an equivalent exam as the given argument. + */ + public boolean contains(Exam toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSameExam); + } + + /** + * Adds an exam to the list. + * The exam must not already exist in the list. + */ + public void add(Exam toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateExamException(); + } + internalList.add(toAdd); + } + + /** + * Replaces the exam {@code target} in the list with {@code editedExam}. + */ + public void setExam(Exam target, Exam editedExam) { + requireAllNonNull(target, editedExam); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new ExamNotFoundException(); + } + + if (!target.isSameExam(editedExam) && contains(editedExam)) { + throw new DuplicateExamException(); + } + + internalList.set(index, editedExam); + } + + /** + * Removes the equivalent exam from the list. + * The exam must exist in the list. + */ + public void remove(Exam toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new ExamNotFoundException(); + } + } + + /** + * Replaces the contents of this list with {@code exams}. + */ + public void setExams(UniqueExamList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + public void setExams(List exams) { + requireAllNonNull(exams); + if (!examsAreUnique(exams)) { + throw new DuplicateExamException(); + } + + internalList.setAll(exams); + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableObservableList() { + return internalUnmodifiableList; + } + + @Override + public Iterator iterator() { + return internalList.iterator(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof UniqueExamList)) { + return false; + } + + UniqueExamList otherUniqueExamList = (UniqueExamList) other; + return internalList.equals(otherUniqueExamList.internalList); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + @Override + public String toString() { + return internalList.toString(); + } + + /** + * Returns true if the list contains only unique exams. + */ + private boolean examsAreUnique(List exams) { + for (int i = 0; i < exams.size() - 1; i++) { + for (int j = i + 1; j < exams.size(); j++) { + if (exams.get(i).isSameExam(exams.get(j))) { + return false; + } + } + } + return true; + } +} diff --git a/src/main/java/seedu/address/model/exam/exceptions/DuplicateExamException.java b/src/main/java/seedu/address/model/exam/exceptions/DuplicateExamException.java new file mode 100644 index 00000000000..da5adbcda49 --- /dev/null +++ b/src/main/java/seedu/address/model/exam/exceptions/DuplicateExamException.java @@ -0,0 +1,11 @@ +package seedu.address.model.exam.exceptions; + +/** + * Signals that the operation will result in duplicate Exams (Exams are considered duplicates if they have the same + * identity). + */ +public class DuplicateExamException extends RuntimeException { + public DuplicateExamException() { + super("Operation would result in duplicate Exams"); + } +} diff --git a/src/main/java/seedu/address/model/exam/exceptions/ExamNotFoundException.java b/src/main/java/seedu/address/model/exam/exceptions/ExamNotFoundException.java new file mode 100644 index 00000000000..e3f42d61b7f --- /dev/null +++ b/src/main/java/seedu/address/model/exam/exceptions/ExamNotFoundException.java @@ -0,0 +1,6 @@ +package seedu.address.model.exam.exceptions; + +/** + * Signals that the operation is unable to find the specified exam. + */ +public class ExamNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java index 469a2cc9a1e..c0ddef6a589 100644 --- a/src/main/java/seedu/address/model/person/Address.java +++ b/src/main/java/seedu/address/model/person/Address.java @@ -9,13 +9,14 @@ */ public class Address { - public static final String MESSAGE_CONSTRAINTS = "Addresses can take any values, and it should not be blank"; + public static final String MESSAGE_CONSTRAINTS = + "Addresses can take any values except \"|\" up to 100 characters, and it should not be blank"; /* * The first character of the address must not be a whitespace, * otherwise " " (a blank string) becomes a valid input. */ - public static final String VALIDATION_REGEX = "[^\\s].*"; + public static final String VALIDATION_REGEX = "[^\\s].{0,99}"; public final String value; diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java index c62e512bc29..8025e82aaea 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/seedu/address/model/person/Email.java @@ -10,24 +10,33 @@ public class Email { private static final String SPECIAL_CHARACTERS = "+_.-"; - public static final String MESSAGE_CONSTRAINTS = "Emails should be of the format local-part@domain " - + "and adhere to the following constraints:\n" - + "1. The local-part should only contain alphanumeric characters and these special characters, excluding " - + "the parentheses, (" + SPECIAL_CHARACTERS + "). The local-part may not start or end with any special " - + "characters.\n" - + "2. This is followed by a '@' and then a domain name. The domain name is made up of domain labels " - + "separated by periods.\n" + public static final String MESSAGE_CONSTRAINTS = "Emails should be of the format local-part@domain, " + + "contain up to 100 characters, and adhere to the following constraints:\n" + + "1. The initial part of the email consists of the local-part.\n" + + "The local-part must:\n" + + " - have at least one character\n" + + " - only contain alphanumeric characters and these special characters, " + + "excluding the parentheses, (" + SPECIAL_CHARACTERS + ")\n" + + " - not start or end with any special characters\n" + + " - not contain two consecutive special characters\n" + + "2. This is followed by a '@' and then a domain name. " + + "The domain name is made up of domain labels " + + "which may be separated by periods.\n" + "The domain name must:\n" + + " - have at least one domain label\n" + + " - seperate domain labels by periods\n" + " - end with a domain label at least 2 characters long\n" - + " - have each domain label start and end with alphanumeric characters\n" - + " - have each domain label consist of alphanumeric characters, separated only by hyphens, if any."; + + "The domain labels must:\n" + + " - start and end with alphanumeric characters\n" + + " - not contain two consecutive hyphens\n" + + " - only contain alphanumeric characters and hyphens"; // alphanumeric and special characters - private static final String ALPHANUMERIC_NO_UNDERSCORE = "[^\\W_]+"; // alphanumeric characters except underscore - private static final String LOCAL_PART_REGEX = "^" + ALPHANUMERIC_NO_UNDERSCORE + "([" + SPECIAL_CHARACTERS + "]" - + ALPHANUMERIC_NO_UNDERSCORE + ")*"; + private static final String ALPHANUMERIC_NO_UNDERSCORE = "[^\\W_]+"; + private static final String LOCAL_PART_REGEX = "^" + ALPHANUMERIC_NO_UNDERSCORE + + "([" + SPECIAL_CHARACTERS + "]" + ALPHANUMERIC_NO_UNDERSCORE + ")*"; private static final String DOMAIN_PART_REGEX = ALPHANUMERIC_NO_UNDERSCORE + "(-" + ALPHANUMERIC_NO_UNDERSCORE + ")*"; - private static final String DOMAIN_LAST_PART_REGEX = "(" + DOMAIN_PART_REGEX + "){2,}$"; // At least two chars + private static final String DOMAIN_LAST_PART_REGEX = "(" + DOMAIN_PART_REGEX + "){2,}$"; private static final String DOMAIN_REGEX = "(" + DOMAIN_PART_REGEX + "\\.)*" + DOMAIN_LAST_PART_REGEX; public static final String VALIDATION_REGEX = LOCAL_PART_REGEX + "@" + DOMAIN_REGEX; @@ -48,7 +57,7 @@ public Email(String email) { * Returns if a given string is a valid email. */ public static boolean isValidEmail(String test) { - return test.matches(VALIDATION_REGEX); + return test.matches(VALIDATION_REGEX) && test.length() <= 100; } @Override @@ -68,7 +77,7 @@ public boolean equals(Object other) { } Email otherEmail = (Email) other; - return value.equals(otherEmail.value); + return value.equalsIgnoreCase(otherEmail.value); } @Override diff --git a/src/main/java/seedu/address/model/person/ExamPredicate.java b/src/main/java/seedu/address/model/person/ExamPredicate.java new file mode 100644 index 00000000000..3b198c866ab --- /dev/null +++ b/src/main/java/seedu/address/model/person/ExamPredicate.java @@ -0,0 +1,115 @@ +package seedu.address.model.person; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_LESS_THAN; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MORE_THAN; + +import java.util.function.Predicate; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.parser.Prefix; +import seedu.address.model.exam.Exam; + +/** + * Tests that a {@code Person}'s details contains the keyword given. + */ +public class ExamPredicate implements Predicate { + private final Prefix prefix; + private final String keyword; + private final Exam exam; + + /** + * Constructor for the PersonDetailContainsKeywordPredicate. + * @param prefix The prefix to indicate the detail of the person to be searched. + * @param keyword The keyword to be searched. + */ + public ExamPredicate(Prefix prefix, String keyword, Exam exam) { + this.prefix = prefix; + this.keyword = keyword; + this.exam = exam; + } + + /** + * Tests if the person's details contain the keyword. + */ + @Override + public boolean test(Person person) { + if (PREFIX_LESS_THAN.equals(prefix)) { + return hasScoreLessThanKeyword(person); + } else if (PREFIX_MORE_THAN.equals(prefix)) { + return hasScoreMoreThanKeyword(person); + } else { + // Code should not reach here + return false; + } + } + + /** + * Checks if the person's exam score is greater than the keyword. + * @param person The person to be checked. + * @return True if the person's exam score is greater than the keyword. + */ + private boolean hasScoreMoreThanKeyword(Person person) { + if (person.getScores().containsKey(exam)) { + return Double.parseDouble(person.getScores().get(exam).toString()) + > Double.parseDouble(keyword); + } else { + // Handle case when the selected exam is not found in the person's scores + return false; + } + } + + /** + * Checks if the person's exam score is less than the keyword. + * @param person The person to be checked. + * @return True if the person's exam score is less than the keyword. + */ + private boolean hasScoreLessThanKeyword(Person person) { + if (person.getScores().containsKey(exam)) { + return Double.parseDouble(person.getScores().get(exam).toString()) + < Double.parseDouble(keyword); + } else { + // Handle case when the selected exam is not found in the person's scores + return false; + } + } + + /** + * Checks if the current PersonDetailContainsKeywordPredicate is equal to another object. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ExamPredicate)) { + return false; + } + + ExamPredicate otherPredicate = + (ExamPredicate) other; + + return keyword.equals(otherPredicate.keyword) && prefix.equals(otherPredicate.prefix) + && exam.equals(otherPredicate.exam); + } + + /** + * Returns the string representation of the PersonDetailContainsKeywordPredicate. + */ + @Override + public String toString() { + return new ToStringBuilder(this).add("prefix", prefix.getPrefix()) + .add("keyword", keyword) + .add("exam", exam).toString(); + } + + /** + * Checks if the prefix requires an exam to be selected. + * @return True if the prefix is PREFIX_LESSTHAN or PREFIX_GREATERTHAN. + */ + public boolean isExamRequired() { + return prefix.equals(PREFIX_LESS_THAN) || prefix.equals(PREFIX_MORE_THAN); + } +} + diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java index 173f15b9b00..040306c7fae 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/person/Name.java @@ -10,13 +10,15 @@ public class Name { public static final String MESSAGE_CONSTRAINTS = - "Names should only contain alphanumeric characters and spaces, and it should not be blank"; + "Names should only contain alphanumeric characters, spaces and the following characters " + + "(',', '-', '.', '/', '(', ')'), it should be up to 80 characters long, " + + "and it should not be blank"; /* * The first character of the address must not be a whitespace, * otherwise " " (a blank string) becomes a valid input. */ - public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ,-./()]{0,79}"; public final String fullName; diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java deleted file mode 100644 index 62d19be2977..00000000000 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ /dev/null @@ -1,44 +0,0 @@ -package seedu.address.model.person; - -import java.util.List; -import java.util.function.Predicate; - -import seedu.address.commons.util.StringUtil; -import seedu.address.commons.util.ToStringBuilder; - -/** - * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. - */ -public class NameContainsKeywordsPredicate implements Predicate { - private final List keywords; - - public NameContainsKeywordsPredicate(List keywords) { - this.keywords = keywords; - } - - @Override - public boolean test(Person person) { - return keywords.stream() - .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof NameContainsKeywordsPredicate)) { - return false; - } - - NameContainsKeywordsPredicate otherNameContainsKeywordsPredicate = (NameContainsKeywordsPredicate) other; - return keywords.equals(otherNameContainsKeywordsPredicate.keywords); - } - - @Override - public String toString() { - return new ToStringBuilder(this).add("keywords", keywords).toString(); - } -} diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index abe8c46b535..8b237fda415 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -3,11 +3,17 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Objects; import java.util.Set; import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.exam.Exam; +import seedu.address.model.student.Matric; +import seedu.address.model.student.Reflection; +import seedu.address.model.student.Studio; import seedu.address.model.tag.Tag; /** @@ -25,16 +31,29 @@ public class Person { private final Address address; private final Set tags = new HashSet<>(); + private final Matric matric; + private final Reflection reflection; + private final Studio studio; + + private final HashMap scores = new HashMap<>(); + + /** * Every field must be present and not null. */ - public Person(Name name, Phone phone, Email email, Address address, Set tags) { - requireAllNonNull(name, phone, email, address, tags); + public Person(Name name, Phone phone, Email email, Address address, + Set tags, Matric matric, Reflection reflection , + Studio studio, Map scores) { + requireAllNonNull(name, phone, email, address, tags, matric, studio); this.name = name; this.phone = phone; this.email = email; this.address = address; this.tags.addAll(tags); + this.matric = matric; + this.reflection = reflection; + this.studio = studio; + this.scores.putAll(scores); } public Name getName() { @@ -61,6 +80,51 @@ public Set getTags() { return Collections.unmodifiableSet(tags); } + public Matric getMatric() { + return matric; + } + + public Studio getStudio() { + return studio; + } + + public Reflection getReflection() { + return reflection; + } + + /** + * Returns an immutable score set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public Map getScores() { + return new HashMap<>(scores); + } + + /** + * Returns true if the person has the given exam in the scores. + */ + public boolean hasExamScore(Exam exam) { + return scores.containsKey(exam); + } + + /** + * Returns a new Person with the given exam added to the scores, maintains immutability. + */ + public Person removeExam(Exam exam) { + Map newScores = new HashMap<>(scores); + newScores.remove(exam); + return new Person(name, phone, email, address, tags, matric, reflection, studio, newScores); + } + + /** + * Returns a new Person with the given exam added to the scores, maintains immutability. + */ + public Person addExamScore(Exam exam, Score score) { + Map newScores = new HashMap<>(scores); + newScores.put(exam, score); + return new Person(name, phone, email, address, tags, matric, reflection, studio, newScores); + } + /** * Returns true if both persons have the same name. * This defines a weaker notion of equality between two persons. @@ -71,7 +135,7 @@ public boolean isSamePerson(Person otherPerson) { } return otherPerson != null - && otherPerson.getName().equals(getName()); + && otherPerson.getEmail().equals(getEmail()); } /** @@ -94,13 +158,17 @@ public boolean equals(Object other) { && phone.equals(otherPerson.phone) && email.equals(otherPerson.email) && address.equals(otherPerson.address) - && tags.equals(otherPerson.tags); + && tags.equals(otherPerson.tags) + && matric.equals(otherPerson.matric) + && reflection.equals(otherPerson.reflection) + && studio.equals(otherPerson.studio) + && scores.equals(otherPerson.scores); } @Override public int hashCode() { // use this method for custom fields hashing instead of implementing your own - return Objects.hash(name, phone, email, address, tags); + return Objects.hash(name, phone, email, address, tags, matric, reflection, studio, scores); } @Override @@ -111,7 +179,10 @@ public String toString() { .add("email", email) .add("address", address) .add("tags", tags) + .add("matriculation number", matric) + .add("reflection", reflection) + .add("studio", studio) + .add("scores", scores) .toString(); } - } diff --git a/src/main/java/seedu/address/model/person/PersonDetailPredicate.java b/src/main/java/seedu/address/model/person/PersonDetailPredicate.java new file mode 100644 index 00000000000..370aadf85f5 --- /dev/null +++ b/src/main/java/seedu/address/model/person/PersonDetailPredicate.java @@ -0,0 +1,91 @@ +package seedu.address.model.person; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MATRIC_NUMBER; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_REFLECTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_STUDIO; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.parser.Prefix; + +/** + * Tests that a {@code Person}'s details contains the keyword given. + */ +public class PersonDetailPredicate implements Predicate { + private final Prefix prefix; + private final String keyword; + + /** + * Constructor for the PersonDetailContainsKeywordPredicate. + * @param prefix The prefix to indicate the detail of the person to be searched. + * @param keyword The keyword to be searched. + */ + public PersonDetailPredicate(Prefix prefix, String keyword) { + this.prefix = prefix; + this.keyword = keyword; + } + + /** + * Tests if the person's details contain the keyword. + */ + @Override + public boolean test(Person person) { + if (PREFIX_NAME.equals(prefix)) { + return StringUtil.containsSubstringIgnoreCase(person.getName().fullName, keyword); + } else if (PREFIX_PHONE.equals(prefix)) { + return StringUtil.containsSubstringIgnoreCase(person.getPhone().value, keyword); + } else if (PREFIX_EMAIL.equals(prefix)) { + return StringUtil.containsSubstringIgnoreCase(person.getEmail().value, keyword); + } else if (PREFIX_ADDRESS.equals(prefix)) { + return StringUtil.containsSubstringIgnoreCase(person.getAddress().value, keyword); + } else if (PREFIX_TAG.equals(prefix)) { + return person.getTags().stream() + .anyMatch(tag -> StringUtil.containsSubstringIgnoreCase(tag.tagName, keyword)); + } else if (PREFIX_MATRIC_NUMBER.equals(prefix)) { + return StringUtil.containsSubstringIgnoreCase(person.getMatric().matricNumber, keyword); + } else if (PREFIX_REFLECTION.equals(prefix)) { + return StringUtil.equalsIgnoreCase(person.getReflection().reflection, keyword); + } else if (PREFIX_STUDIO.equals(prefix)) { + return StringUtil.equalsIgnoreCase(person.getStudio().studio, keyword); + } else { + // Code should not reach here + return false; + } + } + + /** + * Checks if the current PersonDetailContainsKeywordPredicate is equal to another object. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof PersonDetailPredicate)) { + return false; + } + + PersonDetailPredicate otherPredicate = + (PersonDetailPredicate) other; + + return keyword.equals(otherPredicate.keyword) && prefix.equals(otherPredicate.prefix); + } + + /** + * Returns the string representation of the PersonDetailContainsKeywordPredicate. + */ + @Override + public String toString() { + return new ToStringBuilder(this).add("prefix", prefix.getPrefix()) + .add("keyword", keyword).toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java index d733f63d739..3136d846d0b 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/seedu/address/model/person/Phone.java @@ -9,10 +9,12 @@ */ public class Phone { - + // Prevent checkstyle violation + public static final String PLUS = "+"; public static final String MESSAGE_CONSTRAINTS = - "Phone numbers should only contain numbers, and it should be at least 3 digits long"; - public static final String VALIDATION_REGEX = "\\d{3,}"; + "Phone numbers should only contain numbers, and it should be between 3 and 30 digits long inclusive.\n" + + "It can start with a '" + PLUS + "' to represent the country code, but it is not necessary."; + public static final String VALIDATION_REGEX = "^\\" + PLUS + "?\\d{3,30}"; public final String value; /** diff --git a/src/main/java/seedu/address/model/person/Score.java b/src/main/java/seedu/address/model/person/Score.java new file mode 100644 index 00000000000..e000a32220f --- /dev/null +++ b/src/main/java/seedu/address/model/person/Score.java @@ -0,0 +1,85 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.text.DecimalFormat; + +/** + * Represents an Exam's score in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidScore(String)} + */ +public class Score implements Comparable { + + public static final String MESSAGE_CONSTRAINTS = + "Scores should be numeric, and it should have between 1 and 7 digits inclusive," + + "with up to 2 decimal places."; + + /* + * The score must be a non-negative integer. and only have up to 7 digits and 2 decimal places. + */ + public static final String VALIDATION_REGEX = "^\\d{1,7}(\\.\\d{1,2})?$"; + + public final double value; + + /** + * Constructs a {@code Score}. + * + * @param score A valid score. + */ + public Score(double score) { + requireNonNull(score); + checkArgument(isValidScore(score), MESSAGE_CONSTRAINTS); + this.value = score; + } + + /** + * Returns true if a given string is a valid score. + * IMPT: This method breaks when the score has more than 9 digits as + * the string representation of the score will be in scientific notation. However, scores are + * limited to 2 decimal places and 9 digits total. + */ + public static boolean isValidScore(double test) { + String str = Double.toString(test); + return str.matches(VALIDATION_REGEX); + } + + public static boolean isValidScoreString(String test) { + return test.matches(VALIDATION_REGEX); + } + + public double getScore() { + return value; + } + + @Override + public String toString() { + DecimalFormat df = new DecimalFormat("0.##"); + return df.format(value); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Score)) { + return false; + } + + Score otherScore = (Score) other; + return value == otherScore.value; + } + + @Override + public int hashCode() { + return Double.hashCode(value); + } + + @Override + public int compareTo(Score o) { + return Double.compare(this.value, o.value); + } +} diff --git a/src/main/java/seedu/address/model/student/Matric.java b/src/main/java/seedu/address/model/student/Matric.java new file mode 100644 index 00000000000..ac694359172 --- /dev/null +++ b/src/main/java/seedu/address/model/student/Matric.java @@ -0,0 +1,79 @@ +package seedu.address.model.student; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Matriculation Number in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidMatric(String)} + */ +public class Matric { + public static final String MESSAGE_CONSTRAINTS = "Matriculation numbers in the style of A1234567Z are accepted." + + "The first character must be 'A'," + + "followed by 7 digits and ending with an uppercase letter."; + public static final String VALIDATION_REGEX = "A[0-9]{7}[A-Z]"; + public final String matricNumber; + + /** + * Constructs a {@code Matric}. + * @param matricNumber A valid matriculation number. + */ + public Matric(String matricNumber) { + requireNonNull(matricNumber); + checkArgument(isValidConstructorParam(matricNumber), MESSAGE_CONSTRAINTS); + this.matricNumber = matricNumber; + } + + /** + * Returns true if a given string is a valid matriculation number for constructing a new Matric. + * @param matricNumber String to be tested + * @return true if the string is a valid matriculation number + */ + public static boolean isValidConstructorParam(String matricNumber) { + return isValidMatric(matricNumber) || isEmptyMatric(matricNumber); + } + + /** + * Returns true if a given string is an empty matriculation number. Only used when the prefix + * for matric is absent from a user command. + * @param matricNumber String to be tested + * @return true if the string is an empty matriculation number + */ + public static boolean isEmptyMatric(String matricNumber) { + return matricNumber.isBlank(); + } + + /** + * Returns true if a given string is a valid matriculation number. + * @param test String to be tested + * @return true if the string is a valid matriculation number + */ + public static boolean isValidMatric(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public boolean equals(Object object) { + if (object == this) { + return true; + } + if (!(object instanceof Matric)) { + return false; + } + Matric otherMatric = (Matric) object; + return matricNumber.equals(otherMatric.matricNumber); + } + + @Override + public int hashCode() { + return matricNumber.hashCode(); + } + + /** + * Format state as text for viewing. + * @return String representation of the matriculation number + */ + public String toString() { + return isEmptyMatric(matricNumber) ? "" : matricNumber; + } +} diff --git a/src/main/java/seedu/address/model/student/Reflection.java b/src/main/java/seedu/address/model/student/Reflection.java new file mode 100644 index 00000000000..dd505d65fc5 --- /dev/null +++ b/src/main/java/seedu/address/model/student/Reflection.java @@ -0,0 +1,81 @@ +package seedu.address.model.student; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Reflection in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidReflection(String)} + */ +public class Reflection { + public static final String MESSAGE_CONSTRAINTS = "Reflections in the style of R1.. are accepted." + + " The first character must be \"R\"," + + " followed by up to 4 digits."; + public static final String VALIDATION_REGEX = "R\\d{1,4}"; + public final String reflection; + + /** + * Constructs a {@code Reflection}. + * @param reflection A valid reflection. + */ + public Reflection(String reflection) { + requireNonNull(reflection); + checkArgument(isValidConstructorParam(reflection), MESSAGE_CONSTRAINTS); + this.reflection = reflection; + } + + /** + * Returns true if a given string is a valid reflection for constructing a new Reflection. + * @param test String to be tested + * @return true if the string is a valid reflection + */ + public static boolean isValidConstructorParam(String test) { + return isValidReflection(test) || isEmptyReflection(test); + } + + /** + * Returns true if a given string is a valid reflection for constructing a new Reflection. + * @param test String to be tested + * @return true if the string is an empty reflection + */ + public static boolean isEmptyReflection(String test) { + return test.isBlank(); + } + + /** + * Returns true if a given string is a valid reflection. + * @param test String to be tested + * @return true if the string is a valid reflection + */ + public static boolean isValidReflection(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public boolean equals(Object object) { + if (object == this) { + return true; + } + if (!(object instanceof Reflection)) { + return false; + } + Reflection otherReflection = (Reflection) object; + return otherReflection.reflection.equals(reflection); + } + + /** + * Returns the hashcode of the reflection. + */ + @Override + public int hashCode() { + return reflection.hashCode(); + } + + /** + * Returns the reflection in string format. + */ + @Override + public String toString() { + return reflection; + } +} diff --git a/src/main/java/seedu/address/model/student/Studio.java b/src/main/java/seedu/address/model/student/Studio.java new file mode 100644 index 00000000000..c4e81bfc469 --- /dev/null +++ b/src/main/java/seedu/address/model/student/Studio.java @@ -0,0 +1,85 @@ +package seedu.address.model.student; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Student's studio in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidStudio(String)} + */ +public class Studio { + public static final String MESSAGE_CONSTRAINTS = "Studios in the style of S12.. are accepted." + + "The first character must be \"S\"," + + "followed by up to 4 digits."; + public static final String VALIDATION_REGEX = "S\\d{1,4}"; + public final String studio; + + /** + * Constructs a {@code Studio}. + * @param studio A valid studio. + */ + public Studio(String studio) { + requireNonNull(studio); + checkArgument(isValidConstructorParam(studio), MESSAGE_CONSTRAINTS); + this.studio = studio; + } + + /** + * Returns true if a given string is a valid studio for constructing a new Studio. + * @param test + * @return + */ + public static boolean isValidConstructorParam(String test) { + return isValidStudio(test) || isEmptyStudio(test); + } + + /** + * Returns true if a given string is an empty studio. Only used when the prefix + * for studio is absent from a user command. + * @param test String to be tested + * @return true if the string is an empty studio + */ + public static boolean isEmptyStudio(String test) { + return test.isBlank(); + } + + /** + * Returns true if a given string is a valid studio. + * @param test String to be tested + * @return true if the string is a valid studio + */ + public static boolean isValidStudio(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public boolean equals(Object object) { + if (object == this) { + return true; + } + if (!(object instanceof Studio)) { + return false; + } + Studio otherStudio = (Studio) object; + return studio.equals(otherStudio.studio); + } + + /** + * Returns the hashcode of the studio + */ + @Override + public int hashCode() { + return studio.hashCode(); + } + + /** + * Returns the studio in string format. + */ + @Override + public String toString() { + return studio; + } + + + +} diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java index f1a0d4e233b..6045cd0c75e 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/seedu/address/model/tag/Tag.java @@ -9,8 +9,9 @@ */ public class Tag { - public static final String MESSAGE_CONSTRAINTS = "Tags names should be alphanumeric"; - public static final String VALIDATION_REGEX = "\\p{Alnum}+"; + public static final String MESSAGE_CONSTRAINTS = + "Tags names should be alphanumeric and can have up to 100 characters."; + public static final String VALIDATION_REGEX = "\\p{Alnum}{1,100}"; public final String tagName; diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facf..949a6a7260d 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -1,6 +1,7 @@ package seedu.address.model.util; import java.util.Arrays; +import java.util.HashMap; import java.util.Set; import java.util.stream.Collectors; @@ -11,6 +12,9 @@ import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.student.Matric; +import seedu.address.model.student.Reflection; +import seedu.address.model.student.Studio; import seedu.address.model.tag.Tag; /** @@ -20,23 +24,27 @@ public class SampleDataUtil { public static Person[] getSamplePersons() { return new Person[] { new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), - new Address("Blk 30 Geylang Street 29, #06-40"), - getTagSet("friends")), + new Address("Blk 30 Geylang Street 29, #06-40"), getTagSet("friends", "student"), + new Matric("A1111111X"), new Reflection("R1"), new Studio("S1"), new HashMap<>()), new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), - new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), - getTagSet("colleagues", "friends")), + new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), + getTagSet("colleagues", "friends", "student"), + new Matric("A2222222X"), new Reflection("R2"), new Studio("S2"), new HashMap<>()), new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), - new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), - getTagSet("neighbours")), + new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), getTagSet("neighbours", "student"), + new Matric("A3333333X"), new Reflection("R9"), new Studio("S3"), new HashMap<>()), new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), - new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), - getTagSet("family")), + new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), + getTagSet("family", "student"), new Matric("A4444444X"), + new Reflection("R3"), new Studio("S4"), new HashMap<>()), new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), - new Address("Blk 47 Tampines Street 20, #17-35"), - getTagSet("classmates")), + new Address("Blk 47 Tampines Street 20, #17-35"), + getTagSet("classmates", "TA"), new Matric("A5555555X"), + new Reflection("R4"), new Studio(""), new HashMap<>()), new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), - new Address("Blk 45 Aljunied Street 85, #11-31"), - getTagSet("colleagues")) + new Address("Blk 45 Aljunied Street 85, #11-31"), + getTagSet("colleagues", "instructor"), new Matric(""), + new Reflection(""), new Studio(""), new HashMap<>()) }; } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedExam.java b/src/main/java/seedu/address/storage/JsonAdaptedExam.java new file mode 100644 index 00000000000..a48af0c8b3a --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedExam.java @@ -0,0 +1,67 @@ +package seedu.address.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.exam.Exam; +import seedu.address.model.person.Score; + +class JsonAdaptedExam { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Exam's %s field is missing!"; + + private final String name; + private final String maxScore; + + /** + * Constructs a {@code JsonAdaptedExam} with the given {@code name}. + */ + @JsonCreator + public JsonAdaptedExam(@JsonProperty("name") String name, @JsonProperty("score") String maxScore) { + this.name = name; + this.maxScore = maxScore; + } + + /** + * Converts a given {@code Tag} into this class for Jackson use. + */ + public JsonAdaptedExam(Exam source) { + name = source.getName(); + maxScore = source.getMaxScore().toString(); + } + + public String getname() { + return name; + } + + public String getMaxScore() { + return maxScore; + } + + /** + * Converts this Jackson-friendly adapted tag object into the model's {@code Tag} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted tag. + */ + public Exam toModelType() throws IllegalValueException { + + if (name == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "name")); + } + if (Exam.isValidExamName(name) == false) { + throw new IllegalValueException(Exam.MESSAGE_CONSTRAINTS); + } + if (maxScore == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "maxScore")); + } + if (Exam.isValidExamScoreString(maxScore) == false) { + throw new IllegalValueException(Exam.MESSAGE_CONSTRAINTS); + } + + final String modelName = this.name; + final Score modelMaxScore = new Score(Double.parseDouble(this.maxScore)); + + return new Exam(modelName, modelMaxScore); + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedExamScore.java b/src/main/java/seedu/address/storage/JsonAdaptedExamScore.java new file mode 100644 index 00000000000..ff1ad9d2091 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedExamScore.java @@ -0,0 +1,52 @@ +package seedu.address.storage; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.model.exam.Exam; +import seedu.address.model.person.Score; + +/** + * Jackson-friendly version of {@link Exam}. + */ +public class JsonAdaptedExamScore { + private final String examName; + private final String examMaxScore; + private final String score; + + /** + * Constructs a {@code JsonAdaptedExamScore} with the given exam details. + */ + @JsonCreator + public JsonAdaptedExamScore(@JsonProperty("examName") String examName, + @JsonProperty("examMaxScore") String examMaxScore, + @JsonProperty("score") String score) { + this.examName = examName; + this.examMaxScore = examMaxScore; + this.score = score; + } + + /** + * Converts a given {@code Exam} into this class for Jackson use. + */ + public JsonAdaptedExamScore(Exam source, Score score) { + examName = source.getName(); + examMaxScore = Double.toString(source.getMaxScore().getScore()); + this.score = Double.toString(score.getScore()); + } + + /** + * Converts a given {@code Exam} into this class for Jackson use. + */ + public Exam toModelTypeExam() throws IllegalValueException { + return new Exam(ParserUtil.parseExamName(examName), ParserUtil.parseExamScore(examMaxScore)); + } + + /** + * Converts a given {@code Score} into this class for Jackson use. + */ + public Score toModelTypeScore() throws IllegalValueException { + return ParserUtil.parseScore(score); + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java index bd1ca0f56c8..8d3dae5cafb 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java @@ -1,8 +1,10 @@ package seedu.address.storage; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -10,11 +12,16 @@ import com.fasterxml.jackson.annotation.JsonProperty; import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.exam.Exam; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Score; +import seedu.address.model.student.Matric; +import seedu.address.model.student.Reflection; +import seedu.address.model.student.Studio; import seedu.address.model.tag.Tag; /** @@ -29,6 +36,10 @@ class JsonAdaptedPerson { private final String email; private final String address; private final List tags = new ArrayList<>(); + private final String matric; + private final String reflection; + private final String studio; + private final List examScores = new ArrayList<>(); /** * Constructs a {@code JsonAdaptedPerson} with the given person details. @@ -36,7 +47,10 @@ class JsonAdaptedPerson { @JsonCreator public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, @JsonProperty("email") String email, @JsonProperty("address") String address, - @JsonProperty("tags") List tags) { + @JsonProperty("tags") List tags, @JsonProperty("matric") String matric, + @JsonProperty("reflection") String reflection, + @JsonProperty("studio") String studio, + @JsonProperty("examScores") List examScores) { this.name = name; this.phone = phone; this.email = email; @@ -44,6 +58,12 @@ public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone if (tags != null) { this.tags.addAll(tags); } + this.matric = matric; + this.reflection = reflection; + this.studio = studio; + if (examScores != null) { + this.examScores.addAll(examScores); + } } /** @@ -57,6 +77,14 @@ public JsonAdaptedPerson(Person source) { tags.addAll(source.getTags().stream() .map(JsonAdaptedTag::new) .collect(Collectors.toList())); + matric = source.getMatric().matricNumber; + reflection = source.getReflection().reflection; + studio = source.getStudio().studio; + examScores.addAll(source.getScores().entrySet().stream() + .map(entry -> new JsonAdaptedExamScore(entry.getKey().getName(), + Double.toString(entry.getKey().getMaxScore().getScore()), + Double.toString(entry.getValue().getScore()))) + .collect(Collectors.toList())); } /** @@ -70,6 +98,11 @@ public Person toModelType() throws IllegalValueException { personTags.add(tag.toModelType()); } + final Map personExamScores = new HashMap<>(); + for (JsonAdaptedExamScore examScore : examScores) { + personExamScores.put(examScore.toModelTypeExam(), examScore.toModelTypeScore()); + } + if (name == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); } @@ -100,10 +133,30 @@ public Person toModelType() throws IllegalValueException { if (!Address.isValidAddress(address)) { throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); } + if (!Matric.isValidConstructorParam(matric)) { + throw new IllegalValueException(Matric.MESSAGE_CONSTRAINTS); + } + if (!Reflection.isValidConstructorParam(reflection)) { + throw new IllegalValueException(Reflection.MESSAGE_CONSTRAINTS); + } + if (!Studio.isValidConstructorParam(studio)) { + throw new IllegalValueException(Studio.MESSAGE_CONSTRAINTS); + } + final Address modelAddress = new Address(address); final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); + + final Matric modelMatric = new Matric(matric); + + final Reflection modelReflection = new Reflection(reflection); + + final Studio modelStudio = new Studio(studio); + + final Map scores = new HashMap<>(personExamScores); + + return new Person(modelName, modelPhone, modelEmail, modelAddress, + modelTags, modelMatric, modelReflection, modelStudio, scores); } } diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java index 5efd834091d..d74fb45509f 100644 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java @@ -11,6 +11,7 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.exam.Exam; import seedu.address.model.person.Person; /** @@ -20,15 +21,20 @@ class JsonSerializableAddressBook { public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; + public static final String MESSAGE_DUPLICATE_EXAM = "Exams list contains duplicate exam(s)."; private final List persons = new ArrayList<>(); + private final List exams = new ArrayList<>(); /** * Constructs a {@code JsonSerializableAddressBook} with the given persons. */ @JsonCreator - public JsonSerializableAddressBook(@JsonProperty("persons") List persons) { + public JsonSerializableAddressBook( + @JsonProperty("persons") List persons, + @JsonProperty("exams") List exams) { this.persons.addAll(persons); + this.exams.addAll(exams); } /** @@ -38,6 +44,7 @@ public JsonSerializableAddressBook(@JsonProperty("persons") List { + + private static final String FXML = "ExamListCard.fxml"; + + public final Exam exam; + + @FXML + private HBox examCardPane; + @FXML + private Label name; + @FXML + private Label id; + @FXML + private Label maxScore; + + /** + * Creates a {@code ExamCard} with the given {@code Exam} and index to display. + */ + public ExamCard(Exam exam, int displayedIndex, ObservableValue selectedExam) { + super(FXML); + this.exam = exam; + id.setText(displayedIndex + ". "); + name.setText(exam.getName()); + maxScore.setText(String.valueOf(exam.getMaxScore())); + + if (this.exam.equals(selectedExam.getValue())) { + highlight(); + } else { + removeHighlight(); + } + } + + public void highlight() { + examCardPane.setStyle("-fx-background-color: #3884a1;"); + } + + public void removeHighlight() { + examCardPane.setStyle("-fx-background-color: transparent;"); + } +} diff --git a/src/main/java/seedu/address/ui/ExamListPanel.java b/src/main/java/seedu/address/ui/ExamListPanel.java new file mode 100644 index 00000000000..540ff3fcead --- /dev/null +++ b/src/main/java/seedu/address/ui/ExamListPanel.java @@ -0,0 +1,53 @@ +package seedu.address.ui; + +import javafx.beans.value.ObservableValue; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.model.exam.Exam; + +/** + * Panel containing the list of exams. + */ +public class ExamListPanel extends UiPart { + private static final String FXML = "ExamListPanel.fxml"; + + @FXML + private ListView examListView; + private ObservableValue selectedExam; + + /** + * Creates a {@code ExamListPanel} with the given {@code ObservableList}. + */ + public ExamListPanel(ObservableList examList, ObservableValue selectedExam) { + super(FXML); + examListView.setItems(examList); + examListView.setCellFactory(listView -> new ExamListViewCell()); + examListView.setSelectionModel(new NoSelectionModel()); + this.selectedExam = selectedExam; + } + + public void update() { + examListView.refresh(); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Exam} using a {@code ExamCard}. + */ + class ExamListViewCell extends ListCell { + @Override + protected void updateItem(Exam exam, boolean empty) { + super.updateItem(exam, empty); + + if (empty || exam == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new ExamCard(exam, getIndex() + 1, selectedExam).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 3f16b2fcf26..24508f5f755 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/address/ui/HelpWindow.java @@ -15,7 +15,7 @@ */ public class HelpWindow extends UiPart { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; + public static final String USERGUIDE_URL = "https://ay2324s2-cs2103t-t10-1.github.io/tp/UserGuide.html"; public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 79e74ef37c0..17cb0d68e4a 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -32,7 +32,9 @@ public class MainWindow extends UiPart { // Independent Ui parts residing in this Ui container private PersonListPanel personListPanel; + private ExamListPanel examListPanel; private ResultDisplay resultDisplay; + private StatusBarFooter statusBarFooter; private HelpWindow helpWindow; @FXML @@ -44,6 +46,9 @@ public class MainWindow extends UiPart { @FXML private StackPane personListPanelPlaceholder; + @FXML + private StackPane examListPanelPlaceholder; + @FXML private StackPane resultDisplayPlaceholder; @@ -110,13 +115,16 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { * Fills up all the placeholders of this window. */ void fillInnerParts() { - personListPanel = new PersonListPanel(logic.getFilteredPersonList()); + personListPanel = new PersonListPanel(logic.getFilteredPersonList(), logic.getSelectedExam()); personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + examListPanel = new ExamListPanel(logic.getExamList(), logic.getSelectedExam()); + examListPanelPlaceholder.getChildren().add(examListPanel.getRoot()); + resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); - StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath()); + statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath(), logic.getSelectedExamStatistics()); statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); CommandBox commandBox = new CommandBox(this::executeCommand); @@ -186,7 +194,13 @@ private CommandResult executeCommand(String commandText) throws CommandException handleExit(); } + personListPanel.update(); + examListPanel.update(); + statusBarFooter.update(); + return commandResult; + + } catch (CommandException | ParseException e) { logger.info("An error occurred while executing command: " + commandText); resultDisplay.setFeedbackToUser(e.getMessage()); diff --git a/src/main/java/seedu/address/ui/NoSelectionModel.java b/src/main/java/seedu/address/ui/NoSelectionModel.java new file mode 100644 index 00000000000..aef334eee34 --- /dev/null +++ b/src/main/java/seedu/address/ui/NoSelectionModel.java @@ -0,0 +1,80 @@ +package seedu.address.ui; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.control.MultipleSelectionModel; + +/** + * A MultipleSelectionModel that disallows selecting any indices. + * Used in {@code PersonListPanel} and {@code ExamListPanel} to prevent user from selecting any items. + */ +//@@author delishad21-reused +//Reused from https://stackoverflow.com/questions/20621752/javafx-make-listview-not-selectable-via-mouse +//with minor modifications +public class NoSelectionModel extends MultipleSelectionModel { + + @Override + public ObservableList getSelectedIndices() { + return FXCollections.emptyObservableList(); + } + + @Override + public ObservableList getSelectedItems() { + return FXCollections.emptyObservableList(); + } + + @Override + public void selectIndices(int index, int... indices) { + } + + @Override + public void selectAll() { + } + + @Override + public void selectFirst() { + } + + @Override + public void selectLast() { + } + + @Override + public void clearAndSelect(int index) { + } + + @Override + public void select(int index) { + } + + @Override + public void select(T obj) { + } + + @Override + public void clearSelection(int index) { + } + + @Override + public void clearSelection() { + } + + @Override + public boolean isSelected(int index) { + return false; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public void selectPrevious() { + } + + @Override + public void selectNext() { + } +} +//@@author diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index 094c42cda82..ed56a3ac1e9 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -2,12 +2,18 @@ import java.util.Comparator; +import javafx.beans.value.ObservableValue; import javafx.fxml.FXML; import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import seedu.address.model.exam.Exam; import seedu.address.model.person.Person; +import seedu.address.model.person.Score; /** * An UI component that displays information of a {@code Person}. @@ -40,11 +46,28 @@ public class PersonCard extends UiPart { private Label email; @FXML private FlowPane tags; + @FXML + private VBox classes; + @FXML + private Label matric; + @FXML + private VBox examScore; + + @FXML + private ImageView phoneicon; + @FXML + private ImageView emailicon; + @FXML + private ImageView addressicon; + + private Image phoneIcon = new Image(this.getClass().getResourceAsStream("/images/phoneicon.png")); + private Image emailIcon = new Image(this.getClass().getResourceAsStream("/images/emailicon.png")); + private Image addressIcon = new Image(this.getClass().getResourceAsStream("/images/addressicon.png")); /** * Creates a {@code PersonCode} with the given {@code Person} and index to display. */ - public PersonCard(Person person, int displayedIndex) { + public PersonCard(Person person, int displayedIndex, ObservableValue selectedExam) { super(FXML); this.person = person; id.setText(displayedIndex + ". "); @@ -52,8 +75,40 @@ public PersonCard(Person person, int displayedIndex) { phone.setText(person.getPhone().value); address.setText(person.getAddress().value); email.setText(person.getEmail().value); + matric.setText(person.getMatric().toString()); + if (matric.getText().isEmpty()) { + matric.setVisible(false); + matric.setManaged(false); + } person.getTags().stream() .sorted(Comparator.comparing(tag -> tag.tagName)) .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + + if (!person.getReflection().toString().isEmpty()) { + classes.getChildren().add(new Label(person.getReflection().toString())); + } + if (!person.getStudio().toString().isEmpty()) { + classes.getChildren().add(new Label(person.getStudio().toString())); + } + + // Update exam score whenever new personcard is created + populateExamScore(selectedExam.getValue()); + + phoneicon.setImage(phoneIcon); + emailicon.setImage(emailIcon); + addressicon.setImage(addressIcon); + } + + /** + * Populates the exam score of the person. + */ + public void populateExamScore(Exam selectedExamValue) { + if (selectedExamValue != null) { + Score score = person.getScores().get(selectedExamValue); + if (score != null) { + examScore.getChildren().add(new Label("Score: \n" + score.toString())); + } + } } + } diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/seedu/address/ui/PersonListPanel.java index f4c501a897b..255bc54feaf 100644 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ b/src/main/java/seedu/address/ui/PersonListPanel.java @@ -1,13 +1,12 @@ package seedu.address.ui; -import java.util.logging.Logger; - +import javafx.beans.value.ObservableValue; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.layout.Region; -import seedu.address.commons.core.LogsCenter; +import seedu.address.model.exam.Exam; import seedu.address.model.person.Person; /** @@ -15,18 +14,24 @@ */ public class PersonListPanel extends UiPart { private static final String FXML = "PersonListPanel.fxml"; - private final Logger logger = LogsCenter.getLogger(PersonListPanel.class); @FXML private ListView personListView; + private ObservableValue selectedExam; /** * Creates a {@code PersonListPanel} with the given {@code ObservableList}. */ - public PersonListPanel(ObservableList personList) { + public PersonListPanel(ObservableList personList, ObservableValue selectedExam) { super(FXML); personListView.setItems(personList); personListView.setCellFactory(listView -> new PersonListViewCell()); + personListView.setSelectionModel(new NoSelectionModel()); + this.selectedExam = selectedExam; + } + + public void update() { + personListView.refresh(); } /** @@ -41,7 +46,7 @@ protected void updateItem(Person person, boolean empty) { setGraphic(null); setText(null); } else { - setGraphic(new PersonCard(person, getIndex() + 1).getRoot()); + setGraphic(new PersonCard(person, getIndex() + 1, selectedExam).getRoot()); } } } diff --git a/src/main/java/seedu/address/ui/StatusBarFooter.java b/src/main/java/seedu/address/ui/StatusBarFooter.java index b577f829423..2f9dee3e2a9 100644 --- a/src/main/java/seedu/address/ui/StatusBarFooter.java +++ b/src/main/java/seedu/address/ui/StatusBarFooter.java @@ -3,10 +3,11 @@ import java.nio.file.Path; import java.nio.file.Paths; +import javafx.beans.value.ObservableValue; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.layout.Region; - +import seedu.address.model.ScoreStatistics; /** * A ui for the status bar that is displayed at the footer of the application. */ @@ -16,13 +17,28 @@ public class StatusBarFooter extends UiPart { @FXML private Label saveLocationStatus; + @FXML + private Label gradeStatistics; + private ObservableValue selectedExamStatistics; /** * Creates a {@code StatusBarFooter} with the given {@code Path}. */ - public StatusBarFooter(Path saveLocation) { + public StatusBarFooter(Path saveLocation, ObservableValue selectedExamStatistics) { super(FXML); saveLocationStatus.setText(Paths.get(".").resolve(saveLocation).toString()); + this.selectedExamStatistics = selectedExamStatistics; + } + + /** + * Updates the status bar to display the grade statistics of the selected exam. + */ + public void update() { + if (selectedExamStatistics.getValue() == null) { + gradeStatistics.setText(""); + } else { + gradeStatistics.setText(selectedExamStatistics.getValue().toString()); + } } } diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java index fdf024138bc..bc4fa27ead0 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/address/ui/UiManager.java @@ -20,7 +20,7 @@ public class UiManager implements Ui { public static final String ALERT_DIALOG_PANE_FIELD_ID = "alertDialogPane"; private static final Logger logger = LogsCenter.getLogger(UiManager.class); - private static final String ICON_APPLICATION = "/images/address_book_32.png"; + private static final String ICON_APPLICATION = "/images/AaLogo.png"; private Logic logic; private MainWindow mainWindow; diff --git a/src/main/resources/images/AaLogo.png b/src/main/resources/images/AaLogo.png new file mode 100644 index 00000000000..55f711ef9f9 Binary files /dev/null and b/src/main/resources/images/AaLogo.png differ diff --git a/src/main/resources/images/address_book_32.png b/src/main/resources/images/address_book_32.png deleted file mode 100644 index 29810cf1fd9..00000000000 Binary files a/src/main/resources/images/address_book_32.png and /dev/null differ diff --git a/src/main/resources/images/addressicon.png b/src/main/resources/images/addressicon.png new file mode 100644 index 00000000000..79888c608fc Binary files /dev/null and b/src/main/resources/images/addressicon.png differ diff --git a/src/main/resources/images/emailicon.png b/src/main/resources/images/emailicon.png new file mode 100644 index 00000000000..7169a25e0af Binary files /dev/null and b/src/main/resources/images/emailicon.png differ diff --git a/src/main/resources/images/phoneicon.png b/src/main/resources/images/phoneicon.png new file mode 100644 index 00000000000..5d122d32634 Binary files /dev/null and b/src/main/resources/images/phoneicon.png differ diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8..93bcbe5d7e3 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -1,6 +1,6 @@ .background { - -fx-background-color: derive(#1d1d1d, 20%); - background-color: #383838; /* Used in the default.html file */ + -fx-background-color: derive(#1d2935, 20%); + background-color: #1f2b37; /* Used in the default.html file */ } .label { @@ -40,9 +40,9 @@ } .table-view { - -fx-base: #1d1d1d; - -fx-control-inner-background: #1d1d1d; - -fx-background-color: #1d1d1d; + -fx-base: #1d2935; + -fx-control-inner-background: #1d2935; + -fx-background-color: #1d2935; -fx-table-cell-border-color: transparent; -fx-table-header-border-color: transparent; -fx-padding: 5; @@ -77,20 +77,20 @@ } .split-pane:horizontal .split-pane-divider { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: derive(#1d2935, 20%); -fx-border-color: transparent transparent transparent #4d4d4d; } .split-pane { -fx-border-radius: 1; -fx-border-width: 1; - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: derive(#1d2935, 20%); } .list-view { -fx-background-insets: 0; -fx-padding: 0; - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: derive(#1d2935, 20%); } .list-cell { @@ -100,11 +100,11 @@ } .list-cell:filled:even { - -fx-background-color: #3c3e3f; + -fx-background-color: #36465b; } .list-cell:filled:odd { - -fx-background-color: #515658; + -fx-background-color: #273545; } .list-cell:filled:selected { @@ -133,17 +133,17 @@ } .stack-pane { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: derive(#1d2935, 20%); } .pane-with-border { - -fx-background-color: derive(#1d1d1d, 20%); - -fx-border-color: derive(#1d1d1d, 10%); + -fx-background-color: derive(#1d2935, 20%); + -fx-border-color: derive(#1d2935, 10%); -fx-border-top-width: 1px; } .status-bar { - -fx-background-color: derive(#1d1d1d, 30%); + -fx-background-color: derive(#1d2935, 30%); } .result-display { @@ -165,8 +165,8 @@ } .status-bar-with-border { - -fx-background-color: derive(#1d1d1d, 30%); - -fx-border-color: derive(#1d1d1d, 25%); + -fx-background-color: derive(#1d2935, 30%); + -fx-border-color: derive(#1d2935, 25%); -fx-border-width: 1px; } @@ -175,17 +175,17 @@ } .grid-pane { - -fx-background-color: derive(#1d1d1d, 30%); - -fx-border-color: derive(#1d1d1d, 30%); + -fx-background-color: derive(#1d2935, 30%); + -fx-border-color: derive(#1d2935, 30%); -fx-border-width: 1px; } .grid-pane .stack-pane { - -fx-background-color: derive(#1d1d1d, 30%); + -fx-background-color: derive(#1d2935, 30%); } .context-menu { - -fx-background-color: derive(#1d1d1d, 50%); + -fx-background-color: derive(#1d2935, 50%); } .context-menu .label { @@ -193,7 +193,7 @@ } .menu-bar { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: derive(#1d2935, 20%); } .menu-bar .label { @@ -217,7 +217,7 @@ -fx-border-color: #e2e2e2; -fx-border-width: 2; -fx-background-radius: 0; - -fx-background-color: #1d1d1d; + -fx-background-color: #1d2935; -fx-font-family: "Segoe UI", Helvetica, Arial, sans-serif; -fx-font-size: 11pt; -fx-text-fill: #d8d8d8; @@ -230,7 +230,7 @@ .button:pressed, .button:default:hover:pressed { -fx-background-color: white; - -fx-text-fill: #1d1d1d; + -fx-text-fill: #1d2935; } .button:focused { @@ -243,7 +243,7 @@ .button:disabled, .button:default:disabled { -fx-opacity: 0.4; - -fx-background-color: #1d1d1d; + -fx-background-color: #1d2935; -fx-text-fill: white; } @@ -257,11 +257,11 @@ } .dialog-pane { - -fx-background-color: #1d1d1d; + -fx-background-color: #1d2935; } .dialog-pane > *.button-bar > *.container { - -fx-background-color: #1d1d1d; + -fx-background-color: #1d2935; } .dialog-pane > *.label.content { @@ -271,7 +271,7 @@ } .dialog-pane:header *.header-panel { - -fx-background-color: derive(#1d1d1d, 25%); + -fx-background-color: derive(#1d2935, 25%); } .dialog-pane:header *.header-panel *.label { @@ -282,11 +282,11 @@ } .scroll-bar { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: derive(#1d2935, 20%); } .scroll-bar .thumb { - -fx-background-color: derive(#1d1d1d, 50%); + -fx-background-color: derive(#1d2935, 50%); -fx-background-insets: 3; } @@ -318,9 +318,9 @@ } #commandTextField { - -fx-background-color: transparent #383838 transparent #383838; + -fx-background-color: transparent #1f2b37 transparent #1f2b37; -fx-background-insets: 0; - -fx-border-color: #383838 #383838 #ffffff #383838; + -fx-border-color: #1f2b37 #1f2b37 #ffffff #1f2b37; -fx-border-insets: 0; -fx-border-width: 1; -fx-font-family: "Segoe UI Light"; @@ -333,7 +333,7 @@ } #resultDisplay .content { - -fx-background-color: transparent, #383838, transparent, #383838; + -fx-background-color: transparent, #1f2b37, transparent, #1f2b37; -fx-background-radius: 0; } @@ -345,8 +345,28 @@ #tags .label { -fx-text-fill: white; -fx-background-color: #3e7b91; - -fx-padding: 1 3 1 3; + -fx-padding: 3 6 3 6; -fx-border-radius: 2; -fx-background-radius: 2; -fx-font-size: 11; } + +#classes { + -fx-hgap: 7; +} + +#classes .label { + -fx-font-size: 14; + -fx-text-fill: white; + -fx-background-color: #5c5c5c; + -fx-padding: 10 20 10 20; + -fx-background-radius: 2; + -fx-font-size: 11; +} + +.label-header-small { + -fx-font-size: 16pt; + -fx-font-family: "Segoe UI Light"; + -fx-text-fill: white; + -fx-opacity: 1; +} diff --git a/src/main/resources/view/ExamListCard.fxml b/src/main/resources/view/ExamListCard.fxml new file mode 100644 index 00000000000..af7b7d1c207 --- /dev/null +++ b/src/main/resources/view/ExamListCard.fxml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/ExamListPanel.fxml b/src/main/resources/view/ExamListPanel.fxml new file mode 100644 index 00000000000..4a013ea4de9 --- /dev/null +++ b/src/main/resources/view/ExamListPanel.fxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/view/Extensions.css b/src/main/resources/view/Extensions.css index bfe82a85964..d02bb48ec57 100644 --- a/src/main/resources/view/Extensions.css +++ b/src/main/resources/view/Extensions.css @@ -5,7 +5,7 @@ .list-cell:empty { /* Empty cells will not have alternating colours */ - -fx-background: #383838; + -fx-background: #1f2b37; } .tag-selector { @@ -18,3 +18,4 @@ .tooltip-text { -fx-text-fill: white; } + diff --git a/src/main/resources/view/HelpWindow.css b/src/main/resources/view/HelpWindow.css index 17e8a8722cd..acc259c77f5 100644 --- a/src/main/resources/view/HelpWindow.css +++ b/src/main/resources/view/HelpWindow.css @@ -15,5 +15,5 @@ } #helpMessageContainer { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: derive(#1d2935, 20%); } diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 7778f666a0a..f15f4ed54d8 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -3,18 +3,20 @@ + - + + + - + - + @@ -33,27 +35,57 @@ - + - + - + - + + + + + + + + + + + + - - - - - - + + + + + + + + + - + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index f5e812e25e6..abcf1abda57 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -2,35 +2,63 @@ + + - - + + - + + + - + - + - + - - + + + + + + + + diff --git a/src/main/resources/view/StatusBarFooter.fxml b/src/main/resources/view/StatusBarFooter.fxml index 7b430f9c6a2..261f7790e49 100644 --- a/src/main/resources/view/StatusBarFooter.fxml +++ b/src/main/resources/view/StatusBarFooter.fxml @@ -3,10 +3,16 @@ + - + + diff --git a/src/test/data/ExportCommandTest/invalidJsonFile.json b/src/test/data/ExportCommandTest/invalidJsonFile.json new file mode 100644 index 00000000000..a1097343b5d --- /dev/null +++ b/src/test/data/ExportCommandTest/invalidJsonFile.json @@ -0,0 +1 @@ +not json format! diff --git a/src/test/data/ImportCommandTest/compulsoryParameterHeaderMissing.csv b/src/test/data/ImportCommandTest/compulsoryParameterHeaderMissing.csv new file mode 100644 index 00000000000..f703622435a --- /dev/null +++ b/src/test/data/ImportCommandTest/compulsoryParameterHeaderMissing.csv @@ -0,0 +1,8 @@ +phone,email,address,matric,reflection,studio,tags +"Alice Pauline",94351253,alice@example.com,"123, Jurong West Ave 6, #08-111",A1111111A,R1,S1,friends +"Benson Meier",98765432,johnd@example.com,"311, Clementi Ave 2, #02-25",A2222222A,R2,S2,friends +"Carl Kurz",95352563,heinz@example.com,"wall street",A3333333A,R3,S3, +"Daniel Meier",87652533,cornelia@example.com,"10th street",A4444444A,R4,S4,friends +"Elle Meyer",9482224,werner@example.com,"michegan ave",A5555555A,R5,S5, +"Fiona Kunz",9482427,lydia@example.com,"little tokyo",A6666666A,R6,S6, +"George Best",9482442,anna@example.com,"4th street",A7777777A,R7,S7, diff --git a/src/test/data/ImportCommandTest/duplicateHeaders.csv b/src/test/data/ImportCommandTest/duplicateHeaders.csv new file mode 100644 index 00000000000..89adb88da26 --- /dev/null +++ b/src/test/data/ImportCommandTest/duplicateHeaders.csv @@ -0,0 +1,8 @@ +name,phone,email,address,matric,reflection,studio,tags,email +"Alice Pauline",94351253,alice@example.com,"123, Jurong West Ave 6, #08-111",A1111111A,R1,S1,friends,alice@example.com +"Benson Meier",98765432,johnd@example.com,"311, Clementi Ave 2, #02-25",A2222222A,R2,S2,friends,johnd@example.com +"Carl Kurz",95352563,heinz@example.com,"wall street",A3333333A,R3,S3,,heinz@example.com +"Daniel Meier",87652533,cornelia@example.com,"10th street",A4444444A,R4,S4,friends,cornelia@example.com +"Elle Meyer",9482224,werner@example.com,"michegan ave",A5555555A,R5,S5,,werner@example.com +"Fiona Kunz",9482427,lydia@example.com,"little tokyo",A6666666A,R6,S6,,lydia@example.com +"George Best",9482442,anna@example.com,"4th street",A7777777A,R7,S7,,anna@example.com diff --git a/src/test/data/ImportCommandTest/empty.csv b/src/test/data/ImportCommandTest/empty.csv new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/test/data/ImportCommandTest/extraHeader.csv b/src/test/data/ImportCommandTest/extraHeader.csv new file mode 100644 index 00000000000..bb0940480d4 --- /dev/null +++ b/src/test/data/ImportCommandTest/extraHeader.csv @@ -0,0 +1,8 @@ +name,phone,email,address,matric,extra,reflection,studio,extra2,tags +"Alice Pauline",94351253,alice@example.com,"123, Jurong West Ave 6, #08-111",A1111111A,extra,R1,S1,extra2,friends +"Benson Meier",98765432,johnd@example.com,"311, Clementi Ave 2, #02-25",A2222222A,extra,R2,S2,extra2,friends +"Carl Kurz",95352563,heinz@example.com,"wall street",A3333333A,extra,R3,S3,extra2, +"Daniel Meier",87652533,cornelia@example.com,"10th street",A4444444A,extra,R4,S4,extra2,friends +"Elle Meyer",9482224,werner@example.com,"michegan ave",A5555555A,extra,R5,S5,extra2, +"Fiona Kunz",9482427,lydia@example.com,"little tokyo",A6666666A,extra,R6,S6,extra2, +"George Best",9482442,anna@example.com,"4th street",A7777777A,extra,R7,S7,extra2, diff --git a/src/test/data/ImportCommandTest/inconsistentRowLength.csv b/src/test/data/ImportCommandTest/inconsistentRowLength.csv new file mode 100644 index 00000000000..4fc4e3c8167 --- /dev/null +++ b/src/test/data/ImportCommandTest/inconsistentRowLength.csv @@ -0,0 +1,8 @@ +name,phone,email,address,matric,reflection,studio +"Alice Pauline",94351253,alice@example.com,"123, Jurong West Ave 6, #08-111",A1111111A,R1,S1,friends +"Benson Meier",98765432,johnd@example.com,"311, Clementi Ave 2, #02-25",A2222222A,R2,S2,friends +"Carl Kurz",95352563,heinz@example.com,"wall street",A3333333A,R3,S3, +"Daniel Meier",87652533,cornelia@example.com,"10th street",A4444444A,R4,S4 +"Elle Meyer",9482224,werner@example.com,"michegan ave",A5555555A,R5,S5, +"Fiona Kunz",9482427,lydia@example.com,"little tokyo",A6666666A,R6,S6, +"George Best",9482442,anna@example.com,"4th street",A7777777A,R7,S7, diff --git a/src/test/data/ImportCommandTest/missingCompulsoryValue.csv b/src/test/data/ImportCommandTest/missingCompulsoryValue.csv new file mode 100644 index 00000000000..18df52f8138 --- /dev/null +++ b/src/test/data/ImportCommandTest/missingCompulsoryValue.csv @@ -0,0 +1,8 @@ +name,phone,email,address,matric,reflection,studio,tags +"Alice Pauline",94351253,alice@example.com,"123, Jurong West Ave 6, #08-111",A1111111A,R1,S1,friends +"Benson Meier",98765432,johnd@example.com,"311, Clementi Ave 2, #02-25",A2222222A,R2,S2,friends +,95352563,heinz@example.com,"wall street",A3333333A,R3,S3, +"Daniel Meier",87652533,cornelia@example.com,"10th street",A4444444A,R4,S4,friends +"Elle Meyer",9482224,werner@example.com,"michegan ave",A5555555A,R5,S5, +"Fiona Kunz",9482427,lydia@example.com,"little tokyo",A6666666A,R6,S6, +,9482442,anna@example.com,"4th street",A7777777A,R7,S7, diff --git a/src/test/data/ImportCommandTest/missingOptionalHeader.csv b/src/test/data/ImportCommandTest/missingOptionalHeader.csv new file mode 100644 index 00000000000..08c980d29e8 --- /dev/null +++ b/src/test/data/ImportCommandTest/missingOptionalHeader.csv @@ -0,0 +1,8 @@ +name,phone,email,address,matric,reflection,studio +"Alice Pauline",94351253,alice@example.com,"123, Jurong West Ave 6, #08-111",A1111111A,R1,S1 +"Benson Meier",98765432,johnd@example.com,"311, Clementi Ave 2, #02-25",A2222222A,R2,S2 +"Carl Kurz",95352563,heinz@example.com,"wall street",A3333333A,R3,S3 +"Daniel Meier",87652533,cornelia@example.com,"10th street",A4444444A,R4,S4 +"Elle Meyer",9482224,werner@example.com,"michegan ave",A5555555A,R5,S5 +"Fiona Kunz",9482427,lydia@example.com,"little tokyo",A6666666A,R6,S6 +"George Best",9482442,anna@example.com,"4th street",A7777777A,R7,S7 diff --git a/src/test/data/ImportCommandTest/missingOptionalValue.csv b/src/test/data/ImportCommandTest/missingOptionalValue.csv new file mode 100644 index 00000000000..68207e8385c --- /dev/null +++ b/src/test/data/ImportCommandTest/missingOptionalValue.csv @@ -0,0 +1,8 @@ +name,phone,email,address,matric,reflection,studio,tags +"Alice Pauline",94351253,alice@example.com,"123, Jurong West Ave 6, #08-111",A1111111A,,S1,friends +"Benson Meier",98765432,johnd@example.com,"311, Clementi Ave 2, #02-25",A2222222A,R2,S2,friends +"Carl Kurz",95352563,heinz@example.com,"wall street",,R3,S3, +"Daniel Meier",87652533,cornelia@example.com,"10th street",A4444444A,R4,S4,friends +"Elle Meyer",9482224,werner@example.com,"michegan ave",A5555555A,,S5, +"Fiona Kunz",9482427,lydia@example.com,"little tokyo",A6666666A,R6,S6, +"George Best",9482442,anna@example.com,"4th street",A7777777A,R7,, diff --git a/src/test/data/ImportCommandTest/multipleCompulsoryParameterHeadersMissing.csv b/src/test/data/ImportCommandTest/multipleCompulsoryParameterHeadersMissing.csv new file mode 100644 index 00000000000..9bb3ac32c16 --- /dev/null +++ b/src/test/data/ImportCommandTest/multipleCompulsoryParameterHeadersMissing.csv @@ -0,0 +1,8 @@ +email,address,matric,reflection,studio,tags +"Alice Pauline",94351253,alice@example.com,"123, Jurong West Ave 6, #08-111",A1111111A,R1,S1,friends +"Benson Meier",98765432,johnd@example.com,"311, Clementi Ave 2, #02-25",A2222222A,R2,S2,friends +"Carl Kurz",95352563,heinz@example.com,"wall street",A3333333A,R3,S3, +"Daniel Meier",87652533,cornelia@example.com,"10th street",A4444444A,R4,S4,friends +"Elle Meyer",9482224,werner@example.com,"michegan ave",A5555555A,R5,S5, +"Fiona Kunz",9482427,lydia@example.com,"little tokyo",A6666666A,R6,S6, +"George Best",9482442,anna@example.com,"4th street",A7777777A,R7,S7, diff --git a/src/test/data/ImportCommandTest/notCsv b/src/test/data/ImportCommandTest/notCsv new file mode 100644 index 00000000000..53a52b6d4fc --- /dev/null +++ b/src/test/data/ImportCommandTest/notCsv @@ -0,0 +1,9 @@ +name,phone,email,address,matric,reflection,studio,tags +"Alice Pauline",94351253,alice@example.com,"123, Jurong West Ave 6, #08-111",A1111111A,R1,S1,friends +"Benson Meier",98765432,johnd@example.com,"311, Clementi Ave 2, #02-25",A2222222A,R2,S2,friends +"Carl Kurz",95352563,heinz@example.com,"wall street",A3333333A,R3,S3, +"Daniel Meier",87652533,cornelia@example.com,"10th street",A4444444A,R4,S4,friends +"Elle Meyer",9482224,werner@example.com,"michegan ave",A5555555A,R5,S5, +"Fiona Kunz",9482427,lydia@example.com,"little tokyo",A6666666A,R6,S6, +"George Best",9482442,anna@example.com,"4th street",A7777777A,R7,S7, +NOT A CSV FILE diff --git a/src/test/data/ImportCommandTest/valid.csv b/src/test/data/ImportCommandTest/valid.csv new file mode 100644 index 00000000000..b831278f402 --- /dev/null +++ b/src/test/data/ImportCommandTest/valid.csv @@ -0,0 +1,8 @@ +name,phone,email,address,matric,reflection,studio,tags +"Alice Pauline",94351253,alice@example.com,"123, Jurong West Ave 6, #08-111",A1111111A,R1,S1,friends +"Benson Meier",98765432,johnd@example.com,"311, Clementi Ave 2, #02-25",A2222222A,R2,S2,friends +"Carl Kurz",95352563,heinz@example.com,"wall street",A3333333A,R3,S3, +"Daniel Meier",87652533,cornelia@example.com,"10th street",A4444444A,R4,S4,friends +"Elle Meyer",9482224,werner@example.com,"michegan ave",A5555555A,R5,S5, +"Fiona Kunz",9482427,lydia@example.com,"little tokyo",A6666666A,R6,S6, +"George Best",9482442,anna@example.com,"4th street",A7777777A,R7,S7, diff --git a/src/test/data/ImportExamCommandTest/duplicate_exams.csv b/src/test/data/ImportExamCommandTest/duplicate_exams.csv new file mode 100644 index 00000000000..7551b4ce928 --- /dev/null +++ b/src/test/data/ImportExamCommandTest/duplicate_exams.csv @@ -0,0 +1,2 @@ +email,Exam:Midterm,Exam:Midterm +alice@example.com,89,90 diff --git a/src/test/data/ImportExamCommandTest/extra.csv b/src/test/data/ImportExamCommandTest/extra.csv new file mode 100644 index 00000000000..7219c8b18bb --- /dev/null +++ b/src/test/data/ImportExamCommandTest/extra.csv @@ -0,0 +1,6 @@ +email,Exam:Midterm,Exam:NonExistent,NotExam +alice@example.com,41,100,eee +johnd@example.com,1200,2,21 +non@example.com,0,0,23 + + diff --git a/src/test/data/ImportExamCommandTest/not_csv.json b/src/test/data/ImportExamCommandTest/not_csv.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/test/data/ImportExamCommandTest/not_number.csv b/src/test/data/ImportExamCommandTest/not_number.csv new file mode 100644 index 00000000000..38e6abc120d --- /dev/null +++ b/src/test/data/ImportExamCommandTest/not_number.csv @@ -0,0 +1,2 @@ +email,Exam:Midterm +alice@example.com,aa diff --git a/src/test/data/ImportExamCommandTest/valid.csv b/src/test/data/ImportExamCommandTest/valid.csv new file mode 100644 index 00000000000..a321cec2635 --- /dev/null +++ b/src/test/data/ImportExamCommandTest/valid.csv @@ -0,0 +1,2 @@ +email,Exam:Midterm +alice@example.com,89 diff --git a/src/test/data/ImportExamCommandTest/valid_empty_csv.csv b/src/test/data/ImportExamCommandTest/valid_empty_csv.csv new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/test/data/ImportExamCommandTest/wrong_headers.csv b/src/test/data/ImportExamCommandTest/wrong_headers.csv new file mode 100644 index 00000000000..205382c9fae --- /dev/null +++ b/src/test/data/ImportExamCommandTest/wrong_headers.csv @@ -0,0 +1,2 @@ +email +alice@example.com,100 diff --git a/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json b/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json index 6a4d2b7181c..5faa564dcb1 100644 --- a/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json +++ b/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json @@ -3,11 +3,17 @@ "name": "Valid Person", "phone": "9482424", "email": "hans@example.com", + "matric": "A1234123X", + "reflection": "R1", + "studio": "S1", "address": "4th street" }, { "name": "Person With Invalid Phone Field", "phone": "948asdf2424", "email": "hans@example.com", + "matric": "A1234123X", + "reflection": "R1", + "studio": "S1", "address": "4th street" } ] } diff --git a/src/test/data/JsonAddressBookStorageTest/invalidJsonStructureAddressBook.json b/src/test/data/JsonAddressBookStorageTest/invalidJsonStructureAddressBook.json new file mode 100644 index 00000000000..89366090151 --- /dev/null +++ b/src/test/data/JsonAddressBookStorageTest/invalidJsonStructureAddressBook.json @@ -0,0 +1,15 @@ +{ + "persons": [ { + "name": "Valid Person", + "phone": "9482424", + "email": "hans@example.com", + "matric": "A1234123X", + "address": "4th street" + }{ + "name": "Person With Invalid Phone Field", + "phone": "948asdf2424", + "email": "hans@example.com", + "matric": "A1234123X", + "address": "4th street" + } ] +} diff --git a/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json index a7427fe7aa2..825a9d1fa76 100644 --- a/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json @@ -4,11 +4,18 @@ "phone": "94351253", "email": "alice@example.com", "address": "123, Jurong West Ave 6, #08-111", + "matric" : "A1234567A", + "reflection": "R1", + "studio" : "S1", "tags": [ "friends" ] }, { - "name": "Alice Pauline", + "name": "Alice Doe", "phone": "94351253", - "email": "pauline@example.com", - "address": "4th street" - } ] + "address": "4th street", + "matric" : "A1234567A", + "reflection": "R1", + "studio" : "S1", + "email": "alice@example.com" + } ], + "exams": [] } diff --git a/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json index ad3f135ae42..9b7ae198449 100644 --- a/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json @@ -4,5 +4,6 @@ "phone": "9482424", "email": "invalid@email!3e", "address": "4th street" - } ] + } ], + "exams": [] } diff --git a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json index 72262099d35..f85dc68b88e 100644 --- a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json @@ -1,46 +1,103 @@ { - "_comment": "AddressBook save file which contains the same Person values as in TypicalPersons#getTypicalAddressBook()", "persons" : [ { "name" : "Alice Pauline", "phone" : "94351253", "email" : "alice@example.com", "address" : "123, Jurong West Ave 6, #08-111", - "tags" : [ "friends" ] + "tags" : [ "friends" ], + "matric" : "A1111111A", + "reflection" : "R1", + "studio" : "S1", + "examScores" : [ ] }, { "name" : "Benson Meier", "phone" : "98765432", "email" : "johnd@example.com", "address" : "311, Clementi Ave 2, #02-25", - "tags" : [ "owesMoney", "friends" ] + "tags" : [ "owesMoney", "friends" ], + "matric" : "A2222222A", + "reflection" : "R2", + "studio" : "S2", + "examScores" : [ ] }, { "name" : "Carl Kurz", "phone" : "95352563", "email" : "heinz@example.com", "address" : "wall street", - "tags" : [ ] + "tags" : [ ], + "matric" : "A3333333A", + "reflection" : "R3", + "studio" : "S3", + "examScores" : [ { + "examName" : "Midterm", + "examMaxScore" : 100, + "score" : 30 + } ] }, { "name" : "Daniel Meier", "phone" : "87652533", "email" : "cornelia@example.com", "address" : "10th street", - "tags" : [ "friends" ] + "tags" : [ "friends" ], + "matric" : "A4444444A", + "reflection" : "R4", + "studio" : "S4", + "examScores" : [ { + "examName" : "Midterm", + "examMaxScore" : 100, + "score" : 40 + } ] }, { "name" : "Elle Meyer", "phone" : "9482224", "email" : "werner@example.com", "address" : "michegan ave", - "tags" : [ ] + "tags" : [ ], + "matric" : "A5555555A", + "reflection" : "R5", + "studio" : "S5", + "examScores" : [ { + "examName" : "Midterm", + "examMaxScore" : 100, + "score" : 50 + } ] }, { "name" : "Fiona Kunz", "phone" : "9482427", "email" : "lydia@example.com", "address" : "little tokyo", - "tags" : [ ] + "tags" : [ ], + "matric" : "A6666666A", + "reflection" : "R6", + "studio" : "S6", + "examScores" : [ { + "examName" : "Midterm", + "examMaxScore" : 100, + "score" : 60 + } ] }, { "name" : "George Best", "phone" : "9482442", "email" : "anna@example.com", "address" : "4th street", - "tags" : [ ] + "tags" : [ ], + "matric" : "A7777777A", + "reflection" : "R7", + "studio" : "S7", + "examScores" : [ { + "examName" : "Midterm", + "examMaxScore" : 100, + "score" : 70 + } ] + } ], + "exams" : [ { + "name" : "Midterm", + "maxScore" : "100" + }, { + "name" : "Final", + "maxScore" : "100" + }, { + "name" : "Quiz", + "maxScore" : "100" } ] } diff --git a/src/test/data/JsonUserPrefsStorageTest/ExtraValuesUserPref.json b/src/test/data/JsonUserPrefsStorageTest/ExtraValuesUserPref.json index 1037548a9cd..82e26d4533a 100644 --- a/src/test/data/JsonUserPrefsStorageTest/ExtraValuesUserPref.json +++ b/src/test/data/JsonUserPrefsStorageTest/ExtraValuesUserPref.json @@ -9,5 +9,5 @@ "z" : 99 } }, - "addressBookFilePath" : "addressbook.json" + "addressBookFilePath" : "avengersassemble.json" } diff --git a/src/test/data/JsonUserPrefsStorageTest/TypicalUserPref.json b/src/test/data/JsonUserPrefsStorageTest/TypicalUserPref.json index b819bed900a..a960232ecc1 100644 --- a/src/test/data/JsonUserPrefsStorageTest/TypicalUserPref.json +++ b/src/test/data/JsonUserPrefsStorageTest/TypicalUserPref.json @@ -7,5 +7,5 @@ "y" : 100 } }, - "addressBookFilePath" : "addressbook.json" + "addressBookFilePath" : "avengersassemble.json" } diff --git a/src/test/java/seedu/address/commons/util/AppUtilTest.java b/src/test/java/seedu/address/commons/util/AppUtilTest.java index 594de1e6365..bb2d25f35f2 100644 --- a/src/test/java/seedu/address/commons/util/AppUtilTest.java +++ b/src/test/java/seedu/address/commons/util/AppUtilTest.java @@ -9,7 +9,7 @@ public class AppUtilTest { @Test public void getImage_exitingImage() { - assertNotNull(AppUtil.getImage("/images/address_book_32.png")); + assertNotNull(AppUtil.getImage("/images/AaLogo.png")); } @Test diff --git a/src/test/java/seedu/address/commons/util/CsvUtilTest.java b/src/test/java/seedu/address/commons/util/CsvUtilTest.java new file mode 100644 index 00000000000..a2a4bb67fd7 --- /dev/null +++ b/src/test/java/seedu/address/commons/util/CsvUtilTest.java @@ -0,0 +1,156 @@ +package seedu.address.commons.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.testutil.Assert.assertThrows; + +import java.io.IOException; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import javafx.util.Pair; +import seedu.address.commons.exceptions.DataLoadingException; + +public class CsvUtilTest { + private final HashSet compulsoryParameters = + new HashSet<>(List.of(new String[]{"name", "phone", "email", "address"})); + + private final HashSet optionalParameters = new HashSet<>( + List.of(new String[]{"matric", "reflection", "studio", "tags"})); + @Test + public void readCsvFile_success() throws IOException { + Pair>>, String> result = CsvUtil.readCsvFile( + Paths.get("src/test/data/ImportCommandTest/valid.csv"), + compulsoryParameters, + optionalParameters); + String expected = ""; + assertEquals(expected, result.getValue()); + } + + @Test + public void readCsvFile_multipleMissingCompulsoryParameter_failure() throws IOException { + String expected = "Missing compulsory header(s) in Csv file: phone, name,"; + assertEquals(expected, + CsvUtil.readCsvFile( + Paths.get("src/test/data/ImportCommandTest/multipleCompulsoryParameterHeadersMissing.csv"), + compulsoryParameters, + optionalParameters).getValue()); + } + + @Test + public void readCsvFile_inconsistentLengthInRow_failure() throws IOException { + String expected = + "Row 0 does not have the same number of values as the number of headers.Given: 8, Expected: 7\n" + + "Row 1 does not have the same number of values as the number of headers.Given: 8, Expected: 7\n" + + "Row 2 does not have the same number of values as the number of headers.Given: 8, Expected: 7\n" + + "Row 4 does not have the same number of values as the number of headers.Given: 8, Expected: 7\n" + + "Row 5 does not have the same number of values as the number of headers.Given: 8, Expected: 7\n" + + "Row 6 does not have the same number of values as the number of headers.Given: 8, Expected: 7\n"; + assertEquals(expected, + CsvUtil.readCsvFile( + Paths.get("src/test/data/ImportCommandTest/inconsistentRowLength.csv"), + compulsoryParameters, + optionalParameters).getValue()); + } + + @Test + public void readCsvFile_missingOptionalHeader_success() throws IOException { + Pair>>, String> result = CsvUtil.readCsvFile( + Paths.get("src/test/data/ImportCommandTest/missingOptionalHeader.csv"), + compulsoryParameters, + optionalParameters); + String expected = ""; + assertEquals(expected, result.getValue()); + } + + @Test + public void readCsvFile_emptyCsv_success() throws IOException { + Optional>> expected = Optional.empty(); + assertEquals(expected, + CsvUtil.readCsvFile( + Paths.get("src/test/data/ImportCommandTest/empty.csv"), + compulsoryParameters, + optionalParameters).getKey()); + } + + @Test + public void readCsvFile_extraHeader_success() throws IOException { + String expected = ""; + assertEquals(expected, + CsvUtil.readCsvFile( + Paths.get("src/test/data/ImportCommandTest/extraHeader.csv"), + compulsoryParameters, + optionalParameters).getValue()); + } + + @Test + public void readCsvFile_duplicateHeader_success() throws IOException { + String expected = ""; + assertEquals(expected, + CsvUtil.readCsvFile( + Paths.get("src/test/data/ImportCommandTest/duplicateHeaders.csv"), + compulsoryParameters, + optionalParameters).getValue()); + } + + @Test + public void checkCompulsoryParameters_missingCompulsoryParameter_failure() { + assertThrows(DataLoadingException.class, () -> + CsvUtil.checkCompulsoryParameters( + compulsoryParameters, + List.of(new String[]{"name", "phone", "email"}))); + } + + @Test + public void checkCompulsoryParameters_multipleMissingCompulsoryParameters_failure() { + assertThrows(DataLoadingException.class, () -> + CsvUtil.checkCompulsoryParameters( + compulsoryParameters, + List.of(new String[]{"name"}))); + } + + @Test + public void checkCompulsoryParameters_success() throws DataLoadingException { + CsvUtil.checkCompulsoryParameters( + compulsoryParameters, + List.of(new String[]{"name", "phone", "email", "address"})); + } + + @Test + public void checkCompulsoryParameters_optionalParameter_success() throws DataLoadingException { + CsvUtil.checkCompulsoryParameters( + compulsoryParameters, + List.of(new String[]{"name", "phone", "email", "address", "matric"})); + } + + @Test + public void columnsToSkip_success() throws DataLoadingException { + CsvUtil.columnsToSkip( + List.of(new String[]{"name", "phone", "email", "address"}), + compulsoryParameters, + optionalParameters); + } + + @Test + public void columnsToSkip_optionalParameter_success() throws DataLoadingException { + CsvUtil.columnsToSkip( + List.of(new String[]{"name", "phone", "email", "address", "matric"}), + compulsoryParameters, + optionalParameters); + } + + @Test + public void columnsToSkip_missingCompulsoryParameter_failure() { + assertThrows(DataLoadingException.class, () -> + CsvUtil.columnsToSkip( + List.of(new String[]{"name", "phone", "email"}), + compulsoryParameters, + optionalParameters)); + } + + +} diff --git a/src/test/java/seedu/address/commons/util/StringUtilTest.java b/src/test/java/seedu/address/commons/util/StringUtilTest.java index c56d407bf3f..d34116948db 100644 --- a/src/test/java/seedu/address/commons/util/StringUtilTest.java +++ b/src/test/java/seedu/address/commons/util/StringUtilTest.java @@ -123,6 +123,106 @@ public void containsWordIgnoreCase_validInputs_correctResult() { assertTrue(StringUtil.containsWordIgnoreCase("AAA bBb ccc bbb", "bbB")); } + //---------------- Tests for containsSubstringIgnoreCase -------------------------------------- + + @Test + public void containsSubstringIgnoreCase_nullWord_throwsNullPointerException() { + assertThrows(NullPointerException.class, () + -> StringUtil.containsSubstringIgnoreCase("typical sentence", null)); + } + + @Test + public void containsSubstringIgnoreCase_emptyWord_throwsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, + "Substring parameter cannot be empty", () + -> StringUtil.containsSubstringIgnoreCase("typical sentence", " ")); + } + + @Test + public void containsSubstringIgnoreCase_nullSentence_throwsNullPointerException() { + assertThrows(NullPointerException.class, () + -> StringUtil.containsSubstringIgnoreCase(null, "abc")); + } + + /* + * Scenarios returning true: + * query substring matches part of a sentence + * query substring matches part of a sentence, different upper/lower case letters + * query substring matches the whole sentence + * query substring matches the whole sentence, different upper/lower case letters + * + * The test method below tries to verify all above with a reasonably low number of test cases. + */ + + @Test + public void containsSubstringIgnoreCase_validInputs_correctResult() { + + // Empty sentence + assertFalse(StringUtil.containsSubstringIgnoreCase("", "abc")); // Boundary case + assertFalse(StringUtil.containsSubstringIgnoreCase(" ", "123")); + + // Matches a partial Substring only + // Sentence Substring bigger than query Substring + assertTrue(StringUtil.containsSubstringIgnoreCase("aaa bbb ccc", "bb")); + // Query Substring bigger + assertFalse(StringUtil.containsSubstringIgnoreCase("aaa bbb ccc", "bbbb")); + // Substring in the middle of the sentence + assertTrue(StringUtil.containsSubstringIgnoreCase("aaa bBb ccc", "Bb")); + // Last Substring (boundary case) + assertTrue(StringUtil.containsSubstringIgnoreCase("aaa bBb ccc@1", "Cc@1")); + + // Matches Substring in the sentence, different upper/lower case letters + assertTrue(StringUtil.containsSubstringIgnoreCase(" AAA bBb ccc ", "aaa")); // Sentence has extra spaces + // Only one Substring in sentence (boundary case) + assertTrue(StringUtil.containsSubstringIgnoreCase("Aaa", "aaa")); + assertTrue(StringUtil.containsSubstringIgnoreCase("aaa bbb ccc", " ccc ")); // Leading/trailing spaces + + // Matches multiple Substrings in sentence + assertTrue(StringUtil.containsSubstringIgnoreCase("AAA bBb ccc bbb", "bbB")); + } + + //---------------- Tests for equalsIgnoreCase -------------------------------------- + + @Test + public void equalsIgnoreCase_nullSentence_throwsNullPointerException() { + assertThrows(NullPointerException.class, () + -> StringUtil.equalsIgnoreCase(null, "abc")); + } + + @Test + public void equalsIgnoreCase_nullWord_throwsNullPointerException() { + assertThrows(NullPointerException.class, () + -> StringUtil.equalsIgnoreCase("typical sentence", null)); + } + + @Test + public void equalsIgnoreCase_emptyWord_throwsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, + "Word parameter cannot be empty", () + -> StringUtil.equalsIgnoreCase("typical sentence", " ")); + } + + /* + * Scenarios returning true: + * query word matches the whole sentence + * query word matches the whole sentence, different upper/lower case letters + * + * The test method below tries to verify all above with a reasonably low number of test cases. + */ + @Test + public void equalsIgnoreCase_validInputs_correctResult() { + + // Empty sentence + assertFalse(StringUtil.equalsIgnoreCase("", "abc")); // Boundary case + assertFalse(StringUtil.equalsIgnoreCase(" ", "123")); + + // Matches the whole word in the sentence + assertTrue(StringUtil.equalsIgnoreCase("aaa bbb ccc", "aaa bbb ccc")); // First word (boundary case) + assertTrue(StringUtil.equalsIgnoreCase("aaa bbb ccc@1", "aaa bbb ccc@1")); // Last word (boundary case) + assertTrue(StringUtil.equalsIgnoreCase("Aa", "aa")); // Only one word in sentence (boundary case) + assertTrue(StringUtil.equalsIgnoreCase("aaa bbb ccc", " aaa bbb ccc ")); // Leading/trailing spaces + } + //---------------- Tests for getDetails -------------------------------------- /* diff --git a/src/test/java/seedu/address/logic/LogicManagerTest.java b/src/test/java/seedu/address/logic/LogicManagerTest.java index baf8ce336a2..db7f569b9cb 100644 --- a/src/test/java/seedu/address/logic/LogicManagerTest.java +++ b/src/test/java/seedu/address/logic/LogicManagerTest.java @@ -5,8 +5,12 @@ import static seedu.address.logic.Messages.MESSAGE_UNKNOWN_COMMAND; import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.MATRIC_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.REFLECTION_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.STUDIO_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_STUDENT; import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalPersons.AMY; @@ -46,8 +50,10 @@ public class LogicManagerTest { @BeforeEach public void setUp() { JsonAddressBookStorage addressBookStorage = - new JsonAddressBookStorage(temporaryFolder.resolve("addressBook.json")); - JsonUserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(temporaryFolder.resolve("userPrefs.json")); + new JsonAddressBookStorage(temporaryFolder.resolve("avengersassemble.json")); + JsonUserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage( + temporaryFolder.resolve("userPrefs.json")); + StorageManager storage = new StorageManager(addressBookStorage, userPrefsStorage); logic = new LogicManager(model, storage); } @@ -87,6 +93,11 @@ public void getFilteredPersonList_modifyList_throwsUnsupportedOperationException assertThrows(UnsupportedOperationException.class, () -> logic.getFilteredPersonList().remove(0)); } + @Test + public void getExamList_modifyList_throwsUnsupportedOperationException() { + assertThrows(UnsupportedOperationException.class, () -> logic.getExamList().remove(0)); + } + /** * Executes the command and confirms that * - no exceptions are thrown
    @@ -166,8 +177,8 @@ public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) // Triggers the saveAddressBook method by executing an add command String addCommand = AddCommand.COMMAND_WORD + NAME_DESC_AMY + PHONE_DESC_AMY - + EMAIL_DESC_AMY + ADDRESS_DESC_AMY; - Person expectedPerson = new PersonBuilder(AMY).withTags().build(); + + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + MATRIC_DESC_AMY + REFLECTION_DESC_AMY + STUDIO_DESC_AMY; + Person expectedPerson = new PersonBuilder(AMY).withTags(VALID_TAG_STUDENT).build(); ModelManager expectedModel = new ModelManager(); expectedModel.addPerson(expectedPerson); assertCommandFailure(addCommand, CommandException.class, expectedMessage, expectedModel); diff --git a/src/test/java/seedu/address/logic/commands/AddCommandIntegrationTest.java b/src/test/java/seedu/address/logic/commands/AddCommandIntegrationTest.java index 162a0c86031..6f4d5b2e620 100644 --- a/src/test/java/seedu/address/logic/commands/AddCommandIntegrationTest.java +++ b/src/test/java/seedu/address/logic/commands/AddCommandIntegrationTest.java @@ -42,7 +42,7 @@ public void execute_newPerson_success() { public void execute_duplicatePerson_throwsCommandException() { Person personInList = model.getAddressBook().getPersonList().get(0); assertCommandFailure(new AddCommand(personInList), model, - AddCommand.MESSAGE_DUPLICATE_PERSON); + AddCommand.MESSAGE_DUPLICATE_EMAIL); } } diff --git a/src/test/java/seedu/address/logic/commands/AddCommandTest.java b/src/test/java/seedu/address/logic/commands/AddCommandTest.java index 90e8253f48e..091c8aae75d 100644 --- a/src/test/java/seedu/address/logic/commands/AddCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/AddCommandTest.java @@ -7,21 +7,16 @@ import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalPersons.ALICE; -import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; -import java.util.function.Predicate; import org.junit.jupiter.api.Test; -import javafx.collections.ObservableList; -import seedu.address.commons.core.GuiSettings; import seedu.address.logic.Messages; import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.util.ModelStub; import seedu.address.model.AddressBook; -import seedu.address.model.Model; import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.ReadOnlyUserPrefs; import seedu.address.model.person.Person; import seedu.address.testutil.PersonBuilder; @@ -50,7 +45,7 @@ public void execute_duplicatePerson_throwsCommandException() { AddCommand addCommand = new AddCommand(validPerson); ModelStub modelStub = new ModelStubWithPerson(validPerson); - assertThrows(CommandException.class, AddCommand.MESSAGE_DUPLICATE_PERSON, () -> addCommand.execute(modelStub)); + assertThrows(CommandException.class, AddCommand.MESSAGE_DUPLICATE_EMAIL, () -> addCommand.execute(modelStub)); } @Test @@ -84,81 +79,6 @@ public void toStringMethod() { assertEquals(expected, addCommand.toString()); } - /** - * A default model stub that have all of the methods failing. - */ - private class ModelStub implements Model { - @Override - public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { - throw new AssertionError("This method should not be called."); - } - - @Override - public ReadOnlyUserPrefs getUserPrefs() { - throw new AssertionError("This method should not be called."); - } - - @Override - public GuiSettings getGuiSettings() { - throw new AssertionError("This method should not be called."); - } - - @Override - public void setGuiSettings(GuiSettings guiSettings) { - throw new AssertionError("This method should not be called."); - } - - @Override - public Path getAddressBookFilePath() { - throw new AssertionError("This method should not be called."); - } - - @Override - public void setAddressBookFilePath(Path addressBookFilePath) { - throw new AssertionError("This method should not be called."); - } - - @Override - public void addPerson(Person person) { - throw new AssertionError("This method should not be called."); - } - - @Override - public void setAddressBook(ReadOnlyAddressBook newData) { - throw new AssertionError("This method should not be called."); - } - - @Override - public ReadOnlyAddressBook getAddressBook() { - throw new AssertionError("This method should not be called."); - } - - @Override - public boolean hasPerson(Person person) { - throw new AssertionError("This method should not be called."); - } - - @Override - public void deletePerson(Person target) { - throw new AssertionError("This method should not be called."); - } - - @Override - public void setPerson(Person target, Person editedPerson) { - throw new AssertionError("This method should not be called."); - } - - @Override - public ObservableList getFilteredPersonList() { - throw new AssertionError("This method should not be called."); - } - - @Override - public void updateFilteredPersonList(Predicate predicate) { - throw new AssertionError("This method should not be called."); - } - } - /** * A Model stub that contains a single person. */ diff --git a/src/test/java/seedu/address/logic/commands/AddExamCommandTest.java b/src/test/java/seedu/address/logic/commands/AddExamCommandTest.java new file mode 100644 index 00000000000..70f4132b66e --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/AddExamCommandTest.java @@ -0,0 +1,112 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.testutil.Assert.assertThrows; + +import java.util.ArrayList; +import java.util.Arrays; + +import org.junit.Test; + +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.util.ModelStub; +import seedu.address.model.AddressBook; +import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.exam.Exam; +import seedu.address.model.person.Score; + +public class AddExamCommandTest { + + @Test + public void constructor_nullExam_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new AddExamCommand(null)); + } + + @Test + public void execute_examAcceptedByModel_addSuccessful() throws Exception { + ModelStubAcceptingExamAdded modelStub = new ModelStubAcceptingExamAdded(); + Exam validExam = new Exam("Math Final", new Score(100)); + CommandResult commandResult = new AddExamCommand(validExam).execute(modelStub); + + assertEquals(String.format(AddExamCommand.MESSAGE_SUCCESS, Messages.format(validExam)), + commandResult.getFeedbackToUser()); + assertEquals(Arrays.asList(validExam), modelStub.examsAdded); + } + + @Test + public void execute_duplicateExam_throwsCommandException() { + Exam validExam = new Exam("Math Final", new Score(100)); + AddExamCommand addCommand = new AddExamCommand(validExam); + ModelStub modelStub = new ModelStubAcceptingExamAdded(); + modelStub.addExam(validExam); + + assertThrows(CommandException.class, + AddExamCommand.MESSAGE_DUPLICATE_EXAM, () -> addCommand.execute(modelStub)); + } + + @Test + public void equals() { + Exam mathFinal = new Exam("Math Final", new Score(100)); + Exam scienceFinal = new Exam("Science Final", new Score(100)); + AddExamCommand addMathFinalCommand = new AddExamCommand(mathFinal); + AddExamCommand addScienceFinalCommand = new AddExamCommand(scienceFinal); + + // same object -> returns true + assertEquals(addMathFinalCommand, addMathFinalCommand); + + // same values -> returns true + AddExamCommand addMathFinalCommandCopy = new AddExamCommand(mathFinal); + assertEquals(addMathFinalCommand, addMathFinalCommandCopy); + + // different types -> returns false + assertEquals(addMathFinalCommand.equals(1), false); + + // null -> returns false + assertEquals(addMathFinalCommand.equals(null), false); + + // different exam -> returns false + assertEquals(addMathFinalCommand.equals(addScienceFinalCommand), false); + } + + @Test + public void execute_nullModel_throwsNullPointerException() { + Exam validExam = new Exam("Math Final", new Score(100)); + AddExamCommand addCommand = new AddExamCommand(validExam); + + assertThrows(NullPointerException.class, () -> addCommand.execute(null)); + } + + @Test + public void toStringMethod() { + Exam mathFinal = new Exam("Math Final", new Score(100)); + AddExamCommand addMathFinalCommand = new AddExamCommand(mathFinal); + + assertEquals(addMathFinalCommand.toString(), + "seedu.address.logic.commands.AddExamCommand{toAdd=Math Final: 100}"); + } + + + private class ModelStubAcceptingExamAdded extends ModelStub { + final ArrayList examsAdded = new ArrayList<>(); + + @Override + public boolean hasExam(Exam exam) { + requireNonNull(exam); + return examsAdded.contains(exam); + } + + @Override + public void addExam(Exam exam) { + requireNonNull(exam); + examsAdded.add(exam); + } + + @Override + public ReadOnlyAddressBook getAddressBook() { + return new AddressBook(); + } + } + +} diff --git a/src/test/java/seedu/address/logic/commands/AddScoreCommandTest.java b/src/test/java/seedu/address/logic/commands/AddScoreCommandTest.java new file mode 100644 index 00000000000..06b7426d526 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/AddScoreCommandTest.java @@ -0,0 +1,91 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static seedu.address.testutil.Assert.assertThrows; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.exam.Exam; +import seedu.address.model.person.Person; +import seedu.address.model.person.Score; +import seedu.address.testutil.PersonBuilder; + +public class AddScoreCommandTest { + + @Test + public void constructor_nullScore_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new AddScoreCommand(null, null)); + } + + @Test + public void execute_scoreAcceptedByModel_addSuccessful() throws CommandException { + Model model = new ModelManager(); + Person validPerson = new PersonBuilder().build(); + model.addPerson(validPerson); + Score validScore = new Score(85); + Exam validExam = new Exam("Math Final", new Score(100)); + model.addExam(validExam); + model.selectExam(validExam); + Index validIndex = Index.fromZeroBased(model.getFilteredPersonList().size() - 1); + CommandResult commandResult = new AddScoreCommand(validIndex, validScore).execute(model); + + assertEquals(String.format(AddScoreCommand.MESSAGE_ADD_SCORE_SUCCESS, validScore, validPerson.getName()), + commandResult.getFeedbackToUser()); + Map expectedScores = new HashMap<>(); + expectedScores.put(validExam, validScore); + assertEquals(expectedScores, model.getFilteredPersonList().get(validIndex.getZeroBased()).getScores()); + } + + @Test + public void execute_duplicateScore_throwsCommandException() throws CommandException { + Model model = new ModelManager(); + Person validPerson = new PersonBuilder().build(); + model.addPerson(validPerson); + Score validScore = new Score(85); + Exam validExam = new Exam("Math Final", new Score(100)); + model.addExam(validExam); + model.selectExam(validExam); + Index validIndex = Index.fromZeroBased(model.getFilteredPersonList().size() - 1); + AddScoreCommand addCommand = new AddScoreCommand(validIndex, validScore); + // add one score in first + addCommand.execute(model); + + assertThrows(CommandException.class, AddScoreCommand.MESSAGE_SCORE_EXISTS, () -> addCommand.execute(model)); + } + + @Test + public void equals() { + Score score = new Score(85); + Index index = Index.fromZeroBased(0); + AddScoreCommand addScoreFirstCommand = new AddScoreCommand(index, score); + AddScoreCommand addScoreSecondCommand = new AddScoreCommand(index, score); + + // same object -> returns true + assertEquals(addScoreFirstCommand, addScoreFirstCommand); + + // same values -> returns true + assertEquals(addScoreFirstCommand, addScoreSecondCommand); + + // different types -> returns false + assertFalse(addScoreFirstCommand.equals(new Object())); + + // null -> returns false + assertFalse(addScoreFirstCommand.equals(null)); + + // different score -> returns false + AddScoreCommand addScoreDifferentScoreCommand = new AddScoreCommand(index, new Score(100)); + assertFalse(addScoreFirstCommand.equals(addScoreDifferentScoreCommand)); + + // different index -> returns false + AddScoreCommand addScoreDifferentIndexCommand = new AddScoreCommand(Index.fromZeroBased(1), score); + assertFalse(addScoreFirstCommand.equals(addScoreDifferentIndexCommand)); + } +} diff --git a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java index 643a1d08069..96e7bbf5dbd 100644 --- a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java +++ b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java @@ -4,21 +4,23 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MATRIC_NUMBER; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_REFLECTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_STUDIO; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import static seedu.address.testutil.Assert.assertThrows; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.AddressBook; import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; import seedu.address.model.person.Person; +import seedu.address.model.person.PersonDetailPredicate; import seedu.address.testutil.EditPersonDescriptorBuilder; /** @@ -26,6 +28,10 @@ */ public class CommandTestUtil { + public static final String VALID_TAG_STUDENT = "student"; + public static final String VALID_TAG_TA = "TA"; + public static final String VALID_TAG_INSTRUCTOR = "instructor"; + public static final String VALID_DESC_STUDENT = " " + PREFIX_TAG + VALID_TAG_STUDENT; public static final String VALID_NAME_AMY = "Amy Bee"; public static final String VALID_NAME_BOB = "Bob Choo"; public static final String VALID_PHONE_AMY = "11111111"; @@ -37,6 +43,13 @@ public class CommandTestUtil { public static final String VALID_TAG_HUSBAND = "husband"; public static final String VALID_TAG_FRIEND = "friend"; + public static final String VALID_MATRIC_NUMBER_AMY = "A1234567X"; + public static final String VALID_MATRIC_NUMBER_BOB = "A1234567Z"; + public static final String VALID_REFLECTION_AMY = "R1"; + public static final String VALID_REFLECTION_BOB = "R2"; + public static final String VALID_STUDIO_AMY = "S1"; + public static final String VALID_STUDIO_BOB = "S2"; + public static final String NAME_DESC_AMY = " " + PREFIX_NAME + VALID_NAME_AMY; public static final String NAME_DESC_BOB = " " + PREFIX_NAME + VALID_NAME_BOB; public static final String PHONE_DESC_AMY = " " + PREFIX_PHONE + VALID_PHONE_AMY; @@ -47,11 +60,20 @@ public class CommandTestUtil { public static final String ADDRESS_DESC_BOB = " " + PREFIX_ADDRESS + VALID_ADDRESS_BOB; public static final String TAG_DESC_FRIEND = " " + PREFIX_TAG + VALID_TAG_FRIEND; public static final String TAG_DESC_HUSBAND = " " + PREFIX_TAG + VALID_TAG_HUSBAND; + public static final String MATRIC_DESC_AMY = " " + PREFIX_MATRIC_NUMBER + VALID_MATRIC_NUMBER_AMY; + public static final String MATRIC_DESC_BOB = " " + PREFIX_MATRIC_NUMBER + VALID_MATRIC_NUMBER_BOB; + public static final String REFLECTION_DESC_AMY = " " + PREFIX_REFLECTION + VALID_REFLECTION_AMY; + public static final String REFLECTION_DESC_BOB = " " + PREFIX_REFLECTION + VALID_REFLECTION_BOB; + public static final String STUDIO_DESC_AMY = " " + PREFIX_STUDIO + VALID_STUDIO_AMY; + public static final String STUDIO_DESC_BOB = " " + PREFIX_STUDIO + VALID_STUDIO_BOB; public static final String INVALID_NAME_DESC = " " + PREFIX_NAME + "James&"; // '&' not allowed in names public static final String INVALID_PHONE_DESC = " " + PREFIX_PHONE + "911a"; // 'a' not allowed in phones public static final String INVALID_EMAIL_DESC = " " + PREFIX_EMAIL + "bob!yahoo"; // missing '@' symbol public static final String INVALID_ADDRESS_DESC = " " + PREFIX_ADDRESS; // empty string not allowed for addresses + public static final String INVALID_MATRIC_DESC = " " + PREFIX_MATRIC_NUMBER + "A1234567"; // missing last character + public static final String INVALID_REFLECTION_DESC = " " + PREFIX_REFLECTION + "R"; // missing number + public static final String INVALID_STUDIO_DESC = " " + PREFIX_STUDIO + "S"; // missing last character public static final String INVALID_TAG_DESC = " " + PREFIX_TAG + "hubby*"; // '*' not allowed in tags public static final String PREAMBLE_WHITESPACE = "\t \r \n"; @@ -120,7 +142,7 @@ public static void showPersonAtIndex(Model model, Index targetIndex) { Person person = model.getFilteredPersonList().get(targetIndex.getZeroBased()); final String[] splitName = person.getName().fullName.split("\\s+"); - model.updateFilteredPersonList(new NameContainsKeywordsPredicate(Arrays.asList(splitName[0]))); + model.updateFilteredPersonList(new PersonDetailPredicate(PREFIX_NAME, splitName[0])); assertEquals(1, model.getFilteredPersonList().size()); } diff --git a/src/test/java/seedu/address/logic/commands/CopyCommandTest.java b/src/test/java/seedu/address/logic/commands/CopyCommandTest.java new file mode 100644 index 00000000000..459644cfa98 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/CopyCommandTest.java @@ -0,0 +1,74 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.Messages.MESSAGE_EMPTY_PERSON_LIST; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalPersons.getTypicalPersons; +import static seedu.address.testutil.TypicalPersons.getTypicalPersonsEmails; + +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.StringSelection; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.io.IOException; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.Person; + +/** + * Contains integration tests (interaction with the Model) for {@code CopyCommand}. + */ +public class CopyCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private Model emptyModel = new ModelManager(new AddressBook(), new UserPrefs()); + @Test + public void equals() { + CopyCommand copyCommand = new CopyCommand(); + + // same object -> returns true + assertTrue(copyCommand.equals(copyCommand)); + + + // different types -> returns false + assertFalse(copyCommand.equals(1)); + + // null -> returns false + assertFalse(copyCommand.equals(null)); + } + + @Test + public void execute_emptyList_noEmailsCopied() { + assertCommandFailure(new CopyCommand(), emptyModel, MESSAGE_EMPTY_PERSON_LIST); + } + + @Test + public void execute_nonEmptyList_emailsCopied() { + assertCommandSuccess(new CopyCommand(), model, CopyCommand.MESSAGE_SUCCESS, model); + } + + @Test + public void getEmailsMethod() { + List lastShownList = getTypicalPersons(); + CopyCommand copyCommand = new CopyCommand(); + StringSelection emails = copyCommand.getEmails(lastShownList); + StringSelection expectedEmails = new StringSelection(getTypicalPersonsEmails()); + + try { + String data1 = (String) emails.getTransferData(DataFlavor.stringFlavor); + String data2 = (String) expectedEmails.getTransferData(DataFlavor.stringFlavor); + assertTrue(data1.equals(data2)); + } catch (UnsupportedFlavorException e) { + throw new AssertionError("DataFlavor not supported"); + } catch (IOException e) { + throw new AssertionError("IOException"); + } + } +} diff --git a/src/test/java/seedu/address/logic/commands/DeleteExamCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteExamCommandTest.java new file mode 100644 index 00000000000..84e512e0179 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/DeleteExamCommandTest.java @@ -0,0 +1,110 @@ +package seedu.address.logic.commands; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_EXAM; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_EXAM; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.exam.Exam; +import seedu.address.model.person.Person; + +public class DeleteExamCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_validIndex_success() { + Exam examToDelete = model.getExamList().get(INDEX_FIRST_EXAM.getZeroBased()); + DeleteExamCommand deleteExamCommand = new DeleteExamCommand(INDEX_FIRST_EXAM); + + String expectedMessage = String.format(DeleteExamCommand.MESSAGE_DELETE_EXAM_SUCCESS, + Messages.format(examToDelete)); + + ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + expectedModel.deleteExam(examToDelete); + + assertCommandSuccess(deleteExamCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndex_throwsCommandException() { + Index outOfBoundIndex = Index.fromOneBased(model.getExamList().size() + 1); + DeleteExamCommand deleteExamCommand = new DeleteExamCommand(outOfBoundIndex); + + assertCommandFailure(deleteExamCommand, model, Messages.MESSAGE_INVALID_EXAM_DISPLAYED_INDEX); + } + + @Test + public void equals() { + DeleteExamCommand deleteFirstCommand = new DeleteExamCommand(INDEX_FIRST_EXAM); + DeleteExamCommand deleteSecondCommand = new DeleteExamCommand(INDEX_SECOND_EXAM); + + // same object -> returns true + assertTrue(deleteFirstCommand.equals(deleteFirstCommand)); + + // same values -> returns true + DeleteExamCommand deleteFirstCommandCopy = new DeleteExamCommand(INDEX_FIRST_EXAM); + assertTrue(deleteFirstCommand.equals(deleteFirstCommandCopy)); + + // different types -> returns false + assertFalse(deleteFirstCommand.equals(1)); + + // null -> returns false + assertFalse(deleteFirstCommand.equals(null)); + + // different exam -> returns false + assertFalse(deleteFirstCommand.equals(deleteSecondCommand)); + } + + @Test + public void execute_validIndex_removesExamFromPersons() { + Exam examToDelete = model.getExamList().get(INDEX_FIRST_EXAM.getZeroBased()); + DeleteExamCommand deleteExamCommand = new DeleteExamCommand(INDEX_FIRST_EXAM); + + boolean examExistsInPersons = model.getAddressBook().getPersonList().stream() + .anyMatch(person -> person.getScores().containsKey(examToDelete)); + assertTrue(examExistsInPersons); + + Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + expectedModel.deleteExam(examToDelete); + + // Execute the command + assertCommandSuccess(deleteExamCommand, model, + String.format(DeleteExamCommand.MESSAGE_DELETE_EXAM_SUCCESS, + Messages.format(examToDelete)), + expectedModel); + + // Verify that the exam is removed from all persons + for (Person person : expectedModel.getAddressBook().getPersonList()) { + assertFalse(person.getScores().containsKey(examToDelete)); + } + } + + @Test + public void execute_selectedExam_setsSelectedExamToNull() throws CommandException { + // Select an exam + model.selectExam(model.getExamList().get(INDEX_FIRST_EXAM.getZeroBased())); + assertEquals(model.getSelectedExam().getValue(), + model.getExamList().get(INDEX_FIRST_EXAM.getZeroBased())); + // Delete the selected exam + DeleteExamCommand deleteExamCommand = new DeleteExamCommand(INDEX_FIRST_EXAM); + deleteExamCommand.execute(model); + + // Verify that the selected exam is set to null + assertNull(model.getSelectedExam().getValue()); + } +} + diff --git a/src/test/java/seedu/address/logic/commands/DeleteScoreCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteScoreCommandTest.java new file mode 100644 index 00000000000..b479a8edbbe --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/DeleteScoreCommandTest.java @@ -0,0 +1,102 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.exam.Exam; +import seedu.address.model.person.Person; +import seedu.address.model.person.Score; +import seedu.address.testutil.PersonBuilder; + +class DeleteScoreCommandTest { + + @Test + public void constructor_nullIndex_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new DeleteScoreCommand(null)); + } + + @Test + public void execute_validIndex_success() throws CommandException { + Model model = new ModelManager(); + Exam validExam = new Exam("mock exam", new Score(100)); + Score validScore = new Score(17); + Map validExamScore = new HashMap<>(); + validExamScore.put(validExam, validScore); + Person validPerson = new PersonBuilder().withScores(validExamScore).build(); + + model.addPerson(validPerson); + model.addExam(validExam); + model.selectExam(validExam); + + Index validIndex = Index.fromZeroBased(model.getFilteredPersonList().size() - 1); + CommandResult commandResult = new DeleteScoreCommand(validIndex).execute(model); + + assertEquals(String.format(DeleteScoreCommand.MESSAGE_DELETE_PERSON_SUCCESS, + validPerson.getName(), + validPerson.getEmail(), + validExam.getName()), + commandResult.getFeedbackToUser()); + } + + @Test + public void execute_invalidIndex_throwsCommandException() { + Model model = new ModelManager(); + Exam validExam = new Exam("mock exam", new Score(100)); + Score validScore = new Score(17); + Map validExamScore = new HashMap<>(); + validExamScore.put(validExam, validScore); + Person validPerson = new PersonBuilder().withScores(validExamScore).build(); + + model.addPerson(validPerson); + model.addExam(validExam); + model.selectExam(validExam); + + Index invalidIndex = Index.fromZeroBased(model.getFilteredPersonList().size()); + + DeleteScoreCommand command = new DeleteScoreCommand(invalidIndex); + assertThrows(CommandException.class, () -> command.execute(model)); + } + + @Test + public void execute_withNoExamSelected_throwsCommandException() { + Model model = new ModelManager(); + Exam validExam = new Exam("mock exam", new Score(100)); + Score validScore = new Score(17); + Map validExamScore = new HashMap<>(); + validExamScore.put(validExam, validScore); + Person validPerson = new PersonBuilder().withScores(validExamScore).build(); + + model.addPerson(validPerson); + model.addExam(validExam); + + Index validIndex = Index.fromZeroBased(model.getFilteredPersonList().size() - 1); + + DeleteScoreCommand command = new DeleteScoreCommand(validIndex); + assertThrows(CommandException.class, () -> command.execute(model)); + } + + @Test + public void execute_noExamScoreToDelete_throwsCommandException() { + Model model = new ModelManager(); + Exam validExam = new Exam("mock exam", new Score(100)); + Person validPerson = new PersonBuilder().build(); + + model.addPerson(validPerson); + model.addExam(validExam); + model.selectExam(validExam); + + Index validIndex = Index.fromZeroBased(model.getFilteredPersonList().size() - 1); + + DeleteScoreCommand command = new DeleteScoreCommand(validIndex); + assertThrows(CommandException.class, () -> command.execute(model)); + } +} diff --git a/src/test/java/seedu/address/logic/commands/DeleteShownCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteShownCommandTest.java new file mode 100644 index 00000000000..240f51e4c94 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/DeleteShownCommandTest.java @@ -0,0 +1,57 @@ +package seedu.address.logic.commands; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.PersonDetailPredicate; + + +/** + * Contains integration tests (interaction with the Model) and unit tests for DeleteShownCommand. + */ +public class DeleteShownCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private Model emptyModel = new ModelManager(); + + @Test + public void execute_tryDeleteAll_throwsCommandException() { + DeleteShownCommand deleteShownCommand = new DeleteShownCommand(); + CommandTestUtil.assertCommandFailure(deleteShownCommand, model, DeleteShownCommand.MESSAGE_NO_FILTER); + } + + @Test + public void execute_tryDeleteNobody_throwsCommandException() { + DeleteShownCommand deleteShownCommand = new DeleteShownCommand(); + CommandTestUtil.assertCommandFailure(deleteShownCommand, emptyModel, DeleteShownCommand.MESSAGE_NO_PERSONS); + } + + @Test + public void execute_success() { + DeleteShownCommand deleteShownCommand = new DeleteShownCommand(); + + PersonDetailPredicate predicate = + new PersonDetailPredicate(PREFIX_NAME, "l"); + model.updateFilteredPersonList(predicate); + + PersonDetailPredicate predicate2 = + new PersonDetailPredicate(PREFIX_NAME, "o"); + expectedModel.updateFilteredPersonList(predicate2); + + CommandTestUtil.assertCommandSuccess(deleteShownCommand, model, + String.format(DeleteShownCommand.MESSAGE_SUCCESS, 4, 3), model); + + // For some reason the expected model and model have the same persons but asserting model == expectedModel + // fails. So we have to manually check if the persons are the same. + for (int i = 0; i < model.getFilteredPersonList().size(); i++) { + assert(model.getFilteredPersonList().get(i).equals(expectedModel.getFilteredPersonList().get(i))); + } + } + + +} diff --git a/src/test/java/seedu/address/logic/commands/DeselectExamCommandTest.java b/src/test/java/seedu/address/logic/commands/DeselectExamCommandTest.java new file mode 100644 index 00000000000..9ef24a927a5 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/DeselectExamCommandTest.java @@ -0,0 +1,48 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.exam.Exam; +import seedu.address.model.person.Score; + +public class DeselectExamCommandTest { + + private Model model; + + @BeforeEach + public void setUp() { + model = new ModelManager(new AddressBook(), new UserPrefs()); + } + + @Test + public void execute_examIsSelected_deselectsExam() throws CommandException { + Exam exam = new Exam("Midterm", new Score(100)); + model.addExam(exam); + model.selectExam(exam); + + DeselectExamCommand command = new DeselectExamCommand(); + + CommandResult result = command.execute(model); + + assertNull(model.getSelectedExam().getValue()); + assertEquals(String.format(DeselectExamCommand.MESSAGE_SUCCESS, "Midterm"), + result.getFeedbackToUser()); + } + + @Test + public void execute_noExamSelected_throwsCommandException() { + DeselectExamCommand command = new DeselectExamCommand(); + + assertThrows(CommandException.class, () -> command.execute(model)); + } +} diff --git a/src/test/java/seedu/address/logic/commands/EditCommandTest.java b/src/test/java/seedu/address/logic/commands/EditCommandTest.java index 469dd97daa7..2df7f53e992 100644 --- a/src/test/java/seedu/address/logic/commands/EditCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/EditCommandTest.java @@ -5,8 +5,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.logic.commands.CommandTestUtil.DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.DESC_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_MATRIC_NUMBER_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_REFLECTION_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_STUDIO_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; @@ -56,10 +59,14 @@ public void execute_someFieldsSpecifiedUnfilteredList_success() { PersonBuilder personInList = new PersonBuilder(lastPerson); Person editedPerson = personInList.withName(VALID_NAME_BOB).withPhone(VALID_PHONE_BOB) - .withTags(VALID_TAG_HUSBAND).build(); + .withTags(VALID_TAG_HUSBAND).withMatric(VALID_MATRIC_NUMBER_BOB) + .withReflection(VALID_REFLECTION_BOB) + .withStudio(VALID_STUDIO_BOB).build(); EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB) - .withPhone(VALID_PHONE_BOB).withTags(VALID_TAG_HUSBAND).build(); + .withPhone(VALID_PHONE_BOB).withTags(VALID_TAG_HUSBAND).withMatric(VALID_MATRIC_NUMBER_BOB) + .withReflection(VALID_REFLECTION_BOB) + .withStudio(VALID_STUDIO_BOB).build(); EditCommand editCommand = new EditCommand(indexLastPerson, descriptor); String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson)); diff --git a/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java b/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java index b17c1f3d5c2..c8fa75537a7 100644 --- a/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java +++ b/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java @@ -7,8 +7,11 @@ import static seedu.address.logic.commands.CommandTestUtil.DESC_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_MATRIC_NUMBER_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_REFLECTION_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_STUDIO_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; import org.junit.jupiter.api.Test; @@ -52,6 +55,18 @@ public void equals() { editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withAddress(VALID_ADDRESS_BOB).build(); assertFalse(DESC_AMY.equals(editedAmy)); + // different matriculation number -> returns false + editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withMatric(VALID_MATRIC_NUMBER_BOB).build(); + assertFalse(DESC_AMY.equals(editedAmy)); + + // different reflection -> returns false + editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withReflection(VALID_REFLECTION_BOB).build(); + assertFalse(DESC_AMY.equals(editedAmy)); + + // different studio -> returns false + editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withStudio(VALID_STUDIO_BOB).build(); + assertFalse(DESC_AMY.equals(editedAmy)); + // different tags -> returns false editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withTags(VALID_TAG_HUSBAND).build(); assertFalse(DESC_AMY.equals(editedAmy)); @@ -65,7 +80,11 @@ public void toStringMethod() { + editPersonDescriptor.getPhone().orElse(null) + ", email=" + editPersonDescriptor.getEmail().orElse(null) + ", address=" + editPersonDescriptor.getAddress().orElse(null) + ", tags=" - + editPersonDescriptor.getTags().orElse(null) + "}"; + + editPersonDescriptor.getMatric().orElse(null) + ", matriculation number=" + + editPersonDescriptor.getReflection().orElse(null) + ", reflection=" + + editPersonDescriptor.getStudio().orElse(null) + ", studio=" + + editPersonDescriptor.getTags().orElse(null) + ", scores=" + + editPersonDescriptor.getScores().orElse(null) + "}"; assertEquals(expected, editPersonDescriptor.toString()); } } diff --git a/src/test/java/seedu/address/logic/commands/EditScoreCommandTest.java b/src/test/java/seedu/address/logic/commands/EditScoreCommandTest.java new file mode 100644 index 00000000000..850264f7f44 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/EditScoreCommandTest.java @@ -0,0 +1,118 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.exam.Exam; +import seedu.address.model.person.Person; +import seedu.address.model.person.Score; +import seedu.address.testutil.PersonBuilder; + +class EditScoreCommandTest { + + @Test + public void constructor_nullScore_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new EditScoreCommand(null, null)); + } + + @Test + public void execute_validIndexAndScore_success() throws CommandException { + Model model = new ModelManager(); + Exam validExam = new Exam("mock exam", new Score(100)); + Score validInitialScore = new Score(17); + Score validFinalScore = new Score(13); + Map validInitialExamScore = new HashMap<>(); + validInitialExamScore.put(validExam, validInitialScore); + Person validPerson = new PersonBuilder().withScores(validInitialExamScore).build(); + model.addPerson(validPerson); + model.addExam(validExam); + model.selectExam(validExam); + Index validIndex = Index.fromZeroBased(model.getFilteredPersonList().size() - 1); + CommandResult commandResult = new EditScoreCommand(validIndex, validFinalScore).execute(model); + + String expectedMessage = String.format(EditScoreCommand.MESSAGE_EDIT_SCORE_SUCCESS, validExam.getName(), + validFinalScore, validPerson.getName(), validPerson.getEmail()); + + assertEquals(expectedMessage, commandResult.getFeedbackToUser()); + + Map expectedExamScore = new HashMap<>(); + expectedExamScore.put(validExam, validFinalScore); + assertEquals(expectedExamScore, model.getFilteredPersonList().get(validIndex.getZeroBased()).getScores()); + } + + @Test + public void execute_invalidIndex_throwsCommandException() { + Model model = new ModelManager(); + Exam validExam = new Exam("mock exam", new Score(100)); + Score validScore = new Score(17); + Map validInitialExamScore = new HashMap<>(); + validInitialExamScore.put(validExam, validScore); + Person validPerson = new PersonBuilder().withScores(validInitialExamScore).build(); + model.addPerson(validPerson); + model.addExam(validExam); + model.selectExam(validExam); + Index invalidIndex = Index.fromZeroBased(model.getFilteredPersonList().size()); + + EditScoreCommand command = new EditScoreCommand(invalidIndex, validScore); + assertThrows(CommandException.class, () -> command.execute(model)); + } + + @Test + public void execute_noExamSelected_throwsCommandException() { + Model model = new ModelManager(); + Exam validExam = new Exam("mock exam", new Score(100)); + Score validScore = new Score(17); + Map validInitialExamScore = new HashMap<>(); + validInitialExamScore.put(validExam, validScore); + Person validPerson = new PersonBuilder().withScores(validInitialExamScore).build(); + model.addPerson(validPerson); + model.addExam(validExam); + Index validIndex = Index.fromZeroBased(model.getFilteredPersonList().size() - 1); + + EditScoreCommand command = new EditScoreCommand(validIndex, validScore); + assertThrows(CommandException.class, () -> command.execute(model)); + } + + @Test + public void execute_newScoreLargerThanMaxScore_throwsCommandException() { + Model model = new ModelManager(); + Exam exam = new Exam("mock exam", new Score(100)); + Score oldScore = new Score(17); + Map initialExamScore = new HashMap<>(); + initialExamScore.put(exam, oldScore); + Person validPerson = new PersonBuilder().withScores(initialExamScore).build(); + model.addPerson(validPerson); + model.addExam(exam); + model.selectExam(exam); + Index validIndex = Index.fromZeroBased(model.getFilteredPersonList().size() - 1); + + Score invalidNewScore = new Score(150); + EditScoreCommand command = new EditScoreCommand(validIndex, invalidNewScore); + assertThrows(CommandException.class, () -> command.execute(model)); + } + + @Test + public void execute_noInitialScore_throwsCommandException() { + Model model = new ModelManager(); + Person validPerson = new PersonBuilder().build(); + Exam exam = new Exam("mock exam", new Score(100)); + Score score = new Score(17); + model.addPerson(validPerson); + model.addExam(exam); + model.selectExam(exam); + Index validIndex = Index.fromZeroBased(model.getFilteredPersonList().size() - 1); + + EditScoreCommand command = new EditScoreCommand(validIndex, score); + assertThrows(CommandException.class, () -> command.execute(model)); + } + +} diff --git a/src/test/java/seedu/address/logic/commands/ExportCommandTest.java b/src/test/java/seedu/address/logic/commands/ExportCommandTest.java new file mode 100644 index 00000000000..d3e239280a5 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/ExportCommandTest.java @@ -0,0 +1,142 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.storage.JsonAddressBookStorage; + +public class ExportCommandTest { + + private static final String TEST_CSV_FILE_PATH = + "./src/test/data/ExportCommandTest/addressbookdata/addressbooktest.csv"; + private static final Path TEST_JSON_FILE_PATH = Paths.get("src", "test", "data", "ExportCommandTest", + "filteredAddressBook.json"); + private static final Path TYPICAL_PERSONS_PATH = Paths.get("src", "test", "data", + "JsonSerializableAddressBookTest", "typicalPersonsAddressBook.json"); + + private ExportCommand exportCommand = new ExportCommand(); + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private Model emptyModel = new ModelManager(new AddressBook(), new UserPrefs()); + + @BeforeEach + public void setUp() { + exportCommand.updateCsvFilePath(TEST_CSV_FILE_PATH); + exportCommand.updateFilteredJsonFilePath(TEST_JSON_FILE_PATH); + model.setAddressBookFilePath(TYPICAL_PERSONS_PATH); + } + + @Test + public void updateCsvFilePathTest_success() { + exportCommand.updateCsvFilePath(TEST_CSV_FILE_PATH); + assertEquals(TEST_CSV_FILE_PATH, exportCommand.getCsvFilePath()); + } + + @Test + public void updateFilteredJsonFilePath_success() { + exportCommand.updateFilteredJsonFilePath(TEST_JSON_FILE_PATH); + assertEquals(TEST_JSON_FILE_PATH, exportCommand.getFilteredJsonFilePath()); + } + + @Test + public void execute_emptyFilteredPersonList_throwsCommandException() { + CommandException thrown = assertThrows(CommandException.class, () -> exportCommand.execute(emptyModel)); + assertEquals(exportCommand.MESSAGE_NOTHING_TO_EXPORT_FAILURE, thrown.getMessage()); + } + + @Test + public void writeToJsonFileThrowsIoException_throwsCommandException() throws IOException { + AddressBook addressBook = new AddressBook(); + JsonAddressBookStorage jsonAddressBookStorage = mock(JsonAddressBookStorage.class); + + doThrow(new IOException("File write error")).when(jsonAddressBookStorage).saveAddressBook(addressBook); + + CommandException exception = assertThrows(CommandException.class, () -> { + exportCommand.writeToJsonFile(jsonAddressBookStorage, addressBook); + }); + assertEquals(exportCommand.MESSAGE_WRITE_TO_JSON_FAILURE, exception.getMessage()); + } + + @Test + public void nonexistentJsonFilePath_throwsCommandException() { + File jsonFile = new File("~/nonexistentpath/nonexistentfile.json"); + CommandException thrown = assertThrows(CommandException.class, () -> { + exportCommand.readJsonFile(jsonFile); + }); + assertEquals(exportCommand.MESSAGE_JSON_FILE_NOT_FOUND_FAILURE, thrown.getMessage()); + } + + @Test + public void invalidJsonFileThrowsJsonParseException_throwsCommandException() { + File jsonFile = new File("./src/test/data/ExportCommandTest/invalidJsonFile.json"); + + CommandException thrown = assertThrows(CommandException.class, () -> { + exportCommand.readJsonFile(jsonFile); + }); + assertEquals(exportCommand.MESSAGE_PARSE_JSON_FILE_FAILURE, thrown.getMessage()); + } + + @Test + public void emptyPersonsArray_throwsCommandException() { + ObjectMapper objectMapper = new ObjectMapper(); + ArrayNode emptyPersonsArray = objectMapper.createArrayNode(); + ObjectNode rootNode = objectMapper.createObjectNode(); + rootNode.set("persons", emptyPersonsArray); + JsonNode jsonTree = rootNode; + + CommandException exception = assertThrows(CommandException.class, () -> { + exportCommand.readPersonsArray(jsonTree); + }); + assertEquals(exportCommand.MESSAGE_EMPTY_JSON_FILE_FAILURE, exception.getMessage()); + } + + @Test + public void missingPersonsArray_throwsCommandException() { + ObjectMapper objectMapper = new ObjectMapper(); + ObjectNode jsonTree = objectMapper.createObjectNode(); + + CommandException exception = assertThrows(CommandException.class, () -> { + exportCommand.readPersonsArray(jsonTree); + }); + assertEquals(exportCommand.MESSAGE_EMPTY_JSON_FILE_FAILURE, exception.getMessage()); + } + + @Test + public void execute_unableToCreateCsvDirectory_throwsCommandException() { + exportCommand.updateCsvFilePath("~/nonexistent/path"); + + CommandException thrown = assertThrows(CommandException.class, () -> exportCommand.execute(model)); + assertEquals(exportCommand.MESSAGE_CREATE_CSV_DIRECTORY_FAILURE, thrown.getMessage()); + } + + @Test + public void execute_validAddressBookExport_success() throws Exception { + CommandResult commandResult = exportCommand.execute(model); + assertEquals(exportCommand.MESSAGE_SUCCESS, commandResult.getFeedbackToUser()); + + File csvFile = new File("./src/test/data/ExportCommandTest/addressbookdata/addressbooktest.csv"); + assertTrue(csvFile.exists()); + } + +} diff --git a/src/test/java/seedu/address/logic/commands/FindCommandTest.java b/src/test/java/seedu/address/logic/commands/FindCommandTest.java index b8b7dbba91a..65efcd736d4 100644 --- a/src/test/java/seedu/address/logic/commands/FindCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/FindCommandTest.java @@ -4,10 +4,19 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.logic.Messages.MESSAGE_PERSONS_LISTED_OVERVIEW; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LESS_THAN; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MORE_THAN; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.testutil.TypicalPersons.ALICE; +import static seedu.address.testutil.TypicalPersons.BENSON; import static seedu.address.testutil.TypicalPersons.CARL; +import static seedu.address.testutil.TypicalPersons.DANIEL; import static seedu.address.testutil.TypicalPersons.ELLE; import static seedu.address.testutil.TypicalPersons.FIONA; +import static seedu.address.testutil.TypicalPersons.GEORGE; import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; import java.util.Arrays; @@ -15,10 +24,14 @@ import org.junit.jupiter.api.Test; +import seedu.address.logic.Messages; import seedu.address.model.Model; import seedu.address.model.ModelManager; import seedu.address.model.UserPrefs; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.exam.Exam; +import seedu.address.model.person.ExamPredicate; +import seedu.address.model.person.PersonDetailPredicate; +import seedu.address.model.person.Score; /** * Contains integration tests (interaction with the Model) for {@code FindCommand}. @@ -29,19 +42,14 @@ public class FindCommandTest { @Test public void equals() { - NameContainsKeywordsPredicate firstPredicate = - new NameContainsKeywordsPredicate(Collections.singletonList("first")); - NameContainsKeywordsPredicate secondPredicate = - new NameContainsKeywordsPredicate(Collections.singletonList("second")); - - FindCommand findFirstCommand = new FindCommand(firstPredicate); - FindCommand findSecondCommand = new FindCommand(secondPredicate); + FindCommand findFirstCommand = new FindCommand(PREFIX_NAME, "first"); + FindCommand findSecondCommand = new FindCommand(PREFIX_NAME, "second"); // same object -> returns true assertTrue(findFirstCommand.equals(findFirstCommand)); // same values -> returns true - FindCommand findFirstCommandCopy = new FindCommand(firstPredicate); + FindCommand findFirstCommandCopy = new FindCommand(PREFIX_NAME, "first"); assertTrue(findFirstCommand.equals(findFirstCommandCopy)); // different types -> returns false @@ -55,37 +63,53 @@ public void equals() { } @Test - public void execute_zeroKeywords_noPersonFound() { + public void execute_name_noPersonFound() { String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 0); - NameContainsKeywordsPredicate predicate = preparePredicate(" "); - FindCommand command = new FindCommand(predicate); - expectedModel.updateFilteredPersonList(predicate); + FindCommand command = new FindCommand(PREFIX_NAME, "Adam"); + expectedModel.updateFilteredPersonList(new PersonDetailPredicate(PREFIX_NAME, "Adam")); assertCommandSuccess(command, model, expectedMessage, expectedModel); assertEquals(Collections.emptyList(), model.getFilteredPersonList()); } @Test - public void execute_multipleKeywords_multiplePersonsFound() { + public void execute_email_multiplePersonsFound() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 7); + FindCommand command = new FindCommand(PREFIX_EMAIL, "@example.com"); + expectedModel.updateFilteredPersonList(new PersonDetailPredicate(PREFIX_EMAIL, "@example.com")); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(ALICE, BENSON, CARL, DANIEL, ELLE, FIONA, GEORGE), model.getFilteredPersonList()); + } + + @Test + public void execute_examRequiredNoExamSelected_throwsCommandException() { + FindCommand command = new FindCommand(PREFIX_LESS_THAN, "50"); + assertCommandFailure(command, model, Messages.MESSAGE_NO_EXAM_SELECTED); + } + + @Test + public void execute_examRequiredExamSelectedValueTooHigh_throwsCommandException() { + model.selectExam(new Exam("Midterm", new Score(100))); + FindCommand command = new FindCommand(PREFIX_MORE_THAN, "101"); + assertCommandFailure(command, model, FindCommand.MESSAGE_SCORE_GREATER_THAN_MAX); + } + + @Test + public void execute_examRequired_multiplePersonsFound() { + model.selectExam(new Exam("Midterm", new Score(100))); String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 3); - NameContainsKeywordsPredicate predicate = preparePredicate("Kurz Elle Kunz"); - FindCommand command = new FindCommand(predicate); - expectedModel.updateFilteredPersonList(predicate); + FindCommand command = new FindCommand(PREFIX_LESS_THAN, "55"); + expectedModel.updateFilteredPersonList(new ExamPredicate(PREFIX_LESS_THAN, "55", + model.getSelectedExam().getValue())); assertCommandSuccess(command, model, expectedMessage, expectedModel); - assertEquals(Arrays.asList(CARL, ELLE, FIONA), model.getFilteredPersonList()); + assertEquals(Arrays.asList(CARL, DANIEL, ELLE), model.getFilteredPersonList()); } @Test public void toStringMethod() { - NameContainsKeywordsPredicate predicate = new NameContainsKeywordsPredicate(Arrays.asList("keyword")); - FindCommand findCommand = new FindCommand(predicate); - String expected = FindCommand.class.getCanonicalName() + "{predicate=" + predicate + "}"; - assertEquals(expected, findCommand.toString()); - } + FindCommand findCommand = new FindCommand(PREFIX_EMAIL, "@example.com"); + String expected = FindCommand.class.getCanonicalName() + "{prefix=" + PREFIX_EMAIL + + ", keyword=" + "@example.com" + "}"; - /** - * Parses {@code userInput} into a {@code NameContainsKeywordsPredicate}. - */ - private NameContainsKeywordsPredicate preparePredicate(String userInput) { - return new NameContainsKeywordsPredicate(Arrays.asList(userInput.split("\\s+"))); + assertEquals(expected, findCommand.toString()); } } diff --git a/src/test/java/seedu/address/logic/commands/HelpCommandTest.java b/src/test/java/seedu/address/logic/commands/HelpCommandTest.java index 4904fc4352e..10995c3b70e 100644 --- a/src/test/java/seedu/address/logic/commands/HelpCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/HelpCommandTest.java @@ -14,7 +14,7 @@ public class HelpCommandTest { @Test public void execute_help_success() { - CommandResult expectedCommandResult = new CommandResult(SHOWING_HELP_MESSAGE, true, false); + CommandResult expectedCommandResult = new CommandResult(SHOWING_HELP_MESSAGE, false, false); assertCommandSuccess(new HelpCommand(), model, expectedCommandResult, expectedModel); } } diff --git a/src/test/java/seedu/address/logic/commands/ImportCommandTest.java b/src/test/java/seedu/address/logic/commands/ImportCommandTest.java new file mode 100644 index 00000000000..0c96916df35 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/ImportCommandTest.java @@ -0,0 +1,73 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.testutil.Assert.assertThrows; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; + +public class ImportCommandTest { + @Test + public void execute_import_success() throws CommandException { + Model model = new ModelManager(new AddressBook(), new UserPrefs()); + Path filePath = Paths.get("src/test/data/ImportCommandTest/valid.csv"); + ImportCommand importCommand = new ImportCommand(filePath); + CommandResult commandResult = importCommand.execute(model); + String expected = "Imported persons successfully!\n\n" + + "All valid persons have been added!\n" + + "Successful imports: 7\n" + + "Unsuccessful imports: 0\n"; + String actual = commandResult.getFeedbackToUser(); + assertEquals(expected , actual); + } + @Test + public void execute_invalidPath_failure() { + Model model = new ModelManager(new AddressBook(), new UserPrefs()); + Path invalidFilePath = Paths.get("src/test/data/ImportCommandTest/nonExistent.csv"); + ImportCommand importCommand = new ImportCommand(invalidFilePath); + assertThrows(CommandException.class, () -> importCommand.execute(model)); + } + + @Test + public void execute_extraHeaders_success() throws CommandException { + Model model = new ModelManager(new AddressBook(), new UserPrefs()); + Path filePath = Paths.get("src/test/data/ImportCommandTest/extraHeader.csv"); + ImportCommand importCommand = new ImportCommand(filePath); + CommandResult commandResult = importCommand.execute(model); + String expected = "Imported persons successfully!\n\n" + + "All valid persons have been added!\n" + + "Successful imports: 7\n" + + "Unsuccessful imports: 0\n"; + String actual = commandResult.getFeedbackToUser(); + assertEquals(expected , actual); + } + + @Test + public void execute_missingOptionalValues_success() throws CommandException { + Model model = new ModelManager(new AddressBook(), new UserPrefs()); + Path filePath = Paths.get("src/test/data/ImportCommandTest/missingOptionalValue.csv"); + ImportCommand importCommand = new ImportCommand(filePath); + CommandResult commandResult = importCommand.execute(model); + String expected = "Imported persons successfully!\n\n" + + "All valid persons have been added!\n" + + "Successful imports: 7\n" + + "Unsuccessful imports: 0\n"; + String actual = commandResult.getFeedbackToUser(); + assertEquals(expected , actual); + } + @Test + public void equals_success() { + Path filePath = Paths.get("src/test/data/ImportCommandTest/valid.csv"); + ImportCommand importCommand = new ImportCommand(filePath); + ImportCommand importCommandCopy = new ImportCommand(filePath); + assertEquals(importCommand, importCommandCopy); + } +} diff --git a/src/test/java/seedu/address/logic/commands/ImportExamScoresCommandTest.java b/src/test/java/seedu/address/logic/commands/ImportExamScoresCommandTest.java new file mode 100644 index 00000000000..600c7077375 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/ImportExamScoresCommandTest.java @@ -0,0 +1,168 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.exam.Exam; +import seedu.address.model.person.Score; + +public class ImportExamScoresCommandTest { + public static final String PATH_VALID = "src/test/data/ImportExamCommandTest/valid.csv"; + public static final String PATH_EXTRA = "src/test/data/ImportExamCommandTest/extra.csv"; + public static final String PATH_EMPTY_CSV = "src/test/data/ImportExamCommandTest/valid_empty_csv.csv"; + public static final String PATH_INVALID = "invalid/path/to/file.csv"; + public static final String PATH_SCORE_NOT_NUMBER = "src/test/data/ImportExamCommandTest/not_number.csv"; + public static final String PATH_DUPLICATE_EXAMS = "src/test/data/ImportExamCommandTest/duplicate_exams.csv"; + public static final String PATH_MISMATCH_CSV_HEADERS = "src/test/data/ImportExamCommandTest/wrong_headers.csv"; + private ImportExamScoresCommand importExamScoresCommand; + private Model model; + + @BeforeEach + public void setUp() { + model = mock(Model.class); + model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + } + + @Test + public void execute_validFilePath_success() { + Path filePath = Paths.get(PATH_EMPTY_CSV); + importExamScoresCommand = new ImportExamScoresCommand(filePath); + + assertCommandFailure(importExamScoresCommand, model, ImportExamScoresCommand.ERROR_EMAIL_FIRST_VALUE); + } + + @Test + public void execute_invalidFilePath_throwsCommandException() { + Path invalidFilePath = Paths.get(PATH_INVALID); + importExamScoresCommand = new ImportExamScoresCommand(invalidFilePath); + + assertThrows(CommandException.class, () -> importExamScoresCommand.execute(model)); + } + + @Test + public void testGenerateErrorReportEmpty() { + String result = new ImportExamScoresCommand(Paths.get(PATH_VALID)).generateErrorReport(); + assertEquals("", result); + } + + @Test + public void testSuccess() { + Path filePath = Paths.get(PATH_VALID); + ImportExamScoresCommand importExamScoresCommand = new ImportExamScoresCommand(filePath); + String expectedMessage = String.format(ImportExamScoresCommand.MESSAGE_SUCCESS, filePath); + assertCommandSuccess(importExamScoresCommand, model, expectedMessage, model); + } + + @Test + public void testEquals() { + Path filePath = Paths.get(PATH_VALID); + ImportExamScoresCommand importExamScoresCommand = new ImportExamScoresCommand(filePath); + ImportExamScoresCommand importExamScoresCommandCopy = new ImportExamScoresCommand(filePath); + assertEquals(importExamScoresCommand, importExamScoresCommandCopy); + } + + @Test + public void testFailing() { + Path filePath = Paths.get(PATH_EXTRA); + Exam midterm = new Exam("BadMidterm", new Score(100)); + model.addExam(midterm); + ImportExamScoresCommand importExamScoresCommand = new ImportExamScoresCommand(filePath); + + String expectedError = buildErrorReport(filePath.toString(), + "email of non@example.com: " + ImportExamScoresCommand.MESSAGE_PERSON_DOES_NOT_EXIST, + "email of johnd@example.com: " + String.format( + ImportExamScoresCommand.MESSAGE_GRADE_TOO_HIGH, "Midterm"), + "exam of NonExistent: " + ImportExamScoresCommand.MESSAGE_EXAM_DOES_NOT_EXIST); + + assertCommandSuccess(importExamScoresCommand, model, expectedError, model); + } + + @Test + public void test_notNumber() { + Path filePath = Paths.get(PATH_SCORE_NOT_NUMBER); + ImportExamScoresCommand importExamScoresCommand = new ImportExamScoresCommand(filePath); + + String expectedError = buildErrorReport(filePath.toString(), + "email of alice@example.com: " + + String.format(ImportExamScoresCommand.MESSAGE_SCORE_NOT_NUMBER, "Midterm")); + + assertCommandSuccess(importExamScoresCommand, model, expectedError, model); + } + + @Test + public void test_duplicateExamHeaders() { + Path filePath = Paths.get(PATH_DUPLICATE_EXAMS); + ImportExamScoresCommand importExamScoresCommand = new ImportExamScoresCommand(filePath); + + String expectedError = buildErrorReport(filePath.toString(), + "exam of Midterm: " + ImportExamScoresCommand.MESSAGE_DUPLICATE_EXAM); + assertCommandSuccess(importExamScoresCommand, model, expectedError, model); + } + + @Test + public void testIsEmailFirstValue_validEmailFirstValue_success() { + ImportExamScoresCommand importExamScoresCommand = new ImportExamScoresCommand(Paths.get(PATH_VALID)); + + String[][] valid = {{"email"}}; + List lst = List.of(valid); + + assertTrue(importExamScoresCommand.isEmailFirstValue(lst)); + } + + @Test + public void testIsEmailFirstValue_listSizeZero_failure() { + ImportExamScoresCommand importExamScoresCommand = new ImportExamScoresCommand(Paths.get(PATH_VALID)); + + String[][] invalid = {}; + List lst = List.of(invalid); + + assertFalse(importExamScoresCommand.isEmailFirstValue(lst)); + } + + @Test + public void testIsEmailFirstValue_stringArraySizeZero_failure() { + ImportExamScoresCommand importExamScoresCommand = new ImportExamScoresCommand(Paths.get(PATH_VALID)); + + String[][] invalid = {{}}; + List lst = List.of(invalid); + + assertFalse(importExamScoresCommand.isEmailFirstValue(lst)); + } + + @Test + public void testMismatchCsvHeaders_failure() { + ImportExamScoresCommand importExamScoresCommand = new ImportExamScoresCommand( + Paths.get(PATH_MISMATCH_CSV_HEADERS)); + + assertCommandFailure(importExamScoresCommand, model, ImportExamScoresCommand.ERROR_WRONG_CSV_FORMAT); + } + + + private String buildErrorReport(String filePath, String... errors) { + StringBuilder errorReport = new StringBuilder(); + errorReport.append(String.format(ImportExamScoresCommand.MESSAGE_SUCCESS, filePath)); + errorReport.append(ImportExamScoresCommand.PREFIX_ERROR_REPORT); + for (String error : errors) { + errorReport.append(error + "\n"); + } + return errorReport.toString(); + } + +} diff --git a/src/test/java/seedu/address/logic/commands/SelectExamCommandTest.java b/src/test/java/seedu/address/logic/commands/SelectExamCommandTest.java new file mode 100644 index 00000000000..5cfd3b411c1 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/SelectExamCommandTest.java @@ -0,0 +1,79 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.exam.Exam; +import seedu.address.model.person.Score; + +public class SelectExamCommandTest { + + @Test + public void execute_examExists_selectsExam() throws CommandException { + AddressBook addressBook = new AddressBook(); + Exam exam = new Exam("Midterm", new Score(100)); + addressBook.addExam(exam); + UserPrefs userPrefs = new UserPrefs(); + Model model = new ModelManager(addressBook, userPrefs); + + SelectExamCommand command = new SelectExamCommand(Index.fromOneBased(1)); + + CommandResult result = command.execute(model); + + assertEquals(String.format(SelectExamCommand.MESSAGE_SELECT_EXAM_SUCCESS, exam), result.getFeedbackToUser()); + } + + @Test + public void execute_examDoesNotExist_throwsCommandException() { + AddressBook addressBook = new AddressBook(); + Exam exam = new Exam("Midterm", new Score(100)); + addressBook.addExam(exam); + UserPrefs userPrefs = new UserPrefs(); + Model model = new ModelManager(addressBook, userPrefs); + + SelectExamCommand command = new SelectExamCommand(Index.fromOneBased(2)); + + assertThrows(CommandException.class, () -> command.execute(model)); + } + + @Test + void equals() { + SelectExamCommand selectExamCommand1 = new SelectExamCommand(Index.fromZeroBased(1)); + SelectExamCommand selectExamCommand2 = new SelectExamCommand(Index.fromZeroBased(1)); + SelectExamCommand selectExamCommand3 = new SelectExamCommand(Index.fromZeroBased(2)); + + // same object -> returns true + assertTrue(selectExamCommand1.equals(selectExamCommand1)); + + // same values -> returns true + assertTrue(selectExamCommand1.equals(selectExamCommand2)); + + // different types -> returns false + assertFalse(selectExamCommand1.equals(1)); + + // null -> returns false + assertFalse(selectExamCommand1.equals(null)); + + // different index -> returns false + assertFalse(selectExamCommand1.equals(selectExamCommand3)); + } + + @Test + void toStringTest() { + SelectExamCommand selectExamCommand = new SelectExamCommand(Index.fromZeroBased(1)); + assertEquals("seedu.address.logic.commands.SelectExamCommand{targetIndex=" + + "seedu.address.commons.core.index.Index{zeroBasedIndex=1}}", + selectExamCommand.toString()); + } + +} diff --git a/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java b/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java index 5bc11d3cdaa..52c6f06190d 100644 --- a/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java @@ -1,33 +1,52 @@ package seedu.address.logic.parser; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.Messages.MESSAGE_MISSING_COMPULSORY_PREFIXES; import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_BOB; import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_BOB; import static seedu.address.logic.commands.CommandTestUtil.INVALID_ADDRESS_DESC; import static seedu.address.logic.commands.CommandTestUtil.INVALID_EMAIL_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_MATRIC_DESC; import static seedu.address.logic.commands.CommandTestUtil.INVALID_NAME_DESC; import static seedu.address.logic.commands.CommandTestUtil.INVALID_PHONE_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_REFLECTION_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_STUDIO_DESC; import static seedu.address.logic.commands.CommandTestUtil.INVALID_TAG_DESC; +import static seedu.address.logic.commands.CommandTestUtil.MATRIC_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.MATRIC_DESC_BOB; import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_BOB; import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_BOB; import static seedu.address.logic.commands.CommandTestUtil.PREAMBLE_NON_EMPTY; import static seedu.address.logic.commands.CommandTestUtil.PREAMBLE_WHITESPACE; +import static seedu.address.logic.commands.CommandTestUtil.REFLECTION_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.REFLECTION_DESC_BOB; +import static seedu.address.logic.commands.CommandTestUtil.STUDIO_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.STUDIO_DESC_BOB; import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_FRIEND; import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_HUSBAND; import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_DESC_STUDENT; import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_MATRIC_NUMBER_AMY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_MATRIC_NUMBER_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_REFLECTION_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_STUDIO_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_FRIEND; import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; +import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_STUDENT; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MATRIC_NUMBER; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_REFLECTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_STUDIO; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; import static seedu.address.testutil.TypicalPersons.AMY; @@ -42,6 +61,9 @@ import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.student.Matric; +import seedu.address.model.student.Reflection; +import seedu.address.model.student.Studio; import seedu.address.model.tag.Tag; import seedu.address.testutil.PersonBuilder; @@ -50,25 +72,32 @@ public class AddCommandParserTest { @Test public void parse_allFieldsPresent_success() { - Person expectedPerson = new PersonBuilder(BOB).withTags(VALID_TAG_FRIEND).build(); + Person expectedPerson = new PersonBuilder(BOB) + .withTags(VALID_TAG_FRIEND, VALID_TAG_STUDENT).withMatric(VALID_MATRIC_NUMBER_BOB) + .withReflection(VALID_REFLECTION_BOB) + .withStudio(VALID_STUDIO_BOB).build(); // whitespace only preamble - assertParseSuccess(parser, PREAMBLE_WHITESPACE + NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB - + ADDRESS_DESC_BOB + TAG_DESC_FRIEND, new AddCommand(expectedPerson)); + assertParseSuccess(parser, + PREAMBLE_WHITESPACE + NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + + ADDRESS_DESC_BOB + TAG_DESC_FRIEND + MATRIC_DESC_BOB + REFLECTION_DESC_BOB + STUDIO_DESC_BOB + + VALID_DESC_STUDENT, + new AddCommand(expectedPerson)); // multiple tags - all accepted - Person expectedPersonMultipleTags = new PersonBuilder(BOB).withTags(VALID_TAG_FRIEND, VALID_TAG_HUSBAND) - .build(); - assertParseSuccess(parser, - NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, + Person expectedPersonMultipleTags = new PersonBuilder(BOB).withTags( + VALID_TAG_FRIEND, VALID_TAG_HUSBAND, VALID_TAG_STUDENT).build(); + assertParseSuccess(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + TAG_DESC_HUSBAND + TAG_DESC_FRIEND + MATRIC_DESC_BOB + REFLECTION_DESC_BOB + STUDIO_DESC_BOB + + VALID_DESC_STUDENT, new AddCommand(expectedPersonMultipleTags)); } @Test public void parse_repeatedNonTagValue_failure() { String validExpectedPersonString = NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB - + ADDRESS_DESC_BOB + TAG_DESC_FRIEND; + + ADDRESS_DESC_BOB + TAG_DESC_FRIEND + MATRIC_DESC_BOB + REFLECTION_DESC_BOB + STUDIO_DESC_BOB; // multiple names assertParseFailure(parser, NAME_DESC_AMY + validExpectedPersonString, @@ -86,11 +115,23 @@ public void parse_repeatedNonTagValue_failure() { assertParseFailure(parser, ADDRESS_DESC_AMY + validExpectedPersonString, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_ADDRESS)); + // multiple matric numbers + assertParseFailure(parser, MATRIC_DESC_AMY + validExpectedPersonString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_MATRIC_NUMBER)); + + // multiple reflections + assertParseFailure(parser, REFLECTION_DESC_AMY + validExpectedPersonString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_REFLECTION)); + + // multiple studios + assertParseFailure(parser, STUDIO_DESC_AMY + validExpectedPersonString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_STUDIO)); + // multiple fields repeated - assertParseFailure(parser, - validExpectedPersonString + PHONE_DESC_AMY + EMAIL_DESC_AMY + NAME_DESC_AMY + ADDRESS_DESC_AMY - + validExpectedPersonString, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NAME, PREFIX_ADDRESS, PREFIX_EMAIL, PREFIX_PHONE)); + assertParseFailure(parser, PHONE_DESC_AMY + EMAIL_DESC_AMY + + NAME_DESC_AMY + ADDRESS_DESC_AMY + validExpectedPersonString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NAME, PREFIX_ADDRESS, + PREFIX_EMAIL, PREFIX_PHONE)); // invalid value followed by valid value @@ -110,6 +151,18 @@ public void parse_repeatedNonTagValue_failure() { assertParseFailure(parser, INVALID_ADDRESS_DESC + validExpectedPersonString, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_ADDRESS)); + // invalid matric number + assertParseFailure(parser, INVALID_MATRIC_DESC + validExpectedPersonString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_MATRIC_NUMBER)); + + // invalid reflection + assertParseFailure(parser, INVALID_REFLECTION_DESC + validExpectedPersonString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_REFLECTION)); + + // invalid studio + assertParseFailure(parser, INVALID_STUDIO_DESC + validExpectedPersonString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_STUDIO)); + // valid value followed by invalid value // invalid name @@ -127,39 +180,145 @@ public void parse_repeatedNonTagValue_failure() { // invalid address assertParseFailure(parser, validExpectedPersonString + INVALID_ADDRESS_DESC, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_ADDRESS)); + + // invalid matric number + assertParseFailure(parser, validExpectedPersonString + INVALID_MATRIC_DESC, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_MATRIC_NUMBER)); + + // invalid reflection + assertParseFailure(parser, validExpectedPersonString + INVALID_REFLECTION_DESC, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_REFLECTION)); + + // invalid studio + assertParseFailure(parser, validExpectedPersonString + INVALID_STUDIO_DESC, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_STUDIO)); + } + + @Test + public void parse_tagMissing_success() { + // zero tags; all other fields present means student + Person expectedPerson = new PersonBuilder(AMY).withTags(VALID_TAG_STUDENT) + .withMatric(VALID_MATRIC_NUMBER_AMY).build(); + assertParseSuccess(parser, NAME_DESC_AMY + PHONE_DESC_AMY + + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + MATRIC_DESC_AMY + REFLECTION_DESC_AMY + STUDIO_DESC_AMY, + new AddCommand(expectedPerson)); + } + + @Test + public void parse_matricMissing_success() { + // no matric number + Person expectedPerson = new PersonBuilder(AMY).withTags(VALID_TAG_FRIEND).withMatric("").build(); + assertParseSuccess(parser, NAME_DESC_AMY + PHONE_DESC_AMY + + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + TAG_DESC_FRIEND + REFLECTION_DESC_AMY + STUDIO_DESC_AMY, + new AddCommand(expectedPerson)); + } + + @Test + public void parse_reflectionMissing_success() { + // no reflection means TA + Person expectedPerson = new PersonBuilder(AMY).withTags(VALID_TAG_FRIEND, VALID_TAG_STUDENT) + .withMatric(VALID_MATRIC_NUMBER_AMY).withReflection("").build(); + assertParseSuccess(parser, NAME_DESC_AMY + PHONE_DESC_AMY + + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + TAG_DESC_FRIEND + MATRIC_DESC_AMY + STUDIO_DESC_AMY, + new AddCommand(expectedPerson)); } @Test - public void parse_optionalFieldsMissing_success() { - // zero tags - Person expectedPerson = new PersonBuilder(AMY).withTags().build(); - assertParseSuccess(parser, NAME_DESC_AMY + PHONE_DESC_AMY + EMAIL_DESC_AMY + ADDRESS_DESC_AMY, + public void parse_studioMissing_success() { + // no studio means TA + Person expectedPerson = new PersonBuilder(AMY).withTags(VALID_TAG_FRIEND, VALID_TAG_STUDENT) + .withMatric(VALID_MATRIC_NUMBER_AMY).withStudio("").build(); + assertParseSuccess(parser, NAME_DESC_AMY + PHONE_DESC_AMY + + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + TAG_DESC_FRIEND + MATRIC_DESC_AMY + REFLECTION_DESC_AMY, + new AddCommand(expectedPerson)); + } + + @Test + public void parse_matricAndReflectionMissing_success() { + // no matric number; no reflection + Person expectedPerson = new PersonBuilder(AMY).withTags(VALID_TAG_FRIEND) + .withMatric("").withReflection("").build(); + assertParseSuccess(parser, NAME_DESC_AMY + PHONE_DESC_AMY + + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + TAG_DESC_FRIEND + STUDIO_DESC_AMY, + new AddCommand(expectedPerson)); + } + + + @Test + public void parse_tagAndStudioMissing_success() { + // zero tags; no studio + Person expectedPerson = new PersonBuilder(AMY).withTags(VALID_TAG_STUDENT).withMatric(VALID_MATRIC_NUMBER_AMY) + .withStudio("").build(); + assertParseSuccess(parser, NAME_DESC_AMY + PHONE_DESC_AMY + + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + MATRIC_DESC_AMY + REFLECTION_DESC_AMY, + new AddCommand(expectedPerson)); + } + + @Test + public void parse_tagAndMatricMissing_success() { + // zero tags; no matric number + Person expectedPerson = new PersonBuilder(AMY).withTags().withMatric("").build(); + assertParseSuccess(parser, NAME_DESC_AMY + PHONE_DESC_AMY + + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + REFLECTION_DESC_AMY + STUDIO_DESC_AMY, + new AddCommand(expectedPerson)); + } + + @Test + public void parse_tagAndMatricAndReflectionMissing_success() { + // zero tags; no matric number; no reflection + Person expectedPerson = new PersonBuilder(AMY).withTags().withMatric("").withReflection("").build(); + assertParseSuccess(parser, NAME_DESC_AMY + PHONE_DESC_AMY + + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + STUDIO_DESC_AMY, + new AddCommand(expectedPerson)); + } + + @Test + public void parse_tagAndMatricAndStudioMissing_success() { + // zero tags; no matric number; no studio + Person expectedPerson = new PersonBuilder(AMY).withTags() + .withMatric("").withStudio("").build(); + assertParseSuccess(parser, NAME_DESC_AMY + PHONE_DESC_AMY + + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + REFLECTION_DESC_AMY, + new AddCommand(expectedPerson)); + } + + @Test + public void parse_matricAndStudioAndReflectionMissing_success() { + Person expectedPerson = new PersonBuilder(AMY).withTags() + .withMatric("").withReflection("").withStudio("").build(); + assertParseSuccess(parser, NAME_DESC_AMY + PHONE_DESC_AMY + + EMAIL_DESC_AMY + ADDRESS_DESC_AMY, new AddCommand(expectedPerson)); } @Test public void parse_compulsoryFieldMissing_failure() { - String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE); // missing name prefix assertParseFailure(parser, VALID_NAME_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB, - expectedMessage); + String.format(MESSAGE_INVALID_COMMAND_FORMAT, MESSAGE_MISSING_COMPULSORY_PREFIXES + + PREFIX_NAME.toString())); // missing phone prefix assertParseFailure(parser, NAME_DESC_BOB + VALID_PHONE_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB, - expectedMessage); + String.format(MESSAGE_INVALID_COMMAND_FORMAT, MESSAGE_MISSING_COMPULSORY_PREFIXES + + PREFIX_PHONE.toString())); // missing email prefix assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + VALID_EMAIL_BOB + ADDRESS_DESC_BOB, - expectedMessage); + String.format(MESSAGE_INVALID_COMMAND_FORMAT, MESSAGE_MISSING_COMPULSORY_PREFIXES + + PREFIX_EMAIL.toString())); // missing address prefix assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + VALID_ADDRESS_BOB, - expectedMessage); + String.format(MESSAGE_INVALID_COMMAND_FORMAT, MESSAGE_MISSING_COMPULSORY_PREFIXES + + PREFIX_ADDRESS.toString())); // all prefixes missing assertParseFailure(parser, VALID_NAME_BOB + VALID_PHONE_BOB + VALID_EMAIL_BOB + VALID_ADDRESS_BOB, - expectedMessage); + String.format(MESSAGE_INVALID_COMMAND_FORMAT, MESSAGE_MISSING_COMPULSORY_PREFIXES + + PREFIX_NAME.toString() + ", " + PREFIX_PHONE.toString() + ", " + + PREFIX_EMAIL.toString() + ", " + PREFIX_ADDRESS.toString())); } @Test @@ -184,6 +343,19 @@ public void parse_invalidValue_failure() { assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + INVALID_TAG_DESC + VALID_TAG_FRIEND, Tag.MESSAGE_CONSTRAINTS); + // invalid matric number + assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + TAG_DESC_HUSBAND + TAG_DESC_FRIEND + INVALID_MATRIC_DESC, Matric.MESSAGE_CONSTRAINTS); + + // invalid reflection + assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + TAG_DESC_HUSBAND + TAG_DESC_FRIEND + INVALID_REFLECTION_DESC, Reflection.MESSAGE_CONSTRAINTS); + + + // invalid studio + assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + TAG_DESC_HUSBAND + TAG_DESC_FRIEND + INVALID_STUDIO_DESC, Studio.MESSAGE_CONSTRAINTS); + // two invalid values, only first invalid value reported assertParseFailure(parser, INVALID_NAME_DESC + PHONE_DESC_BOB + EMAIL_DESC_BOB + INVALID_ADDRESS_DESC, Name.MESSAGE_CONSTRAINTS); diff --git a/src/test/java/seedu/address/logic/parser/AddExamCommandParserTest.java b/src/test/java/seedu/address/logic/parser/AddExamCommandParserTest.java new file mode 100644 index 00000000000..cd386de89a5 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/AddExamCommandParserTest.java @@ -0,0 +1,72 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCORE; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.Messages; +import seedu.address.logic.commands.AddExamCommand; +import seedu.address.model.exam.Exam; +import seedu.address.model.person.Score; + +public class AddExamCommandParserTest { + private AddExamCommandParser parser = new AddExamCommandParser(); + + @Test + public void parse_allFieldsPresent_success() { + Exam expectedExam = new Exam("Midterm", new Score(100)); + + // whitespace only preamble + assertParseSuccess(parser, " " + PREFIX_NAME + "Midterm " + PREFIX_SCORE + "100", + new AddExamCommand(expectedExam)); + } + + @Test + public void parse_repeatedValue_failure() { + + // repeated name prefix + assertParseFailure(parser, " " + PREFIX_NAME + "Midterm " + PREFIX_NAME + "Final " + PREFIX_SCORE + "100", + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NAME)); + + // repeated score prefix + assertParseFailure(parser, " " + PREFIX_NAME + "Midterm " + PREFIX_SCORE + "100 " + PREFIX_SCORE + "200", + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_SCORE)); + } + + @Test + public void parse_invalidValue_failure() { + // invalid name + assertParseFailure(parser, " " + PREFIX_NAME + " " + PREFIX_SCORE + "100", + Exam.MESSAGE_CONSTRAINTS); + + // invalid name + assertParseFailure(parser, " " + PREFIX_NAME + "Midterm& " + PREFIX_SCORE + "100", + Exam.MESSAGE_CONSTRAINTS); + + // invalid score + assertParseFailure(parser, " " + PREFIX_NAME + "Midterm " + PREFIX_SCORE + " ", + Exam.MESSAGE_CONSTRAINTS); + + // invalid score + assertParseFailure(parser, " " + PREFIX_NAME + "Midterm " + PREFIX_SCORE + "abc", + Exam.MESSAGE_CONSTRAINTS); + } + + @Test + public void parse_compulsoryFieldMissing_failure() { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddExamCommand.MESSAGE_USAGE); + + // missing name prefix + assertParseFailure(parser, " " + PREFIX_SCORE + "100", expectedMessage); + + // missing score prefix + assertParseFailure(parser, " " + PREFIX_NAME + "Midterm", expectedMessage); + + // all prefixes missing + assertParseFailure(parser, "", expectedMessage); + } +} diff --git a/src/test/java/seedu/address/logic/parser/AddScoreCommandParserTest.java b/src/test/java/seedu/address/logic/parser/AddScoreCommandParserTest.java new file mode 100644 index 00000000000..9428d063995 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/AddScoreCommandParserTest.java @@ -0,0 +1,57 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCORE; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.AddScoreCommand; +import seedu.address.model.person.Score; + +public class AddScoreCommandParserTest { + + private AddScoreCommandParser parser = new AddScoreCommandParser(); + + @Test + public void parse_allFieldsPresent_success() { + Index expectedIndex = Index.fromOneBased(1); + Score expectedScore = new Score(100); + + // whitespace only preamble + assertParseSuccess(parser, " 1 " + PREFIX_SCORE + "100", + new AddScoreCommand(expectedIndex, expectedScore)); + } + + @Test + public void parse_invalidScore_failure() { + String expectedMessage = Score.MESSAGE_CONSTRAINTS; + // invalid score + assertParseFailure(parser, "1 " + PREFIX_SCORE + "abd", expectedMessage); + } + + @Test + public void parse_repeatedScore_failure() { + String expectedMessage = Messages.getErrorMessageForDuplicatePrefixes(PREFIX_SCORE); + + // repeated score prefix + assertParseFailure(parser, "1 " + PREFIX_SCORE + "100 " + PREFIX_SCORE + "100", expectedMessage); + } + + @Test + public void parse_compulsoryFieldMissing_failure() { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddScoreCommand.MESSAGE_USAGE); + + // missing index + assertParseFailure(parser, " " + PREFIX_SCORE + "100", expectedMessage); + + // missing score prefix + assertParseFailure(parser, "1", expectedMessage); + + // all prefixes missing + assertParseFailure(parser, "", expectedMessage); + } +} diff --git a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java index 5a1ab3dbc0c..66ded37cf3e 100644 --- a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java +++ b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java @@ -4,27 +4,39 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.Messages.MESSAGE_UNKNOWN_COMMAND; +import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_STUDENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_IMPORT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCORE; import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - import org.junit.jupiter.api.Test; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddExamCommand; +import seedu.address.logic.commands.AddScoreCommand; import seedu.address.logic.commands.ClearCommand; +import seedu.address.logic.commands.CopyCommand; import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.DeleteExamCommand; +import seedu.address.logic.commands.DeleteScoreCommand; +import seedu.address.logic.commands.DeleteShownCommand; +import seedu.address.logic.commands.DeselectExamCommand; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; +import seedu.address.logic.commands.EditScoreCommand; import seedu.address.logic.commands.ExitCommand; +import seedu.address.logic.commands.ExportCommand; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.ImportCommand; +import seedu.address.logic.commands.ImportExamScoresCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.SelectExamCommand; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; import seedu.address.model.person.Person; +import seedu.address.model.person.Score; import seedu.address.testutil.EditPersonDescriptorBuilder; import seedu.address.testutil.PersonBuilder; import seedu.address.testutil.PersonUtil; @@ -35,7 +47,7 @@ public class AddressBookParserTest { @Test public void parseCommand_add() throws Exception { - Person person = new PersonBuilder().build(); + Person person = new PersonBuilder().withTags(VALID_TAG_STUDENT).build(); AddCommand command = (AddCommand) parser.parseCommand(PersonUtil.getAddCommand(person)); assertEquals(new AddCommand(person), command); } @@ -62,6 +74,15 @@ public void parseCommand_edit() throws Exception { assertEquals(new EditCommand(INDEX_FIRST_PERSON, descriptor), command); } + @Test + public void parseCommand_editScore() throws Exception { + String prefix = PREFIX_SCORE.toString(); + Score score = new Score(17); + EditScoreCommand command = (EditScoreCommand) parser.parseCommand(EditScoreCommand.COMMAND_WORD + " " + + INDEX_FIRST_PERSON.getOneBased() + " " + prefix + score.getScore()); + assertEquals(new EditScoreCommand(INDEX_FIRST_PERSON, score), command); + } + @Test public void parseCommand_exit() throws Exception { assertTrue(parser.parseCommand(ExitCommand.COMMAND_WORD) instanceof ExitCommand); @@ -70,10 +91,11 @@ public void parseCommand_exit() throws Exception { @Test public void parseCommand_find() throws Exception { - List keywords = Arrays.asList("foo", "bar", "baz"); + String prefix = PREFIX_NAME.toString(); + String parameter = "Johan"; FindCommand command = (FindCommand) parser.parseCommand( - FindCommand.COMMAND_WORD + " " + keywords.stream().collect(Collectors.joining(" "))); - assertEquals(new FindCommand(new NameContainsKeywordsPredicate(keywords)), command); + FindCommand.COMMAND_WORD + " " + prefix + parameter); + assertEquals(new FindCommand(PREFIX_NAME, parameter), command); } @Test @@ -88,10 +110,73 @@ public void parseCommand_list() throws Exception { assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD + " 3") instanceof ListCommand); } + @Test + public void parseCommand_copy() throws Exception { + assertTrue(parser.parseCommand(CopyCommand.COMMAND_WORD) instanceof CopyCommand); + assertTrue(parser.parseCommand(CopyCommand.COMMAND_WORD + " 3") instanceof CopyCommand); + } + + @Test + public void parseCommand_export() throws Exception { + assertTrue(parser.parseCommand(ExportCommand.COMMAND_WORD) instanceof ExportCommand); + } + + @Test + public void parseCommand_import() throws Exception { + assertTrue(parser.parseCommand(ImportCommand.COMMAND_WORD + " " + + PREFIX_IMPORT + "src.csv") instanceof ImportCommand); + } + + @Test + public void parseCommand_importExam() throws Exception { + assertTrue(parser.parseCommand( + ImportExamScoresCommand.COMMAND_WORD + " " + PREFIX_IMPORT + "src.csv") + instanceof ImportExamScoresCommand); + } + + @Test + public void parseCommand_deleteShown() throws Exception { + assertTrue(parser.parseCommand(DeleteShownCommand.COMMAND_WORD) instanceof DeleteShownCommand); + } + + @Test + public void parseCommand_selectExam() throws Exception { + assertTrue(parser.parseCommand(SelectExamCommand.COMMAND_WORD + " 1") instanceof SelectExamCommand); + } + @Test public void parseCommand_unrecognisedInput_throwsParseException() { - assertThrows(ParseException.class, String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE), () - -> parser.parseCommand("")); + assertThrows(ParseException.class, String.format(MESSAGE_INVALID_COMMAND_FORMAT, + HelpCommand.MESSAGE_USAGE), () -> parser.parseCommand("")); + } + + @Test + void parseCommand_addExam() throws Exception { + assertTrue(parser.parseCommand(AddExamCommand.COMMAND_WORD + " " + + PREFIX_NAME + "Midterm " + PREFIX_SCORE + "100") + instanceof AddExamCommand); + } + + @Test + public void parseCommand_deleteExam() throws Exception { + assertTrue(parser.parseCommand("deleteExam 1") instanceof DeleteExamCommand); + } + + @Test + public void parseCommand_deleteScore() throws Exception { + DeleteScoreCommand command = (DeleteScoreCommand) parser.parseCommand( + DeleteScoreCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased()); + assertEquals(new DeleteScoreCommand(INDEX_FIRST_PERSON), command); + } + + @Test + public void parseCommand_deselectExam() throws Exception { + assertTrue(parser.parseCommand("deselectExam") instanceof DeselectExamCommand); + } + + @Test + public void parseCommand_addScore() throws Exception { + assertTrue(parser.parseCommand("addScore 1 " + PREFIX_SCORE + "100") instanceof AddScoreCommand); } @Test diff --git a/src/test/java/seedu/address/logic/parser/ArgumentMultiMapTest.java b/src/test/java/seedu/address/logic/parser/ArgumentMultiMapTest.java new file mode 100644 index 00000000000..cdcc4c45637 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/ArgumentMultiMapTest.java @@ -0,0 +1,66 @@ +package seedu.address.logic.parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class ArgumentMultiMapTest { + + private ArgumentMultimap testArgumentMultimap = new ArgumentMultimap(); + private Prefix testPrefix = new Prefix("n/"); + private Prefix testPrefix2 = new Prefix("e/"); + private Prefix preamblePrefix = new Prefix(""); + + @Test + public void testisSinglePrefix_onePrefix_returnTrue() { + testArgumentMultimap = new ArgumentMultimap(); + testArgumentMultimap.put(testPrefix, "Alice"); + assertEquals(true, testArgumentMultimap.isSinglePrefix()); + } + + @Test + public void testisSinglePrefix_multiplePrefixes_returnFalse() { + testArgumentMultimap = new ArgumentMultimap(); + testArgumentMultimap.put(testPrefix, "Alice"); + testArgumentMultimap.put(testPrefix2, "alice@gmail.com"); + assertEquals(false, testArgumentMultimap.isSinglePrefix()); + } + + @Test + public void testisSinglePrefix_emptyMultimap_returnFalse() { + testArgumentMultimap = new ArgumentMultimap(); + assertEquals(false, testArgumentMultimap.isSinglePrefix()); + } + + @Test + public void testisSinglePrefix_onlyPreamble_returnFalse() { + testArgumentMultimap = new ArgumentMultimap(); + testArgumentMultimap.put(preamblePrefix, "preamble"); + assertEquals(false, testArgumentMultimap.isSinglePrefix()); + } + + @Test + public void testisSinglePrefix_preambleAndPrefix_returnFalse() { + testArgumentMultimap = new ArgumentMultimap(); + testArgumentMultimap.put(preamblePrefix, "preamble"); + testArgumentMultimap.put(testPrefix, "Alice"); + assertEquals(false, testArgumentMultimap.isSinglePrefix()); + } + + @Test + public void testisSinglePrefix_emptyPreambleAndPrefix_returnTrue() { + testArgumentMultimap = new ArgumentMultimap(); + testArgumentMultimap.put(preamblePrefix, ""); + testArgumentMultimap.put(testPrefix, "Alice"); + assertEquals(true, testArgumentMultimap.isSinglePrefix()); + } + + @Test + public void testisSinglePrefix_emptyPreambleAndMultiplePrefix_returnFalse() { + testArgumentMultimap = new ArgumentMultimap(); + testArgumentMultimap.put(preamblePrefix, ""); + testArgumentMultimap.put(testPrefix, "Alice"); + testArgumentMultimap.put(testPrefix2, "alice@gmail.com"); + assertEquals(false, testArgumentMultimap.isSinglePrefix()); + } +} diff --git a/src/test/java/seedu/address/logic/parser/DeleteExamCommandParserTest.java b/src/test/java/seedu/address/logic/parser/DeleteExamCommandParserTest.java new file mode 100644 index 00000000000..7a24ecd7456 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/DeleteExamCommandParserTest.java @@ -0,0 +1,31 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.DeleteExamCommand; + +public class DeleteExamCommandParserTest { + + private DeleteExamCommandParser parser = new DeleteExamCommandParser(); + + @Test + public void parse_validArgs_returnsDeleteCommand() { + assertParseSuccess(parser, "1", new DeleteExamCommand(Index.fromOneBased(1))); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteExamCommand.MESSAGE_USAGE); + + // non-integer input + assertParseFailure(parser, "a", expectedMessage); + + // empty string + assertParseFailure(parser, "", expectedMessage); + } +} diff --git a/src/test/java/seedu/address/logic/parser/DeleteScoreCommandParserTest.java b/src/test/java/seedu/address/logic/parser/DeleteScoreCommandParserTest.java new file mode 100644 index 00000000000..6c8fd389f4c --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/DeleteScoreCommandParserTest.java @@ -0,0 +1,27 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.DeleteScoreCommand; + +public class DeleteScoreCommandParserTest { + + private DeleteScoreCommandParser parser = new DeleteScoreCommandParser(); + + @Test + public void parse_validArgs_returnsDeleteScoreCommand() { + assertParseSuccess(parser, "1", new DeleteScoreCommand(INDEX_FIRST_PERSON)); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + assertParseFailure(parser, "a", String.format(MESSAGE_INVALID_COMMAND_FORMAT, + DeleteScoreCommand.MESSAGE_USAGE)); + } + +} diff --git a/src/test/java/seedu/address/logic/parser/EditScoreCommandParserTest.java b/src/test/java/seedu/address/logic/parser/EditScoreCommandParserTest.java new file mode 100644 index 00000000000..efd7c98d510 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/EditScoreCommandParserTest.java @@ -0,0 +1,48 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCORE; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.EditScoreCommand; +import seedu.address.model.person.Score; + +public class EditScoreCommandParserTest { + + private EditScoreCommandParser parser = new EditScoreCommandParser(); + + @Test + public void parse_allFieldsPresent_success() { + Index index = Index.fromOneBased(1); + Score score = new Score(17); + + assertParseSuccess(parser, "1 " + PREFIX_SCORE + "17", + new EditScoreCommand(index, score)); + } + + @Test + public void parse_multipleRepeatedFields_failure() { + assertParseFailure(parser, "1 " + PREFIX_SCORE + "17 " + PREFIX_SCORE + "17", + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_SCORE)); + } + + @Test + public void parse_invalidScore_failure() { + String expectedMessage = Score.MESSAGE_CONSTRAINTS; + assertParseFailure(parser, "1 " + PREFIX_SCORE + "hello", expectedMessage); + } + + @Test + public void parse_compulsoryFieldMissing_failure() { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditScoreCommand.MESSAGE_USAGE); + + assertParseFailure(parser, " " + PREFIX_SCORE + "17", expectedMessage); + assertParseFailure(parser, "1", expectedMessage); + assertParseFailure(parser, "", expectedMessage); + } +} diff --git a/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java b/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java index d92e64d12f9..a8ffdcbe14a 100644 --- a/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java @@ -1,34 +1,127 @@ package seedu.address.logic.parser; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LESS_THAN; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MORE_THAN; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SPECIAL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; -import java.util.Arrays; - import org.junit.jupiter.api.Test; import seedu.address.logic.commands.FindCommand; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.Score; public class FindCommandParserTest { private FindCommandParser parser = new FindCommandParser(); + private String invalidPrefix = "z" + PREFIX_SPECIAL.toString(); @Test public void parse_emptyArg_throwsParseException() { - assertParseFailure(parser, " ", String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); + assertParseFailure(parser, " ", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); + } + + + @Test + public void parse_invalidPrefix_throwsParseException() { + // no leading and trailing whitespaces + assertParseFailure(parser, " " + invalidPrefix + "Alice", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_validNameArg_returnsFindCommand() { + // no leading and trailing whitespaces + FindCommand expectedFindCommand = + new FindCommand(PREFIX_NAME, "Alice"); + assertParseSuccess(parser, " " + PREFIX_NAME + "Alice", expectedFindCommand); + + // whitespace before keyword + assertParseSuccess(parser, " " + PREFIX_NAME + " Alice", expectedFindCommand); + } + + @Test + public void parse_validEmailArg_returnsFindCommand() { + // no leading and trailing whitespaces + FindCommand expectedFindCommand = + new FindCommand(PREFIX_EMAIL, "alice@gmail.com"); + assertParseSuccess(parser, " " + PREFIX_EMAIL + "alice@gmail.com", expectedFindCommand); + + // whitespace before keyword + assertParseSuccess(parser, " " + PREFIX_EMAIL + " alice@gmail.com ", expectedFindCommand); + } + + @Test + public void parse_validPhoneArg_returnsFindCommand() { + // no leading and trailing whitespaces + FindCommand expectedFindCommand = + new FindCommand(PREFIX_PHONE, "91234567"); + assertParseSuccess(parser, " " + PREFIX_PHONE + "91234567", expectedFindCommand); + + // whitespace before and after keyword + assertParseSuccess(parser, " " + PREFIX_PHONE + " 91234567 ", expectedFindCommand); } @Test - public void parse_validArgs_returnsFindCommand() { + public void parse_validAddressArg_returnsFindCommand() { // no leading and trailing whitespaces FindCommand expectedFindCommand = - new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList("Alice", "Bob"))); - assertParseSuccess(parser, "Alice Bob", expectedFindCommand); + new FindCommand(PREFIX_ADDRESS, "123, Jurong West Ave 6"); + assertParseSuccess(parser, " " + PREFIX_ADDRESS + "123, Jurong West Ave 6", expectedFindCommand); - // multiple whitespaces between keywords - assertParseSuccess(parser, " \n Alice \n \t Bob \t", expectedFindCommand); + // whitespace before keyword + assertParseSuccess(parser, " " + PREFIX_ADDRESS + " 123, Jurong West Ave 6 ", expectedFindCommand); } + @Test + public void parse_validTagArg_returnsFindCommand() { + // no leading and trailing whitespaces + FindCommand expectedFindCommand = + new FindCommand(PREFIX_TAG, "friends"); + assertParseSuccess(parser, " " + PREFIX_TAG + "friends", expectedFindCommand); + + // whitespace before keyword + assertParseSuccess(parser, " " + PREFIX_TAG + " friends ", expectedFindCommand); + } + + @Test + public void parse_validLessThanArg_returnsFindCommand() { + // no leading and trailing whitespaces + FindCommand expectedFindCommand = + new FindCommand(PREFIX_LESS_THAN, "50"); + assertParseSuccess(parser, " " + PREFIX_LESS_THAN + "50", expectedFindCommand); + + // whitespace before keyword + assertParseSuccess(parser, " " + PREFIX_LESS_THAN + " 50 ", expectedFindCommand); + } + + @Test + public void parse_validMoreThanArg_returnsFindCommand() { + // no leading and trailing whitespaces + FindCommand expectedFindCommand = + new FindCommand(PREFIX_MORE_THAN, "50.55"); + assertParseSuccess(parser, " " + PREFIX_MORE_THAN + "50.55", expectedFindCommand); + + // whitespace before keyword + assertParseSuccess(parser, " " + PREFIX_MORE_THAN + " 50.55 ", expectedFindCommand); + } + + @Test + public void parse_invalidMoreThanArg_throwsParseException() { + assertParseFailure(parser, " " + PREFIX_MORE_THAN + "20.201", + Score.MESSAGE_CONSTRAINTS); + } + + @Test + public void parse_invalidLessThanArg_throwsParseException() { + assertParseFailure(parser, " " + PREFIX_LESS_THAN + "abc", + Score.MESSAGE_CONSTRAINTS); + } } diff --git a/src/test/java/seedu/address/logic/parser/ImportCommandParserTest.java b/src/test/java/seedu/address/logic/parser/ImportCommandParserTest.java new file mode 100644 index 00000000000..948ec1a8eb8 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/ImportCommandParserTest.java @@ -0,0 +1,41 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.ImportCommand; + + +public class ImportCommandParserTest { + private ImportCommandParser parser = new ImportCommandParser(); + @Test + public void parse_noArgsPassed_failure() { + // filePath left empty + assertParseFailure( + parser, "import ", String.format(MESSAGE_INVALID_COMMAND_FORMAT, ImportCommand.MESSAGE_USAGE)); + } + @Test + public void parse_emptyPreamble_failure() { + // filePath left empty + assertParseFailure( + parser, " ", String.format(MESSAGE_INVALID_COMMAND_FORMAT, ImportCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_prefixMissing_failure() { + // filePath left empty + assertParseFailure( + parser, "import go", String.format(MESSAGE_INVALID_COMMAND_FORMAT, ImportCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_notCsvFileGiven_failure() { + // filePath left empty + assertParseFailure( + parser, "import i/notCsv", String.format(MESSAGE_INVALID_COMMAND_FORMAT, ImportCommand.MESSAGE_USAGE)); + } +} + + diff --git a/src/test/java/seedu/address/logic/parser/ImportExamScoresCommandParserTest.java b/src/test/java/seedu/address/logic/parser/ImportExamScoresCommandParserTest.java new file mode 100644 index 00000000000..9717eb3b263 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/ImportExamScoresCommandParserTest.java @@ -0,0 +1,41 @@ +package seedu.address.logic.parser; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.ImportExamScoresCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +public class ImportExamScoresCommandParserTest { + private ImportExamScoresCommandParser parser = new ImportExamScoresCommandParser(); + + @Test + public void parse_noArgsPassed_failure() { + assertParseFailure( + parser, "importExamScores i/", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ImportExamScoresCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_notCsvFile_failure() { + String command = "importExamScores i/file.json"; + assertParseFailure( + parser, command, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ImportExamScoresCommand.MESSAGE_USAGE)); + assertThrows(ParseException.class, () -> parser.parse(command)); + } + + @Test + public void parse_invertedCommas_failure() { + String command = "importExamScores i/\"file.csv\""; + + assertParseFailure( + parser, command, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ImportExamScoresCommand.MESSAGE_USAGE)); + + assertThrows(ParseException.class, () -> parser.parse(command)); + } +} diff --git a/src/test/java/seedu/address/logic/parser/ParserUtilTest.java b/src/test/java/seedu/address/logic/parser/ParserUtilTest.java index 4256788b1a7..a545eb2cf59 100644 --- a/src/test/java/seedu/address/logic/parser/ParserUtilTest.java +++ b/src/test/java/seedu/address/logic/parser/ParserUtilTest.java @@ -18,14 +18,20 @@ import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Phone; +import seedu.address.model.student.Matric; +import seedu.address.model.student.Reflection; +import seedu.address.model.student.Studio; import seedu.address.model.tag.Tag; public class ParserUtilTest { private static final String INVALID_NAME = "R@chel"; - private static final String INVALID_PHONE = "+651234"; + private static final String INVALID_PHONE = "]651234"; private static final String INVALID_ADDRESS = " "; private static final String INVALID_EMAIL = "example.com"; private static final String INVALID_TAG = "#friend"; + private static final String INVALID_MATRIC = "zz"; + private static final String INVALID_REFLECTION = "R"; + private static final String INVALID_STUDIO = "S"; private static final String VALID_NAME = "Rachel Walker"; private static final String VALID_PHONE = "123456"; @@ -33,6 +39,9 @@ public class ParserUtilTest { private static final String VALID_EMAIL = "rachel@example.com"; private static final String VALID_TAG_1 = "friend"; private static final String VALID_TAG_2 = "neighbour"; + private static final String VALID_MATRIC = "A1234567M"; + private static final String VALID_REFLECTION = "R1"; + private static final String VALID_STUDIO = "S1"; private static final String WHITESPACE = " \t\r\n"; @@ -193,4 +202,78 @@ public void parseTags_collectionWithValidTags_returnsTagSet() throws Exception { assertEquals(expectedTagSet, actualTagSet); } + + @Test + public void parseMatric_validMatric_success() throws ParseException { + Matric expected = new Matric(VALID_MATRIC); + assertEquals(expected, ParserUtil.parseMatric(VALID_MATRIC)); + } + + @Test + public void parseMatric_invalidMatric_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.parseMatric(INVALID_MATRIC)); + } + + @Test + public void parseMatricForEdit_invalidMatric_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.parseMatricForEdit(INVALID_MATRIC)); + } + + @Test + public void parseMatric_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> ParserUtil.parseMatric(null)); + } + + @Test + public void parseReflection_validReflection_success() throws ParseException { + Reflection expected = new Reflection(VALID_REFLECTION); + assertEquals(expected, ParserUtil.parseReflection(VALID_REFLECTION)); + } + + @Test + public void parseReflection_invalidReflection_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.parseReflection(INVALID_REFLECTION)); + } + + @Test + public void parseReflection_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> ParserUtil.parseReflection(null)); + } + + @Test + public void parseReflection_optionalFieldMissing_returnsEmptyStudio() throws ParseException { + assertThrows(ParseException.class, () -> ParserUtil.parseReflection("")); + } + + @Test + public void parseReflectionForEdit_invalidReflection_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.parseReflectionForEdit(INVALID_REFLECTION)); + } + + @Test + public void parseStudio_validStudio_success() throws ParseException { + seedu.address.model.student.Studio expected = new Studio(VALID_STUDIO); + assertEquals(expected, ParserUtil.parseStudio(VALID_STUDIO)); + } + + @Test + public void parseStudio_invalidStudio_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.parseStudio(INVALID_STUDIO)); + } + + @Test + public void parseStudio_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> ParserUtil.parseStudio(null)); + } + + @Test + public void parseStudio_optionalFieldMissing_returnsEmptyStudio() throws ParseException { + seedu.address.model.student.Studio expected = new Studio(""); + assertThrows(ParseException.class, () -> ParserUtil.parseStudio("")); + } + + @Test + public void parseStudioForEdit_invalidStudio_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.parseStudioForEdit(INVALID_STUDIO)); + } } diff --git a/src/test/java/seedu/address/logic/parser/SelectExamCommandParserTest.java b/src/test/java/seedu/address/logic/parser/SelectExamCommandParserTest.java new file mode 100644 index 00000000000..a31c451ed50 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/SelectExamCommandParserTest.java @@ -0,0 +1,42 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.SelectExamCommand; + +public class SelectExamCommandParserTest { + + private SelectExamCommandParser parser = new SelectExamCommandParser(); + + @Test + public void parse_validArgs_returnsSelectCommand() { + assertParseSuccess(parser, "1", new SelectExamCommand(Index.fromOneBased(1))); + } + + @Test + public void parse_validArgs_returnsSelectCommand2() { + assertParseSuccess(parser, "2", new SelectExamCommand(Index.fromOneBased(2))); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, SelectExamCommand.MESSAGE_USAGE); + + // no index provided + assertParseFailure(parser, "", expectedMessage); + + // non-integer index + assertParseFailure(parser, "a", expectedMessage); + + // zero index + assertParseFailure(parser, "0", expectedMessage); + + // negative index + assertParseFailure(parser, "-1", expectedMessage); + } +} diff --git a/src/test/java/seedu/address/logic/util/ModelStub.java b/src/test/java/seedu/address/logic/util/ModelStub.java new file mode 100644 index 00000000000..ec296d2901d --- /dev/null +++ b/src/test/java/seedu/address/logic/util/ModelStub.java @@ -0,0 +1,151 @@ +package seedu.address.logic.util; + +import java.nio.file.Path; +import java.util.function.Predicate; + +import javafx.beans.value.ObservableValue; +import javafx.collections.ObservableList; +import seedu.address.commons.core.GuiSettings; +import seedu.address.model.Model; +import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyUserPrefs; +import seedu.address.model.ScoreStatistics; +import seedu.address.model.exam.Exam; +import seedu.address.model.person.Person; +import seedu.address.model.person.Score; + +/** + * A default model stub that have all of the methods failing. + */ +public class ModelStub implements Model { + @Override + public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyUserPrefs getUserPrefs() { + throw new AssertionError("This method should not be called."); + } + + @Override + public GuiSettings getGuiSettings() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setGuiSettings(GuiSettings guiSettings) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Path getAddressBookFilePath() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setAddressBookFilePath(Path addressBookFilePath) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addPerson(Person person) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setAddressBook(ReadOnlyAddressBook newData) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyAddressBook getAddressBook() { + throw new AssertionError("This method should not be called."); + } + + @Override + public boolean hasPerson(Person person) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deletePerson(Person target) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setPerson(Person target, Person editedPerson) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList getFilteredPersonList() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void updateFilteredPersonList(Predicate predicate) { + throw new AssertionError("This method should not be called."); + } + + @Override + public boolean hasExam(Exam exam) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deleteExam(Exam target) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addExam(Exam exam) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList getPersonByEmail(String email) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList getExamByName(String examName) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void selectExam(Exam target) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deselectExam() { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableValue getSelectedExam() { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList getExamList() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addExamScoreToPerson(Person target, Exam exam, Score score) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void removeExamScoreFromPerson(Person target, Exam exam) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableValue getSelectedExamStatistics() { + throw new AssertionError("This method should not be called."); + } + +} diff --git a/src/test/java/seedu/address/model/AddressBookTest.java b/src/test/java/seedu/address/model/AddressBookTest.java index 68c8c5ba4d5..8613cc514d6 100644 --- a/src/test/java/seedu/address/model/AddressBookTest.java +++ b/src/test/java/seedu/address/model/AddressBookTest.java @@ -7,6 +7,7 @@ import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalPersons.ALICE; +import static seedu.address.testutil.TypicalPersons.BOB; import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; import java.util.Arrays; @@ -18,7 +19,10 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import seedu.address.model.exam.Exam; +import seedu.address.model.exam.exceptions.DuplicateExamException; import seedu.address.model.person.Person; +import seedu.address.model.person.Score; import seedu.address.model.person.exceptions.DuplicatePersonException; import seedu.address.testutil.PersonBuilder; @@ -49,27 +53,55 @@ public void resetData_withDuplicatePersons_throwsDuplicatePersonException() { Person editedAlice = new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND) .build(); List newPersons = Arrays.asList(ALICE, editedAlice); - AddressBookStub newData = new AddressBookStub(newPersons); + AddressBookStub newData = new AddressBookStub(newPersons, Collections.emptyList()); assertThrows(DuplicatePersonException.class, () -> addressBook.resetData(newData)); } + @Test + public void resetData_withDuplicateExams_throwsDuplicateExamException() { + // Two exams with the same identity fields + Exam midterm = new Exam("Midterm", new Score(100)); + Exam editedMidterm = new Exam("Midterm", new Score(50)); + List newExams = Arrays.asList(midterm, editedMidterm); + AddressBookStub newData = new AddressBookStub(Collections.emptyList(), newExams); + + assertThrows(DuplicateExamException.class, () -> addressBook.resetData(newData)); + } + @Test public void hasPerson_nullPerson_throwsNullPointerException() { assertThrows(NullPointerException.class, () -> addressBook.hasPerson(null)); } + @Test + public void hasExam_nullExam_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> addressBook.hasExam(null)); + } + @Test public void hasPerson_personNotInAddressBook_returnsFalse() { assertFalse(addressBook.hasPerson(ALICE)); } + @Test + public void hasExam_examNotInAddressBook_returnsFalse() { + assertFalse(addressBook.hasExam(new Exam("Midterm", new Score(100)))); + } + @Test public void hasPerson_personInAddressBook_returnsTrue() { addressBook.addPerson(ALICE); assertTrue(addressBook.hasPerson(ALICE)); } + @Test + public void hasExam_examInAddressBook_returnsTrue() { + Exam midterm = new Exam("Midterm", new Score(100)); + addressBook.addExam(midterm); + assertTrue(addressBook.hasExam(midterm)); + } + @Test public void hasPerson_personWithSameIdentityFieldsInAddressBook_returnsTrue() { addressBook.addPerson(ALICE); @@ -78,31 +110,135 @@ public void hasPerson_personWithSameIdentityFieldsInAddressBook_returnsTrue() { assertTrue(addressBook.hasPerson(editedAlice)); } + @Test + public void hasExam_examWithSameIdentityFieldsInAddressBook_returnsTrue() { + Exam midterm = new Exam("Midterm", new Score(100)); + addressBook.addExam(midterm); + Exam editedMidterm = new Exam("Midterm", new Score(50)); + assertTrue(addressBook.hasExam(editedMidterm)); + } + @Test public void getPersonList_modifyList_throwsUnsupportedOperationException() { assertThrows(UnsupportedOperationException.class, () -> addressBook.getPersonList().remove(0)); } + @Test + public void getExamList_modifyList_throwsUnsupportedOperationException() { + assertThrows(UnsupportedOperationException.class, () -> addressBook.getExamList().remove(0)); + } + @Test public void toStringMethod() { - String expected = AddressBook.class.getCanonicalName() + "{persons=" + addressBook.getPersonList() + "}"; + String expected = AddressBook.class.getCanonicalName() + "{persons=" + addressBook.getPersonList() + + ", exams=" + addressBook.getExamList() + "}"; assertEquals(expected, addressBook.toString()); } + @Test + public void equalsMethod() { + AddressBook addressBook = new AddressBook(); + AddressBook addressBook2 = new AddressBook(); + List persons = Arrays.asList(ALICE); + List exams = Arrays.asList(new Exam("Midterm", new Score(100))); + addressBook.resetData(new AddressBookStub(persons, exams)); + addressBook2.resetData(new AddressBookStub(persons, exams)); + + // same object -> returns true + assertTrue(addressBook.equals(addressBook)); + + // same values -> returns true + assertTrue(addressBook.equals(addressBook2)); + + // different types -> returns false + assertFalse(addressBook.equals(5)); + + // null -> returns false + assertFalse(addressBook.equals(null)); + + // different person -> returns false + List persons2 = Arrays.asList(new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).build()); + addressBook2.resetData(new AddressBookStub(persons2, exams)); + assertFalse(addressBook.equals(addressBook2)); + + // different exam -> returns false + List exams2 = Arrays.asList(new Exam("Midterm", new Score(50))); + addressBook2.resetData(new AddressBookStub(persons, exams2)); + assertFalse(addressBook.equals(addressBook2)); + } + + @Test + public void hashCodeMethod() { + AddressBook addressBook = new AddressBook(); + AddressBook addressBook2 = new AddressBook(); + List persons = Arrays.asList(ALICE); + List exams = Arrays.asList(new Exam("Midterm", new Score(100))); + addressBook.resetData(new AddressBookStub(persons, exams)); + addressBook2.resetData(new AddressBookStub(persons, exams)); + + // same object -> returns true + assertEquals(addressBook.hashCode(), addressBook.hashCode()); + + // same values -> returns true + assertEquals(addressBook.hashCode(), addressBook2.hashCode()); + + // different values -> returns false + List persons2 = Arrays.asList(new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).build()); + addressBook2.resetData(new AddressBookStub(persons2, exams)); + assertFalse(addressBook.hashCode() == addressBook2.hashCode()); + } + /** * A stub ReadOnlyAddressBook whose persons list can violate interface constraints. */ private static class AddressBookStub implements ReadOnlyAddressBook { private final ObservableList persons = FXCollections.observableArrayList(); + private final ObservableList exams = FXCollections.observableArrayList(); - AddressBookStub(Collection persons) { + AddressBookStub(Collection persons, Collection exams) { this.persons.setAll(persons); + this.exams.setAll(exams); } @Override public ObservableList getPersonList() { return persons; } + + @Override + public ObservableList getExamList() { + return exams; + } + } + + @Test + public void getPersonByEmail_emailDoesNotExist() { + ObservableList expected = FXCollections.observableArrayList(); + assertEquals(expected, addressBook.getPersonByEmail("this person does not exist")); + } + + @Test + public void getPersonByEmail_emailExists() { + addressBook.addPerson(BOB); + addressBook.addPerson(ALICE); + ObservableList expected = FXCollections.observableArrayList(ALICE); + assertEquals(expected, addressBook.getPersonByEmail(ALICE.getEmail().toString())); + } + + @Test + public void getExamByName_examExists() { + Exam exam2 = new Exam("Midterm2", new Score(100)); + Exam midterm = new Exam("Midterm", new Score(100)); + addressBook.addExam(exam2); + addressBook.addExam(midterm); + ObservableList expected = FXCollections.observableArrayList(midterm); + assertEquals(expected, addressBook.getExamByName("Midterm")); + } + + @Test + public void getExamByName_examDoesNotExist() { + ObservableList expected = FXCollections.observableArrayList(); + assertEquals(expected, addressBook.getExamByName("this exam does not exist")); } } diff --git a/src/test/java/seedu/address/model/ModelManagerTest.java b/src/test/java/seedu/address/model/ModelManagerTest.java index 2cf1418d116..f6d2fe50977 100644 --- a/src/test/java/seedu/address/model/ModelManagerTest.java +++ b/src/test/java/seedu/address/model/ModelManagerTest.java @@ -1,21 +1,32 @@ package seedu.address.model; +import static org.junit.Assert.assertNull; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalPersons.ALICE; import static seedu.address.testutil.TypicalPersons.BENSON; +import static seedu.address.testutil.TypicalPersons.CARL; +import static seedu.address.testutil.TypicalPersons.DANIEL; +import static seedu.address.testutil.TypicalPersons.ELLE; +import static seedu.address.testutil.TypicalPersons.FIONA; +import static seedu.address.testutil.TypicalPersons.GEORGE; +import static seedu.address.testutil.TypicalPersons.MIDTERM; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Arrays; import org.junit.jupiter.api.Test; import seedu.address.commons.core.GuiSettings; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.exam.Exam; +import seedu.address.model.person.Person; +import seedu.address.model.person.PersonDetailPredicate; +import seedu.address.model.person.Score; import seedu.address.testutil.AddressBookBuilder; public class ModelManagerTest { @@ -77,22 +88,184 @@ public void hasPerson_nullPerson_throwsNullPointerException() { assertThrows(NullPointerException.class, () -> modelManager.hasPerson(null)); } + @Test + public void hasExam_nullExam_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> modelManager.hasExam(null)); + } + @Test public void hasPerson_personNotInAddressBook_returnsFalse() { assertFalse(modelManager.hasPerson(ALICE)); } + @Test + public void hasExam_examNotInAddressBook_returnsFalse() { + assertFalse(modelManager.hasExam(new Exam("Midterm", new Score(100)))); + } + @Test public void hasPerson_personInAddressBook_returnsTrue() { modelManager.addPerson(ALICE); assertTrue(modelManager.hasPerson(ALICE)); } + @Test + public void hasExam_examInAddressBook_returnsTrue() { + Exam midterm = new Exam("Midterm", new Score(100)); + modelManager.addExam(midterm); + assertTrue(modelManager.hasExam(midterm)); + } + @Test public void getFilteredPersonList_modifyList_throwsUnsupportedOperationException() { assertThrows(UnsupportedOperationException.class, () -> modelManager.getFilteredPersonList().remove(0)); } + @Test + public void getFilteredExamList_modifyList_throwsUnsupportedOperationException() { + assertThrows(UnsupportedOperationException.class, () -> modelManager.getExamList().remove(0)); + } + + @Test + public void selectExam_deselectExam_getSelectedExam() { + Exam exam = new Exam("Midterm", new Score(100)); + + // initially, no exam is selected + assertNull(modelManager.getSelectedExam().getValue()); + + // select an exam + modelManager.selectExam(exam); + assertEquals(exam, modelManager.getSelectedExam().getValue()); + + // deselect the exam + modelManager.deselectExam(); + assertNull(modelManager.getSelectedExam().getValue()); + } + + @Test + public void deleteExam_examInAddressBook_removesExamFromPersons() { + Exam midterm = new Exam("Midterm", new Score(100)); + modelManager.addExam(midterm); + modelManager.addPerson(ALICE); + modelManager.selectExam(midterm); + + // add score for ALICE + modelManager.addExamScoreToPerson(ALICE, midterm, new Score(85)); + + // delete the exam + modelManager.deleteExam(midterm); + + // ALICE should not have the exam in her scores + assertFalse(modelManager.getFilteredPersonList().get(0).getScores().containsKey(midterm)); + } + + @Test + public void getExamScoreStatistics_scoresInEvenPersonsInAddressBook_success() { + ModelManager modelManager = new ModelManager(); + + // Add some persons with scores + Person daniel = DANIEL; + Person elle = ELLE; + Person fiona = FIONA; + Person george = GEORGE; + + modelManager.addPerson(daniel); + modelManager.addPerson(elle); + modelManager.addPerson(fiona); + modelManager.addPerson(george); + + Exam midterm = MIDTERM; + + // Calculate statistics + modelManager.selectExam(midterm); + ScoreStatistics stats = modelManager.getSelectedExamStatistics().getValue(); + + // Verify the statistics + assertEquals(55, stats.getMean()); + assertEquals(55, stats.getMedian()); + } + + @Test + public void getExamScoreStatistics_scoresInAllPersonsInAddressBook_success() { + ModelManager modelManager = new ModelManager(); + + // Add some persons with scores + Person carl = CARL; + Person daniel = DANIEL; + Person elle = ELLE; + Person fiona = FIONA; + Person george = GEORGE; + + modelManager.addPerson(carl); + modelManager.addPerson(daniel); + modelManager.addPerson(elle); + modelManager.addPerson(fiona); + modelManager.addPerson(george); + + Exam midterm = MIDTERM; + + // Calculate statistics + modelManager.selectExam(midterm); + ScoreStatistics stats = modelManager.getSelectedExamStatistics().getValue(); + + // Verify the statistics + assertEquals(50, stats.getMean()); + assertEquals(50, stats.getMedian()); + } + + @Test + public void getExamScoreStatistics_someScoresInPersonsInAddressBook_success() { + + ModelManager modelManager = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + Exam midterm = MIDTERM; + + // Calculate statistics + modelManager.selectExam(midterm); + ScoreStatistics stats = modelManager.getSelectedExamStatistics().getValue(); + + // Verify the statistics + assertEquals(50, stats.getMean()); + assertEquals(50, stats.getMedian()); + } + + @Test + public void getExamScoreStatistics_noScoresInPersonsInAddressBook_success() { + ModelManager modelManager = new ModelManager(); + + // Add some persons without scores + Person alice = ALICE; + Person benson = BENSON; + + modelManager.addPerson(alice); + modelManager.addPerson(benson); + + Exam midterm = MIDTERM; + + // Calculate statistics + modelManager.selectExam(midterm); + ScoreStatistics stats = modelManager.getSelectedExamStatistics().getValue(); + + // Verify the statistics + assertEquals(-1, stats.getMean()); + assertEquals(-1, stats.getMedian()); + } + + @Test + public void getExamScoreStatistics_noPersonsInAddressBook_success() { + ModelManager modelManager = new ModelManager(); + + Exam midterm = MIDTERM; + + // Calculate statistics + modelManager.selectExam(midterm); + ScoreStatistics stats = modelManager.getSelectedExamStatistics().getValue(); + + // Verify the statistics + assertEquals(-1, stats.getMean()); + assertEquals(-1, stats.getMedian()); + } + @Test public void equals() { AddressBook addressBook = new AddressBookBuilder().withPerson(ALICE).withPerson(BENSON).build(); @@ -117,8 +290,8 @@ public void equals() { assertFalse(modelManager.equals(new ModelManager(differentAddressBook, userPrefs))); // different filteredList -> returns false - String[] keywords = ALICE.getName().fullName.split("\\s+"); - modelManager.updateFilteredPersonList(new NameContainsKeywordsPredicate(Arrays.asList(keywords))); + modelManager.updateFilteredPersonList( + new PersonDetailPredicate(PREFIX_NAME, ALICE.getName().fullName)); assertFalse(modelManager.equals(new ModelManager(addressBook, userPrefs))); // resets modelManager to initial state for upcoming tests diff --git a/src/test/java/seedu/address/model/ScoreStatisticsTest.java b/src/test/java/seedu/address/model/ScoreStatisticsTest.java new file mode 100644 index 00000000000..dc7bd9aa1f4 --- /dev/null +++ b/src/test/java/seedu/address/model/ScoreStatisticsTest.java @@ -0,0 +1,33 @@ +package seedu.address.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + + +class ScoreStatisticsTest { + + @Test + void testConstructor() { + ScoreStatistics stats = new ScoreStatistics(50.0, 50.0); + assertEquals(50.0, stats.getMean()); + assertEquals(50.0, stats.getMedian()); + } + + @Test + void testNoScoresConstructor() { + ScoreStatistics stats = new ScoreStatistics(); + assertEquals(-1, stats.getMean()); + assertEquals(-1, stats.getMedian()); + } + + @Test + void testToString() { + ScoreStatistics stats = new ScoreStatistics(50.0, 50.0); + String expected = "Mean: 50, Median: 50"; + assertEquals(expected, stats.toString()); + + ScoreStatistics noScoresStats = new ScoreStatistics(); + assertEquals("No scores available", noScoresStats.toString()); + } +} diff --git a/src/test/java/seedu/address/model/exam/ExamTest.java b/src/test/java/seedu/address/model/exam/ExamTest.java new file mode 100644 index 00000000000..bd39572e004 --- /dev/null +++ b/src/test/java/seedu/address/model/exam/ExamTest.java @@ -0,0 +1,64 @@ +package seedu.address.model.exam; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.person.Score; + +public class ExamTest { + + @Test + public void isValidName() { + assertTrue(Exam.isValidExamName("Midterm")); // alphanumeric characters + assertTrue(Exam.isValidExamName("Midterm Exam")); // alphanumeric characters with spaces + assertFalse(Exam.isValidExamName("")); // empty string + assertFalse(Exam.isValidExamName(" ")); // spaces only + assertFalse(Exam.isValidExamName("^")); // contains non-alphanumeric characters + assertFalse(Exam.isValidExamName("Midterm*")); // contains non-alphanumeric characters + } + + @Test + public void isValidExamScoreString() { + assertTrue(Exam.isValidExamScoreString("1")); // positive score + assertFalse(Exam.isValidExamScoreString("0")); // zero score + assertFalse(Exam.isValidExamScoreString("-1")); // negative score + assertFalse(Exam.isValidExamScoreString("")); // empty string + assertTrue(Exam.isValidExamScoreString("1.2")); // one decimal place + assertTrue(Exam.isValidExamScoreString("1.23")); // two decimal places + assertTrue(Exam.isValidExamScoreString("0.1")); // one decimal place w zero + assertFalse(Exam.isValidExamScoreString("1.")); // no decimal places but with decimal point + assertFalse(Exam.isValidExamScoreString("1.234")); // three decimal places + assertFalse(Exam.isValidExamScoreString("1.2345")); // four decimal places + + } + + @Test + public void isSameExam() { + Exam exam1 = new Exam("Midterm", new Score(100)); + Exam exam2 = new Exam("Midterm", new Score(100)); + Exam exam3 = new Exam("Final", new Score(100)); + + assertTrue(exam1.isSameExam(exam2)); // same name, same score + assertFalse(exam1.isSameExam(exam3)); // different name, same score + } + + @Test + public void equals() { + Exam exam1 = new Exam("Midterm", new Score(100)); + Exam exam2 = new Exam("Midterm", new Score(100)); + Exam exam3 = new Exam("Final", new Score(100)); + + assertEquals(exam1, exam2); // same name, same score + assertFalse(exam1.equals(exam3)); // different name, same score + } + + @Test + public void hashCodeTest() { + Exam exam = new Exam("Midterm", new Score(100)); + int expectedHashCode = exam.getName().hashCode() + exam.getMaxScore().hashCode(); + assertEquals(expectedHashCode, exam.hashCode()); + } +} diff --git a/src/test/java/seedu/address/model/exam/UniqueExamListTest.java b/src/test/java/seedu/address/model/exam/UniqueExamListTest.java new file mode 100644 index 00000000000..9a6a1a4efd8 --- /dev/null +++ b/src/test/java/seedu/address/model/exam/UniqueExamListTest.java @@ -0,0 +1,92 @@ +package seedu.address.model.exam; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.exam.exceptions.DuplicateExamException; +import seedu.address.model.exam.exceptions.ExamNotFoundException; +import seedu.address.model.person.Score; + +public class UniqueExamListTest { + + private final UniqueExamList uniqueExamList = new UniqueExamList(); + + @Test + public void contains_nullExam_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueExamList.contains(null)); + } + + @Test + public void contains_examNotInList_returnsFalse() { + Exam exam = new Exam("Midterm", new Score(100)); + assertFalse(uniqueExamList.contains(exam)); + } + + @Test + public void contains_examInList_returnsTrue() { + Exam exam = new Exam("Midterm", new Score(100)); + uniqueExamList.add(exam); + assertTrue(uniqueExamList.contains(exam)); + } + + @Test + public void add_nullExam_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueExamList.add(null)); + } + + @Test + public void add_duplicateExam_throwsDuplicateExamException() { + Exam exam = new Exam("Midterm", new Score(100)); + uniqueExamList.add(exam); + assertThrows(DuplicateExamException.class, () -> uniqueExamList.add(exam)); + } + + @Test + public void remove_nullExam_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueExamList.remove(null)); + } + + @Test + public void remove_examDoesNotExist_throwsExamNotFoundException() { + Exam exam = new Exam("Midterm", new Score(100)); + assertThrows(ExamNotFoundException.class, () -> uniqueExamList.remove(exam)); + } + + @Test + public void remove_existingExam_removesExam() { + Exam exam = new Exam("Midterm", new Score(100)); + uniqueExamList.add(exam); + uniqueExamList.remove(exam); + assertFalse(uniqueExamList.contains(exam)); + } + + @Test + public void setExams_nullList_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueExamList.setExams((List) null)); + } + + @Test + public void setExams_list_replacesOwnListWithProvidedList() { + Exam examA = new Exam("Midterm", new Score(100)); + Exam examB = new Exam("Final", new Score(200)); + uniqueExamList.add(examA); + List examList = Collections.singletonList(examB); + uniqueExamList.setExams(examList); + assertFalse(uniqueExamList.contains(examA)); + assertTrue(uniqueExamList.contains(examB)); + } + + @Test + public void setExams_listWithDuplicateExams_throwsDuplicateExamException() { + Exam exam = new Exam("Midterm", new Score(100)); + List listWithDuplicateExams = Arrays.asList(exam, exam); + assertThrows(DuplicateExamException.class, () -> uniqueExamList.setExams(listWithDuplicateExams)); + } +} diff --git a/src/test/java/seedu/address/model/person/EmailTest.java b/src/test/java/seedu/address/model/person/EmailTest.java index f08cdff0a64..98d4a086d88 100644 --- a/src/test/java/seedu/address/model/person/EmailTest.java +++ b/src/test/java/seedu/address/model/person/EmailTest.java @@ -66,6 +66,12 @@ public void isValidEmail() { assertTrue(Email.isValidEmail("e1234567@u.nus.edu")); // more than one period in domain } + @Test + public void testEmailLength_fail() { + String test = "a".repeat(101) + "@example.com"; + assertFalse(Email.isValidEmail(test)); + } + @Test public void equals() { Email email = new Email("valid@email"); diff --git a/src/test/java/seedu/address/model/person/ExamPredicateTest.java b/src/test/java/seedu/address/model/person/ExamPredicateTest.java new file mode 100644 index 00000000000..e7834dad17f --- /dev/null +++ b/src/test/java/seedu/address/model/person/ExamPredicateTest.java @@ -0,0 +1,135 @@ +package seedu.address.model.person; + + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LESS_THAN; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MORE_THAN; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.testutil.TypicalPersons; + +public class ExamPredicateTest { + + @Test + public void equals() { + String firstPredicateKeyword = "55"; + String secondPredicateKeyword = "45"; + + ExamPredicate firstPredicate = + new ExamPredicate( + PREFIX_LESS_THAN, firstPredicateKeyword, TypicalPersons.MIDTERM); + ExamPredicate secondPredicate = + new ExamPredicate( + PREFIX_LESS_THAN, secondPredicateKeyword, TypicalPersons.MIDTERM); + ExamPredicate thirdPredicate = + new ExamPredicate( + PREFIX_MORE_THAN, firstPredicateKeyword, TypicalPersons.MIDTERM); + ExamPredicate fourthPredicate = + new ExamPredicate( + PREFIX_MORE_THAN, secondPredicateKeyword, TypicalPersons.MIDTERM); + + // same object -> returns true + assertEquals(firstPredicate, firstPredicate); + + // different types -> returns false + assertFalse(firstPredicate.equals(1)); + + // null -> returns false + assertFalse(firstPredicate.equals(null)); + + // same values -> returns true + ExamPredicate firstPredicateCopy = + new ExamPredicate( + PREFIX_LESS_THAN, firstPredicateKeyword, TypicalPersons.MIDTERM); + + assertEquals(firstPredicate, firstPredicateCopy); + + // same prefix, different keyword -> returns false + assertFalse(firstPredicate.equals(secondPredicate)); + + // same keyword, different prefix -> returns false + assertFalse(firstPredicate.equals(thirdPredicate)); + + // different prefix and keyword -> returns false + assertFalse(firstPredicate.equals(fourthPredicate)); + } + + @Test + public void toString_validParams_returnsCorrectString() { + String keyword = "55"; + ExamPredicate predicate = + new ExamPredicate( + PREFIX_LESS_THAN, keyword, TypicalPersons.MIDTERM); + + String expectedString = new ToStringBuilder("seedu.address.model.person." + + "ExamPredicate") + .add("prefix", PREFIX_LESS_THAN) + .add("keyword", keyword) + .add("exam", TypicalPersons.MIDTERM) + .toString(); + + assertEquals(expectedString, predicate.toString()); + } + + @Test + public void test_prefixGreaterThanKeyword_returnsTrue() { + // One keyword + ExamPredicate predicate = + new ExamPredicate( + PREFIX_MORE_THAN, "55", TypicalPersons.MIDTERM); + + // keyword matches score + assertTrue(predicate.test(TypicalPersons.FIONA)); + + // keyword does not match score + assertFalse(predicate.test(TypicalPersons.ELLE)); + } + + @Test + public void test_examNotFound_returnsFalse() { + // One keyword + ExamPredicate predicate = + new ExamPredicate( + PREFIX_MORE_THAN, "55", TypicalPersons.QUIZ); + + // keyword matches score for different exam + assertFalse(predicate.test(TypicalPersons.FIONA)); + + // keyword does not match score for different exam + assertFalse(predicate.test(TypicalPersons.ELLE)); + } + + @Test + public void test_wrongPrefix_returnsFalse() { + // One keyword + ExamPredicate predicate = + new ExamPredicate( + PREFIX_NAME, "55", TypicalPersons.MIDTERM); + + // should not match + assertFalse(predicate.test(TypicalPersons.FIONA)); + } + + @Test + public void isExamRequired_validPrefixes_returnsTrue() { + ExamPredicate predicate = + new ExamPredicate( + PREFIX_MORE_THAN, "55", TypicalPersons.MIDTERM); + + assertTrue(predicate.isExamRequired()); + } + + @Test + public void isExamRequired_invalidPrefixes_returnsFalse() { + ExamPredicate predicate = + new ExamPredicate( + PREFIX_NAME, "55", TypicalPersons.MIDTERM); + + assertFalse(predicate.isExamRequired()); + } +} diff --git a/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java b/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java deleted file mode 100644 index 6b3fd90ade7..00000000000 --- a/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java +++ /dev/null @@ -1,85 +0,0 @@ -package seedu.address.model.person; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import org.junit.jupiter.api.Test; - -import seedu.address.testutil.PersonBuilder; - -public class NameContainsKeywordsPredicateTest { - - @Test - public void equals() { - List firstPredicateKeywordList = Collections.singletonList("first"); - List secondPredicateKeywordList = Arrays.asList("first", "second"); - - NameContainsKeywordsPredicate firstPredicate = new NameContainsKeywordsPredicate(firstPredicateKeywordList); - NameContainsKeywordsPredicate secondPredicate = new NameContainsKeywordsPredicate(secondPredicateKeywordList); - - // same object -> returns true - assertTrue(firstPredicate.equals(firstPredicate)); - - // same values -> returns true - NameContainsKeywordsPredicate firstPredicateCopy = new NameContainsKeywordsPredicate(firstPredicateKeywordList); - assertTrue(firstPredicate.equals(firstPredicateCopy)); - - // different types -> returns false - assertFalse(firstPredicate.equals(1)); - - // null -> returns false - assertFalse(firstPredicate.equals(null)); - - // different person -> returns false - assertFalse(firstPredicate.equals(secondPredicate)); - } - - @Test - public void test_nameContainsKeywords_returnsTrue() { - // One keyword - NameContainsKeywordsPredicate predicate = new NameContainsKeywordsPredicate(Collections.singletonList("Alice")); - assertTrue(predicate.test(new PersonBuilder().withName("Alice Bob").build())); - - // Multiple keywords - predicate = new NameContainsKeywordsPredicate(Arrays.asList("Alice", "Bob")); - assertTrue(predicate.test(new PersonBuilder().withName("Alice Bob").build())); - - // Only one matching keyword - predicate = new NameContainsKeywordsPredicate(Arrays.asList("Bob", "Carol")); - assertTrue(predicate.test(new PersonBuilder().withName("Alice Carol").build())); - - // Mixed-case keywords - predicate = new NameContainsKeywordsPredicate(Arrays.asList("aLIce", "bOB")); - assertTrue(predicate.test(new PersonBuilder().withName("Alice Bob").build())); - } - - @Test - public void test_nameDoesNotContainKeywords_returnsFalse() { - // Zero keywords - NameContainsKeywordsPredicate predicate = new NameContainsKeywordsPredicate(Collections.emptyList()); - assertFalse(predicate.test(new PersonBuilder().withName("Alice").build())); - - // Non-matching keyword - predicate = new NameContainsKeywordsPredicate(Arrays.asList("Carol")); - assertFalse(predicate.test(new PersonBuilder().withName("Alice Bob").build())); - - // Keywords match phone, email and address, but does not match name - predicate = new NameContainsKeywordsPredicate(Arrays.asList("12345", "alice@email.com", "Main", "Street")); - assertFalse(predicate.test(new PersonBuilder().withName("Alice").withPhone("12345") - .withEmail("alice@email.com").withAddress("Main Street").build())); - } - - @Test - public void toStringMethod() { - List keywords = List.of("keyword1", "keyword2"); - NameContainsKeywordsPredicate predicate = new NameContainsKeywordsPredicate(keywords); - - String expected = NameContainsKeywordsPredicate.class.getCanonicalName() + "{keywords=" + keywords + "}"; - assertEquals(expected, predicate.toString()); - } -} diff --git a/src/test/java/seedu/address/model/person/PersonDetailPredicateTest.java b/src/test/java/seedu/address/model/person/PersonDetailPredicateTest.java new file mode 100644 index 00000000000..6c51616181e --- /dev/null +++ b/src/test/java/seedu/address/model/person/PersonDetailPredicateTest.java @@ -0,0 +1,286 @@ +package seedu.address.model.person; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MATRIC_NUMBER; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_REFLECTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_STUDIO; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.parser.Prefix; +import seedu.address.testutil.PersonBuilder; + +public class PersonDetailPredicateTest { + + @Test + public void equals() { + String firstPredicateKeyword = "first"; + String secondPredicateKeyword = "second"; + + PersonDetailPredicate firstPredicate = + new PersonDetailPredicate(PREFIX_NAME, firstPredicateKeyword); + PersonDetailPredicate secondPredicate = + new PersonDetailPredicate(PREFIX_NAME, secondPredicateKeyword); + PersonDetailPredicate thirdPredicate = + new PersonDetailPredicate(PREFIX_EMAIL, firstPredicateKeyword); + PersonDetailPredicate fourthPredicate = + new PersonDetailPredicate(PREFIX_EMAIL, secondPredicateKeyword); + + // same object -> returns true + assertTrue(firstPredicate.equals(firstPredicate)); + + // different types -> returns false + assertFalse(firstPredicate.equals(1)); + + // null -> returns false + assertFalse(firstPredicate.equals(null)); + + // same values -> returns true + PersonDetailPredicate firstPredicateCopy = + new PersonDetailPredicate(PREFIX_NAME, firstPredicateKeyword); + assertTrue(firstPredicate.equals(firstPredicateCopy)); + + // same prefix, different keyword -> returns false + assertFalse(firstPredicate.equals(secondPredicate)); + + // same keyword, different prefix -> returns false + assertFalse(firstPredicate.equals(thirdPredicate)); + + // different prefix and keyword -> returns false + assertFalse(firstPredicate.equals(fourthPredicate)); + + } + + @Test + public void test_nameContainsKeyword_returnsTrue() { + // One keyword + PersonDetailPredicate predicate = + new PersonDetailPredicate(PREFIX_NAME, "Alice"); + + assertTrue(predicate.test(new PersonBuilder().withName("Alice Bob").build())); + // Mixed-case keywords + predicate = new PersonDetailPredicate(PREFIX_NAME, "aLiCe"); + assertTrue(predicate.test(new PersonBuilder().withName("Alice Bob").build())); + } + + @Test + public void test_nameDoesNotContainKeyword_returnsFalse() { + // Non-matching keyword + PersonDetailPredicate predicate = + new PersonDetailPredicate(PREFIX_NAME, "Adam"); + assertFalse(predicate.test(new PersonBuilder().withName("Alice Bob").build())); + + // Keywords match email, but does not match name + predicate = new PersonDetailPredicate(PREFIX_NAME, "alice@gmail.com"); + assertFalse(predicate.test(new PersonBuilder().withName("Alice").withPhone("12345") + .withEmail("alice@gmail.com").withAddress("Main Street").build())); + } + + @Test + public void test_emailContainsKeyword_returnsTrue() { + // One keyword + PersonDetailPredicate predicate = + new PersonDetailPredicate(PREFIX_EMAIL, "alice@gmail.com"); + + assertTrue(predicate.test(new PersonBuilder().withEmail("alice@gmail.com").build())); + // Mixed-case keywords + predicate = new PersonDetailPredicate(PREFIX_EMAIL, "aLiCe@GMAIl.cOm"); + assertTrue(predicate.test(new PersonBuilder().withEmail("alice@gmail.com").build())); + } + + @Test + public void test_emailDoesNotContainKeyword_returnsFalse() { + // Non-matching keyword + PersonDetailPredicate predicate = + new PersonDetailPredicate(PREFIX_EMAIL, "adam@gmail.com"); + assertFalse(predicate.test(new PersonBuilder().withEmail("alice@gmail.com").build())); + + // Keywords match name, but does not match email + predicate = new PersonDetailPredicate(PREFIX_EMAIL, "Alice"); + assertFalse(predicate.test(new PersonBuilder().withName("Alice").withPhone("12345") + .withEmail("alisu@gmail.com").withAddress("Main Street").build())); + } + + @Test + public void test_phoneContainsKeyword_returnsTrue() { + // One keyword + PersonDetailPredicate predicate = + new PersonDetailPredicate(PREFIX_PHONE, "123456"); + + assertTrue(predicate.test(new PersonBuilder().withPhone("123456").build())); + } + + @Test + public void test_phoneDoesNotContainKeyword_returnsFalse() { + // Non-matching keyword + PersonDetailPredicate predicate = + new PersonDetailPredicate(PREFIX_PHONE, "123456"); + + assertFalse(predicate.test(new PersonBuilder().withPhone("1232346").build())); + + // Keywords match email, but does not match phone + predicate = new PersonDetailPredicate(PREFIX_PHONE, "alice@gmail.com"); + assertFalse(predicate.test(new PersonBuilder().withName("Alice").withPhone("12345") + .withEmail("alice@gmail.com").withAddress("Main Street").build())); + } + + @Test + public void test_addressContainsKeyword_returnsTrue() { + // One keyword + PersonDetailPredicate predicate = + new PersonDetailPredicate(PREFIX_ADDRESS, "Main Street"); + + assertTrue(predicate.test(new PersonBuilder().withAddress("Main Street").build())); + // Mixed-case keywords + predicate = new PersonDetailPredicate(PREFIX_ADDRESS, "mAin STReet"); + assertTrue(predicate.test(new PersonBuilder().withAddress("Main Street").build())); + } + + @Test + public void test_addressDoesNotContainKeyword_returnsFalse() { + // Non-matching keyword + PersonDetailPredicate predicate = + new PersonDetailPredicate(PREFIX_ADDRESS, "Side Street"); + + assertFalse(predicate.test(new PersonBuilder().withAddress("Main Street").build())); + + // Keywords match phone, but does not match address + predicate = new PersonDetailPredicate(PREFIX_ADDRESS, "12345"); + assertFalse(predicate.test(new PersonBuilder().withName("Alice").withPhone("12345") + .withEmail("alice@email.com").withAddress("Main Street").build())); + } + + @Test + public void test_tagContainsKeyword_returnsTrue() { + // One keyword + PersonDetailPredicate predicate = + new PersonDetailPredicate(PREFIX_TAG, "Friend"); + + assertTrue(predicate.test(new PersonBuilder().withTags("Friend").build())); + // Mixed-case keywords + predicate = new PersonDetailPredicate(PREFIX_TAG, "fRieNd"); + assertTrue(predicate.test(new PersonBuilder().withTags("Friend").build())); + } + + @Test + public void test_tagDoesNotContainKeyword_returnsFalse() { + // Non-matching keyword + PersonDetailPredicate predicate = + new PersonDetailPredicate(PREFIX_TAG, "Friend"); + + assertFalse(predicate.test(new PersonBuilder().withTags("Family").build())); + + // Keywords match address, but does not match tag + predicate = new PersonDetailPredicate(PREFIX_TAG, "Main Street"); + assertFalse(predicate.test(new PersonBuilder().withName("Alice").withPhone("12345") + .withEmail("alice@email.com").withAddress("Main Street").withTags("Friend").build())); + } + + @Test + public void test_matricContainsKeyword_returnsTrue() { + // One keyword + PersonDetailPredicate predicate = + new PersonDetailPredicate(PREFIX_MATRIC_NUMBER, "A1234567A"); + + assertTrue(predicate.test(new PersonBuilder().withMatric("A1234567A").build())); + // Mixed-case keywords + predicate = new PersonDetailPredicate(PREFIX_MATRIC_NUMBER, "a1234567a"); + assertTrue(predicate.test(new PersonBuilder().withMatric("A1234567A").build())); + } + + @Test + public void test_matricDoesNotContainKeyword_returnsFalse() { + // Non-matching keyword + PersonDetailPredicate predicate = + new PersonDetailPredicate(PREFIX_MATRIC_NUMBER, "A1234567A"); + + assertFalse(predicate.test(new PersonBuilder().withMatric("A1234567B").build())); + + // Keywords match tag, but does not match matric + predicate = new PersonDetailPredicate(PREFIX_MATRIC_NUMBER, "Friend"); + assertFalse(predicate.test(new PersonBuilder().withName("Alice").withPhone("12345") + .withEmail("alice@email.com").withAddress("Main Street").withTags("Friend").build())); + } + + @Test + public void test_reflectionContainsKeyword_returnsTrue() { + // One keyword + PersonDetailPredicate predicate = + new PersonDetailPredicate(PREFIX_REFLECTION, "R1"); + + assertTrue(predicate.test(new PersonBuilder().withReflection("R1").build())); + // Mixed-case keywords + predicate = new PersonDetailPredicate(PREFIX_REFLECTION, "r1"); + assertTrue(predicate.test(new PersonBuilder().withReflection("R1").build())); + } + + @Test + public void test_reflectionDoesNotContainKeyword_returnsFalse() { + // Non-matching keyword + PersonDetailPredicate predicate = + new PersonDetailPredicate(PREFIX_REFLECTION, "R1"); + + assertFalse(predicate.test(new PersonBuilder().withReflection("R2").build())); + + // Keywords match matric, but does not match reflection + predicate = new PersonDetailPredicate(PREFIX_REFLECTION, "A1234567A"); + assertFalse(predicate.test(new PersonBuilder().withName("Alice").withPhone("12345") + .withEmail("alice@email.com").withMatric("A1234567A").withReflection("R1").build())); + } + + @Test + public void test_studioContainsKeyword_returnsTrue() { + // One keyword + PersonDetailPredicate predicate = + new PersonDetailPredicate(PREFIX_STUDIO, "S1"); + + assertTrue(predicate.test(new PersonBuilder().withStudio("S1").build())); + // Mixed-case keywords + predicate = new PersonDetailPredicate(PREFIX_STUDIO, "s1"); + assertTrue(predicate.test(new PersonBuilder().withStudio("S1").build())); + } + + @Test + public void test_studioDoesNotContainKeyword_returnsFalse() { + // Non-matching keyword + PersonDetailPredicate predicate = + new PersonDetailPredicate(PREFIX_STUDIO, "S1"); + + assertFalse(predicate.test(new PersonBuilder().withStudio("S2").build())); + + // Keywords match reflection, but does not match studio + predicate = new PersonDetailPredicate(PREFIX_STUDIO, "R1"); + assertFalse(predicate.test(new PersonBuilder().withName("Alice").withPhone("12345") + .withEmail("alice@email.com").withReflection("R1").withStudio("S1").build())); + } + + @Test + public void test_invalidPrefix_returnsFalse() { + // Non-matching keyword + + Prefix invalidPrefix = new Prefix("i/"); + PersonDetailPredicate predicate = + new PersonDetailPredicate(invalidPrefix, "abc"); + + assertFalse(predicate.test(new PersonBuilder().withTags("Family").build())); + } + + @Test + public void toStringMethod() { + String keyword = "example"; + PersonDetailPredicate predicate = + new PersonDetailPredicate(PREFIX_NAME, keyword); + + String expected = PersonDetailPredicate.class.getCanonicalName() + + "{prefix=" + PREFIX_NAME + ", " + + "keyword=" + keyword + "}"; + assertEquals(expected, predicate.toString()); + } +} diff --git a/src/test/java/seedu/address/model/person/PersonTest.java b/src/test/java/seedu/address/model/person/PersonTest.java index 31a10d156c9..03e24ea7136 100644 --- a/src/test/java/seedu/address/model/person/PersonTest.java +++ b/src/test/java/seedu/address/model/person/PersonTest.java @@ -11,6 +11,10 @@ import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalPersons.ALICE; import static seedu.address.testutil.TypicalPersons.BOB; +import static seedu.address.testutil.TypicalPersons.MIDTERM; + +import java.util.Collections; +import java.util.Objects; import org.junit.jupiter.api.Test; @@ -32,23 +36,18 @@ public void isSamePerson() { // null -> returns false assertFalse(ALICE.isSamePerson(null)); - // same name, all other attributes different -> returns true - Person editedAlice = new PersonBuilder(ALICE).withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_BOB) + // same email, all other attributes different -> returns true + Person editedAlice = new PersonBuilder(ALICE).withName(VALID_NAME_BOB).withPhone(VALID_PHONE_BOB) .withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND).build(); assertTrue(ALICE.isSamePerson(editedAlice)); - // different name, all other attributes same -> returns false - editedAlice = new PersonBuilder(ALICE).withName(VALID_NAME_BOB).build(); + // different email, all other attributes same -> returns false + editedAlice = new PersonBuilder(ALICE).withEmail(VALID_EMAIL_BOB).build(); assertFalse(ALICE.isSamePerson(editedAlice)); - // name differs in case, all other attributes same -> returns false - Person editedBob = new PersonBuilder(BOB).withName(VALID_NAME_BOB.toLowerCase()).build(); - assertFalse(BOB.isSamePerson(editedBob)); - - // name has trailing spaces, all other attributes same -> returns false - String nameWithTrailingSpaces = VALID_NAME_BOB + " "; - editedBob = new PersonBuilder(BOB).withName(nameWithTrailingSpaces).build(); - assertFalse(BOB.isSamePerson(editedBob)); + // email differs in case, all other attributes same -> returns true + Person editedBob = new PersonBuilder(BOB).withEmail(VALID_EMAIL_BOB.toUpperCase()).build(); + assertTrue(BOB.isSamePerson(editedBob)); } @Test @@ -88,12 +87,43 @@ public void equals() { // different tags -> returns false editedAlice = new PersonBuilder(ALICE).withTags(VALID_TAG_HUSBAND).build(); assertFalse(ALICE.equals(editedAlice)); + + // different matriculation number -> returns false + editedAlice = new PersonBuilder(ALICE).withMatric("A1234567A").build(); + assertFalse(ALICE.equals(editedAlice)); + + // different reflection -> returns false + editedAlice = new PersonBuilder(ALICE).withReflection("R10").build(); + assertFalse(ALICE.equals(editedAlice)); + + // different studio -> returns false + editedAlice = new PersonBuilder(ALICE).withStudio("S143").build(); + assertFalse(ALICE.equals(editedAlice)); + + // different scores -> returns false + editedAlice = new PersonBuilder(ALICE).withScores(Collections.singletonMap(MIDTERM, new Score(70))) + .build(); + assertFalse(ALICE.equals(editedAlice)); + } + + @Test + public void hashCodeMethod() { + int expected = Objects.hash( + ALICE.getName(), ALICE.getPhone(), ALICE.getEmail(), + ALICE.getAddress(), ALICE.getTags(), ALICE.getMatric(), + ALICE.getReflection(), ALICE.getStudio(), ALICE.getScores()); + int actual = ALICE.hashCode(); + assertEquals(expected, actual); } @Test public void toStringMethod() { String expected = Person.class.getCanonicalName() + "{name=" + ALICE.getName() + ", phone=" + ALICE.getPhone() - + ", email=" + ALICE.getEmail() + ", address=" + ALICE.getAddress() + ", tags=" + ALICE.getTags() + "}"; + + ", email=" + ALICE.getEmail() + ", address=" + ALICE.getAddress() + + ", tags=" + ALICE.getTags() + ", matriculation number=" + ALICE.getMatric() + + ", reflection=" + ALICE.getReflection() + + ", studio=" + ALICE.getStudio() + + ", scores=" + ALICE.getScores() + "}"; assertEquals(expected, ALICE.toString()); } } diff --git a/src/test/java/seedu/address/model/person/ScoreTest.java b/src/test/java/seedu/address/model/person/ScoreTest.java new file mode 100644 index 00000000000..2b726e6bdea --- /dev/null +++ b/src/test/java/seedu/address/model/person/ScoreTest.java @@ -0,0 +1,87 @@ +package seedu.address.model.person; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public class ScoreTest { + + @Test + public void constructor_invalidScore_throwsIllegalArgumentException() { + int invalidScore = -1; + assertThrows(IllegalArgumentException.class, () -> new Score(invalidScore)); + } + + @Test + public void isValidScore() { + // negative score + assertFalse(Score.isValidScore(-1)); + // zero score + assertTrue(Score.isValidScore(0)); + // positive score + assertTrue(Score.isValidScore(1)); + // zero with 1 decimal place + assertTrue(Score.isValidScore(0.1)); + // zero with 2 decimal places + assertTrue(Score.isValidScore(0.01)); + // zero with 3 decimal places + assertFalse(Score.isValidScore(0.001)); + // negative with 1 decimal place + assertFalse(Score.isValidScore(-0.1)); + // negative with 2 decimal places + assertFalse(Score.isValidScore(-0.01)); + // 1 with 1 decimal place + assertTrue(Score.isValidScore(1.1)); + // 1 with 2 decimal places + assertTrue(Score.isValidScore(1.01)); + // 1 with 3 decimal places + assertFalse(Score.isValidScore(1.001)); + + } + + @Test + public void getScore() { + int scoreValue = 100; + Score score = new Score(scoreValue); + assertEquals(scoreValue, score.getScore()); + } + + @Test + public void testToString() { + double scoreValue = 100; + Score score = new Score(scoreValue); + assertEquals("100", score.toString()); + } + + @Test + public void testEquals() { + Score scoreA = new Score(100); + Score scoreB = new Score(100); + Score scoreC = new Score(200); + + // same object -> returns true + assertTrue(scoreA.equals(scoreA)); + + // same values -> returns true + assertTrue(scoreA.equals(scoreB)); + + // different types -> returns false + assertFalse(scoreA.equals(1)); + + // null -> returns false + assertFalse(scoreA.equals(null)); + + // different score -> returns false + assertFalse(scoreA.equals(scoreC)); + } + + @Test + public void testHashCode() { + double scoreValue = 100.25; + Score score = new Score(scoreValue); + assertEquals(Double.hashCode(scoreValue), score.hashCode()); + } +} diff --git a/src/test/java/seedu/address/model/student/MatricTest.java b/src/test/java/seedu/address/model/student/MatricTest.java new file mode 100644 index 00000000000..57d7f1e2d11 --- /dev/null +++ b/src/test/java/seedu/address/model/student/MatricTest.java @@ -0,0 +1,94 @@ +package seedu.address.model.student; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import org.junit.jupiter.api.Test; + +public class MatricTest { + + @Test + public void constructor_validMatricNumber_success() { + String validMatricNumber = "A1234567Z"; + Matric matric = new Matric(validMatricNumber); + assertEquals(validMatricNumber, matric.matricNumber); + } + + @Test + public void constructor_invalidMatricNumber_throwsIllegalArgumentException() { + String invalidMatricNumber = "InvalidMatric"; + assertThrows(IllegalArgumentException.class, () -> new Matric(invalidMatricNumber)); + } + + @Test + public void isValidConstructorParam_emptyMatric_returnsTrue() { + assertTrue(Matric.isValidConstructorParam("")); + } + + @Test + public void isValidConstructorParam_validMatric_returnsTrue() { + assertTrue(Matric.isValidConstructorParam("A1234567Z")); + } + + @Test + public void isValidConstructorParam_invalidMatric_returnsFalse() { + assertFalse(Matric.isValidConstructorParam("InvalidMatric")); + } + + @Test + public void isValidMatric_validMatric_returnsTrue() { + assertTrue(Matric.isValidMatric("A1234567Z")); + } + + @Test + public void isValidMatric_invalidMatric_returnsFalse() { + assertFalse(Matric.isValidMatric("InvalidMatric")); + } + + @Test + public void equals_sameValues_returnsTrue() { + Matric matric1 = new Matric("A1234567Z"); + Matric matric2 = new Matric("A1234567Z"); + assertTrue(matric1.equals(matric2)); + } + + @Test + public void equals_sameMatric_returnsTrue() { + Matric matric1 = new Matric("A1234567Z"); + assertTrue(matric1.equals(matric1)); + } + + @Test + public void equals_differentObject_returnsFalse() { + Matric matric1 = new Matric("A1234567Z"); + assertFalse(matric1.equals("matric1")); + } + + @Test + public void equals_differentMatric_returnsFalse() { + Matric matric1 = new Matric("A1234567Z"); + Matric matric2 = new Matric("A7654321Y"); + assertFalse(matric1.equals(matric2)); + } + + @Test + public void hashCode_sameMatric_returnsSameHashCode() { + Matric matric1 = new Matric("A1234567Z"); + Matric matric2 = new Matric("A1234567Z"); + assertEquals(matric1.hashCode(), matric2.hashCode()); + } + + @Test + public void toString_emptyMatric_returnsNoMatriculationNumber() { + Matric matric = new Matric(""); + assertEquals("", matric.toString()); + } + + @Test + public void toString_validMatric_returnsMatriculationNumber() { + Matric matric = new Matric("A1234567Z"); + assertEquals("A1234567Z", matric.toString()); + } +} diff --git a/src/test/java/seedu/address/model/student/ReflectionTest.java b/src/test/java/seedu/address/model/student/ReflectionTest.java new file mode 100644 index 00000000000..a6a0994d44b --- /dev/null +++ b/src/test/java/seedu/address/model/student/ReflectionTest.java @@ -0,0 +1,89 @@ +package seedu.address.model.student; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import org.junit.jupiter.api.Test; + +public class ReflectionTest { + @Test + public void constructor_validReflection_success() { + String validReflection = "R1"; + Reflection reflection = new Reflection(validReflection); + assertEquals(validReflection, reflection.reflection); + } + + @Test + public void constructor_invalidReflection_throwsIllegalArgumentException() { + String invalidReflection = "InvalidReflection"; + assertThrows(IllegalArgumentException.class, () -> new Reflection(invalidReflection)); + } + + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new Reflection(null)); + } + + @Test void isValidConstructorParam_emptyReflection_returnsTrue() { + assertTrue(Reflection.isValidConstructorParam("")); + } + + @Test + public void isValidConstructorParam_validReflection_returnsTrue() { + assertTrue(Reflection.isValidConstructorParam("R1")); + } + + @Test + public void isValidConstructorParam_invalidReflection_returnsFalse() { + assertFalse(Reflection.isValidConstructorParam("InvalidReflection")); + } + + @Test + public void isValidReflection_emptyReflection_returnsTrue() { + assertTrue(Reflection.isEmptyReflection("")); + } + + @Test + public void isValidReflection_validReflection_returnsTrue() { + assertTrue(Reflection.isValidReflection("R1")); + } + + @Test + public void isValidReflection_invalidReflection_returnsFalse() { + assertFalse(Reflection.isValidReflection("InvalidReflection")); + } + + @Test + public void equals_sameValues_returnsTrue() { + Reflection reflection1 = new Reflection("R1"); + Reflection reflection2 = new Reflection("R1"); + assertTrue(reflection1.equals(reflection2)); + } + + @Test + public void equals_sameReflection_returnsTrue() { + Reflection reflection1 = new Reflection("R1"); + assertTrue(reflection1.equals(reflection1)); + } + + @Test + public void equals_differentReflection_returnsFalse() { + Reflection reflection1 = new Reflection("R1"); + Reflection reflection2 = new Reflection("R2"); + assertFalse(reflection1.equals(reflection2)); + } + + @Test + public void equals_differentType_returnsFalse() { + Reflection reflection1 = new Reflection("R1"); + assertFalse(reflection1.equals(5)); + } + + @Test + public void equals_null_returnsFalse() { + Reflection reflection1 = new Reflection("R1"); + assertFalse(reflection1.equals(null)); + } +} diff --git a/src/test/java/seedu/address/model/student/StudioTest.java b/src/test/java/seedu/address/model/student/StudioTest.java new file mode 100644 index 00000000000..329c313955d --- /dev/null +++ b/src/test/java/seedu/address/model/student/StudioTest.java @@ -0,0 +1,74 @@ +package seedu.address.model.student; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import org.junit.jupiter.api.Test; + +public class StudioTest { + + @Test + public void constructor_validStudioNumber_success() { + String validStudioNumber = "S2"; + Studio studio = new Studio(validStudioNumber); + assertEquals(validStudioNumber, studio.studio); + } + + @Test + public void constructor_invalidStudioNumber_throwsIllegalArgumentException() { + String invalidStudioNumber = "InvalidStudio"; + assertThrows(IllegalArgumentException.class, () -> new Studio(invalidStudioNumber)); + } + + @Test + public void isValidConstructorParam_emptyStudio_returnsTrue() { + assertTrue(Studio.isValidConstructorParam("")); + } + + @Test + public void isValidConstructorParam_validStudio_returnsTrue() { + assertTrue(Studio.isValidConstructorParam("S2")); + } + + @Test + public void isValidConstructorParam_invalidStudio_returnsFalse() { + assertFalse(Studio.isValidConstructorParam("InvalidStudio")); + } + + @Test + public void isValidStudio_validStudio_returnsTrue() { + assertTrue(Studio.isValidStudio("S2")); + } + + @Test + public void isValidStudio_invalidStudio_returnsFalse() { + assertFalse(Studio.isValidStudio("InvalidStudio")); + } + + @Test + public void equals_sameValues_returnsTrue() { + Studio studio1 = new Studio("S2"); + Studio studio2 = new Studio("S2"); + assertTrue(studio1.equals(studio2)); + } + + @Test + public void equals_sameStudio_returnsTrue() { + Studio studio1 = new Studio("S2"); + assertTrue(studio1.equals(studio1)); + } + + @Test + public void equals_differentStudio_returnsFalse() { + Studio studio1 = new Studio("S2"); + Studio studio2 = new Studio("S3"); + assertFalse(studio1.equals(studio2)); + } + + @Test void equals_differentObject_returnsFalse() { + Studio studio1 = new Studio("S2"); + assertFalse(studio1.equals("studio1")); + } +} diff --git a/src/test/java/seedu/address/storage/JsonAdaptedExamTest.java b/src/test/java/seedu/address/storage/JsonAdaptedExamTest.java new file mode 100644 index 00000000000..e2f1f2c760c --- /dev/null +++ b/src/test/java/seedu/address/storage/JsonAdaptedExamTest.java @@ -0,0 +1,43 @@ +package seedu.address.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.exam.Exam; +import seedu.address.model.person.Score; + +public class JsonAdaptedExamTest { + + @Test + public void toModelType_validExamDetails_returnsExam() throws Exception { + JsonAdaptedExam exam = new JsonAdaptedExam("Math", "100"); + assertEquals(new Exam("Math", new Score(100)), exam.toModelType()); + } + + @Test + public void toModelType_invalidName_throwsIllegalValueException() { + JsonAdaptedExam exam = new JsonAdaptedExam("", "100"); + assertThrows(IllegalValueException.class, exam::toModelType); + } + + @Test + public void toModelType_nullName_throwsIllegalValueException() { + JsonAdaptedExam exam = new JsonAdaptedExam(null, "100"); + assertThrows(IllegalValueException.class, exam::toModelType); + } + + @Test + public void toModelType_invalidScore_throwsIllegalValueException() { + JsonAdaptedExam exam = new JsonAdaptedExam("Math", "-1"); + assertThrows(IllegalValueException.class, exam::toModelType); + } + + @Test + public void toModelType_nullScore_throwsIllegalValueException() { + JsonAdaptedExam exam = new JsonAdaptedExam("Math", null); + assertThrows(IllegalValueException.class, exam::toModelType); + } +} diff --git a/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java b/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java index 83b11331cdb..f8443a29de3 100644 --- a/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java +++ b/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java @@ -4,6 +4,7 @@ import static seedu.address.storage.JsonAdaptedPerson.MISSING_FIELD_MESSAGE_FORMAT; import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalPersons.BENSON; +import static seedu.address.testutil.TypicalPersons.GEORGE; import java.util.ArrayList; import java.util.List; @@ -16,13 +17,19 @@ import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Phone; +import seedu.address.model.student.Matric; +import seedu.address.model.student.Reflection; +import seedu.address.model.student.Studio; public class JsonAdaptedPersonTest { private static final String INVALID_NAME = "R@chel"; - private static final String INVALID_PHONE = "+651234"; + private static final String INVALID_PHONE = "[651234"; private static final String INVALID_ADDRESS = " "; private static final String INVALID_EMAIL = "example.com"; private static final String INVALID_TAG = "#friend"; + private static final String INVALID_MATRIC = "zz"; + private static final String INVALID_REFLECTION = "R"; + private static final String INVALID_STUDIO = "T23"; private static final String VALID_NAME = BENSON.getName().toString(); private static final String VALID_PHONE = BENSON.getPhone().toString(); @@ -31,6 +38,19 @@ public class JsonAdaptedPersonTest { private static final List VALID_TAGS = BENSON.getTags().stream() .map(JsonAdaptedTag::new) .collect(Collectors.toList()); + private static final String VALID_MATRIC = "A1234567M"; + private static final String VALID_REFLECTION = "R1"; + private static final String VALID_STUDIO = "S1"; + + private static final String VALID_EXAM_NAME = "Midterm"; + private static final String VALID_EXAM_MAX_SCORE = "100"; + private static final String VALID_EXAM_SCORE = "70"; + + private static final List EMPTY_SCORES = new ArrayList<>(); + private static final List VALID_SCORES = new ArrayList<>(List.of( + new JsonAdaptedExamScore(VALID_EXAM_NAME, VALID_EXAM_MAX_SCORE, VALID_EXAM_SCORE) + )); + @Test public void toModelType_validPersonDetails_returnsPerson() throws Exception { @@ -38,17 +58,26 @@ public void toModelType_validPersonDetails_returnsPerson() throws Exception { assertEquals(BENSON, person.toModelType()); } + @Test + public void toModelType_validPersonDetailsWithScores_returnsPerson() throws Exception { + JsonAdaptedPerson person = new JsonAdaptedPerson(GEORGE); + assertEquals(GEORGE, person.toModelType()); + } + @Test public void toModelType_invalidName_throwsIllegalValueException() { JsonAdaptedPerson person = - new JsonAdaptedPerson(INVALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + new JsonAdaptedPerson(INVALID_NAME, VALID_PHONE, VALID_EMAIL, + VALID_ADDRESS, VALID_TAGS, VALID_MATRIC, VALID_REFLECTION, + VALID_STUDIO, EMPTY_SCORES); String expectedMessage = Name.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullName_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(null, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + JsonAdaptedPerson person = new JsonAdaptedPerson(null, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + VALID_TAGS, VALID_MATRIC, VALID_REFLECTION, VALID_STUDIO, EMPTY_SCORES); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName()); assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @@ -56,14 +85,18 @@ public void toModelType_nullName_throwsIllegalValueException() { @Test public void toModelType_invalidPhone_throwsIllegalValueException() { JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, INVALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + new JsonAdaptedPerson(VALID_NAME, INVALID_PHONE, VALID_EMAIL, + VALID_ADDRESS, VALID_TAGS, VALID_MATRIC, + VALID_REFLECTION, VALID_STUDIO, EMPTY_SCORES); String expectedMessage = Phone.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullPhone_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, null, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, null, VALID_EMAIL, VALID_ADDRESS, + VALID_TAGS, VALID_MATRIC, VALID_REFLECTION, + VALID_STUDIO, EMPTY_SCORES); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName()); assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @@ -71,29 +104,35 @@ public void toModelType_nullPhone_throwsIllegalValueException() { @Test public void toModelType_invalidEmail_throwsIllegalValueException() { JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, INVALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, INVALID_EMAIL, VALID_ADDRESS, + VALID_TAGS, VALID_MATRIC, VALID_REFLECTION, VALID_STUDIO, EMPTY_SCORES); String expectedMessage = Email.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullEmail_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, null, VALID_ADDRESS, VALID_TAGS); + JsonAdaptedPerson person = new JsonAdaptedPerson( + VALID_NAME, VALID_PHONE, null, VALID_ADDRESS, VALID_TAGS, + VALID_MATRIC, VALID_REFLECTION, VALID_STUDIO, EMPTY_SCORES); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName()); assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_invalidAddress_throwsIllegalValueException() { - JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, INVALID_ADDRESS, VALID_TAGS); + JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, INVALID_ADDRESS, + VALID_TAGS, VALID_MATRIC, VALID_REFLECTION, + VALID_STUDIO, EMPTY_SCORES); String expectedMessage = Address.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullAddress_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, null, VALID_TAGS); + JsonAdaptedPerson person = new JsonAdaptedPerson( + VALID_NAME, VALID_PHONE, VALID_EMAIL, null, VALID_TAGS, VALID_MATRIC, + VALID_REFLECTION, VALID_STUDIO, EMPTY_SCORES); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName()); assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @@ -103,8 +142,44 @@ public void toModelType_invalidTags_throwsIllegalValueException() { List invalidTags = new ArrayList<>(VALID_TAGS); invalidTags.add(new JsonAdaptedTag(INVALID_TAG)); JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, invalidTags); + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + invalidTags, VALID_MATRIC, VALID_REFLECTION, VALID_STUDIO, EMPTY_SCORES); assertThrows(IllegalValueException.class, person::toModelType); } + @Test + public void toModelType_invalidMatric_throwsIllegalValueException() { + JsonAdaptedPerson person = + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, + VALID_ADDRESS, VALID_TAGS, INVALID_MATRIC, VALID_REFLECTION, VALID_STUDIO, EMPTY_SCORES); + String expectedMessage = Matric.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + } + + @Test + public void toModelType_invalidReflection_throwsIllegalValueException() { + JsonAdaptedPerson person = + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, + VALID_ADDRESS, VALID_TAGS, VALID_MATRIC, INVALID_REFLECTION, VALID_STUDIO, EMPTY_SCORES); + String expectedMessage = Reflection.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + } + + @Test + public void toModelType_invalidStudio_throwsIllegalValueException() { + JsonAdaptedPerson person = + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + VALID_TAGS, VALID_MATRIC, VALID_REFLECTION, INVALID_STUDIO, EMPTY_SCORES); + String expectedMessage = Studio.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + } + + @Test + public void toModelType_invalidScores_throwsIllegalValueException() { + List invalidScores = new ArrayList<>(VALID_SCORES); + invalidScores.add(new JsonAdaptedExamScore(VALID_EXAM_NAME, VALID_EXAM_MAX_SCORE, "-23.2")); + JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + VALID_TAGS, VALID_MATRIC, VALID_REFLECTION, VALID_STUDIO, invalidScores); + assertThrows(IllegalValueException.class, person::toModelType); + } } diff --git a/src/test/java/seedu/address/storage/JsonAddressBookStorageTest.java b/src/test/java/seedu/address/storage/JsonAddressBookStorageTest.java index 4e5ce9200c8..401b26e6fd2 100644 --- a/src/test/java/seedu/address/storage/JsonAddressBookStorageTest.java +++ b/src/test/java/seedu/address/storage/JsonAddressBookStorageTest.java @@ -3,10 +3,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static seedu.address.testutil.Assert.assertThrows; -import static seedu.address.testutil.TypicalPersons.ALICE; -import static seedu.address.testutil.TypicalPersons.HOON; -import static seedu.address.testutil.TypicalPersons.IDA; -import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + import static seedu.address.testutil.TypicalPersons.ALICE; + import static seedu.address.testutil.TypicalPersons.HOON; + import static seedu.address.testutil.TypicalPersons.IDA; + import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; import java.io.IOException; import java.nio.file.Path; @@ -65,25 +65,21 @@ public void readAndSaveAddressBook_allInOrder_success() throws Exception { Path filePath = testFolder.resolve("TempAddressBook.json"); AddressBook original = getTypicalAddressBook(); JsonAddressBookStorage jsonAddressBookStorage = new JsonAddressBookStorage(filePath); - // Save in new file and read back jsonAddressBookStorage.saveAddressBook(original, filePath); ReadOnlyAddressBook readBack = jsonAddressBookStorage.readAddressBook(filePath).get(); assertEquals(original, new AddressBook(readBack)); - // Modify data, overwrite exiting file, and read back original.addPerson(HOON); original.removePerson(ALICE); jsonAddressBookStorage.saveAddressBook(original, filePath); readBack = jsonAddressBookStorage.readAddressBook(filePath).get(); assertEquals(original, new AddressBook(readBack)); - // Save and read without specifying file path original.addPerson(IDA); - jsonAddressBookStorage.saveAddressBook(original); // file path not specified - readBack = jsonAddressBookStorage.readAddressBook().get(); // file path not specified + jsonAddressBookStorage.saveAddressBook(original); + readBack = jsonAddressBookStorage.readAddressBook().get(); assertEquals(original, new AddressBook(readBack)); - } @Test diff --git a/src/test/java/seedu/address/storage/JsonUserPrefsStorageTest.java b/src/test/java/seedu/address/storage/JsonUserPrefsStorageTest.java index ed0a413526a..21bf35b4302 100644 --- a/src/test/java/seedu/address/storage/JsonUserPrefsStorageTest.java +++ b/src/test/java/seedu/address/storage/JsonUserPrefsStorageTest.java @@ -73,7 +73,7 @@ public void readUserPrefs_extraValuesInFile_extraValuesIgnored() throws DataLoad private UserPrefs getTypicalUserPrefs() { UserPrefs userPrefs = new UserPrefs(); userPrefs.setGuiSettings(new GuiSettings(1000, 500, 300, 100)); - userPrefs.setAddressBookFilePath(Paths.get("addressbook.json")); + userPrefs.setAddressBookFilePath(Paths.get("avengersassemble.json")); return userPrefs; } diff --git a/src/test/java/seedu/address/storage/StorageManagerTest.java b/src/test/java/seedu/address/storage/StorageManagerTest.java index 99a16548970..803c6ec7a63 100644 --- a/src/test/java/seedu/address/storage/StorageManagerTest.java +++ b/src/test/java/seedu/address/storage/StorageManagerTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +// import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; import java.nio.file.Path; @@ -11,6 +12,8 @@ import org.junit.jupiter.api.io.TempDir; import seedu.address.commons.core.GuiSettings; +// import seedu.address.model.AddressBook; +// import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.UserPrefs; diff --git a/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java b/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java index 4584bd5044e..34e517d7081 100644 --- a/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java +++ b/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java @@ -10,6 +10,9 @@ import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.student.Matric; +import seedu.address.model.student.Reflection; +import seedu.address.model.student.Studio; import seedu.address.model.tag.Tag; /** @@ -37,6 +40,10 @@ public EditPersonDescriptorBuilder(Person person) { descriptor.setEmail(person.getEmail()); descriptor.setAddress(person.getAddress()); descriptor.setTags(person.getTags()); + descriptor.setMatric(person.getMatric()); + descriptor.setReflection(person.getReflection()); + descriptor.setStudio(person.getStudio()); + // descriptor will never have setScore as it is not used in EditCommand } /** @@ -81,6 +88,36 @@ public EditPersonDescriptorBuilder withTags(String... tags) { return this; } + /** + * Sets the {@code Matric} of the {@code Person} that we are building. + * @param matric matric number of the person + * @return EditPersonDescriptorBuilder object + */ + public EditPersonDescriptorBuilder withMatric(String matric) { + descriptor.setMatric(new Matric(matric)); + return this; + } + + /** + * Sets the {@code Reflection} of the {@code Person} that we are building. + * @param reflection reflection of the person + * @return EditPersonDescriptorBuilder object + */ + public EditPersonDescriptorBuilder withReflection(String reflection) { + descriptor.setReflection(new Reflection(reflection)); + return this; + } + + /** + * Sets the {@code Studio} of the {@code Person} that we are building. + * @param studio studio of the person + * @return EditPersonDescriptorBuilder object + */ + public EditPersonDescriptorBuilder withStudio(String studio) { + descriptor.setStudio(new Studio(studio)); + return this; + } + public EditPersonDescriptor build() { return descriptor; } diff --git a/src/test/java/seedu/address/testutil/PersonBuilder.java b/src/test/java/seedu/address/testutil/PersonBuilder.java index 6be381d39ba..1e320365e43 100644 --- a/src/test/java/seedu/address/testutil/PersonBuilder.java +++ b/src/test/java/seedu/address/testutil/PersonBuilder.java @@ -1,13 +1,20 @@ package seedu.address.testutil; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; +import seedu.address.model.exam.Exam; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Score; +import seedu.address.model.student.Matric; +import seedu.address.model.student.Reflection; +import seedu.address.model.student.Studio; import seedu.address.model.tag.Tag; import seedu.address.model.util.SampleDataUtil; @@ -20,12 +27,19 @@ public class PersonBuilder { public static final String DEFAULT_PHONE = "85355255"; public static final String DEFAULT_EMAIL = "amy@gmail.com"; public static final String DEFAULT_ADDRESS = "123, Jurong West Ave 6, #08-111"; + public static final String DEFAULT_MATRIC = "A1234567X"; + public static final String DEFAULT_REFLECTION = "R1"; + public static final String DEFAULT_STUDIO = "S2"; private Name name; private Phone phone; private Email email; private Address address; private Set tags; + private Matric matric; + private Reflection reflection; + private Studio studio; + private Map scores; /** * Creates a {@code PersonBuilder} with the default details. @@ -36,6 +50,10 @@ public PersonBuilder() { email = new Email(DEFAULT_EMAIL); address = new Address(DEFAULT_ADDRESS); tags = new HashSet<>(); + matric = new Matric(DEFAULT_MATRIC); + reflection = new Reflection(DEFAULT_REFLECTION); + studio = new Studio(DEFAULT_STUDIO); + scores = new HashMap<>(); } /** @@ -47,6 +65,10 @@ public PersonBuilder(Person personToCopy) { email = personToCopy.getEmail(); address = personToCopy.getAddress(); tags = new HashSet<>(personToCopy.getTags()); + matric = personToCopy.getMatric(); + reflection = personToCopy.getReflection(); + studio = personToCopy.getStudio(); + scores = personToCopy.getScores(); } /** @@ -89,8 +111,52 @@ public PersonBuilder withEmail(String email) { return this; } + /** + * Sets the {@code Matric} of the {@code Person} that we are building. + * @param matric matric number + * @return PersonBuilder + */ + public PersonBuilder withMatric(String matric) { + this.matric = new Matric(matric); + return this; + } + + /** + * Sets the {@code Studio} of the {@code Person} that we are building. + * @param studio studio number + * @return PersonBuilder + */ + public PersonBuilder withStudio(String studio) { + this.studio = new Studio(studio); + return this; + } + + /** + * Sets the {@code Reflection} of the {@code Person} that we are building. + * @param reflection reflection + * @return PersonBuilder + */ + public PersonBuilder withReflection(String reflection) { + this.reflection = new Reflection(reflection); + return this; + } + + /** + * Sets the {@code Exam} and {@code Score} of the {@code Person} that we are building. + * @param scores exam scores + * @return PersonBuilder + */ + public PersonBuilder withScores(Map scores) { + this.scores = scores; + return this; + } + + /** + * Builds the person object. + * @return Person + */ public Person build() { - return new Person(name, phone, email, address, tags); + return new Person(name, phone, email, address, tags, matric, reflection, studio, scores); } } diff --git a/src/test/java/seedu/address/testutil/PersonUtil.java b/src/test/java/seedu/address/testutil/PersonUtil.java index 90849945183..7632a953e7b 100644 --- a/src/test/java/seedu/address/testutil/PersonUtil.java +++ b/src/test/java/seedu/address/testutil/PersonUtil.java @@ -2,8 +2,11 @@ import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MATRIC_NUMBER; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_REFLECTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_STUDIO; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import java.util.Set; @@ -34,6 +37,9 @@ public static String getPersonDetails(Person person) { sb.append(PREFIX_PHONE + person.getPhone().value + " "); sb.append(PREFIX_EMAIL + person.getEmail().value + " "); sb.append(PREFIX_ADDRESS + person.getAddress().value + " "); + sb.append(PREFIX_MATRIC_NUMBER + person.getMatric().matricNumber + " "); + sb.append(PREFIX_REFLECTION + person.getReflection().reflection + " "); + sb.append(PREFIX_STUDIO + person.getStudio().studio + " "); person.getTags().stream().forEach( s -> sb.append(PREFIX_TAG + s.tagName + " ") ); @@ -49,6 +55,11 @@ public static String getEditPersonDescriptorDetails(EditPersonDescriptor descrip descriptor.getPhone().ifPresent(phone -> sb.append(PREFIX_PHONE).append(phone.value).append(" ")); descriptor.getEmail().ifPresent(email -> sb.append(PREFIX_EMAIL).append(email.value).append(" ")); descriptor.getAddress().ifPresent(address -> sb.append(PREFIX_ADDRESS).append(address.value).append(" ")); + descriptor.getMatric().ifPresent( + matric -> sb.append(PREFIX_MATRIC_NUMBER).append(matric.matricNumber).append(" ")); + descriptor.getReflection().ifPresent( + reflection -> sb.append(PREFIX_REFLECTION).append(reflection.reflection).append(" ")); + descriptor.getStudio().ifPresent(studio -> sb.append(PREFIX_STUDIO).append(studio.studio).append(" ")); if (descriptor.getTags().isPresent()) { Set tags = descriptor.getTags().get(); if (tags.isEmpty()) { @@ -57,6 +68,9 @@ public static String getEditPersonDescriptorDetails(EditPersonDescriptor descrip tags.forEach(s -> sb.append(PREFIX_TAG).append(s.tagName).append(" ")); } } + descriptor.getScores().ifPresent( + scores -> scores.forEach((exam, score) -> sb.append(exam.getName()).append(score).append(" "))); + return sb.toString(); } } diff --git a/src/test/java/seedu/address/testutil/TypicalIndexes.java b/src/test/java/seedu/address/testutil/TypicalIndexes.java index 1e613937657..757523fcdb4 100644 --- a/src/test/java/seedu/address/testutil/TypicalIndexes.java +++ b/src/test/java/seedu/address/testutil/TypicalIndexes.java @@ -9,4 +9,6 @@ public class TypicalIndexes { public static final Index INDEX_FIRST_PERSON = Index.fromOneBased(1); public static final Index INDEX_SECOND_PERSON = Index.fromOneBased(2); public static final Index INDEX_THIRD_PERSON = Index.fromOneBased(3); + public static final Index INDEX_FIRST_EXAM = Index.fromOneBased(1); + public static final Index INDEX_SECOND_EXAM = Index.fromOneBased(2); } diff --git a/src/test/java/seedu/address/testutil/TypicalPersons.java b/src/test/java/seedu/address/testutil/TypicalPersons.java index fec76fb7129..c24172f9b0c 100644 --- a/src/test/java/seedu/address/testutil/TypicalPersons.java +++ b/src/test/java/seedu/address/testutil/TypicalPersons.java @@ -4,56 +4,91 @@ import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_AMY; import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_MATRIC_NUMBER_AMY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_MATRIC_NUMBER_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_AMY; import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_AMY; import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_REFLECTION_AMY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_REFLECTION_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_STUDIO_AMY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_STUDIO_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_FRIEND; import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import seedu.address.model.AddressBook; +import seedu.address.model.exam.Exam; import seedu.address.model.person.Person; +import seedu.address.model.person.Score; /** * A utility class containing a list of {@code Person} objects to be used in tests. */ public class TypicalPersons { + public static final Exam MIDTERM = new Exam("Midterm", new Score(100)); + public static final Exam FINAL = new Exam("Final", new Score(100)); + public static final Exam QUIZ = new Exam("Quiz", new Score(100)); + public static final Person ALICE = new PersonBuilder().withName("Alice Pauline") .withAddress("123, Jurong West Ave 6, #08-111").withEmail("alice@example.com") - .withPhone("94351253") - .withTags("friends").build(); + .withPhone("94351253").withTags("friends").withMatric("A1111111A") + .withReflection("R1").withStudio("S1") + .build(); public static final Person BENSON = new PersonBuilder().withName("Benson Meier") .withAddress("311, Clementi Ave 2, #02-25") .withEmail("johnd@example.com").withPhone("98765432") - .withTags("owesMoney", "friends").build(); + .withTags("owesMoney", "friends").withMatric("A2222222A") + .withReflection("R2").withStudio("S2") + .build(); public static final Person CARL = new PersonBuilder().withName("Carl Kurz").withPhone("95352563") - .withEmail("heinz@example.com").withAddress("wall street").build(); + .withEmail("heinz@example.com").withAddress("wall street") + .withMatric("A3333333A") + .withReflection("R3").withStudio("S3") + .withScores(Collections.singletonMap(MIDTERM, new Score(30))).build(); public static final Person DANIEL = new PersonBuilder().withName("Daniel Meier").withPhone("87652533") - .withEmail("cornelia@example.com").withAddress("10th street").withTags("friends").build(); + .withEmail("cornelia@example.com").withAddress("10th street") + .withTags("friends").withMatric("A4444444A") + .withReflection("R4").withStudio("S4") + .withScores(Collections.singletonMap(MIDTERM, new Score(40))).build(); public static final Person ELLE = new PersonBuilder().withName("Elle Meyer").withPhone("9482224") - .withEmail("werner@example.com").withAddress("michegan ave").build(); + .withEmail("werner@example.com").withAddress("michegan ave") + .withMatric("A5555555A").withReflection("R5").withStudio("S5") + .withScores(Collections.singletonMap(MIDTERM, new Score(50))).build(); public static final Person FIONA = new PersonBuilder().withName("Fiona Kunz").withPhone("9482427") - .withEmail("lydia@example.com").withAddress("little tokyo").build(); + .withEmail("lydia@example.com").withAddress("little tokyo") + .withMatric("A6666666A").withReflection("R6").withStudio("S6") + .withScores(Collections.singletonMap(MIDTERM, new Score(60))).build(); public static final Person GEORGE = new PersonBuilder().withName("George Best").withPhone("9482442") - .withEmail("anna@example.com").withAddress("4th street").build(); + .withEmail("anna@example.com").withAddress("4th street") + .withMatric("A7777777A").withReflection("R7").withStudio("S7") + .withScores(Collections.singletonMap(MIDTERM, new Score(70))).build(); // Manually added public static final Person HOON = new PersonBuilder().withName("Hoon Meier").withPhone("8482424") - .withEmail("stefan@example.com").withAddress("little india").build(); + .withEmail("stefan@example.com").withAddress("little india") + .withMatric("A1234567X") + .withReflection("R5").withStudio("S3").build(); public static final Person IDA = new PersonBuilder().withName("Ida Mueller").withPhone("8482131") - .withEmail("hans@example.com").withAddress("chicago ave").build(); + .withEmail("hans@example.com").withAddress("chicago ave") + .withMatric("A1234567X") + .withReflection("R5").withStudio("S3").build(); // Manually added - Person's details found in {@code CommandTestUtil} public static final Person AMY = new PersonBuilder().withName(VALID_NAME_AMY).withPhone(VALID_PHONE_AMY) - .withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY).withTags(VALID_TAG_FRIEND).build(); + .withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY) + .withTags(VALID_TAG_FRIEND).withMatric(VALID_MATRIC_NUMBER_AMY) + .withReflection(VALID_REFLECTION_AMY).withStudio(VALID_STUDIO_AMY).build(); public static final Person BOB = new PersonBuilder().withName(VALID_NAME_BOB).withPhone(VALID_PHONE_BOB) .withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND) - .build(); + .withMatric(VALID_MATRIC_NUMBER_BOB).withReflection(VALID_REFLECTION_BOB) + .withStudio(VALID_STUDIO_BOB).build(); public static final String KEYWORD_MATCHING_MEIER = "Meier"; // A keyword that matches MEIER @@ -67,10 +102,27 @@ public static AddressBook getTypicalAddressBook() { for (Person person : getTypicalPersons()) { ab.addPerson(person); } + for (Exam exam : Arrays.asList(MIDTERM, FINAL, QUIZ)) { + ab.addExam(exam); + } return ab; } + /** + * Returns a list of typical persons. + */ public static List getTypicalPersons() { return new ArrayList<>(Arrays.asList(ALICE, BENSON, CARL, DANIEL, ELLE, FIONA, GEORGE)); } + + /** + * Returns the emails of all the typical persons. + */ + public static String getTypicalPersonsEmails() { + StringBuilder emails = new StringBuilder(); + for (Person person : getTypicalPersons()) { + emails.append(person.getEmail().value).append("; "); + } + return emails.toString().trim(); + } }