diff --git a/.gitignore b/.gitignore index 71c9194e8bd..23e761fb059 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ src/main/resources/docs/ /config.json /preferences.json /*.log.* +/*sourceControl*.csv # Test sandbox files src/test/data/sandbox/ diff --git a/README.md b/README.md index 13f5c77403f..703c38836ed 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,30 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) +# Source Control + +[![CI Status](https://github.com/AY2122S1-CS2103T-W08-2/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2122S1-CS2103T-W08-2/tp/actions) + +**Source Control** is a desktop app for CS1101S professors to manage the performance of their students. + +This application allows you to **store comprehensive data** of each student quickly, and allows you to search through them easily. With Source Control, you will never have to worry about your large student cohort and tracking numerous assessments! + +Source Control can also give you both a quick overview and a closer look of how your students are performing. This application **provides in-depth data analysis** of the performance of your students in each assessment, giving you timely feedback on the pace and difficulty level of the module. + +Source Control is **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, Source Control can help you track your students’ performance faster than traditional GUI apps! ![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. +## Usage +* Keeps track of administrative info and academic records of large student intake. +* Analyzes the performance of individual, groups and the cohort in assessments. + +## User Guide +If you are interested in using **Source Control**, head over to the [Quick Start section of the **User Guide**](https://ay2122s1-cs2103t-w08-2.github.io/tp/UserGuide.html#quick-start). + +## Developer Guide +If you are interested in developing **Source Control**, check out the **[Developer Guide](https://ay2122s1-cs2103t-w08-2.github.io/tp/DeveloperGuide.html)** for information on how the application is designed. + +## About Us +If you are interested in contacting **Source Control** team, visit the [**About Us** website](https://ay2122s1-cs2103t-w08-2.github.io/tp/AboutUs.html). + +## Acknowledgements +* Libraries used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5) +* Original project: [AddressBook Level-3](https://se-education.org/addressbook-level3) project created as part of the [SE-EDU](https://se-education.org) initiative diff --git a/build.gradle b/build.gradle index be2d2905dde..93bd6903d05 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { id 'jacoco' } -mainClassName = 'seedu.address.Main' +mainClassName = 'seedu.sourcecontrol.Main' sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 @@ -16,6 +16,10 @@ repositories { maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } } +run { + enableAssertions = true +} + checkstyle { toolVersion = '8.29' } @@ -66,7 +70,7 @@ dependencies { } shadowJar { - archiveName = 'addressbook.jar' + archiveName = 'sourceControl.jar' } defaultTasks 'clean', 'test' diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 1c9514e966a..434bad48037 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -5,55 +5,54 @@ title: 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` +You can reach us at the email `e0544170@u.nus.edu` ## Project team -### John Doe +### Jonas Chow - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/jonas-chow)] +[[portfolio](team/jonas-chow.md)] -* Role: Project Advisor +* Role: Team Lead +* Responsibilities: Testing + Scheduling and tracking -### Jane Doe +### Tang Zhiying - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/zhing22)] +[[portfolio](team/zhing22.md)] -* Role: Team Lead -* Responsibilities: UI +* Role: Developer +* Responsibilities: Documentation + Code quality -### Johnny Doe +### Gan Hong Yao - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](https://github.com/ganhongyao)] [[portfolio](team/ganhongyao.md)] * Role: Developer -* Responsibilities: Data +* Responsibilities: Testing + Integration -### Jean Doe +### Nguyen Ba Van Nhi - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/nbvannhi)] [[portfolio](team/nbvannhi.md)] * Role: Developer -* Responsibilities: Dev Ops + Threading +* Responsibilities: Documentation + Code quality -### James Doe +### Leong Hong Fai - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/leonghongfai)] +[[portfolio](team/leonghongfai.md)] * Role: Developer -* Responsibilities: UI +* Responsibilities: Integration + Deliverables and Deadlines diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 46eae8ee565..0b809a7d009 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -2,20 +2,32 @@ layout: page title: Developer Guide --- +## **Table of Contents** + * Table of Contents {:toc} +
+ -------------------------------------------------------------------------------------------------------------------- ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +* Libraries used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5) +* Original project: [AddressBook Level-3](https://se-education.org/addressbook-level3) Project created as part of the [SE-EDU](https://se-education.org) initiative +* Application logo: Inspired by [Source Academy](https://sourceacademy.nus.edu.sg/) +* Code snippet for getting jar file directory: Taken from [this Stackoverflow post](https://stackoverflow.com/questions/320542/how-to-get-the-path-of-a-running-jar-file) +* PlantUML sprite for rake symbol: Taken from [this PlantUML forum post](https://forum.plantuml.net/195/is-there-any-support-for-subactivity-or-the-rake-symbol) + +
-------------------------------------------------------------------------------------------------------------------- -## **Setting up, getting started** +## **Setting Up and Getting Started** -Refer to the guide [_Setting up and getting started_](SettingUp.md). +Refer to the guide [_Setting Up and Getting Started_](SettingUp.md). + +
-------------------------------------------------------------------------------------------------------------------- @@ -23,225 +35,586 @@ Refer to the guide [_Setting up and getting started_](SettingUp.md).
-:bulb: **Tip:** The `.puml` files used to create diagrams in this document can be found in the [diagrams](https://github.com/se-edu/addressbook-level3/tree/master/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. +:bulb: **Tip:** The `.puml` files used to create diagrams in this document can be found in the [diagrams](https://github.com/AY2122S1-CS2103T-W08-2/tp/tree/master/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. +The ***Architecture Diagram*** given above explains the high-level design of Source Control. Given below is a quick overview of main components and how they interact with each other. +
+ **Main components of the architecture** -**`Main`** has two classes called [`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). It is responsible for, +**`Main`** has two classes called [`Main`](https://github.com/AY2122S1-CS2103T-W08-2/tp/tree/master/src/main/java/seedu/sourcecontrol/Main.java) and [`MainApp`](https://github.com/AY2122S1-CS2103T-W08-2/tp/tree/master/src/main/java/seedu/sourcecontrol/MainApp.java). It is responsible for, * At app launch: Initializes the components in the correct sequence, and connects them up with each other. * At shut down: Shuts down the components and invokes cleanup methods where necessary. [**`Commons`**](#common-classes) represents a collection of classes used by multiple other components. -The rest of the App consists of four components. +The rest of Source Control consists of four components. -* [**`UI`**](#ui-component): The UI of the App. +* [**`UI`**](#ui-component): The UI. * [**`Logic`**](#logic-component): The command executor. -* [**`Model`**](#model-component): Holds the data of the App in memory. +* [**`Model`**](#model-component): Holds the data of Source Control in memory. * [**`Storage`**](#storage-component): Reads data from, and writes data to, the hard disk. **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`. +The *Sequence Diagram* below shows how the components interact with each other for the scenario where the user executes 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. +
+ 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) +The **API** of this component is specified in [`Ui.java`](https://github.com/AY2122S1-CS2103T-W08-2/tp/tree/master/src/main/java/seedu/sourcecontrol/ui/Ui.java) + +
+ +
+
+ -![Structure of the UI Component](images/UiClassDiagram.png) +The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `StudentListPanel`, `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 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/AY2122S1-CS2103T-W08-2/tp/tree/master/src/main/java/seedu/sourcecontrol/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/AY2122S1-CS2103T-W08-2/tp/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/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, * executes user commands using the `Logic` component. * listens 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 it displays `Student` object residing in the `Model`. + +
### Logic component -**API** : [`Logic.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/logic/Logic.java) +**API** : [`Logic.java`](https://github.com/AY2122S1-CS2103T-W08-2/tp/tree/master/src/main/java/seedu/sourcecontrol/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 uses the `AddressBookParser` class to parse the user command. -1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `AddCommand`) which is executed by the `LogicManager`. -1. The command can communicate with the `Model` when it is executed (e.g. to add a person). +1. When `Logic` is called upon to execute a command, it uses the `SourceControlParser` class to parse the user command. +1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g. `AddCommand`) which is executed by the `LogicManager`. +1. The command can communicate with the `Model` when it is executed (e.g. to add a student). 1. The result of the command execution is encapsulated as a `CommandResult` object which is returned back from `Logic`. +
+ +The Activity Diagram below illustrates how user input is parsed by the `Logic` component. + +
+ +
+
+ +
+ The Sequence Diagram below illustrates the interactions within the `Logic` component for the `execute("delete 1")` API call. -![Interactions Inside the Logic Component for the `delete 1` Command](images/DeleteSequenceDiagram.png) +
+ +
+
:information_source: **Note:** The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram.
+
+ 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 `SourceControlParser` 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 `SourceControlParser` 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) - +**API** : [`Model.java`](https://github.com/AY2122S1-CS2103T-W08-2/tp/tree/master/src/main/java/seedu/sourcecontrol/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 application data i.e. all `Student` objects (which are contained in a `UniqueStudentList` object). +* stores the currently 'selected' `Student` 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 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.
+
+ +
:information_source: **Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `SourceControl`, which `Student` references. This allows `SourceControl` to only require one `Tag` object per unique tag, instead of each `Student` needing their own `Tag` objects. Note that other classes such as `Group` and `Assessment` are omitted here for brevity.
- +
+ +
+
### Storage component -**API** : [`Storage.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/storage/Storage.java) +**API** : [`Storage.java`](https://github.com/AY2122S1-CS2103T-W08-2/tp/tree/master/src/main/java/seedu/sourcecontrol/storage/Storage.java) - +
+ +
+
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). +* can save both application data and user preference data in json format, and read them back into corresponding objects. +* inherits from both `SourceControlStorage` 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 -Classes used by multiple components are in the `seedu.addressbook.commons` package. +Classes used by multiple components are in the `seedu.sourcecontrol.commons` package. + +
-------------------------------------------------------------------------------------------------------------------- +
+ ## **Implementation** This section describes some noteworthy details on how certain features are implemented. -### \[Proposed\] Undo/redo feature +### Adding a student + +The `addstudent` feature adds a student with the provided name and NUSNET ID into the database. If the student comes with optionally specified groups and tags, these fields will be added accordingly. + +#### How the `addstudent` command works -#### Proposed Implementation +1. The user specifies the student name, NUSNET ID, and if applicable, the groups and tags belonging to the student. +2. If the name or NUSNET ID is not provided, the user will be prompted to enter them via an error message. +3. The ID is cross-referenced with the current students in the database, and an error is thrown if the student to add has the same ID as a pre-existing student. +4. If the student to add has a unique NUSNET ID, the groups, if provided, will be parsed individually. + * If the provided group exists, the student will be added into that group. + * If the provided group does not exist, a new group will be added, and the student will be added into that group subsequently. +5. A new `Student` object is created with the given name, NUSNET ID, groups, and tags, which is then added into the database. -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: -* `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. +The following activity diagram summarizes what happens when a user executes the `addstudent` command to add a new student. In the case where the student is not added, an error message will be displayed with the reason. -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. +
+ +
+
-Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. +The following sequence diagram summarizes what happens when the user inputs an `addstudent` command together with the name and NUSNET ID of the student to be added. -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. +
+ +
+
-![UndoRedoState0](images/UndoRedoState0.png) +
:information_source: **Note:** The lifeline for `AddStudentCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. This is also applicable to other sequence diagrams in this guide. +
-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. +
-![UndoRedoState1](images/UndoRedoState1.png) +### Creating a new group -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`. +The `addgroup` feature allows users to create new groups, as well as specify students to be added to the group to be created. -![UndoRedoState2](images/UndoRedoState2.png) +#### How the `addgroup` command works -
: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`. +1. The user specifies the group name, as well as a list of names and/or IDs of the students to be added into the group. +2. For each of the names and IDs, an `AllocDescriptor` is created. +3. For each of the `AllocDescriptors`, a search is done against the current `StudentList` to find students that match the descriptors. + * If there is one and only one match, the student is added to the group. +4. The group is added to the application if Step 3 completes without any exceptions. +
:information_source: **Note:** In the case where there are more than one students matched because they share the same name, an error message will be displayed to the user. The user will then have to specify the student to be added using his/her NUSNET ID.
-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. -![UndoRedoState3](images/UndoRedoState3.png) +
+ +The following activity diagrams summarizes what happens when a user executes the `addgroup` command to add a new group. In the case where the group is not added, an error message will be displayed with the reason. + + +
+ +
+
+ +
+ + +
+ +The following sequence diagram summarizes what happens when the user inputs an `addgroup` command together with a student to be added. + +
+ +
+
+
+ +
-
: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. +
+### Adding a student into a group + +The `addalloc` feature allows users to allocate a student into a group. + +#### How the `addalloc` command works + +1. The user specifies the group name, and the name or ID of the student to be allocated into the group. +2. An `AllocDescriptor` containing info of the group and the student is created. +3. The `AllocDescriptor` is used to find the group and the student(s) as specified. + * If there is only one matched student, the student is added to the group. + * If there are multiple matched students, the allocation is not made successfully, and the student list is updated with all matched students. +4. The student is allocated into the group. + +
:information_source: **Note:** In the case where there are more than one students matched because they share the same name, an error message will be displayed to the user. The user will then have to specify the student to be added using his/her NUSNET ID.
-The following sequence diagram shows how the undo operation works: +
+ +The following activity diagram summarizes what happens when a user executes the `addalloc` command to allocate a student into a group. In the case where the student is not added into the group, an error message will be displayed with the reason. + +
+ +
+
+ +
+ +The following sequence diagram summarizes what happens when the user inputs an `addalloc` command together with a group and a student, specified by name, to be allocated. + +
+ +
+
+ +
+ +
-![UndoSequenceDiagram](images/UndoSequenceDiagram.png) +
-
: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. +### Adding a score +The `addscore` feature allows users to add score for an assessment of a student. + +#### How the `addscore` command works + +1. The user specifies the assessment name, the name or ID of the student, and the score to be added. +2. An `ScoreDescriptor` containing info of the group, the student and the score is created. +3. The `ScoreDescriptor` is used to find the assessment and the student(s) as specified. + * If there is only one matched student, the assessment of the student will be updated with the new score. + * If there are multiple matched students, the update is not made successfully, and the student list is updated with all matched students. +4. The score is updated in the assessment of the student. + +
:information_source: **Note:** In the case where there are more than one students matched because they share the same name, an error message will be displayed to the user. The user will then have to specify the student to be added using his/her NUSNET ID.
-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. +
+ +The following activity diagram summarizes what happens when a user executes the `addscore` command to add score for an assessment of a student. In the case where the score is not added/updated, an error message will be displayed with the reason. + +
+ +
+
+ +
+ +The following sequence diagram summarizes what happens when the user inputs an `addscore` command together with an assessment, a student, specified by name, and a score to be added. + +
+ +
+
+
+ +
+ +
+ +### Searching for students + +The `search` feature allows user to filter student list by name, NUSNET ID, groups, or tags. -
: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. +#### How the `search` command works +The following sequence diagram summarizes what happens when the user inputs an `search` command together with a name to be searched for. + +
+ +
+
+ +A `Predicate` object will be created for each search command. +It contains `test(Student student)` function which checks if the given student matches the list of keywords given. + +To support the differentiated search functionality for different flags (e.g. name, NUSNET ID, group name, or tag), +multiple classes extending from `Predicate` can be created, +each with different implementation of the `test(Student student)` function. + +
+ +* `NameContainsKeywordsPredicate`: checks if any word in the full name of student matches exactly any word in the + given keywords. e.g. `Alex Yu` will match `Alex Yeoh` and `Bernice Yu`. Partial search is not supported + e.g. `Han` will not match `Hans`. +* `IdContainsKeywordsPredicate`: checks if the ID of student contains any word in the given keywords. + Partial search is supported. e.g. `E05` will match `E0523412`. +* `GroupContainsKeywordsPredicate`: checks if any group of student contains any word in the given keywords. + Partial search is supported. e.g. `T02` will match `T02A` and `T02B`. +* `TagContainsKeywordsPredicate`: checks if the tag of student contains any word in the given keywords. + Partial search is supported. e.g. `beginner` will match `beginners`. + +The following activity diagrams summarizes what happens when a user executes the `search` command to search for students with different filters. + +
+ +
+
+ +
:information_source: **Note:** SearchCommandParser checks +if the command input is valid. The command is invalid if the user input is empty, or if the user entered more or less than one flag.
-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. -![UndoRedoState4](images/UndoRedoState4.png) +
+ +### Importing data + +The `import` feature allows users to load data as specified in the provided CSV file. -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. +#### How the `import` command works -![UndoRedoState5](images/UndoRedoState5.png) +The following activity diagram summarizes what happens when a user executes the `import` command to import a CSV data file. In the case where the file is not imported, an error message will be displayed with the reason. -The following activity diagram summarizes what happens when a user executes a new command: +
+ +
+
- +
-#### Design considerations: +There are several important details left out of the activity diagram for the sake of clarity: -**Aspect: How undo & redo executes:** +1. The import feature is reliant on having a correctly formatted csv file (which is to be exported from sites like LumiNUS and Source Academy, and modified to fit the format). + The user needs to provide the number of `Groups`, `Assessments`, and `Tags` since we can't detect this automatically from the format of the file. The proper format of the file can be found in the user guide. -* **Alternative 1 (current choice):** Saves the entire address book. - * Pros: Easy to implement. - * Cons: May have performance issues in terms of memory usage. +1. A `CommandException` will be thrown if any input does not follow the formatting specified in the respective classes such as `Name`, `ID`, and `Score`. -* **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. +1. When reading a student's groups, the command will try to use an existing `Group` if possible, to ensure that the `Group` holds a reference to all `Students` in the group. A new `Group` will only be created in the case where the group hasn't already been created. -_{more aspects and alternatives to be added}_ +1. When reading a student's scores, the command will add the score to the `Student`, as well as the `Assessment` created from reading the first row. -### \[Proposed\] Data archiving +1. Columns can be empty, except for the assessment name columns in the header row, and the name and ID columns of each student. Empty columns are assumed to be missing data. -_{Explain here how the data archiving feature will be implemented}_ +
+### Showing assessment result analysis + +The `show` feature allows users to show the performance analysis of a student, a group or the cohort in an assessment. + +#### How the `show` command works + +1. The user specifies the student (by either name, ID or index), the group name or the assessment name. +2. An `ScoreDescriptor` containing info of the group, the student and the score is created. +3. The `ScoreDescriptor` is used to find the assessment and the student(s) as specified. + * If there is only one matched student, the assessment of the student will be updated with the new score. + * If there are multiple matched students, the update is not made successfully, and the student list is updated with all matched students. +4. The performance analysis of the student, the group or the cohort in an assessment is displayed. + +
:information_source: **Note:** In the case where the performance analysis of a student is requested by identity and there are more than one students matched because they share the same name, an error message will be displayed to the user. The user will then have to specify the student to be added using his/her NUSNET ID. +
+ + +The following activity diagrams summarize what happens when a user executes the `show` command to show the performance analysis of a student, a group or the cohort in an assessment. In the case where the display is not presented successfully, an error message will be displayed with the reason. + +
+ +
+ +
+ +
+ +
+
+ +
+
+ +
+ +The following sequence diagram summarizes what happens when the user inputs an `show` command together with a student specified by name. + +
+ +
+
+
+ +
+ +
+ +### Setting customized aliases for commands + +The `alias` feature allows users to define their own aliases for commands. This is useful to shorten the input for commands that the user uses often. + +An alias is basically just a mapping of a user-provided string to a command word. It works by directly replacing the first word in the user's input command with the alias word, then parsing again. +An alias contains two strings: An `aliasWord` which is the new user-defined word, and a `commandWord` which is the command word of an existing default command (e.g. `addstudent`, `clear`, `exit`). + +#### How the `alias` command works + +Parsing of an alias command follows the following steps: +1. The alias word is checked to ensure that it is one word long. +1. The alias word is also checked to ensure that it does not overlap with any default command word. This is to prevent the re-mapping of any default command words and potentially losing the functionality of the application. +1. The command word is checked for validity by attempting to parse the command word. If the parser does not recognize the command word, it is not valid. +1. If the command word is an alias, it is replaced with the command word that the alias maps to. +1. The new `Alias` and `AliasCommand` is created, and executed. +1. The alias is added to both the parser, and the model (so that it can be saved in the `UserPrefs`) + * If the alias word is already present in the parser, the command word it is mapped to is replaced with the new command word instead. + * If the alias word and the command word are identical, it is assumed that the user is trying to delete an existing alias. The existing alias is removed instead. + +If the command word of any future user input matches the alias, the first word of the user input will be replaced by the command word of the alias. +The activity diagram of how a command is parsed can be found in the [Logic section of this guide](#logic-component). + +
+ +#### Design considerations + +**Aspect 1: How aliases are stored and parsed** + +There were two ideas on how this could be done: +1. Store the command's `Parser` in each alias. The parser attribute of the alias can then be retrieved and `Parser::execute` can be called when parsing. + * Pros: + * Very elegant and streamlined parsing. Almost no changes need to be made to the logic of `SourceControlParser`. + * Cons: + * It is not easy to obtain the type of `Parser` simply through the command word provided by the user given the current implementation of the project. You'd need to iterate through all possible `Commands`, and match their command word with the provided word. This approach would not be very extensible if more commands are to be added in the future. + * It is also not easy to save/load the alias to/from the `UserPrefs`. The best approach is likely to just save the command word, then generate the `Parser` when loading. However, this poses a similar problem as the previous point, which is that it is not easy to generate the `Parser` just from the command word. +1. Store the command word in each alias. Replace the command word of the user input if the user inputs an aliased command, and parse it again. + * Pros: + * Very easy to store. Each alias is essentially just two strings. + * Cons: + * Have to be careful with the implementation to prevent any edge cases where the user can define aliases to create an infinite loop. + +We decided to go with the easier implementation of storing each alias as two strings. However, there were still more aspects to be considered. + +
+ +**Aspect 2: How to handle aliases of aliases** + +That is, what happens when the user does `alias -c -as `? There are two choices for this: +1. The new alias maps to the existing alias word directly. + * The command word stored in the new `Alias` would be the alias word of the existing `Alias`. + * Assuming that an alias `ag` already exists for `addgroup`, the following commands have the following effects: + * `alias -c ag -as addg` would make `addg` map to `ag`, which is in turn mapped to `addgroup`. When parsing, `addg` will be replaced with `ag`, which is then replaced by `addgroup`. + * `alias -c addalloc -as ag` would make `ag` map to `addalloc`. `addg` would also then map to `addalloc`, since it gets replaced by `ag` when parsing, which then gets replaced by `addalloc`. + * Pros: + * Intuitive approach to aliases: "I want `addg` to mean `ag`". + * Cons: + * Possible to make parsing a very expensive task due to having to replace the command word and parsing multiple times. + * Possible to have an infinite loop by mapping two aliases to each other. + * It's not very useful to have two aliases always mean the same thing. +2. The new alias maps to the command word that the existing alias maps to. + * The command word stored in the new `Alias` would be the command word of the existing `Alias`. + * Assuming that an alias `ag` already exists for `addgroup`, the following commands have the following effects: + * `alias -c ag -as addg` would make `addg` map to `addgroup` directly. + * `alias -c addalloc -as ag` would make `ag` map to `addalloc`. `addg` would still map to `addgroup`. + * Pros: + * Naturally prevents the creation of infinite loops. + * Parsing is guaranteed to only take one recursive call to the `parse` method. + * Cons: + * It is a less intuitive approach to aliases: "I want `addg` to mean what `ag` means at the current moment" + +We decided to go with implementation 2 due to its ability to naturally handle infinite loops and better performance. Our target audience is also Computer Science professors, who should be very familiar with this style of referencing, since that is exactly how names refer to primitive values in programming. + +
+ +**Aspect 3: Removing aliases** + +We believe that there needs to be a way to remove aliases. Otherwise, there will eventually be a very large amount of aliases, and some typo might lead to executing a command you didn't intend to execute. Hopefully, that command isn't `clear`. + +Again, there are two choices we could take: +1. Make a new command for removing an alias, e.g. `removealias`. + * Pros: + * Intuitive to know what the new command does from its command word. + * Cons: + * Might end up having too many commands for a less prominent function like aliasing. + * Takes a little more effort to implement. +2. Modify the existing `alias` command to accept a special case to remove aliases. + * More specifically, `alias -c -as ` will remove the alias. + * Pros: + * Easy to implement: Just check if alias word and command word are the same. + * One command for the whole alias functionality. + * Cons: + * Not particularly intuitive. The `alias` command feels like a command to use for adding aliases, but it is being used to remove one instead. + +We ended up going with the second approach since the alias functionality was a very small part of the application. We also believe that it does make sense that mapping an alias to itself would remove it. + +Furthermore, removing aliases is likely a very rare use case, and dedicating a whole command to it does feel like a bit of a waste. + +
-------------------------------------------------------------------------------------------------------------------- -## **Documentation, logging, testing, configuration, dev-ops** +
+ +## **Documentation, Testing, Logging, Configuration, Dev-Ops** * [Documentation guide](Documentation.md) * [Testing guide](Testing.md) @@ -249,83 +622,250 @@ _{Explain here how the data archiving feature will be implemented}_ * [Configuration guide](Configuration.md) * [DevOps guide](DevOps.md) +
+ -------------------------------------------------------------------------------------------------------------------- ## **Appendix: Requirements** ### Product scope -**Target user profile**: +**Target user profile:** -* has a need to manage a significant number of contacts +Targets professors of CS1101S who: +* has a need to manage a significant number of students +* has a need to analyse students' performance * prefer desktop apps over other types * can type fast * prefers typing to mouse interactions * is reasonably comfortable using CLI apps -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app +**Value proposition:** This app will help CS1101S professors keep track of students' performance after each assessment, +doing so faster than a typical mouse/GUI driven app. +It can analyse results of individual students, tutorial groups, or the whole cohort in each assessment, +in order to identify students who may require additional help. +
### User stories Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` +Importing and exporting data: + +| Priority | As a …​ | I want to …​ | So that I can…​ | +| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | +| `* * *` | CS1101S Professor | Upload large amounts of data from a file | Upload all assessment scores of all students at once | +| `* *` | Long-time user | Clear all data | Remove records from the previous semester | +| `* *` | CS1101S Professor | Export data | Share interesting findings with my colleagues | +| `*` | New user | Import data from the previous semester | Have an idea of how intakes of the previous cohort performed | + +Adding and editing data fields: + +| Priority | As a …​ | I want to …​ | So that I can…​ | +| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | +| `* * *` | CS1101S Professor | Add a new assessment | Keep track of assessment scores of students | +| `* * *` | CS1101S Professor | Add a new student | Add a new student without having to make a new file | +| `* * *` | CS1101S Professor | Allocate a student into existing group | Allocate groupings without having to make a new file | +| `* * *` | CS1101S Professor | Organize students into groups | Encourage peer learning | +| `* *` | CS1101S Professor | Remove a specific student | Update the system accordingly when a student drops the module | +| `* *` | CS1101S Professor | Annotate a student with a tag | See categories of students quickly | +| `* *` | CS1101S Professor | Add remarks to particular students | Be aware of any special conditions the student is facing | +| `* *` | CS1101S Professor | Edit assessment score for a particular student | Make changes after the initial grading | +| `*` | CS1101S Professor | Add attendance records for each class | Track the students present at each class | +| `*` | CS1101S Professor | Upload students’ feedback about their tutors | Provide timely feedback to the tutors | + +Viewing and searching data: + +| Priority | As a …​ | I want to …​ | So that I can…​ | +| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | +| `* * *` | CS1101S Professor | Search for specific student | Track his/her progress to facilitate better learning | +| `* * *` | CS1101S Professor | Search for students by classes and groups | Track them by groups easily | +| `* * *` | CS1101S Professor | Check a student’s grades | See individual performance | +| `*` | CS1101S Professor | Check attendance records for each student | Track if the student has been attending classes diligently | + +
+ +Analysing data: + | 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 | +| `* * *` | CS1101S Professor | Analyse the performances of individual students | Give special assistance to those in need | +| `* * *` | CS1101S Professor | Analyse performance of cohort in each assessment | See how the cohort is performing | +| `* *` | CS1101S Professor | Analyse the performances of students in groups | See which studio is under-performing and check in with the tutor | +| `* *` | CS1101S Professor | Calculate overall grades | Easily decide on grade ranges | +| `*` | Detailed user | View the performances under different kinds of graph | Have better visualisation about the performances of students | +| `*` | CS1101S Professor | Analyse the cohort performance for each question | Understand which are the topics students require more help with | +| `*` | CS1101S Professor | Compare between different batches of students | See if the module is too hard this semester as compared to previous semesters | +| `*` | CS1101S Professor | Check the overall performance of the tutors based on several indicators | Identify excellent tutors to be called back next semester | + +
-*{More to be added}* +Others: + +| Priority | As a …​ | I want to …​ | So that I can…​ | +| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | +| `* * *` | New user | Get command prompts when typing | Do not have to keep checking user guide | +| `* *` | Expert user | Have shortcuts for commands | Save time | +| `* *` | Potential user | View the app populated with sample data | See how the app looks like when in use | +| `*` | Forgetful user | Access the user guide with an easy to remember command | Lookup how to use a command/what command to use | +| `*` | Long-term user | Store meeting timings | Not miss any meetings that have been planned | +| `*` | Forgetful user | Have reminders about upcoming meetings | Avoid missing any important events | +| `*` | CS1101S Professor | Add TODO bug fixes accumulated throughout the semester | Fix them during CP3108 | + +
### Use cases -(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise) +(For all use cases below, the **System** is `Source Control` and the **Actor** is the `user`, unless specified otherwise) -**Use case: Delete a person** +#### Use case: Import student roster **MSS** -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 +1. User has a comma-separated values (`.csv`) file of the student roster. +2. User requests to import the file into Source Control. +3. Source Control shows the list of students parsed and imported. + Use case ends. +**Extensions** + +* 2a. User enters an incorrectly formatted csv file. + * 2a1. Source Control shows an error message. + * 2a2. User fixes the csv file. + Use case resumes at step 2. + + +#### Use case: Create a new group + +**MSS** + +1. User requests to create a new group and enters the group name and optionally the students' names or NUSNET IDs. +2. Source Control creates the group with the specified students. Use case ends. **Extensions** -* 2a. The list is empty. +* 1a. User enters an invalid group name. + * 1a1. Source Control shows an error message. + Use case resumes at step 1. - Use case ends. -* 3a. The given index is invalid. +* 1b. User enters a group name which coincides with an existing group in the database. + * 1b1. Source Control shows an error message. + Use case resumes at step 1. + +* 1c. User enters a student name which match with multiple students in the Source Control database. + * 1c1. Source Control shows the list of students with matching names, and prompts the user to resolve the conflict by specifying the target student's NUSNET ID instead. + Use case resumes at step 1. - * 3a1. AddressBook shows an error message. - Use case resumes at step 2. +* 1d. User enters a list of student name contains duplicated students. + * 1d1. Source Control shows an error message to inform user of the duplicates. + Use case resumes at step 1. + + +#### Use case: Showing a student's performance + +**MSS** + +1. User requests to view the performance of a specified student by providing the index of the student in the list, or the student's name or NUSNET ID. +2. Source Control displays a line chart showing the specified student's performance against the cohort performance for each assessment. + Use case ends. + +**Extensions** + +* 1a. User enters a student name or ID that does not exist in the database. + * 1a1. Source Control shows an error message. + Use case resumes at step 1. + +* 1b. User enters an invalid index. + * 1b1. Source Control shows an error message. + Use case resumes at step 1. + + +#### Use case: Showing a group's performance + +**MSS** + +1. User requests to view the performance of a specified group by providing the group name. +2. Source Control displays a line chart showing the specified group's performance against the cohort performance for each assessment. + Use case ends. + +**Extensions** -*{More to be added}* +* 1a. User enters a group name that does not exist in the database. + * 1a1. Source Control shows an error message. + Use case resumes at step 1. -### 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 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. +#### Use case: Showing cohort performance for an assessment + +**MSS** + +1. User requests to view the cohort performance for a specified assessment. +2. Source Control displays a histogram showing the score distribution of the cohort in the specified assessment. + Use case ends. + +**Extensions** + +* 1a. User enters an assessment that does not exist in the database. + * 1a1. Source Control shows an error message. + Use case resumes at step 1. + +
+ +#### Use case: Creating an alias for an existing command + +**MSS** -*{More to be added}* +1. User requests to create an alias for a specified existing command. +2. Source Control accepts the user-created alias. + +**Extensions** + +* 1a. User enters a command that does not exist. + * 1a1. Source Control shows an error message. + Use case resumes at step 1. + + +* 1b. User tries to create an alias using a default command. + * 1b1. Source Control shows an error message. + Use case resumes at step 1. + + +* 1c. User enters an invalid alias name. + * 1c1. Source Control shows an error message. + Use case resumes at step 1. + +
+ +### 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 1000 students without a noticeable sluggishness in performance for typical usage. +3. Should be able to analyse data of up to 1000 students without a noticeable sluggishness in performance for typical usage. +4. 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. +5. Should work without internet connections. +6. New users can pick up the basic functionalities of Source Control easily. ### Glossary -* **Mainstream OS**: Windows, Linux, Unix, OS-X -* **Private contact detail**: A contact detail that is not meant to be shared with others +* **Mainstream OS:** Windows, Linux, Unix, OS-X. +* **Student:** A student in the database, identified by their name and NUSNET ID. Each student can be in multiple groups, and can have scores for multiple assessments. +* **Group:** A group of students, identified by the group name. +* **Assessment:** An assessment is identified by the assessment name. +* **Score:** The score that a student has attained for an assessment, stored in percentage and can be recorded up to 2 decimal places. Each student can only have 1 score per assessment. +* **Student list:** The list of students displayed on the right panel of Source Control. Student list can be filtered to display selected students only. +* **Flag:** Arguments flags are used to indicate different types of user inputs e.g. `-n` for student name, and `-g` for group. More about flags can be found [here](https://ay2122s1-cs2103t-w08-2.github.io/tp/UserGuide.html#glossary). + +
-------------------------------------------------------------------------------------------------------------------- -## **Appendix: Instructions for manual testing** +
+ +## **Appendix: Instructions for Manual Testing** Given below are instructions to test the app manually. @@ -338,40 +878,184 @@ testers are expected to do more *exploratory* testing. 1. Initial launch - 1. Download the jar file and copy into an empty folder + 1. Download the jar file and copy into an empty folder. 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. -1. Saving window preferences +2. Saving window preferences 1. Resize the window to an optimum size. Move the window to a different location. Close the window. 1. Re-launch the app by double-clicking the jar file.
Expected: The most recent window size and location is retained. -1. _{ more test cases …​ }_ +3. Shutdown + + 1. Test case: `exit`
Expected: Source Control exits and shut down automatically. + +
+ +### Adding a group + +1. Adding a group successfully. + + 1. Prerequisites: Group should not already exist in database. Database contain a student with name `Hong Fai` and another student `A` with ID `E0123456`. + + 2. Test case: `addgroup -g T01A`
Expected: Group `T01A` will be added to the database. + + 3. Test case: `addgroup -g T02B -n Hong Fai -i E0123456`
Expected: Group `T02B` will be added to database with students `Hong Fai` and `A` in the group. + +2. Adding a group with incorrect formats. + + 1. Test case: `addgroup`
Expected: No group is created. Error detail shown in the status message to inform user of the correct command format. + + 2. Test case: `addgroup -g`
Expected: No group is created. Error detail shown in the status message to inform user that group name cannot be blank. + + 3. Test case: `addgroup -g Tutorial@Wednesday`
Expected: No group is created. Error detail shown in the status message to inform user that group name must be alphanumeric. + +3. Adding a group that already exists in database. + + 1. Prerequisite: Group specified already exists in database. + + 2. Test case: `addgroup -g T01A`
Expected: The group will not be re-created. Error details shown in the status message to inform user group already exists. + +4. Adding a group with duplicated student instances. + + 1. Prerequisites: Database contain a student with name `Hong Fai` with the ID `E0123456`. + + 2. Test case: `addgroup -g T01A -n Hong Fai -i E0123456`
Expected: The group will not be created. Error detail shown in the status message to inform user that the student `Hong Fai` has been specified more than once. -### Deleting a person +
-1. Deleting a person while all persons are being shown +5. Adding a group with non-existent students. - 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. + 1. Prerequisites: Database does not contain a student with name `Hong Fai`. + + 2. Test case: `addgroup -g T01A -n Hong Fai`
Expected: The group will not be created. Error detail shown in the status message to inform user that the student `Hong Fai` cannot be found in the database. - 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. - 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. +### Deleting a Student - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
- Expected: Similar to previous. +1. Deleting a student while a list of all students are being shown. -1. _{ more test cases …​ }_ + 1. Prerequisites: List all students using the `list` command. Some students are displayed in the list. + + 1. Test case: `delete 1`
+ Expected: First student is deleted from the list. Details of the deleted student is shown in the status message. Timestamp in the status bar is updated. + + 1. Test case: `delete 0`
+ Expected: No student is deleted. Error details shown in the status message. Status bar remains the same. + + 1. Other incorrect delete commands to try: `delete`, `delete x` (where x is an invalid number)
+ Expected: Similar to previous. + +2. Deleting a student after searching. + + 1. Prerequisites: List selected students using the `search` command. Some students are displayed in the list. + + 2. Test case: `delete 1`
Expected: First student displayed on the sorted list is deleted. Details of the deleted student is shown in the status message. Timestamp in the status bar is updated. + + 3. Other incorrect delete commands to try: `delete`, `delete x` (where x is an invalid number)
+ Expected: Similar to previous. + +
+ +### Adding an alias + +1. Adding an alias successfully. + + 1. Test case: `alias -c addstudent -as example`
Expected: A new alias `example` is added for `addstudent`.
`example -n Zhiying -i E1234567` will add student `Zhiying` to the database. + + 2. Test case: `alias -c example -as example2`
Expected: A new alias `examples` is added for `addstudent` (the command `example` is mapped to). + + 3. Test case: `alias -c addgroup -as example`
Expected: The alias `example` is mapped to `addgroup` and no longer represent `addgroup`.
`example -g T02A` will create a new group `T02A`. + +2. Deleting an alias successfully. + + 1. Prerequisites: An alias `example` exists. + + 2. Test case: `alias -c example -as example`
Expected: Removes the alias `example`. `example` will no longer be recognized as a command. + +3. Deleting an alias unsuccessfully. + + 1. Test case: `alias -c addstudent -as addstudent`
Expected: The alias `addstudent` will not be removed. + Error detail shown in the status message to inform user that default command cannot be overwritten.
+
+4. Adding an alias unsuccessfully. + + 1. Test case: `alias -c addgroup -as addstudent `
Expected: No alias will be created. Error detail shown in the status message to inform user that default command cannot be overwritten. + + 2. Test case: `alias -c addstudent -as add student `
Expected: No alias will be created. Error detail shown in the status message to inform user that alias can only be one alphanumeric word. + +
### Saving data -1. Dealing with missing/corrupted data files +1. Dealing with missing data file - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ + 1. Prerequisites: `./data/sourceControl.json` (or the file path specified in `preferences.json`) has been deleted. + + 1. Test case: Run the application.
Expected: Application is populated with sample data. `./data/sourceControl.json` will be created after exiting the application. + +1. Dealing with corrupted data file + + 1. Prerequisites: Deleted the data file at `./data/sourceControl.json`, then launched the jar file to populate the data file with sample data. + + 1. Test case: Add an `*` to the `name` of the first person in the data file and launch the jar file.
Expected: No data will be loaded into the application. The data file will lose all its data when the app exits. + + 1. Test case: Add the group `new group` into the `groups` array of the first person and launch the jar file.
Expected: The new group is not added to the first person. The data file will no longer contain the new group when the app exits. + + 1. Test case: Add the assessment `new assessment` into the `assessments` array of the first person and launch the jar file.
Expected: The new assessment is not added to the first person. The data file will no longer contain the new assessment when the app exits. + + 1. Test case: Add an extra `{` as the first character of the data file.
Expected: No data will be loaded into the application. The data file will lose all its data when the app exits. + + +-------------------------------------------------------------------------------------------------------------------- -1. _{ more test cases …​ }_ +
+ +## **Appendix: Effort** + +In general, we believe that we have put in a lot of effort for this project. We managed to implement the target features we originally set out to do, plus a few extra nice-to-have features. + +We also managed to keep the code coverage reasonably high at 76%, and there was only a small set of unique bugs found in the PE-D. + +### Challenges faced + +The following features were relatively hard to implement: +1. JavaFX Charts are a part of the JavaFX library which we had to learn how to use from scratch. However, it was an essential part of our goals for the application. + * It was not easy to customize the chart styles (such as the colour scheme) to fit with the overall design of the application. + * Many things about the format of the charts were difficult to customize, such as the direction and layout of axis labels. + +1. Aliases were deceptively hard to implement, and took more time than expected. + * Even though Jonas implemented an alias feature for his iP, the implementation had to be completely different since the parser was using a completely different design. + * Many aspects had to be considered and carefully weighed. You can read more about it in the [alias section of this guide](#alias-feature). + +1. Adapting the AB3 code to fit into the context of Source Control. + * While adding and removing classes wasn't too bad, adapting the tests was on a whole new level. + * There were a lot of tests that we didn't want to delete, and instead chose to adapt them as well. + This proved to be highly time-consuming, since you had to read through the test code and completely understand what it was doing in order to properly adapt it. + * Our application has three different classes (`Student`, `Assessment`, `Group`) that are highly coupled to each other. AB3 did not have such problems as it only contained one `Person` class. + It was difficult to ensure that our objects are sharing information with each other properly, while trying to keep the level of coupling as low as possible. This coupling also proved to be the source of several hard-to-find bugs. + +
+ +### Features scrapped + +The following features were scrapped due to the high difficulty or error-prone nature of their implementations. +1. Allowing the user to specify their file path to export data or graphs + * From the feedback received from the PE-D, we realized how easy it was to exploit the ability to specify your file path through user input. + * One problem was how different OSes handled file paths. Some paths were valid only on some OSes, and checking the user input was non-trivial. + * Java's `Path` and `File` felt insufficient to properly check all possible user inputs for validity. Through testing, it seemed like some invalid file names were still leaking through any checks we had implemented. + * These problems were discovered too late to settle with a good, bug-free solution. As such, we decided to drop the ability for users to specify files to write to. + * While one solution was to use the `FileChooser` class from JavaFX, that would violate the requirements of the application being CLI-based. + +2. Commands longer than one word in length + * Initially, `addstudent` was `add student` (the same goes for other similar commands in the `add` family). This required some minor tweaking of the parser which was relatively straightforward. + * However, when it came to implementing aliases, we realized that it was difficult to ensure that the aliases don't overlap with the multi-worded commands without drastically changing the parser. + * Thankfully, the simple solution was to just enforce everything to be one word long. + +3. Allowing non-alphanumeric characters in student names + * As Source Control stores the official names of students, we had to consider that these names could contain non-alphanumeric characters, such as "Mary-Ann Tan". This is in contrast with AB3, where names of contacts do not need to be official names and these non-alphanumeric characters can be left out. + * However, allowing non-alphanumeric characters in student names would lead to problems such as not being able to identify invalid names like "@@@". It also leads to complications with parsing, for example when "-g" is meant to be part of the student name but would be parsed as an argument. + * As such, given the time constraint, we decided to maintain the original functionality of AB3, and only allow alphanumeric characters. diff --git a/docs/SettingUp.md b/docs/SettingUp.md index 275445bd551..3780c18e3df 100644 --- a/docs/SettingUp.md +++ b/docs/SettingUp.md @@ -23,7 +23,7 @@ If you plan to use Intellij IDEA (highly recommended): 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. **Verify the setup**: - 1. Run the `seedu.address.Main` and try a few commands. + 1. Run the `seedu.sourcecontrol.Main` and try a few commands. 1. [Run the tests](Testing.md) to ensure they all pass. -------------------------------------------------------------------------------------------------------------------- diff --git a/docs/Testing.md b/docs/Testing.md index 8a99e82438a..59271dd528f 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -29,8 +29,8 @@ There are two ways to run tests. This project has three types of tests: 1. *Unit tests* targeting the lowest level methods/classes.
- e.g. `seedu.address.commons.StringUtilTest` + e.g. `seedu.sourcecontrol.commons.StringUtilTest` 1. *Integration tests* that are checking the integration of multiple code units (those code units are assumed to be working).
- e.g. `seedu.address.storage.StorageManagerTest` + e.g. `seedu.sourcecontrol.storage.StorageManagerTest` 1. Hybrids of unit and integration tests. These test are checking multiple code units as well as how the are connected together.
- e.g. `seedu.address.logic.LogicManagerTest` + e.g. `seedu.sourcecontrol.logic.LogicManagerTest` diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 3716f3ca8a4..837b9a1e84e 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -2,191 +2,681 @@ layout: page title: User Guide --- +Welcome to **Source Control** 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. +Source Control is a **desktop app for CS1101S professors to manage the performance of their students**. -* Table of Contents -{:toc} +This application allows you to **store comprehensive data** of each student quickly, and allows you to search through them easily. +With Source Control, you will never have to worry about your large student cohort and tracking numerous assessments! + +Source Control can also give you both a quick overview and a closer look of how your students are performing. +This application **provides in-depth data analysis** of the performance of your students in each assessment, +giving you timely feedback on the pace and difficulty level of the module. + +Source Control is **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, Source Control can help you track your students' performance faster than traditional GUI apps! + +This guide takes you through all the _latest features_ of Source Control. If you are a new user, this guide provides you +all the basic knowledge to [get started](#quick-start) with Source Control. If you are an experienced user, +feel free to skip to the [command summary](#command-summary) and experiment with our more [advanced features](#setting-customised-aliases-for-commands--alias)! + +
-------------------------------------------------------------------------------------------------------------------- -## Quick start +
+ +## **Table of Contents** + +* [**Overview**](#) +* [**Table of Contents**](#table-of-contents) +* [**Quick Start**](#quick-start) +* [**Features**](#features) + * [Notes on Command Format](#features) + * Adding Data + * [Adding a student : `addstudent`](#adding-a-student--addstudent) + * [Creating a new group : `addgroup`](#creating-a-new-group--addgroup) + * [Adding a student into a group : `addalloc`](#adding-a-student-into-a-group--addalloc) + * [Creating a new assessment : `addassessment`](#creating-a-new-assessment--addassessment) + * [Adding a score : `addscore`](#adding-a-score--addscore) + * Searching Data + * [Listing all students : `list`](#listing-all-students--list) + * [Searching for students : `search`](#searching-for-students--search) + * Analysing Data + * [Showing assessment result analysis : `show`](#showing-assessment-result-analysis--show) + * Editing Data + * [Editing a student : `edit`](#editing-a-student--edit) + * [Deleting a student : `delete`](#deleting-a-student--delete) + * Importing and Exporting Data + * [Importing data : `import`](#importing-data--import) + * [Exporting data : `export`](#exporting-data--export) + * General Features + * [Clearing all data : `clear`](#clearing-all-data--clear) + * [Exiting the app : `exit`](#exiting-the-app--exit) + * [Viewing help : `help`](#viewing-help--help) + * [Accessing command history](#accessing-command-history) + * Advanced Features + * [Setting customized aliases for commands: `alias`](#setting-customized-aliases-for-commands--alias) + * [Editing the data file](#editing-the-data-file) +* [**FAQ**](#faq) +* [**Glossary**](#glossary) +* [**Command Summary**](#command-summary) + +
+ +-------------------------------------------------------------------------------------------------------------------- + +
+ +## **Quick Start** 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. Download the latest `sourceControl.jar` from [here](https://github.com/AY2122S1-CS2103T-W08-2/tp/releases). -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +1. Copy the file to the folder you want to use as the _home folder_ for Source Control. -1. Double-click the file to start the app. The GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.
- ![Ui](images/Ui.png) +1. Double-click the file to start the app. The GUI shown in the image below should appear in a few seconds. Note how the app contains some sample data.
+ ![StartingUi](images/StartingUi.png) -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.
+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: - * **`list`** : Lists all contacts. + * **`addstudent`**`-n John Doe -i E0123456` : Adds a student named `John Doe` with NUSNET ID `E0123456` into the database. - * **`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. + * **`addgroup`**`-g T01A -n John Doe` : Adds a group called `T01A` into the database and student `John Doe` into the group. - * **`delete`**`3` : Deletes the 3rd contact shown in the current list. + * **`addscore`**`-a P01 -n John Doe -s 12`: Adds score for assessment `P01` as `12` for student `John Doe`. - * **`clear`** : Deletes all contacts. + * **`search`**`-n John Doe` : Searches for student `John Doe`. - * **`exit`** : Exits the app. + * **`show`**`-n John Doe` : Displays information of student `John Doe`. 1. Refer to the [Features](#features) below for details of each command. +[Return to Table of Contents](#table-of-contents) + +
+ -------------------------------------------------------------------------------------------------------------------- -## Features +
+ +## **Features**
**:information_source: Notes about the command format:**
-* 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`. +* ``:
+ Words in `` are the parameters to be supplied by the user.
+ E.g. in `addsudent -n `, `` is a placeholder which can be used as `addstudent -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`. +* `(round brackets)`:
+ Parameters in `(round brackets)` separated by `|` are mutually exclusive options for input. Only one input should to be supplied by the user.
+ E.g. `(-n | -i | -group )` can be used as `-n John Doe`, or as `-i E0123456`, or as `-g T02A` -* 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. +* `[square brackets]`:
+ Parameters in `[square brackets]` are optional.
+ E.g. `-n [-g ]` can be used as `-n John Doe -g T01A`, or as `-n John Doe`. + +* `...​`:
+ Items with `...​` after them can be used multiple times, including zero times.
+ E.g. `[-g ]...` can be used as ` ` (i.e. 0 times), or `-g T01A -g R01A`. * 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 -g `, then `-g -n ` is also acceptable. -* If a parameter is expected only once in the command but you specified it multiple times, only the last occurrence of the parameter will be taken.
- e.g. if you specify `p/12341234 p/56785678`, only `p/56785678` will be taken. +* If a parameter is expected only once in the command, but you specify it multiple times, only the last occurrence of the parameter will be taken.
+ E.g. if you specify `-g T02A -g T03B` and the command only expects one group, only `-g T03B` will be taken. -* 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`. +* Extraneous parameters for commands that do not take in parameters (such as `clear`) will be ignored.
+ E.g. if the command specifies `clear 123`, it will be interpreted as `clear`.
-### Viewing help : `help` +[Return to Table of Contents](#table-of-contents) -Shows a message explaning how to access the help page. +
-![help message](images/helpMessage.png) +### Adding a student : `addstudent` -Format: `help` +Adds a student into the database. + +Format: `addstudent -n -i [-g ]... [-t ]...` +* Adds a new student into the database with the given name and NUSNET ID. +* Adds the student into the specified groups if applicable. If the group does not already exist, a new group would be created. +* Adds the tags to the student if applicable. Tag name has to be one alphanumeric word. +* There should not be an existing student with the same NUSNET ID. If there is, the student to be added is considered invalid. -### Adding a person: `add` +Examples: +* `addstudent -n Jonas Chow -i E0123456` + * adds the student Jonas Chow with the given NUSNET ID. +* `addstudent -n Jonas Chow -i E0123456 -g T01A -g R01A` + * adds the student Jonas Chow and allocates him into groups `T01A` and `R01A`. +* `addstudent -n Jonas Chow -i E0123456 -t beginner` + * adds the student Jonas Chow and tags him with `beginner`. -Adds a person to the address book. +[Return to Table of Contents](#table-of-contents) -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` -
:bulb: **Tip:** -A person can have any number of tags (including 0) +### Creating a new group : `addgroup` + +Creates a new group and adds students into the group. + +Format: `addgroup -g [(-n | -i )]...` + +* Creates a new group with the given group name. The group should not already exist. +* Group name can be any number of alphanumeric words. +* Students can be identified by their name or NUSNET ID. +Name is case-sensitive and only allows exact match. E.g. `Jonas` will not match `Jonas Chow`. +* If multiple students have the same name, NUSNET ID needs to be used to identify them. + +
+**:information_source: Notes:**
+ +If duplicated students are found in the list of names and NUSNET IDs input, an error will be shown to inform you of +the clash, and the group will not be created.
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` +* `addgroup -g T01A` + * creates group `T01A`. +* `addgroup -g T01A -n Hong Yao -n Hong Fai` + * creates group `T01A` and adds `Hong Yao` and `Hong Fai` into the group. +* `addgroup -g T01A -n Hong Yao -i E0123456` + * creates group `T01A` and adds `Hong Yao` and student with NUSNET ID `E0123456` into the group. + +[Return to Table of Contents](#table-of-contents) + + +### Adding a student into a group : `addalloc` + +Allocates an existing student into an existing group. + +Format: `addalloc -g (-n | -i )` +* Adds the student into an existing group specified by the group name. The group specified must already exist. +* Students can be identified by their name or NUSNET ID. + Name is case-sensitive and only allows exact match. E.g. `Jonas` will not match `Jonas Chow`. +* If multiple students have the same name, NUSNET ID needs to be used to identify them. + +Examples: +* `addalloc -g T01A -n Zhiying` + * adds `Zhiying` into the tutorial group `T01A`. +* `addalloc -g T02A -i E0123456` + * adds student with NUSNET ID `E0123456` into the tutorial group `T02A`. + +[Return to Table of Contents](#table-of-contents) + + +### Creating a new assessment : `addassessment` -### Listing all persons : `list` +Creates a new assessment in the database. -Shows a list of all persons in the address book. +Format: `addassessment -a ` +* The assessment should not already exist in database. +* Assessment name can be any number of alphanumeric words. + +
+ +Examples: +* `addassessment -a P01` + * creates a new assessment `P01`. +* `addassessment -a Midterm Assessment` + * creates a new assessment `Midterm Assessment`. + +[Return to Table of Contents](#table-of-contents) + + +### Adding a score : `addscore` + +Adds students' score for an existing assessment into the database. + +Format: `addscore -a (-n | -i ) -s ` +* The assessment must already exist in database. +* Updates the student's score if the student already has a score for the assessment. +* Scores given should be in percentage out of a 100, and can be accurate to 2 decimal places. +* Students can be identified by their name or NUSNET ID. + Name is case-sensitive and only allows exact match. E.g. `Jonas` will not match `Jonas Chow`. +* If multiple students have the same name, NUSNET ID needs to be used to identify them. + +Examples: +* `addscore -a Midterm -n Van Nhi -s 95` + * records score for `Van Nhi` in `Midterm` to be `95.00` percent. +* `addscore -a P01 -i E0123456 -s 75.25` + * records score for NUSNET ID `E0123456` in `P01` to be `75.25` percent. + +[Return to Table of Contents](#table-of-contents) + + +### Listing all students : `list` + +Displays a list of all students in the student list. Format: `list` +
+ +[Return to Table of Contents](#table-of-contents) + +
+ +### Searching for students : `search` + +Finds students who match the input keywords. + +Format: `search (-n | -i | -g | -t )` + +* Search for students by their name, NUSNET ID, tag, or the group they belong in. +Only one flag should be used for each search (e.g. having `-n` and `-g` arguments are not allowed.) . +* To search with multiple keywords, separate keywords with spaces. E.g. `search -g T02A R03C`. +* The search is case-insensitive. E.g. `jonas` will match `Jonas`. +* The order of the keywords does not matter. E.g. `Jonas Chow` will match `Chow Jonas`. +* Students matching at least one keyword will be returned. E.g. `Jonas Leong` will return `Jonas Chow`, `Leong Hong Fai`. +* For search with name, + * only full words will be matched. E.g. `Jon` will not match `Jonas`. +* For search with NUSNET ID, group name or tag, + * partial search is supported. E.g. `T02` will match `T02A` and `T02B`. `beginner` will match `beginners`. + +Examples: +* `search -n Jonas Chow` + * returns a list of students with part of names that matches `Jonas` and `Chow`. +* `search -i E0123456` + * returns the student with NUSNET ID `E0123456`, if found in database. +* `search -g T02B R03C` + * returns a list of students in the groups `T02B` and `R03C`. +* `search -g T02` + * returns a list of students in all tutorial groups with names containing `T02`, e.g. `T02A` and `T02B`. +* `search -t beginner` + * returns a list of students tagged as `beginner`. + +[Return to Table of Contents](#table-of-contents) + +
+ +### Showing assessment result analysis : `show` + +Shows the in-depth data analysis of individual, group, or the cohort's performance for assessments. + +Format: `show ( | -n | -i | -g | -a ) [-f]` + +* Using ``, `-n ` or `-i ` displays the information of the particular student's performance in all his graded assessments. + * The student's score in each assessment, as well as the cohort mean and median score, will be shown as a line graph. + * The `` refers to the index number shown in the displayed student list. The `` must be a positive integer 1, 2, 3, … + * If multiple students have the same name, NUSNET ID needs to be used to identify them. +* Using `-g ` displays the information of the group's performance in all their graded assessments. + * The group's mean score in each assessment, as well as the cohort mean and median score, will be shown as a line graph. +* Using `-a ` displays the information of the cohort's performance in the particular assessment. + * The cohort's score distribution will be shown as a histogram. +* Keywords are case-sensitive and only allows exact match. E.g. `T01` does not match `T01A`, `jonas` will not match `Jonas`. +* Entering `-f` exports the graph produced from the command to a `.png` file in the same directory as your JAR file. + +
+ +Examples: +* `show -n Hong Fai` + * displays the following line chart of `Hong Fai`'s performance in all his assessments.
+ ![showStudentExample](images/UGExamples/showstu.png) + +* `show -n Hong Fai -f` + * exports the following line chart of `Hong Fai`'s performance in all his assessments.
+ ![showExportExample](images/UGExamples/showexport.png) + +* `show -g T02A` + * displays line chart of group `T02A`'s performance in their assessments.
+ ![showGroupExample](images/UGExamples/showgrp.png) -### Editing a person : `edit` +* `show -a Midterm Examination` + * displays histogram of the distribution of scores in the `Midterm Examination` assessment.
+ ![showAssessmentExample](images/UGExamples/showasses.png) -Edits an existing person in the address book. +[Return to Table of Contents](#table-of-contents) -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +
-* 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, …​ +### Editing a student : `edit` + +Edits the information of an existing student. + +Format: `edit [-n ] [-i ] [-g ]... [-t ]...` + +* Edits the student at the specified ``. Search can be used before this to identify the student in need. +* The `` refers to the index number shown in the displayed student list. The `` 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. +* When editing tags and groups, all existing tags and groups of the student will be removed, i.e. adding of tags and groups are not cumulative. +* You can remove all the student’s tags or groups by typing `-t` or `-g` without specifying any values after it. 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. +* `edit 1 -n Hong Yao -i E1234567` + * edits the name of the 1st student on the displayed list to `Hong Yao` and his NUSNET ID to `E1234567`. +* `edit 3 -g T01C -g R01A` + * edits the group of the 3rd student on the displayed list to `T01C` and `R01A`. +* `search -n Hong Yao` followed by `edit 1 -t` + * removes all tags of the 1st student on the displayed list after searching for `Hong Yao`. + +[Return to Table of Contents](#table-of-contents) + + +### Deleting a student : `delete` -### Locating persons by name: `find` +Deletes the specified student from the student list. -Finds persons whose names contain any of the given keywords. +Format: `delete ` -Format: `find KEYWORD [MORE_KEYWORDS]` +* Deletes the student at the specified ``. +* The `` refers to the index number shown in the displayed student list. The `` **must be a positive integer** 1, 2, 3, … -* 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) +* `list` followed by `delete 2` + * deletes the 2nd student in the student list. +* `search -n Van Nhi` followed by `delete 1` + * deletes the 1st student in the results of the `search` command. + +[Return to Table of Contents](#table-of-contents) + + +### Importing data : `import` + +Loads data as specified in the provided CSV file. + +Format: `import -f [-g ] [-a ] [-t ]` + +* The file path can be either the absolute path or the relative path. The relative path will be relative to the folder your JAR file is located. +* The first row of the CSV file needs to be headers for the respective columns. +* The header for the assessment columns should the name of the assessment. For example, `Rune Trials`. The header for every other column does not matter. +* Every row apart from the first represents a student. +* The first two columns should contain the student’s name and NUSNET ID. +* The next `` columns, should contain the student’s groups. +* The next `` columns, should contain the student’s grade in the respective assessments. +* The next `` columns, should contain the student's tags. +* The number of group columns, assessment columns, and tag columns are assumed to be 0 if they are not specified. + +
+**:information_source: Notes:**
+ +* `` refers to the number of **columns** the groups occupy in the CSV file, not the number of **types** of groups. +The similar applies for ``. + +* If the student does not have as many groups as the number of group columns, you should leave several group columns blank. + +* If the student does not have a grade for some assessment, you should leave the corresponding assessment column blank. + +
+ +
+ +
:exclamation: **Caution:** +The import command overwrites the current data with the newly imported data. Make sure you don't need the existing data before importing new data. -### Deleting a person : `delete` +You can use the [`export`](#exporting-data--export) command (explained in the next section) to get a backup of the current data before importing new data. +
-Deletes the specified person from the address book. +An example of how the CSV should be formatted is shown below: -Format: `delete INDEX` +![CsvFormatExample](images/UGExamples/importeg.png) -* 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, …​ +The command to import this file would be `import -f student_data.csv -g 2 -a 2 -t 2`. +(There are 2 group columns, 2 assessments columns, and 2 tag columns.) 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. +* `import -f /home/prof/CS1101S/student_data.csv -g 2 -a 10 -t 1` + * imports from the absolute path, and database has 2 group columns, 10 assessments columns, and 1 tag column. +* `import -f student_data.csv -g 5 -a 20` + * imports from the relative path, and database has 5 group columns and 20 assessments. + +[Return to Table of Contents](#table-of-contents) + +
+ +### Exporting data : `export` + +Exports data into a CSV file. Can be used to back up the data, or to share the data to others for them to load into their copy of Source Control. + +Format: `export` + +* The CSV file will be saved to `sourceControl.csv` in the same directory as your JAR file. +* The format of the CSV file saved matches exactly the CSV format used by the `import` command. + +
+ +:bulb: **Tips:**
+ +* The exported CSV file can be imported again to restore the state of the application when the data was exported. + +
+ +[Return to Table of Contents](#table-of-contents) -### Clearing all entries : `clear` -Clears all entries from the address book. +### Clearing all data : `clear` + +Clears all existing data. + +
:exclamation: **Caution:** + +You can't undo this command! You can use the [`export`](#exporting-data--export) command to get a backup of the data before clearing. + +
Format: `clear` -### Exiting the program : `exit` +[Return to Table of Contents](#table-of-contents) + + +### Exiting the app : `exit` -Exits the program. +Exits the application. Format: `exit` -### Saving the data +[Return to Table of Contents](#table-of-contents) + + +### Viewing help : `help` + +Shows a message explaining how to access the help page. + +![help message](images/helpMessage.png) + +Format: `help` + +[Return to Table of Contents](#table-of-contents) + + +### Accessing command history + +Retrieves past command lines input. + +* This can be done using the up and down arrow keys. +* Only successful command input would be recorded. Unsuccessful inputs which shows an error will not be saved. +* Using the up arrow key retrieves the previous input. +* Using the down arrow key retrieves the next input. + +[Return to Table of Contents](#table-of-contents) + +
+ +### Setting customized aliases for commands : `alias` + +Sets up an alias that can be used instead of a command. + +Format: `alias -c -as ` + +* `` refers to any command keywords e.g. `search`, `addalloc`, `addstudent`. +* The `` can only be a single alphanumeric word. +* Multiple aliases can be set for each command. +* Default and existing aliases can still be used after new alias is added. E.g. after `alias -c addstudent -as student`, both `student` and `addstudent` can be used. +* The `` can also take any existing alias as an input. The new alias will be added as an alternative to the default command. +* If you find that you have too many aliases, you can remove an alias by using `alias -c -as `. + +
+**:information_source: Notes:**
-AddressBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. +* The aliases created will persist between Source Control sessions, as long as the application is in the same directory as it was previously. +* Default commands cannot be overwritten to perform different functionalities, and cannot be deleted. + +
+ + +Examples: +* `alias -c addstudent -as example` + * adds a new alias to `addstudent` command. + * `example -n Zhiying -i E1234567` will add student `Zhiying` to the database. +* `alias -c addgroup -as example` + * replaces the mapping of the alias `example` to the `addgroup` command, i.e. `example` no longer function as `addstudent`. + * `example -g T02A` will create a new group `T02A`. +* `alias -c example -as example2` + * adds the alias `example2` to the command that `example` currently maps to, i.e. `example2` functions as `addgroup`. +* `alias -c example -as example` + * removes the alias `example`. `example` will no longer be recognized as a command. + +[Return to Table of Contents](#table-of-contents) + +
### Editing the data file -AddressBook data are saved as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. +Source Control data are saved as a JSON file `[JAR file location]/data/sourcecontrol.json`. Advanced users are welcome to update data directly by editing that data file. + +If you are adding groups or assessments to a student, make sure you also add the groups to the group list, and the assessments to the assessment list. Otherwise, the changes you made could be lost!
: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. +If your changes to the data file makes its format invalid, Source Control will discard all data and start with an empty data file at the next run.
-### Archiving data files `[coming in v2.0]` +[Return to Table of Contents](#table-of-contents) + +
+ +-------------------------------------------------------------------------------------------------------------------- + +
+ +## **FAQ** + +### Double-clicking isn't opening the app! What can I do? +Open your Command Prompt (Windows) or your Terminal (MacOS, Linux) and navigate to the folder your JAR file resides in. + +Run the JAR file by using the command `java -jar sourceControl.jar`. + +On an OS based off of Linux, it might be necessary to run `chmod +x sourceControl.jar` on Terminal to allow opening of the application via double-clicking. + +[Return to Table of Contents](#table-of-contents) + + +### How can I back up my data? +Source Control data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. + +If you wish to back up the data, you can use the `export` command to retrieve a CSV file containing the data. The `import` command can then load the data back into Source Control. + +[Return to Table of Contents](#table-of-contents) -_Details coming soon ..._ + +### Why is my data is gone when I reopen the app? +Please ensure that the JAR file is in the same directory as it was before. Our application stores the data in external files, +and data would be lost if either these files are deleted, or if the JAR file is removed from the directory. + +Some of these data stored externally include students and their particulars (e.g. NUSNET IDs, groups, assessments and scores), +as well as you alias preferences. + +[Return to Table of Contents](#table-of-contents) + +
-------------------------------------------------------------------------------------------------------------------- -## FAQ +
+ +## **Glossary** + +Below is a table of the argument flags used in our document. + +Flag | Full Form | Usage +-------|--------|---------- +-n | name | `-n ` +-i | id | `-i ` +-g | group | `-g ` +-a | assessment | `-a ` +-s | score | `-s ` +-t | tag | `-t ` +-f | file | `-f ` +-c | command | `-c ` +-as | as alias | `-as ` + + +
+**:information_source: Notes:**
+ +In `import` command, `-g`, `-a`, `-t` are used differently from other commands. More information can be found in the +Import command description. +
+ +[Return to Table of Contents](#table-of-contents) + +
+
+ +Below is a table of the restrictions of data fields used in our application. + +Fields | Restrictions | Examples +-------|--------|---- +Name | Name can be any alphanumeric words. | `David Roger the 2nd` +NUSNET ID | ID has to start with the letter E followed by 7 numerical numbers. | `E1234567` +Group | Group name can be any alphanumeric words. | `Tutorial Group 2` +Assessment | Assessment name can be any alphanumeric words. | `Practical Assessment 2` +Score | Score is in percentage, recorded up to 2 decimal places. | `67.75` +Tag | Tag can be a single alphanumeric word. | `Beginner1` +Alias | Command alias can be a single alphanumeric word. | `addstu1` + +[Return to Table of Contents](#table-of-contents) + +
+ +Below is a table explaining some terms used in this document. + +Word | Meaning +-------|-------- +Alphanumeric | A character that is either an alphabet (capitalized and non-capitalized) or a numerical number. +Command | Instruction entered by the user e.g. `list`, `exit`. +Command Line Interface (CLI) | A text-based interface that is used to operate software, allowing the user to respond to visual prompts via typing commands. +Graphic User Interface (GUI) | A system of interactive visual components for computer software, which allows users to interact via many visual components. +CSV | A comma-separated values (CSV) file is a delimited text file that uses a comma to separate values. It is generally used to move data to and from programs such as Microsoft Excel and Google Sheets. +JAR | A package file format to combine many Java class files and resources (e.g. text and images) into one file for distribution. Source Control can be opened from the JAR file `sourceControl.jar` retrieved from our website. +Java | A computing platform for application development. Source Control runs on Java. +JSON | JavaScript Object Notation (JSON) is an open standard file format and data interchange format that uses human-readable text to store and transmit data objects consisting of attribute–value pairs and arrays (or other serializable values). +Directory | It is where files in the computer are stored, or commonly known as folder. +Path | A string of characters to uniquely identify the location in the computer. Absolute path starts from the root directory and relative path starts from the current working directory. + +[Return to Table of Contents](#table-of-contents) -**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. +
-------------------------------------------------------------------------------------------------------------------- -## 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 Summary** + +Action | Format | Examples +--------|--------|---------- +**Add Student** | `addstudent -n -i [-g ]... [-t ]...` | `addstudent -n Jonas Chow -i E0123456 -g T01A -g R01A -t beginner` +**Add Group** | `addgroup -g [(-n | -i )]...` | `addgroup -g FG1`, `addgroup -g FG1 -n Hong Fai -i E0123456` +**Add Allocation** | `addalloc -g (-n | -i )` | `addalloc -g T01A -n Hong Yao`, `addalloc -g T02A -i E0123456` +**Add Assessment** | `addassessment -a ` | `addassessment -a Midterm Examination` +**Add Score** | `addscore -a (-n | -i ) -s ` | `addscore -a Midterm -n Van Nhi -s 75`, `addscore -a Finals -i E0123456 -s 87.65` +**List** | `list` | +**Search** | `search (-n | -i | -g | -t )` | `search -n Zhiying`, `search -g T02B R04D` +**Show Analysis** | `show ( | -n | -i | -g | -a ) [-f]` | `show -n Jonas Chow`, `show -a Midterm Examination -f` +**Edit Student** | `edit [-n ] [-i ] [-g ]... [-t ]...` | `edit 1 -n Hong Fai -i E1234567 -g T01 -g R01` +**Delete Student** | `delete ` | `delete 2` +**Import Data** | `import -f [-g ] [-a ] [-t ]` | `import -f student_data.csv -g 2 -a 10 -t 1` +**Export Data** | `export` | +**Clear Data** | `clear` | +**Exit App** | `exit` | +**Help** | `help` | +**Set Alias** | `alias -c -as ` | `alias -c addstudent -as addstu` + +[Return to Table of Contents](#table-of-contents) diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..a0b1fa382d7 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "Source Control" theme: minima header_pages: @@ -8,7 +8,7 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2122S1-CS2103T-W08-2/tp" github_icon: "images/github-icon.png" plugins: diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..5aee3ebe612 100644 --- a/docs/_sass/minima/_base.scss +++ b/docs/_sass/minima/_base.scss @@ -288,7 +288,7 @@ table { text-align: center; } .site-header:before { - content: "AB-3"; + content: "Source Control"; font-size: 32px; } } diff --git a/docs/diagrams/AddAllocActivityDiagram.puml b/docs/diagrams/AddAllocActivityDiagram.puml new file mode 100644 index 00000000000..e4cf41b01b9 --- /dev/null +++ b/docs/diagrams/AddAllocActivityDiagram.puml @@ -0,0 +1,27 @@ +@startuml +!include style.puml + +start +if () then ([group found in database]) + if () then ([student(s) found in database]) + :Find matched student(s); + if () then ([one matched\nstudent found]) + if () then ([student is not in group]) + :Allocate student\ninto group; + else ([else]) + :Display an\nerror message; + endif + else ([multiple matched\n students found]) + :Update student list\nwith matched students; + :Display an\nerror message; + endif + else ([else]) + :Display an\nerror message; + endif +else ([else]) + :Display an\nerror message; +endif + +stop + +@enduml diff --git a/docs/diagrams/AddAllocSequenceDiagram.puml b/docs/diagrams/AddAllocSequenceDiagram.puml new file mode 100644 index 00000000000..69f233568e6 --- /dev/null +++ b/docs/diagrams/AddAllocSequenceDiagram.puml @@ -0,0 +1,81 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":SourceControlParser" as SourceControlParser LOGIC_COLOR +participant ":AddAllocCommandParser" as AddAllocCommandParser LOGIC_COLOR +participant "allocDescriptor\n:AllocDescriptor" as AllocDescriptor LOGIC_COLOR +participant "addAllocCommand\n:AddAllocCommand" as AddAllocCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("addalloc \n-g T01A -n Zhiying") +activate LogicManager + +LogicManager -> SourceControlParser : parseCommand("addalloc \n-g T01A -n Zhiying") +activate SourceControlParser + +create AddAllocCommandParser +SourceControlParser -> AddAllocCommandParser +activate AddAllocCommandParser + +AddAllocCommandParser --> SourceControlParser +deactivate AddAllocCommandParser + +SourceControlParser -> AddAllocCommandParser : parse("-g T01A -n Zhiying") +activate AddAllocCommandParser + +create AllocDescriptor +AddAllocCommandParser -> AllocDescriptor +activate AllocDescriptor + +AllocDescriptor --> AddAllocCommandParser : allocDescriptor +deactivate AllocDescriptor + +create AddAllocCommand +AddAllocCommandParser -> AddAllocCommand : AddAllocCommand(allocDescriptor) +activate AddAllocCommand + +AddAllocCommand --> AddAllocCommandParser : addAllocCommand +deactivate AddAllocCommand + +AddAllocCommandParser --> SourceControlParser : addAllocCommand +deactivate AddAllocCommandParser + +destroy AllocDescriptor + +'hidden arrow to position destroy marker +Model -[hidden]-> +destroy AddAllocCommandParser + +SourceControlParser --> LogicManager : addAllocCommand +deactivate SourceControlParser + +'hidden arrow to position destroy marker +Model -[hidden]-> +destroy SourceControlParser + +LogicManager -> AddAllocCommand : execute() +activate AddAllocCommand + +ref over AddAllocCommand, Model: add allocation to model + +create CommandResult +AddAllocCommand -> CommandResult +activate CommandResult + +CommandResult --> AddAllocCommand +deactivate CommandResult + +AddAllocCommand --> LogicManager +deactivate AddAllocCommand + +[<--LogicManager +deactivate LogicManager + +@enduml diff --git a/docs/diagrams/AddAllocToModelSequenceDiagram.puml b/docs/diagrams/AddAllocToModelSequenceDiagram.puml new file mode 100644 index 00000000000..645c5529905 --- /dev/null +++ b/docs/diagrams/AddAllocToModelSequenceDiagram.puml @@ -0,0 +1,36 @@ +@startuml +!include style.puml + +mainframe **sd** add allocation to model\t + +box Logic LOGIC_COLOR_T1 +participant "addAllocCommand\n:AddAllocCommand" as AddAllocCommand LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +participant "groupToEdit\n:Group" MODEL_COLOR +end box + +AddAllocCommand -> Model : getGroupList() +AddAllocCommand -> Model : getStudentList() + +AddAllocCommand -> AddAllocCommand : getAllocGroup() +activate AddAllocCommand + +AddAllocCommand --> AddAllocCommand : groupToEdit +deactivate AddAllocCommand + +'hidden arrow to add extra spacing +Model -[hidden]-> + +AddAllocCommand -> AddAllocCommand : getAllocStudents() +activate AddAllocCommand + +AddAllocCommand --> AddAllocCommand : studentToEdit +deactivate AddAllocCommand + +AddAllocCommand -> "groupToEdit\n:Group" : addStudent() +AddAllocCommand -> Model : setStudent() + +@enduml diff --git a/docs/diagrams/AddGroupActivityDiagram.puml b/docs/diagrams/AddGroupActivityDiagram.puml new file mode 100644 index 00000000000..daa2931e85e --- /dev/null +++ b/docs/diagrams/AddGroupActivityDiagram.puml @@ -0,0 +1,19 @@ +@startuml +!include style.puml + +start +if () then ([group does not\n already exist]) + :Add students to group <$rake> ; + if () then ([no exceptions occurred]) + :Add group\nto database; + :Update group list\nof students; + else ([else]) + :Display an\nerror message; + endif +else ([group already exists]) + :Display an\nerror message; +endif + +stop + +@enduml diff --git a/docs/diagrams/AddGroupSequenceDiagram.puml b/docs/diagrams/AddGroupSequenceDiagram.puml new file mode 100644 index 00000000000..7343de48dd6 --- /dev/null +++ b/docs/diagrams/AddGroupSequenceDiagram.puml @@ -0,0 +1,88 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":SourceControlParser" as SourceControlParser LOGIC_COLOR +participant ":AddGroupCommandParser" as AddGroupCommandParser LOGIC_COLOR +participant "allocDescriptor\n:AllocDescriptor" as JohnDoeDescriptor LOGIC_COLOR +participant "addGroupCommand\n:AddGroupCommand" as AddGroupCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +participant "T01A:Group" as Group MODEL_COLOR +end box + +[-> LogicManager : execute("addgroup \n-g T01A \n-n John Doe") +activate LogicManager + +LogicManager -> SourceControlParser : parseCommand("addgroup \n-g T01A \n-n John Doe") +activate SourceControlParser + +create AddGroupCommandParser +SourceControlParser -> AddGroupCommandParser +activate AddGroupCommandParser + +AddGroupCommandParser --> SourceControlParser +deactivate AddGroupCommandParser + +SourceControlParser -> AddGroupCommandParser : parse(" -g T01A \n-n John Doe") +activate AddGroupCommandParser + +create Group +AddGroupCommandParser -> Group +activate Group + +Group --> AddGroupCommandParser : T01A +deactivate Group + +create JohnDoeDescriptor +AddGroupCommandParser --> JohnDoeDescriptor +activate JohnDoeDescriptor + +JohnDoeDescriptor --> AddGroupCommandParser : allocDescriptor +deactivate JohnDoeDescriptor + +create AddGroupCommand +AddGroupCommandParser -> AddGroupCommand : AddGroupCommand(T01A, \nallocDescriptor) +activate AddGroupCommand + +AddGroupCommand --> AddGroupCommandParser : addGroupCommand +deactivate AddGroupCommand + +AddGroupCommandParser --> SourceControlParser : addGroupCommand +deactivate AddGroupCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +AddGroupCommandParser -[hidden]-> SourceControlParser +destroy AddGroupCommandParser + +SourceControlParser --> LogicManager : addGroupCommand +deactivate SourceControlParser + +LogicManager -> AddGroupCommand : execute() +activate AddGroupCommand + +ref over AddGroupCommand, Model, Group: add group to model + +JohnDoeDescriptor -[hidden]-> JohnDoeDescriptor +destroy JohnDoeDescriptor + +create CommandResult +AddGroupCommand -> CommandResult + +activate CommandResult + +CommandResult --> AddGroupCommand +deactivate CommandResult + +AddGroupCommand --> LogicManager : result +deactivate AddGroupCommand +'Hidden arrow to position the destroy marker below the end of the activation bar. +AddGroupCommand -[hidden]-> CommandResult +destroy AddGroupCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/AddGroupToModelSequenceDiagram.puml b/docs/diagrams/AddGroupToModelSequenceDiagram.puml new file mode 100644 index 00000000000..d56d50a4268 --- /dev/null +++ b/docs/diagrams/AddGroupToModelSequenceDiagram.puml @@ -0,0 +1,24 @@ +@startuml +!include style.puml + +mainframe **sd** add group to model\t + +box Logic LOGIC_COLOR_T1 +participant "addGroupCommand\n:AddGroupCommand" as AddGroupCommand LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +participant "T01A:Group" as Group MODEL_COLOR +end box + +AddGroupCommand -> Model : getAllocStudent(allocDescriptor) +activate Model + +Model --> AddGroupCommand : John Doe +deactivate Model + +AddGroupCommand -> Group : addStudent(John Doe) + +AddGroupCommand -> Model : addGroup(T01A) +@enduml diff --git a/docs/diagrams/AddScoreActivityDiagram.puml b/docs/diagrams/AddScoreActivityDiagram.puml new file mode 100644 index 00000000000..72c4f8c3e09 --- /dev/null +++ b/docs/diagrams/AddScoreActivityDiagram.puml @@ -0,0 +1,27 @@ +@startuml +!include style.puml + +start +if () then ([assessment found in database]) + if () then ([student(s) found in database]) + :Find matched student(s); + if () then ([one matched\nstudent found]) + if () then ([assessment is\nalready graded]) + :Update score\nof assessment; + else ([else]) + :Add score\nof assessment; + endif + else ([multiple matched\n students found]) + :Update student list\nwith matched students; + :Display an\nerror message; + endif + else ([else]) + :Display an\nerror message; + endif +else ([else]) + :Display an\nerror message; +endif + +stop + +@enduml diff --git a/docs/diagrams/AddScoreSequenceDiagram.puml b/docs/diagrams/AddScoreSequenceDiagram.puml new file mode 100644 index 00000000000..ca27a861ffb --- /dev/null +++ b/docs/diagrams/AddScoreSequenceDiagram.puml @@ -0,0 +1,82 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":SourceControlParser" as SourceControlParser LOGIC_COLOR +participant ":AddScoreCommandParser" as AddScoreCommandParser LOGIC_COLOR +participant "scoreDescriptor\n:ScoreDescriptor" as ScoreDescriptor LOGIC_COLOR +participant "addScoreCommand\n:AddScoreCommand" as AddScoreCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("addscore \n-a Midterm -n Van Nhi -s 95") +activate LogicManager + +LogicManager -> SourceControlParser : parseCommand("addscore \n-a Midterm -n Van Nhi -s 95") +activate SourceControlParser + +create AddScoreCommandParser +SourceControlParser -> AddScoreCommandParser +activate AddScoreCommandParser + +AddScoreCommandParser --> SourceControlParser +deactivate AddScoreCommandParser + +SourceControlParser -> AddScoreCommandParser : parse("-a Midterm \n-n Van Nhi -s 95") +activate AddScoreCommandParser + +create ScoreDescriptor +AddScoreCommandParser -> ScoreDescriptor +activate ScoreDescriptor + +ScoreDescriptor --> AddScoreCommandParser : scoreDescriptor +deactivate ScoreDescriptor + + +create AddScoreCommand +AddScoreCommandParser -> AddScoreCommand : AddScoreCommand(scoreDescriptor) +activate AddScoreCommand + +AddScoreCommand --> AddScoreCommandParser : addScoreCommand +deactivate AddScoreCommand + +AddScoreCommandParser --> SourceControlParser : addScoreCommand +deactivate AddScoreCommandParser + +destroy ScoreDescriptor + +'hidden arrow to position destroy marker +Model -[hidden]-> +destroy AddScoreCommandParser + +SourceControlParser --> LogicManager : addScoreCommand +deactivate SourceControlParser + +'hidden arrow to position destroy marker +Model -[hidden]-> +destroy SourceControlParser + +LogicManager -> AddScoreCommand : execute() +activate AddScoreCommand + +ref over AddScoreCommand, Model : add score to model + +create CommandResult +AddScoreCommand -> CommandResult +activate CommandResult + +CommandResult --> AddScoreCommand +deactivate CommandResult + +AddScoreCommand --> LogicManager +deactivate AddScoreCommand + +[<--LogicManager +deactivate LogicManager + +@enduml diff --git a/docs/diagrams/AddScoreToModelSequenceDiagram.puml b/docs/diagrams/AddScoreToModelSequenceDiagram.puml new file mode 100644 index 00000000000..3fb96d068b5 --- /dev/null +++ b/docs/diagrams/AddScoreToModelSequenceDiagram.puml @@ -0,0 +1,36 @@ +@startuml +!include style.puml + +mainframe **sd** add score to model\t + +box Logic LOGIC_COLOR_T1 +participant "addScoreCommand\n:AddScoreCommand" as AddScoreCommand LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +participant "assessmentToEdit\n:Assessment" MODEL_COLOR +end box + +AddScoreCommand -> Model : getAssessmentList() +AddScoreCommand -> Model : getStudentList() + +AddScoreCommand -> AddScoreCommand : getToEditAssessment() +activate AddScoreCommand + +AddScoreCommand --> AddScoreCommand : assessmentToEdit +deactivate AddScoreCommand + +'hidden arrow to add extra spacing +Model -[hidden]-> + +AddScoreCommand -> AddScoreCommand : getToEditStudents() +activate AddScoreCommand + +AddScoreCommand --> AddScoreCommand : studentToEdit +deactivate AddScoreCommand + +AddScoreCommand -> "assessmentToEdit\n:Assessment" : setScore() +AddScoreCommand -> Model : setStudent() + +@enduml diff --git a/docs/diagrams/AddStudentActivityDiagram.puml b/docs/diagrams/AddStudentActivityDiagram.puml new file mode 100644 index 00000000000..5aae315f83a --- /dev/null +++ b/docs/diagrams/AddStudentActivityDiagram.puml @@ -0,0 +1,12 @@ +@startuml +!include style.puml + +start +if() then ([student does not\n already exist]) + :Add student\nto database; +else ([student already exists]) + :Display an\nerror message; +endif +stop + +@enduml diff --git a/docs/diagrams/AddStudentSequenceDiagram.puml b/docs/diagrams/AddStudentSequenceDiagram.puml new file mode 100644 index 00000000000..fedb8a4dc65 --- /dev/null +++ b/docs/diagrams/AddStudentSequenceDiagram.puml @@ -0,0 +1,77 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":SourceControlParser" as SourceControlParser LOGIC_COLOR +participant ":AddStudentCommandParser" as AddStudentCommandParser LOGIC_COLOR +participant "addStudentCommand\n:AddStudentCommand" as AddStudentCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +participant "Jonas:Student" as Student MODEL_COLOR +end box + +[-> LogicManager : execute("addstudent \n-n Jonas \n-i E0123456") +activate LogicManager + +LogicManager -> SourceControlParser : parseCommand(\n"addstudent -n Jonas \n-i E0123456") +activate SourceControlParser + +create AddStudentCommandParser +SourceControlParser -> AddStudentCommandParser +activate AddStudentCommandParser + +AddStudentCommandParser --> SourceControlParser +deactivate AddStudentCommandParser + +SourceControlParser -> AddStudentCommandParser : parse(" -n Jonas\n -i E0123456") +activate AddStudentCommandParser + +create Student +AddStudentCommandParser -> Student +activate Student + +Student --> AddStudentCommandParser : Jonas +deactivate Student + +create AddStudentCommand +AddStudentCommandParser -> AddStudentCommand : AddStudentCommand(Jonas) +activate AddStudentCommand + +AddStudentCommand --> AddStudentCommandParser : addStudentCommand +deactivate AddStudentCommand + +AddStudentCommandParser --> SourceControlParser : addStudentCommand +deactivate AddStudentCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +AddStudentCommandParser -[hidden]-> SourceControlParser +destroy AddStudentCommandParser + +SourceControlParser --> LogicManager : addStudentCommand +deactivate SourceControlParser + +LogicManager -> AddStudentCommand : execute() +activate AddStudentCommand + +AddStudentCommand -> Model : addStudent(Jonas) +activate Model + +Model --> AddStudentCommand +deactivate Model + +create CommandResult +AddStudentCommand -> CommandResult +activate CommandResult + +CommandResult --> AddStudentCommand +deactivate CommandResult + +AddStudentCommand --> LogicManager : result +deactivate AddStudentCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/AddStudentsToGroupActivityDiagram.puml b/docs/diagrams/AddStudentsToGroupActivityDiagram.puml new file mode 100644 index 00000000000..f79c90ec8f8 --- /dev/null +++ b/docs/diagrams/AddStudentsToGroupActivityDiagram.puml @@ -0,0 +1,18 @@ +@startuml +!include style.puml + +title: Add students to group + +start +while () is ([has remaining student(s) to add]) + :Find matched student; + if () then ( [one matched student found\nand student not already in group]) + :Add student\nto group; + else ([else]) + :Display an\nerror message; + stop + endif +endwhile ([else]) +stop + +@enduml diff --git a/docs/diagrams/ArchitectureSequenceDiagram.puml b/docs/diagrams/ArchitectureSequenceDiagram.puml index ef81d18c337..328ab06192c 100644 --- a/docs/diagrams/ArchitectureSequenceDiagram.puml +++ b/docs/diagrams/ArchitectureSequenceDiagram.puml @@ -13,13 +13,13 @@ activate ui UI_COLOR ui -[UI_COLOR]> logic : execute("delete 1") activate logic LOGIC_COLOR -logic -[LOGIC_COLOR]> model : deletePerson(p) +logic -[LOGIC_COLOR]> model : deleteStudent(s) activate model MODEL_COLOR model -[MODEL_COLOR]-> logic deactivate model -logic -[LOGIC_COLOR]> storage : saveAddressBook(addressBook) +logic -[LOGIC_COLOR]> storage : saveSourceControl(sourceControl) activate storage STORAGE_COLOR storage -[STORAGE_COLOR]> storage : Save to file diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml index 5731f9cbaa1..85a09e079d1 100644 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ b/docs/diagrams/BetterModelClassDiagram.puml @@ -4,18 +4,21 @@ skinparam arrowThickness 1.1 skinparam arrowColor MODEL_COLOR skinparam classBackgroundColor MODEL_COLOR -AddressBook *-right-> "1" UniquePersonList -AddressBook *-right-> "1" UniqueTagList -UniqueTagList -[hidden]down- UniquePersonList -UniqueTagList -[hidden]down- UniquePersonList +SourceControl *-right-> "1" UniqueStudentList +SourceControl *-right-> "1" UniqueTagList +UniqueTagList -[hidden]down- UniqueStudentList +UniqueTagList -[hidden]down- UniqueStudentList UniqueTagList *-right-> "*" Tag -UniquePersonList -right-> Person +UniqueStudentList -right-> Student -Person -up-> "*" Tag +Student -up-> "*" Tag +Student *--> Name +Student *--> ID + +note as N1 + Other associated classes such as Group and + Assessment are omitted here for brevity. +end note -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address @enduml diff --git a/docs/diagrams/DeleteSequenceDiagram.puml b/docs/diagrams/DeleteSequenceDiagram.puml index 1dc2311b245..7a4b3ba9d09 100644 --- a/docs/diagrams/DeleteSequenceDiagram.puml +++ b/docs/diagrams/DeleteSequenceDiagram.puml @@ -3,7 +3,7 @@ box Logic LOGIC_COLOR_T1 participant ":LogicManager" as LogicManager LOGIC_COLOR -participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":SourceControlParser" as SourceControlParser LOGIC_COLOR participant ":DeleteCommandParser" as DeleteCommandParser LOGIC_COLOR participant "d:DeleteCommand" as DeleteCommand LOGIC_COLOR participant ":CommandResult" as CommandResult LOGIC_COLOR @@ -16,17 +16,17 @@ end box [-> LogicManager : execute("delete 1") activate LogicManager -LogicManager -> AddressBookParser : parseCommand("delete 1") -activate AddressBookParser +LogicManager -> SourceControlParser : parseCommand("delete 1") +activate SourceControlParser create DeleteCommandParser -AddressBookParser -> DeleteCommandParser +SourceControlParser -> DeleteCommandParser activate DeleteCommandParser -DeleteCommandParser --> AddressBookParser +DeleteCommandParser --> SourceControlParser deactivate DeleteCommandParser -AddressBookParser -> DeleteCommandParser : parse("1") +SourceControlParser -> DeleteCommandParser : parse("1") activate DeleteCommandParser create DeleteCommand @@ -36,19 +36,19 @@ activate DeleteCommand DeleteCommand --> DeleteCommandParser : d deactivate DeleteCommand -DeleteCommandParser --> AddressBookParser : d +DeleteCommandParser --> SourceControlParser : d deactivate DeleteCommandParser 'Hidden arrow to position the destroy marker below the end of the activation bar. -DeleteCommandParser -[hidden]-> AddressBookParser +DeleteCommandParser -[hidden]-> SourceControlParser destroy DeleteCommandParser -AddressBookParser --> LogicManager : d -deactivate AddressBookParser +SourceControlParser --> LogicManager : d +deactivate SourceControlParser LogicManager -> DeleteCommand : execute() activate DeleteCommand -DeleteCommand -> Model : deletePerson(1) +DeleteCommand -> Model : deleteStudent(1) activate Model Model --> DeleteCommand diff --git a/docs/diagrams/ImportActivityDiagram.puml b/docs/diagrams/ImportActivityDiagram.puml new file mode 100644 index 00000000000..e347f7f7573 --- /dev/null +++ b/docs/diagrams/ImportActivityDiagram.puml @@ -0,0 +1,34 @@ +@startuml +!include style.puml + +start +'Since the beta syntax does not support placing the condition outside the +'diamond we place it as the true branch instead. +if () then ([file path is valid]) + :Read lines and assessment names; +else ([else]) + :Display an error message; + stop +endif + +if () then ([assessment names are valid\n and not duplicated]) + :Make new assessments; +else ([else]) + :Display an error message; + stop +endif + +while () is ([has remaining line(s) to read]) + :Read student info; +if () then ([all info is valid]) + :Add student to database; +else ([else]) + :Display an error message; + stop +endif +endwhile ([else]) + +:Overwrite sample database with new database; + +stop +@enduml diff --git a/docs/diagrams/LogicClassDiagram.puml b/docs/diagrams/LogicClassDiagram.puml index 6d14b17b361..26934dd062c 100644 --- a/docs/diagrams/LogicClassDiagram.puml +++ b/docs/diagrams/LogicClassDiagram.puml @@ -6,7 +6,7 @@ skinparam classBackgroundColor LOGIC_COLOR package Logic { -Class AddressBookParser +Class SourceControlParser Class XYZCommand Class CommandResult Class "{abstract}\nCommand" as Command @@ -27,8 +27,8 @@ Class HiddenOutside #FFFFFF HiddenOutside ..> Logic LogicManager .right.|> Logic -LogicManager -right->"1" AddressBookParser -AddressBookParser ..> XYZCommand : creates > +LogicManager -right->"1" SourceControlParser +SourceControlParser ..> XYZCommand : creates > XYZCommand -up-|> Command LogicManager .left.> Command : executes > @@ -38,7 +38,7 @@ LogicManager --> Storage Storage --[hidden] Model Command .[hidden]up.> Storage Command .right.> Model -note right of XYZCommand: XYZCommand = AddCommand, \nFindCommand, etc +note right of XYZCommand: XYZCommand = AddCommand, \nListCommand, etc Logic ..> CommandResult LogicManager .down.> CommandResult diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 1122257bd9a..c6b5e6c6d7f 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -5,50 +5,64 @@ skinparam arrowColor MODEL_COLOR skinparam classBackgroundColor MODEL_COLOR Package Model <>{ -Interface ReadOnlyAddressBook <> +Interface ReadOnlySourceControl <> Interface ReadOnlyUserPrefs <> Interface Model <> -Class AddressBook -Class ReadOnlyAddressBook +Class SourceControl +Class ReadOnlySourceControl Class Model Class ModelManager Class UserPrefs Class ReadOnlyUserPrefs -Class UniquePersonList -Class Person -Class Address -Class Email +Class UniqueStudentList +Class Student { +- scores: Map +} Class Name -Class Phone +Class ID Class Tag +Class GroupList +Class Group +Class AssessmentList +Class Score +Class Assessment { +- scores: Map +} } Class HiddenOutside #FFFFFF HiddenOutside ..> Model -AddressBook .up.|> ReadOnlyAddressBook +SourceControl .up.|> ReadOnlySourceControl ModelManager .up.|> Model Model .right.> ReadOnlyUserPrefs -Model .left.> ReadOnlyAddressBook -ModelManager -left-> "1" AddressBook +Model .left.> ReadOnlySourceControl +ModelManager -left-> "1" SourceControl ModelManager -right-> "1" UserPrefs -UserPrefs .up.|> ReadOnlyUserPrefs -AddressBook *--> "1" UniquePersonList -UniquePersonList --> "~* all" Person -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address -Person *--> "*" Tag +UserPrefs .up..|> ReadOnlyUserPrefs + +SourceControl *--> "1" UniqueStudentList +SourceControl *--> "1" GroupList +SourceControl *--> "1" AssessmentList + +UniqueStudentList --> " ~* all" Student +Student *--> "1" Name +Student *--> "1" ID +Student *--> "~* " Tag + +GroupList --> "~* " Group +Group --> "*" ID + +AssessmentList --> "~* " Assessment -Name -[hidden]right-> Phone -Phone -[hidden]right-> Address -Address -[hidden]right-> Email +Name -[hidden]right-> ID +Score -[hidden]right-> ID +Score -[hidden]up-> Assessment -ModelManager -->"~* filtered" Person +ModelManager --> "filtered ~*\t" Student @enduml diff --git a/docs/diagrams/ParseActivityDiagram.puml b/docs/diagrams/ParseActivityDiagram.puml new file mode 100644 index 00000000000..9128e88221a --- /dev/null +++ b/docs/diagrams/ParseActivityDiagram.puml @@ -0,0 +1,33 @@ +@startuml +!include style.puml + +start +:Parser parses command; +:Command word and arguments are parsed; + +if () then ([command word is valid]) +else ([else]) + :Display an error message; + stop +endif + +while () is ([command word is an alias]) + :Replace aliased command word\nwith default command word; +endwhile ([else]) + +if () then ([command has arguments]) + :Parse arguments; + if () then ([arguments are valid]) + :Create command; + else ([else]) + :Display an error message; + stop + endif +else ([else]) + :Create command; +endif + +:Execute created command; + +stop +@enduml diff --git a/docs/diagrams/ParserClasses.puml b/docs/diagrams/ParserClasses.puml index 6ba585cba01..f04cc64763f 100644 --- a/docs/diagrams/ParserClasses.puml +++ b/docs/diagrams/ParserClasses.puml @@ -9,7 +9,7 @@ Class XYZCommand package "Parser classes"{ Interface Parser <> -Class AddressBookParser +Class SourceControlParser Class XYZCommandParser Class CliSyntax Class ParserUtil @@ -19,12 +19,12 @@ Class Prefix } Class HiddenOutside #FFFFFF -HiddenOutside ..> AddressBookParser +HiddenOutside ..> SourceControlParser -AddressBookParser .down.> XYZCommandParser: creates > +SourceControlParser .down.> XYZCommandParser: creates > XYZCommandParser ..> XYZCommand : creates > -AddressBookParser ..> Command : returns > +SourceControlParser ..> Command : returns > XYZCommandParser .up.|> Parser XYZCommandParser ..> ArgumentMultimap XYZCommandParser ..> ArgumentTokenizer diff --git a/docs/diagrams/SearchActivityDiagram.puml b/docs/diagrams/SearchActivityDiagram.puml new file mode 100644 index 00000000000..cdfa3465cd5 --- /dev/null +++ b/docs/diagrams/SearchActivityDiagram.puml @@ -0,0 +1,19 @@ +@startuml +!include style.puml + +start + +:User executes command; +:Identify type of input; + +if () then ([input is valid]) + :Process input argument\ninto list of words; + :Create corresponding predicates; + :Update student list\nwith matched students; +else ([else]) + :Display an\nerror message; +endif + +stop + +@enduml diff --git a/docs/diagrams/SearchSequenceDiagram.puml b/docs/diagrams/SearchSequenceDiagram.puml new file mode 100644 index 00000000000..1cabc3ef50f --- /dev/null +++ b/docs/diagrams/SearchSequenceDiagram.puml @@ -0,0 +1,70 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":SourceControlParser" as SourceControlParser LOGIC_COLOR +participant ":SearchCommandParser" as SearchCommandParser LOGIC_COLOR +participant "searchCommand\n:SearchCommand" as SearchCommand LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +participant "predicate\n:NameContains\nKeywordsPredicate" as NameContainsKeywordsPredicate MODEL_COLOR + +end box + +[-> LogicManager : execute("search\n -n Alex Yu") +activate LogicManager + +LogicManager -> SourceControlParser : parseCommand(\n"search -n Alex Yu") +activate SourceControlParser + +create SearchCommandParser +SourceControlParser -> SearchCommandParser +activate SearchCommandParser + +SearchCommandParser --> SourceControlParser +deactivate SearchCommandParser + +SourceControlParser -> SearchCommandParser : parse(" -n Alex") +activate SearchCommandParser + +create NameContainsKeywordsPredicate +SearchCommandParser -> NameContainsKeywordsPredicate : NameContainsKeywords\nPredicate(["Alex", "Yu"]) +activate NameContainsKeywordsPredicate + +NameContainsKeywordsPredicate --> SearchCommandParser : predicate +deactivate NameContainsKeywordsPredicate + +create SearchCommand +SearchCommandParser -> SearchCommand : SearchCommand(\npredicate) +activate SearchCommand + +SearchCommand --> SearchCommandParser : searchCommand +deactivate SearchCommand + +SearchCommandParser --> SourceControlParser : SearchCommand +deactivate SearchCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +SearchCommandParser -[hidden]-> SourceControlParser +destroy SearchCommandParser + +SourceControlParser --> LogicManager : SearchCommand +deactivate SourceControlParser + +LogicManager -> SearchCommand : execute() +activate SearchCommand + +SearchCommand -> Model : updateFiltered\nStudentList(\npredicate) +activate Model + +Model --> SearchCommand +deactivate Model + +SearchCommand --> LogicManager : result +deactivate SearchCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/ShowActivityDiagram.puml b/docs/diagrams/ShowActivityDiagram.puml new file mode 100644 index 00000000000..cd5ae25a92a --- /dev/null +++ b/docs/diagrams/ShowActivityDiagram.puml @@ -0,0 +1,13 @@ +@startuml +!include style.puml + +start +if () then ([student analysis\n is requested]) + :Show performance analysis\nof a student <$rake>; +else ([group / assessment\nanalysis is requested]) + :Show performance analysis\nof a group / an assessment <$rake>; +endif + +stop + +@enduml diff --git a/docs/diagrams/ShowSequenceDiagram.puml b/docs/diagrams/ShowSequenceDiagram.puml new file mode 100644 index 00000000000..07694e5e979 --- /dev/null +++ b/docs/diagrams/ShowSequenceDiagram.puml @@ -0,0 +1,79 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":SourceControlParser" as SourceControlParser LOGIC_COLOR +participant ":ShowCommandParser" as ShowCommandParser LOGIC_COLOR +participant "showCommand\n:ShowCommand" as ShowCommand LOGIC_COLOR +participant "info\n:Info" as Info LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +participant "studentStatistics\n:StudentStatistics" as StudentStatistics MODEL_COLOR +end box + +[-> LogicManager : execute("show \n-n Hong Fai") +activate LogicManager + +LogicManager -> SourceControlParser : parseCommand(\n"show -n Hong Fai") +activate SourceControlParser + +create ShowCommandParser +SourceControlParser -> ShowCommandParser +activate ShowCommandParser + +ShowCommandParser --> SourceControlParser +deactivate ShowCommandParser + +SourceControlParser -> ShowCommandParser : parse("-n Hong Fai") +activate ShowCommandParser + +ShowCommandParser -> ShowCommandParser : parseByPrefixes() +activate ShowCommandParser + +create ShowCommand +ShowCommandParser -> ShowCommand : ShowCommand(scoreDescriptor) +activate ShowCommand + +ShowCommand --> ShowCommandParser : showCommand +deactivate ShowCommand + +ShowCommandParser --> ShowCommandParser : showCommand +deactivate ShowCommandParser + +ShowCommandParser --> SourceControlParser : showCommand +deactivate ShowCommandParser + +'hidden arrow to position destroy marker +Model -[hidden]-> +destroy ShowCommandParser + +SourceControlParser --> LogicManager : showCommand +deactivate SourceControlParser + +'hidden arrow to position destroy marker +Model -[hidden]-> +destroy SourceControlParser + +LogicManager -> ShowCommand : execute() +activate ShowCommand + +ref over ShowCommand, Model : show statistics from model + +create CommandResult +ShowCommand -> CommandResult +activate CommandResult + +CommandResult --> ShowCommand +deactivate CommandResult + +ShowCommand --> LogicManager +deactivate ShowCommand + +[<--LogicManager +deactivate LogicManager + +@enduml diff --git a/docs/diagrams/ShowStatsFromModelSequenceDiagram.puml b/docs/diagrams/ShowStatsFromModelSequenceDiagram.puml new file mode 100644 index 00000000000..4c84d52ffb0 --- /dev/null +++ b/docs/diagrams/ShowStatsFromModelSequenceDiagram.puml @@ -0,0 +1,40 @@ +@startuml +!include style.puml + +mainframe **sd** show statistics from model\t + +box Logic LOGIC_COLOR_T1 +participant "showCommand\n:ShowCommand" as ShowCommand LOGIC_COLOR +participant "info\n:Info" as Info LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +participant "studentStatistics\n:StudentStatistics" as StudentStatistics MODEL_COLOR +end box + +ShowCommand -> ShowCommand : createStudentPredicate() +activate ShowCommand + +ShowCommand --> ShowCommand : predicate +deactivate ShowCommand + +ShowCommand -> Model : updateFilteredStudentList(predicate) +Model --> ShowCommand : matchedStudent + +create Info +ShowCommand -> Info : Info(matchedStudent) +activate Info + +Info --> ShowCommand : info +deactivate Info + +create StudentStatistics +ShowCommand -> StudentStatistics : StudentStatistics(matchedStudent) +activate StudentStatistics + +StudentStatistics --> ShowCommand : studentStatistics +deactivate StudentStatistics + +@enduml diff --git a/docs/diagrams/ShowStudentActivityDiagram.puml b/docs/diagrams/ShowStudentActivityDiagram.puml new file mode 100644 index 00000000000..defb218a1ab --- /dev/null +++ b/docs/diagrams/ShowStudentActivityDiagram.puml @@ -0,0 +1,22 @@ +@startuml +!include style.puml + +title: Show performance analysis\nof a student + +start +if () then ([student(s) found in database]) + :Update student list + with matched student(s); + if () then ([one matched\nstudent found]) + :Show summary\ninfo of student; + :Show performance\ngraph of student; + else ([multiple matched\n students found]) + :Display an\nerror message; + endif +else ([else]) + :Display an\nerror message; +endif + +stop + +@enduml diff --git a/docs/diagrams/ShowStudentsActivityDiagram.puml b/docs/diagrams/ShowStudentsActivityDiagram.puml new file mode 100644 index 00000000000..ac8c8e957ea --- /dev/null +++ b/docs/diagrams/ShowStudentsActivityDiagram.puml @@ -0,0 +1,17 @@ +@startuml +!include style.puml + +title: Show performance analysis\nof a group / an assessment + +start +if () then ([group / assessment\n found in database]) + :Update student list with\nstudents in group / cohort; + :Show summary info of\nstudents in group / cohort; + :Show performance graph of\nstudents in group / cohort; +else ([else]) + :Display an\nerror message; +endif + +stop + +@enduml diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index 85ac3ea2dee..4d781ff8926 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -14,12 +14,24 @@ Class JsonUserPrefsStorage Interface Storage <> Class StorageManager -package "AddressBook Storage" #F4F6F6{ -Interface AddressBookStorage <> -Class JsonAddressBookStorage -Class JsonSerializableAddressBook -Class JsonAdaptedPerson +package "SourceControl Storage" #F4F6F6{ +Interface SourceControlStorage <> +Class JsonSourceControlStorage +Class JsonSerializableSourceControl +Class JsonAdaptedStudent { ++ name: String ++ id: String ++ groups: String [*] ++ assessments: String [*] +} Class JsonAdaptedTag +Class JsonAdaptedAssessment { ++ name: String ++ scores: Map +} +Class JsonAdaptedGroup { ++ name: String +} } } @@ -29,15 +41,17 @@ HiddenOutside ..> Storage StorageManager .up.|> Storage StorageManager -up-> "1" UserPrefsStorage -StorageManager -up-> "1" AddressBookStorage +StorageManager -up-> "1" SourceControlStorage Storage -left-|> UserPrefsStorage -Storage -right-|> AddressBookStorage +Storage -right-|> SourceControlStorage JsonUserPrefsStorage .up.|> UserPrefsStorage -JsonAddressBookStorage .up.|> AddressBookStorage -JsonAddressBookStorage ..> JsonSerializableAddressBook -JsonSerializableAddressBook --> "*" JsonAdaptedPerson -JsonAdaptedPerson --> "*" JsonAdaptedTag +JsonSourceControlStorage .up.|> SourceControlStorage +JsonSourceControlStorage ..> JsonSerializableSourceControl +JsonSerializableSourceControl --> "*" JsonAdaptedStudent +JsonSerializableSourceControl --> "*" JsonAdaptedGroup +JsonSerializableSourceControl --> "*" JsonAdaptedAssessment +JsonAdaptedStudent --> "*" JsonAdaptedTag @enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index ecae4876432..bd7f16de7fe 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -10,9 +10,11 @@ Class "{abstract}\nUiPart" as UiPart Class UiManager Class MainWindow Class HelpWindow +Class InfoDisplay +Class GraphDisplay Class ResultDisplay -Class PersonListPanel -Class PersonCard +Class StudentListPanel +Class StudentCard Class StatusBarFooter Class CommandBox } @@ -30,31 +32,32 @@ HiddenOutside ..> Ui UiManager .left.|> Ui UiManager -down-> "1" MainWindow -MainWindow *-down-> "1" CommandBox -MainWindow *-down-> "1" ResultDisplay -MainWindow *-down-> "1" PersonListPanel -MainWindow *-down-> "1" StatusBarFooter -MainWindow --> "0..1" HelpWindow +MainWindow *-down--> "1" CommandBox +MainWindow *-down--> "1" InfoDisplay +MainWindow *-down--> "1 " GraphDisplay +MainWindow *-down--> "1" ResultDisplay +MainWindow *-down--> "1" StudentListPanel +MainWindow *-down--> "1" StatusBarFooter +MainWindow *----> "0..1" HelpWindow -PersonListPanel -down-> "*" PersonCard +StudentListPanel -down-> "~* " StudentCard MainWindow -left-|> UiPart -ResultDisplay --|> UiPart -CommandBox --|> UiPart -PersonListPanel --|> UiPart -PersonCard --|> UiPart -StatusBarFooter --|> UiPart -HelpWindow --|> UiPart +ResultDisplay ---|> UiPart +CommandBox ---|> UiPart +StudentListPanel ---|> UiPart +StudentCard ---|> UiPart +StatusBarFooter ---|> UiPart +HelpWindow ---|> UiPart +GraphDisplay ---|> UiPart +InfoDisplay ---|> UiPart -PersonCard ..> Model +StudentCard ..> Model UiManager -right-> Logic MainWindow -left-> Logic -PersonListPanel -[hidden]left- HelpWindow -HelpWindow -[hidden]left- CommandBox -CommandBox -[hidden]left- ResultDisplay -ResultDisplay -[hidden]left- StatusBarFooter +StatusBarFooter -[hidden]left- CommandBox MainWindow -[hidden]-|> UiPart @enduml diff --git a/docs/diagrams/UndoRedoState0.puml b/docs/diagrams/UndoRedoState0.puml deleted file mode 100644 index 96e30744d24..00000000000 --- a/docs/diagrams/UndoRedoState0.puml +++ /dev/null @@ -1,20 +0,0 @@ -@startuml -!include style.puml -skinparam ClassFontColor #000000 -skinparam ClassBorderColor #000000 - -title Initial state - -package States { - class State1 as "__ab0:AddressBook__" - class State2 as "__ab1:AddressBook__" - class State3 as "__ab2:AddressBook__" -} -State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 -hide State2 -hide State3 - -class Pointer as "Current State" #FFFFF -Pointer -up-> State1 -@end diff --git a/docs/diagrams/UndoRedoState1.puml b/docs/diagrams/UndoRedoState1.puml deleted file mode 100644 index 01fcb9b2b96..00000000000 --- a/docs/diagrams/UndoRedoState1.puml +++ /dev/null @@ -1,22 +0,0 @@ -@startuml -!include style.puml -skinparam ClassFontColor #000000 -skinparam ClassBorderColor #000000 - -title After command "delete 5" - -package States <> { - class State1 as "__ab0:AddressBook__" - class State2 as "__ab1:AddressBook__" - class State3 as "__ab2:AddressBook__" -} - -State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 - -hide State3 - -class Pointer as "Current State" #FFFFF - -Pointer -up-> State2 -@end diff --git a/docs/diagrams/UndoRedoState2.puml b/docs/diagrams/UndoRedoState2.puml deleted file mode 100644 index bccc230a5d1..00000000000 --- a/docs/diagrams/UndoRedoState2.puml +++ /dev/null @@ -1,20 +0,0 @@ -@startuml -!include style.puml -skinparam ClassFontColor #000000 -skinparam ClassBorderColor #000000 - -title After command "add n/David" - -package States <> { - class State1 as "__ab0:AddressBook__" - class State2 as "__ab1:AddressBook__" - class State3 as "__ab2:AddressBook__" -} - -State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 - -class Pointer as "Current State" #FFFFF - -Pointer -up-> State3 -@end diff --git a/docs/diagrams/UndoRedoState3.puml b/docs/diagrams/UndoRedoState3.puml deleted file mode 100644 index ea29c9483e4..00000000000 --- a/docs/diagrams/UndoRedoState3.puml +++ /dev/null @@ -1,20 +0,0 @@ -@startuml -!include style.puml -skinparam ClassFontColor #000000 -skinparam ClassBorderColor #000000 - -title After command "undo" - -package States <> { - class State1 as "__ab0:AddressBook__" - class State2 as "__ab1:AddressBook__" - class State3 as "__ab2:AddressBook__" -} - -State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 - -class Pointer as "Current State" #FFFFF - -Pointer -up-> State2 -@end diff --git a/docs/diagrams/UndoRedoState4.puml b/docs/diagrams/UndoRedoState4.puml deleted file mode 100644 index 1b784cece80..00000000000 --- a/docs/diagrams/UndoRedoState4.puml +++ /dev/null @@ -1,20 +0,0 @@ -@startuml -!include style.puml -skinparam ClassFontColor #000000 -skinparam ClassBorderColor #000000 - -title After command "list" - -package States <> { - class State1 as "__ab0:AddressBook__" - class State2 as "__ab1:AddressBook__" - class State3 as "__ab2:AddressBook__" -} - -State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 - -class Pointer as "Current State" #FFFFF - -Pointer -up-> State2 -@end diff --git a/docs/diagrams/UndoRedoState5.puml b/docs/diagrams/UndoRedoState5.puml deleted file mode 100644 index 88927be32bc..00000000000 --- a/docs/diagrams/UndoRedoState5.puml +++ /dev/null @@ -1,21 +0,0 @@ -@startuml -!include style.puml -skinparam ClassFontColor #000000 -skinparam ClassBorderColor #000000 - -title After command "clear" - -package States <> { - class State1 as "__ab0:AddressBook__" - class State2 as "__ab1:AddressBook__" - class State3 as "__ab3:AddressBook__" -} - -State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 - -class Pointer as "Current State" #FFFFF - -Pointer -up-> State3 -note right on link: State ab2 deleted. -@end diff --git a/docs/diagrams/UndoSequenceDiagram.puml b/docs/diagrams/UndoSequenceDiagram.puml deleted file mode 100644 index 410aab4e412..00000000000 --- a/docs/diagrams/UndoSequenceDiagram.puml +++ /dev/null @@ -1,53 +0,0 @@ -@startuml -!include style.puml - -box Logic LOGIC_COLOR_T1 -participant ":LogicManager" as LogicManager LOGIC_COLOR -participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR -participant "u:UndoCommand" as UndoCommand LOGIC_COLOR -end box - -box Model MODEL_COLOR_T1 -participant ":Model" as Model MODEL_COLOR -participant ":VersionedAddressBook" as VersionedAddressBook MODEL_COLOR -end box -[-> LogicManager : execute(undo) -activate LogicManager - -LogicManager -> AddressBookParser : parseCommand(undo) -activate AddressBookParser - -create UndoCommand -AddressBookParser -> UndoCommand -activate UndoCommand - -UndoCommand --> AddressBookParser -deactivate UndoCommand - -AddressBookParser --> LogicManager : u -deactivate AddressBookParser - -LogicManager -> UndoCommand : execute() -activate UndoCommand - -UndoCommand -> Model : undoAddressBook() -activate Model - -Model -> VersionedAddressBook : undo() -activate VersionedAddressBook - -VersionedAddressBook -> VersionedAddressBook :resetData(ReadOnlyAddressBook) -VersionedAddressBook --> Model : -deactivate VersionedAddressBook - -Model --> UndoCommand -deactivate Model - -UndoCommand --> LogicManager : result -deactivate UndoCommand -UndoCommand -[hidden]-> LogicManager : result -destroy UndoCommand - -[<--LogicManager -deactivate LogicManager -@enduml diff --git a/docs/diagrams/style.puml b/docs/diagrams/style.puml index fad8b0adeaa..4d15899e1f1 100644 --- a/docs/diagrams/style.puml +++ b/docs/diagrams/style.puml @@ -33,7 +33,7 @@ !define USER_COLOR #000000 -skinparam BackgroundColor #FFFFFFF +skinparam BackgroundColor transparent skinparam Shadowing false @@ -70,6 +70,24 @@ skinparam Shadowing false skinparam DefaultTextAlignment center skinparam packageStyle Rectangle +' this makes the visibility + - ~, etc. instead of using icons +skinparam classAttributeIconSize 0 +' this makes the font color of fields white as well +skinparam classAttributeFontColor #FFFFFF + hide footbox -hide members +hide empty members hide circle + +' @@author ganhongyao-reused +' Reused 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 +} diff --git a/docs/images/AddAllocActivityDiagram.png b/docs/images/AddAllocActivityDiagram.png new file mode 100644 index 00000000000..44f27ff2ce9 Binary files /dev/null and b/docs/images/AddAllocActivityDiagram.png differ diff --git a/docs/images/AddAllocSequenceDiagram.png b/docs/images/AddAllocSequenceDiagram.png new file mode 100644 index 00000000000..b4d87aaa80d Binary files /dev/null and b/docs/images/AddAllocSequenceDiagram.png differ diff --git a/docs/images/AddAllocToModelSequenceDiagram.png b/docs/images/AddAllocToModelSequenceDiagram.png new file mode 100644 index 00000000000..49cbad4e79a Binary files /dev/null and b/docs/images/AddAllocToModelSequenceDiagram.png differ diff --git a/docs/images/AddGroupActivityDiagram.png b/docs/images/AddGroupActivityDiagram.png new file mode 100644 index 00000000000..7d9d372168b Binary files /dev/null and b/docs/images/AddGroupActivityDiagram.png differ diff --git a/docs/images/AddGroupSequenceDiagram.png b/docs/images/AddGroupSequenceDiagram.png new file mode 100644 index 00000000000..2c9c7bea79a Binary files /dev/null and b/docs/images/AddGroupSequenceDiagram.png differ diff --git a/docs/images/AddGroupToModelSequenceDiagram.png b/docs/images/AddGroupToModelSequenceDiagram.png new file mode 100644 index 00000000000..ff8de64cd25 Binary files /dev/null and b/docs/images/AddGroupToModelSequenceDiagram.png differ diff --git a/docs/images/AddScoreActivityDiagram.png b/docs/images/AddScoreActivityDiagram.png new file mode 100644 index 00000000000..05bdcf583c5 Binary files /dev/null and b/docs/images/AddScoreActivityDiagram.png differ diff --git a/docs/images/AddScoreSequenceDiagram.png b/docs/images/AddScoreSequenceDiagram.png new file mode 100644 index 00000000000..2909daf2ad0 Binary files /dev/null and b/docs/images/AddScoreSequenceDiagram.png differ diff --git a/docs/images/AddScoreToModelSequenceDiagram.png b/docs/images/AddScoreToModelSequenceDiagram.png new file mode 100644 index 00000000000..32eb7e9f3fc Binary files /dev/null and b/docs/images/AddScoreToModelSequenceDiagram.png differ diff --git a/docs/images/AddStudentActivityDiagram.png b/docs/images/AddStudentActivityDiagram.png new file mode 100644 index 00000000000..4fe7510123e Binary files /dev/null and b/docs/images/AddStudentActivityDiagram.png differ diff --git a/docs/images/AddStudentSequenceDiagram.png b/docs/images/AddStudentSequenceDiagram.png new file mode 100644 index 00000000000..96955f05b98 Binary files /dev/null and b/docs/images/AddStudentSequenceDiagram.png differ diff --git a/docs/images/AddStudentsToGroupActivityDiagram.png b/docs/images/AddStudentsToGroupActivityDiagram.png new file mode 100644 index 00000000000..15da7214531 Binary files /dev/null and b/docs/images/AddStudentsToGroupActivityDiagram.png differ diff --git a/docs/images/ArchitectureSequenceDiagram.png b/docs/images/ArchitectureSequenceDiagram.png index 2f1346869d0..42146386da0 100644 Binary files a/docs/images/ArchitectureSequenceDiagram.png and b/docs/images/ArchitectureSequenceDiagram.png differ diff --git a/docs/images/BetterModelClassDiagram.png b/docs/images/BetterModelClassDiagram.png index 1ec62caa2a5..5f7cb7b94e5 100644 Binary files a/docs/images/BetterModelClassDiagram.png and b/docs/images/BetterModelClassDiagram.png differ diff --git a/docs/images/CommitActivityDiagram.png b/docs/images/CommitActivityDiagram.png deleted file mode 100644 index c08c13f5c8b..00000000000 Binary files a/docs/images/CommitActivityDiagram.png and /dev/null differ diff --git a/docs/images/CsvFormatExample.png b/docs/images/CsvFormatExample.png new file mode 100644 index 00000000000..8951e9a18a3 Binary files /dev/null and b/docs/images/CsvFormatExample.png differ diff --git a/docs/images/DeleteSequenceDiagram.png b/docs/images/DeleteSequenceDiagram.png index fa327b39618..f9e1868204f 100644 Binary files a/docs/images/DeleteSequenceDiagram.png and b/docs/images/DeleteSequenceDiagram.png differ diff --git a/docs/images/ImportActivityDiagram.png b/docs/images/ImportActivityDiagram.png new file mode 100644 index 00000000000..c2cf3479778 Binary files /dev/null and b/docs/images/ImportActivityDiagram.png differ diff --git a/docs/images/LogicClassDiagram.png b/docs/images/LogicClassDiagram.png index c3028aa1cda..a720ac35cfc 100644 Binary files a/docs/images/LogicClassDiagram.png and b/docs/images/LogicClassDiagram.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index 39d7aec4b33..bbb12fa5687 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/ParseActivityDiagram.png b/docs/images/ParseActivityDiagram.png new file mode 100644 index 00000000000..55a7d359623 Binary files /dev/null and b/docs/images/ParseActivityDiagram.png differ diff --git a/docs/images/ParserClasses.png b/docs/images/ParserClasses.png index 58ad22ce16a..548b8424c9c 100644 Binary files a/docs/images/ParserClasses.png and b/docs/images/ParserClasses.png differ diff --git a/docs/images/SearchActivityDiagram.png b/docs/images/SearchActivityDiagram.png new file mode 100644 index 00000000000..a0b4544e911 Binary files /dev/null and b/docs/images/SearchActivityDiagram.png differ diff --git a/docs/images/SearchSequenceDiagram.png b/docs/images/SearchSequenceDiagram.png new file mode 100644 index 00000000000..d5cf9caa034 Binary files /dev/null and b/docs/images/SearchSequenceDiagram.png differ diff --git a/docs/images/ShowActivityDiagram.png b/docs/images/ShowActivityDiagram.png new file mode 100644 index 00000000000..282eab58a63 Binary files /dev/null and b/docs/images/ShowActivityDiagram.png differ diff --git a/docs/images/ShowSequenceDiagram.png b/docs/images/ShowSequenceDiagram.png new file mode 100644 index 00000000000..f2ac92933d9 Binary files /dev/null and b/docs/images/ShowSequenceDiagram.png differ diff --git a/docs/images/ShowStatsFromModelSequenceDiagram.png b/docs/images/ShowStatsFromModelSequenceDiagram.png new file mode 100644 index 00000000000..9bf3d273e35 Binary files /dev/null and b/docs/images/ShowStatsFromModelSequenceDiagram.png differ diff --git a/docs/images/ShowStudentActivityDiagram.png b/docs/images/ShowStudentActivityDiagram.png new file mode 100644 index 00000000000..6655e930c2a Binary files /dev/null and b/docs/images/ShowStudentActivityDiagram.png differ diff --git a/docs/images/ShowStudentsActivityDiagram.png b/docs/images/ShowStudentsActivityDiagram.png new file mode 100644 index 00000000000..1b0d26440e0 Binary files /dev/null and b/docs/images/ShowStudentsActivityDiagram.png differ diff --git a/docs/images/StartingUi.png b/docs/images/StartingUi.png new file mode 100644 index 00000000000..5ef0a3f6134 Binary files /dev/null and b/docs/images/StartingUi.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 82c66f8f16e..8fde131f592 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/UGExamples/addalloc1.png b/docs/images/UGExamples/addalloc1.png new file mode 100644 index 00000000000..a1430130070 Binary files /dev/null and b/docs/images/UGExamples/addalloc1.png differ diff --git a/docs/images/UGExamples/addass2.png b/docs/images/UGExamples/addass2.png new file mode 100644 index 00000000000..69f79e0d0fd Binary files /dev/null and b/docs/images/UGExamples/addass2.png differ diff --git a/docs/images/UGExamples/addgroup2.png b/docs/images/UGExamples/addgroup2.png new file mode 100644 index 00000000000..f89a834effb Binary files /dev/null and b/docs/images/UGExamples/addgroup2.png differ diff --git a/docs/images/UGExamples/addgroup3.png b/docs/images/UGExamples/addgroup3.png new file mode 100644 index 00000000000..c407a6b3b32 Binary files /dev/null and b/docs/images/UGExamples/addgroup3.png differ diff --git a/docs/images/UGExamples/addscore1.png b/docs/images/UGExamples/addscore1.png new file mode 100644 index 00000000000..eaed141088c Binary files /dev/null and b/docs/images/UGExamples/addscore1.png differ diff --git a/docs/images/UGExamples/addscore2.png b/docs/images/UGExamples/addscore2.png new file mode 100644 index 00000000000..db9ca8a5ca1 Binary files /dev/null and b/docs/images/UGExamples/addscore2.png differ diff --git a/docs/images/UGExamples/addstu2.png b/docs/images/UGExamples/addstu2.png new file mode 100644 index 00000000000..3b40eec0138 Binary files /dev/null and b/docs/images/UGExamples/addstu2.png differ diff --git a/docs/images/UGExamples/addstu3.png b/docs/images/UGExamples/addstu3.png new file mode 100644 index 00000000000..95871c41843 Binary files /dev/null and b/docs/images/UGExamples/addstu3.png differ diff --git a/docs/images/UGExamples/alias1.png b/docs/images/UGExamples/alias1.png new file mode 100644 index 00000000000..69cff7d8ce2 Binary files /dev/null and b/docs/images/UGExamples/alias1.png differ diff --git a/docs/images/UGExamples/alias4.png b/docs/images/UGExamples/alias4.png new file mode 100644 index 00000000000..1802f660e67 Binary files /dev/null and b/docs/images/UGExamples/alias4.png differ diff --git a/docs/images/UGExamples/delete1.png b/docs/images/UGExamples/delete1.png new file mode 100644 index 00000000000..1188e8c1ae0 Binary files /dev/null and b/docs/images/UGExamples/delete1.png differ diff --git a/docs/images/UGExamples/edit1.png b/docs/images/UGExamples/edit1.png new file mode 100644 index 00000000000..aed8863e188 Binary files /dev/null and b/docs/images/UGExamples/edit1.png differ diff --git a/docs/images/UGExamples/help.png b/docs/images/UGExamples/help.png new file mode 100644 index 00000000000..3c8bf63422e Binary files /dev/null and b/docs/images/UGExamples/help.png differ diff --git a/docs/images/UGExamples/import0.png b/docs/images/UGExamples/import0.png new file mode 100644 index 00000000000..e5130931fe8 Binary files /dev/null and b/docs/images/UGExamples/import0.png differ diff --git a/docs/images/UGExamples/importeg.png b/docs/images/UGExamples/importeg.png new file mode 100644 index 00000000000..5574ddcbbab Binary files /dev/null and b/docs/images/UGExamples/importeg.png differ diff --git a/docs/images/UGExamples/list.png b/docs/images/UGExamples/list.png new file mode 100644 index 00000000000..26973c71e4c Binary files /dev/null and b/docs/images/UGExamples/list.png differ diff --git a/docs/images/UGExamples/search1.png b/docs/images/UGExamples/search1.png new file mode 100644 index 00000000000..bef77661f20 Binary files /dev/null and b/docs/images/UGExamples/search1.png differ diff --git a/docs/images/UGExamples/search3.png b/docs/images/UGExamples/search3.png new file mode 100644 index 00000000000..7f596c0a807 Binary files /dev/null and b/docs/images/UGExamples/search3.png differ diff --git a/docs/images/UGExamples/search4.png b/docs/images/UGExamples/search4.png new file mode 100644 index 00000000000..bf07975e8c4 Binary files /dev/null and b/docs/images/UGExamples/search4.png differ diff --git a/docs/images/UGExamples/search5.png b/docs/images/UGExamples/search5.png new file mode 100644 index 00000000000..f75350d1222 Binary files /dev/null and b/docs/images/UGExamples/search5.png differ diff --git a/docs/images/UGExamples/show1.png b/docs/images/UGExamples/show1.png new file mode 100644 index 00000000000..ed9e57e2441 Binary files /dev/null and b/docs/images/UGExamples/show1.png differ diff --git a/docs/images/UGExamples/show2.png b/docs/images/UGExamples/show2.png new file mode 100644 index 00000000000..5596bb09a60 Binary files /dev/null and b/docs/images/UGExamples/show2.png differ diff --git a/docs/images/UGExamples/show3.png b/docs/images/UGExamples/show3.png new file mode 100644 index 00000000000..c4768118b5f Binary files /dev/null and b/docs/images/UGExamples/show3.png differ diff --git a/docs/images/UGExamples/show4.png b/docs/images/UGExamples/show4.png new file mode 100644 index 00000000000..6739e509e74 Binary files /dev/null and b/docs/images/UGExamples/show4.png differ diff --git a/docs/images/UGExamples/showasses.png b/docs/images/UGExamples/showasses.png new file mode 100644 index 00000000000..08a7e13d2ea Binary files /dev/null and b/docs/images/UGExamples/showasses.png differ diff --git a/docs/images/UGExamples/showexport.png b/docs/images/UGExamples/showexport.png new file mode 100644 index 00000000000..8e3f0d4f6c7 Binary files /dev/null and b/docs/images/UGExamples/showexport.png differ diff --git a/docs/images/UGExamples/showgrp.png b/docs/images/UGExamples/showgrp.png new file mode 100644 index 00000000000..538ed6c6dac Binary files /dev/null and b/docs/images/UGExamples/showgrp.png differ diff --git a/docs/images/UGExamples/showstu.png b/docs/images/UGExamples/showstu.png new file mode 100644 index 00000000000..4d7a18e02d4 Binary files /dev/null and b/docs/images/UGExamples/showstu.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 91488fd1a0f..3cf127b407c 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 index 4bb8b2ce591..64146505721 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/UndoRedoState0.png b/docs/images/UndoRedoState0.png deleted file mode 100644 index 8f7538cd884..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 df9908d0948..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 36519c1015b..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 19959d01712..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 4c623e4f2c5..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 84ad2afa6bd..00000000000 Binary files a/docs/images/UndoRedoState5.png and /dev/null differ diff --git a/docs/images/UndoSequenceDiagram.png b/docs/images/UndoSequenceDiagram.png deleted file mode 100644 index 6addcd3a8d9..00000000000 Binary files a/docs/images/UndoSequenceDiagram.png and /dev/null differ diff --git a/docs/images/ganhongyao.png b/docs/images/ganhongyao.png new file mode 100644 index 00000000000..90878d88e68 Binary files /dev/null and b/docs/images/ganhongyao.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png index b1f70470137..a76bd278490 100644 Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ diff --git a/docs/images/johndoe.png b/docs/images/johndoe.png deleted file mode 100644 index 1ce7ce16dc8..00000000000 Binary files a/docs/images/johndoe.png and /dev/null differ diff --git a/docs/images/jonas-chow.png b/docs/images/jonas-chow.png new file mode 100644 index 00000000000..0f7632cbc4f Binary files /dev/null and b/docs/images/jonas-chow.png differ diff --git a/docs/images/leonghongfai.png b/docs/images/leonghongfai.png new file mode 100644 index 00000000000..6288ca8933e Binary files /dev/null and b/docs/images/leonghongfai.png differ diff --git a/docs/images/nbvannhi.png b/docs/images/nbvannhi.png new file mode 100644 index 00000000000..a754a1628a7 Binary files /dev/null and b/docs/images/nbvannhi.png differ diff --git a/docs/images/zhing22.png b/docs/images/zhing22.png new file mode 100644 index 00000000000..e31c22729e1 Binary files /dev/null and b/docs/images/zhing22.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..3dab246af2c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,19 +1,34 @@ --- layout: page -title: AddressBook Level-3 +title: Source Control --- -[![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) +[![CI Status](https://github.com/AY2122S1-CS2103T-W08-2/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2122S1-CS2103T-W08-2/tp/actions) +[![codecov](https://codecov.io/gh/AY2122S1-CS2103T-W08-2/tp/branch/master/graph/badge.svg?token=L5I73XVDJQ)](https://codecov.io/gh/AY2122S1-CS2103T-W08-2/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). +**Source Control** is a desktop app for CS1101S professors to manage the performance of their students. -* 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. +Source Control is **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, Source Control can help you track your students’ performance faster than traditional GUI apps! -**Acknowledgements** +## Usage +* Keeps track of administrative info and academic records of large student intake. +* Analyzes the performance of individual, groups and the cohort in assessments. +## User Guide +If you are interested in using **Source Control**, head over to the [Quick Start section of the **User Guide**](https://ay2122s1-cs2103t-w08-2.github.io/tp/UserGuide.html#quick-start). + +## Developer Guide +If you are interested in developing **Source Control**, check out the **[Developer Guide](https://ay2122s1-cs2103t-w08-2.github.io/tp/DeveloperGuide.html)** for information on how the application is designed. + +## About Us +If you are interested in contacting **Source Control** team, visit the [**About Us** website](https://ay2122s1-cs2103t-w08-2.github.io/tp/AboutUs.html). + +## Acknowledgements * Libraries used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5) +* Original project: [AddressBook Level-3](https://se-education.org/addressbook-level3) project created as part of the [SE-EDU](https://se-education.org) initiative +* Application logo: Inspired by [Source Academy](https://sourceacademy.nus.edu.sg/) +* Code snippet for getting jar file directory: Taken from [this Stackoverflow post](https://stackoverflow.com/questions/320542/how-to-get-the-path-of-a-running-jar-file) +* PlantUML sprite for rake symbol: Taken from [this PlantUML forum post](https://forum.plantuml.net/195/is-there-any-support-for-subactivity-or-the-rake-symbol) diff --git a/docs/team/ganhongyao.md b/docs/team/ganhongyao.md new file mode 100644 index 00000000000..6ff3e1dc16a --- /dev/null +++ b/docs/team/ganhongyao.md @@ -0,0 +1,50 @@ +--- +layout: page +title: Gan Hong Yao's Project Portfolio Page +--- + +### Project: Source Control + +Source Control is a desktop application for CS1101S professors to manage the performance of their students. Users interact with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to add groups. + * What it does: allows the user to add groups within the module. Students can also be added to the new group within the same command. + * Justification: This feature is necessary for the target user to know which groups a student belongs to. It also allows for detailed analysis of the performance of a particular group, relative to the cohort. + * Highlights: This enhancement is the sole entrypoint for the creation of groups. The implementation was challenging as there were many edge cases to consider, such as if a student to be added was specified more than once. + +* **New Feature**: Added the ability to render charts for better visualisation of assessment performance. + * What it does: allows the user to visualise how the cohort performed in an assessment using a histogram. + * Justification: This feature is necessary for the target user to analyse assessment results easily, such as whether the score distribution follows a desired bell-curve shape. + * Highlights: This enhancement is the first feature in Source Control that dealt with JavaFX Charts. The implementation was not trivial as JavaFX Charts were not easy to work with, and many style customisations had to be made for the chart to fit well with the application's theme. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s1.github.io/tp-dashboard/?search=ganhongyao) + +* **Project management**: + * Set up project issue tracker with relevant issue tags +
+ +* **Enhancements to existing features**: + * Adapted code from Address Book 3 to suit the context of Source Control (Pull requests [\#92](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/92), [\#94](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/94)) + * Updated the application title and icon (Pull request [\#180](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/180)) + * Wrote additional tests for existing features to increase coverage from 66% to 71% (Pull request [\#256](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/256)) + +* **Documentation**: + * User Guide: + * Added documentation for the features `addgroup` (Pull request [\#41](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/41)) + * Did cosmetic tweaks to existing documentation of `help` feature and glossary: (Pull request [\#189](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/189)) + * Developer Guide: + * Added implementation details of the `addgroup` feature. + * Updated project-level documentation and UML diagrams (Pull requests [\#273](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/273), [\#284](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/284)) + +* **Community**: + * PRs reviewed (with non-trivial review comments): (Pull requests [\#168](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/168), [\#177](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/177), [\#127](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/127)) + * Proposed working solution to UI bugs faced by several classmates (see [\#168](https://github.com/nus-cs2103-AY2122S1/forum/issues/168)) + * Contributed to forum discussions (examples: [\#191](https://github.com/nus-cs2103-AY2122S1/forum/issues/191), [\#141](https://github.com/nus-cs2103-AY2122S1/forum/issues/141)) + * Reported bugs and suggestions for other teams in the class (examples: [\#269](https://github.com/AY2122S1-CS2103T-F13-3/tp/issues/269), [#245](https://github.com/AY2122S1-CS2103T-F13-3/tp/issues/245), [\#239](https://github.com/AY2122S1-CS2103T-F13-3/tp/issues/239)) + +* **Tools**: + * Integrated JavaFX Charts to the project ([\#158](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/158)) + + diff --git a/docs/team/jonas-chow.md b/docs/team/jonas-chow.md new file mode 100644 index 00000000000..4b97658cf41 --- /dev/null +++ b/docs/team/jonas-chow.md @@ -0,0 +1,56 @@ +--- +layout: page +title: Jonas Chow's Project Portfolio Page +--- + +### Project: Source Control + +Source Control is a desktop application for CS1101S professors to manage the performance of their students. Users interact with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to import data from a CSV file. + * What it does: Allows the user to add students, groups, and assessments in bulk rather than by inputting hundreds of similar commands. + * Justification: The professor usually wants to input data for many students and assessments. Student data from external sources like Source Academy or lumiNUS can be exported to excel with a CSV file. This command allows the professor to easily import the data from external platforms. + * Highlights: Relative file paths are relative to some temporary folder on macOS when running the application via double-clicking the JAR file. This rather inconvenient feature required us to find a way to force the user inputted path to be relative to the folder which the application is in. + * Credits: [This](https://stackoverflow.com/questions/320542/how-to-get-the-path-of-a-running-jar-file) StackOverflow post on how to get the folder the JAR file is in. + +* **New Feature**: Added the ability to add aliases to commands. + * What it does: Allows the user to rename commonly used commands to shorter ones. The aliases persist even after leaving the application. + * Justification: We have many long command names such as `addassessment` and `addstudent`. Given that speed and ease of usage via the CLI interface is a priority, we allow the user to define their own shorter, more convenient aliases for commands that they use often. + * Highlights: There were many non-trivial design decisions considered when implementing this feature. Some of these include preventing cyclic alias dependencies, and a way to remove the alias. You can read more about these in the [developer guide](../DeveloperGuide.md). + +
+ +* **New Feature**: Added the ability to scroll through past commands. + * What it does: Pressing the `up` and `down` keys will scroll through your previous commands, similar to any conventional CLI interface. + * Justification: The case where the user would want to add scores for the same assessment to multiple students is one of many cases where a similar command has to be run multiple times. This feature allows one to input consecutive similar commands easily. + +* **New Feature**: Added the ability to export data. + * What it does: Allows the user to export the data to a CSV file. + * Justification: Exporting the data serves as both a way to share the data with the user's colleagues for them to import to their copy of the application, and a way to back up the data. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s1.github.io/tp-dashboard/?search=jonas-chow) + +* **Project management**: + * Set up and managed the milestones `v1.1` - `v1.3` (4 milestones) on GitHub + * Managed the releases `v1.1` - `v1.3.trial` (4 releases) on GitHub + * Enabled assertions for Gradle + +* **Enhancements to existing features**: + * Adapted storage code from Address Book 3 to suit the context of Source Control (Pull request [\#99](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/99)) + * Adapted tests from Address Book 3 to pass for the newly adapted Source Control code (Pull requests [\#103](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/103), [\#104](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/104), [\#107](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/107)) + * Enable the export graph function for the `show` command (Pull request [\#183](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/183)) + * Wrote additional tests for existing features (Pull request [\#257](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/257)) + +* **Documentation**: + * User Guide: + * Adapted content from Address Book 3 to suit the context of Source Control (Pull request [\#33](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/33)) + * Added documentation for the features `import`, `export`, and `alias` (Pull requests [\#33](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/33), [\#181](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/181)) + * Developer Guide: + * Updated glossary section (Pull request [\#50](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/50)) + * Added implementation details of the `import` and `alias` feature (Pull requests [\#144](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/144), [\#280](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/280)) + +* **Community**: + * PRs reviewed (with non-trivial review comments): (Pull requests [\#170](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/170), [\#244](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/244), [\#267](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/267)) + * Reported bugs and suggestions for other teams in the class (Examples: [\#177](https://github.com/AY2122S1-CS2103T-T09-2/tp/issues/177), [\#187](https://github.com/AY2122S1-CS2103T-T09-2/tp/issues/187), [\#1](https://github.com/jonas-chow/ped/issues/1)) diff --git a/docs/team/leonghongfai.md b/docs/team/leonghongfai.md new file mode 100644 index 00000000000..f89d5524e10 --- /dev/null +++ b/docs/team/leonghongfai.md @@ -0,0 +1,79 @@ +--- +layout: page +title: Leong Hong Fai's Project Portfolio Page +--- + +### Project: Source Control + +Source Control is a desktop application for CS1101S professors to manage the performance of their students. Users interact with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to add an individual student. + * What it does: allows the user to add an individual student into the database. If groups are provided, then the student is added to the group too. + * Justification: The feature is necessary for the target user as there needs to be a way to add in individual students after using the import function. This makes adding students to the database quicker for smaller number of additions than doing edits on the csv file. + * Highlights: This feature is the alternative method for adding students into the database besides the import function. Implementation was not too challenging, but involved a lot of refactoring as a whole from the old AddressBook3 for the team. + + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s1.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&since=2021-09-17&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=false&tabOpen=true&tabType=authorship&tabAuthor=leonghongfai&tabRepo=AY2122S1-CS2103T-W08-2%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code~other&authorshipIsBinaryFileTypeChecked=false) + + +* **Project management**: + * Updated site-wide settings + [\#43](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/43) + + +* **Enhancements to existing features**: + * Revamped the base layout of the GUI (Pull requests + [\#154](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/154), + [\#156](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/156)) + * Minor GUI updates + [\#106](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/106) + * Enhanced ABParser to take in two-word commands (Pull requests + [\#95](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/95), + [\#96](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/96), + [\#97](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/97), + [\#126](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/126), + [\#127](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/127)) + * Revert ABParser back to take in only single word commands due to issues caused by `alias` command + [\#239](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/239) + * Enhanced `addalloc`, `addgroup`, and `addscore` commands to show the list of students with naming conflicts + [\#174](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/174) + * Wrote additional tests for existing features to increase coverage (Pull requests + [\#98](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/98), + [\#191](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/191)) + + +* **Documentation**: + * User Guide: + * Added documentation for the add feature [\#54](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/54) + * Did cosmetic tweaks to existing documentation of features `clear`, `exit`: [\#74]() + * Developer Guide: + * Updated NFR subsection [\#44](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/44) + * Added `addstudent` feature implementation (Pull requests + [\#147](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/147), + [\#190](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/190), + [\#191](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/191), + [\#288](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/288)) + + +* **Community**: + * PRs reviewed (with non-trivial review comments): + [\#148](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/148), + [\#179](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/179), + [\#183](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/183), + + * Reported bugs and suggestions for other teams in the class + (examples: + [1](https://github.com/AY2122S1-CS2103-F10-2/tp/issues/111), + [2](https://github.com/AY2122S1-CS2103-F10-2/tp/issues/112), + [3](https://github.com/AY2122S1-CS2103-F10-2/tp/issues/118), + [4](https://github.com/AY2122S1-CS2103-F10-2/tp/issues/119), + [5](https://github.com/AY2122S1-CS2103-F10-2/tp/issues/121), + [6](https://github.com/AY2122S1-CS2103-F10-2/tp/issues/127), + [7](https://github.com/AY2122S1-CS2103-F10-2/tp/issues/134), + [8](https://github.com/AY2122S1-CS2103-F10-2/tp/issues/135), + [9](https://github.com/AY2122S1-CS2103-F10-2/tp/issues/163), + [10](https://github.com/AY2122S1-CS2103-F10-2/tp/issues/140), + [11](https://github.com/AY2122S1-CS2103-F10-2/tp/issues/142), + [12](https://github.com/AY2122S1-CS2103-F10-2/tp/issues/144)) diff --git a/docs/team/nbvannhi.md b/docs/team/nbvannhi.md new file mode 100644 index 00000000000..5ed45cae64a --- /dev/null +++ b/docs/team/nbvannhi.md @@ -0,0 +1,51 @@ +--- +layout: page +title: Nguyen Ba Van Nhi's Project Portfolio Page +--- + +### Project: Source Control + +Source Control is a desktop application for CS1101S professors to manage the performance of their students. Users interact with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +Given below are my contributions to the project. + +* **New Feature** `addalloc`: Added the ability to add allocation + * PR [\#117](https://git.io/JPAgf) + * This feature is necessary for the target user to know which groups a student belongs to. It also serves as a follow-up feature of the add group feature. + * This enhancement is the sole entrypoint for individual allocation of students to existing groups. The implementation was not trivial as a sub-class needed to be created to pass over the allocation information. + +* **New Feature** `addscore`: Added the ability to add assessment score + * PR [\#118](https://git.io/JPAw5), [\#243](https://git.io/JPAKh) + * This feature is necessary for the target user to add and modify scores apart from bulk import. + * This enhancement is the first feature that dealt with the modification of assessment data. The implementation was challenging due to the two-referral between assessments and students. Although the feature is quite similar to `addalloc` feature, a considerable amount of time was required for writing the code and tests. + +* **New Feature** `show`: Added the ability to show summary information + * PR [\#168](https://git.io/JPA6Y), [\#176](https://git.io/JPA6c), [\#177](https://git.io/JPA6l), [\#267](https://git.io/JPA6B) + * This feature is necessary for the target user to view the summary information together with the distribution graph. + * This enhancement is the only feature that was implemented jointly by three different members. The implementation required considerable efforts to understand the graph implementation built by others in order to integrate supplementary parts into the complete feature. + +* **Code contributed**: [RepoSense link](https://git.io/JPAzY) + +
+ +* **Project management**: + * Released [`v1.3.demo` on GitHub](https://git.io/Ji2HC) + +* **Enhancements to existing features**: + * Adapted code from AB3 to suit the context of Source Control (PR [\#77](https://git.io/JPAa8), [\#78](https://git.io/JPAau), [\#80](https://git.io/JPAa2), [\#81](https://git.io/JPAaM), [\#85](https://git.io/JPAaH), [\#86](https://git.io/JPAad), [\#87](https://git.io/JPAaF), [\#89](https://git.io/JPAaA)) + * Maintained consistency of error messages (PR [\#244](https://git.io/JPAzp)) + +* **Documentation**: + * User Guide: + * Adapted content from AB3 to suit the context of Source Control (PR [\#56](https://git.io/JPAgt), [\#57](https://git.io/JPAgm)) + * Added documentation for the features `addalloc` and `addscore` (PR [\#58](https://git.io/JPAgY)) + * Made cosmetic enhancements jointly via PR reviews (PR [\#203](https://git.io/JPAgO)) + * Developer Guide: + * Added implementation details of the `addalloc`, `addscore`, `show` features (PR [\#278](https://git.io/JXRf6)) + * Maintain the consistency between activity diagrams used in the guide (PR [\#283](https://git.io/JXRfk)) + +* **Community**: + * PRs reviewed (with non-trivial review comments) (PR [\#203](https://git.io/JPAgO), [\#174](https://git.io/JPAg2)) + * Resolved merge conflicts by choice (PR [\#183](https://git.io/JPAgE)) + * Added stub classes and typical data used commonly as test utilities (PR [\#117](https://git.io/JPAgf), [\#130](https://git.io/JPAgI)) + * Reported bugs and suggestions for other teams in the class (see [\#325](https://git.io/JPAgC), [\#327](https://git.io/JPAgn), [\#307](https://git.io/JPAgs)) diff --git a/docs/team/zhing22.md b/docs/team/zhing22.md new file mode 100644 index 00000000000..038467cc74a --- /dev/null +++ b/docs/team/zhing22.md @@ -0,0 +1,57 @@ +--- +layout: page +title: Tang Zhiying's Project Portfolio Page +--- + +### Project: Source Control + +Source Control is a desktop app for CS1101S professors to manage the performance of their students. +Source Control can store comprehensive data such as groupings and assessment scores of each student, for a large cohort size. +Source Control can provide data analysis for assessment performances, for students, groups, as well as the cohort. + +Given below are my contributions to the project. + +* **New Feature**: + * Added the ability to **search** based on name, NUSNET ID, groups, or tags of the student. [\#128](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/128) + * Added data analysis for performance of a particular student, and display using a line chart. [\#170](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/170) + * Added data analysis for performance of a particular group, and display using a line chart. [\#178](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/178) + + +* **Adapting project**: + * Removed irrelevant classes and methods from original AB3 code. [\#79](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/79) + * Added new data field: ID. [\#82](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/82) + * Added new data field: group. [\#88](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/88) + * Added new data field: groupList. [\#91](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/91) + + +* **Enhancements to existing features**: + * Enable wrapping of long chart axis names. [\#248](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/248) + * Improve console return messages. [\#252](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/252) + * Wrote additional tests for existing features: [\#254](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/254), [\#275](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/275) + + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s1.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2021-09-17&tabOpen=true&tabType=authorship&tabAuthor=zhing22&tabRepo=AY2122S1-CS2103T-W08-2%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false) + + +* **Project management**: + * Managed releases `v1.3` on GitHub. [release](https://github.com/AY2122S1-CS2103T-W08-2/tp/releases/tag/v1.3) + * Managed PDF conversion for User Guide and Developer Guide. + + +* **Documentation**: + * User Guide: + * Added documentation for the features `search` and `clear`. [\#35](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/35) + * Major revamp to update User Guide for practical dry run. [\#181](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/181) + * Fix PED bugs raised. [\#203](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/203) + * Added visual aids for command examples. [\#260](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/260) + * Developer Guide: + * Updated target user profile, value proposition and user stories [\#34](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/34) + * Added implementation details of the `search` feature. [\#152](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/152) + * Updated Appendix sections. [\#276](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/276), [\#268](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/268), [\#292](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/292) + * Others: + * Update README and index.md. [\#196](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/196) + + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#144](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/144) [\#158](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/158) [\#179](https://github.com/AY2122S1-CS2103T-W08-2/tp/pull/179) + * Reported bugs and suggestions for other teams in the class: [\#207](https://github.com/AY2122S1-CS2103-W14-1/tp/issues/207) [\#188](https://github.com/AY2122S1-CS2103-W14-1/tp/issues/188) [\#189](https://github.com/AY2122S1-CS2103-W14-1/tp/issues/189) [\#190](https://github.com/AY2122S1-CS2103-W14-1/tp/issues/190) [\#195](https://github.com/AY2122S1-CS2103-W14-1/tp/issues/195) [\#204](https://github.com/AY2122S1-CS2103-W14-1/tp/issues/204) diff --git a/docs/tutorials/AddRemark.md b/docs/tutorials/AddRemark.md index 8919d8eaa17..e75155ba6e4 100644 --- a/docs/tutorials/AddRemark.md +++ b/docs/tutorials/AddRemark.md @@ -23,9 +23,9 @@ For now, let’s keep `RemarkCommand` as simple as possible and print some outpu **`RemarkCommand.java`:** ``` java -package seedu.address.logic.commands; +package seedu.sourcecontrol.logic.commands; -import seedu.address.model.Model; +import seedu.sourcecontrol.model.Model; /** * Changes the remark of an existing person in the address book. @@ -91,7 +91,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 -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.sourcecontrol.commons.util.CollectionUtil.requireAllNonNull; //... public class RemarkCommand extends Command { //... @@ -142,7 +142,7 @@ Your code should look something like [this](https://github.com/se-edu/addressboo Now let’s move on to writing a parser that will extract the index and remark from the input provided by the user. -Create a `RemarkCommandParser` class in the `seedu.address.logic.parser` package. The class must extend the `Parser` interface. +Create a `RemarkCommandParser` class in the `seedu.sourcecontrol.logic.parser` package. The class must extend the `Parser` interface. ![The relationship between Parser and RemarkCommandParser](../images/add-remark/ParserInterface.png) @@ -229,7 +229,7 @@ Now that we have all the information that we need, let’s lay the groundwork fo ### Add a new `Remark` class -Create a new `Remark` in `seedu.address.model.person`. Since a `Remark` is a field that is similar to `Address`, we can reuse a significant bit of code. +Create a new `Remark` in `seedu.sourcecontrol.model.person`. Since a `Remark` is a field that is similar to `Address`, we can reuse a significant bit of code. A copy-paste and search-replace later, you should have something like [this](https://github.com/se-edu/addressbook-level3/commit/4516e099699baa9e2d51801bd26f016d812dedcc#diff-af2f075d24dfcd333876f0fbce321f25). Note how `Remark` has no constrains and thus does not require input validation. @@ -242,7 +242,7 @@ Let’s change `RemarkCommand` and `RemarkCommandParser` to use the new `Remark` Without getting too deep into `fxml`, let’s go on a 5 minute adventure to get some placeholder text to show up for each person. -Simply add the following to [`seedu.address.ui.PersonCard`](https://github.com/se-edu/addressbook-level3/commit/850b78879582f38accb05dd20c245963c65ea599#diff-0c6b6abcfac8c205e075294f25e851fe). +Simply add the following to [`seedu.sourcecontrol.ui.StudentCard`](https://github.com/se-edu/addressbook-level3/commit/850b78879582f38accb05dd20c245963c65ea599#diff-0c6b6abcfac8c205e075294f25e851fe). **`PersonCard.java`:** diff --git a/docs/tutorials/RemovingFields.md b/docs/tutorials/RemovingFields.md index f29169bc924..419e4192d1b 100644 --- a/docs/tutorials/RemovingFields.md +++ b/docs/tutorials/RemovingFields.md @@ -28,7 +28,7 @@ IntelliJ IDEA provides a refactoring tool that can identify *most* parts of a re ### Assisted refactoring -The `address` field in `Person` is actually an instance of the `seedu.address.model.person.Address` class. Since removing the `Address` class will break the application, we start by identifying `Address`'s usages. This allows us to see code that depends on `Address` to function properly and edit them on a case-by-case basis. Right-click the `Address` class and select `Refactor` \> `Safe Delete` through the menu. +The `address` field in `Person` is actually an instance of the `seedu.sourcecontrol.model.person.Address` class. Since removing the `Address` class will break the application, we start by identifying `Address`'s usages. This allows us to see code that depends on `Address` to function properly and edit them on a case-by-case basis. Right-click the `Address` class and select `Refactor` \> `Safe Delete` through the menu. * :bulb: To make things simpler, you can unselect the options `Search in comments and strings` and `Search for text occurrences` ![Usages detected](../images/remove/UnsafeDelete.png) diff --git a/docs/tutorials/TracingCode.md b/docs/tutorials/TracingCode.md index 4fb62a83ef6..c1346bb2588 100644 --- a/docs/tutorials/TracingCode.md +++ b/docs/tutorials/TracingCode.md @@ -39,7 +39,7 @@ In our case, we would want to begin the tracing at the very point where the App -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`. +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.sourcecontrol.logic.Logic`. @@ -48,7 +48,7 @@ According to the sequence diagram you saw earlier (and repeated above for refere :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`.
-A quick look at the `seedu.address.logic.Logic` (an extract given below) confirms that this indeed might be what we’re looking for. +A quick look at the `seedu.sourcecontrol.logic.Logic` (an extract given below) confirms that this indeed might be what we’re looking for. ```java public interface Logic { diff --git a/src/main/java/seedu/address/commons/core/Messages.java b/src/main/java/seedu/address/commons/core/Messages.java deleted file mode 100644 index 1deb3a1e469..00000000000 --- a/src/main/java/seedu/address/commons/core/Messages.java +++ /dev/null @@ -1,13 +0,0 @@ -package seedu.address.commons.core; - -/** - * Container for user visible messages. - */ -public class Messages { - - public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; - 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_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; - -} diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java deleted file mode 100644 index 9d9c6d15bdc..00000000000 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ /dev/null @@ -1,81 +0,0 @@ -package seedu.address.logic; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.logging.Logger; - -import javafx.collections.ObservableList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.commons.core.LogsCenter; -import seedu.address.logic.commands.Command; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.AddressBookParser; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.Model; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; -import seedu.address.storage.Storage; - -/** - * The main LogicManager of the app. - */ -public class LogicManager implements Logic { - public static final String FILE_OPS_ERROR_MESSAGE = "Could not save data to file: "; - private final Logger logger = LogsCenter.getLogger(LogicManager.class); - - private final Model model; - private final Storage storage; - private final AddressBookParser addressBookParser; - - /** - * Constructs a {@code LogicManager} with the given {@code Model} and {@code Storage}. - */ - public LogicManager(Model model, Storage storage) { - this.model = model; - this.storage = storage; - addressBookParser = new AddressBookParser(); - } - - @Override - public CommandResult execute(String commandText) throws CommandException, ParseException { - logger.info("----------------[USER COMMAND][" + commandText + "]"); - - CommandResult commandResult; - Command command = addressBookParser.parseCommand(commandText); - commandResult = command.execute(model); - - try { - storage.saveAddressBook(model.getAddressBook()); - } catch (IOException ioe) { - throw new CommandException(FILE_OPS_ERROR_MESSAGE + ioe, ioe); - } - - return commandResult; - } - - @Override - public ReadOnlyAddressBook getAddressBook() { - return model.getAddressBook(); - } - - @Override - public ObservableList getFilteredPersonList() { - return model.getFilteredPersonList(); - } - - @Override - public Path getAddressBookFilePath() { - return model.getAddressBookFilePath(); - } - - @Override - public GuiSettings getGuiSettings() { - return model.getGuiSettings(); - } - - @Override - public void setGuiSettings(GuiSettings guiSettings) { - model.setGuiSettings(guiSettings); - } -} diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java deleted file mode 100644 index 71656d7c5c8..00000000000 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ /dev/null @@ -1,67 +0,0 @@ -package seedu.address.logic.commands; - -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_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.person.Person; - -/** - * Adds a person to the address book. - */ -public class AddCommand extends Command { - - public static final String COMMAND_WORD = "add"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " - + "Parameters: " - + PREFIX_NAME + "NAME " - + PREFIX_PHONE + "PHONE " - + PREFIX_EMAIL + "EMAIL " - + PREFIX_ADDRESS + "ADDRESS " - + "[" + 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_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"; - - private final Person toAdd; - - /** - * Creates an AddCommand to add the specified {@code Person} - */ - public AddCommand(Person person) { - requireNonNull(person); - toAdd = person; - } - - @Override - public CommandResult execute(Model model) throws CommandException { - requireNonNull(model); - - if (model.hasPerson(toAdd)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); - } - - model.addPerson(toAdd); - return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof AddCommand // instanceof handles nulls - && toAdd.equals(((AddCommand) other).toAdd)); - } -} diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java deleted file mode 100644 index 02fd256acba..00000000000 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ /dev/null @@ -1,53 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import java.util.List; - -import seedu.address.commons.core.Messages; -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.person.Person; - -/** - * Deletes a person identified using it's displayed index from the address book. - */ -public class DeleteCommand extends Command { - - public static final String COMMAND_WORD = "delete"; - - 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" - + "Example: " + COMMAND_WORD + " 1"; - - public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; - - private final Index targetIndex; - - public DeleteCommand(Index targetIndex) { - this.targetIndex = targetIndex; - } - - @Override - public CommandResult execute(Model model) throws CommandException { - requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); - - if (targetIndex.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); - model.deletePerson(personToDelete); - return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, personToDelete)); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof DeleteCommand // instanceof handles nulls - && targetIndex.equals(((DeleteCommand) other).targetIndex)); // state check - } -} diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java deleted file mode 100644 index 7e36114902f..00000000000 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ /dev/null @@ -1,226 +0,0 @@ -package seedu.address.logic.commands; - -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_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -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.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import seedu.address.commons.core.Messages; -import seedu.address.commons.core.index.Index; -import seedu.address.commons.util.CollectionUtil; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -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.tag.Tag; - -/** - * Edits the details of an existing person in the address book. - */ -public class EditCommand extends Command { - - public static final String COMMAND_WORD = "edit"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the person identified " - + "by the index number used in the displayed person list. " - + "Existing values will be overwritten by the input values.\n" - + "Parameters: INDEX (must be a positive integer) " - + "[" + PREFIX_NAME + "NAME] " - + "[" + PREFIX_PHONE + "PHONE] " - + "[" + PREFIX_EMAIL + "EMAIL] " - + "[" + PREFIX_ADDRESS + "ADDRESS] " - + "[" + PREFIX_TAG + "TAG]...\n" - + "Example: " + COMMAND_WORD + " 1 " - + PREFIX_PHONE + "91234567 " - + PREFIX_EMAIL + "johndoe@example.com"; - - public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; - public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; - - private final Index index; - private final EditPersonDescriptor editPersonDescriptor; - - /** - * @param index of the person in the filtered person list to edit - * @param editPersonDescriptor details to edit the person with - */ - public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { - requireNonNull(index); - requireNonNull(editPersonDescriptor); - - this.index = index; - this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor); - } - - @Override - public CommandResult execute(Model model) throws CommandException { - requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); - - if (index.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - Person personToEdit = lastShownList.get(index.getZeroBased()); - Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); - - if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); - } - - model.setPerson(personToEdit, editedPerson); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedPerson)); - } - - /** - * Creates and returns a {@code Person} with the details of {@code personToEdit} - * edited with {@code editPersonDescriptor}. - */ - private static Person createEditedPerson(Person personToEdit, EditPersonDescriptor editPersonDescriptor) { - assert personToEdit != null; - - Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName()); - Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); - 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); - } - - @Override - public boolean equals(Object other) { - // short circuit if same object - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof EditCommand)) { - return false; - } - - // state check - EditCommand e = (EditCommand) other; - return index.equals(e.index) - && editPersonDescriptor.equals(e.editPersonDescriptor); - } - - /** - * Stores the details to edit the person with. Each non-empty field value will replace the - * corresponding field value of the person. - */ - public static class EditPersonDescriptor { - private Name name; - private Phone phone; - private Email email; - private Address address; - private Set tags; - - public EditPersonDescriptor() {} - - /** - * Copy constructor. - * A defensive copy of {@code tags} is used internally. - */ - public EditPersonDescriptor(EditPersonDescriptor toCopy) { - setName(toCopy.name); - setPhone(toCopy.phone); - setEmail(toCopy.email); - setAddress(toCopy.address); - setTags(toCopy.tags); - } - - /** - * Returns true if at least one field is edited. - */ - public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); - } - - public void setName(Name name) { - this.name = name; - } - - public Optional getName() { - return Optional.ofNullable(name); - } - - public void setPhone(Phone phone) { - this.phone = phone; - } - - public Optional getPhone() { - return Optional.ofNullable(phone); - } - - public void setEmail(Email email) { - this.email = email; - } - - public Optional getEmail() { - return Optional.ofNullable(email); - } - - public void setAddress(Address address) { - this.address = address; - } - - public Optional
getAddress() { - return Optional.ofNullable(address); - } - - /** - * Sets {@code tags} to this object's {@code tags}. - * A defensive copy of {@code tags} is used internally. - */ - public void setTags(Set tags) { - this.tags = (tags != null) ? new HashSet<>(tags) : null; - } - - /** - * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} - * if modification is attempted. - * Returns {@code Optional#empty()} if {@code tags} is null. - */ - public Optional> getTags() { - return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); - } - - @Override - public boolean equals(Object other) { - // short circuit if same object - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof EditPersonDescriptor)) { - return false; - } - - // state check - EditPersonDescriptor e = (EditPersonDescriptor) other; - - return getName().equals(e.getName()) - && getPhone().equals(e.getPhone()) - && getEmail().equals(e.getEmail()) - && getAddress().equals(e.getAddress()) - && getTags().equals(e.getTags()); - } - } -} diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java deleted file mode 100644 index d6b19b0a0de..00000000000 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ /dev/null @@ -1,42 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import seedu.address.commons.core.Messages; -import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; - -/** - * 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 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"; - - private final NameContainsKeywordsPredicate predicate; - - public FindCommand(NameContainsKeywordsPredicate predicate) { - this.predicate = predicate; - } - - @Override - public CommandResult execute(Model model) { - requireNonNull(model); - model.updateFilteredPersonList(predicate); - return new CommandResult( - String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof FindCommand // instanceof handles nulls - && predicate.equals(((FindCommand) other).predicate)); // state check - } -} diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java deleted file mode 100644 index 84be6ad2596..00000000000 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ /dev/null @@ -1,24 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; - -import seedu.address.model.Model; - -/** - * Lists all persons in the address book to the user. - */ -public class ListCommand extends Command { - - public static final String COMMAND_WORD = "list"; - - public static final String MESSAGE_SUCCESS = "Listed all persons"; - - - @Override - public CommandResult execute(Model model) { - requireNonNull(model); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(MESSAGE_SUCCESS); - } -} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java deleted file mode 100644 index 3b8bfa035e8..00000000000 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ /dev/null @@ -1,60 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.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_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import java.util.Set; -import java.util.stream.Stream; - -import seedu.address.logic.commands.AddCommand; -import seedu.address.logic.parser.exceptions.ParseException; -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.tag.Tag; - -/** - * Parses input arguments and creates a new AddCommand object - */ -public class AddCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the AddCommand - * and returns an AddCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public AddCommand parse(String args) throws ParseException { - ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); - - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) - || !argMultimap.getPreamble().isEmpty()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); - } - - 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()); - Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - - Person person = new Person(name, phone, email, address, tagList); - - return new AddCommand(person); - } - - /** - * 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/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java deleted file mode 100644 index 1e466792b46..00000000000 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ /dev/null @@ -1,76 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import seedu.address.logic.commands.AddCommand; -import seedu.address.logic.commands.ClearCommand; -import seedu.address.logic.commands.Command; -import seedu.address.logic.commands.DeleteCommand; -import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.ExitCommand; -import seedu.address.logic.commands.FindCommand; -import seedu.address.logic.commands.HelpCommand; -import seedu.address.logic.commands.ListCommand; -import seedu.address.logic.parser.exceptions.ParseException; - -/** - * Parses user input. - */ -public class AddressBookParser { - - /** - * Used for initial separation of command word and args. - */ - private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); - - /** - * Parses user input into command for execution. - * - * @param userInput full user input string - * @return the command based on the user input - * @throws ParseException if the user input does not conform the expected format - */ - public Command parseCommand(String userInput) throws ParseException { - final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); - if (!matcher.matches()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); - } - - final String commandWord = matcher.group("commandWord"); - final String arguments = matcher.group("arguments"); - switch (commandWord) { - - case AddCommand.COMMAND_WORD: - return new AddCommandParser().parse(arguments); - - case EditCommand.COMMAND_WORD: - return new EditCommandParser().parse(arguments); - - case DeleteCommand.COMMAND_WORD: - return new DeleteCommandParser().parse(arguments); - - case ClearCommand.COMMAND_WORD: - return new ClearCommand(); - - case FindCommand.COMMAND_WORD: - return new FindCommandParser().parse(arguments); - - case ListCommand.COMMAND_WORD: - return new ListCommand(); - - case ExitCommand.COMMAND_WORD: - return new ExitCommand(); - - case HelpCommand.COMMAND_WORD: - return new HelpCommand(); - - default: - throw new ParseException(MESSAGE_UNKNOWN_COMMAND); - } - } - -} diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java deleted file mode 100644 index 75b1a9bf119..00000000000 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ /dev/null @@ -1,15 +0,0 @@ -package seedu.address.logic.parser; - -/** - * Contains Command Line Interface (CLI) syntax definitions common to multiple commands - */ -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/"); - -} diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java deleted file mode 100644 index 522b93081cc..00000000000 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ /dev/null @@ -1,29 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; - -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.DeleteCommand; -import seedu.address.logic.parser.exceptions.ParseException; - -/** - * Parses input arguments and creates a new DeleteCommand object - */ -public class DeleteCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the DeleteCommand - * and returns a DeleteCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public DeleteCommand parse(String args) throws ParseException { - try { - Index index = ParserUtil.parseIndex(args); - return new DeleteCommand(index); - } catch (ParseException pe) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); - } - } - -} diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java deleted file mode 100644 index 845644b7dea..00000000000 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ /dev/null @@ -1,82 +0,0 @@ -package seedu.address.logic.parser; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.core.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_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import java.util.Collection; -import java.util.Collections; -import java.util.Optional; -import java.util.Set; - -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.tag.Tag; - -/** - * Parses input arguments and creates a new EditCommand object - */ -public class EditCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the EditCommand - * and returns an EditCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public EditCommand parse(String args) throws ParseException { - requireNonNull(args); - ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); - - Index index; - - try { - index = ParserUtil.parseIndex(argMultimap.getPreamble()); - } catch (ParseException pe) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); - } - - EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); - if (argMultimap.getValue(PREFIX_NAME).isPresent()) { - editPersonDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); - } - if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { - editPersonDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get())); - } - if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { - editPersonDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get())); - } - if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { - editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); - } - parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); - - if (!editPersonDescriptor.isAnyFieldEdited()) { - throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); - } - - return new EditCommand(index, editPersonDescriptor); - } - - /** - * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. - * If {@code tags} contain only one element which is an empty string, it will be parsed into a - * {@code Set} containing zero tags. - */ - private Optional> parseTagsForEdit(Collection tags) throws ParseException { - assert tags != null; - - if (tags.isEmpty()) { - return Optional.empty(); - } - Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; - return Optional.of(ParserUtil.parseTags(tagSet)); - } - -} diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java deleted file mode 100644 index 4fb71f23103..00000000000 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ /dev/null @@ -1,33 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; - -import java.util.Arrays; - -import seedu.address.logic.commands.FindCommand; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; - -/** - * Parses input arguments and creates a new FindCommand object - */ -public class FindCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the FindCommand - * and returns a FindCommand object for execution. - * @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)); - } - - String[] nameKeywords = trimmedArgs.split("\\s+"); - - return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); - } - -} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java deleted file mode 100644 index b117acb9c55..00000000000 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ /dev/null @@ -1,124 +0,0 @@ -package seedu.address.logic.parser; - -import static java.util.Objects.requireNonNull; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; - -import seedu.address.commons.core.index.Index; -import seedu.address.commons.util.StringUtil; -import seedu.address.logic.parser.exceptions.ParseException; -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.tag.Tag; - -/** - * Contains utility methods used for parsing strings in the various *Parser classes. - */ -public class ParserUtil { - - public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; - - /** - * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be - * trimmed. - * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). - */ - public static Index parseIndex(String oneBasedIndex) throws ParseException { - String trimmedIndex = oneBasedIndex.trim(); - if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) { - throw new ParseException(MESSAGE_INVALID_INDEX); - } - return Index.fromOneBased(Integer.parseInt(trimmedIndex)); - } - - /** - * Parses a {@code String name} into a {@code Name}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code name} is invalid. - */ - public static Name parseName(String name) throws ParseException { - requireNonNull(name); - String trimmedName = name.trim(); - if (!Name.isValidName(trimmedName)) { - throw new ParseException(Name.MESSAGE_CONSTRAINTS); - } - return new Name(trimmedName); - } - - /** - * Parses a {@code String phone} into a {@code Phone}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code phone} is invalid. - */ - public static Phone parsePhone(String phone) throws ParseException { - requireNonNull(phone); - String trimmedPhone = phone.trim(); - if (!Phone.isValidPhone(trimmedPhone)) { - throw new ParseException(Phone.MESSAGE_CONSTRAINTS); - } - return new Phone(trimmedPhone); - } - - /** - * Parses a {@code String address} into an {@code Address}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code address} is invalid. - */ - public static Address parseAddress(String address) throws ParseException { - requireNonNull(address); - String trimmedAddress = address.trim(); - if (!Address.isValidAddress(trimmedAddress)) { - throw new ParseException(Address.MESSAGE_CONSTRAINTS); - } - return new Address(trimmedAddress); - } - - /** - * Parses a {@code String email} into an {@code Email}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code email} is invalid. - */ - public static Email parseEmail(String email) throws ParseException { - requireNonNull(email); - String trimmedEmail = email.trim(); - if (!Email.isValidEmail(trimmedEmail)) { - throw new ParseException(Email.MESSAGE_CONSTRAINTS); - } - return new Email(trimmedEmail); - } - - /** - * Parses a {@code String tag} into a {@code Tag}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code tag} is invalid. - */ - public static Tag parseTag(String tag) throws ParseException { - requireNonNull(tag); - String trimmedTag = tag.trim(); - if (!Tag.isValidTagName(trimmedTag)) { - throw new ParseException(Tag.MESSAGE_CONSTRAINTS); - } - return new Tag(trimmedTag); - } - - /** - * Parses {@code Collection tags} into a {@code Set}. - */ - public static Set parseTags(Collection tags) throws ParseException { - requireNonNull(tags); - final Set tagSet = new HashSet<>(); - for (String tagName : tags) { - tagSet.add(parseTag(tagName)); - } - return tagSet; - } -} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java deleted file mode 100644 index 1a943a0781a..00000000000 --- a/src/main/java/seedu/address/model/AddressBook.java +++ /dev/null @@ -1,120 +0,0 @@ -package seedu.address.model; - -import static java.util.Objects.requireNonNull; - -import java.util.List; - -import javafx.collections.ObservableList; -import seedu.address.model.person.Person; -import seedu.address.model.person.UniquePersonList; - -/** - * Wraps all data at the address-book level - * Duplicates are not allowed (by .isSamePerson comparison) - */ -public class AddressBook implements ReadOnlyAddressBook { - - private final UniquePersonList persons; - - /* - * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication - * between constructors. See https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html - * - * Note that non-static init blocks are not recommended to use. There are other ways to avoid duplication - * among constructors. - */ - { - persons = new UniquePersonList(); - } - - public AddressBook() {} - - /** - * Creates an AddressBook using the Persons in the {@code toBeCopied} - */ - public AddressBook(ReadOnlyAddressBook toBeCopied) { - this(); - resetData(toBeCopied); - } - - //// list overwrite operations - - /** - * Replaces the contents of the person list with {@code persons}. - * {@code persons} must not contain duplicate persons. - */ - public void setPersons(List persons) { - this.persons.setPersons(persons); - } - - /** - * Resets the existing data of this {@code AddressBook} with {@code newData}. - */ - public void resetData(ReadOnlyAddressBook newData) { - requireNonNull(newData); - - setPersons(newData.getPersonList()); - } - - //// person-level operations - - /** - * Returns true if a person with the same identity as {@code person} exists in the address book. - */ - public boolean hasPerson(Person person) { - requireNonNull(person); - return persons.contains(person); - } - - /** - * Adds a person to the address book. - * The person must not already exist in the address book. - */ - public void addPerson(Person p) { - persons.add(p); - } - - /** - * Replaces the given person {@code target} in the list with {@code editedPerson}. - * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. - */ - public void setPerson(Person target, Person editedPerson) { - requireNonNull(editedPerson); - - persons.setPerson(target, editedPerson); - } - - /** - * Removes {@code key} from this {@code AddressBook}. - * {@code key} must exist in the address book. - */ - public void removePerson(Person key) { - persons.remove(key); - } - - //// util methods - - @Override - public String toString() { - return persons.asUnmodifiableObservableList().size() + " persons"; - // TODO: refine later - } - - @Override - public ObservableList getPersonList() { - return persons.asUnmodifiableObservableList(); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof AddressBook // instanceof handles nulls - && persons.equals(((AddressBook) other).persons)); - } - - @Override - public int hashCode() { - return persons.hashCode(); - } -} diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java deleted file mode 100644 index d54df471c1f..00000000000 --- a/src/main/java/seedu/address/model/Model.java +++ /dev/null @@ -1,87 +0,0 @@ -package seedu.address.model; - -import java.nio.file.Path; -import java.util.function.Predicate; - -import javafx.collections.ObservableList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.model.person.Person; - -/** - * The API of the Model component. - */ -public interface Model { - /** {@code Predicate} that always evaluate to true */ - Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; - - /** - * Replaces user prefs data with the data in {@code userPrefs}. - */ - void setUserPrefs(ReadOnlyUserPrefs userPrefs); - - /** - * Returns the user prefs. - */ - ReadOnlyUserPrefs getUserPrefs(); - - /** - * Returns the user prefs' GUI settings. - */ - GuiSettings getGuiSettings(); - - /** - * Sets the user prefs' GUI settings. - */ - void setGuiSettings(GuiSettings guiSettings); - - /** - * Returns the user prefs' address book file path. - */ - Path getAddressBookFilePath(); - - /** - * Sets the user prefs' address book file path. - */ - void setAddressBookFilePath(Path addressBookFilePath); - - /** - * Replaces address book data with the data in {@code addressBook}. - */ - void setAddressBook(ReadOnlyAddressBook addressBook); - - /** Returns the AddressBook */ - ReadOnlyAddressBook getAddressBook(); - - /** - * Returns true if a person with the same identity as {@code person} exists in the address book. - */ - boolean hasPerson(Person person); - - /** - * Deletes the given person. - * The person must exist in the address book. - */ - void deletePerson(Person target); - - /** - * Adds the given person. - * {@code person} must not already exist in the address book. - */ - void addPerson(Person person); - - /** - * Replaces the given person {@code target} with {@code editedPerson}. - * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. - */ - void setPerson(Person target, Person editedPerson); - - /** Returns an unmodifiable view of the filtered person list */ - ObservableList getFilteredPersonList(); - - /** - * Updates the filter of the filtered person list to filter by the given {@code predicate}. - * @throws NullPointerException if {@code predicate} is null. - */ - void updateFilteredPersonList(Predicate predicate); -} diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java deleted file mode 100644 index 0650c954f5c..00000000000 --- a/src/main/java/seedu/address/model/ModelManager.java +++ /dev/null @@ -1,151 +0,0 @@ -package seedu.address.model; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; - -import java.nio.file.Path; -import java.util.function.Predicate; -import java.util.logging.Logger; - -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.person.Person; - -/** - * Represents the in-memory model of the address book data. - */ -public class ModelManager implements Model { - private static final Logger logger = LogsCenter.getLogger(ModelManager.class); - - private final AddressBook addressBook; - private final UserPrefs userPrefs; - private final FilteredList filteredPersons; - - /** - * Initializes a ModelManager with the given addressBook and userPrefs. - */ - public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs) { - super(); - requireAllNonNull(addressBook, userPrefs); - - logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); - - this.addressBook = new AddressBook(addressBook); - this.userPrefs = new UserPrefs(userPrefs); - filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); - } - - public ModelManager() { - this(new AddressBook(), new UserPrefs()); - } - - //=========== UserPrefs ================================================================================== - - @Override - public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { - requireNonNull(userPrefs); - this.userPrefs.resetData(userPrefs); - } - - @Override - public ReadOnlyUserPrefs getUserPrefs() { - return userPrefs; - } - - @Override - public GuiSettings getGuiSettings() { - return userPrefs.getGuiSettings(); - } - - @Override - public void setGuiSettings(GuiSettings guiSettings) { - requireNonNull(guiSettings); - userPrefs.setGuiSettings(guiSettings); - } - - @Override - public Path getAddressBookFilePath() { - return userPrefs.getAddressBookFilePath(); - } - - @Override - public void setAddressBookFilePath(Path addressBookFilePath) { - requireNonNull(addressBookFilePath); - userPrefs.setAddressBookFilePath(addressBookFilePath); - } - - //=========== AddressBook ================================================================================ - - @Override - public void setAddressBook(ReadOnlyAddressBook addressBook) { - this.addressBook.resetData(addressBook); - } - - @Override - public ReadOnlyAddressBook getAddressBook() { - return addressBook; - } - - @Override - public boolean hasPerson(Person person) { - requireNonNull(person); - return addressBook.hasPerson(person); - } - - @Override - public void deletePerson(Person target) { - addressBook.removePerson(target); - } - - @Override - public void addPerson(Person person) { - addressBook.addPerson(person); - updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - } - - @Override - public void setPerson(Person target, Person editedPerson) { - requireAllNonNull(target, editedPerson); - - addressBook.setPerson(target, editedPerson); - } - - //=========== Filtered Person List Accessors ============================================================= - - /** - * Returns an unmodifiable view of the list of {@code Person} backed by the internal list of - * {@code versionedAddressBook} - */ - @Override - public ObservableList getFilteredPersonList() { - return filteredPersons; - } - - @Override - public void updateFilteredPersonList(Predicate predicate) { - requireNonNull(predicate); - filteredPersons.setPredicate(predicate); - } - - @Override - public boolean equals(Object obj) { - // short circuit if same object - if (obj == this) { - return true; - } - - // instanceof handles nulls - if (!(obj instanceof ModelManager)) { - return false; - } - - // state check - ModelManager other = (ModelManager) obj; - return addressBook.equals(other.addressBook) - && userPrefs.equals(other.userPrefs) - && filteredPersons.equals(other.filteredPersons); - } - -} diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java deleted file mode 100644 index 6ddc2cd9a29..00000000000 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ /dev/null @@ -1,17 +0,0 @@ -package seedu.address.model; - -import javafx.collections.ObservableList; -import seedu.address.model.person.Person; - -/** - * Unmodifiable view of an address book - */ -public interface ReadOnlyAddressBook { - - /** - * Returns an unmodifiable view of the persons list. - * This list will not contain any duplicate persons. - */ - ObservableList getPersonList(); - -} diff --git a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java b/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java deleted file mode 100644 index befd58a4c73..00000000000 --- a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java +++ /dev/null @@ -1,16 +0,0 @@ -package seedu.address.model; - -import java.nio.file.Path; - -import seedu.address.commons.core.GuiSettings; - -/** - * Unmodifiable view of user prefs. - */ -public interface ReadOnlyUserPrefs { - - GuiSettings getGuiSettings(); - - Path getAddressBookFilePath(); - -} diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java deleted file mode 100644 index 60472ca22a0..00000000000 --- a/src/main/java/seedu/address/model/person/Address.java +++ /dev/null @@ -1,57 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's address in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} - */ -public class Address { - - public static final String MESSAGE_CONSTRAINTS = "Addresses can take any values, 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 final String value; - - /** - * Constructs an {@code Address}. - * - * @param address A valid address. - */ - public Address(String address) { - requireNonNull(address); - checkArgument(isValidAddress(address), MESSAGE_CONSTRAINTS); - value = address; - } - - /** - * Returns true if a given string is a valid email. - */ - public static boolean isValidAddress(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Address // instanceof handles nulls - && value.equals(((Address) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java deleted file mode 100644 index f866e7133de..00000000000 --- a/src/main/java/seedu/address/model/person/Email.java +++ /dev/null @@ -1,71 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's email in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidEmail(String)} - */ -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" - + "The domain name must:\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."; - // 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 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_REGEX = "(" + DOMAIN_PART_REGEX + "\\.)*" + DOMAIN_LAST_PART_REGEX; - public static final String VALIDATION_REGEX = LOCAL_PART_REGEX + "@" + DOMAIN_REGEX; - - public final String value; - - /** - * Constructs an {@code Email}. - * - * @param email A valid email address. - */ - public Email(String email) { - requireNonNull(email); - checkArgument(isValidEmail(email), MESSAGE_CONSTRAINTS); - value = email; - } - - /** - * Returns if a given string is a valid email. - */ - public static boolean isValidEmail(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Email // instanceof handles nulls - && value.equals(((Email) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java deleted file mode 100644 index 8ff1d83fe89..00000000000 --- a/src/main/java/seedu/address/model/person/Person.java +++ /dev/null @@ -1,123 +0,0 @@ -package seedu.address.model.person; - -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; - -import seedu.address.model.tag.Tag; - -/** - * Represents a Person in the address book. - * Guarantees: details are present and not null, field values are validated, immutable. - */ -public class Person { - - // Identity fields - private final Name name; - private final Phone phone; - private final Email email; - - // Data fields - private final Address address; - private final Set tags = new HashSet<>(); - - /** - * 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); - this.name = name; - this.phone = phone; - this.email = email; - this.address = address; - this.tags.addAll(tags); - } - - public Name getName() { - return name; - } - - public Phone getPhone() { - return phone; - } - - public Email getEmail() { - return email; - } - - public Address getAddress() { - return address; - } - - /** - * Returns an immutable tag set, which throws {@code UnsupportedOperationException} - * if modification is attempted. - */ - public Set getTags() { - return Collections.unmodifiableSet(tags); - } - - /** - * Returns true if both persons have the same name. - * This defines a weaker notion of equality between two persons. - */ - public boolean isSamePerson(Person otherPerson) { - if (otherPerson == this) { - return true; - } - - return otherPerson != null - && otherPerson.getName().equals(getName()); - } - - /** - * Returns true if both persons have the same identity and data fields. - * This defines a stronger notion of equality between two persons. - */ - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - if (!(other instanceof Person)) { - return false; - } - - Person otherPerson = (Person) other; - return otherPerson.getName().equals(getName()) - && otherPerson.getPhone().equals(getPhone()) - && otherPerson.getEmail().equals(getEmail()) - && otherPerson.getAddress().equals(getAddress()) - && otherPerson.getTags().equals(getTags()); - } - - @Override - public int hashCode() { - // use this method for custom fields hashing instead of implementing your own - return Objects.hash(name, phone, email, address, tags); - } - - @Override - public String toString() { - final StringBuilder builder = new StringBuilder(); - builder.append(getName()) - .append("; Phone: ") - .append(getPhone()) - .append("; Email: ") - .append(getEmail()) - .append("; Address: ") - .append(getAddress()); - - Set tags = getTags(); - if (!tags.isEmpty()) { - builder.append("; Tags: "); - tags.forEach(builder::append); - } - return builder.toString(); - } - -} diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java deleted file mode 100644 index 872c76b382f..00000000000 --- a/src/main/java/seedu/address/model/person/Phone.java +++ /dev/null @@ -1,53 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's phone number in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidPhone(String)} - */ -public class Phone { - - - 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,}"; - public final String value; - - /** - * Constructs a {@code Phone}. - * - * @param phone A valid phone number. - */ - public Phone(String phone) { - requireNonNull(phone); - checkArgument(isValidPhone(phone), MESSAGE_CONSTRAINTS); - value = phone; - } - - /** - * Returns true if a given string is a valid phone number. - */ - public static boolean isValidPhone(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Phone // instanceof handles nulls - && value.equals(((Phone) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java deleted file mode 100644 index 0fee4fe57e6..00000000000 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ /dev/null @@ -1,137 +0,0 @@ -package seedu.address.model.person; - -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.person.exceptions.DuplicatePersonException; -import seedu.address.model.person.exceptions.PersonNotFoundException; - -/** - * A list of persons that enforces uniqueness between its elements and does not allow nulls. - * A person is considered unique by comparing using {@code Person#isSamePerson(Person)}. As such, adding and updating of - * persons uses Person#isSamePerson(Person) for equality so as to ensure that the person being added or updated is - * unique in terms of identity in the UniquePersonList. However, the removal of a person uses Person#equals(Object) so - * as to ensure that the person with exactly the same fields will be removed. - * - * Supports a minimal set of list operations. - * - * @see Person#isSamePerson(Person) - */ -public class UniquePersonList implements Iterable { - - private final ObservableList internalList = FXCollections.observableArrayList(); - private final ObservableList internalUnmodifiableList = - FXCollections.unmodifiableObservableList(internalList); - - /** - * Returns true if the list contains an equivalent person as the given argument. - */ - public boolean contains(Person toCheck) { - requireNonNull(toCheck); - return internalList.stream().anyMatch(toCheck::isSamePerson); - } - - /** - * Adds a person to the list. - * The person must not already exist in the list. - */ - public void add(Person toAdd) { - requireNonNull(toAdd); - if (contains(toAdd)) { - throw new DuplicatePersonException(); - } - internalList.add(toAdd); - } - - /** - * Replaces the person {@code target} in the list with {@code editedPerson}. - * {@code target} must exist in the list. - * The person identity of {@code editedPerson} must not be the same as another existing person in the list. - */ - public void setPerson(Person target, Person editedPerson) { - requireAllNonNull(target, editedPerson); - - int index = internalList.indexOf(target); - if (index == -1) { - throw new PersonNotFoundException(); - } - - if (!target.isSamePerson(editedPerson) && contains(editedPerson)) { - throw new DuplicatePersonException(); - } - - internalList.set(index, editedPerson); - } - - /** - * Removes the equivalent person from the list. - * The person must exist in the list. - */ - public void remove(Person toRemove) { - requireNonNull(toRemove); - if (!internalList.remove(toRemove)) { - throw new PersonNotFoundException(); - } - } - - public void setPersons(UniquePersonList replacement) { - requireNonNull(replacement); - internalList.setAll(replacement.internalList); - } - - /** - * Replaces the contents of this list with {@code persons}. - * {@code persons} must not contain duplicate persons. - */ - public void setPersons(List persons) { - requireAllNonNull(persons); - if (!personsAreUnique(persons)) { - throw new DuplicatePersonException(); - } - - internalList.setAll(persons); - } - - /** - * 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) { - return other == this // short circuit if same object - || (other instanceof UniquePersonList // instanceof handles nulls - && internalList.equals(((UniquePersonList) other).internalList)); - } - - @Override - public int hashCode() { - return internalList.hashCode(); - } - - /** - * Returns true if {@code persons} contains only unique persons. - */ - private boolean personsAreUnique(List persons) { - for (int i = 0; i < persons.size() - 1; i++) { - for (int j = i + 1; j < persons.size(); j++) { - if (persons.get(i).isSamePerson(persons.get(j))) { - return false; - } - } - } - return true; - } -} diff --git a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java b/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java deleted file mode 100644 index d7290f59442..00000000000 --- a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java +++ /dev/null @@ -1,11 +0,0 @@ -package seedu.address.model.person.exceptions; - -/** - * Signals that the operation will result in duplicate Persons (Persons are considered duplicates if they have the same - * identity). - */ -public class DuplicatePersonException extends RuntimeException { - public DuplicatePersonException() { - super("Operation would result in duplicate persons"); - } -} diff --git a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java b/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java deleted file mode 100644 index fa764426ca7..00000000000 --- a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java +++ /dev/null @@ -1,6 +0,0 @@ -package seedu.address.model.person.exceptions; - -/** - * Signals that the operation is unable to find the specified person. - */ -public class PersonNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java deleted file mode 100644 index 1806da4facf..00000000000 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ /dev/null @@ -1,60 +0,0 @@ -package seedu.address.model.util; - -import java.util.Arrays; -import java.util.Set; -import java.util.stream.Collectors; - -import seedu.address.model.AddressBook; -import seedu.address.model.ReadOnlyAddressBook; -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.tag.Tag; - -/** - * Contains utility methods for populating {@code AddressBook} with sample data. - */ -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 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 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 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 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 Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), - new Address("Blk 45 Aljunied Street 85, #11-31"), - getTagSet("colleagues")) - }; - } - - public static ReadOnlyAddressBook getSampleAddressBook() { - AddressBook sampleAb = new AddressBook(); - for (Person samplePerson : getSamplePersons()) { - sampleAb.addPerson(samplePerson); - } - return sampleAb; - } - - /** - * Returns a tag set containing the list of strings given. - */ - public static Set getTagSet(String... strings) { - return Arrays.stream(strings) - .map(Tag::new) - .collect(Collectors.toSet()); - } - -} diff --git a/src/main/java/seedu/address/storage/AddressBookStorage.java b/src/main/java/seedu/address/storage/AddressBookStorage.java deleted file mode 100644 index 4599182b3f9..00000000000 --- a/src/main/java/seedu/address/storage/AddressBookStorage.java +++ /dev/null @@ -1,45 +0,0 @@ -package seedu.address.storage; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; - -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; - -/** - * Represents a storage for {@link seedu.address.model.AddressBook}. - */ -public interface AddressBookStorage { - - /** - * Returns the file path of the data file. - */ - Path getAddressBookFilePath(); - - /** - * Returns AddressBook data as a {@link ReadOnlyAddressBook}. - * Returns {@code Optional.empty()} if storage file is not found. - * @throws DataConversionException if the data in storage is not in the expected format. - * @throws IOException if there was any problem when reading from the storage. - */ - Optional readAddressBook() throws DataConversionException, IOException; - - /** - * @see #getAddressBookFilePath() - */ - Optional readAddressBook(Path filePath) throws DataConversionException, IOException; - - /** - * Saves the given {@link ReadOnlyAddressBook} to the storage. - * @param addressBook cannot be null. - * @throws IOException if there was any problem writing to the file. - */ - void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; - - /** - * @see #saveAddressBook(ReadOnlyAddressBook) - */ - void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException; - -} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java deleted file mode 100644 index a6321cec2ea..00000000000 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ /dev/null @@ -1,109 +0,0 @@ -package seedu.address.storage; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -import seedu.address.commons.exceptions.IllegalValueException; -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.tag.Tag; - -/** - * Jackson-friendly version of {@link Person}. - */ -class JsonAdaptedPerson { - - public static final String MISSING_FIELD_MESSAGE_FORMAT = "Person's %s field is missing!"; - - private final String name; - private final String phone; - private final String email; - private final String address; - private final List tagged = new ArrayList<>(); - - /** - * Constructs a {@code JsonAdaptedPerson} with the given person details. - */ - @JsonCreator - public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, - @JsonProperty("email") String email, @JsonProperty("address") String address, - @JsonProperty("tagged") List tagged) { - this.name = name; - this.phone = phone; - this.email = email; - this.address = address; - if (tagged != null) { - this.tagged.addAll(tagged); - } - } - - /** - * Converts a given {@code Person} into this class for Jackson use. - */ - public JsonAdaptedPerson(Person source) { - name = source.getName().fullName; - phone = source.getPhone().value; - email = source.getEmail().value; - address = source.getAddress().value; - tagged.addAll(source.getTags().stream() - .map(JsonAdaptedTag::new) - .collect(Collectors.toList())); - } - - /** - * Converts this Jackson-friendly adapted person object into the model's {@code Person} object. - * - * @throws IllegalValueException if there were any data constraints violated in the adapted person. - */ - public Person toModelType() throws IllegalValueException { - final List personTags = new ArrayList<>(); - for (JsonAdaptedTag tag : tagged) { - personTags.add(tag.toModelType()); - } - - if (name == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); - } - if (!Name.isValidName(name)) { - throw new IllegalValueException(Name.MESSAGE_CONSTRAINTS); - } - final Name modelName = new Name(name); - - if (phone == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName())); - } - if (!Phone.isValidPhone(phone)) { - throw new IllegalValueException(Phone.MESSAGE_CONSTRAINTS); - } - final Phone modelPhone = new Phone(phone); - - if (email == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName())); - } - if (!Email.isValidEmail(email)) { - throw new IllegalValueException(Email.MESSAGE_CONSTRAINTS); - } - final Email modelEmail = new Email(email); - - if (address == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName())); - } - if (!Address.isValidAddress(address)) { - throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); - } - final Address modelAddress = new Address(address); - - final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); - } - -} diff --git a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java b/src/main/java/seedu/address/storage/JsonAddressBookStorage.java deleted file mode 100644 index dfab9daaa0d..00000000000 --- a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java +++ /dev/null @@ -1,80 +0,0 @@ -package seedu.address.storage; - -import static java.util.Objects.requireNonNull; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; -import java.util.logging.Logger; - -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.commons.util.FileUtil; -import seedu.address.commons.util.JsonUtil; -import seedu.address.model.ReadOnlyAddressBook; - -/** - * A class to access AddressBook data stored as a json file on the hard disk. - */ -public class JsonAddressBookStorage implements AddressBookStorage { - - private static final Logger logger = LogsCenter.getLogger(JsonAddressBookStorage.class); - - private Path filePath; - - public JsonAddressBookStorage(Path filePath) { - this.filePath = filePath; - } - - public Path getAddressBookFilePath() { - return filePath; - } - - @Override - public Optional readAddressBook() throws DataConversionException { - return readAddressBook(filePath); - } - - /** - * Similar to {@link #readAddressBook()}. - * - * @param filePath location of the data. Cannot be null. - * @throws DataConversionException if the file is not in the correct format. - */ - public Optional readAddressBook(Path filePath) throws DataConversionException { - requireNonNull(filePath); - - Optional jsonAddressBook = JsonUtil.readJsonFile( - filePath, JsonSerializableAddressBook.class); - if (!jsonAddressBook.isPresent()) { - return Optional.empty(); - } - - try { - return Optional.of(jsonAddressBook.get().toModelType()); - } catch (IllegalValueException ive) { - logger.info("Illegal values found in " + filePath + ": " + ive.getMessage()); - throw new DataConversionException(ive); - } - } - - @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException { - saveAddressBook(addressBook, filePath); - } - - /** - * Similar to {@link #saveAddressBook(ReadOnlyAddressBook)}. - * - * @param filePath location of the data. Cannot be null. - */ - public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException { - requireNonNull(addressBook); - requireNonNull(filePath); - - FileUtil.createIfMissing(filePath); - JsonUtil.saveJsonFile(new JsonSerializableAddressBook(addressBook), filePath); - } - -} diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java deleted file mode 100644 index 5efd834091d..00000000000 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ /dev/null @@ -1,60 +0,0 @@ -package seedu.address.storage; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonRootName; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.AddressBook; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; - -/** - * An Immutable AddressBook that is serializable to JSON format. - */ -@JsonRootName(value = "addressbook") -class JsonSerializableAddressBook { - - public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; - - private final List persons = new ArrayList<>(); - - /** - * Constructs a {@code JsonSerializableAddressBook} with the given persons. - */ - @JsonCreator - public JsonSerializableAddressBook(@JsonProperty("persons") List persons) { - this.persons.addAll(persons); - } - - /** - * Converts a given {@code ReadOnlyAddressBook} into this class for Jackson use. - * - * @param source future changes to this will not affect the created {@code JsonSerializableAddressBook}. - */ - public JsonSerializableAddressBook(ReadOnlyAddressBook source) { - persons.addAll(source.getPersonList().stream().map(JsonAdaptedPerson::new).collect(Collectors.toList())); - } - - /** - * Converts this address book into the model's {@code AddressBook} object. - * - * @throws IllegalValueException if there were any data constraints violated. - */ - public AddressBook toModelType() throws IllegalValueException { - AddressBook addressBook = new AddressBook(); - for (JsonAdaptedPerson jsonAdaptedPerson : persons) { - Person person = jsonAdaptedPerson.toModelType(); - if (addressBook.hasPerson(person)) { - throw new IllegalValueException(MESSAGE_DUPLICATE_PERSON); - } - addressBook.addPerson(person); - } - return addressBook; - } - -} diff --git a/src/main/java/seedu/address/storage/Storage.java b/src/main/java/seedu/address/storage/Storage.java deleted file mode 100644 index beda8bd9f11..00000000000 --- a/src/main/java/seedu/address/storage/Storage.java +++ /dev/null @@ -1,32 +0,0 @@ -package seedu.address.storage; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; - -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; - -/** - * API of the Storage component - */ -public interface Storage extends AddressBookStorage, UserPrefsStorage { - - @Override - Optional readUserPrefs() throws DataConversionException, IOException; - - @Override - void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException; - - @Override - Path getAddressBookFilePath(); - - @Override - Optional readAddressBook() throws DataConversionException, IOException; - - @Override - void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; - -} diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/address/storage/StorageManager.java deleted file mode 100644 index 79868290974..00000000000 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ /dev/null @@ -1,79 +0,0 @@ -package seedu.address.storage; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; -import java.util.logging.Logger; - -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; - -/** - * Manages storage of AddressBook data in local storage. - */ -public class StorageManager implements Storage { - - private static final Logger logger = LogsCenter.getLogger(StorageManager.class); - private AddressBookStorage addressBookStorage; - private UserPrefsStorage userPrefsStorage; - - /** - * Creates a {@code StorageManager} with the given {@code AddressBookStorage} and {@code UserPrefStorage}. - */ - public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage) { - super(); - this.addressBookStorage = addressBookStorage; - this.userPrefsStorage = userPrefsStorage; - } - - // ================ UserPrefs methods ============================== - - @Override - public Path getUserPrefsFilePath() { - return userPrefsStorage.getUserPrefsFilePath(); - } - - @Override - public Optional readUserPrefs() throws DataConversionException, IOException { - return userPrefsStorage.readUserPrefs(); - } - - @Override - public void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException { - userPrefsStorage.saveUserPrefs(userPrefs); - } - - - // ================ AddressBook methods ============================== - - @Override - public Path getAddressBookFilePath() { - return addressBookStorage.getAddressBookFilePath(); - } - - @Override - public Optional readAddressBook() throws DataConversionException, IOException { - return readAddressBook(addressBookStorage.getAddressBookFilePath()); - } - - @Override - public Optional readAddressBook(Path filePath) throws DataConversionException, IOException { - logger.fine("Attempting to read data from file: " + filePath); - return addressBookStorage.readAddressBook(filePath); - } - - @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException { - saveAddressBook(addressBook, addressBookStorage.getAddressBookFilePath()); - } - - @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException { - logger.fine("Attempting to write to data file: " + filePath); - addressBookStorage.saveAddressBook(addressBook, filePath); - } - -} diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java deleted file mode 100644 index 7fc927bc5d9..00000000000 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ /dev/null @@ -1,77 +0,0 @@ -package seedu.address.ui; - -import java.util.Comparator; - -import javafx.fxml.FXML; -import javafx.scene.control.Label; -import javafx.scene.layout.FlowPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Region; -import seedu.address.model.person.Person; - -/** - * An UI component that displays information of a {@code Person}. - */ -public class PersonCard extends UiPart { - - private static final String FXML = "PersonListCard.fxml"; - - /** - * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. - * As a consequence, UI elements' variable names cannot be set to such keywords - * or an exception will be thrown by JavaFX during runtime. - * - * @see The issue on AddressBook level 4 - */ - - public final Person person; - - @FXML - private HBox cardPane; - @FXML - private Label name; - @FXML - private Label id; - @FXML - private Label phone; - @FXML - private Label address; - @FXML - private Label email; - @FXML - private FlowPane tags; - - /** - * Creates a {@code PersonCode} with the given {@code Person} and index to display. - */ - public PersonCard(Person person, int displayedIndex) { - super(FXML); - this.person = person; - id.setText(displayedIndex + ". "); - name.setText(person.getName().fullName); - phone.setText(person.getPhone().value); - address.setText(person.getAddress().value); - email.setText(person.getEmail().value); - person.getTags().stream() - .sorted(Comparator.comparing(tag -> tag.tagName)) - .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); - } - - @Override - public boolean equals(Object other) { - // short circuit if same object - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof PersonCard)) { - return false; - } - - // state check - PersonCard card = (PersonCard) other; - return id.getText().equals(card.id.getText()) - && person.equals(card.person); - } -} diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/seedu/address/ui/PersonListPanel.java deleted file mode 100644 index f4c501a897b..00000000000 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ /dev/null @@ -1,49 +0,0 @@ -package seedu.address.ui; - -import java.util.logging.Logger; - -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.person.Person; - -/** - * Panel containing the list of persons. - */ -public class PersonListPanel extends UiPart { - private static final String FXML = "PersonListPanel.fxml"; - private final Logger logger = LogsCenter.getLogger(PersonListPanel.class); - - @FXML - private ListView personListView; - - /** - * Creates a {@code PersonListPanel} with the given {@code ObservableList}. - */ - public PersonListPanel(ObservableList personList) { - super(FXML); - personListView.setItems(personList); - personListView.setCellFactory(listView -> new PersonListViewCell()); - } - - /** - * Custom {@code ListCell} that displays the graphics of a {@code Person} using a {@code PersonCard}. - */ - class PersonListViewCell extends ListCell { - @Override - protected void updateItem(Person person, boolean empty) { - super.updateItem(person, empty); - - if (empty || person == null) { - setGraphic(null); - setText(null); - } else { - setGraphic(new PersonCard(person, getIndex() + 1).getRoot()); - } - } - } - -} diff --git a/src/main/java/seedu/address/AppParameters.java b/src/main/java/seedu/sourcecontrol/AppParameters.java similarity index 90% rename from src/main/java/seedu/address/AppParameters.java rename to src/main/java/seedu/sourcecontrol/AppParameters.java index ab552c398f3..5371ce239eb 100644 --- a/src/main/java/seedu/address/AppParameters.java +++ b/src/main/java/seedu/sourcecontrol/AppParameters.java @@ -1,14 +1,13 @@ -package seedu.address; +package seedu.sourcecontrol; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Map; import java.util.Objects; import java.util.logging.Logger; import javafx.application.Application; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.util.FileUtil; +import seedu.sourcecontrol.commons.core.LogsCenter; +import seedu.sourcecontrol.commons.util.FileUtil; /** * Represents the parsed command-line parameters given to the application. @@ -38,7 +37,7 @@ public static AppParameters parse(Application.Parameters parameters) { logger.warning("Invalid config path " + configPathParameter + ". Using default config path."); configPathParameter = null; } - appParameters.setConfigPath(configPathParameter != null ? Paths.get(configPathParameter) : null); + appParameters.setConfigPath(configPathParameter != null ? FileUtil.pathOf(configPathParameter) : null); return appParameters; } diff --git a/src/main/java/seedu/address/Main.java b/src/main/java/seedu/sourcecontrol/Main.java similarity index 96% rename from src/main/java/seedu/address/Main.java rename to src/main/java/seedu/sourcecontrol/Main.java index 052a5068631..96469dbdb29 100644 --- a/src/main/java/seedu/address/Main.java +++ b/src/main/java/seedu/sourcecontrol/Main.java @@ -1,4 +1,4 @@ -package seedu.address; +package seedu.sourcecontrol; import javafx.application.Application; diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/sourcecontrol/MainApp.java similarity index 66% rename from src/main/java/seedu/address/MainApp.java rename to src/main/java/seedu/sourcecontrol/MainApp.java index 4133aaa0151..91c90e3b911 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/sourcecontrol/MainApp.java @@ -1,4 +1,4 @@ -package seedu.address; +package seedu.sourcecontrol; import java.io.IOException; import java.nio.file.Path; @@ -7,29 +7,29 @@ import javafx.application.Application; import javafx.stage.Stage; -import seedu.address.commons.core.Config; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.core.Version; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.util.ConfigUtil; -import seedu.address.commons.util.StringUtil; -import seedu.address.logic.Logic; -import seedu.address.logic.LogicManager; -import seedu.address.model.AddressBook; -import seedu.address.model.Model; -import seedu.address.model.ModelManager; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; -import seedu.address.model.util.SampleDataUtil; -import seedu.address.storage.AddressBookStorage; -import seedu.address.storage.JsonAddressBookStorage; -import seedu.address.storage.JsonUserPrefsStorage; -import seedu.address.storage.Storage; -import seedu.address.storage.StorageManager; -import seedu.address.storage.UserPrefsStorage; -import seedu.address.ui.Ui; -import seedu.address.ui.UiManager; +import seedu.sourcecontrol.commons.core.Config; +import seedu.sourcecontrol.commons.core.LogsCenter; +import seedu.sourcecontrol.commons.core.Version; +import seedu.sourcecontrol.commons.exceptions.DataConversionException; +import seedu.sourcecontrol.commons.util.ConfigUtil; +import seedu.sourcecontrol.commons.util.StringUtil; +import seedu.sourcecontrol.logic.Logic; +import seedu.sourcecontrol.logic.LogicManager; +import seedu.sourcecontrol.model.Model; +import seedu.sourcecontrol.model.ModelManager; +import seedu.sourcecontrol.model.ReadOnlySourceControl; +import seedu.sourcecontrol.model.ReadOnlyUserPrefs; +import seedu.sourcecontrol.model.SourceControl; +import seedu.sourcecontrol.model.UserPrefs; +import seedu.sourcecontrol.model.util.SampleDataUtil; +import seedu.sourcecontrol.storage.JsonSourceControlStorage; +import seedu.sourcecontrol.storage.JsonUserPrefsStorage; +import seedu.sourcecontrol.storage.SourceControlStorage; +import seedu.sourcecontrol.storage.Storage; +import seedu.sourcecontrol.storage.StorageManager; +import seedu.sourcecontrol.storage.UserPrefsStorage; +import seedu.sourcecontrol.ui.Ui; +import seedu.sourcecontrol.ui.UiManager; /** * Runs the application. @@ -48,7 +48,7 @@ public class MainApp extends Application { @Override public void init() throws Exception { - logger.info("=============================[ Initializing AddressBook ]==========================="); + logger.info("=============================[ Initializing SourceControl ]==========================="); super.init(); AppParameters appParameters = AppParameters.parse(getParameters()); @@ -56,8 +56,8 @@ public void init() throws Exception { UserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(config.getUserPrefsFilePath()); UserPrefs userPrefs = initPrefs(userPrefsStorage); - AddressBookStorage addressBookStorage = new JsonAddressBookStorage(userPrefs.getAddressBookFilePath()); - storage = new StorageManager(addressBookStorage, userPrefsStorage); + SourceControlStorage sourceControlStorage = new JsonSourceControlStorage(userPrefs.getSourceControlFilePath()); + storage = new StorageManager(sourceControlStorage, userPrefsStorage); initLogging(config); @@ -69,25 +69,25 @@ public void init() throws Exception { } /** - * Returns a {@code ModelManager} with the data from {@code storage}'s address book and {@code userPrefs}.
- * The data from the sample address book will be used instead if {@code storage}'s address book is not found, - * or an empty address book will be used instead if errors occur when reading {@code storage}'s address book. + * Returns a {@code ModelManager} with the data from {@code storage}'s Source Control and {@code userPrefs}.
+ * The data from the sample Source Control will be used instead if {@code storage}'s Source Control is not found, + * or an empty Source Control will be used instead if errors occur when reading {@code storage}'s Source Control. */ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { - Optional addressBookOptional; - ReadOnlyAddressBook initialData; + Optional sourceControlOptional; + ReadOnlySourceControl initialData; try { - addressBookOptional = storage.readAddressBook(); - if (!addressBookOptional.isPresent()) { - logger.info("Data file not found. Will be starting with a sample AddressBook"); + sourceControlOptional = storage.readSourceControl(); + if (!sourceControlOptional.isPresent()) { + logger.info("Data file not found. Will be starting with a sample SourceControl"); } - initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); + initialData = sourceControlOptional.orElseGet(SampleDataUtil::getSampleSourceControl); } catch (DataConversionException e) { - logger.warning("Data file not in the correct format. Will be starting with an empty AddressBook"); - initialData = new AddressBook(); + logger.warning("Data file not in the correct format. Will be starting with an empty SourceControl"); + initialData = new SourceControl(); } catch (IOException e) { - logger.warning("Problem while reading from the file. Will be starting with an empty AddressBook"); - initialData = new AddressBook(); + logger.warning("Problem while reading from the file. Will be starting with an empty SourceControl"); + initialData = new SourceControl(); } return new ModelManager(initialData, userPrefs); @@ -151,7 +151,7 @@ protected UserPrefs initPrefs(UserPrefsStorage storage) { + "Using default user prefs"); initializedPrefs = new UserPrefs(); } catch (IOException e) { - logger.warning("Problem while reading from the file. Will be starting with an empty AddressBook"); + logger.warning("Problem while reading from the file. Will be starting with an empty SourceControl"); initializedPrefs = new UserPrefs(); } @@ -167,13 +167,13 @@ protected UserPrefs initPrefs(UserPrefsStorage storage) { @Override public void start(Stage primaryStage) { - logger.info("Starting AddressBook " + MainApp.VERSION); + logger.info("Starting SourceControl " + MainApp.VERSION); ui.start(primaryStage); } @Override public void stop() { - logger.info("============================ [ Stopping Address Book ] ============================="); + logger.info("============================ [ Stopping Source Control ] ============================="); try { storage.saveUserPrefs(model.getUserPrefs()); } catch (IOException e) { diff --git a/src/main/java/seedu/address/commons/core/Config.java b/src/main/java/seedu/sourcecontrol/commons/core/Config.java similarity index 84% rename from src/main/java/seedu/address/commons/core/Config.java rename to src/main/java/seedu/sourcecontrol/commons/core/Config.java index 91145745521..319d52e4cf6 100644 --- a/src/main/java/seedu/address/commons/core/Config.java +++ b/src/main/java/seedu/sourcecontrol/commons/core/Config.java @@ -1,20 +1,21 @@ -package seedu.address.commons.core; +package seedu.sourcecontrol.commons.core; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Objects; import java.util.logging.Level; +import seedu.sourcecontrol.commons.util.FileUtil; + /** * Config values used by the app */ public class Config { - public static final Path DEFAULT_CONFIG_FILE = Paths.get("config.json"); + public static final Path DEFAULT_CONFIG_FILE = FileUtil.pathOf("config.json"); // Config values customizable through config file private Level logLevel = Level.INFO; - private Path userPrefsFilePath = Paths.get("preferences.json"); + private Path userPrefsFilePath = FileUtil.pathOf("preferences.json"); public Level getLogLevel() { return logLevel; diff --git a/src/main/java/seedu/address/commons/core/GuiSettings.java b/src/main/java/seedu/sourcecontrol/commons/core/GuiSettings.java similarity index 78% rename from src/main/java/seedu/address/commons/core/GuiSettings.java rename to src/main/java/seedu/sourcecontrol/commons/core/GuiSettings.java index ba33653be67..58b0a2288b4 100644 --- a/src/main/java/seedu/address/commons/core/GuiSettings.java +++ b/src/main/java/seedu/sourcecontrol/commons/core/GuiSettings.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package seedu.sourcecontrol.commons.core; import java.awt.Point; import java.io.Serializable; @@ -12,10 +12,12 @@ public class GuiSettings implements Serializable { private static final double DEFAULT_HEIGHT = 600; private static final double DEFAULT_WIDTH = 740; + private static final double DEFAULT_DIVIDER_POSITION = 0.5; private final double windowWidth; private final double windowHeight; private final Point windowCoordinates; + private final double dividerPosition; /** * Constructs a {@code GuiSettings} with the default height, width and position. @@ -24,15 +26,21 @@ public GuiSettings() { windowWidth = DEFAULT_WIDTH; windowHeight = DEFAULT_HEIGHT; windowCoordinates = null; // null represent no coordinates + dividerPosition = DEFAULT_DIVIDER_POSITION; } /** * Constructs a {@code GuiSettings} with the specified height, width and position. */ - public GuiSettings(double windowWidth, double windowHeight, int xPosition, int yPosition) { + public GuiSettings(double windowWidth, double windowHeight, int xPosition, int yPosition, double dividerPosition) { this.windowWidth = windowWidth; this.windowHeight = windowHeight; windowCoordinates = new Point(xPosition, yPosition); + this.dividerPosition = dividerPosition; + } + + public double getDividerPosition() { + return dividerPosition; } public double getWindowWidth() { @@ -60,12 +68,13 @@ public boolean equals(Object other) { return windowWidth == o.windowWidth && windowHeight == o.windowHeight - && Objects.equals(windowCoordinates, o.windowCoordinates); + && Objects.equals(windowCoordinates, o.windowCoordinates) + && dividerPosition == o.dividerPosition; } @Override public int hashCode() { - return Objects.hash(windowWidth, windowHeight, windowCoordinates); + return Objects.hash(windowWidth, windowHeight, windowCoordinates, dividerPosition); } @Override @@ -73,7 +82,8 @@ public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Width : " + windowWidth + "\n"); sb.append("Height : " + windowHeight + "\n"); - sb.append("Position : " + windowCoordinates); + sb.append("Position : " + windowCoordinates + "\n"); + sb.append("Divider : " + dividerPosition); return sb.toString(); } } diff --git a/src/main/java/seedu/address/commons/core/LogsCenter.java b/src/main/java/seedu/sourcecontrol/commons/core/LogsCenter.java similarity index 97% rename from src/main/java/seedu/address/commons/core/LogsCenter.java rename to src/main/java/seedu/sourcecontrol/commons/core/LogsCenter.java index 431e7185e76..4e6140404fd 100644 --- a/src/main/java/seedu/address/commons/core/LogsCenter.java +++ b/src/main/java/seedu/sourcecontrol/commons/core/LogsCenter.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package seedu.sourcecontrol.commons.core; import java.io.IOException; import java.util.Arrays; @@ -18,7 +18,7 @@ public class LogsCenter { private static final int MAX_FILE_COUNT = 5; private static final int MAX_FILE_SIZE_IN_BYTES = (int) (Math.pow(2, 20) * 5); // 5MB - private static final String LOG_FILE = "addressbook.log"; + private static final String LOG_FILE = "sourceControl.log"; private static Level currentLogLevel = Level.INFO; private static final Logger logger = LogsCenter.getLogger(LogsCenter.class); private static FileHandler fileHandler; diff --git a/src/main/java/seedu/sourcecontrol/commons/core/Messages.java b/src/main/java/seedu/sourcecontrol/commons/core/Messages.java new file mode 100644 index 00000000000..b57ad80abd6 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/commons/core/Messages.java @@ -0,0 +1,14 @@ +package seedu.sourcecontrol.commons.core; + +/** + * Container for user visible messages. + */ +public class Messages { + + public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command. "; + public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; + public static final String MESSAGE_INVALID_STUDENT_DISPLAYED_INDEX = + "The student index is invalid. The number of students in the list is %1$d. "; + public static final String MESSAGE_STUDENTS_LISTED_OVERVIEW = "%1$s listed!"; + +} diff --git a/src/main/java/seedu/address/commons/core/Version.java b/src/main/java/seedu/sourcecontrol/commons/core/Version.java similarity index 98% rename from src/main/java/seedu/address/commons/core/Version.java rename to src/main/java/seedu/sourcecontrol/commons/core/Version.java index 12142ec1e32..1d95b79ed5d 100644 --- a/src/main/java/seedu/address/commons/core/Version.java +++ b/src/main/java/seedu/sourcecontrol/commons/core/Version.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package seedu.sourcecontrol.commons.core; import java.util.regex.Matcher; import java.util.regex.Pattern; diff --git a/src/main/java/seedu/address/commons/core/index/Index.java b/src/main/java/seedu/sourcecontrol/commons/core/index/Index.java similarity index 97% rename from src/main/java/seedu/address/commons/core/index/Index.java rename to src/main/java/seedu/sourcecontrol/commons/core/index/Index.java index 19536439c09..f46b30fa2b1 100644 --- a/src/main/java/seedu/address/commons/core/index/Index.java +++ b/src/main/java/seedu/sourcecontrol/commons/core/index/Index.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core.index; +package seedu.sourcecontrol.commons.core.index; /** * Represents a zero-based or one-based index. diff --git a/src/main/java/seedu/address/commons/exceptions/DataConversionException.java b/src/main/java/seedu/sourcecontrol/commons/exceptions/DataConversionException.java similarity index 82% rename from src/main/java/seedu/address/commons/exceptions/DataConversionException.java rename to src/main/java/seedu/sourcecontrol/commons/exceptions/DataConversionException.java index 1f689bd8e3f..cd5cba0d784 100644 --- a/src/main/java/seedu/address/commons/exceptions/DataConversionException.java +++ b/src/main/java/seedu/sourcecontrol/commons/exceptions/DataConversionException.java @@ -1,4 +1,4 @@ -package seedu.address.commons.exceptions; +package seedu.sourcecontrol.commons.exceptions; /** * Represents an error during conversion of data from one format to another diff --git a/src/main/java/seedu/address/commons/exceptions/IllegalValueException.java b/src/main/java/seedu/sourcecontrol/commons/exceptions/IllegalValueException.java similarity index 92% rename from src/main/java/seedu/address/commons/exceptions/IllegalValueException.java rename to src/main/java/seedu/sourcecontrol/commons/exceptions/IllegalValueException.java index 19124db485c..2af35d0dfe8 100644 --- a/src/main/java/seedu/address/commons/exceptions/IllegalValueException.java +++ b/src/main/java/seedu/sourcecontrol/commons/exceptions/IllegalValueException.java @@ -1,4 +1,4 @@ -package seedu.address.commons.exceptions; +package seedu.sourcecontrol.commons.exceptions; /** * Signals that some given data does not fulfill some constraints. diff --git a/src/main/java/seedu/address/commons/util/AppUtil.java b/src/main/java/seedu/sourcecontrol/commons/util/AppUtil.java similarity index 93% rename from src/main/java/seedu/address/commons/util/AppUtil.java rename to src/main/java/seedu/sourcecontrol/commons/util/AppUtil.java index 87aa89c0326..1338eb3442f 100644 --- a/src/main/java/seedu/address/commons/util/AppUtil.java +++ b/src/main/java/seedu/sourcecontrol/commons/util/AppUtil.java @@ -1,9 +1,9 @@ -package seedu.address.commons.util; +package seedu.sourcecontrol.commons.util; import static java.util.Objects.requireNonNull; import javafx.scene.image.Image; -import seedu.address.MainApp; +import seedu.sourcecontrol.MainApp; /** * A container for App specific utility functions diff --git a/src/main/java/seedu/sourcecontrol/commons/util/ChartUtil.java b/src/main/java/seedu/sourcecontrol/commons/util/ChartUtil.java new file mode 100644 index 00000000000..7c017133466 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/commons/util/ChartUtil.java @@ -0,0 +1,191 @@ +package seedu.sourcecontrol.commons.util; + +import java.util.ArrayList; +import java.util.Map; + +import javafx.scene.chart.BarChart; +import javafx.scene.chart.CategoryAxis; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.XYChart; + +/** + * Creates JavaFX charts + */ +public class ChartUtil { + private static final double DEFAULT_TICK_UNIT = 5.0; + + /** + * Creates a JavaFX BarChart with the given title, axis labels and data points. + */ + public static BarChart createBarChart(String title, String xLabel, String yLabel, + Map data) { + final CategoryAxis xAxis = new CategoryAxis(); + xAxis.setLabel(xLabel); + + final NumberAxis yAxis = new NumberAxis(); + yAxis.setLabel(yLabel); + // Disable auto-ranging so that we can configure our own tick units + yAxis.setAutoRanging(false); + yAxis.setTickUnit(DEFAULT_TICK_UNIT); + + final BarChart barChart = new BarChart(xAxis, yAxis); + barChart.setTitle(title); + barChart.setLegendVisible(false); + + double maxY = DEFAULT_TICK_UNIT; + + XYChart.Series series = new XYChart.Series<>(); + for (Map.Entry entry : data.entrySet()) { + series.getData().add(new XYChart.Data<>(entry.getKey(), entry.getValue())); + maxY = Math.max(maxY, entry.getValue().doubleValue()); + } + + double yUpperBound = roundUpToNearestMultiple(maxY, (int) DEFAULT_TICK_UNIT); + + // Add another tick unit as padding, else if there is a data point having a value which is a multiple of + // the tick unit, the graph will look as if it is cut-off. + yUpperBound += DEFAULT_TICK_UNIT; + yAxis.setUpperBound(yUpperBound); + + barChart.getData().add(series); + return barChart; + } + + /** + * Creates a JavaFX LineChart with the given title, axis labels and data points. + */ + public static LineChart createLineChart(String title, String xLabel, String yLabel, + String dataLabel, Map data, + Map mean, + Map median) { + + // Defining axes + final CategoryAxis xAxis = new CategoryAxis(); + xAxis.setLabel(xLabel); + final NumberAxis yAxis = new NumberAxis(); + yAxis.setLabel(yLabel); + + yAxis.setAutoRanging(false); + yAxis.setLowerBound(0); + yAxis.setUpperBound(100); + yAxis.setTickUnit(10); + + final LineChart lineChart = new LineChart(xAxis, yAxis); + lineChart.setTitle(title); + lineChart.setId("chart1"); + + // Input score data points + XYChart.Series seriesScore = new XYChart.Series<>(); + seriesScore.setName(dataLabel); + for (Map.Entry entry : data.entrySet()) { + seriesScore.getData().add(new XYChart.Data<>(wrap(entry.getKey()), entry.getValue())); + } + lineChart.getData().add(seriesScore); + + // Input mean data points + XYChart.Series seriesMean = new XYChart.Series<>(); + seriesMean.setName("cohort mean"); + for (Map.Entry entry : mean.entrySet()) { + seriesMean.getData().add(new XYChart.Data<>(wrap(entry.getKey()), entry.getValue())); + } + lineChart.getData().add(seriesMean); + + // Input median data points + XYChart.Series seriesMedian = new XYChart.Series<>(); + seriesMedian.setName("cohort median"); + for (Map.Entry entry : median.entrySet()) { + seriesMedian.getData().add(new XYChart.Data<>(wrap(entry.getKey()), entry.getValue())); + } + lineChart.getData().add(seriesMedian); + + return lineChart; + } + + /** + * Rounds up {@code double val} to multiples of {@code int multiple}. + */ + public static double roundUpToNearestMultiple(double val, int multiple) { + return Math.round(val / multiple) * multiple; + } + + /** + * Wraps the given string such that each line contains maximum of 12 characters, with a maximum of 3 lines. + */ + public static String wrap(String string) { + String[] words = string.split(" "); + int count = 0; + ArrayList result = new ArrayList<>(); + int line = 0; + + while (count < words.length) { + // check if exceed line limit + if (line == 3) { + break; + } + + int currLineLength = 0; + if (result.size() - 1 == line) { + currLineLength = result.get(line).length(); + } + int wordLength = words[count].length(); + + // if nothing in current line, and next word can fit + if (currLineLength == 0 && wordLength <= 12) { + result.add(words[count]); + count++; + continue; + } + + // word can fit into previous line + if (wordLength + currLineLength + 1 <= 12) { + result.set(line, result.get(line) + " " + words[count]); + count++; + continue; + } + + // word cannot fit into previous line, but word is below size limit + if (wordLength <= 12 && line < 2) { + line++; + result.add(words[count]); + count++; + continue; + } + + // if nothing in curr line, but next word cannot fit + if (currLineLength == 0) { + result.add(words[count].substring(0, 12)); + words[count] = words[count].substring(12); + line++; + continue; + } + + // if something in current line, fill up current line first + int temp = 12 - 1 - currLineLength; + if (temp < 0) { + line++; + continue; + } + result.set(line, result.get(line) + " " + words[count].substring(0, temp)); + words[count] = words[count].substring(temp); + line++; + } + + int numOfLines = result.size() - 1; + + if (count < words.length) { + result.set(2, result.get(2).substring(0, 10) + "..."); + } + + String wrappedString = result.get(0); + if (numOfLines >= 1) { + wrappedString += "\n" + result.get(1); + } + if (numOfLines >= 2) { + wrappedString += "\n" + result.get(2); + } + + return wrappedString; + + } +} diff --git a/src/main/java/seedu/address/commons/util/CollectionUtil.java b/src/main/java/seedu/sourcecontrol/commons/util/CollectionUtil.java similarity index 68% rename from src/main/java/seedu/address/commons/util/CollectionUtil.java rename to src/main/java/seedu/sourcecontrol/commons/util/CollectionUtil.java index eafe4dfd681..4cd2d930982 100644 --- a/src/main/java/seedu/address/commons/util/CollectionUtil.java +++ b/src/main/java/seedu/sourcecontrol/commons/util/CollectionUtil.java @@ -1,9 +1,10 @@ -package seedu.address.commons.util; +package seedu.sourcecontrol.commons.util; import static java.util.Objects.requireNonNull; import java.util.Arrays; import java.util.Collection; +import java.util.List; import java.util.Objects; import java.util.stream.Stream; @@ -32,4 +33,15 @@ public static void requireAllNonNull(Collection items) { public static boolean isAnyNonNull(Object... items) { return items != null && Arrays.stream(items).anyMatch(Objects::nonNull); } + + /** + * Returns true if two lists are equal, ignoring the order of elements within the list. + */ + public static boolean equalsIgnoreOrder(List l1, List l2) { + if (l1 == null || l2 == null) { + return l1 == l2; + } + return l1.size() == l2.size() + && l1.stream().allMatch(l2::contains); + } } diff --git a/src/main/java/seedu/address/commons/util/ConfigUtil.java b/src/main/java/seedu/sourcecontrol/commons/util/ConfigUtil.java similarity index 75% rename from src/main/java/seedu/address/commons/util/ConfigUtil.java rename to src/main/java/seedu/sourcecontrol/commons/util/ConfigUtil.java index f7f8a2bd44c..b6d1df332ce 100644 --- a/src/main/java/seedu/address/commons/util/ConfigUtil.java +++ b/src/main/java/seedu/sourcecontrol/commons/util/ConfigUtil.java @@ -1,11 +1,11 @@ -package seedu.address.commons.util; +package seedu.sourcecontrol.commons.util; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.core.Config; -import seedu.address.commons.exceptions.DataConversionException; +import seedu.sourcecontrol.commons.core.Config; +import seedu.sourcecontrol.commons.exceptions.DataConversionException; /** * A class for accessing the Config File. diff --git a/src/main/java/seedu/address/commons/util/FileUtil.java b/src/main/java/seedu/sourcecontrol/commons/util/FileUtil.java similarity index 59% rename from src/main/java/seedu/address/commons/util/FileUtil.java rename to src/main/java/seedu/sourcecontrol/commons/util/FileUtil.java index b1e2767cdd9..cec14b309de 100644 --- a/src/main/java/seedu/address/commons/util/FileUtil.java +++ b/src/main/java/seedu/sourcecontrol/commons/util/FileUtil.java @@ -1,6 +1,8 @@ -package seedu.address.commons.util; +package seedu.sourcecontrol.commons.util; +import java.io.File; import java.io.IOException; +import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; @@ -12,6 +14,7 @@ public class FileUtil { private static final String CHARSET = "UTF-8"; + private static Path enclosingFolder; public static boolean isFileExists(Path file) { return Files.exists(file) && Files.isRegularFile(file); @@ -80,4 +83,38 @@ public static void writeToFile(Path file, String content) throws IOException { Files.write(file, content.getBytes(CHARSET)); } + /** + * Gets the path of the folder which the jar file is located in (or whatever is used to launch the app) + */ + private static Path getAppEnclosingFolder() { + try { + // @@author jonas-chow-reused + // taken from https://stackoverflow.com/questions/320542/how-to-get-the-path-of-a-running-jar-file + if (enclosingFolder == null) { + enclosingFolder = Path.of(new File(FileUtil.class.getProtectionDomain() + .getCodeSource().getLocation().toURI()).getPath()).getParent(); + } + // @@author jonas-chow + return enclosingFolder; + } catch (URISyntaxException e) { + return null; + } + } + + /** + * Gets the absolute path, relative to where the jar file is located. + */ + public static Path pathOf(String first, String... more) { + Path path = Path.of(first, more); + return getAppEnclosingFolder() == null ? path : enclosingFolder.resolve(path); + } + + /** + * Gets the path, relative to where the jar file is located. + */ + public static String getRelativePathString(Path path) { + return getAppEnclosingFolder() == null + ? path.toString() + : Path.of(".").resolve(getAppEnclosingFolder().relativize(path)).toString(); + } } diff --git a/src/main/java/seedu/address/commons/util/JsonUtil.java b/src/main/java/seedu/sourcecontrol/commons/util/JsonUtil.java similarity index 97% rename from src/main/java/seedu/address/commons/util/JsonUtil.java rename to src/main/java/seedu/sourcecontrol/commons/util/JsonUtil.java index 8ef609f055d..4153791023f 100644 --- a/src/main/java/seedu/address/commons/util/JsonUtil.java +++ b/src/main/java/seedu/sourcecontrol/commons/util/JsonUtil.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package seedu.sourcecontrol.commons.util; import static java.util.Objects.requireNonNull; @@ -20,8 +20,8 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataConversionException; +import seedu.sourcecontrol.commons.core.LogsCenter; +import seedu.sourcecontrol.commons.exceptions.DataConversionException; /** * Converts a Java object instance to JSON and vice versa diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/sourcecontrol/commons/util/StringUtil.java similarity index 95% rename from src/main/java/seedu/address/commons/util/StringUtil.java rename to src/main/java/seedu/sourcecontrol/commons/util/StringUtil.java index 61cc8c9a1cb..98da6397d36 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/seedu/sourcecontrol/commons/util/StringUtil.java @@ -1,7 +1,7 @@ -package seedu.address.commons.util; +package seedu.sourcecontrol.commons.util; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; +import static seedu.sourcecontrol.commons.util.AppUtil.checkArgument; import java.io.PrintWriter; import java.io.StringWriter; diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/sourcecontrol/logic/Logic.java similarity index 51% rename from src/main/java/seedu/address/logic/Logic.java rename to src/main/java/seedu/sourcecontrol/logic/Logic.java index 92cd8fa605a..170bf085210 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/sourcecontrol/logic/Logic.java @@ -1,14 +1,14 @@ -package seedu.address.logic; +package seedu.sourcecontrol.logic; import java.nio.file.Path; 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.person.Person; +import seedu.sourcecontrol.commons.core.GuiSettings; +import seedu.sourcecontrol.logic.commands.CommandResult; +import seedu.sourcecontrol.logic.commands.exceptions.CommandException; +import seedu.sourcecontrol.logic.parser.exceptions.ParseException; +import seedu.sourcecontrol.model.ReadOnlySourceControl; +import seedu.sourcecontrol.model.student.Student; /** * API of the Logic component @@ -24,19 +24,19 @@ public interface Logic { CommandResult execute(String commandText) throws CommandException, ParseException; /** - * Returns the AddressBook. + * Returns the SourceControl. * - * @see seedu.address.model.Model#getAddressBook() + * @see seedu.sourcecontrol.model.Model#getSourceControl() */ - ReadOnlyAddressBook getAddressBook(); + ReadOnlySourceControl getSourceControl(); - /** Returns an unmodifiable view of the filtered list of persons */ - ObservableList getFilteredPersonList(); + /** Returns an unmodifiable view of the filtered list of students */ + ObservableList getFilteredStudentList(); /** - * Returns the user prefs' address book file path. + * Returns the user prefs' source control file path. */ - Path getAddressBookFilePath(); + Path getSourceControlFilePath(); /** * Returns the user prefs' GUI settings. diff --git a/src/main/java/seedu/sourcecontrol/logic/LogicManager.java b/src/main/java/seedu/sourcecontrol/logic/LogicManager.java new file mode 100644 index 00000000000..1f328a8d2a7 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/LogicManager.java @@ -0,0 +1,109 @@ +package seedu.sourcecontrol.logic; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Map; +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import seedu.sourcecontrol.commons.core.GuiSettings; +import seedu.sourcecontrol.commons.core.LogsCenter; +import seedu.sourcecontrol.logic.commands.Command; +import seedu.sourcecontrol.logic.commands.CommandResult; +import seedu.sourcecontrol.logic.commands.exceptions.CommandException; +import seedu.sourcecontrol.logic.parser.Alias; +import seedu.sourcecontrol.logic.parser.AliasCommandParser; +import seedu.sourcecontrol.logic.parser.SourceControlParser; +import seedu.sourcecontrol.logic.parser.exceptions.ParseException; +import seedu.sourcecontrol.model.Model; +import seedu.sourcecontrol.model.ReadOnlySourceControl; +import seedu.sourcecontrol.model.student.Student; +import seedu.sourcecontrol.storage.Storage; + +/** + * The main LogicManager of the app. + */ +public class LogicManager implements Logic { + public static final String FILE_OPS_ERROR_MESSAGE = "Could not save data to file: "; + private final Logger logger = LogsCenter.getLogger(LogicManager.class); + + private final Model model; + private final Storage storage; + private final SourceControlParser sourceControlParser; + + /** + * Constructs a {@code LogicManager} with the given {@code Model} and {@code Storage}. + */ + public LogicManager(Model model, Storage storage) { + this.model = model; + this.storage = storage; + sourceControlParser = new SourceControlParser(); + loadAliases(); + } + + @Override + public CommandResult execute(String commandText) throws CommandException, ParseException { + logger.info("----------------[USER COMMAND][" + commandText + "]"); + + CommandResult commandResult; + Command command = sourceControlParser.parseCommand(commandText); + commandResult = command.execute(model); + + try { + storage.saveSourceControl(model.getSourceControl()); + } catch (IOException ioe) { + throw new CommandException(FILE_OPS_ERROR_MESSAGE + ioe, ioe); + } + + return commandResult; + } + + @Override + public ReadOnlySourceControl getSourceControl() { + return model.getSourceControl(); + } + + @Override + public ObservableList getFilteredStudentList() { + return model.getFilteredStudentList(); + } + + @Override + public Path getSourceControlFilePath() { + return model.getSourceControlFilePath(); + } + + @Override + public GuiSettings getGuiSettings() { + return model.getGuiSettings(); + } + + @Override + public void setGuiSettings(GuiSettings guiSettings) { + model.setGuiSettings(guiSettings); + } + + /** + * Loads the aliases from UserPrefs into the parser. + */ + public void loadAliases() { + Map aliases = model.getAliases(); + + // Verify that all imported aliases are valid. If any are not valid, remove the alias from the UserPrefs + for (String aliasWord : aliases.keySet()) { + try { + AliasCommandParser.checkAliasWord(aliasWord, sourceControlParser); + AliasCommandParser.checkCommandWord(aliases.get(aliasWord), sourceControlParser); + } catch (ParseException e) { + logger.info("Invalid alias removed: " + aliasWord); + aliases.remove(aliasWord); + } + } + + model.setAliases(aliases); + + for (Map.Entry alias: aliases.entrySet()) { + sourceControlParser.addAlias(new Alias(alias.getKey(), alias.getValue())); + } + } +} diff --git a/src/main/java/seedu/sourcecontrol/logic/commands/AddAllocCommand.java b/src/main/java/seedu/sourcecontrol/logic/commands/AddAllocCommand.java new file mode 100644 index 00000000000..c0d9924c259 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/commands/AddAllocCommand.java @@ -0,0 +1,248 @@ +package seedu.sourcecontrol.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_ID; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_NAME; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import seedu.sourcecontrol.logic.commands.exceptions.CommandException; +import seedu.sourcecontrol.model.Model; +import seedu.sourcecontrol.model.ReadOnlySourceControl; +import seedu.sourcecontrol.model.student.Student; +import seedu.sourcecontrol.model.student.assessment.Assessment; +import seedu.sourcecontrol.model.student.assessment.Score; +import seedu.sourcecontrol.model.student.group.Group; +import seedu.sourcecontrol.model.student.id.Id; +import seedu.sourcecontrol.model.student.name.Name; +import seedu.sourcecontrol.model.student.name.NameEqualsPredicate; +import seedu.sourcecontrol.model.student.tag.Tag; + +/** + * Allocates an existing student to an existing group. + */ +public class AddAllocCommand extends Command { + + public static final String COMMAND_WORD = "addalloc"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a student to an existing group. " + + "Parameters: " + + PREFIX_GROUP + " " + + "(" + PREFIX_NAME + " | " + + PREFIX_ID + ")\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_GROUP + "T01A " + + PREFIX_NAME + "Jonas Chow"; + + public static final String MESSAGE_SUCCESS = "New allocation added: %1$s"; + public static final String MESSAGE_NONEXISTENT_GROUP = "This group does not exist."; + public static final String MESSAGE_NONEXISTENT_STUDENT = "This student does not exist."; + public static final String MESSAGE_DUPLICATE_STUDENT_NAME = + "This student needs to be allocated using ID due to duplicate naming."; + public static final String MESSAGE_DUPLICATE_STUDENT = "This student already exists in the group."; + + private final AllocDescriptor allocDescriptor; + + /** + * @param allocDescriptor details of the allocation + */ + public AddAllocCommand(AllocDescriptor allocDescriptor) { + requireNonNull(allocDescriptor); + this.allocDescriptor = new AllocDescriptor(allocDescriptor); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + ReadOnlySourceControl sourceControl = model.getSourceControl(); + List groupList = sourceControl.getGroupList(); + List studentList = sourceControl.getStudentList(); + + assert allocDescriptor.getGroup().isPresent(); + if (!groupList.contains(allocDescriptor.getGroup().get())) { + throw new CommandException(MESSAGE_NONEXISTENT_GROUP); + } + + Group groupToEdit = getAllocGroup(groupList, allocDescriptor); + List studentsToEdit = getAllocStudents(studentList, allocDescriptor); + + if (studentsToEdit.isEmpty()) { + throw new CommandException(MESSAGE_NONEXISTENT_STUDENT); + } + + if (studentsToEdit.size() > 1) { + List matchedIds = studentsToEdit.stream() + .map(Student::getName).map(Name::toString) + .collect(Collectors.toList()); + Predicate predicate = new NameEqualsPredicate(matchedIds.get(0)); + model.updateFilteredStudentList(predicate); + throw new CommandException(MESSAGE_DUPLICATE_STUDENT_NAME); + } + + Student studentToEdit = studentsToEdit.get(0); + + if (groupToEdit.hasStudent(studentToEdit.getId())) { + throw new CommandException(MESSAGE_DUPLICATE_STUDENT); + } + + Student editedStudent = createEditedStudent(studentToEdit, allocDescriptor); + + groupToEdit.addStudent(editedStudent.getId()); + model.setStudent(studentToEdit, editedStudent); + return new CommandResult(String.format(MESSAGE_SUCCESS, editedStudent)); + } + + /** + * Gets and returns a list of {@code Student} with matching identity specified in the {@code allocDescriptor}. + */ + public static Group getAllocGroup(List groups, AllocDescriptor allocDescriptor) { + List allocGroups = groups.stream() + .filter(allocDescriptor.isAllocGroup()) + .collect(Collectors.toList()); + assert allocGroups.size() == 1; + return allocGroups.get(0); + } + + /** + * Gets and returns a list of {@code Student} with matching identity specified in the {@code allocDescriptor}. + */ + public static List getAllocStudents(List students, AllocDescriptor allocDescriptor) { + return students.stream() + .filter(allocDescriptor.isAllocStudent()) + .collect(Collectors.toUnmodifiableList()); + } + + /** + * Creates and returns a {@code Student} with the details of {@code preAllocStudent} + * allocated to the group specified in the {@code allocDescriptor}. + */ + public static Student createEditedStudent(Student toEditStudent, AllocDescriptor allocDescriptor) { + assert toEditStudent != null; + assert allocDescriptor.getGroup().isPresent(); + + Name name = toEditStudent.getName(); + Id id = toEditStudent.getId(); + List groups = toEditStudent.getGroups(); + Map scores = toEditStudent.getScores(); + Set tags = toEditStudent.getTags(); + + Group groupToEdit = allocDescriptor.getGroup().get(); + assert !groups.contains(groupToEdit); + + List editedGroups = new ArrayList<>(groups); + editedGroups.add(groupToEdit); + + return new Student(name, id, editedGroups, scores, tags); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddAllocCommand // instanceof handles nulls + && allocDescriptor.equals(((AddAllocCommand) other).allocDescriptor)); + } + + /** + * Stores the details to edit the student with. Each non-empty field value will replace the + * corresponding field value of the student. + */ + public static class AllocDescriptor { + private Name name; + private Id id; + private Group group; + + public AllocDescriptor() {} + + /** + * Creates a new {@code AllocDescriptor} with the specified {@code Group} and {@code Id}. + */ + public AllocDescriptor(Group group, Id id) { + setGroup(group); + setId(id); + } + + /** + * Creates a new {@code AllocDescriptor} with the specified {@code Group} and {@code Name}. + */ + public AllocDescriptor(Group group, Name name) { + setGroup(group); + setName(name); + } + + /** + * Copy constructor. + */ + public AllocDescriptor(AllocDescriptor toCopy) { + setName(toCopy.name); + setId(toCopy.id); + setGroup(toCopy.group); + } + + public void setName(Name name) { + this.name = name; + } + + public Optional getName() { + return Optional.ofNullable(name); + } + + public void setId(Id id) { + this.id = id; + } + + public Optional getId() { + return Optional.ofNullable(id); + } + + public void setGroup(Group group) { + this.group = group; + } + + public Optional getGroup() { + return Optional.ofNullable(group); + } + + /** + * Returns a {@code Predicate} checking if a group have a matched name. + */ + public Predicate isAllocGroup() { + return toCheck -> toCheck.equals(group); + } + + /** + * Returns a {@code Predicate} checking if a student have a matched name or ID. + */ + public Predicate isAllocStudent() { + return toCheck -> toCheck.getName().equals(name) + || toCheck.getId().equals(id); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AllocDescriptor)) { + return false; + } + + // state check + AllocDescriptor e = (AllocDescriptor) other; + + return getName().equals(e.getName()) + && getId().equals(e.getId()) + && getGroup().equals(e.getGroup()); + } + } +} diff --git a/src/main/java/seedu/sourcecontrol/logic/commands/AddAssessmentCommand.java b/src/main/java/seedu/sourcecontrol/logic/commands/AddAssessmentCommand.java new file mode 100644 index 00000000000..21f9b4358a2 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/commands/AddAssessmentCommand.java @@ -0,0 +1,52 @@ +package seedu.sourcecontrol.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_ASSESSMENT; + +import seedu.sourcecontrol.logic.commands.exceptions.CommandException; +import seedu.sourcecontrol.model.Model; +import seedu.sourcecontrol.model.student.assessment.Assessment; + +/** + * Adds a group to the Source Control application. + */ +public class AddAssessmentCommand extends Command { + + public static final String COMMAND_WORD = "addassessment"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds an assessment to the database. \n" + + "Parameters: " + + PREFIX_ASSESSMENT + "\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_ASSESSMENT + "P01"; + + public static final String MESSAGE_SUCCESS = "New assessment added: %1$s"; + public static final String MESSAGE_DUPLICATE_ASSESSMENT = "This assessment already exists in the database. "; + + private final Assessment assessment; + + /** + * Creates an AddAssessmentCommand to add the specified {@code Assessment} + */ + public AddAssessmentCommand(Assessment assessment) { + requireNonNull(assessment); + this.assessment = assessment; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + if (model.hasAssessment(assessment)) { + throw new CommandException(MESSAGE_DUPLICATE_ASSESSMENT); + } + model.addAssessment(assessment); + return new CommandResult(String.format(MESSAGE_SUCCESS, assessment)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddAssessmentCommand // instanceof handles nulls + && assessment.equals(((AddAssessmentCommand) other).assessment)); + } +} diff --git a/src/main/java/seedu/sourcecontrol/logic/commands/AddGroupCommand.java b/src/main/java/seedu/sourcecontrol/logic/commands/AddGroupCommand.java new file mode 100644 index 00000000000..af396cbb92e --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/commands/AddGroupCommand.java @@ -0,0 +1,153 @@ +package seedu.sourcecontrol.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.sourcecontrol.commons.util.CollectionUtil.equalsIgnoreOrder; +import static seedu.sourcecontrol.logic.commands.AddAllocCommand.AllocDescriptor; +import static seedu.sourcecontrol.logic.commands.AddAllocCommand.createEditedStudent; +import static seedu.sourcecontrol.logic.commands.AddAllocCommand.getAllocStudents; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_ID; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_NAME; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import seedu.sourcecontrol.logic.commands.exceptions.CommandException; +import seedu.sourcecontrol.model.Model; +import seedu.sourcecontrol.model.student.Student; +import seedu.sourcecontrol.model.student.group.Group; +import seedu.sourcecontrol.model.student.name.Name; +import seedu.sourcecontrol.model.student.name.NameEqualsPredicate; + +/** + * Adds a group to the Source Control application. + */ +public class AddGroupCommand extends Command { + + public static final String COMMAND_WORD = "addgroup"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Creates a new group with the specified students (if any). " + + "Parameters: " + + PREFIX_GROUP + " " + + "[(" + PREFIX_NAME + "" + " | " + + PREFIX_ID + "" + ")]...\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_GROUP + "T01A " + + PREFIX_NAME + "Gan Hong Yao " + + PREFIX_ID + "E0123456"; + + public static final String MESSAGE_SUCCESS = "New group added: %1$s\n"; + public static final String MESSAGE_STUDENTS_ADDED = "Students added to group: %1$s\n"; + public static final String MESSAGE_NONEXISTENT_STUDENT = "Student with name or ID \"%1$s\" does not exist."; + public static final String MESSAGE_DUPLICATE_GROUP = "This group already exists in the database."; + public static final String MESSAGE_DUPLICATE_STUDENT = + "The student \"%1$s\" needs to be allocated manually using ID due to duplicate naming."; + public static final String MESSAGE_DUPLICATE_STUDENT_IN_GROUP = + "The student \"%1$s\" (%2$s) was specified more than once."; + + private final Group groupToAdd; + private final List allocDescriptors; + + /** + * Creates an AddGroupCommand to add the specified {@code Group} + */ + public AddGroupCommand(Group group, List allocDescriptors) { + requireNonNull(group); + requireNonNull(allocDescriptors); + groupToAdd = group; + this.allocDescriptors = new ArrayList<>(allocDescriptors); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + if (model.hasGroup(groupToAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_GROUP); + } + + List originalStudents = new ArrayList<>(); + List addedStudents = new ArrayList<>(); + + for (AllocDescriptor allocDescriptor : allocDescriptors) { + List matchedStudents = getAllocStudents( + model.getSourceControl().getStudentList(), allocDescriptor); + + if (matchedStudents.isEmpty()) { + String studentKey = allocDescriptor.getName().isPresent() + ? allocDescriptor.getName().get().toString() + : allocDescriptor.getId().get().toString(); + throw new CommandException(String.format(MESSAGE_NONEXISTENT_STUDENT, studentKey)); + } + + Student studentToEdit = matchedStudents.get(0); + + if (matchedStudents.size() > 1) { + List matchedIds = matchedStudents.stream() + .map(Student::getName).map(Name::toString) + .collect(Collectors.toList()); + Predicate predicate = new NameEqualsPredicate(matchedIds.get(0)); + model.updateFilteredStudentList(predicate); + throw new CommandException(String.format(MESSAGE_DUPLICATE_STUDENT, studentToEdit.getName())); + } + + if (groupToAdd.hasStudent(studentToEdit.getId())) { + throw new CommandException(String.format(MESSAGE_DUPLICATE_STUDENT_IN_GROUP, + studentToEdit.getName(), studentToEdit.getId())); + } + + originalStudents.add(studentToEdit); + Student editedStudent = createEditedStudent(studentToEdit, allocDescriptor); + groupToAdd.addStudent(editedStudent.getId()); + addedStudents.add(editedStudent); + } + + model.addGroup(groupToAdd); + + assert originalStudents.size() == addedStudents.size(); + for (int i = 0; i < originalStudents.size(); i++) { + model.setStudent(originalStudents.get(i), addedStudents.get(i)); + } + + return new CommandResult(formatSuccessMessage(addedStudents)); + } + + /** + * Returns the formatted success message, depending on whether there were students added to the new group. + */ + public String formatSuccessMessage(List addedStudents) { + String groupAddedMessage = String.format(MESSAGE_SUCCESS, groupToAdd.name); + + if (addedStudents.isEmpty()) { + return groupAddedMessage; + } + + String studentNames = addedStudents.stream() + .map(student -> student.getName().fullName) + .collect(Collectors.joining(", ")); + + return groupAddedMessage + String.format(MESSAGE_STUDENTS_ADDED, studentNames); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AddGroupCommand)) { + return false; + } + + // state check + AddGroupCommand e = (AddGroupCommand) other; + + return groupToAdd.equals(e.groupToAdd) + && equalsIgnoreOrder(allocDescriptors, e.allocDescriptors); + } +} diff --git a/src/main/java/seedu/sourcecontrol/logic/commands/AddScoreCommand.java b/src/main/java/seedu/sourcecontrol/logic/commands/AddScoreCommand.java new file mode 100644 index 00000000000..ed141f6a7a9 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/commands/AddScoreCommand.java @@ -0,0 +1,255 @@ +package seedu.sourcecontrol.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_ASSESSMENT; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_ID; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_SCORE; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import seedu.sourcecontrol.logic.commands.exceptions.CommandException; +import seedu.sourcecontrol.model.Model; +import seedu.sourcecontrol.model.ReadOnlySourceControl; +import seedu.sourcecontrol.model.student.Student; +import seedu.sourcecontrol.model.student.assessment.Assessment; +import seedu.sourcecontrol.model.student.assessment.Score; +import seedu.sourcecontrol.model.student.group.Group; +import seedu.sourcecontrol.model.student.id.Id; +import seedu.sourcecontrol.model.student.name.Name; +import seedu.sourcecontrol.model.student.name.NameEqualsPredicate; +import seedu.sourcecontrol.model.student.tag.Tag; + +/** + * Allocates a score for an existing assessment for an existing student. + */ +public class AddScoreCommand extends Command { + + public static final String COMMAND_WORD = "addscore"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds score of an assessment for a student. " + + "Parameters: " + + PREFIX_ASSESSMENT + " " + + "(" + PREFIX_NAME + " | " + + PREFIX_ID + ") " + + PREFIX_SCORE + "\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_ASSESSMENT + "Midterm " + + PREFIX_NAME + "Tang Zhiying " + + PREFIX_SCORE + "68.5"; + + public static final String MESSAGE_ADD_SUCCESS = "New score added: %1$s"; + public static final String MESSAGE_UPDATE_SUCCESS = "Score updated from %1$s to %2$s: %3$s"; + public static final String MESSAGE_NONEXISTENT_ASSESSMENT = "This assessment does not exist."; + public static final String MESSAGE_NONEXISTENT_STUDENT = "This student does not exist."; + public static final String MESSAGE_DUPLICATE_STUDENT_NAME = + "Score needs to be added through ID for this student due to duplicate naming."; + + private final ScoreDescriptor scoreDescriptor; + + /** + * @param scoreDescriptor details of the score + */ + public AddScoreCommand(ScoreDescriptor scoreDescriptor) { + requireNonNull(scoreDescriptor); + this.scoreDescriptor = new ScoreDescriptor(scoreDescriptor); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + ReadOnlySourceControl sourceControl = model.getSourceControl(); + List assessmentList = sourceControl.getAssessmentList(); + List studentList = sourceControl.getStudentList(); + + assert scoreDescriptor.getAssessment().isPresent(); + assert scoreDescriptor.getScore().isPresent(); + + if (!assessmentList.contains(scoreDescriptor.getAssessment().get())) { + throw new CommandException(MESSAGE_NONEXISTENT_ASSESSMENT); + } + + Assessment assessmentToEdit = getToEditAssessment(assessmentList, scoreDescriptor); + List studentsToEdit = getToEditStudents(studentList, scoreDescriptor); + + if (studentsToEdit.isEmpty()) { + throw new CommandException(MESSAGE_NONEXISTENT_STUDENT); + } + + if (studentsToEdit.size() > 1) { + List matchedIds = studentsToEdit.stream() + .map(Student::getName).map(Name::toString) + .collect(Collectors.toList()); + Predicate predicate = new NameEqualsPredicate(matchedIds.get(0)); + model.updateFilteredStudentList(predicate); + throw new CommandException(MESSAGE_DUPLICATE_STUDENT_NAME); + } + + Student studentToEdit = studentsToEdit.get(0); + + boolean wasGraded = assessmentToEdit.isGraded(studentToEdit.getId()); + Score oldScore = null; + if (wasGraded) { + oldScore = assessmentToEdit.getScores().get(studentToEdit.getId()); + } + + Student editedStudent = createEditedStudents(studentToEdit, scoreDescriptor, assessmentToEdit); + + assessmentToEdit.setScore(editedStudent.getId(), scoreDescriptor.getScore().get()); + model.setStudent(studentToEdit, editedStudent); + return new CommandResult(wasGraded + ? String.format(MESSAGE_UPDATE_SUCCESS, oldScore, scoreDescriptor.getScore().get(), editedStudent) + : String.format(MESSAGE_ADD_SUCCESS, editedStudent)); + } + + /** + * Gets and returns a map of {@code Assessment} with matching info + * as specified in the {@code scoreDescriptor}. + */ + public static Assessment getToEditAssessment(List assessments, ScoreDescriptor scoreDescriptor) { + List assessmentToEdit = assessments.stream() + .filter(scoreDescriptor.isToEditAssessment()) + .collect(Collectors.toList()); + assert assessmentToEdit.size() == 1; + return assessmentToEdit.get(0); + } + + /** + * Get and returns a list of {@code Student} with matching info + * as specified in the {@code scoreDescriptor}. + */ + public static List getToEditStudents(List students, ScoreDescriptor scoreDescriptor) { + return students.stream() + .filter(scoreDescriptor.isToEditStudent()) + .collect(Collectors.toUnmodifiableList()); + } + + /** + * Creates and returns a {@code Student} with the details of {@code preScoreStudent} + * being added score as specified in {@code scoreDescriptor}. + */ + public static Student createEditedStudents(Student toEditStudent, ScoreDescriptor scoreDescriptor, + Assessment assessmentToEdit) { + assert toEditStudent != null; + assert scoreDescriptor.getScore().isPresent(); + assert assessmentToEdit != null; + + Name name = toEditStudent.getName(); + Id id = toEditStudent.getId(); + List groups = toEditStudent.getGroups(); + Map scores = toEditStudent.getScores(); + Set tags = toEditStudent.getTags(); + + Score scoreToEdit = scoreDescriptor.getScore().get(); + + Map editedScores = new LinkedHashMap<>(scores); + editedScores.put(assessmentToEdit, scoreToEdit); + + return new Student(name, id, groups, editedScores, tags); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddScoreCommand // instanceof handles nulls + && scoreDescriptor.equals(((AddScoreCommand) other).scoreDescriptor)); + } + + /** + * Stores the details to edit the student with. Each non-empty field value will replace the + * corresponding field value of the student. + */ + public static class ScoreDescriptor { + private Name name; + private Id id; + private Assessment assessment; + private Score score; + + public ScoreDescriptor() {} + + /** + * Copy constructor. + */ + public ScoreDescriptor(ScoreDescriptor toCopy) { + setName(toCopy.name); + setId(toCopy.id); + setAssessment(toCopy.assessment); + setScore(toCopy.score); + } + + public void setName(Name name) { + this.name = name; + } + + public Optional getName() { + return Optional.ofNullable(name); + } + + public void setId(Id id) { + this.id = id; + } + + public Optional getId() { + return Optional.ofNullable(id); + } + + public void setAssessment(Assessment assessment) { + this.assessment = assessment; + } + + public Optional getAssessment() { + return Optional.ofNullable(assessment); + } + + public void setScore(Score score) { + this.score = score; + } + + public Optional getScore() { + return Optional.ofNullable(score); + } + + /** + * Returns a {@code Predicate} checking if an assessment have a matched name. + */ + public Predicate isToEditAssessment() { + return toCheck -> toCheck.equals(assessment); + } + + /** + * Returns a {@code Predicate} checking if a student have a matched name or ID. + */ + public Predicate isToEditStudent() { + return toCheck -> toCheck.getName().equals(name) + || toCheck.getId().equals(id); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ScoreDescriptor)) { + return false; + } + + // state check + ScoreDescriptor e = (ScoreDescriptor) other; + + return getName().equals(e.getName()) + && getId().equals(e.getId()) + && getAssessment().equals(e.getAssessment()) + && getScore().equals(e.getScore()); + } + } +} diff --git a/src/main/java/seedu/sourcecontrol/logic/commands/AddStudentCommand.java b/src/main/java/seedu/sourcecontrol/logic/commands/AddStudentCommand.java new file mode 100644 index 00000000000..a958ec1f0f4 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/commands/AddStudentCommand.java @@ -0,0 +1,64 @@ +package seedu.sourcecontrol.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_ID; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_TAG; + +import seedu.sourcecontrol.logic.commands.exceptions.CommandException; +import seedu.sourcecontrol.model.Model; +import seedu.sourcecontrol.model.student.Student; + +/** + * Adds a student to the Source Control application. + */ +public class AddStudentCommand extends Command { + + public static final String COMMAND_WORD = "addstudent"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a student to the database. \n" + + "Parameters: " + + PREFIX_NAME + " " + + PREFIX_ID + " " + + "[" + PREFIX_GROUP + "]... " + + "[" + PREFIX_TAG + "]...\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_NAME + "Leong Hong Fai " + + PREFIX_ID + "E0543948 " + + PREFIX_GROUP + "T01A " + + PREFIX_GROUP + "R02B " + + PREFIX_TAG + "beginner"; + + public static final String MESSAGE_SUCCESS = "New student added: %1$s"; + public static final String MESSAGE_DUPLICATE_STUDENT = "This student ID already exists in Source Control. "; + + private final Student toAdd; + + /** + * Creates an AddStudentCommand to add the specified {@code Student} + */ + public AddStudentCommand(Student student) { + requireNonNull(student); + toAdd = student; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + if (model.hasStudent(toAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_STUDENT); + } + + model.addStudent(toAdd); + return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddStudentCommand // instanceof handles nulls + && toAdd.equals(((AddStudentCommand) other).toAdd)); + } +} diff --git a/src/main/java/seedu/sourcecontrol/logic/commands/AliasCommand.java b/src/main/java/seedu/sourcecontrol/logic/commands/AliasCommand.java new file mode 100644 index 00000000000..67a7037bac7 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/commands/AliasCommand.java @@ -0,0 +1,63 @@ +package seedu.sourcecontrol.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.sourcecontrol.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_ALIAS; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_COMMAND; + +import seedu.sourcecontrol.logic.parser.Alias; +import seedu.sourcecontrol.logic.parser.SourceControlParser; +import seedu.sourcecontrol.model.Model; + +/** + * Adds an alias to the SourceControlParser and UserPrefs. + */ +public class AliasCommand extends Command { + + public static final String COMMAND_WORD = "alias"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds an alias for a command. \n" + + "Parameters: " + + PREFIX_COMMAND + " " + PREFIX_ALIAS + "\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_COMMAND + AddStudentCommand.COMMAND_WORD + " " + + PREFIX_ALIAS + "addstu"; + + public static final String MESSAGE_ADD_SUCCESS = "New alias added: %1$s"; + public static final String MESSAGE_REMOVE_SUCCESS = "Alias removed: %1$s"; + public static final String MESSAGE_UNKNOWN_OLD_COMMAND = "Specified command to be aliased does not exist. "; + public static final String MESSAGE_OVERWRITE_DEFAULT = "You can't overwrite a default command. "; + + private final Alias alias; + private final SourceControlParser parser; + + /** + * Creates an AliasCommand to add the specified {@code Alias} + */ + public AliasCommand(Alias alias, SourceControlParser parser) { + requireAllNonNull(alias, parser); + this.alias = alias; + this.parser = parser; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + if (alias.isRedundant()) { + model.removeAlias(alias.getAliasWord()); + parser.removeAlias(alias.getAliasWord()); + return new CommandResult(String.format(MESSAGE_REMOVE_SUCCESS, alias.getAliasWord())); + } else { + model.addAlias(alias); + parser.addAlias(alias); + return new CommandResult(String.format(MESSAGE_ADD_SUCCESS, alias.getAliasWord())); + } + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AliasCommand // instanceof handles nulls + && alias.equals(((AliasCommand) other).alias)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/sourcecontrol/logic/commands/ClearCommand.java similarity index 50% rename from src/main/java/seedu/address/logic/commands/ClearCommand.java rename to src/main/java/seedu/sourcecontrol/logic/commands/ClearCommand.java index 9c86b1fa6e4..4994adee3cc 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/sourcecontrol/logic/commands/ClearCommand.java @@ -1,23 +1,23 @@ -package seedu.address.logic.commands; +package seedu.sourcecontrol.logic.commands; import static java.util.Objects.requireNonNull; -import seedu.address.model.AddressBook; -import seedu.address.model.Model; +import seedu.sourcecontrol.model.Model; +import seedu.sourcecontrol.model.SourceControl; /** - * Clears the address book. + * Clears the Source Control application. */ 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 = "Source Control has been cleared!"; @Override public CommandResult execute(Model model) { requireNonNull(model); - model.setAddressBook(new AddressBook()); + model.setSourceControl(new SourceControl()); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/Command.java b/src/main/java/seedu/sourcecontrol/logic/commands/Command.java similarity index 76% rename from src/main/java/seedu/address/logic/commands/Command.java rename to src/main/java/seedu/sourcecontrol/logic/commands/Command.java index 64f18992160..77807f0d634 100644 --- a/src/main/java/seedu/address/logic/commands/Command.java +++ b/src/main/java/seedu/sourcecontrol/logic/commands/Command.java @@ -1,7 +1,7 @@ -package seedu.address.logic.commands; +package seedu.sourcecontrol.logic.commands; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; +import seedu.sourcecontrol.logic.commands.exceptions.CommandException; +import seedu.sourcecontrol.model.Model; /** * Represents a command with hidden internal logic and the ability to be executed. diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/sourcecontrol/logic/commands/CommandResult.java similarity index 50% rename from src/main/java/seedu/address/logic/commands/CommandResult.java rename to src/main/java/seedu/sourcecontrol/logic/commands/CommandResult.java index 92f900b7916..9b05fabbc7a 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/seedu/sourcecontrol/logic/commands/CommandResult.java @@ -1,9 +1,13 @@ -package seedu.address.logic.commands; +package seedu.sourcecontrol.logic.commands; import static java.util.Objects.requireNonNull; +import static seedu.sourcecontrol.logic.commands.ShowCommand.Info; +import java.nio.file.Path; import java.util.Objects; +import javafx.scene.chart.Chart; + /** * Represents the result of a command execution. */ @@ -11,6 +15,12 @@ public class CommandResult { private final String feedbackToUser; + private final Info info; + + private final Chart chart; + + private final Path savePath; + /** Help information should be shown to the user. */ private final boolean showHelp; @@ -20,8 +30,11 @@ public class CommandResult { /** * Constructs a {@code CommandResult} with the specified fields. */ - public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { + public CommandResult(String feedbackToUser, Info info, Chart chart, Path savePath, boolean showHelp, boolean exit) { this.feedbackToUser = requireNonNull(feedbackToUser); + this.info = info; + this.chart = chart; + this.savePath = savePath; this.showHelp = showHelp; this.exit = exit; } @@ -31,13 +44,57 @@ public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { * and other fields set to their default value. */ public CommandResult(String feedbackToUser) { - this(feedbackToUser, false, false); + this(feedbackToUser, null, null, null, false, false); + } + + /** + * Constructs a {@code CommandResult} with the specified {@code feedbackToUser}, + * {@code info}, {@code chart}, and other fields set to their default value. + */ + public CommandResult(String feedbackToUser, Info info, Chart chart, Path savePath) { + this(feedbackToUser, info, chart, savePath, false, false); + } + + public CommandResult(String feedbackToUser, Info info) { + this(feedbackToUser, info, null, null, false, false); + } + + public CommandResult(String feedbackToUser, Chart chart, Path savePath) { + this(feedbackToUser, null, chart, savePath, false, false); + } + + public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { + this(feedbackToUser, null, null, null, showHelp, exit); } public String getFeedbackToUser() { return feedbackToUser; } + public Info getInfo() { + return info; + } + + public boolean hasInfo() { + return info != null; + } + + public Path getSavePath() { + return savePath; + } + + public boolean hasSavePath() { + return savePath != null; + } + + public Chart getChart() { + return chart; + } + + public boolean hasChart() { + return chart != null; + } + public boolean isShowHelp() { return showHelp; } diff --git a/src/main/java/seedu/sourcecontrol/logic/commands/DeleteCommand.java b/src/main/java/seedu/sourcecontrol/logic/commands/DeleteCommand.java new file mode 100644 index 00000000000..91127ce54ed --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/commands/DeleteCommand.java @@ -0,0 +1,54 @@ +package seedu.sourcecontrol.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.sourcecontrol.commons.core.Messages; +import seedu.sourcecontrol.commons.core.index.Index; +import seedu.sourcecontrol.logic.commands.exceptions.CommandException; +import seedu.sourcecontrol.model.Model; +import seedu.sourcecontrol.model.student.Student; + +/** + * Deletes a student identified using it's displayed index from the Source Control application. + */ +public class DeleteCommand extends Command { + + public static final String COMMAND_WORD = "delete"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the student identified by the index number used in the displayed student list.\n" + + "Parameters: (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_DELETE_STUDENT_SUCCESS = "Deleted Student: %1$s"; + + private final Index targetIndex; + + public DeleteCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredStudentList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(String.format( + Messages.MESSAGE_INVALID_STUDENT_DISPLAYED_INDEX, lastShownList.size())); + } + + Student studentToDelete = lastShownList.get(targetIndex.getZeroBased()); + model.deleteStudent(studentToDelete); + return new CommandResult(String.format(MESSAGE_DELETE_STUDENT_SUCCESS, studentToDelete)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeleteCommand // instanceof handles nulls + && targetIndex.equals(((DeleteCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/seedu/sourcecontrol/logic/commands/EditCommand.java b/src/main/java/seedu/sourcecontrol/logic/commands/EditCommand.java new file mode 100644 index 00000000000..045b41ddec2 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/commands/EditCommand.java @@ -0,0 +1,240 @@ +package seedu.sourcecontrol.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_ID; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.sourcecontrol.model.Model.PREDICATE_SHOW_ALL_STUDENTS; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import seedu.sourcecontrol.commons.core.Messages; +import seedu.sourcecontrol.commons.core.index.Index; +import seedu.sourcecontrol.commons.util.CollectionUtil; +import seedu.sourcecontrol.logic.commands.exceptions.CommandException; +import seedu.sourcecontrol.model.Model; +import seedu.sourcecontrol.model.student.Student; +import seedu.sourcecontrol.model.student.assessment.Assessment; +import seedu.sourcecontrol.model.student.assessment.Score; +import seedu.sourcecontrol.model.student.group.Group; +import seedu.sourcecontrol.model.student.id.Id; +import seedu.sourcecontrol.model.student.name.Name; +import seedu.sourcecontrol.model.student.tag.Tag; + +/** + * Edits the details of an existing student in the Source Control application. + */ +public class EditCommand extends Command { + + public static final String COMMAND_WORD = "edit"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the student identified " + + "by the index number used in the displayed student list. " + + "Existing values will be overwritten by the input values.\n" + + "Parameters: (must be a positive integer) " + + "[" + PREFIX_NAME + "] " + + "[" + PREFIX_ID + "] " + + "[" + PREFIX_GROUP + "]... " + + "[" + PREFIX_TAG + "TAG]...\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_ID + "E0656831"; + + public static final String MESSAGE_EDIT_STUDENT_SUCCESS = "Edited Student: %1$s"; + public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; + public static final String MESSAGE_DUPLICATE_STUDENT = "This student already exists in Source Control."; + public static final String MESSAGE_NONEXISTENT_GROUP = "This group does not exist."; + + private final Index index; + private final EditStudentDescriptor editStudentDescriptor; + + /** + * @param index of the student in the filtered student list to edit + * @param editStudentDescriptor details to edit the student with + */ + public EditCommand(Index index, EditStudentDescriptor editStudentDescriptor) { + requireNonNull(index); + requireNonNull(editStudentDescriptor); + + this.index = index; + this.editStudentDescriptor = new EditStudentDescriptor(editStudentDescriptor); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredStudentList(); + List groupList = model.getSourceControl().getGroupList(); + + if (!groupList.containsAll(editStudentDescriptor.getGroups().orElse(new ArrayList<>()))) { + throw new CommandException(MESSAGE_NONEXISTENT_GROUP); + } + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException( + String.format(Messages.MESSAGE_INVALID_STUDENT_DISPLAYED_INDEX, lastShownList.size())); + } + + Student studentToEdit = lastShownList.get(index.getZeroBased()); + Student editedStudent = createEditedStudent(studentToEdit, editStudentDescriptor, groupList); + + if (!studentToEdit.isSameStudent(editedStudent) && model.hasStudent(editedStudent)) { + throw new CommandException(MESSAGE_DUPLICATE_STUDENT); + } + + model.setStudent(studentToEdit, editedStudent); + model.updateFilteredStudentList(PREDICATE_SHOW_ALL_STUDENTS); + return new CommandResult(String.format(MESSAGE_EDIT_STUDENT_SUCCESS, editedStudent)); + } + + /** + * Creates and returns a {@code Student} with the details of {@code studentToEdit} + * edited with {@code editStudentDescriptor}. + */ + private static Student createEditedStudent(Student studentToEdit, EditStudentDescriptor editStudentDescriptor, + List groups) { + assert studentToEdit != null; + + Name updatedName = editStudentDescriptor.getName().orElse(studentToEdit.getName()); + Id updatedId = editStudentDescriptor.getId().orElse(studentToEdit.getId()); + + // ensure that the groups added are the same as those in the group list + List updatedGroups = editStudentDescriptor.getGroups().orElse(studentToEdit.getGroups()); + updatedGroups = updatedGroups.stream() + .map(group -> groups.get(groups.indexOf(group))) + .collect(Collectors.toList()); + + // it is not the job of the edit command to edit scores + Map updatedScores = studentToEdit.getScores(); + Set updatedTags = editStudentDescriptor.getTags().orElse(studentToEdit.getTags()); + + return new Student(updatedName, updatedId, updatedGroups, updatedScores, updatedTags); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditCommand)) { + return false; + } + + // state check + EditCommand e = (EditCommand) other; + return index.equals(e.index) + && editStudentDescriptor.equals(e.editStudentDescriptor); + } + + /** + * Stores the details to edit the student with. Each non-empty field value will replace the + * corresponding field value of the student. + */ + public static class EditStudentDescriptor { + private Name name; + private Id id; + private List groups; + private Set tags; + + public EditStudentDescriptor() {} + + /** + * Copy constructor. + * A defensive copy of {@code tags} is used internally. + */ + public EditStudentDescriptor(EditStudentDescriptor toCopy) { + setName(toCopy.name); + setId(toCopy.id); + setGroups(toCopy.groups); + setTags(toCopy.tags); + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(name, id, groups, tags); + } + + public void setName(Name name) { + this.name = name; + } + + public Optional getName() { + return Optional.ofNullable(name); + } + + public void setId(Id id) { + this.id = id; + } + + public Optional getId() { + return Optional.ofNullable(id); + } + + /** + * Sets {@code groups} to this object's {@code groups}. + * A defensive copy of {@code groups} is used internally. + */ + public void setGroups(List groups) { + this.groups = (groups != null) ? groups.stream().distinct().collect(Collectors.toList()) : null; + } + + /** + * Returns an unmodifiable group list, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code groups} is null. + */ + public Optional> getGroups() { + return (groups != null) ? Optional.of(Collections.unmodifiableList(groups)) : Optional.empty(); + } + + /** + * Sets {@code tags} to this object's {@code tags}. + * A defensive copy of {@code tags} is used internally. + */ + public void setTags(Set tags) { + this.tags = (tags != null) ? new HashSet<>(tags) : null; + } + + /** + * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code tags} is null. + */ + public Optional> getTags() { + return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditStudentDescriptor)) { + return false; + } + + // state check + EditStudentDescriptor e = (EditStudentDescriptor) other; + + return getName().equals(e.getName()) + && getId().equals(e.getId()) + && getGroups().equals(e.getGroups()) + && getTags().equals(e.getTags()); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/sourcecontrol/logic/commands/ExitCommand.java similarity index 75% rename from src/main/java/seedu/address/logic/commands/ExitCommand.java rename to src/main/java/seedu/sourcecontrol/logic/commands/ExitCommand.java index 3dd85a8ba90..5ed081a4265 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/sourcecontrol/logic/commands/ExitCommand.java @@ -1,6 +1,6 @@ -package seedu.address.logic.commands; +package seedu.sourcecontrol.logic.commands; -import seedu.address.model.Model; +import seedu.sourcecontrol.model.Model; /** * Terminates the program. @@ -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 Source Control ..."; @Override public CommandResult execute(Model model) { diff --git a/src/main/java/seedu/sourcecontrol/logic/commands/ExportCommand.java b/src/main/java/seedu/sourcecontrol/logic/commands/ExportCommand.java new file mode 100644 index 00000000000..09f7249aed5 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/commands/ExportCommand.java @@ -0,0 +1,162 @@ +package seedu.sourcecontrol.logic.commands; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +import seedu.sourcecontrol.commons.util.FileUtil; +import seedu.sourcecontrol.logic.commands.exceptions.CommandException; +import seedu.sourcecontrol.model.Model; +import seedu.sourcecontrol.model.student.Student; +import seedu.sourcecontrol.model.student.assessment.Assessment; +import seedu.sourcecontrol.model.student.assessment.Score; +import seedu.sourcecontrol.model.student.group.Group; +import seedu.sourcecontrol.model.student.id.Id; +import seedu.sourcecontrol.model.student.tag.Tag; + + +public class ExportCommand extends Command { + public static final String COMMAND_WORD = "export"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Exports data from to a file. \n" + + "Example: " + COMMAND_WORD; + + public static final String MESSAGE_SUCCESS = "Exported to file: %1$s"; + public static final String MESSAGE_FAILURE = "Failed to export to file. "; + + public static final String BASE_PATH = "sourceControl%1$s.csv"; + + private int groupColumns; + private int tagColumns; + private Path file; + + /** + * Creates an ExportCommand to export data to a file. + */ + public ExportCommand() { + this.file = generateNewPath(0); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + StringBuilder writeContent = new StringBuilder(); + writeContent.append("Name,ID,"); + + List students = model.getSourceControl().getStudentList(); + appendGroupHeaders(writeContent, students); + + List assessments = model.getSourceControl().getAssessmentList(); + appendAssessmentHeaders(writeContent, assessments); + + appendTagHeaders(writeContent, students); + + replaceLastCharacterWithNewLine(writeContent); + + for (Student student : students) { + appendStudentRow(writeContent, student, assessments); + } + + try { + FileUtil.createIfMissing(file); + FileUtil.writeToFile(file, writeContent.toString()); + } catch (IOException e) { + throw new CommandException(MESSAGE_FAILURE); + } + + return new CommandResult(String.format(MESSAGE_SUCCESS, file)); + } + + private void appendGroupHeaders(StringBuilder writeContent, List students) { + groupColumns = 0; + for (Student student : students) { + groupColumns = Math.max(groupColumns, student.getGroups().size()); + } + + for (int i = 1; i <= groupColumns; i++) { + writeContent.append("Group ").append(i).append(","); + } + } + + private void appendAssessmentHeaders(StringBuilder writeContent, List assessments) { + for (Assessment assessment : assessments) { + writeContent.append(assessment.name).append(","); + } + } + + private void appendTagHeaders(StringBuilder writeContent, List students) { + tagColumns = 0; + for (Student student : students) { + tagColumns = Math.max(tagColumns, student.getTags().size()); + } + + for (int i = 1; i <= tagColumns; i++) { + writeContent.append("Tag ").append(i).append(","); + } + } + + private void appendStudentRow(StringBuilder writeContent, Student student, List assessments) { + writeContent.append(student.getName().fullName).append(","); + writeContent.append(student.getId().getValue()).append(","); + + List groups = student.getGroups(); + for (Group group : groups) { + writeContent.append(group.getName()).append(","); + } + for (int i = groups.size(); i < groupColumns; i++) { + writeContent.append(","); + } + + for (Assessment assessment : assessments) { + Map scores = assessment.getScores(); + if (scores.containsKey(student.getId())) { + writeContent.append(scores.get(student.getId()).getValue()); + } + writeContent.append(","); + } + + List tags = List.copyOf(student.getTags()); + for (Tag tag : tags) { + writeContent.append(tag.tagName).append(","); + } + for (int i = tags.size(); i < tagColumns; i++) { + writeContent.append(","); + } + + replaceLastCharacterWithNewLine(writeContent); + } + + private void replaceLastCharacterWithNewLine(StringBuilder writeContent) { + int length = writeContent.length(); + writeContent.replace(length - 1, length, "\n"); + } + + /** + * Generates a path to save the csv. Ensures that the csv saved does not overwrite any existing file. + * Default path is ./sourceControl.csv. + */ + public static Path generateNewPath(int tries) { + String pathString = String.format(BASE_PATH, tries == 0 ? "" : "(" + tries + ")"); + Path path = FileUtil.pathOf(pathString); + if (FileUtil.isFileExists(path)) { + return generateNewPath(tries + 1); + } else { + return path; + } + } + + public Path getFile() { + return this.file; + } + + public void setFile(Path file) { + this.file = file; + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof ExportCommand + && this.file.equals(((ExportCommand) other).file)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/sourcecontrol/logic/commands/HelpCommand.java similarity index 86% rename from src/main/java/seedu/address/logic/commands/HelpCommand.java rename to src/main/java/seedu/sourcecontrol/logic/commands/HelpCommand.java index bf824f91bd0..2bca04fbf72 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/seedu/sourcecontrol/logic/commands/HelpCommand.java @@ -1,6 +1,6 @@ -package seedu.address.logic.commands; +package seedu.sourcecontrol.logic.commands; -import seedu.address.model.Model; +import seedu.sourcecontrol.model.Model; /** * Format full help instructions for every command for display. diff --git a/src/main/java/seedu/sourcecontrol/logic/commands/ImportCommand.java b/src/main/java/seedu/sourcecontrol/logic/commands/ImportCommand.java new file mode 100644 index 00000000000..4f27bf2d508 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/commands/ImportCommand.java @@ -0,0 +1,210 @@ +package seedu.sourcecontrol.logic.commands; + +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_ASSESSMENT; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_FILE; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_TAG; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import seedu.sourcecontrol.commons.util.FileUtil; +import seedu.sourcecontrol.logic.commands.exceptions.CommandException; +import seedu.sourcecontrol.model.Model; +import seedu.sourcecontrol.model.SourceControl; +import seedu.sourcecontrol.model.student.Student; +import seedu.sourcecontrol.model.student.assessment.Assessment; +import seedu.sourcecontrol.model.student.assessment.Score; +import seedu.sourcecontrol.model.student.exceptions.DuplicateStudentException; +import seedu.sourcecontrol.model.student.group.Group; +import seedu.sourcecontrol.model.student.id.Id; +import seedu.sourcecontrol.model.student.name.Name; +import seedu.sourcecontrol.model.student.tag.Tag; + + +public class ImportCommand extends Command { + public static final String COMMAND_WORD = "import"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Imports data from a file. \n" + + "Parameters: " + + PREFIX_FILE + " " + + "[" + PREFIX_GROUP + "] " + + "[" + PREFIX_ASSESSMENT + "] " + + "[" + PREFIX_TAG + "]\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_FILE + "data.csv " + + PREFIX_GROUP + "2 " + + PREFIX_ASSESSMENT + "10 " + + PREFIX_TAG + "3"; + + + public static final String MESSAGE_SUCCESS = "Imported all students. "; + public static final String MESSAGE_INVALID_FILE = "Failed to read from the file. "; + public static final String MESSAGE_INVALID_NUMBER = "Number of columns should be a non-negative integer. "; + public static final String MESSAGE_OUT_OF_BOUNDS = "Reached unexpected end of line while reading from file. "; + public static final String MESSAGE_DUPLICATE_ASSESSMENT = "Duplicate assessment found in file. "; + public static final String MESSAGE_DUPLICATE_ID = "Duplicate student ID found in file. "; + + + private final int groupCount; + private final int assessmentCount; + private final int tagCount; + private final Path file; + + /** + * Creates an ImportCommand to import data from the given {@code file} + */ + public ImportCommand(int groupCount, int assessmentCount, int tagCount, Path file) { + this.groupCount = groupCount; + this.assessmentCount = assessmentCount; + this.tagCount = tagCount; + this.file = file; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + String fileContents; + try { + fileContents = FileUtil.readFromFile(file); + } catch (IOException e) { + throw new CommandException(MESSAGE_INVALID_FILE); + } + + SourceControl newAb = new SourceControl(); + List assessments = new ArrayList<>(); + + String[] lines = fileContents.split("\n"); + String[] headers = lines[0].split(",", -1); + for (int i = 0; i < assessmentCount; i++) { + String assessmentName = readValue(headers, groupCount + 2 + i); + Assessment assessment = makeAssessment(assessmentName); + if (assessments.contains(assessment)) { + throw new CommandException(MESSAGE_DUPLICATE_ASSESSMENT); + } + assessments.add(assessment); + newAb.addAssessment(assessment); + } + + for (int i = 1; i < lines.length; i++) { + try { + newAb.addStudent(readStudentFromRow(lines[i], assessments, newAb.getGroupList())); + } catch (DuplicateStudentException e) { + throw new CommandException(MESSAGE_DUPLICATE_ID); + } + } + + model.setSourceControl(newAb); + + return new CommandResult(MESSAGE_SUCCESS); + } + + private Student readStudentFromRow(String row, + List assessments, + List groupList) throws CommandException { + String[] values = row.split(",", -1); + Name name = makeName(readValue(values, 0)); + Id id = makeId(readValue(values, 1)); + int readingColumn = 2; + + List groups = new ArrayList<>(); + for (int i = 0; i < groupCount; i++, readingColumn++) { + String groupName = readValue(values, readingColumn); + if (!groupName.isEmpty()) { + Group group = makeGroup(groupName); + // if the group already exists in the group list, use the existing group. + // otherwise, use the new group. + Group toAdd = groupList.stream().filter(grp -> grp.equals(group)) + .findFirst().orElse(group); + group.addStudent(id); + groups.add(toAdd); + } + } + + Map scores = new LinkedHashMap<>(); + for (int i = 0; i < assessmentCount; i++, readingColumn++) { + String assessmentScore = readValue(values, readingColumn); + if (!assessmentScore.isEmpty()) { + Score score = makeScore(assessmentScore); + scores.put(assessments.get(i), score); + assessments.get(i).setScore(id, score); + } + } + + Set tags = new HashSet<>(); + for (int i = 0; i < tagCount; i++, readingColumn++) { + String tagName = readValue(values, readingColumn); + if (!tagName.isEmpty()) { + tags.add(makeTag(tagName)); + } + } + + return new Student(name, id, groups, scores, tags); + } + + private String readValue(String[] row, int column) throws CommandException { + if (column >= row.length) { + // avoid array out of bounds error + throw new CommandException(MESSAGE_OUT_OF_BOUNDS); + } + return row[column].trim(); + } + + private Name makeName(String name) throws CommandException { + if (!Name.isValidName(name)) { + throw new CommandException(Name.MESSAGE_CONSTRAINTS); + } + return new Name(name); + } + + private Id makeId(String id) throws CommandException { + if (!Id.isValidID(id)) { + throw new CommandException(Id.MESSAGE_CONSTRAINTS); + } + return new Id(id); + } + + private Group makeGroup(String groupName) throws CommandException { + if (!Group.isValidGroup(groupName)) { + throw new CommandException(Group.MESSAGE_CONSTRAINTS); + } + return new Group(groupName); + } + + private Score makeScore(String score) throws CommandException { + if (!Score.isValidScore(score)) { + throw new CommandException(Score.MESSAGE_CONSTRAINTS); + } + return new Score(score); + } + + private Assessment makeAssessment(String assessmentName) throws CommandException { + if (!Assessment.isValidAssessment(assessmentName)) { + throw new CommandException(Assessment.MESSAGE_CONSTRAINTS); + } + return new Assessment(assessmentName); + } + + private Tag makeTag(String tagName) throws CommandException { + if (!Tag.isValidTagName(tagName)) { + throw new CommandException(Tag.MESSAGE_CONSTRAINTS); + } + return new Tag(tagName); + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof ImportCommand + && this.file.equals(((ImportCommand) other).file) + && this.groupCount == ((ImportCommand) other).groupCount + && this.assessmentCount == ((ImportCommand) other).assessmentCount + && this.tagCount == ((ImportCommand) other).tagCount); + } +} + diff --git a/src/main/java/seedu/sourcecontrol/logic/commands/ListCommand.java b/src/main/java/seedu/sourcecontrol/logic/commands/ListCommand.java new file mode 100644 index 00000000000..9d75acc01c2 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/commands/ListCommand.java @@ -0,0 +1,24 @@ +package seedu.sourcecontrol.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.sourcecontrol.model.Model.PREDICATE_SHOW_ALL_STUDENTS; + +import seedu.sourcecontrol.model.Model; + +/** + * Lists all students in the Source Control application to the user. + */ +public class ListCommand extends Command { + + public static final String COMMAND_WORD = "list"; + + public static final String MESSAGE_SUCCESS = "Listed all students. "; + + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredStudentList(PREDICATE_SHOW_ALL_STUDENTS); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/sourcecontrol/logic/commands/SearchCommand.java b/src/main/java/seedu/sourcecontrol/logic/commands/SearchCommand.java new file mode 100644 index 00000000000..5b7bfc15712 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/commands/SearchCommand.java @@ -0,0 +1,76 @@ +package seedu.sourcecontrol.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_ID; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.function.Predicate; + +import seedu.sourcecontrol.commons.core.Messages; +import seedu.sourcecontrol.model.Model; +import seedu.sourcecontrol.model.student.Student; +import seedu.sourcecontrol.model.student.group.GroupContainsKeywordsPredicate; +import seedu.sourcecontrol.model.student.id.IdContainsKeywordsPredicate; +import seedu.sourcecontrol.model.student.name.NameContainsKeywordsPredicate; +import seedu.sourcecontrol.model.student.tag.TagContainsKeywordsPredicate; + + +/** + * Finds and lists all students in Source Control application whose fields contains any of the argument keywords. + * Keyword matching is case insensitive. + */ +public class SearchCommand extends Command { + + public static final String COMMAND_WORD = "search"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Finds all students whose names contain any of " + + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" + + "Parameters: " + + "(" + PREFIX_NAME + "" + " | " + + PREFIX_ID + "" + " | " + + PREFIX_GROUP + "" + " | " + + PREFIX_TAG + "" + ")\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_NAME + "Tang Zhiying, " + + COMMAND_WORD + " " + + PREFIX_GROUP + "T01A R04B"; + + private final Predicate predicate; + + public SearchCommand(NameContainsKeywordsPredicate predicate) { + this.predicate = predicate; + } + + public SearchCommand(IdContainsKeywordsPredicate predicate) { + this.predicate = predicate; + } + + public SearchCommand(GroupContainsKeywordsPredicate predicate) { + this.predicate = predicate; + } + + public SearchCommand(TagContainsKeywordsPredicate predicate) { + this.predicate = predicate; + } + + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredStudentList(predicate); + int numStudents = model.getFilteredStudentList().size(); + return new CommandResult( + String.format(Messages.MESSAGE_STUDENTS_LISTED_OVERVIEW, + numStudents + " student" + (numStudents == 1 ? "" : "s"))); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof SearchCommand // instanceof handles nulls + && predicate.equals(((SearchCommand) other).predicate)); // state check + } +} diff --git a/src/main/java/seedu/sourcecontrol/logic/commands/ShowCommand.java b/src/main/java/seedu/sourcecontrol/logic/commands/ShowCommand.java new file mode 100644 index 00000000000..2628ed51fdb --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/commands/ShowCommand.java @@ -0,0 +1,373 @@ +package seedu.sourcecontrol.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_ASSESSMENT; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_FILE; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_ID; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.sourcecontrol.model.Model.PREDICATE_SHOW_ALL_STUDENTS; + +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import seedu.sourcecontrol.commons.core.Messages; +import seedu.sourcecontrol.commons.core.index.Index; +import seedu.sourcecontrol.logic.commands.exceptions.CommandException; +import seedu.sourcecontrol.model.Model; +import seedu.sourcecontrol.model.student.Student; +import seedu.sourcecontrol.model.student.assessment.Assessment; +import seedu.sourcecontrol.model.student.assessment.AssessmentStatistics; +import seedu.sourcecontrol.model.student.assessment.GroupStatistics; +import seedu.sourcecontrol.model.student.assessment.StudentStatistics; +import seedu.sourcecontrol.model.student.group.Group; +import seedu.sourcecontrol.model.student.id.Id; +import seedu.sourcecontrol.model.student.id.IdContainsKeywordsPredicate; +import seedu.sourcecontrol.model.student.name.Name; +import seedu.sourcecontrol.model.student.name.NameEqualsPredicate; + +/** + * Shows information of a student or an assessment. + */ +public class ShowCommand extends Command { + + public static final String COMMAND_WORD = "show"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Shows performance analysis of a student, an assessment or a group. " + + "Parameters: " + + "( | " + + PREFIX_NAME + " | " + + PREFIX_ID + " | " + + PREFIX_ASSESSMENT + " | " + + PREFIX_GROUP + ") " + + "[ " + PREFIX_FILE + "]\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_NAME + "Tang Zhiying, " + + COMMAND_WORD + " " + + PREFIX_ASSESSMENT + "Midterm"; + + public static final String MESSAGE_SUCCESS = "Info requested successfully. "; + public static final String MESSAGE_NONEXISTENT_STUDENT = "This student does not exist."; + public static final String MESSAGE_NONEXISTENT_ASSESSMENT = "This assessment does not exist."; + public static final String MESSAGE_NONEXISTENT_GROUP = "This group does not exist."; + public static final String MESSAGE_DUPLICATE_STUDENT_NAME = + "This student needs to be specified using INDEX or ID due to duplicate naming."; + + public static final String BASE_PATH = "graph%1$s.png"; + + private Index index; + private Name name; + private Id id; + private Assessment assessment; + private Group group; + private Path savePath; + + /** + * Constructor for a {@code ShowCommand} with given {@code Index}. + */ + public ShowCommand(Index index, Path savePath) { + requireNonNull(index); + setIndex(index); + setSavePath(savePath); + } + + /** + * Constructor for a {@code ShowCommand} with given {@code Name}. + */ + public ShowCommand(Name name, Path savePath) { + requireNonNull(name); + setName(name); + setSavePath(savePath); + } + + /** + * Constructor for a {@code ShowCommand} with given {@code Id}. + */ + public ShowCommand(Id id, Path savePath) { + requireNonNull(id); + setId(id); + setSavePath(savePath); + } + + /** + * Constructor for a {@code ShowCommand} with given {@code Assessment}. + */ + public ShowCommand(Assessment assessment, Path savePath) { + requireNonNull(assessment); + setAssessment(assessment); + setSavePath(savePath); + } + + /** + * Constructor for a {@code ShowCommand} with given {@code Group}. + */ + public ShowCommand(Group group, Path savePath) { + requireNonNull(group); + setGroup(group); + setSavePath(savePath); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + return getIndex().isPresent() + ? showStudentByIndex(model) + : getAssessment().isPresent() + ? showAssessment(model) + : getGroup().isPresent() + ? showGroup(model) + : showStudentByPrefixes(model); + } + + /** + * Executes command when a {@code Student} info is requested by an {@code Index}. + */ + private CommandResult showStudentByIndex(Model model) throws CommandException { + assert getIndex().isPresent(); + + List students = model.getFilteredStudentList(); + + if (index.getZeroBased() >= students.size()) { + throw new CommandException( + String.format(Messages.MESSAGE_INVALID_STUDENT_DISPLAYED_INDEX, students.size())); + } + + Student matchedStudent = students.get(index.getZeroBased()); + + Info info = new Info(matchedStudent); + StudentStatistics statistics = new StudentStatistics(matchedStudent); + return new CommandResult(MESSAGE_SUCCESS, info, statistics.toLineChart(), savePath); + } + + /** + * Executes command when a {@code Student} info is requested by a {@code Name} or an {@code Id}. + */ + private CommandResult showStudentByPrefixes(Model model) throws CommandException { + assert getName().isPresent() || getId().isPresent(); + + // filter student list into students with matched identity + Predicate predicate = createStudentPredicate(); + model.updateFilteredStudentList(predicate); + List matchedStudents = model.getFilteredStudentList(); + + if (matchedStudents.size() == 0) { + throw new CommandException(MESSAGE_NONEXISTENT_STUDENT); + } + + if (matchedStudents.size() > 1) { + throw new CommandException(MESSAGE_DUPLICATE_STUDENT_NAME); + } + + Student matchedStudent = matchedStudents.get(0); + + Info info = new Info(matchedStudent); + StudentStatistics statistics = new StudentStatistics(matchedStudent); + return new CommandResult(MESSAGE_SUCCESS, info, statistics.toLineChart(), savePath); + } + + /** + * Executes command when an {@code Assessment} info is requested. + */ + private CommandResult showAssessment(Model model) throws CommandException { + assert getAssessment().isPresent(); + Assessment matchedAssessment = model.getAssessment(assessment); + + // reset filtered student list (if any) into normal list + model.updateFilteredStudentList(PREDICATE_SHOW_ALL_STUDENTS); + + if (matchedAssessment == null) { + throw new CommandException(MESSAGE_NONEXISTENT_ASSESSMENT); + } + + Info info = new Info(matchedAssessment); + AssessmentStatistics statistics = new AssessmentStatistics(matchedAssessment); + return new CommandResult(MESSAGE_SUCCESS, info, statistics.toHistogram(), savePath); + } + + /** + * Executes command when a {@code Group} info is requested. + */ + private CommandResult showGroup(Model model) throws CommandException { + assert getGroup().isPresent(); + Group matchedGroup = model.getGroup(group); + + if (matchedGroup == null) { + throw new CommandException(MESSAGE_NONEXISTENT_GROUP); + } + + // filter student list into students in matched group + // (to allow easier reference to students in the interest group) + group = matchedGroup; + Predicate predicate = createStudentPredicate(); + model.updateFilteredStudentList(predicate); + + Info info = new Info(matchedGroup); + GroupStatistics statistics = new GroupStatistics(matchedGroup, model.getSourceControl().getAssessmentList()); + return new CommandResult(MESSAGE_SUCCESS, info, statistics.toLineChart(), savePath); + } + + /** + * Creates a {@code Predicate} checking if a student has a matched + * {@code Name}, or {@code Id}, or belonging to a {@code Group}. + */ + private Predicate createStudentPredicate() { + if (getName().isPresent()) { + return new NameEqualsPredicate(name.fullName); + } + + if (getId().isPresent()) { + return new IdContainsKeywordsPredicate(List.of(id.toString())); + } + + if (getGroup().isPresent()) { + List ids = group.getStudents().stream() + .map(Id::toString).collect(Collectors.toList()); + return new IdContainsKeywordsPredicate(ids); + } + + return null; + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ShowCommand)) { + return false; + } + + // state check + ShowCommand toCompare = (ShowCommand) other; + boolean isIndexEquals = Objects.equals(index, toCompare.index); + boolean isNameEquals = Objects.equals(name, toCompare.name); + boolean isIdEquals = Objects.equals(id, toCompare.id); + boolean isAssessmentEquals = Objects.equals(assessment, toCompare.assessment); + boolean isGroupEquals = Objects.equals(group, toCompare.group); + boolean isSavePathEquals = Objects.equals(savePath, toCompare.savePath); + + return isIndexEquals && isNameEquals && isIdEquals + && isAssessmentEquals && isGroupEquals && isSavePathEquals; + } + + public void setIndex(Index index) { + this.index = index; + } + + public void setName(Name name) { + this.name = name; + } + + public void setId(Id id) { + this.id = id; + } + + public void setAssessment(Assessment assessment) { + this.assessment = assessment; + } + + public void setGroup(Group group) { + this.group = group; + } + + public void setSavePath(Path savePath) { + this.savePath = savePath; + } + + public Optional getIndex() { + return Optional.ofNullable(index); + } + + public Optional getName() { + return Optional.ofNullable(name); + } + + public Optional getId() { + return Optional.ofNullable(id); + } + + public Optional getAssessment() { + return Optional.ofNullable(assessment); + } + + public Optional getGroup() { + return Optional.ofNullable(group); + } + + public Optional getSavePath() { + return Optional.ofNullable(savePath); + } + + /** + * Stores info of a student or an assessment. + */ + public static class Info { + private Student student; + private Assessment assessment; + private Group group; + + public Info(Student student) { + setStudent(student); + } + + public Info(Assessment assessment) { + setAssessment(assessment); + } + + public Info(Group group) { + setGroup(group); + } + + public void setStudent(Student student) { + this.student = student; + } + + public void setAssessment(Assessment assessment) { + this.assessment = assessment; + } + + public void setGroup(Group group) { + this.group = group; + } + + public Optional getStudent() { + return Optional.ofNullable(student); + } + + public Optional getAssessment() { + return Optional.ofNullable(assessment); + } + + public Optional getGroup() { + return Optional.ofNullable(group); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Info)) { + return false; + } + + // state check + Info e = (Info) other; + + return getStudent().equals(e.getStudent()) + && getAssessment().equals(e.getAssessment()) + && getGroup().equals(e.getGroup()); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java b/src/main/java/seedu/sourcecontrol/logic/commands/exceptions/CommandException.java similarity index 88% rename from src/main/java/seedu/address/logic/commands/exceptions/CommandException.java rename to src/main/java/seedu/sourcecontrol/logic/commands/exceptions/CommandException.java index a16bd14f2cd..52687e784cb 100644 --- a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java +++ b/src/main/java/seedu/sourcecontrol/logic/commands/exceptions/CommandException.java @@ -1,4 +1,4 @@ -package seedu.address.logic.commands.exceptions; +package seedu.sourcecontrol.logic.commands.exceptions; /** * Represents an error which occurs during execution of a {@link Command}. diff --git a/src/main/java/seedu/sourcecontrol/logic/parser/AddAllocCommandParser.java b/src/main/java/seedu/sourcecontrol/logic/parser/AddAllocCommandParser.java new file mode 100644 index 00000000000..3791bd08961 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/parser/AddAllocCommandParser.java @@ -0,0 +1,56 @@ +package seedu.sourcecontrol.logic.parser; + +import static seedu.sourcecontrol.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_ID; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_NAME; + +import java.util.stream.Stream; + +import seedu.sourcecontrol.logic.commands.AddAllocCommand; +import seedu.sourcecontrol.logic.commands.AddAllocCommand.AllocDescriptor; +import seedu.sourcecontrol.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new AddAllocCommand object + */ +public class AddAllocCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddAllocCommand + * and returns an AddAllocCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddAllocCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_GROUP, PREFIX_NAME, PREFIX_ID); + + if (argMultimap.getValue(PREFIX_GROUP).isEmpty() + || (argMultimap.getValue(PREFIX_NAME).isEmpty() + && argMultimap.getValue(PREFIX_ID).isEmpty()) + || arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ID) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddAllocCommand.MESSAGE_USAGE)); + } + + AddAllocCommand.AllocDescriptor allocDescriptor = new AllocDescriptor(); + allocDescriptor.setGroup(ParserUtil.parseGroup(argMultimap.getValue(PREFIX_GROUP).get())); + if (argMultimap.getValue(PREFIX_NAME).isPresent()) { + allocDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); + } + if (argMultimap.getValue(PREFIX_ID).isPresent()) { + allocDescriptor.setId(ParserUtil.parseID(argMultimap.getValue(PREFIX_ID).get())); + } + + return new AddAllocCommand(allocDescriptor); + } + + /** + * 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/sourcecontrol/logic/parser/AddAssessmentCommandParser.java b/src/main/java/seedu/sourcecontrol/logic/parser/AddAssessmentCommandParser.java new file mode 100644 index 00000000000..3ea947f5143 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/parser/AddAssessmentCommandParser.java @@ -0,0 +1,34 @@ +package seedu.sourcecontrol.logic.parser; + +import static seedu.sourcecontrol.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_ASSESSMENT; + +import seedu.sourcecontrol.logic.commands.AddAssessmentCommand; +import seedu.sourcecontrol.logic.parser.exceptions.ParseException; +import seedu.sourcecontrol.model.student.assessment.Assessment; + + +/** + * Parses input arguments and creates a new AddStudentCommand object + */ +public class AddAssessmentCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddStudentCommand + * and returns an AddStudentCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddAssessmentCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_ASSESSMENT); + + if (argMultimap.getValue(PREFIX_ASSESSMENT).isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddAssessmentCommand.MESSAGE_USAGE)); + } + + Assessment assessment = ParserUtil.parseAssessment(argMultimap.getValue(PREFIX_ASSESSMENT).get()); + + return new AddAssessmentCommand(assessment); + } + +} diff --git a/src/main/java/seedu/sourcecontrol/logic/parser/AddGroupCommandParser.java b/src/main/java/seedu/sourcecontrol/logic/parser/AddGroupCommandParser.java new file mode 100644 index 00000000000..5ab6637d811 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/parser/AddGroupCommandParser.java @@ -0,0 +1,62 @@ +package seedu.sourcecontrol.logic.parser; + +import static seedu.sourcecontrol.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_ID; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_NAME; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import seedu.sourcecontrol.logic.commands.AddAllocCommand.AllocDescriptor; +import seedu.sourcecontrol.logic.commands.AddGroupCommand; +import seedu.sourcecontrol.logic.parser.exceptions.ParseException; +import seedu.sourcecontrol.model.student.group.Group; +import seedu.sourcecontrol.model.student.id.Id; +import seedu.sourcecontrol.model.student.name.Name; + +/** + * Parses input arguments and creates a new AddGroupCommand object + */ +public class AddGroupCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddGroupCommand + * and returns an AddGroupCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddGroupCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_GROUP, PREFIX_NAME, PREFIX_ID); + + if (!arePrefixesPresent(argMultimap, PREFIX_GROUP) || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddGroupCommand.MESSAGE_USAGE)); + } + + Group group = ParserUtil.parseGroup(argMultimap.getValue(PREFIX_GROUP).get()); + + List allocDescriptors = new ArrayList<>(); + + List names = ParserUtil.parseNames(argMultimap.getAllValues(PREFIX_NAME)); + for (Name name : names) { + allocDescriptors.add(new AllocDescriptor(group, name)); + + } + + List ids = ParserUtil.parseIds(argMultimap.getAllValues(PREFIX_ID)); + for (Id id : ids) { + allocDescriptors.add(new AllocDescriptor(group, id)); + } + + return new AddGroupCommand(group, allocDescriptors); + } + + /** + * 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/sourcecontrol/logic/parser/AddScoreCommandParser.java b/src/main/java/seedu/sourcecontrol/logic/parser/AddScoreCommandParser.java new file mode 100644 index 00000000000..3d55e765fa1 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/parser/AddScoreCommandParser.java @@ -0,0 +1,59 @@ +package seedu.sourcecontrol.logic.parser; + +import static seedu.sourcecontrol.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_ASSESSMENT; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_ID; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_SCORE; + +import java.util.stream.Stream; + +import seedu.sourcecontrol.logic.commands.AddScoreCommand; +import seedu.sourcecontrol.logic.commands.AddScoreCommand.ScoreDescriptor; +import seedu.sourcecontrol.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new AddAllocCommand object + */ +public class AddScoreCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddAllocCommand + * and returns an AddAllocCommand 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_ASSESSMENT, PREFIX_NAME, PREFIX_ID, PREFIX_SCORE); + + if (argMultimap.getValue(PREFIX_ASSESSMENT).isEmpty() + || argMultimap.getValue(PREFIX_SCORE).isEmpty() + || (argMultimap.getValue(PREFIX_NAME).isEmpty() + && argMultimap.getValue(PREFIX_ID).isEmpty()) + || arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ID) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddScoreCommand.MESSAGE_USAGE)); + } + + AddScoreCommand.ScoreDescriptor scoreDescriptor = new ScoreDescriptor(); + scoreDescriptor.setAssessment(ParserUtil.parseAssessment(argMultimap.getValue(PREFIX_ASSESSMENT).get())); + if (argMultimap.getValue(PREFIX_NAME).isPresent()) { + scoreDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); + } + if (argMultimap.getValue(PREFIX_ID).isPresent()) { + scoreDescriptor.setId(ParserUtil.parseID(argMultimap.getValue(PREFIX_ID).get())); + } + scoreDescriptor.setScore(ParserUtil.parseScore(argMultimap.getValue(PREFIX_SCORE).get())); + + return new AddScoreCommand(scoreDescriptor); + } + + /** + * 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/sourcecontrol/logic/parser/AddStudentCommandParser.java b/src/main/java/seedu/sourcecontrol/logic/parser/AddStudentCommandParser.java new file mode 100644 index 00000000000..64bda1230f9 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/parser/AddStudentCommandParser.java @@ -0,0 +1,69 @@ +package seedu.sourcecontrol.logic.parser; + +import static seedu.sourcecontrol.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_ID; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import seedu.sourcecontrol.logic.commands.AddStudentCommand; +import seedu.sourcecontrol.logic.parser.exceptions.ParseException; +import seedu.sourcecontrol.model.student.Student; +import seedu.sourcecontrol.model.student.assessment.Assessment; +import seedu.sourcecontrol.model.student.assessment.Score; +import seedu.sourcecontrol.model.student.group.Group; +import seedu.sourcecontrol.model.student.id.Id; +import seedu.sourcecontrol.model.student.name.Name; +import seedu.sourcecontrol.model.student.tag.Tag; + +/** + * Parses input arguments and creates a new AddStudentCommand object + */ +public class AddStudentCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddStudentCommand + * and returns an AddStudentCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddStudentCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_ID, PREFIX_GROUP, PREFIX_TAG); + + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ID) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddStudentCommand.MESSAGE_USAGE)); + } + + Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); + Id id = ParserUtil.parseID(argMultimap.getValue(PREFIX_ID).get()); + + List groupList = ParserUtil.parseGroups(argMultimap.getAllValues(PREFIX_GROUP)); + groupList = (groupList != null) + ? groupList.stream().distinct().collect(Collectors.toList()) + : null; + Map emptyScores = new LinkedHashMap<>(); + Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); + + Student student = new Student(name, id, groupList, emptyScores, tagList); + + + return new AddStudentCommand(student); + } + + /** + * 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/sourcecontrol/logic/parser/Alias.java b/src/main/java/seedu/sourcecontrol/logic/parser/Alias.java new file mode 100644 index 00000000000..00f693bd99d --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/parser/Alias.java @@ -0,0 +1,61 @@ +package seedu.sourcecontrol.logic.parser; + +import static seedu.sourcecontrol.commons.util.AppUtil.checkArgument; +import static seedu.sourcecontrol.commons.util.CollectionUtil.requireAllNonNull; + +public class Alias { + public static final String MESSAGE_CONSTRAINTS = + "Aliases should only be one word comprised of only alphanumeric characters, and it should not be blank"; + public static final String VALIDATION_REGEX = "[\\p{Alnum}]+"; + + private final String commandWord; + private final String aliasWord; + + /** + * Creates an Alias with the given aliasWord and commandWord. + */ + public Alias(String aliasWord, String commandWord) { + requireAllNonNull(aliasWord, commandWord); + checkArgument(isValidAlias(aliasWord), MESSAGE_CONSTRAINTS); + this.commandWord = commandWord; + this.aliasWord = aliasWord; + } + + /** + * Checks if the provided aliasWord matches the given constraints. + */ + public static boolean isValidAlias(String aliasWord) { + return aliasWord.matches(VALIDATION_REGEX); + } + + /** + * Replaces the first word of the userInput with the commandWord. + */ + public String replaceFirst(String userInput) { + assert userInput.startsWith(aliasWord); + return userInput.replaceFirst(aliasWord, commandWord); + } + + /** + * Returns true if the alias does nothing (i.e. aliasWord maps to itself) + */ + public boolean isRedundant() { + return commandWord.equals(aliasWord); + } + + public String getAliasWord() { + return aliasWord; + } + + public String getCommandWord() { + return commandWord; + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof Alias + && aliasWord.equals(((Alias) other).aliasWord) + && commandWord.equals(((Alias) other).commandWord)); + } +} diff --git a/src/main/java/seedu/sourcecontrol/logic/parser/AliasCommandParser.java b/src/main/java/seedu/sourcecontrol/logic/parser/AliasCommandParser.java new file mode 100644 index 00000000000..53b4c8c1435 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/parser/AliasCommandParser.java @@ -0,0 +1,98 @@ +package seedu.sourcecontrol.logic.parser; + +import static seedu.sourcecontrol.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_ALIAS; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_COMMAND; + +import java.util.Optional; + +import seedu.sourcecontrol.logic.commands.AliasCommand; +import seedu.sourcecontrol.logic.parser.exceptions.ParseException; + +public class AliasCommandParser implements Parser { + private final SourceControlParser parser; + + public AliasCommandParser(SourceControlParser parser) { + this.parser = parser; + } + + @Override + public AliasCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_ALIAS, PREFIX_COMMAND); + + if (argMultimap.getValue(PREFIX_ALIAS).isEmpty() + || argMultimap.getValue(PREFIX_COMMAND).isEmpty()) { + throw new ParseException(AliasCommand.MESSAGE_USAGE); + } + + String aliasWord = argMultimap.getValue(PREFIX_ALIAS).get().strip(); + String commandWord = argMultimap.getValue(PREFIX_COMMAND).get().strip(); + + checkAliasWord(aliasWord, parser); + checkCommandWord(commandWord, parser); + + // short circuit if removing existing alias + if (aliasWord.equals(commandWord)) { + Alias newAlias = new Alias(aliasWord, commandWord); + return new AliasCommand(newAlias, parser); + } + + commandWord = flattenCommandWord(commandWord, parser); + + Alias newAlias = new Alias(aliasWord, commandWord); + return new AliasCommand(newAlias, parser); + } + + /** + * Checks if the provided commandWord is a single word, and that the parser recognises it as a command word. + */ + private static boolean isValidCommandWord(String commandWord, SourceControlParser parser) { + if (commandWord.isEmpty() || commandWord.contains(" ")) { + return false; + } + + try { + parser.parseCommand(commandWord); + } catch (ParseException e) { + if (e.getMessage().equals(MESSAGE_UNKNOWN_COMMAND)) { + return false; + } + } + + return true; + } + + /** + * Verifies if the alias word can make a valid alias for the provided parser. + * Checks that the alias is a single alphanumeric word, and that it is not a default command. + */ + public static void checkAliasWord(String aliasWord, SourceControlParser parser) throws ParseException { + if (!Alias.isValidAlias(aliasWord)) { + throw new ParseException(Alias.MESSAGE_CONSTRAINTS); + } + + // prevents trying to alias a default command + if (isValidCommandWord(aliasWord, parser) && parser.getAlias(aliasWord).isEmpty()) { + throw new ParseException(AliasCommand.MESSAGE_OVERWRITE_DEFAULT); + } + } + + /** + * Verifies if the command word can be parsed by the + */ + public static void checkCommandWord(String commandWord, SourceControlParser parser) throws ParseException { + if (!isValidCommandWord(commandWord, parser)) { + throw new ParseException(AliasCommand.MESSAGE_UNKNOWN_OLD_COMMAND); + } + } + + /** + * Prevents the chaining of aliases. + * If there is an existing alias "bye" for "exit", and "bye" is the command word, it will be flattened to "exit". + */ + public static String flattenCommandWord(String commandWord, SourceControlParser parser) { + Optional existingAlias = parser.getAlias(commandWord); + return existingAlias.map(Alias::getCommandWord).orElse(commandWord); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java b/src/main/java/seedu/sourcecontrol/logic/parser/ArgumentMultimap.java similarity index 98% rename from src/main/java/seedu/address/logic/parser/ArgumentMultimap.java rename to src/main/java/seedu/sourcecontrol/logic/parser/ArgumentMultimap.java index 954c8e18f8e..034c48ab469 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java +++ b/src/main/java/seedu/sourcecontrol/logic/parser/ArgumentMultimap.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package seedu.sourcecontrol.logic.parser; import java.util.ArrayList; import java.util.HashMap; diff --git a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java b/src/main/java/seedu/sourcecontrol/logic/parser/ArgumentTokenizer.java similarity index 99% rename from src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java rename to src/main/java/seedu/sourcecontrol/logic/parser/ArgumentTokenizer.java index 5c9aebfa488..21d66e476cf 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java +++ b/src/main/java/seedu/sourcecontrol/logic/parser/ArgumentTokenizer.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package seedu.sourcecontrol.logic.parser; import java.util.ArrayList; import java.util.Arrays; diff --git a/src/main/java/seedu/sourcecontrol/logic/parser/CliSyntax.java b/src/main/java/seedu/sourcecontrol/logic/parser/CliSyntax.java new file mode 100644 index 00000000000..a7ae7514e04 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/parser/CliSyntax.java @@ -0,0 +1,19 @@ +package seedu.sourcecontrol.logic.parser; + +/** + * Contains Command Line Interface (CLI) syntax definitions common to multiple commands + */ +public class CliSyntax { + + /* Prefix definitions */ + public static final Prefix PREFIX_NAME = new Prefix("-n "); + public static final Prefix PREFIX_ID = new Prefix("-i "); + public static final Prefix PREFIX_GROUP = new Prefix("-g "); + public static final Prefix PREFIX_ASSESSMENT = new Prefix("-a "); + public static final Prefix PREFIX_SCORE = new Prefix("-s "); + public static final Prefix PREFIX_TAG = new Prefix("-t "); + public static final Prefix PREFIX_FILE = new Prefix("-f "); + public static final Prefix PREFIX_ALIAS = new Prefix("-as "); + public static final Prefix PREFIX_COMMAND = new Prefix("-c "); + +} diff --git a/src/main/java/seedu/sourcecontrol/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/sourcecontrol/logic/parser/DeleteCommandParser.java new file mode 100644 index 00000000000..7c513437040 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/parser/DeleteCommandParser.java @@ -0,0 +1,49 @@ +package seedu.sourcecontrol.logic.parser; + +import static seedu.sourcecontrol.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.stream.Stream; + +import seedu.sourcecontrol.commons.core.index.Index; +import seedu.sourcecontrol.logic.commands.DeleteCommand; +import seedu.sourcecontrol.logic.parser.exceptions.ParseException; + + +/** + * Parses input arguments and creates a new DeleteCommand object + */ +public class DeleteCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteCommand + * and returns a DeleteCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteCommand parse(String args) throws ParseException { + Prefix[] prefixes = new Prefix[]{ + CliSyntax.PREFIX_NAME, CliSyntax.PREFIX_ID, CliSyntax.PREFIX_GROUP, + CliSyntax.PREFIX_ASSESSMENT, CliSyntax.PREFIX_SCORE, CliSyntax.PREFIX_TAG, + CliSyntax.PREFIX_FILE, CliSyntax.PREFIX_ALIAS, CliSyntax.PREFIX_COMMAND}; + + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, prefixes); + + if (argMultimap.getPreamble().isEmpty() || !isNoPrefixPresent(argMultimap, prefixes)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); + } + + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteCommand(index); + } catch (ParseException pe) { + throw new ParseException(ParserUtil.MESSAGE_INVALID_INDEX); + } + } + + /** + * Returns true if none of the prefixes present in the given {@code ArgumentMultimap}. + */ + private static boolean isNoPrefixPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isEmpty()); + } + +} diff --git a/src/main/java/seedu/sourcecontrol/logic/parser/EditCommandParser.java b/src/main/java/seedu/sourcecontrol/logic/parser/EditCommandParser.java new file mode 100644 index 00000000000..8ae1b31ae04 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/parser/EditCommandParser.java @@ -0,0 +1,97 @@ +package seedu.sourcecontrol.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.sourcecontrol.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_ID; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import seedu.sourcecontrol.commons.core.index.Index; +import seedu.sourcecontrol.logic.commands.EditCommand; +import seedu.sourcecontrol.logic.commands.EditCommand.EditStudentDescriptor; +import seedu.sourcecontrol.logic.parser.exceptions.ParseException; +import seedu.sourcecontrol.model.student.group.Group; +import seedu.sourcecontrol.model.student.tag.Tag; + +/** + * Parses input arguments and creates a new EditCommand object + */ +public class EditCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the EditCommand + * and returns an EditCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public EditCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_ID, PREFIX_GROUP, PREFIX_TAG); + + if (argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE)); + } + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(ParserUtil.MESSAGE_INVALID_INDEX); + } + + EditCommand.EditStudentDescriptor editStudentDescriptor = new EditStudentDescriptor(); + if (argMultimap.getValue(PREFIX_NAME).isPresent()) { + editStudentDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); + } + if (argMultimap.getValue(PREFIX_ID).isPresent()) { + editStudentDescriptor.setId(ParserUtil.parseID(argMultimap.getValue(PREFIX_ID).get())); + } + parseGroupsForEdit(argMultimap.getAllValues(PREFIX_GROUP)).ifPresent(editStudentDescriptor::setGroups); + parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editStudentDescriptor::setTags); + + if (!editStudentDescriptor.isAnyFieldEdited()) { + throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); + } + + return new EditCommand(index, editStudentDescriptor); + } + + /** + * Parses {@code Collection groups} into a {@code List} if {@code groups} is non-empty. + * If {@code groups} contain only one element which is an empty string, it will be parsed into a + * {@code List} containing zero groups. + */ + private Optional> parseGroupsForEdit(Collection groups) throws ParseException { + assert groups != null; + + if (groups.isEmpty()) { + return Optional.empty(); + } + Collection groupList = groups.size() == 1 && groups.contains("") ? Collections.emptyList() : groups; + return Optional.of(ParserUtil.parseGroups(groupList)); + } + + /** + * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. + * If {@code tags} contain only one element which is an empty string, it will be parsed into a + * {@code Set} containing zero tags. + */ + private Optional> parseTagsForEdit(Collection tags) throws ParseException { + assert tags != null; + + if (tags.isEmpty()) { + return Optional.empty(); + } + Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; + return Optional.of(ParserUtil.parseTags(tagSet)); + } + +} diff --git a/src/main/java/seedu/sourcecontrol/logic/parser/ImportCommandParser.java b/src/main/java/seedu/sourcecontrol/logic/parser/ImportCommandParser.java new file mode 100644 index 00000000000..7cc0339ba9f --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/parser/ImportCommandParser.java @@ -0,0 +1,46 @@ +package seedu.sourcecontrol.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.sourcecontrol.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_ASSESSMENT; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_FILE; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_TAG; + +import java.nio.file.Path; + +import seedu.sourcecontrol.logic.commands.ImportCommand; +import seedu.sourcecontrol.logic.parser.exceptions.ParseException; + +public class ImportCommandParser implements Parser { + @Override + public ImportCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_FILE, PREFIX_GROUP, PREFIX_ASSESSMENT, PREFIX_TAG); + + if (argMultimap.getValue(PREFIX_FILE).isEmpty() || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ImportCommand.MESSAGE_USAGE)); + } + + Path filePath = ParserUtil.parsePath(argMultimap.getValue(PREFIX_FILE).get(), ".csv"); + + int groupCount = getCount(argMultimap, PREFIX_GROUP); + int assessmentCount = getCount(argMultimap, PREFIX_ASSESSMENT); + int tagCount = getCount(argMultimap, PREFIX_TAG); + + return new ImportCommand(groupCount, assessmentCount, tagCount, filePath); + } + + private int getCount(ArgumentMultimap argumentMultimap, Prefix prefix) throws ParseException { + try { + int count = argumentMultimap.getValue(prefix).map(String::trim).map(Integer::parseInt).orElse(0); + if (count < 0) { + throw new NumberFormatException(); + } + return count; + } catch (NumberFormatException e) { + throw new ParseException(ImportCommand.MESSAGE_INVALID_NUMBER); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/Parser.java b/src/main/java/seedu/sourcecontrol/logic/parser/Parser.java similarity index 70% rename from src/main/java/seedu/address/logic/parser/Parser.java rename to src/main/java/seedu/sourcecontrol/logic/parser/Parser.java index d6551ad8e3f..b44a6a8d1c5 100644 --- a/src/main/java/seedu/address/logic/parser/Parser.java +++ b/src/main/java/seedu/sourcecontrol/logic/parser/Parser.java @@ -1,7 +1,7 @@ -package seedu.address.logic.parser; +package seedu.sourcecontrol.logic.parser; -import seedu.address.logic.commands.Command; -import seedu.address.logic.parser.exceptions.ParseException; +import seedu.sourcecontrol.logic.commands.Command; +import seedu.sourcecontrol.logic.parser.exceptions.ParseException; /** * Represents a Parser that is able to parse user input into a {@code Command} of type {@code T}. diff --git a/src/main/java/seedu/sourcecontrol/logic/parser/ParserUtil.java b/src/main/java/seedu/sourcecontrol/logic/parser/ParserUtil.java new file mode 100644 index 00000000000..6691bdaa702 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/parser/ParserUtil.java @@ -0,0 +1,210 @@ +package seedu.sourcecontrol.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.sourcecontrol.commons.util.CollectionUtil.requireAllNonNull; + +import java.io.File; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import seedu.sourcecontrol.commons.core.index.Index; +import seedu.sourcecontrol.commons.util.FileUtil; +import seedu.sourcecontrol.commons.util.StringUtil; +import seedu.sourcecontrol.logic.parser.exceptions.ParseException; +import seedu.sourcecontrol.model.student.assessment.Assessment; +import seedu.sourcecontrol.model.student.assessment.Score; +import seedu.sourcecontrol.model.student.group.Group; +import seedu.sourcecontrol.model.student.id.Id; +import seedu.sourcecontrol.model.student.name.Name; +import seedu.sourcecontrol.model.student.tag.Tag; + +/** + * Contains utility methods used for parsing strings in the various *Parser classes. + */ +public class ParserUtil { + + public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer. "; + public static final String MESSAGE_EMPTY_PATH = "Path should not be empty. "; + public static final String MESSAGE_INVALID_PATH = "Path provided is invalid. "; + public static final String MESSAGE_DIRECTORY = "Path provided should not be for a directory. "; + public static final String MESSAGE_WRONG_EXTENSION = "Path provided should end with %1$s. "; + + /** + * Parses {@code oneBasedIndex} into an {@code Index} and returns it. + * Leading and trailing whitespaces will be trimmed. + * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). + */ + public static Index parseIndex(String oneBasedIndex) throws ParseException { + String trimmedIndex = oneBasedIndex.trim(); + if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) { + throw new ParseException(MESSAGE_INVALID_INDEX); + } + return Index.fromOneBased(Integer.parseInt(trimmedIndex)); + } + + /** + * Parses a {@code String name} into a {@code Name}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code name} is invalid. + */ + public static Name parseName(String name) throws ParseException { + requireNonNull(name); + String trimmedName = name.trim(); + if (!Name.isValidName(trimmedName)) { + throw new ParseException(Name.MESSAGE_CONSTRAINTS); + } + return new Name(trimmedName); + } + + /** + * Parses {@code Collection names} into a {@code List}. + */ + public static List parseNames(Collection names) throws ParseException { + requireNonNull(names); + final List nameList = new ArrayList<>(); + for (String name : names) { + nameList.add(parseName(name)); + } + return nameList; + } + + /** + * Parses a {@code String score} into a {@code Score}. + * 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.isValidScore(trimmedScore)) { + throw new ParseException(Score.MESSAGE_CONSTRAINTS); + } + return new Score(trimmedScore); + } + + /** + * Parses a {@code String assessment} into an {@code Assessment}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code assessment} is invalid. + */ + public static Assessment parseAssessment(String assessment) throws ParseException { + requireNonNull(assessment); + String trimmedAssessment = assessment.trim(); + if (!Assessment.isValidAssessment(trimmedAssessment)) { + throw new ParseException(Assessment.MESSAGE_CONSTRAINTS); + } + return new Assessment(trimmedAssessment); + } + + /** + * Parses a {@code String Id} into an {@code Id}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code Id} is invalid. + */ + public static Id parseID(String id) throws ParseException { + requireNonNull(id); + String trimmedID = id.trim(); + if (!Id.isValidID(trimmedID)) { + throw new ParseException(Id.MESSAGE_CONSTRAINTS); + } + return new Id(trimmedID); + } + + /** + * Parses {@code Collection ids} into a {@code List}. + */ + public static List parseIds(Collection ids) throws ParseException { + requireNonNull(ids); + final List idList = new ArrayList<>(); + for (String id : ids) { + idList.add(parseID(id)); + } + return idList; + } + + /** + * Parses a {@code String Group} into an {@code Group}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code Group} is invalid. + */ + public static Group parseGroup(String group) throws ParseException { + requireNonNull(group); + String trimmedGroup = group.trim(); + if (!Group.isValidGroup(trimmedGroup)) { + throw new ParseException(Group.MESSAGE_CONSTRAINTS); + } + return new Group(trimmedGroup); + } + + /** + * Parses {@code Collection groups} into a {@code List}. + */ + public static List parseGroups(Collection groups) throws ParseException { + requireNonNull(groups); + final List groupList = new ArrayList<>(); + for (String groupName : groups) { + groupList.add(parseGroup(groupName)); + } + return groupList; + } + + /** + * Parses a {@code String tag} into a {@code Tag}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code tag} is invalid. + */ + public static Tag parseTag(String tag) throws ParseException { + requireNonNull(tag); + String trimmedTag = tag.trim(); + if (!Tag.isValidTagName(trimmedTag)) { + throw new ParseException(Tag.MESSAGE_CONSTRAINTS); + } + return new Tag(trimmedTag); + } + + /** + * Parses {@code Collection tags} into a {@code Set}. + */ + public static Set parseTags(Collection tags) throws ParseException { + requireNonNull(tags); + final Set tagSet = new HashSet<>(); + for (String tagName : tags) { + tagSet.add(parseTag(tagName)); + } + return tagSet; + } + + /** + * Parses a given path into a path relative to the app's folder, and ensures that the path has the right extension. + */ + public static Path parsePath(String path, String extension) throws ParseException { + requireAllNonNull(path, extension); + if (path.equals("")) { + throw new ParseException(MESSAGE_EMPTY_PATH); + } + + if (!FileUtil.isValidPath(path)) { + throw new ParseException(MESSAGE_INVALID_PATH); + } + + if (new File(path).isDirectory()) { + throw new ParseException(MESSAGE_DIRECTORY); + } + + if (!path.endsWith(extension)) { + throw new ParseException(String.format(MESSAGE_WRONG_EXTENSION, extension)); + } + + return FileUtil.pathOf(path); + } +} diff --git a/src/main/java/seedu/address/logic/parser/Prefix.java b/src/main/java/seedu/sourcecontrol/logic/parser/Prefix.java similarity index 90% rename from src/main/java/seedu/address/logic/parser/Prefix.java rename to src/main/java/seedu/sourcecontrol/logic/parser/Prefix.java index c859d5fa5db..0b5a3e806e6 100644 --- a/src/main/java/seedu/address/logic/parser/Prefix.java +++ b/src/main/java/seedu/sourcecontrol/logic/parser/Prefix.java @@ -1,8 +1,8 @@ -package seedu.address.logic.parser; +package seedu.sourcecontrol.logic.parser; /** * A prefix that marks the beginning of an argument in an arguments string. - * E.g. 't/' in 'add James t/ friend'. + * E.g. '-t ' in 'add James -t friend'. */ public class Prefix { private final String prefix; diff --git a/src/main/java/seedu/sourcecontrol/logic/parser/SearchCommandParser.java b/src/main/java/seedu/sourcecontrol/logic/parser/SearchCommandParser.java new file mode 100644 index 00000000000..83e8992744f --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/parser/SearchCommandParser.java @@ -0,0 +1,106 @@ +package seedu.sourcecontrol.logic.parser; + +import static seedu.sourcecontrol.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_ID; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.ArrayList; +import java.util.List; + +import seedu.sourcecontrol.logic.commands.SearchCommand; +import seedu.sourcecontrol.logic.parser.exceptions.ParseException; +import seedu.sourcecontrol.model.student.group.GroupContainsKeywordsPredicate; +import seedu.sourcecontrol.model.student.id.IdContainsKeywordsPredicate; +import seedu.sourcecontrol.model.student.name.NameContainsKeywordsPredicate; +import seedu.sourcecontrol.model.student.tag.TagContainsKeywordsPredicate; + +/** + * Parses input arguments and creates a new SearchCommand object + */ +public class SearchCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the SearchCommand + * and returns a SearchCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public SearchCommand parse(String args) throws ParseException { + + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_ID, PREFIX_GROUP, PREFIX_TAG); + + // catch case of empty input + if (!argMultimap.getPreamble().isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SearchCommand.MESSAGE_USAGE)); + } + + int count = 0; + String searchType = ""; + String prefix = ""; + + // identify search input type + if (!argMultimap.getValue(PREFIX_NAME).isEmpty()) { + searchType = "NAME"; + prefix = " -n "; + count++; + } + + if (!argMultimap.getValue(PREFIX_ID).isEmpty()) { + searchType = "ID"; + prefix = " -i "; + count++; + } + + if (!argMultimap.getValue(PREFIX_GROUP).isEmpty()) { + searchType = "GROUP"; + prefix = " -g "; + count++; + } + + if (!argMultimap.getValue(PREFIX_TAG).isEmpty()) { + searchType = "TAG"; + prefix = " -t "; + count++; + } + + // catch case of more than one search input type + if (count != 1) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SearchCommand.MESSAGE_USAGE)); + } + + int prefixIndex = args.indexOf(prefix, 0); + String searchTerm = args.substring(prefixIndex + 4).trim(); + + // separate all search inputs by spaces + String[] keywords = searchTerm.split("\\s+"); + List keywordsList = new ArrayList<>(); + for (String keyword : keywords) { + if (!keyword.isEmpty()) { + keywordsList.add(keyword); + } + } + + // execute according to search input type + switch(searchType) { + case "NAME": + return new SearchCommand(new NameContainsKeywordsPredicate(keywordsList)); + + case "ID": + return new SearchCommand(new IdContainsKeywordsPredicate(keywordsList)); + + case "GROUP": + return new SearchCommand(new GroupContainsKeywordsPredicate(keywordsList)); + + case "TAG": + return new SearchCommand(new TagContainsKeywordsPredicate(keywordsList)); + + default: + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SearchCommand.MESSAGE_USAGE)); + } + } +} diff --git a/src/main/java/seedu/sourcecontrol/logic/parser/ShowCommandParser.java b/src/main/java/seedu/sourcecontrol/logic/parser/ShowCommandParser.java new file mode 100644 index 00000000000..55a29a9b61b --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/parser/ShowCommandParser.java @@ -0,0 +1,107 @@ +package seedu.sourcecontrol.logic.parser; + +import static seedu.sourcecontrol.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_ASSESSMENT; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_FILE; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_ID; +import static seedu.sourcecontrol.logic.parser.CliSyntax.PREFIX_NAME; + +import java.nio.file.Path; +import java.util.stream.Stream; + +import seedu.sourcecontrol.commons.core.index.Index; +import seedu.sourcecontrol.commons.util.FileUtil; +import seedu.sourcecontrol.logic.commands.ShowCommand; +import seedu.sourcecontrol.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ShowCommand object + */ +public class ShowCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the ShowCommand + * and returns an ShowCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ShowCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_ID, PREFIX_ASSESSMENT, PREFIX_GROUP, PREFIX_FILE); + + Path savePath = null; + if (argMultimap.getValue(PREFIX_FILE).isPresent()) { + savePath = generateNewPath(0); + } + + return argMultimap.getPreamble().isEmpty() + ? parseByPrefixes(argMultimap, savePath) + : parseByIndex(argMultimap, savePath); + } + + /** + * Handles parsing by {@code Index}. + */ + public ShowCommand parseByIndex(ArgumentMultimap argMultimap, Path savePath) throws ParseException { + Index index; + + if (!isNoPrefixPresent(argMultimap, PREFIX_NAME, PREFIX_ID, PREFIX_ASSESSMENT, PREFIX_GROUP)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ShowCommand.MESSAGE_USAGE)); + } + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(ParserUtil.MESSAGE_INVALID_INDEX); + } + + return new ShowCommand(index, savePath); + } + + /** + * Handles parsing by prefixes. + */ + public ShowCommand parseByPrefixes(ArgumentMultimap argMultimap, Path savePath) throws ParseException { + if (isInvalidPrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ID, PREFIX_ASSESSMENT, PREFIX_GROUP)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ShowCommand.MESSAGE_USAGE)); + } + + return argMultimap.getValue(PREFIX_NAME).isPresent() + ? new ShowCommand(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()), savePath) + : argMultimap.getValue(PREFIX_ID).isPresent() + ? new ShowCommand(ParserUtil.parseID(argMultimap.getValue(PREFIX_ID).get()), savePath) + : argMultimap.getValue(PREFIX_ASSESSMENT).isPresent() + ? new ShowCommand(ParserUtil.parseAssessment(argMultimap.getValue(PREFIX_ASSESSMENT).get()), savePath) + : new ShowCommand(ParserUtil.parseGroup(argMultimap.getValue(PREFIX_GROUP).get()), savePath); + } + + /** + * Returns true if only one of the prefixes is present in the given {@code ArgumentMultimap}, + * i.e. false if there are multiple prefixes or no prefix present. + */ + private static boolean isInvalidPrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).filter(prefix -> argumentMultimap.getValue(prefix).isPresent()).count() != 1; + } + + /** + * Generates a path to save the graph. Ensures that the graph saved does not overwrite any existing file. + * Default path is ./graph.png. + */ + public Path generateNewPath(int tries) { + String pathString = String.format(ShowCommand.BASE_PATH, tries == 0 ? "" : "(" + tries + ")"); + Path path = FileUtil.pathOf(pathString); + if (FileUtil.isFileExists(path)) { + return generateNewPath(tries + 1); + } else { + return path; + } + } + + /** + * Returns true if none of the prefixes present in the given {@code ArgumentMultimap}. + */ + private static boolean isNoPrefixPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isEmpty()); + } + +} diff --git a/src/main/java/seedu/sourcecontrol/logic/parser/SourceControlParser.java b/src/main/java/seedu/sourcecontrol/logic/parser/SourceControlParser.java new file mode 100644 index 00000000000..0bf91c4bdcd --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/logic/parser/SourceControlParser.java @@ -0,0 +1,169 @@ +package seedu.sourcecontrol.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.sourcecontrol.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.sourcecontrol.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; +import static seedu.sourcecontrol.commons.util.CollectionUtil.equalsIgnoreOrder; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import seedu.sourcecontrol.logic.commands.AddAllocCommand; +import seedu.sourcecontrol.logic.commands.AddAssessmentCommand; +import seedu.sourcecontrol.logic.commands.AddGroupCommand; +import seedu.sourcecontrol.logic.commands.AddScoreCommand; +import seedu.sourcecontrol.logic.commands.AddStudentCommand; +import seedu.sourcecontrol.logic.commands.AliasCommand; +import seedu.sourcecontrol.logic.commands.ClearCommand; +import seedu.sourcecontrol.logic.commands.Command; +import seedu.sourcecontrol.logic.commands.DeleteCommand; +import seedu.sourcecontrol.logic.commands.EditCommand; +import seedu.sourcecontrol.logic.commands.ExitCommand; +import seedu.sourcecontrol.logic.commands.ExportCommand; +import seedu.sourcecontrol.logic.commands.HelpCommand; +import seedu.sourcecontrol.logic.commands.ImportCommand; +import seedu.sourcecontrol.logic.commands.ListCommand; +import seedu.sourcecontrol.logic.commands.SearchCommand; +import seedu.sourcecontrol.logic.commands.ShowCommand; +import seedu.sourcecontrol.logic.parser.exceptions.ParseException; + +/** + * Parses user input. + */ +public class SourceControlParser { + /** + * Used for initial separation of command word and args. + */ + private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); + private final List aliases = new ArrayList<>(); + + /** + * Parses user input into command for execution. + * + * @param userInput full user input string + * @return the command based on the user input + * @throws ParseException if the user input does not conform the expected format + */ + public Command parseCommand(String userInput) throws ParseException { + requireNonNull(userInput); + final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); + if (!matcher.matches()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); + } + + String commandWord = matcher.group("commandWord"); + String arguments = matcher.group("arguments"); + + // if it ends with a prefix, add a space so that the prefix can be picked up by the tokenizer + if (arguments.matches(".* -[a-z]+$")) { + arguments = arguments + " "; + } + + switch (commandWord) { + + case EditCommand.COMMAND_WORD: + return new EditCommandParser().parse(arguments); + + case DeleteCommand.COMMAND_WORD: + return new DeleteCommandParser().parse(arguments); + + case ClearCommand.COMMAND_WORD: + return new ClearCommand(); + + case SearchCommand.COMMAND_WORD: + return new SearchCommandParser().parse(arguments); + + case ShowCommand.COMMAND_WORD: + return new ShowCommandParser().parse(arguments); + + case ListCommand.COMMAND_WORD: + return new ListCommand(); + + case ExitCommand.COMMAND_WORD: + return new ExitCommand(); + + case HelpCommand.COMMAND_WORD: + return new HelpCommand(); + + case ImportCommand.COMMAND_WORD: + return new ImportCommandParser().parse(arguments); + + case ExportCommand.COMMAND_WORD: + return new ExportCommand(); + + case AliasCommand.COMMAND_WORD: + return new AliasCommandParser(this).parse(arguments); + + case AddGroupCommand.COMMAND_WORD: + return new AddGroupCommandParser().parse(arguments); + + case AddAllocCommand.COMMAND_WORD: + return new AddAllocCommandParser().parse(arguments); + + case AddAssessmentCommand.COMMAND_WORD: + return new AddAssessmentCommandParser().parse(arguments); + + case AddStudentCommand.COMMAND_WORD: + return new AddStudentCommandParser().parse(arguments); + + case AddScoreCommand.COMMAND_WORD: + return new AddScoreCommandParser().parse(arguments); + + default: + return parseAliases(userInput); + } + } + + /** + * Adds or replaces the alias in the parser's list of aliases. + */ + public void addAlias(Alias alias) { + requireNonNull(alias); + removeAlias(alias.getAliasWord()); + aliases.add(alias); + } + + /** + * Removes the provided alias from the parser's list of aliases. + */ + public void removeAlias(String aliasWord) { + requireNonNull(aliasWord); + Optional existing = getAlias(aliasWord); + existing.ifPresent(aliases::remove); + } + + /** + * Returns an optional containing the alias with that alias word, or an empty optional if no alias matches. + */ + public Optional getAlias(String aliasWord) { + return aliases.stream() + .filter(a -> a.getAliasWord().equals(aliasWord)) + .findFirst(); + } + + /** + * Tries to match the user input to an alias. Throws ParseException if none match. + */ + private Command parseAliases(String userInput) throws ParseException { + // matches the first word to the alias + Optional firstAlias = aliases.stream() + .filter(alias -> userInput.split(" ")[0].equals(alias.getAliasWord())) + .findFirst(); + + Alias alias = firstAlias.orElseThrow(() -> new ParseException(MESSAGE_UNKNOWN_COMMAND)); + + // replace the command of the user input, then try to parse again + String newUserInput = alias.replaceFirst(userInput); + return parseCommand(newUserInput); + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof SourceControlParser + && equalsIgnoreOrder(aliases, ((SourceControlParser) other).aliases)); + } +} diff --git a/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java b/src/main/java/seedu/sourcecontrol/logic/parser/exceptions/ParseException.java similarity index 71% rename from src/main/java/seedu/address/logic/parser/exceptions/ParseException.java rename to src/main/java/seedu/sourcecontrol/logic/parser/exceptions/ParseException.java index 158a1a54c1c..91d75ede4c3 100644 --- a/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java +++ b/src/main/java/seedu/sourcecontrol/logic/parser/exceptions/ParseException.java @@ -1,6 +1,6 @@ -package seedu.address.logic.parser.exceptions; +package seedu.sourcecontrol.logic.parser.exceptions; -import seedu.address.commons.exceptions.IllegalValueException; +import seedu.sourcecontrol.commons.exceptions.IllegalValueException; /** * Represents a parse error encountered by a parser. diff --git a/src/main/java/seedu/sourcecontrol/model/Model.java b/src/main/java/seedu/sourcecontrol/model/Model.java new file mode 100644 index 00000000000..523ce38cf6b --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/Model.java @@ -0,0 +1,152 @@ +package seedu.sourcecontrol.model; + +import java.nio.file.Path; +import java.util.Map; +import java.util.function.Predicate; + +import javafx.collections.ObservableList; +import seedu.sourcecontrol.commons.core.GuiSettings; +import seedu.sourcecontrol.logic.parser.Alias; +import seedu.sourcecontrol.model.student.Student; +import seedu.sourcecontrol.model.student.assessment.Assessment; +import seedu.sourcecontrol.model.student.group.Group; + +/** + * The API of the Model component. + */ +public interface Model { + /** {@code Predicate} that always evaluate to true */ + Predicate PREDICATE_SHOW_ALL_STUDENTS = unused -> true; + + /** + * Replaces user prefs data with the data in {@code userPrefs}. + */ + void setUserPrefs(ReadOnlyUserPrefs userPrefs); + + /** + * Returns the user prefs. + */ + ReadOnlyUserPrefs getUserPrefs(); + + /** + * Returns the user prefs' GUI settings. + */ + GuiSettings getGuiSettings(); + + /** + * Sets the user prefs' GUI settings. + */ + void setGuiSettings(GuiSettings guiSettings); + + /** + * Returns the user prefs' Source Control file path. + */ + Path getSourceControlFilePath(); + + /** + * Sets the user prefs' Source Control file path. + */ + void setSourceControlFilePath(Path sourceControlFilePath); + + /** + * Sets the user prefs' aliases. + */ + void setAliases(Map aliases); + + /** + * Returns the user prefs' aliases. + */ + Map getAliases(); + + /** + * Adds an alias to the user prefs' aliases. + */ + void addAlias(Alias alias); + + /** + * Removes an alias from the user pref's aliases. + */ + void removeAlias(String aliasWord); + + /** + * Replaces Source Control data with the data in {@code sourceControl}. + */ + void setSourceControl(ReadOnlySourceControl sourceControl); + + /** Returns the SourceControl */ + ReadOnlySourceControl getSourceControl(); + + /** + * Returns true if a student with the same identity as {@code student} exists in the Source Control application. + */ + boolean hasStudent(Student student); + + /** + * Deletes the given student. + * The student must exist in the Source Control application. + */ + void deleteStudent(Student target); + + /** + * Adds the given student. + * {@code student} must not already exist in the Source Control application. + */ + void addStudent(Student student); + + /** + * Replaces the given student {@code target} with {@code editedStudent}. + * {@code target} must exist in the Source Control application. + * The student identity of {@code editedStudent} must not be the same as + * another existing student in the Source Control application. + */ + void setStudent(Student target, Student editedStudent); + + /** + * Returns true if a group with the same identity as {@code group} exists in the application. + */ + boolean hasGroup(Group group); + + /** + * Adds the given group. + * {@code group} must not already exist in the Source Control application. + */ + void addGroup(Group group); + + /** + * Returns true if a group with the same identity as {@code assessment} exists in the application. + */ + boolean hasAssessment(Assessment assessment); + + /** + * Returns assessment in Source Control with same identity as {@code assessmentToMatch} if exists. + */ + Assessment getAssessment(Assessment assessmentToMatch); + + /** + * Adds the given assessment. + * {@code assessment} must not already exist in the Source Control application. + */ + void addAssessment(Assessment assessment); + + /** + * Replaces the given assessment {@code target} with {@code editedAssessment}. + * {@code target} must exist in the Source Control application. + * The assessment identity of {@code editedAssessment} must not be the same as + * another existing assessment in the Source Control application. + */ + void setAssessment(Assessment target, Assessment editedAssessment); + + /** + * Returns group in Source Control with same identity as {@code groupToMatch} if exists. + */ + Group getGroup(Group groupToMatch); + + /** Returns an unmodifiable view of the filtered student list */ + ObservableList getFilteredStudentList(); + + /** + * Updates the filter of the filtered student list to filter by the given {@code predicate}. + * @throws NullPointerException if {@code predicate} is null. + */ + void updateFilteredStudentList(Predicate predicate); +} diff --git a/src/main/java/seedu/sourcecontrol/model/ModelManager.java b/src/main/java/seedu/sourcecontrol/model/ModelManager.java new file mode 100644 index 00000000000..a0d9aba5da0 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/ModelManager.java @@ -0,0 +1,218 @@ +package seedu.sourcecontrol.model; + +import static java.util.Objects.requireNonNull; +import static seedu.sourcecontrol.commons.util.CollectionUtil.requireAllNonNull; + +import java.nio.file.Path; +import java.util.Map; +import java.util.function.Predicate; +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; +import seedu.sourcecontrol.commons.core.GuiSettings; +import seedu.sourcecontrol.commons.core.LogsCenter; +import seedu.sourcecontrol.logic.parser.Alias; +import seedu.sourcecontrol.model.student.Student; +import seedu.sourcecontrol.model.student.assessment.Assessment; +import seedu.sourcecontrol.model.student.group.Group; + +/** + * Represents the in-memory model of the Source Control data. + */ +public class ModelManager implements Model { + private static final Logger logger = LogsCenter.getLogger(ModelManager.class); + + private final SourceControl sourceControl; + private final UserPrefs userPrefs; + private final FilteredList filteredStudents; + + /** + * Initializes a ModelManager with the given Source Control and userPrefs. + */ + public ModelManager(ReadOnlySourceControl sourceControl, ReadOnlyUserPrefs userPrefs) { + super(); + requireAllNonNull(sourceControl, userPrefs); + + logger.fine("Initializing with Source Control: " + sourceControl + " and user prefs " + userPrefs); + + this.sourceControl = new SourceControl(sourceControl); + this.userPrefs = new UserPrefs(userPrefs); + filteredStudents = new FilteredList<>(this.sourceControl.getStudentList()); + } + + public ModelManager() { + this(new SourceControl(), new UserPrefs()); + } + + //=========== UserPrefs ================================================================================== + + @Override + public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { + requireNonNull(userPrefs); + this.userPrefs.resetData(userPrefs); + } + + @Override + public ReadOnlyUserPrefs getUserPrefs() { + return userPrefs; + } + + @Override + public GuiSettings getGuiSettings() { + return userPrefs.getGuiSettings(); + } + + @Override + public void setGuiSettings(GuiSettings guiSettings) { + requireNonNull(guiSettings); + userPrefs.setGuiSettings(guiSettings); + } + + @Override + public Path getSourceControlFilePath() { + return userPrefs.getSourceControlFilePath(); + } + + @Override + public void setSourceControlFilePath(Path sourceControlFilePath) { + requireNonNull(sourceControlFilePath); + userPrefs.setSourceControlFilePath(sourceControlFilePath); + } + + @Override + public Map getAliases() { + return userPrefs.getAliases(); + } + + @Override + public void setAliases(Map aliases) { + userPrefs.setAliases(aliases); + } + + @Override + public void addAlias(Alias alias) { + userPrefs.addAlias(alias); + } + + @Override + public void removeAlias(String aliasWord) { + userPrefs.removeAlias(aliasWord); + } + + //=========== SourceControl ================================================================================ + + @Override + public void setSourceControl(ReadOnlySourceControl sourceControl) { + this.sourceControl.resetData(sourceControl); + } + + @Override + public ReadOnlySourceControl getSourceControl() { + return sourceControl; + } + + @Override + public boolean hasStudent(Student student) { + requireNonNull(student); + return sourceControl.hasStudent(student); + } + + @Override + public void deleteStudent(Student target) { + sourceControl.removeStudent(target); + } + + @Override + public void addStudent(Student student) { + sourceControl.addStudent(student); + updateFilteredStudentList(PREDICATE_SHOW_ALL_STUDENTS); + } + + @Override + public void setStudent(Student target, Student editedStudent) { + requireAllNonNull(target, editedStudent); + + sourceControl.setStudent(target, editedStudent); + } + + @Override + public boolean hasGroup(Group group) { + requireNonNull(group); + return sourceControl.hasGroup(group); + } + + @Override + public void addGroup(Group group) { + sourceControl.addGroup(group); + updateFilteredStudentList(PREDICATE_SHOW_ALL_STUDENTS); + } + + @Override + public Group getGroup(Group groupToMatch) { + requireNonNull(groupToMatch); + return sourceControl.getGroup(groupToMatch); + } + + @Override + public boolean hasAssessment(Assessment assessment) { + requireNonNull(assessment); + return sourceControl.hasAssessment(assessment); + } + + @Override + public Assessment getAssessment(Assessment assessmentToMatch) { + requireNonNull(assessmentToMatch); + return sourceControl.getAssessment(assessmentToMatch); + } + + @Override + public void addAssessment(Assessment assessment) { + sourceControl.addAssessment(assessment); + updateFilteredStudentList(PREDICATE_SHOW_ALL_STUDENTS); + } + + @Override + public void setAssessment(Assessment target, Assessment editedAssessment) { + requireAllNonNull(target, editedAssessment); + + sourceControl.setAssessment(target, editedAssessment); + } + + //=========== Filtered Student List Accessors ============================================================= + + /** + * Returns an unmodifiable view of the list of {@code Student} backed by the internal list of + * {@code versionedSourceControl} + */ + @Override + public ObservableList getFilteredStudentList() { + return filteredStudents; + } + + @Override + public void updateFilteredStudentList(Predicate predicate) { + requireNonNull(predicate); + filteredStudents.setPredicate(predicate); + } + + @Override + public boolean equals(Object obj) { + // short circuit if same object + if (obj == this) { + return true; + } + + // instanceof handles nulls + if (!(obj instanceof ModelManager)) { + return false; + } + + // state check + ModelManager other = (ModelManager) obj; + return sourceControl.equals(other.sourceControl) + && userPrefs.equals(other.userPrefs) + && filteredStudents.equals(other.filteredStudents); + } + +} diff --git a/src/main/java/seedu/sourcecontrol/model/ReadOnlySourceControl.java b/src/main/java/seedu/sourcecontrol/model/ReadOnlySourceControl.java new file mode 100644 index 00000000000..3fc80d33245 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/ReadOnlySourceControl.java @@ -0,0 +1,30 @@ +package seedu.sourcecontrol.model; + +import java.util.List; + +import javafx.collections.ObservableList; +import seedu.sourcecontrol.model.student.Student; +import seedu.sourcecontrol.model.student.assessment.Assessment; +import seedu.sourcecontrol.model.student.group.Group; + +/** + * Unmodifiable view of a Source Control. + */ +public interface ReadOnlySourceControl { + + /** + * Returns an unmodifiable view of the students list. + * This list will not contain any duplicate students. + */ + ObservableList getStudentList(); + + /** + * Returns a list of groups. + */ + List getGroupList(); + + /** + * Returns a list of assessments. + */ + List getAssessmentList(); +} diff --git a/src/main/java/seedu/sourcecontrol/model/ReadOnlyUserPrefs.java b/src/main/java/seedu/sourcecontrol/model/ReadOnlyUserPrefs.java new file mode 100644 index 00000000000..12b22089e66 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/ReadOnlyUserPrefs.java @@ -0,0 +1,19 @@ +package seedu.sourcecontrol.model; + +import java.nio.file.Path; +import java.util.Map; + +import seedu.sourcecontrol.commons.core.GuiSettings; + +/** + * Unmodifiable view of user prefs. + */ +public interface ReadOnlyUserPrefs { + + GuiSettings getGuiSettings(); + + Path getSourceControlFilePath(); + + Map getAliases(); + +} diff --git a/src/main/java/seedu/sourcecontrol/model/SourceControl.java b/src/main/java/seedu/sourcecontrol/model/SourceControl.java new file mode 100644 index 00000000000..1b6fddc7b7b --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/SourceControl.java @@ -0,0 +1,249 @@ +package seedu.sourcecontrol.model; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.Objects; + +import javafx.collections.ObservableList; +import seedu.sourcecontrol.model.student.Student; +import seedu.sourcecontrol.model.student.UniqueStudentList; +import seedu.sourcecontrol.model.student.assessment.Assessment; +import seedu.sourcecontrol.model.student.assessment.AssessmentList; +import seedu.sourcecontrol.model.student.group.Group; +import seedu.sourcecontrol.model.student.group.GroupList; + +/** + * Wraps all data at the Source Control level + * Duplicates are not allowed (by .isSameStudent comparison) + */ +public class SourceControl implements ReadOnlySourceControl { + + private final UniqueStudentList students; + private final GroupList groups; + private final AssessmentList assessments; + + /* + * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication + * between constructors. See https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html + * + * Note that non-static init blocks are not recommended to use. There are other ways to avoid duplication + * among constructors. + */ + { + students = new UniqueStudentList(); + groups = new GroupList(); + assessments = new AssessmentList(); + } + + public SourceControl() {} + + /** + * Creates an SourceControl using the Students in the {@code toBeCopied} + */ + public SourceControl(ReadOnlySourceControl toBeCopied) { + this(); + resetData(toBeCopied); + } + + //// list overwrite operations + + /** + * Replaces the contents of the student list with {@code students}. + * {@code students} must not contain duplicate students. + */ + public void setStudents(List students) { + this.students.setStudents(students); + } + + /** + * Replaces the contents of the group list with {@code groups}. + * {@code groups} must not contain duplicate groups. + */ + public void setGroups(List groups) { + this.groups.setGroups(groups); + } + + /** + * Replaces the contents of the assessment list with {@code assessments}. + * {@code assessments} must not contain duplicate assessments. + */ + public void setAssessments(List assessments) { + this.assessments.setAssessments(assessments); + } + + /** + * Resets the existing data of this {@code SourceControl} with {@code newData}. + */ + public void resetData(ReadOnlySourceControl newData) { + requireNonNull(newData); + + setStudents(newData.getStudentList()); + setGroups(newData.getGroupList()); + setAssessments(newData.getAssessmentList()); + } + + //// student-level operations + + /** + * Returns true if a student with the same identity as {@code student} exists in the Source Control application. + */ + public boolean hasStudent(Student student) { + requireNonNull(student); + return students.contains(student); + } + + /** + * Adds a student to the Source Control application. + * The student must not already exist in the Source Control application. + * Any new groups that the student has are added into the group list. + * Any new assessments that the student has are added into the assessment list. + */ + public void addStudent(Student s) { + groups.update(s); + assessments.update(s); + students.add(s); + } + + /** + * Replaces the given student {@code target} in the list with {@code editedStudent}. + * {@code target} must exist in the Source Control application. + * The student identity of {@code editedStudent} must not be the same as + * another existing student in the Source Control application. + */ + public void setStudent(Student target, Student editedStudent) { + requireNonNull(editedStudent); + + // ID has changed, update IDs in assessments + if (!target.getId().equals(editedStudent.getId())) { + assessments.replaceStudent(target, editedStudent); + } + + // Update group memberships + groups.replaceStudent(target, editedStudent); + + students.setStudent(target, editedStudent); + } + + /** + * Removes {@code key} from this {@code SourceControl}. + * {@code key} must exist in the Source Control application. + */ + public void removeStudent(Student key) { + students.remove(key); + groups.removeStudent(key); + assessments.removeStudent(key); + } + + //// group-level operations + + /** + * Returns true if a group with the same identity as {@code group} exists in the Source Control application. + */ + public boolean hasGroup(Group group) { + requireNonNull(group); + return groups.contains(group); + } + + /** + * Adds a group to the Source Control application. + * The group must not already exist in the Source Control application. + */ + public void addGroup(Group g) { + groups.add(g); + } + + /** + * Returns group in Source Control with same identity as {@code groupToMatch} if exists. + */ + public Group getGroup(Group groupToMatch) { + requireNonNull(groupToMatch); + for (Group group : getGroupList()) { + if (group.equals(groupToMatch)) { + return group; + } + } + return null; + } + + //// assessment-level operations + + /** + * Returns true if an assessment with the same identity as {@code assessment} exists in + * the Source Control application. + */ + public boolean hasAssessment(Assessment assessment) { + requireNonNull(assessment); + return assessments.contains(assessment); + } + + /** + * Returns assessment in Source Control with same identity as {@code assessmentToMatch} if exists. + */ + public Assessment getAssessment(Assessment assessmentToMatch) { + requireNonNull(assessmentToMatch); + for (Assessment assessment : getAssessmentList()) { + if (assessment.equals(assessmentToMatch)) { + return assessment; + } + } + return null; + } + + /** + * Adds an assessment to the Source Control application. + * The assessment must not already exist in the Source Control application. + */ + public void addAssessment(Assessment a) { + assessments.add(a); + } + + /** + * Replaces the given assessment {@code target} in the list with {@code editedAssessment}. + * {@code target} must exist in the Source Control application. + * The assessment identity of {@code editedStudent} must not be the same as + * another existing assessment in the Source Control application. + */ + public void setAssessment(Assessment target, Assessment editedAssessment) { + requireNonNull(editedAssessment); + + assessments.setAssessment(target, editedAssessment); + } + + //// util methods + + @Override + public String toString() { + return students.asUnmodifiableObservableList().size() + " students"; + // TODO: refine later + } + + @Override + public ObservableList getStudentList() { + return students.asUnmodifiableObservableList(); + } + + @Override + public List getGroupList() { + return groups.groups; + } + + @Override + public List getAssessmentList() { + return assessments.assessments; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof SourceControl // instanceof handles nulls + && students.equals(((SourceControl) other).students) + && groups.equals(((SourceControl) other).groups) + && assessments.equals(((SourceControl) other).assessments)); + } + + @Override + public int hashCode() { + return Objects.hash(students, groups, assessments); + } +} diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/sourcecontrol/model/UserPrefs.java similarity index 52% rename from src/main/java/seedu/address/model/UserPrefs.java rename to src/main/java/seedu/sourcecontrol/model/UserPrefs.java index 25a5fd6eab9..ed1aa148852 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/seedu/sourcecontrol/model/UserPrefs.java @@ -1,12 +1,15 @@ -package seedu.address.model; +package seedu.sourcecontrol.model; import static java.util.Objects.requireNonNull; import java.nio.file.Path; -import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; -import seedu.address.commons.core.GuiSettings; +import seedu.sourcecontrol.commons.core.GuiSettings; +import seedu.sourcecontrol.commons.util.FileUtil; +import seedu.sourcecontrol.logic.parser.Alias; /** * Represents User's preferences. @@ -14,7 +17,8 @@ public class UserPrefs implements ReadOnlyUserPrefs { private GuiSettings guiSettings = new GuiSettings(); - private Path addressBookFilePath = Paths.get("data" , "addressbook.json"); + private Path sourceControlFilePath = FileUtil.pathOf("data" , "sourcecontrol.json"); + private Map aliases = new HashMap<>(); /** * Creates a {@code UserPrefs} with default values. @@ -35,7 +39,8 @@ public UserPrefs(ReadOnlyUserPrefs userPrefs) { public void resetData(ReadOnlyUserPrefs newUserPrefs) { requireNonNull(newUserPrefs); setGuiSettings(newUserPrefs.getGuiSettings()); - setAddressBookFilePath(newUserPrefs.getAddressBookFilePath()); + setSourceControlFilePath(newUserPrefs.getSourceControlFilePath()); + setAliases(newUserPrefs.getAliases()); } public GuiSettings getGuiSettings() { @@ -47,13 +52,30 @@ public void setGuiSettings(GuiSettings guiSettings) { this.guiSettings = guiSettings; } - public Path getAddressBookFilePath() { - return addressBookFilePath; + public Path getSourceControlFilePath() { + return sourceControlFilePath; } - public void setAddressBookFilePath(Path addressBookFilePath) { - requireNonNull(addressBookFilePath); - this.addressBookFilePath = addressBookFilePath; + public void setSourceControlFilePath(Path sourceControlFilePath) { + requireNonNull(sourceControlFilePath); + this.sourceControlFilePath = sourceControlFilePath; + } + + public Map getAliases() { + return aliases; + } + + public void setAliases(Map aliases) { + requireNonNull(aliases); + this.aliases = new HashMap<>(aliases); + } + + public void addAlias(Alias alias) { + this.aliases.put(alias.getAliasWord(), alias.getCommandWord()); + } + + public void removeAlias(String aliasWord) { + this.aliases.remove(aliasWord); } @Override @@ -68,19 +90,20 @@ public boolean equals(Object other) { UserPrefs o = (UserPrefs) other; return guiSettings.equals(o.guiSettings) - && addressBookFilePath.equals(o.addressBookFilePath); + && sourceControlFilePath.equals(o.sourceControlFilePath) + && aliases.equals(o.aliases); } @Override public int hashCode() { - return Objects.hash(guiSettings, addressBookFilePath); + return Objects.hash(guiSettings, sourceControlFilePath, aliases); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Gui Settings : " + guiSettings); - sb.append("\nLocal data file location : " + addressBookFilePath); + sb.append("\nLocal data file location : " + sourceControlFilePath); return sb.toString(); } diff --git a/src/main/java/seedu/sourcecontrol/model/student/Student.java b/src/main/java/seedu/sourcecontrol/model/student/Student.java new file mode 100644 index 00000000000..101d2de99b3 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/student/Student.java @@ -0,0 +1,188 @@ +package seedu.sourcecontrol.model.student; + +import static seedu.sourcecontrol.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import seedu.sourcecontrol.model.student.assessment.Assessment; +import seedu.sourcecontrol.model.student.assessment.Score; +import seedu.sourcecontrol.model.student.group.Group; +import seedu.sourcecontrol.model.student.id.Id; +import seedu.sourcecontrol.model.student.name.Name; +import seedu.sourcecontrol.model.student.tag.Tag; + +/** + * Represents a Student in the Source Control application. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Student { + + // Identity fields + private final Name name; + private final Id id; + + // Data fields + private final List groups = new ArrayList<>(); + private final Map scores = new LinkedHashMap<>(); + private final Set tags = new HashSet<>(); + + /** + * Constructs a {@code Student} object. + */ + public Student(Name name, Id id) { + requireAllNonNull(name, id); + this.name = name; + this.id = id; + } + + /** + * Constructs a {@code Student} object. + * Every field must be present and not null. + */ + public Student(Name name, Id id, List groups, Map scores, Set tags) { + requireAllNonNull(name, id, groups, scores, tags); + this.name = name; + this.id = id; + this.groups.addAll(groups); + this.scores.putAll(scores); + this.tags.addAll(tags); + } + + public Name getName() { + return name; + } + + public Id getId() { + return id; + } + + /** + * Returns an immutable list of groups, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public List getGroups() { + return Collections.unmodifiableList(groups); + } + + /** + * Returns an immutable map of assessment scores, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public Map getScores() { + return Collections.unmodifiableMap(scores); + } + + /** + * Adds all scores provided + */ + public void addScores(Map scores) { + this.scores.putAll(scores); + } + + /** + * Returns an immutable tag set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public Set getTags() { + return Collections.unmodifiableSet(tags); + } + + /** + * Returns true if both students have the same ID. + * This defines a weaker notion of equality between two students. + */ + public boolean isSameStudent(Student otherStudent) { + if (otherStudent == this) { + return true; + } + + return otherStudent != null + && otherStudent.getId().equals(getId()); + } + + /** + * Returns true if both students have the same name. + * This defines a weaker notion of equality between two students. + */ + public boolean isSameName(Student otherStudent) { + if (otherStudent == this) { + return true; + } + + return otherStudent != null + && otherStudent.getName().equals(getName()); + } + + /** + * Returns true if both students have the same identity and data fields. + * This defines a stronger notion of equality between two students. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Student)) { + return false; + } + + Student otherStudent = (Student) other; + return otherStudent.getName().equals(getName()) + && otherStudent.getId().equals(getId()) + && otherStudent.getGroups().equals(getGroups()) + && otherStudent.getScores().equals(getScores()) + && otherStudent.getTags().equals(getTags()); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(name, id, groups, scores, tags); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(getName()) + .append("; NUSNET ID: ") + .append(getId()); + + List groups = getGroups(); + if (!groups.isEmpty()) { + builder.append("; Groups: "); + groups.forEach(group -> { + builder.append(group); + builder.append(", "); + }); + //removes last ", " + builder.delete(builder.length() - 2, builder.length()); + } + + Map scores = getScores(); + if (!scores.isEmpty()) { + builder.append("; Assessment Scores: ") + .append(scores); + } + + Set tags = getTags(); + if (!tags.isEmpty()) { + builder.append("; Tags: "); + tags.forEach(tag -> { + builder.append(tag); + builder.append(", "); + }); + //removes last ", " + builder.delete(builder.length() - 2, builder.length()); + } + return builder.toString(); + } + +} diff --git a/src/main/java/seedu/sourcecontrol/model/student/UniqueStudentList.java b/src/main/java/seedu/sourcecontrol/model/student/UniqueStudentList.java new file mode 100644 index 00000000000..49d19f86368 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/student/UniqueStudentList.java @@ -0,0 +1,138 @@ +package seedu.sourcecontrol.model.student; + +import static java.util.Objects.requireNonNull; +import static seedu.sourcecontrol.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Iterator; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.sourcecontrol.model.student.exceptions.DuplicateStudentException; +import seedu.sourcecontrol.model.student.exceptions.StudentNotFoundException; + +/** + * A list of students that enforces uniqueness between its elements and does not allow nulls. + * A student is considered unique by comparing using {@code Student#isSameStudent(Student)}. As such, adding and + * updating of students uses Student#isSameStudent(Student) for equality so as to ensure that the student being added + * or updated is unique in terms of identity in the UniqueStudentList. However, the removal of a student + * uses Student#equals(Object) so as to ensure that the student with exactly the same fields will be removed. + * + * Supports a minimal set of list operations. + * + * @see Student#isSameStudent(Student) + */ +public class UniqueStudentList implements Iterable { + + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Returns true if the list contains an equivalent student as the given argument. + */ + public boolean contains(Student toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSameStudent); + } + + /** + * Adds a student to the list. + * The student must not already exist in the list. + */ + public void add(Student toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateStudentException(); + } + internalList.add(toAdd); + } + + /** + * Replaces the student {@code target} in the list with {@code editedStudent}. + * {@code target} must exist in the list. + * The student identity of {@code editedStudent} must not be the same as another existing student in the list. + */ + public void setStudent(Student target, Student editedStudent) { + requireAllNonNull(target, editedStudent); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new StudentNotFoundException(); + } + + if (!target.isSameStudent(editedStudent) && contains(editedStudent)) { + throw new DuplicateStudentException(); + } + + internalList.set(index, editedStudent); + } + + /** + * Removes the equivalent student from the list. + * The student must exist in the list. + */ + public void remove(Student toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new StudentNotFoundException(); + } + } + + public void setStudents(UniqueStudentList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code students}. + * {@code students} must not contain duplicate students. + */ + public void setStudents(List students) { + requireAllNonNull(students); + if (!studentsAreUnique(students)) { + throw new DuplicateStudentException(); + } + + internalList.setAll(students); + } + + /** + * 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) { + return other == this // short circuit if same object + || (other instanceof UniqueStudentList // instanceof handles nulls + && internalList.size() == ((UniqueStudentList) other).internalList.size() + && internalList.stream().allMatch(((UniqueStudentList) other)::contains)); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + /** + * Returns true if {@code students} contains only unique students. + */ + private boolean studentsAreUnique(List students) { + for (int i = 0; i < students.size() - 1; i++) { + for (int j = i + 1; j < students.size(); j++) { + if (students.get(i).isSameStudent(students.get(j))) { + return false; + } + } + } + return true; + } +} diff --git a/src/main/java/seedu/sourcecontrol/model/student/assessment/Assessment.java b/src/main/java/seedu/sourcecontrol/model/student/assessment/Assessment.java new file mode 100644 index 00000000000..90e11d9c6e7 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/student/assessment/Assessment.java @@ -0,0 +1,100 @@ +package seedu.sourcecontrol.model.student.assessment; + +import static java.util.Objects.requireNonNull; +import static seedu.sourcecontrol.commons.util.AppUtil.checkArgument; + +import java.util.LinkedHashMap; +import java.util.Map; + +import seedu.sourcecontrol.model.student.id.Id; + +/** + * Represents a Student's assessment. + * Guarantees: immutable; is valid as declared in {@link #isValidAssessment(String)} + */ +public class Assessment { + + public static final String MESSAGE_CONSTRAINTS = + "Assessment names should only contain alphanumeric characters and spaces, and it should not be blank"; + public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + + // Assessment score list + public final Map scores = new LinkedHashMap<>(); + + // Assessment name + public final String name; + + /** + * Constructs an {@code Assessment}. + * + * @param name A valid assessment name. + */ + public Assessment(String name) { + requireNonNull(name); + checkArgument(isValidAssessment(name), MESSAGE_CONSTRAINTS); + this.name = name; + } + + /** + * Returns true if a given string is a valid assessment name. + */ + public static boolean isValidAssessment(String test) { + return test.matches(VALIDATION_REGEX); + } + + public String getName() { + return name; + } + + public Map getScores() { + return scores; + } + + public void setScore(Id id, Score score) { + scores.put(id, score); + } + + public void setScores(Map scores) { + this.scores.putAll(scores); + } + + /** + * Returns true if the student with the given {@code id} is already graded. + */ + public boolean isGraded(Id id) { + requireNonNull(id); + return scores.containsKey(id); + } + + @Override + public String toString() { + return name; + } + + /** + * Returns true if both assessments have the same name. + * This defines a weaker notion of equality between two assessments. + */ + public boolean isSameAssessment(Assessment otherAssessment) { + if (otherAssessment == this) { + return true; + } + + return otherAssessment != null + && otherAssessment.getName().equals(getName()); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Assessment // instanceof handles nulls + && name.equals(((Assessment) other).name)); // state check + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + +} diff --git a/src/main/java/seedu/sourcecontrol/model/student/assessment/AssessmentList.java b/src/main/java/seedu/sourcecontrol/model/student/assessment/AssessmentList.java new file mode 100644 index 00000000000..2202ad014fd --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/student/assessment/AssessmentList.java @@ -0,0 +1,174 @@ +package seedu.sourcecontrol.model.student.assessment; + +import static java.util.Objects.requireNonNull; +import static seedu.sourcecontrol.commons.util.CollectionUtil.equalsIgnoreOrder; +import static seedu.sourcecontrol.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import seedu.sourcecontrol.model.SourceControl; +import seedu.sourcecontrol.model.student.Student; +import seedu.sourcecontrol.model.student.exceptions.AssessmentNotFoundException; +import seedu.sourcecontrol.model.student.exceptions.DuplicateAssessmentException; +import seedu.sourcecontrol.model.student.id.Id; + +/** + * Represents a list of assessments. + * Guarantees: immutable; is valid as declared in {@link #isValidNumOfAssessments(String)} (String)} + */ +public class AssessmentList { + + public static final String MESSAGE_CONSTRAINTS = + "Number of assessments should be a positive integer"; + public static final String VALIDATION_REGEX = "\\d+"; + public final List assessments = new ArrayList<>(); + + /** + * Returns true if a given string is a valid number of assessments. + */ + public static boolean isValidNumOfAssessments(String test) { + return test.matches(VALIDATION_REGEX); + } + + /** + * Returns true if the list contains an equivalent assessment as the given argument. + */ + public boolean contains(Assessment toCheck) { + requireNonNull(toCheck); + return assessments.stream().anyMatch(toCheck::isSameAssessment); + } + + /** + * Adds an assessment to the list. + * The assessment must not already exist in the list. + */ + public void add(Assessment toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateAssessmentException(); + } + assessments.add(toAdd); + } + + /** + * Updates the assessment list accordingly to the student info. + * + * @see SourceControl#addStudent(Student) + */ + public void update(Student toUpdate) { + requireNonNull(toUpdate); + + Map scores = toUpdate.getScores(); + + for (Assessment assessment : scores.keySet()) { + if (!contains(assessment)) { + add(assessment); + } + int index = assessments.indexOf(assessment); + Id id = toUpdate.getId(); + Score score = scores.get(assessment); + assessments.get(index).setScore(id, score); + } + } + + /** + * Removes the equivalent assessment from the list. + * The assessment must exist in the list. + */ + public void remove(Assessment toRemove) { + requireNonNull(toRemove); + if (!assessments.remove(toRemove)) { + throw new AssessmentNotFoundException(); + } + } + + /** + * Removes all references to the student from the assessment list + */ + public void removeStudent(Student toRemove) { + requireNonNull(toRemove); + + for (Assessment assessment : assessments) { + assessment.getScores().remove(toRemove.getId()); + } + } + + /** + * Removes all references to the previous student and adds in references to the edited student + */ + public void replaceStudent(Student previous, Student edited) { + requireAllNonNull(previous, edited); + + removeStudent(previous); + update(edited); + } + + /** + * Replaces the assessment {@code target} in the list with {@code editedAssessment}. + * {@code target} must exist in the list. + * The assessment identity of {@code editedAssessment} must not be the same as another existing assessment + * in the list. + */ + public void setAssessment(Assessment target, Assessment editedAssessment) { + requireAllNonNull(target, editedAssessment); + + int index = assessments.indexOf(target); + if (index == -1) { + throw new AssessmentNotFoundException(); + } + + if (!target.isSameAssessment(editedAssessment) && contains(editedAssessment)) { + throw new DuplicateAssessmentException(); + } + + assessments.set(index, editedAssessment); + } + + public void setAssessments(AssessmentList replacement) { + requireNonNull(replacement); + Collections.copy(assessments, replacement.assessments); + } + + /** + * Replaces the contents of this list with {@code assessments}. + * {@code assessments} must not contain duplicate assessments. + */ + public void setAssessments(List assessments) { + requireAllNonNull(assessments); + if (!assessmentsAreUnique(assessments)) { + throw new DuplicateAssessmentException(); + } + + this.assessments.clear(); + this.assessments.addAll(assessments); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AssessmentList // instanceof handles nulls + && equalsIgnoreOrder(assessments, ((AssessmentList) other).assessments)); // state check + } + + @Override + public int hashCode() { + return assessments.hashCode(); + } + + /** + * Returns true if {@code assessments} contains only unique assessments. + */ + private boolean assessmentsAreUnique(List assessments) { + for (int i = 0; i < assessments.size() - 1; i++) { + for (int j = i + 1; j < assessments.size(); j++) { + if (assessments.get(i).isSameAssessment(assessments.get(j))) { + return false; + } + } + } + return true; + } +} diff --git a/src/main/java/seedu/sourcecontrol/model/student/assessment/AssessmentStatistics.java b/src/main/java/seedu/sourcecontrol/model/student/assessment/AssessmentStatistics.java new file mode 100644 index 00000000000..a418e3580c4 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/student/assessment/AssessmentStatistics.java @@ -0,0 +1,246 @@ +package seedu.sourcecontrol.model.student.assessment; + +import static java.util.Objects.requireNonNull; +import static seedu.sourcecontrol.commons.util.AppUtil.checkArgument; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import javafx.scene.chart.Chart; +import seedu.sourcecontrol.commons.util.ChartUtil; + +/** + * Represents statistics about an assessment and the students' performance. + */ +public class AssessmentStatistics { + public static final double DEFAULT_BIN_SIZE = 10.0; + + private static final String CHART_TITLE = "Cohort Performance for %1$s"; + private static final String CHART_X_AXIS_LABEL = "Scores"; + private static final String CHART_Y_AXIS_LABEL = "Number of Students"; + + private final Assessment assessment; + + /** + * Constructs a {@code AssessmentStatistics} with the specified {@code Assessment}. + */ + public AssessmentStatistics(Assessment assessment) { + requireNonNull(assessment); + requireNonNull(assessment.scores); + + this.assessment = assessment; + } + + /** + * Creates a list of bins spanning from the minimum to maximum score, with each {@code Bin} having the specified + * bin size. + */ + public static List createBins(double binSize) { + checkArgument(binSize > Score.MIN_SCORE && binSize <= Score.MAX_SCORE); + + List bins = new ArrayList<>(); + + double binLowestValue = Score.MIN_SCORE; + + while (binLowestValue < Score.MAX_SCORE) { + Score binLowestScore = new Score(String.valueOf(binLowestValue)); + double binHighestValue = Math.min(binLowestValue + binSize, Score.MAX_SCORE); + Score binHighestScore = new Score(String.valueOf(binHighestValue)); + bins.add(new Bin(binLowestScore, binHighestScore)); + binLowestValue = binHighestValue; + } + + return bins; + } + + /** + * Returns the {@code Bin} that the {@code Score} belongs in. + */ + public static Bin getBinForScore(Collection bins, Score score) { + for (Bin b : bins) { + if (b.includesScore(score)) { + return b; + } + } + + // Should not happen since a Score must be between the minimum and maximum value (inclusive) + assert false; + + return null; + } + + /** + * Adds the specified {@code Score} to its corresponding {@code Bin}. + */ + public static void addScoreToBin(Map binCounts, Score score) { + Bin binForScore = getBinForScore(binCounts.keySet(), score); + binCounts.put(binForScore, binCounts.get(binForScore) + 1); + } + + /** + * Returns a distribution of scores for the assessment, with the bins in their string representations. + */ + public Map getScoreDistribution() { + Map binCounts = new HashMap<>(); + + List bins = createBins(DEFAULT_BIN_SIZE); + + for (Bin b : bins) { + binCounts.put(b, 0); + } + + for (Score score : assessment.scores.values()) { + addScoreToBin(binCounts, score); + } + + Map distribution = new TreeMap<>(); + binCounts.forEach((bin, count) -> distribution.put(bin.toString(), count)); + return distribution; + } + + /** + * Returns the minimum score for the {@code Assessment}. + */ + public double getMin() { + Collection scores = assessment.getScores().values(); + Optional min = scores.stream() + .map(Score::getNumericValue) + .min(Comparator.naturalOrder()); + return min.orElse(Score.MIN_SCORE); + } + + /** + * Returns the maximum score for the {@code Assessment}. + */ + public double getMax() { + Collection scores = assessment.getScores().values(); + Optional max = scores.stream() + .map(Score::getNumericValue) + .max(Comparator.naturalOrder()); + return max.orElse(Score.MIN_SCORE); + } + + /** + * Returns the median score for the {@code Assessment}. + */ + public double getMedian() { + Collection scores = assessment.getScores().values(); + List sorted = scores.stream() + .map(Score::getNumericValue) + .sorted().collect(Collectors.toList()); + + if (sorted.isEmpty()) { + return Score.MIN_SCORE; + } + + long size = sorted.size(); + int midPos; // middle position of the sorted list + if (size % 2 == 1) { // odd number of scores + midPos = (int) ((size + 1) / 2.0 - 1); + return sorted.get(midPos); + } else { // even number of scores + midPos = (int) (size / 2.0); + return (sorted.get(midPos - 1) + + sorted.get(midPos)) / 2.0; + } + } + + /** + * Returns the mean score for the {@code Assessment}. + */ + public double getMean() { + int numScores = 0; + double sumOfScores = 0.0; + for (Score score : assessment.scores.values()) { + numScores++; + sumOfScores += score.getNumericValue(); + } + return numScores == 0 ? Score.MIN_SCORE : sumOfScores / numScores; + } + + /** + * Returns the Xth percentile score for the {@code Assessment}. + */ + public double getXPercentile(int x) { + assert x > -1 && x < 101; + + Collection scores = assessment.getScores().values(); + List sorted = scores.stream() + .map(Score::getNumericValue) + .sorted().collect(Collectors.toList()); + + if (sorted.isEmpty()) { + return Score.MIN_SCORE; + } + + long size = sorted.size(); + int xPercentilePos = (int) Math.ceil(x / 100.0 * size); + return sorted.get(xPercentilePos - 1); // list indexing starts at 0 + } + + /** + * Returns a histogram representing the scores for the assessment. + */ + public Chart toHistogram() { + return ChartUtil.createBarChart(String.format(CHART_TITLE, assessment.getName()), + CHART_X_AXIS_LABEL, CHART_Y_AXIS_LABEL, getScoreDistribution()); + } + + /** + * Represents a bin in the histogram. + */ + public static class Bin { + private final Score binMinimum; + private final Score binMaximum; + private final boolean maxIsInclusive; + + /** + * Constructs a {@code Bin} spanning from the specified minimum to the specified maximum score. + */ + public Bin(Score binMinimum, Score binMaximum) { + this.binMinimum = binMinimum; + this.binMaximum = binMaximum; + maxIsInclusive = binMaximum.isMaxScore(); + } + + /** + * Returns whether the specified {@code Score} is included in this bin. + */ + public boolean includesScore(Score score) { + return score.getNumericValue() >= binMinimum.getNumericValue() + && (maxIsInclusive + ? score.getNumericValue() <= binMaximum.getNumericValue() + : score.getNumericValue() < binMaximum.getNumericValue()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Bin otherBin = (Bin) o; + return binMinimum.equals(otherBin.binMinimum) && binMaximum.equals(otherBin.binMaximum); + } + + @Override + public int hashCode() { + return Objects.hash(binMinimum, binMaximum); + } + + @Override + public String toString() { + return String.format("%.0f-%.0f", binMinimum.getNumericValue(), binMaximum.getNumericValue()); + } + } +} diff --git a/src/main/java/seedu/sourcecontrol/model/student/assessment/GroupStatistics.java b/src/main/java/seedu/sourcecontrol/model/student/assessment/GroupStatistics.java new file mode 100644 index 00000000000..2f69265e684 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/student/assessment/GroupStatistics.java @@ -0,0 +1,122 @@ +package seedu.sourcecontrol.model.student.assessment; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javafx.scene.chart.Chart; +import seedu.sourcecontrol.commons.util.ChartUtil; +import seedu.sourcecontrol.model.student.group.Group; +import seedu.sourcecontrol.model.student.id.Id; + +/** + * Represents statistics about a student and the students' performance in each assessment. + */ +public class GroupStatistics { + + private static final String CHART_TITLE = "Performance of %1$s"; + private static final String CHART_X_AXIS_LABEL = "Assessments"; + private static final String CHART_Y_AXIS_LABEL = "Scores"; + + private final Group group; + private final List studentList; + private final List assessmentList; + + /** + * Constructs a {@code GroupStatistics} with the specified {@code group}. + */ + public GroupStatistics(Group group, List assessmentList) { + requireNonNull(group); + requireNonNull(group.getStudents()); + requireNonNull(assessmentList); + this.group = group; + this.studentList = group.getStudents(); + this.assessmentList = assessmentList; + } + + /** + * Returns true if more than one student in group is graded in {@code assessment}. + */ + public boolean isGraded(Assessment assessment) { + for (Id id : studentList) { + if (assessment.isGraded(id)) { + return true; + } + } + return false; + } + + /** + * Returns median score of students in group who are graded in {@code assessment}. + */ + public double getMedian(Assessment assessment) { + int count = 0; + ArrayList scores = new ArrayList<>(); + for (Id id : studentList) { + if (!assessment.isGraded(id)) { + continue; + } + scores.add(assessment.getScores().get(id)); + count++; + } + + double median; + + List sorted = scores.stream() + .map(Score::getNumericValue) + .sorted().collect(Collectors.toList()); + int midPos = count / 2; // middle position of the sorted list + if (count % 2 == 1) { // odd number of scores + median = sorted.get(midPos); + } else { // even number of scores + median = (sorted.get(midPos - 1) + + sorted.get(midPos)) / 2.0; + } + + return median; + } + + /** + * Returns a distribution of scores for the assessment. + * + * @return array of {@code Map} where + * the first element contains the group median distribution, + * the second element contains the cohort mean distribution, + * and the third element contains the cohort median distribution. + */ + public Map[] getDataSet() { + Map groupMedian = new LinkedHashMap<>(); + Map cohortMean = new LinkedHashMap<>(); + Map cohortMedian = new LinkedHashMap<>(); + + for (Assessment assessment: assessmentList) { + if (!isGraded(assessment)) { + continue; + } + // group median + groupMedian.put(assessment.getName(), getMedian(assessment)); + + // cohort mean and median + AssessmentStatistics statistics = new AssessmentStatistics(assessment); + cohortMean.put(assessment.getName(), statistics.getMean()); + cohortMedian.put(assessment.getName(), statistics.getMedian()); + } + + @SuppressWarnings("unchecked") + Map[] dataSet = new Map[]{groupMedian, cohortMean, cohortMedian}; + return dataSet; + } + + /** + * Returns a line chart representing the scores of student for each assessment. + */ + public Chart toLineChart() { + Map[] dataSet = getDataSet(); + return ChartUtil.createLineChart(String.format(CHART_TITLE, group.getName()), + CHART_X_AXIS_LABEL, CHART_Y_AXIS_LABEL, "group median ", dataSet[0], dataSet[1], dataSet[2]); + } +} diff --git a/src/main/java/seedu/sourcecontrol/model/student/assessment/Score.java b/src/main/java/seedu/sourcecontrol/model/student/assessment/Score.java new file mode 100644 index 00000000000..a434210cbf8 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/student/assessment/Score.java @@ -0,0 +1,89 @@ +package seedu.sourcecontrol.model.student.assessment; + +import static java.util.Objects.requireNonNull; +import static seedu.sourcecontrol.commons.util.AppUtil.checkArgument; + +/** + * Represents a Student's score for an assessment. + * Guarantees: immutable; is valid as declared in {@link #isValidScore(String)} + */ +public class Score { + + public static final String MESSAGE_CONSTRAINTS = + "Score should be an unsigned number in percentage format, and it should have at most 2 decimal places"; + public static final String INTEGRAL_PART_REGEX = "\\d{1,2}"; + public static final String DECIMAL_PART_REGEX = "(\\.\\d{1,2})?"; + public static final double MIN_SCORE = 0.0; + public static final double MAX_SCORE = 100.0; + public static final String MAX_SCORE_INT = "100"; + public static final String MAX_SCORE_DEC_1 = "100.0"; + public static final String MAX_SCORE_DEC_2 = "100.00"; + public static final String VALIDATION_REGEX = INTEGRAL_PART_REGEX + DECIMAL_PART_REGEX; + public final String value; + + /** + * Constructs a {@code Score}. + * + * @param score A valid score. + */ + public Score(String score) { + requireNonNull(score); + checkArgument(isValidScore(score), MESSAGE_CONSTRAINTS); + value = reformatScore(score); + } + + /** + * Returns true if a given string is a valid score. + */ + public static boolean isValidScore(String test) { + return test.matches(VALIDATION_REGEX) + || test.equals(MAX_SCORE_INT) + || test.equals(MAX_SCORE_DEC_1) + || test.equals(MAX_SCORE_DEC_2); + } + + /** + * Reformats valid score to 2 decimal places. + */ + public static String reformatScore(String score) { + String[] parts = score.split("\\."); // split along decimal point + assert parts.length == 2 || parts.length == 1; // integral part must exist and decimal part is optional + String integral = parts[0]; + String decimal = parts.length == 2 ? parts[1] : "00"; + assert decimal.length() <= 2; // decimal part contains at most 2 digits + decimal += "0".repeat(2 - decimal.length()); // add additional 0 as needed + return integral + "." + decimal; + } + + public double getNumericValue() { + // NumberFormatException is not handled since + // Score cannot be initialized with invalid values + return Double.parseDouble(value); + } + + public boolean isMaxScore() { + return getNumericValue() == Score.MAX_SCORE; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Score // instanceof handles nulls + && value.equals(((Score) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + +} diff --git a/src/main/java/seedu/sourcecontrol/model/student/assessment/StudentStatistics.java b/src/main/java/seedu/sourcecontrol/model/student/assessment/StudentStatistics.java new file mode 100644 index 00000000000..31aa6ab8bf2 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/student/assessment/StudentStatistics.java @@ -0,0 +1,70 @@ +package seedu.sourcecontrol.model.student.assessment; + +import static java.util.Objects.requireNonNull; + +import java.util.LinkedHashMap; +import java.util.Map; + +import javafx.scene.chart.Chart; +import seedu.sourcecontrol.commons.util.ChartUtil; +import seedu.sourcecontrol.model.student.Student; + +/** + * Represents statistics about a student and the students' performance in each assessment. + */ +public class StudentStatistics { + + private static final String CHART_TITLE = "'s Results"; + private static final String CHART_X_AXIS_LABEL = "Assessments"; + private static final String CHART_Y_AXIS_LABEL = "Scores"; + + private final Student student; + private final Map scoreMap; + + /** + * Constructs a {@code studentStatistics} with the specified {@code student}. + */ + public StudentStatistics(Student student) { + requireNonNull(student); + requireNonNull(student.getScores()); + this.student = student; + this.scoreMap = student.getScores(); + } + + /** + * Returns a distribution of student's scores for the assessment. + */ + public Map getScoreDistribution() { + Map distribution = new LinkedHashMap<>(); + scoreMap.forEach((assessment, score) -> distribution.put(assessment.getName(), score.getNumericValue())); + return distribution; + } + + /** + * Returns a distribution of cohort mean and median for the assessment. + * + * @return array of {@code Map} where the first element contains the mean distribution, + * and the second element contains the median distribution. + */ + public Map[] getDataSet() { + Map mean = new LinkedHashMap<>(); + Map median = new LinkedHashMap<>(); + scoreMap.forEach((assessment, score) -> { + AssessmentStatistics statistics = new AssessmentStatistics(assessment); + mean.put(assessment.getName(), statistics.getMean()); + median.put(assessment.getName(), statistics.getMedian()); + }); + @SuppressWarnings("unchecked") + Map[] dataSet = new Map[]{mean, median}; + return dataSet; + } + + /** + * Returns a line chart representing the scores of student for each assessment. + */ + public Chart toLineChart() { + Map[] dataSet = getDataSet(); + return ChartUtil.createLineChart(student.getName() + CHART_TITLE, + CHART_X_AXIS_LABEL, CHART_Y_AXIS_LABEL, "score", getScoreDistribution(), dataSet[0], dataSet[1]); + } +} diff --git a/src/main/java/seedu/sourcecontrol/model/student/exceptions/AssessmentNotFoundException.java b/src/main/java/seedu/sourcecontrol/model/student/exceptions/AssessmentNotFoundException.java new file mode 100644 index 00000000000..2a523021be4 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/student/exceptions/AssessmentNotFoundException.java @@ -0,0 +1,6 @@ +package seedu.sourcecontrol.model.student.exceptions; + +/** + * Signals that the operation is unable to find the specified assessment. + */ +public class AssessmentNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/sourcecontrol/model/student/exceptions/DuplicateAssessmentException.java b/src/main/java/seedu/sourcecontrol/model/student/exceptions/DuplicateAssessmentException.java new file mode 100644 index 00000000000..c7565709a5a --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/student/exceptions/DuplicateAssessmentException.java @@ -0,0 +1,11 @@ +package seedu.sourcecontrol.model.student.exceptions; + +/** + * Signals that the operation will result in duplicate Assessments + * (Assessments are considered duplicates if they have the same name). + */ +public class DuplicateAssessmentException extends RuntimeException { + public DuplicateAssessmentException() { + super("Operation would result in duplicate assessments"); + } +} diff --git a/src/main/java/seedu/sourcecontrol/model/student/exceptions/DuplicateGroupException.java b/src/main/java/seedu/sourcecontrol/model/student/exceptions/DuplicateGroupException.java new file mode 100644 index 00000000000..1903741574c --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/student/exceptions/DuplicateGroupException.java @@ -0,0 +1,7 @@ +package seedu.sourcecontrol.model.student.exceptions; + +public class DuplicateGroupException extends RuntimeException { + public DuplicateGroupException() { + super("Operation would result in duplicate groups"); + } +} diff --git a/src/main/java/seedu/sourcecontrol/model/student/exceptions/DuplicateStudentException.java b/src/main/java/seedu/sourcecontrol/model/student/exceptions/DuplicateStudentException.java new file mode 100644 index 00000000000..a1b2441a7eb --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/student/exceptions/DuplicateStudentException.java @@ -0,0 +1,11 @@ +package seedu.sourcecontrol.model.student.exceptions; + +/** + * Signals that the operation will result in duplicate Students (Students are considered duplicates if they have the + * same identity). + */ +public class DuplicateStudentException extends RuntimeException { + public DuplicateStudentException() { + super("Operation would result in duplicate students"); + } +} diff --git a/src/main/java/seedu/sourcecontrol/model/student/exceptions/GroupNotFoundException.java b/src/main/java/seedu/sourcecontrol/model/student/exceptions/GroupNotFoundException.java new file mode 100644 index 00000000000..8c485ea824b --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/student/exceptions/GroupNotFoundException.java @@ -0,0 +1,7 @@ +package seedu.sourcecontrol.model.student.exceptions; + +public class GroupNotFoundException extends RuntimeException { + public GroupNotFoundException() { + super("Group does not exist"); + } +} diff --git a/src/main/java/seedu/sourcecontrol/model/student/exceptions/StudentNotFoundException.java b/src/main/java/seedu/sourcecontrol/model/student/exceptions/StudentNotFoundException.java new file mode 100644 index 00000000000..b15fca1c2fe --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/student/exceptions/StudentNotFoundException.java @@ -0,0 +1,6 @@ +package seedu.sourcecontrol.model.student.exceptions; + +/** + * Signals that the operation is unable to find the specified student. + */ +public class StudentNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/sourcecontrol/model/student/group/Group.java b/src/main/java/seedu/sourcecontrol/model/student/group/Group.java new file mode 100644 index 00000000000..94cb6f687f8 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/student/group/Group.java @@ -0,0 +1,107 @@ +package seedu.sourcecontrol.model.student.group; + +import static java.util.Objects.requireNonNull; +import static seedu.sourcecontrol.commons.util.AppUtil.checkArgument; + +import java.util.ArrayList; +import java.util.List; + +import seedu.sourcecontrol.model.student.id.Id; + +public class Group { + + public static final String MESSAGE_CONSTRAINTS = + "Group names should only contain alphanumeric characters and spaces, and it should not be blank"; + public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + + // Group student list + public final List students; + + // Group name + public final String name; + + /** + * Constructs a {@code Group}. + * + * @param name A valid Group name. + */ + public Group(String name) { + requireNonNull(name); + checkArgument(isValidGroup(name), MESSAGE_CONSTRAINTS); + this.name = name; + this.students = new ArrayList<>(); + } + + /** + * Constructs a {@code Group} with the specified students. + * + * @param name A valid Group name. + * @param students Students to add into the group. + */ + public Group(String name, List students) { + requireNonNull(name); + checkArgument(isValidGroup(name), MESSAGE_CONSTRAINTS); + this.name = name; + this.students = new ArrayList<>(students); + } + + /** + * Returns true if the given string {@code test} is a valid group name. + */ + public static boolean isValidGroup(String test) { + return test.matches(VALIDATION_REGEX); + } + + public String getName() { + return name; + } + + public List getStudents() { + return students; + } + + /** + * Returns true if the given ID {@code id} specifies a student in the group. + */ + public boolean hasStudent(Id id) { + requireNonNull(id); + return students.contains(id); + } + + /** + * Adds student with ID {@code id} to this {@code Group}. + * {@code id} must not already exist in the group. + */ + public void addStudent(Id id) { + requireNonNull(id); + if (!hasStudent(id)) { + students.add(id); + } + } + + /** + * Removes student with ID {@code key} from this {@code Group}. + * {@code key} must exist in the group. + */ + public void removeStudent(Id key) { + students.remove(key); + } + + @Override + public String toString() { + return name; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Group // instanceof handles nulls + && name.equals(((Group) other).name)); // state check + } + + @Override + public int hashCode() { + return name.hashCode(); + } + +} diff --git a/src/main/java/seedu/sourcecontrol/model/student/group/GroupContainsKeywordsPredicate.java b/src/main/java/seedu/sourcecontrol/model/student/group/GroupContainsKeywordsPredicate.java new file mode 100644 index 00000000000..10bfd3c9448 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/student/group/GroupContainsKeywordsPredicate.java @@ -0,0 +1,36 @@ +package seedu.sourcecontrol.model.student.group; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.sourcecontrol.model.student.Student; + +/** + * Tests that a {@code Student}'s {@code Group} matches any of the keywords given. + * Supports partial searching of group names. + */ +public class GroupContainsKeywordsPredicate implements Predicate { + private final List keywords; + + /** + * Creates a {@code GroupContainsKeywordsPredicate} with the given list of keywords. + */ + public GroupContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Student student) { + return student.getGroups().stream() + .anyMatch(group -> keywords.stream() + .anyMatch(keyword -> group.name.toLowerCase().contains(keyword.toLowerCase()))); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof GroupContainsKeywordsPredicate // instanceof handles nulls + && keywords.equals(((GroupContainsKeywordsPredicate) other).keywords)); // state check + } + +} diff --git a/src/main/java/seedu/sourcecontrol/model/student/group/GroupList.java b/src/main/java/seedu/sourcecontrol/model/student/group/GroupList.java new file mode 100644 index 00000000000..d927e9571da --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/student/group/GroupList.java @@ -0,0 +1,171 @@ +package seedu.sourcecontrol.model.student.group; + +import static java.util.Objects.requireNonNull; +import static seedu.sourcecontrol.commons.util.CollectionUtil.equalsIgnoreOrder; +import static seedu.sourcecontrol.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import seedu.sourcecontrol.model.SourceControl; +import seedu.sourcecontrol.model.student.Student; +import seedu.sourcecontrol.model.student.exceptions.DuplicateGroupException; +import seedu.sourcecontrol.model.student.exceptions.GroupNotFoundException; + +public class GroupList { + + public static final String MESSAGE_CONSTRAINTS = + "Number of Groups should be a positive integer"; + public static final String VALIDATION_REGEX = "\\d+"; + public final List groups = new ArrayList<>(); + + /** + * Returns true if a given string is a valid number of Groups. + */ + public static boolean isValidNumOfGroups(String test) { + return test.matches(VALIDATION_REGEX); + } + + /** + * Returns true if the list contains an equivalent Group as the given argument. + */ + public boolean contains(Group group) { + requireNonNull(group); + return groups.stream().anyMatch(group::equals); + } + + /** + * Adds a Group into the list. + */ + public void add(Group group) { + requireNonNull(group); + if (contains(group)) { + return; + } + groups.add(group); + } + + /** + * Updates the group list accordingly to the student info. + * Ensures that the student is properly recorded in every group in the group list + * + * @see SourceControl#addStudent(Student) + */ + public void update(Student toUpdate) { + requireNonNull(toUpdate); + + List studentGroups = toUpdate.getGroups(); + for (Group group : studentGroups) { + if (!groups.contains(group)) { + groups.add(group); + } else { + Group groupInList = groups.get(groups.indexOf(group)); + groupInList.addStudent(toUpdate.getId()); + } + } + } + + /** + * Replaces the Group {@code target} in the list with {@code editedGroup}. + * {@code target} must exist in the list. + * The Group identity of {@code editedGroup} must not be the same as another existing Group in the list. + */ + public void setGroup(Group target, Group editedGroup) { + requireAllNonNull(target, editedGroup); + + int index = groups.indexOf(target); + if (index == -1) { + throw new GroupNotFoundException(); + } + + if (!target.equals(editedGroup) && contains(editedGroup)) { + throw new DuplicateGroupException(); + } + + groups.set(index, editedGroup); + } + + /** + * Removes any references to the student in groups. + */ + public void removeStudent(Student toRemove) { + requireNonNull(toRemove); + + for (Group group : groups) { + if (group.hasStudent(toRemove.getId())) { + group.removeStudent(toRemove.getId()); + } + } + } + + /** + * Removes any references to the previous student and inserts references to the new student + */ + public void replaceStudent(Student previous, Student edited) { + requireAllNonNull(previous, edited); + + removeStudent(previous); + update(edited); + } + + /** + * Removes the equivalent Group from the list. + * The Group must exist in the list. + */ + public void remove(Group group) { + requireNonNull(group); + if (!groups.remove(group)) { + throw new GroupNotFoundException(); + } + } + + /** + * Replaces the contents of this list with {@code Group}. + */ + public void setGroups(GroupList group) { + requireNonNull(group); + Collections.copy(groups, group.groups); + } + + /** + * Replaces the contents of this list with {@code groups}. + * {@code groups} must not contain duplicate groups. + */ + public void setGroups(List groups) { + requireAllNonNull(groups); + if (!groupsAreUnique(groups)) { + throw new DuplicateGroupException(); + } + + this.groups.clear(); + this.groups.addAll(groups); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof GroupList // instanceof handles nulls + && equalsIgnoreOrder(groups, ((GroupList) other).groups)); // state check + } + + @Override + public int hashCode() { + return groups.hashCode(); + } + + /** + * Returns true if {@code groups} contains only unique groups. + */ + private boolean groupsAreUnique(List groups) { + for (int i = 0; i < groups.size() - 1; i++) { + for (int j = i + 1; j < groups.size(); j++) { + if (groups.get(i).equals(groups.get(j))) { + return false; + } + } + } + return true; + } + +} diff --git a/src/main/java/seedu/sourcecontrol/model/student/id/Id.java b/src/main/java/seedu/sourcecontrol/model/student/id/Id.java new file mode 100644 index 00000000000..68bf0515659 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/student/id/Id.java @@ -0,0 +1,61 @@ +package seedu.sourcecontrol.model.student.id; + +import static java.util.Objects.requireNonNull; +import static seedu.sourcecontrol.commons.util.AppUtil.checkArgument; + +public class Id { + + public static final String MESSAGE_CONSTRAINTS = + "ID should start with E followed by 7 numerical numbers"; + public static final String VALIDATION_REGEX = "[Ee]\\d{7}"; + public final String value; + + /** + * Constructs a {@code Id}. + * + * @param id A valid NUSNET ID. + */ + public Id(String id) { + requireNonNull(id); + checkArgument(isValidID(id), MESSAGE_CONSTRAINTS); + value = reformatId(id); + } + + /** + * Returns true if a given string is a valid phone number. + */ + public static boolean isValidID(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public String toString() { + return value; + } + + /** + * Reformats valid ID to uppercase. + */ + public static String reformatId(String name) { + assert name.length() == 8; // ID should already be validated + char upperCase = Character.toUpperCase(name.charAt(0)); + return upperCase + name.substring(1); + } + + public String getValue() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Id // instanceof handles nulls + && value.equals(((Id) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + +} diff --git a/src/main/java/seedu/sourcecontrol/model/student/id/IdContainsKeywordsPredicate.java b/src/main/java/seedu/sourcecontrol/model/student/id/IdContainsKeywordsPredicate.java new file mode 100644 index 00000000000..0108b98a7e3 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/student/id/IdContainsKeywordsPredicate.java @@ -0,0 +1,35 @@ +package seedu.sourcecontrol.model.student.id; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.sourcecontrol.model.student.Student; + +/** + * Tests that a {@code Student}'s {@code Id} matches any of the keywords given. + * Supports partial searching of IDs. + */ +public class IdContainsKeywordsPredicate implements Predicate { + private final List keywords; + + /** + * Creates a {@code IdContainsKeywordsPredicate} with the given list of keywords. + */ + public IdContainsKeywordsPredicate(List keyword) { + this.keywords = keyword; + } + + @Override + public boolean test(Student student) { + return keywords.stream() + .anyMatch(keyword -> student.getId().value.toLowerCase().contains(keyword.toLowerCase())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof IdContainsKeywordsPredicate // instanceof handles nulls + && keywords.equals(((IdContainsKeywordsPredicate) other).keywords)); // state check + } + +} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/sourcecontrol/model/student/name/Name.java similarity index 84% rename from src/main/java/seedu/address/model/person/Name.java rename to src/main/java/seedu/sourcecontrol/model/student/name/Name.java index 79244d71cf7..aee9cea31f4 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/sourcecontrol/model/student/name/Name.java @@ -1,10 +1,10 @@ -package seedu.address.model.person; +package seedu.sourcecontrol.model.student.name; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; +import static seedu.sourcecontrol.commons.util.AppUtil.checkArgument; /** - * Represents a Person's name in the address book. + * Represents a Student's name in the Source Control application. * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} */ public class Name { @@ -13,7 +13,7 @@ public class Name { "Names should only contain alphanumeric characters and spaces, and it should not be blank"; /* - * The first character of the address must not be a whitespace, + * The first character of the name must not be a whitespace, * otherwise " " (a blank string) becomes a valid input. */ public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/sourcecontrol/model/student/name/NameContainsKeywordsPredicate.java similarity index 62% rename from src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java rename to src/main/java/seedu/sourcecontrol/model/student/name/NameContainsKeywordsPredicate.java index c9b5868427c..609c2fdb489 100644 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ b/src/main/java/seedu/sourcecontrol/model/student/name/NameContainsKeywordsPredicate.java @@ -1,24 +1,28 @@ -package seedu.address.model.person; +package seedu.sourcecontrol.model.student.name; import java.util.List; import java.util.function.Predicate; -import seedu.address.commons.util.StringUtil; +import seedu.sourcecontrol.commons.util.StringUtil; +import seedu.sourcecontrol.model.student.Student; /** - * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. + * Tests that a {@code Student}'s {@code Name} matches any of the keywords given. */ -public class NameContainsKeywordsPredicate implements Predicate { +public class NameContainsKeywordsPredicate implements Predicate { private final List keywords; + /** + * Creates a {@code NameContainsKeywordsPredicate} with the given list of keywords. + */ public NameContainsKeywordsPredicate(List keywords) { this.keywords = keywords; } @Override - public boolean test(Person person) { + public boolean test(Student student) { return keywords.stream() - .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(student.getName().fullName, keyword)); } @Override diff --git a/src/main/java/seedu/sourcecontrol/model/student/name/NameEqualsPredicate.java b/src/main/java/seedu/sourcecontrol/model/student/name/NameEqualsPredicate.java new file mode 100644 index 00000000000..879df1de8fe --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/student/name/NameEqualsPredicate.java @@ -0,0 +1,35 @@ +package seedu.sourcecontrol.model.student.name; + +import static java.util.Objects.requireNonNull; + +import java.util.function.Predicate; + +import seedu.sourcecontrol.model.student.Student; + +/** + * Tests that a {@code Student}'s {@code Name} matches the given name exactly. + */ +public class NameEqualsPredicate implements Predicate { + private final String nameToMatch; + + /** + * Creates a {@code NameEqualsPredicate} with the given name to match. + */ + public NameEqualsPredicate(String nameToMatch) { + requireNonNull(nameToMatch); + this.nameToMatch = nameToMatch; + } + + @Override + public boolean test(Student student) { + return nameToMatch.equals(student.getName().fullName); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof NameEqualsPredicate // instanceof handles nulls + && nameToMatch.equals(((NameEqualsPredicate) other).nameToMatch)); // state check + } + +} diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/sourcecontrol/model/student/tag/Tag.java similarity index 82% rename from src/main/java/seedu/address/model/tag/Tag.java rename to src/main/java/seedu/sourcecontrol/model/student/tag/Tag.java index b0ea7e7dad7..7b68b890db7 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/seedu/sourcecontrol/model/student/tag/Tag.java @@ -1,10 +1,10 @@ -package seedu.address.model.tag; +package seedu.sourcecontrol.model.student.tag; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; +import static seedu.sourcecontrol.commons.util.AppUtil.checkArgument; /** - * Represents a Tag in the address book. + * Represents a Tag in the Source Control application. * Guarantees: immutable; name is valid as declared in {@link #isValidTagName(String)} */ public class Tag { @@ -39,6 +39,13 @@ public boolean equals(Object other) { && tagName.equals(((Tag) other).tagName)); // state check } + /** + * Returns tag name. + */ + public String getTagName() { + return tagName; + } + @Override public int hashCode() { return tagName.hashCode(); diff --git a/src/main/java/seedu/sourcecontrol/model/student/tag/TagContainsKeywordsPredicate.java b/src/main/java/seedu/sourcecontrol/model/student/tag/TagContainsKeywordsPredicate.java new file mode 100644 index 00000000000..148f256e28e --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/student/tag/TagContainsKeywordsPredicate.java @@ -0,0 +1,33 @@ +package seedu.sourcecontrol.model.student.tag; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.sourcecontrol.model.student.Student; + +/** + * Tests that a {@code Student}'s {@code Tag} matches any of the keywords given. + * Supports partial searching of tags. + */ +public class TagContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public TagContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Student student) { + return student.getTags().stream() + .anyMatch(tag -> keywords.stream() + .anyMatch(keyword -> tag.tagName.toLowerCase().contains(keyword.toLowerCase()))); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof TagContainsKeywordsPredicate // instanceof handles nulls + && keywords.equals(((TagContainsKeywordsPredicate) other).keywords)); // state check + } + +} diff --git a/src/main/java/seedu/sourcecontrol/model/util/SampleDataUtil.java b/src/main/java/seedu/sourcecontrol/model/util/SampleDataUtil.java new file mode 100644 index 00000000000..1d594ab7f1c --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/model/util/SampleDataUtil.java @@ -0,0 +1,188 @@ +package seedu.sourcecontrol.model.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import seedu.sourcecontrol.model.ReadOnlySourceControl; +import seedu.sourcecontrol.model.SourceControl; +import seedu.sourcecontrol.model.student.Student; +import seedu.sourcecontrol.model.student.assessment.Assessment; +import seedu.sourcecontrol.model.student.assessment.Score; +import seedu.sourcecontrol.model.student.group.Group; +import seedu.sourcecontrol.model.student.id.Id; +import seedu.sourcecontrol.model.student.name.Name; +import seedu.sourcecontrol.model.student.tag.Tag; + +/** + * Contains utility methods for populating {@code SourceControl} with sample data. + */ +public class SampleDataUtil { + private static final Random random = new Random(); + + //// Configuration variables + private static final String[] SAMPLE_STUDENT_NAMES = { + "How Kai Xin", "Wu Wei Le", "Ruchi Rattan", "Eva Porter", "Lai Guo Qiang Jack", "Tang Jia Xin", + "Damini Kashyap", "Zhuang Yi Xi", "Winnie Ong Jia Ling", "Irfan Syahid Bin Mohammad Aydin", "Chen Kok Meng", + "Sameera Raval", "Freya Murray", "Chang Yong Quan", "Haleigh Green", "Qian Jia Wen", "Ho Yi Xin", + "Nur Sharifah Binte Azmi Noh", "Betania Suartini", "Zhuo Jia Wen", "Sim Ming Hao Jeremy", "Ng Sing Yu", + "Rosey Shah", "Angus Johnson", "Lye Kai Feng", "Joel Jesus", "Nhung Phan", "Roberto Ferraro", + "Qian Kai Wen", "Lew Si Hui", "Mohammad Syaril Bin Mohammad Irfan", "Loh Xin Yi", "Choo Yi Ling", + "Juli Mandasari", "Jonathan Hong Yong Quan", "Teoh Yi Ling", "Leung Zheng Min", "Toh Hui Wen", + "David Balasubramanian", "Tin Zhi Xin", "Asher Edwards", "Baey Yi De", "Low Hui Qi", "Chang Hao Ming", + "Jackson Adams", "Edward Davidson", "Sim Hui Qi", "Kanika Kothari", "Tang Xin En", "Tin Zhi En" + }; + // Max group number and suffix is kept sufficiently small so that each group has a reasonable number of students + private static final int MAX_GROUP_NUMBER = 3; + private static final char NUMBER_OF_SUFFIXES = 3; + private static final String[] SAMPLE_TAG_NAMES = {"beginner", "intermediate", "advanced"}; + private static final String[] SAMPLE_ASSESSMENT_NAMES = { + "Reading Assessment 1", "Reading Assessment 2", "Midterm Examination", "Practical Assessment", + "Final Examination" + }; + private static final double MEAN_SCORE = 60.0; + private static final double SCORE_STANDARD_DEVIATION = 15.0; + + //// Shared instances + private static final List SAMPLE_TUTORIAL_GROUPS = getRandomGroups("T"); + private static final List SAMPLE_RECITATION_GROUPS = getRandomGroups("R"); + private static final List SAMPLE_ASSESSMENTS = Arrays.stream(SAMPLE_ASSESSMENT_NAMES) + .map(Assessment::new).collect(Collectors.toList()); + + /** + * Stores the IDs that have been randomly generated and created, to avoid generating duplicate IDs. + */ + private static final Set TAKEN_IDS = new HashSet<>(); + + public static List getSampleStudents() { + List sampleStudents = new ArrayList<>(); + + for (String studentName : SAMPLE_STUDENT_NAMES) { + Id studentId = getRandomStudentId(); + List groups = Arrays.asList(getRandomTutorialGroup(), getRandomRecitationGroup()); + for (Group group : groups) { + group.addStudent(studentId); + } + Student student = new Student(new Name(studentName), studentId, groups, + getRandomAssessmentScores(), getTagSet(getRandomTag())); + sampleStudents.add(student); + } + + return sampleStudents; + } + + public static ReadOnlySourceControl getSampleSourceControl() { + SourceControl sampleAb = new SourceControl(); + for (Student sampleStudent : getSampleStudents()) { + sampleAb.addStudent(sampleStudent); + } + return sampleAb; + } + + /** + * Generates a random {@code Id}. A random six digit number is generated and a '0' is prepended to it. + * Ensures that only unique IDs will be generated each time. + */ + public static Id getRandomStudentId() { + int randomSixDigitNumber = random.nextInt((int) Math.pow(10, 6)); + String studentId = "E" + String.format("%07d", randomSixDigitNumber); + + if (TAKEN_IDS.contains(studentId)) { + // If the generated student ID has already been taken, generate a new student ID. + return getRandomStudentId(); + } + + TAKEN_IDS.add(studentId); + return new Id(studentId); + } + + public static List getRandomGroups(String prefix) { + assert MAX_GROUP_NUMBER >= 1; + List groupNumbers = IntStream.rangeClosed(1, MAX_GROUP_NUMBER).boxed().collect(Collectors.toList()); + + assert NUMBER_OF_SUFFIXES >= 1; + List suffixes = IntStream.range(0, NUMBER_OF_SUFFIXES).boxed() + .map(num -> (char) ('A' + num)).collect(Collectors.toList()); + + List groups = new ArrayList<>(); + for (Integer groupNumber : groupNumbers) { + for (Character suffix : suffixes) { + String groupName = prefix + String.format("%02d", groupNumber) + suffix; + groups.add(new Group(groupName)); + } + } + + return groups; + } + + /** + * Returns a random group from the list of generated sample tutorial groups. + */ + public static Group getRandomTutorialGroup() { + return SAMPLE_TUTORIAL_GROUPS.get(random.nextInt(SAMPLE_TUTORIAL_GROUPS.size())); + } + + /** + * Returns a random group from the list of generated sample recitation groups. + */ + public static Group getRandomRecitationGroup() { + return SAMPLE_RECITATION_GROUPS.get(random.nextInt(SAMPLE_RECITATION_GROUPS.size())); + } + + /** + * Generates a score map for the sample assessments. + */ + public static Map getRandomAssessmentScores() { + Map scores = new LinkedHashMap<>(); + + for (Assessment assessment : SAMPLE_ASSESSMENTS) { + String s = String.format("%.2f", getRandomScore()); + scores.put(assessment, new Score(s)); + } + + return scores; + } + + /** + * Generates a random score, following a normal distribution with mean = 60 and standard deviation = 15. + */ + public static double getRandomScore() { + double randomScore = MEAN_SCORE + random.nextGaussian() * SCORE_STANDARD_DEVIATION; + randomScore = Math.min(randomScore, Score.MAX_SCORE); + randomScore = Math.max(randomScore, Score.MIN_SCORE); + return randomScore; + } + + /** + * Gets a random tag from the sample tags. + */ + public static String getRandomTag() { + return SAMPLE_TAG_NAMES[random.nextInt(SAMPLE_TAG_NAMES.length)]; + } + + /** + * Returns a group list containing the list of strings given. + */ + public static List getGroupList(String... strings) { + return Arrays.stream(strings) + .map(Group::new) + .collect(Collectors.toList()); + } + + /** + * Returns a tag set containing the list of strings given. + */ + public static Set getTagSet(String... strings) { + return Arrays.stream(strings) + .map(Tag::new) + .collect(Collectors.toSet()); + } + +} diff --git a/src/main/java/seedu/sourcecontrol/storage/JsonAdaptedAssessment.java b/src/main/java/seedu/sourcecontrol/storage/JsonAdaptedAssessment.java new file mode 100644 index 00000000000..e7ef574389f --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/storage/JsonAdaptedAssessment.java @@ -0,0 +1,67 @@ +package seedu.sourcecontrol.storage; + +import java.util.LinkedHashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.sourcecontrol.commons.exceptions.IllegalValueException; +import seedu.sourcecontrol.model.student.assessment.Assessment; +import seedu.sourcecontrol.model.student.assessment.Score; +import seedu.sourcecontrol.model.student.id.Id; + +public class JsonAdaptedAssessment { + private final String name; + private final Map scores; + + /** + * Constructs a {@code JsonAdaptedAssessment} with the given {@code assessmentName}. + */ + @JsonCreator + public JsonAdaptedAssessment(@JsonProperty("name") String name, + @JsonProperty("scores") Map scores) { + this.name = name; + this.scores = scores; + } + + /** + * Converts a given {@code Assessment} into this class for Jackson use. + */ + public JsonAdaptedAssessment(Assessment source) { + name = source.name; + scores = new LinkedHashMap<>(); + for (Id id : source.scores.keySet()) { + scores.put(id.value, source.scores.get(id).value); + } + } + + /** + * Converts this Jackson-friendly adapted tag object into the model's {@code Assessment} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted assessment. + */ + public Assessment toModelType() throws IllegalValueException { + if (!Assessment.isValidAssessment(name)) { + throw new IllegalValueException(Assessment.MESSAGE_CONSTRAINTS); + } + + Assessment assessment = new Assessment(name); + + // scores is not yet existent in the database + if (scores == null) { + return assessment; + } + + for (String id : scores.keySet()) { + if (!Id.isValidID(id)) { + throw new IllegalValueException(Id.MESSAGE_CONSTRAINTS); + } + if (!Score.isValidScore(scores.get(id))) { + throw new IllegalValueException(Score.MESSAGE_CONSTRAINTS); + } + assessment.scores.put(new Id(id), new Score(scores.get(id))); + } + return assessment; + } +} diff --git a/src/main/java/seedu/sourcecontrol/storage/JsonAdaptedGroup.java b/src/main/java/seedu/sourcecontrol/storage/JsonAdaptedGroup.java new file mode 100644 index 00000000000..98eac43ca1a --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/storage/JsonAdaptedGroup.java @@ -0,0 +1,38 @@ +package seedu.sourcecontrol.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.sourcecontrol.commons.exceptions.IllegalValueException; +import seedu.sourcecontrol.model.student.group.Group; + +public class JsonAdaptedGroup { + private final String name; + + /** + * Constructs a {@code JsonAdaptedGroup} with the given {@code groupName}. + */ + @JsonCreator + public JsonAdaptedGroup(@JsonProperty("name") String name) { + this.name = name; + } + + /** + * Converts a given {@code Group} into this class for Jackson use. + */ + public JsonAdaptedGroup(Group source) { + name = source.name; + } + + /** + * Converts this Jackson-friendly adapted tag object into the model's {@code Group} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted group. + */ + public Group toModelType() throws IllegalValueException { + if (!Group.isValidGroup(name)) { + throw new IllegalValueException(Group.MESSAGE_CONSTRAINTS); + } + return new Group(name); + } +} diff --git a/src/main/java/seedu/sourcecontrol/storage/JsonAdaptedStudent.java b/src/main/java/seedu/sourcecontrol/storage/JsonAdaptedStudent.java new file mode 100644 index 00000000000..fb4c1dbed2f --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/storage/JsonAdaptedStudent.java @@ -0,0 +1,119 @@ +package seedu.sourcecontrol.storage; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.sourcecontrol.commons.exceptions.IllegalValueException; +import seedu.sourcecontrol.model.student.Student; +import seedu.sourcecontrol.model.student.assessment.Assessment; +import seedu.sourcecontrol.model.student.assessment.Score; +import seedu.sourcecontrol.model.student.group.Group; +import seedu.sourcecontrol.model.student.id.Id; +import seedu.sourcecontrol.model.student.name.Name; +import seedu.sourcecontrol.model.student.tag.Tag; + +/** + * Jackson-friendly version of {@link Student}. + */ +class JsonAdaptedStudent { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Student's %s field is missing!"; + + private final String name; + private final String id; + private final List groups = new ArrayList<>(); + // Note: Scores are stored on the JSON-adapted Assessments, and retrieved from there to avoid duplicate objects + private final List assessments = new ArrayList<>(); + private final List tagged = new ArrayList<>(); + + /** + * Constructs a {@code JsonAdaptedStudent} with the given student details. + */ + @JsonCreator + public JsonAdaptedStudent(@JsonProperty("name") String name, @JsonProperty("id") String id, + @JsonProperty("groups") List groups, + @JsonProperty("assessments") List assessments, + @JsonProperty("tagged") List tagged) { + this.name = name; + this.id = id; + if (groups != null) { + this.groups.addAll(groups); + } + if (assessments != null) { + this.assessments.addAll(assessments); + } + if (tagged != null) { + this.tagged.addAll(tagged); + } + } + + /** + * Converts a given {@code Student} into this class for Jackson use. + */ + public JsonAdaptedStudent(Student source) { + name = source.getName().fullName; + id = source.getId().value; + tagged.addAll(source.getTags().stream() + .map(JsonAdaptedTag::new) + .collect(Collectors.toList())); + groups.addAll(source.getGroups().stream() + .map(group -> group.name) + .collect(Collectors.toList())); + assessments.addAll(source.getScores().keySet().stream() + .map(Assessment::getName) + .collect(Collectors.toList())); + } + + /** + * Converts this Jackson-friendly adapted student object into the model's {@code Student} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted student. + */ + public Student toModelType(List groupList, List assessmentList) throws IllegalValueException { + if (name == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); + } + if (!Name.isValidName(name)) { + throw new IllegalValueException(Name.MESSAGE_CONSTRAINTS); + } + final Name modelName = new Name(name); + + if (id == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Id.class.getSimpleName())); + } + if (!Id.isValidID(id)) { + throw new IllegalValueException(Id.MESSAGE_CONSTRAINTS); + } + final Id modelId = new Id(id); + + final Map modelScores = new LinkedHashMap<>(); + + assessmentList.stream() + .filter(assessment -> assessments.contains(assessment.getName())) + .forEach(assessment -> modelScores.put(assessment, assessment.scores.get(modelId))); + + final List modelGroups = groupList.stream() + .filter(group -> groups.contains(group.name)) + .collect(Collectors.toList()); + for (Group group : modelGroups) { + group.addStudent(modelId); + } + + final List studentTags = new ArrayList<>(); + for (JsonAdaptedTag tag : tagged) { + studentTags.add(tag.toModelType()); + } + final Set modelTags = new HashSet<>(studentTags); + + + return new Student(modelName, modelId, modelGroups, modelScores, modelTags); + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedTag.java b/src/main/java/seedu/sourcecontrol/storage/JsonAdaptedTag.java similarity index 87% rename from src/main/java/seedu/address/storage/JsonAdaptedTag.java rename to src/main/java/seedu/sourcecontrol/storage/JsonAdaptedTag.java index 0df22bdb754..9fa18e5a5bd 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedTag.java +++ b/src/main/java/seedu/sourcecontrol/storage/JsonAdaptedTag.java @@ -1,10 +1,10 @@ -package seedu.address.storage; +package seedu.sourcecontrol.storage; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.tag.Tag; +import seedu.sourcecontrol.commons.exceptions.IllegalValueException; +import seedu.sourcecontrol.model.student.tag.Tag; /** * Jackson-friendly version of {@link Tag}. diff --git a/src/main/java/seedu/sourcecontrol/storage/JsonSerializableSourceControl.java b/src/main/java/seedu/sourcecontrol/storage/JsonSerializableSourceControl.java new file mode 100644 index 00000000000..f1f5ee67130 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/storage/JsonSerializableSourceControl.java @@ -0,0 +1,99 @@ +package seedu.sourcecontrol.storage; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; + +import seedu.sourcecontrol.commons.exceptions.IllegalValueException; +import seedu.sourcecontrol.model.ReadOnlySourceControl; +import seedu.sourcecontrol.model.SourceControl; +import seedu.sourcecontrol.model.student.Student; +import seedu.sourcecontrol.model.student.assessment.Assessment; +import seedu.sourcecontrol.model.student.group.Group; + +/** + * An Immutable SourceControl that is serializable to JSON format. + */ +@JsonRootName(value = "sourcecontrol") +class JsonSerializableSourceControl { + + public static final String MESSAGE_DUPLICATE_STUDENT = "Student list contains duplicate student(s)."; + public static final String MESSAGE_DUPLICATE_ASSESSMENT = "Assessment list contains duplicate assessment(s)."; + public static final String MESSAGE_DUPLICATE_GROUP = "Group list contains duplicate group(s)."; + + private final List students = new ArrayList<>(); + private final List groups = new ArrayList<>(); + private final List assessments = new ArrayList<>(); + + /** + * Constructs a {@code JsonSerializableSourceControl} with the given students. + */ + @JsonCreator + public JsonSerializableSourceControl(@JsonProperty("students") List students, + @JsonProperty("groups") List groups, + @JsonProperty("assessments") List assessments) { + this.students.addAll(students); + this.groups.addAll(groups); + this.assessments.addAll(assessments); + } + + /** + * Converts a given {@code ReadOnlySourceControl} into this class for Jackson use. + * + * @param source future changes to this will not affect the created {@code JsonSerializableSourceControl}. + */ + public JsonSerializableSourceControl(ReadOnlySourceControl source) { + students.addAll(source.getStudentList().stream() + .map(JsonAdaptedStudent::new) + .collect(Collectors.toList())); + groups.addAll(source.getGroupList().stream() + .map(JsonAdaptedGroup::new) + .collect(Collectors.toList())); + assessments.addAll(source.getAssessmentList().stream() + .map(JsonAdaptedAssessment::new) + .collect(Collectors.toList())); + } + + /** + * Converts this Source Control into the model's {@code SourceControl} object. + * + * @throws IllegalValueException if there were any data constraints violated. + */ + public SourceControl toModelType() throws IllegalValueException { + SourceControl sourceControl = new SourceControl(); + + for (JsonAdaptedAssessment jsonAdaptedAssessment : assessments) { + Assessment assessment = jsonAdaptedAssessment.toModelType(); + if (sourceControl.hasAssessment(assessment)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_ASSESSMENT); + } + sourceControl.addAssessment(assessment); + } + + for (JsonAdaptedGroup jsonAdaptedGroup : groups) { + Group group = jsonAdaptedGroup.toModelType(); + if (sourceControl.hasGroup(group)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_GROUP); + } + sourceControl.addGroup(group); + } + + + for (JsonAdaptedStudent jsonAdaptedStudent : students) { + Student student = jsonAdaptedStudent.toModelType( + sourceControl.getGroupList(), + sourceControl.getAssessmentList()); + if (sourceControl.hasStudent(student)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_STUDENT); + } + sourceControl.addStudent(student); + } + + return sourceControl; + } + +} diff --git a/src/main/java/seedu/sourcecontrol/storage/JsonSourceControlStorage.java b/src/main/java/seedu/sourcecontrol/storage/JsonSourceControlStorage.java new file mode 100644 index 00000000000..565957ea3d8 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/storage/JsonSourceControlStorage.java @@ -0,0 +1,80 @@ +package seedu.sourcecontrol.storage; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +import seedu.sourcecontrol.commons.core.LogsCenter; +import seedu.sourcecontrol.commons.exceptions.DataConversionException; +import seedu.sourcecontrol.commons.exceptions.IllegalValueException; +import seedu.sourcecontrol.commons.util.FileUtil; +import seedu.sourcecontrol.commons.util.JsonUtil; +import seedu.sourcecontrol.model.ReadOnlySourceControl; + +/** + * A class to access SourceControl data stored as a json file on the hard disk. + */ +public class JsonSourceControlStorage implements SourceControlStorage { + + private static final Logger logger = LogsCenter.getLogger(JsonSourceControlStorage.class); + + private Path filePath; + + public JsonSourceControlStorage(Path filePath) { + this.filePath = filePath; + } + + public Path getSourceControlFilePath() { + return filePath; + } + + @Override + public Optional readSourceControl() throws DataConversionException { + return readSourceControl(filePath); + } + + /** + * Similar to {@link #readSourceControl()}. + * + * @param filePath location of the data. Cannot be null. + * @throws DataConversionException if the file is not in the correct format. + */ + public Optional readSourceControl(Path filePath) throws DataConversionException { + requireNonNull(filePath); + + Optional jsonSourceControl = JsonUtil.readJsonFile( + filePath, JsonSerializableSourceControl.class); + if (!jsonSourceControl.isPresent()) { + return Optional.empty(); + } + + try { + return Optional.of(jsonSourceControl.get().toModelType()); + } catch (IllegalValueException ive) { + logger.info("Illegal values found in " + filePath + ": " + ive.getMessage()); + throw new DataConversionException(ive); + } + } + + @Override + public void saveSourceControl(ReadOnlySourceControl sourceControl) throws IOException { + saveSourceControl(sourceControl, filePath); + } + + /** + * Similar to {@link #saveSourceControl(ReadOnlySourceControl)}. + * + * @param filePath location of the data. Cannot be null. + */ + public void saveSourceControl(ReadOnlySourceControl sourceControl, Path filePath) throws IOException { + requireNonNull(sourceControl); + requireNonNull(filePath); + + FileUtil.createIfMissing(filePath); + JsonUtil.saveJsonFile(new JsonSerializableSourceControl(sourceControl), filePath); + } + +} diff --git a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java b/src/main/java/seedu/sourcecontrol/storage/JsonUserPrefsStorage.java similarity index 81% rename from src/main/java/seedu/address/storage/JsonUserPrefsStorage.java rename to src/main/java/seedu/sourcecontrol/storage/JsonUserPrefsStorage.java index bc2bbad84aa..e607f303c1e 100644 --- a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java +++ b/src/main/java/seedu/sourcecontrol/storage/JsonUserPrefsStorage.java @@ -1,13 +1,13 @@ -package seedu.address.storage; +package seedu.sourcecontrol.storage; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.util.JsonUtil; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; +import seedu.sourcecontrol.commons.exceptions.DataConversionException; +import seedu.sourcecontrol.commons.util.JsonUtil; +import seedu.sourcecontrol.model.ReadOnlyUserPrefs; +import seedu.sourcecontrol.model.UserPrefs; /** * A class to access UserPrefs stored in the hard disk as a json file diff --git a/src/main/java/seedu/sourcecontrol/storage/SourceControlStorage.java b/src/main/java/seedu/sourcecontrol/storage/SourceControlStorage.java new file mode 100644 index 00000000000..4a0a6edaf5a --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/storage/SourceControlStorage.java @@ -0,0 +1,46 @@ +package seedu.sourcecontrol.storage; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import seedu.sourcecontrol.commons.exceptions.DataConversionException; +import seedu.sourcecontrol.model.ReadOnlySourceControl; +import seedu.sourcecontrol.model.SourceControl; + +/** + * Represents a storage for {@link SourceControl}. + */ +public interface SourceControlStorage { + + /** + * Returns the file path of the data file. + */ + Path getSourceControlFilePath(); + + /** + * Returns SourceControl data as a {@link ReadOnlySourceControl}. + * Returns {@code Optional.empty()} if storage file is not found. + * @throws DataConversionException if the data in storage is not in the expected format. + * @throws IOException if there was any problem when reading from the storage. + */ + Optional readSourceControl() throws DataConversionException, IOException; + + /** + * @see #getSourceControlFilePath() + */ + Optional readSourceControl(Path filePath) throws DataConversionException, IOException; + + /** + * Saves the given {@link ReadOnlySourceControl} to the storage. + * @param sourceControl cannot be null. + * @throws IOException if there was any problem writing to the file. + */ + void saveSourceControl(ReadOnlySourceControl sourceControl) throws IOException; + + /** + * @see #saveSourceControl(ReadOnlySourceControl) + */ + void saveSourceControl(ReadOnlySourceControl sourceControl, Path filePath) throws IOException; + +} diff --git a/src/main/java/seedu/sourcecontrol/storage/Storage.java b/src/main/java/seedu/sourcecontrol/storage/Storage.java new file mode 100644 index 00000000000..c19d80eb9f3 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/storage/Storage.java @@ -0,0 +1,32 @@ +package seedu.sourcecontrol.storage; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import seedu.sourcecontrol.commons.exceptions.DataConversionException; +import seedu.sourcecontrol.model.ReadOnlySourceControl; +import seedu.sourcecontrol.model.ReadOnlyUserPrefs; +import seedu.sourcecontrol.model.UserPrefs; + +/** + * API of the Storage component + */ +public interface Storage extends SourceControlStorage, UserPrefsStorage { + + @Override + Optional readUserPrefs() throws DataConversionException, IOException; + + @Override + void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException; + + @Override + Path getSourceControlFilePath(); + + @Override + Optional readSourceControl() throws DataConversionException, IOException; + + @Override + void saveSourceControl(ReadOnlySourceControl sourceControl) throws IOException; + +} diff --git a/src/main/java/seedu/sourcecontrol/storage/StorageManager.java b/src/main/java/seedu/sourcecontrol/storage/StorageManager.java new file mode 100644 index 00000000000..b0486df88cc --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/storage/StorageManager.java @@ -0,0 +1,80 @@ +package seedu.sourcecontrol.storage; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +import seedu.sourcecontrol.commons.core.LogsCenter; +import seedu.sourcecontrol.commons.exceptions.DataConversionException; +import seedu.sourcecontrol.model.ReadOnlySourceControl; +import seedu.sourcecontrol.model.ReadOnlyUserPrefs; +import seedu.sourcecontrol.model.UserPrefs; + +/** + * Manages storage of SourceControl data in local storage. + */ +public class StorageManager implements Storage { + + private static final Logger logger = LogsCenter.getLogger(StorageManager.class); + private SourceControlStorage sourceControlStorage; + private UserPrefsStorage userPrefsStorage; + + /** + * Creates a {@code StorageManager} with the given {@code SourceControlStorage} and {@code UserPrefStorage}. + */ + public StorageManager(SourceControlStorage sourceControlStorage, UserPrefsStorage userPrefsStorage) { + super(); + this.sourceControlStorage = sourceControlStorage; + this.userPrefsStorage = userPrefsStorage; + } + + // ================ UserPrefs methods ============================== + + @Override + public Path getUserPrefsFilePath() { + return userPrefsStorage.getUserPrefsFilePath(); + } + + @Override + public Optional readUserPrefs() throws DataConversionException, IOException { + return userPrefsStorage.readUserPrefs(); + } + + @Override + public void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException { + userPrefsStorage.saveUserPrefs(userPrefs); + } + + + // ================ SourceControl methods ============================== + + @Override + public Path getSourceControlFilePath() { + return sourceControlStorage.getSourceControlFilePath(); + } + + @Override + public Optional readSourceControl() throws DataConversionException, IOException { + return readSourceControl(sourceControlStorage.getSourceControlFilePath()); + } + + @Override + public Optional readSourceControl(Path filePath) + throws DataConversionException, IOException { + logger.fine("Attempting to read data from file: " + filePath); + return sourceControlStorage.readSourceControl(filePath); + } + + @Override + public void saveSourceControl(ReadOnlySourceControl sourceControl) throws IOException { + saveSourceControl(sourceControl, sourceControlStorage.getSourceControlFilePath()); + } + + @Override + public void saveSourceControl(ReadOnlySourceControl sourceControl, Path filePath) throws IOException { + logger.fine("Attempting to write to data file: " + filePath); + sourceControlStorage.saveSourceControl(sourceControl, filePath); + } + +} diff --git a/src/main/java/seedu/address/storage/UserPrefsStorage.java b/src/main/java/seedu/sourcecontrol/storage/UserPrefsStorage.java similarity index 69% rename from src/main/java/seedu/address/storage/UserPrefsStorage.java rename to src/main/java/seedu/sourcecontrol/storage/UserPrefsStorage.java index 29eef178dbc..2010ffa707a 100644 --- a/src/main/java/seedu/address/storage/UserPrefsStorage.java +++ b/src/main/java/seedu/sourcecontrol/storage/UserPrefsStorage.java @@ -1,15 +1,15 @@ -package seedu.address.storage; +package seedu.sourcecontrol.storage; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; +import seedu.sourcecontrol.commons.exceptions.DataConversionException; +import seedu.sourcecontrol.model.ReadOnlyUserPrefs; +import seedu.sourcecontrol.model.UserPrefs; /** - * Represents a storage for {@link seedu.address.model.UserPrefs}. + * Represents a storage for {@link seedu.sourcecontrol.model.UserPrefs}. */ public interface UserPrefsStorage { @@ -27,7 +27,7 @@ public interface UserPrefsStorage { Optional readUserPrefs() throws DataConversionException, IOException; /** - * Saves the given {@link seedu.address.model.ReadOnlyUserPrefs} to the storage. + * Saves the given {@link seedu.sourcecontrol.model.ReadOnlyUserPrefs} to the storage. * @param userPrefs cannot be null. * @throws IOException if there was any problem writing to the file. */ diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/seedu/sourcecontrol/ui/CommandBox.java similarity index 57% rename from src/main/java/seedu/address/ui/CommandBox.java rename to src/main/java/seedu/sourcecontrol/ui/CommandBox.java index 9e75478664b..1bbba6f83d9 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/seedu/sourcecontrol/ui/CommandBox.java @@ -1,12 +1,16 @@ -package seedu.address.ui; +package seedu.sourcecontrol.ui; + +import java.util.ArrayList; +import java.util.List; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.TextField; +import javafx.scene.input.KeyEvent; import javafx.scene.layout.Region; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.exceptions.ParseException; +import seedu.sourcecontrol.logic.commands.CommandResult; +import seedu.sourcecontrol.logic.commands.exceptions.CommandException; +import seedu.sourcecontrol.logic.parser.exceptions.ParseException; /** * The UI component that is responsible for receiving user command inputs. @@ -18,6 +22,11 @@ public class CommandBox extends UiPart { private final CommandExecutor commandExecutor; + // To keep track of input history so user can switch between past inputs + private final List pastInputs = new ArrayList<>(); + private int inputIndex = -1; + private String currentInput; + @FXML private TextField commandTextField; @@ -44,11 +53,45 @@ private void handleCommandEntered() { try { commandExecutor.execute(commandText); commandTextField.setText(""); + pastInputs.add(0, commandText); + inputIndex = -1; } catch (CommandException | ParseException e) { setStyleToIndicateCommandFailure(); } } + /** + * Switches between past inputs when user keys up or down. + */ + @FXML + private void handleUserKeystroke(KeyEvent keyEvent) { + if (inputIndex == -1) { + // save the current input so it's accessible when returning + currentInput = commandTextField.getText(); + } + + switch (keyEvent.getCode()) { + case UP: + if (inputIndex + 1 < pastInputs.size()) { + inputIndex++; + commandTextField.setText(pastInputs.get(inputIndex)); + commandTextField.end(); + } + break; + case DOWN: + if (inputIndex > -1) { + inputIndex--; + String newText = inputIndex == -1 ? currentInput : pastInputs.get(inputIndex); + commandTextField.setText(newText); + commandTextField.end(); + } + break; + default: + // nothing special to do otherwise + break; + } + } + /** * Sets the command box style to use the default style. */ @@ -77,7 +120,7 @@ public interface CommandExecutor { /** * Executes the command and returns the result. * - * @see seedu.address.logic.Logic#execute(String) + * @see seedu.sourcecontrol.logic.Logic#execute(String) */ CommandResult execute(String commandText) throws CommandException, ParseException; } diff --git a/src/main/java/seedu/sourcecontrol/ui/GraphDisplay.java b/src/main/java/seedu/sourcecontrol/ui/GraphDisplay.java new file mode 100644 index 00000000000..99d68bd35af --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/ui/GraphDisplay.java @@ -0,0 +1,33 @@ +package seedu.sourcecontrol.ui; + + +import javafx.fxml.FXML; +import javafx.scene.chart.Chart; +import javafx.scene.control.TextArea; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; + +public class GraphDisplay extends UiPart { + + private static final String FXML = "GraphDisplay.fxml"; + + @FXML + private StackPane placeHolder; + + @FXML + private TextArea resultDisplay; + + public GraphDisplay() { + super(FXML); + } + + public void setChart(Chart chart) { + // to make the image save properly + chart.setAnimated(false); + placeHolder.getChildren().add(chart); + } + + public void clearCharts() { + placeHolder.getChildren().clear(); + } +} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/sourcecontrol/ui/HelpWindow.java similarity index 92% rename from src/main/java/seedu/address/ui/HelpWindow.java rename to src/main/java/seedu/sourcecontrol/ui/HelpWindow.java index 9a665915949..8da1d1caf60 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/sourcecontrol/ui/HelpWindow.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package seedu.sourcecontrol.ui; import java.util.logging.Logger; @@ -8,14 +8,14 @@ import javafx.scene.input.Clipboard; import javafx.scene.input.ClipboardContent; import javafx.stage.Stage; -import seedu.address.commons.core.LogsCenter; +import seedu.sourcecontrol.commons.core.LogsCenter; /** * Controller for a help page */ 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://ay2122s1-cs2103t-w08-2.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/sourcecontrol/ui/InfoDisplay.java b/src/main/java/seedu/sourcecontrol/ui/InfoDisplay.java new file mode 100644 index 00000000000..d67153f1168 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/ui/InfoDisplay.java @@ -0,0 +1,152 @@ +package seedu.sourcecontrol.ui; + +import static seedu.sourcecontrol.logic.commands.ShowCommand.Info; + +import java.util.stream.Collectors; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; +import seedu.sourcecontrol.model.student.Student; +import seedu.sourcecontrol.model.student.assessment.Assessment; +import seedu.sourcecontrol.model.student.assessment.AssessmentStatistics; +import seedu.sourcecontrol.model.student.group.Group; +import seedu.sourcecontrol.model.student.tag.Tag; + +/** + * A UI component that displays information of a {@code Student}. + */ +public class InfoDisplay extends UiPart { + + private static final String FXML = "InfoDisplay.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + private Student student; + private Assessment assessment; + private Group group; + + @FXML + private Label name; + @FXML + private Label info1; + @FXML + private Label info2; + @FXML + private Label info3; + @FXML + private Label info4; + @FXML + private Label info5; + + public InfoDisplay() { + super(FXML); + } + + public void setInfo(Info info) { + if (info.getStudent().isPresent()) { + student = info.getStudent().get(); + setStudentInfo(student); + } + + if (info.getAssessment().isPresent()) { + assessment = info.getAssessment().get(); + setAssessmentInfo(assessment); + } + + if (info.getGroup().isPresent()) { + group = info.getGroup().get(); + setGroupInfo(group); + } + } + + /** + * Displays info of a {@code student}. + */ + public void setStudentInfo(Student student) { + name.setText(student.getName().fullName); + info1.setText("ID: " + student.getId().value); + + String groupsString = student.getGroups().stream() + .map(Group::toString).collect(Collectors.joining(", ")); + info2.setText("Groups: " + groupsString); + + String tagsString = student.getTags().stream() + .map(Tag::getTagName).collect(Collectors.joining(", ")); + info3.setText("Tags: " + tagsString); + } + + /** + * Displays info of an {@code assessment}. + */ + public void setAssessmentInfo(Assessment assessment) { + name.setText(assessment.getName()); + + AssessmentStatistics statistics = new AssessmentStatistics(assessment); + + String min = reformatStats(statistics.getMin()); + String max = reformatStats(statistics.getMax()); + String median = reformatStats(statistics.getMedian()); + String mean = reformatStats(statistics.getMean()); + String percentile25 = reformatStats(statistics.getXPercentile(25)); + String percentile75 = reformatStats(statistics.getXPercentile(75)); + + info1.setText("Grade range: " + min + " - " + max); + info2.setText("Median: " + median); + info3.setText("Mean: " + mean); + info4.setText("25th percentile: " + percentile25); + info5.setText("75th percentile: " + percentile75); + } + + /** + * Displays info of a {@code group}. + */ + public void setGroupInfo(Group group) { + name.setText(group.getName()); + + info1.setText("Number of students: " + group.getStudents().size()); + info2.setText("List of students are displayed in the lower left window."); + } + + /** + * Clears any existing info. + */ + public void clearInfo() { + name.setText(null); + info1.setText(null); + info2.setText(null); + info3.setText(null); + info4.setText(null); + info5.setText(null); + } + + /** + * Reformats numeric values to two decimal places. + */ + private static String reformatStats(double stats) { + return String.format("%.2f", stats); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof InfoDisplay)) { + return false; + } + + // state check + InfoDisplay info = (InfoDisplay) other; + return student.equals(info.student); + } +} diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/sourcecontrol/ui/MainWindow.java similarity index 59% rename from src/main/java/seedu/address/ui/MainWindow.java rename to src/main/java/seedu/sourcecontrol/ui/MainWindow.java index 9106c3aa6e5..8b91c2cf818 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/sourcecontrol/ui/MainWindow.java @@ -1,21 +1,33 @@ -package seedu.address.ui; +package seedu.sourcecontrol.ui; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; import java.util.logging.Logger; +import javax.imageio.ImageIO; import javafx.event.ActionEvent; import javafx.fxml.FXML; +import javafx.scene.SnapshotParameters; +import javafx.scene.chart.Chart; import javafx.scene.control.MenuItem; +import javafx.scene.control.SplitPane; import javafx.scene.control.TextInputControl; +import javafx.scene.image.PixelReader; +import javafx.scene.image.WritableImage; import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; import javafx.scene.layout.StackPane; import javafx.stage.Stage; -import seedu.address.commons.core.GuiSettings; -import seedu.address.commons.core.LogsCenter; -import seedu.address.logic.Logic; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.exceptions.ParseException; +import seedu.sourcecontrol.commons.core.GuiSettings; +import seedu.sourcecontrol.commons.core.LogsCenter; +import seedu.sourcecontrol.commons.util.FileUtil; +import seedu.sourcecontrol.logic.Logic; +import seedu.sourcecontrol.logic.commands.CommandResult; +import seedu.sourcecontrol.logic.commands.ShowCommand; +import seedu.sourcecontrol.logic.commands.exceptions.CommandException; +import seedu.sourcecontrol.logic.parser.exceptions.ParseException; /** * The Main Window. Provides the basic application layout containing @@ -23,17 +35,25 @@ */ public class MainWindow extends UiPart { + public static final String GRAPH_SAVE_FAIL = "Graph failed to save"; private static final String FXML = "MainWindow.fxml"; private final Logger logger = LogsCenter.getLogger(getClass()); + private double divideRatio; + private Stage primaryStage; private Logic logic; // Independent Ui parts residing in this Ui container - private PersonListPanel personListPanel; + private StudentListPanel studentListPanel; private ResultDisplay resultDisplay; private HelpWindow helpWindow; + private InfoDisplay infoDisplay; + private GraphDisplay graphDisplay; + + @FXML + private SplitPane splitPane; @FXML private StackPane commandBoxPlaceholder; @@ -42,7 +62,7 @@ public class MainWindow extends UiPart { private MenuItem helpMenuItem; @FXML - private StackPane personListPanelPlaceholder; + private StackPane studentListPanelPlaceholder; @FXML private StackPane resultDisplayPlaceholder; @@ -50,6 +70,12 @@ public class MainWindow extends UiPart { @FXML private StackPane statusbarPlaceholder; + @FXML + private StackPane infoDisplayPlaceholder; + + @FXML + private StackPane graphDisplayPlaceholder; + /** * Creates a {@code MainWindow} with the given {@code Stage} and {@code Logic}. */ @@ -110,17 +136,23 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { * Fills up all the placeholders of this window. */ void fillInnerParts() { - personListPanel = new PersonListPanel(logic.getFilteredPersonList()); - personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + studentListPanel = new StudentListPanel(logic.getFilteredStudentList()); + studentListPanelPlaceholder.getChildren().add(studentListPanel.getRoot()); resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); - StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath()); + StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getSourceControlFilePath()); statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); CommandBox commandBox = new CommandBox(this::executeCommand); commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); + + infoDisplay = new InfoDisplay(); + infoDisplayPlaceholder.getChildren().add(infoDisplay.getRoot()); + + graphDisplay = new GraphDisplay(); + graphDisplayPlaceholder.getChildren().add(graphDisplay.getRoot()); } /** @@ -133,6 +165,7 @@ private void setWindowDefaultSize(GuiSettings guiSettings) { primaryStage.setX(guiSettings.getWindowCoordinates().getX()); primaryStage.setY(guiSettings.getWindowCoordinates().getY()); } + this.divideRatio = guiSettings.getDividerPosition(); } /** @@ -156,27 +189,47 @@ void show() { */ @FXML private void handleExit() { + double[] dividerPositions = splitPane.getDividerPositions(); GuiSettings guiSettings = new GuiSettings(primaryStage.getWidth(), primaryStage.getHeight(), - (int) primaryStage.getX(), (int) primaryStage.getY()); + (int) primaryStage.getX(), (int) primaryStage.getY(), dividerPositions[0]); logic.setGuiSettings(guiSettings); helpWindow.hide(); primaryStage.hide(); } - public PersonListPanel getPersonListPanel() { - return personListPanel; + public StudentListPanel getStudentListPanel() { + return studentListPanel; } /** * Executes the command and returns the result. * - * @see seedu.address.logic.Logic#execute(String) + * @see seedu.sourcecontrol.logic.Logic#execute(String) */ private CommandResult executeCommand(String commandText) throws CommandException, ParseException { try { CommandResult commandResult = logic.execute(commandText); logger.info("Result: " + commandResult.getFeedbackToUser()); resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); + // Clear any old info or charts + infoDisplay.clearInfo(); + graphDisplay.clearCharts(); + + if (commandResult.hasInfo()) { + ShowCommand.Info info = commandResult.getInfo(); + assert info != null; + infoDisplay.setInfo(info); + } + + if (commandResult.hasChart()) { + Chart chart = commandResult.getChart(); + assert chart != null; + graphDisplay.setChart(chart); + logger.info("Graph displayed"); + if (commandResult.hasSavePath()) { + saveGraph(commandResult.getSavePath()); + } + } if (commandResult.isShowHelp()) { handleHelp(); @@ -193,4 +246,36 @@ private CommandResult executeCommand(String commandText) throws CommandException throw e; } } + + public void loadDividerRatio() { + splitPane.setDividerPositions(divideRatio); + } + + private void saveGraph(Path savePath) throws CommandException { + assert savePath != null; + WritableImage img = graphDisplayPlaceholder.snapshot(new SnapshotParameters(), null); + int width = (int) img.getWidth(); + int height = (int) img.getHeight(); + + PixelReader pixelReader = img.getPixelReader(); + + BufferedImage bimg = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + bimg.setRGB(i, j, pixelReader.getArgb(i, j)); + } + } + + File file = savePath.toFile(); + + try { + FileUtil.createIfMissing(savePath); + ImageIO.write(bimg, "png", file); + } catch (IOException e) { + throw new CommandException(GRAPH_SAVE_FAIL); + } + + resultDisplay.setFeedbackToUser("Graph saved successfully at " + savePath); + logger.info("Graph exported to " + savePath); + } } diff --git a/src/main/java/seedu/address/ui/ResultDisplay.java b/src/main/java/seedu/sourcecontrol/ui/ResultDisplay.java similarity index 95% rename from src/main/java/seedu/address/ui/ResultDisplay.java rename to src/main/java/seedu/sourcecontrol/ui/ResultDisplay.java index 7d98e84eedf..9d01c3e9067 100644 --- a/src/main/java/seedu/address/ui/ResultDisplay.java +++ b/src/main/java/seedu/sourcecontrol/ui/ResultDisplay.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package seedu.sourcecontrol.ui; import static java.util.Objects.requireNonNull; diff --git a/src/main/java/seedu/address/ui/StatusBarFooter.java b/src/main/java/seedu/sourcecontrol/ui/StatusBarFooter.java similarity index 76% rename from src/main/java/seedu/address/ui/StatusBarFooter.java rename to src/main/java/seedu/sourcecontrol/ui/StatusBarFooter.java index b577f829423..409da3f5de1 100644 --- a/src/main/java/seedu/address/ui/StatusBarFooter.java +++ b/src/main/java/seedu/sourcecontrol/ui/StatusBarFooter.java @@ -1,11 +1,11 @@ -package seedu.address.ui; +package seedu.sourcecontrol.ui; import java.nio.file.Path; -import java.nio.file.Paths; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.layout.Region; +import seedu.sourcecontrol.commons.util.FileUtil; /** * A ui for the status bar that is displayed at the footer of the application. @@ -22,7 +22,7 @@ public class StatusBarFooter extends UiPart { */ public StatusBarFooter(Path saveLocation) { super(FXML); - saveLocationStatus.setText(Paths.get(".").resolve(saveLocation).toString()); + saveLocationStatus.setText(FileUtil.getRelativePathString(saveLocation)); } } diff --git a/src/main/java/seedu/sourcecontrol/ui/StudentCard.java b/src/main/java/seedu/sourcecontrol/ui/StudentCard.java new file mode 100644 index 00000000000..715816fe2ef --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/ui/StudentCard.java @@ -0,0 +1,87 @@ +package seedu.sourcecontrol.ui; + +import java.util.Comparator; +import java.util.Map; +import java.util.stream.Collectors; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.sourcecontrol.model.student.Student; +import seedu.sourcecontrol.model.student.group.Group; + +/** + * A UI component that displays information of a {@code Student}. + */ +public class StudentCard extends UiPart { + + private static final String FXML = "StudentListCard.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on SourceControl level 4 + */ + + public final Student student; + + @FXML + private HBox cardPane; + @FXML + private Label name; + @FXML + private Label id; + @FXML + private Label nusNetId; + @FXML + private Label group; + @FXML + private Label assessment; + @FXML + private FlowPane tags; + + /** + * Creates a {@code StudentCard} with the given {@code Student} and index to display. + */ + public StudentCard(Student student, int displayedIndex) { + super(FXML); + this.student = student; + id.setText(displayedIndex + ". "); + name.setText(student.getName().fullName); + nusNetId.setText("ID: " + student.getId().value); + + String groupsString = student.getGroups().stream() + .map(Group::toString).sorted().collect(Collectors.joining(", ")); + group.setText("Groups: " + groupsString); + + String assessmentsString = student.getScores().entrySet().stream() + .map(Map.Entry::toString).collect(Collectors.joining(", ")); + assessment.setText("Assessments: " + assessmentsString); + + student.getTags().stream() + .sorted(Comparator.comparing(tag -> tag.tagName)) + .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof StudentCard)) { + return false; + } + + // state check + StudentCard card = (StudentCard) other; + return id.getText().equals(card.id.getText()) + && student.equals(card.student); + } +} diff --git a/src/main/java/seedu/sourcecontrol/ui/StudentListPanel.java b/src/main/java/seedu/sourcecontrol/ui/StudentListPanel.java new file mode 100644 index 00000000000..3708b55d0c7 --- /dev/null +++ b/src/main/java/seedu/sourcecontrol/ui/StudentListPanel.java @@ -0,0 +1,49 @@ +package seedu.sourcecontrol.ui; + +import java.util.logging.Logger; + +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.sourcecontrol.commons.core.LogsCenter; +import seedu.sourcecontrol.model.student.Student; + +/** + * Panel containing the list of students. + */ +public class StudentListPanel extends UiPart { + private static final String FXML = "StudentListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(StudentListPanel.class); + + @FXML + private ListView studentListView; + + /** + * Creates a {@code StudentListPanel} with the given {@code ObservableList}. + */ + public StudentListPanel(ObservableList studentList) { + super(FXML); + studentListView.setItems(studentList); + studentListView.setCellFactory(listView -> new StudentListViewCell()); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Student} using a {@code StudentCard}. + */ + class StudentListViewCell extends ListCell { + @Override + protected void updateItem(Student student, boolean empty) { + super.updateItem(student, empty); + + if (empty || student == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new StudentCard(student, getIndex() + 1).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/Ui.java b/src/main/java/seedu/sourcecontrol/ui/Ui.java similarity index 83% rename from src/main/java/seedu/address/ui/Ui.java rename to src/main/java/seedu/sourcecontrol/ui/Ui.java index 17aa0b494fe..e259044d1eb 100644 --- a/src/main/java/seedu/address/ui/Ui.java +++ b/src/main/java/seedu/sourcecontrol/ui/Ui.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package seedu.sourcecontrol.ui; import javafx.stage.Stage; diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/sourcecontrol/ui/UiManager.java similarity index 89% rename from src/main/java/seedu/address/ui/UiManager.java rename to src/main/java/seedu/sourcecontrol/ui/UiManager.java index 882027e4537..c0d1d77c06d 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/sourcecontrol/ui/UiManager.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package seedu.sourcecontrol.ui; import java.util.logging.Logger; @@ -7,10 +7,10 @@ import javafx.scene.control.Alert.AlertType; import javafx.scene.image.Image; import javafx.stage.Stage; -import seedu.address.MainApp; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.util.StringUtil; -import seedu.address.logic.Logic; +import seedu.sourcecontrol.MainApp; +import seedu.sourcecontrol.commons.core.LogsCenter; +import seedu.sourcecontrol.commons.util.StringUtil; +import seedu.sourcecontrol.logic.Logic; /** * The manager of the UI component. @@ -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/source_control.png"; private Logic logic; private MainWindow mainWindow; @@ -44,6 +44,7 @@ public void start(Stage primaryStage) { mainWindow = new MainWindow(primaryStage, logic); mainWindow.show(); //This should be called before creating other UI parts mainWindow.fillInnerParts(); + mainWindow.loadDividerRatio(); } catch (Throwable e) { logger.severe(StringUtil.getDetails(e)); diff --git a/src/main/java/seedu/address/ui/UiPart.java b/src/main/java/seedu/sourcecontrol/ui/UiPart.java similarity index 97% rename from src/main/java/seedu/address/ui/UiPart.java rename to src/main/java/seedu/sourcecontrol/ui/UiPart.java index fc820e01a9c..9a42f8e4862 100644 --- a/src/main/java/seedu/address/ui/UiPart.java +++ b/src/main/java/seedu/sourcecontrol/ui/UiPart.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package seedu.sourcecontrol.ui; import static java.util.Objects.requireNonNull; @@ -6,7 +6,7 @@ import java.net.URL; import javafx.fxml.FXMLLoader; -import seedu.address.MainApp; +import seedu.sourcecontrol.MainApp; /** * Represents a distinct part of the UI. e.g. Windows, dialogs, panels, status bars, etc. 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/source_control.png b/src/main/resources/images/source_control.png new file mode 100644 index 00000000000..27f3eaf0739 Binary files /dev/null and b/src/main/resources/images/source_control.png differ diff --git a/src/main/resources/view/CommandBox.fxml b/src/main/resources/view/CommandBox.fxml index 09f6d6fe9e4..b3abdb4bd09 100644 --- a/src/main/resources/view/CommandBox.fxml +++ b/src/main/resources/view/CommandBox.fxml @@ -4,6 +4,5 @@ - + - diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 9ce9bcfb569..de36e4f012f 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -123,13 +123,13 @@ .cell_big_label { -fx-font-family: "Open Sans Semibold"; -fx-font-size: 16px; - -fx-text-fill: #010504; + -fx-text-fill: white; } .cell_small_label { -fx-font-family: "Open Sans Regular"; -fx-font-size: 13px; - -fx-text-fill: #010504; + -fx-text-fill: white; } .stack-pane { @@ -307,6 +307,49 @@ -fx-padding: 8 1 8 1; } +/* Graph */ + +.axis { + -fx-font-size: 2.0em; + -fx-tick-label-fill: white; + -fx-tick-length: 20; + -fx-minor-tick-length: 10; +} + +.axis-label { + -fx-text-fill: white; +} + +.chart-title { + -fx-text-fill: white; +} + +.chart-bar { + -fx-background-color: #3e7b91; +} + +.chart-title { + -fx-padding: 0 0 0 56; +} + +.chart-legend { + -fx-background-color: transparent; + -fx-padding: 0 0 0 73; +} + +.chart-legend-item{ + -fx-text-fill: white; +} + +#chart1 .default-color1.chart-series-line { + -fx-stroke: transparent; +} + +#chart1 .default-color2.chart-series-line { + -fx-stroke: transparent; +} + + #cardPane { -fx-background-color: transparent; -fx-border-width: 0; diff --git a/src/main/resources/view/GraphDisplay.fxml b/src/main/resources/view/GraphDisplay.fxml new file mode 100644 index 00000000000..0b4da61af06 --- /dev/null +++ b/src/main/resources/view/GraphDisplay.fxml @@ -0,0 +1,8 @@ + + + + + + +