diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 07acb40a13d..5fff5df1f0a 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -42,4 +42,4 @@ jobs: if: runner.os == 'Linux' with: file: ${{ github.workspace }}/build/reports/jacoco/coverage/coverage.xml - fail_ci_if_error: true + fail_ci_if_error: false diff --git a/README.md b/README.md index 13f5c77403f..dc406189187 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,22 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) +[![Java CI](https://github.com/AY2122S1-CS2103T-W15-3/tp/actions/workflows/gradle.yml/badge.svg)](https://github.com/AY2122S1-CS2103T-W15-3/tp/actions/workflows/gradle.yml) +[![codecov](https://codecov.io/gh/AY2122S1-CS2103T-W15-3/tp/branch/master/graph/badge.svg?token=40MOICZDNE)](https://codecov.io/gh/AY2122S1-CS2103T-W15-3/tp) ![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. +## Introduction +SoConnect is a desktop application for students :man_technologist::woman_technologist: in [NUS School of Computing](https://www.comp.nus.edu.sg/) +to **manage the contacts** of their professors and tutors, and **keep track of** their busy schedule of **events**. + +## Usage +- Add, delete, view contact information of professors + - Save email addresses, zoom links, office locations and more! +- Add, delete, view events + - Check upcoming events + - [Future implementation] Link events to other contacts as participants! +- View the detailed documentation of this project at our [user guide](https://ay2122s1-cs2103t-w15-3.github.io/tp/UserGuide.html) + +## Acknowledgement + +This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). + +If you would like to contribute code to other SE-EDU projects, see [se-education.org](https://se-education.org#https://se-education.org/#contributing) for more info. diff --git a/build.gradle b/build.gradle index be2d2905dde..9563234d4e9 100644 --- a/build.gradle +++ b/build.gradle @@ -57,6 +57,8 @@ dependencies { implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'mac' implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'linux' + implementation group: 'com.calendarfx', name: 'view', version: '11.10.1' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.7.0' implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.7.4' @@ -66,7 +68,11 @@ dependencies { } shadowJar { - archiveName = 'addressbook.jar' + archiveName = 'soconnect.jar' +} + +run { + enableAssertions = true } defaultTasks 'clean', 'test' diff --git a/codecov.exe b/codecov.exe new file mode 100644 index 00000000000..ae25d325e41 Binary files /dev/null and b/codecov.exe differ diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 1c9514e966a..d70dfd16d50 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -5,55 +5,62 @@ 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` - ## Project team -### John Doe - - -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +### Gordon Yit Hongyao -* Role: Project Advisor + -### Jane Doe +[github](http://github.com/gordon25) | +[portfolio](team/gordon25.md) | +[email](mailto:e0564958@u.nus.edu) - +* Role: Documentation, Scheduling and Tracking +* Responsibilities: Storage -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] -* Role: Team Lead -* Responsibilities: UI +### Janice Chen -### Johnny Doe + - +[github](http://github.com/janjanchen) | +[portfolio](team/janjanchen.md) | +[email](mailto:e0559731@u.nus.edu) -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +* Role: Code Quality +* Responsibilities: Model -* Role: Developer -* Responsibilities: Data -### Jean Doe +### Lee Chun Wei - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[github](http://github.com/chunweii) | +[portfolio](team/chunweii.md) | +[email](mailto:chunweilee.99@u.nus.edu) -* Role: Developer -* Responsibilities: Dev Ops + Threading +* Role: Team Lead, Deliverables and Deadlines +* Responsibilities: UI -### James Doe +### Ng Xiang Jun - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[github](http://github.com/xiangjunn) | +[portfolio](team/xiangjunn.md) | +[email](mailto:ng.xiangjun99@u.nus.edu) -* Role: Developer +* Role: Integration * Responsibilities: UI + +### Pham Chau Giang + + + +[github](http://github.com/pcgiang) | +[portfolio](team/pcgiang.md) | +[email](mailto:e0564779@u.nus.edu) + +* Role: Testing +* Responsibilities: Model diff --git a/docs/DevOps.md b/docs/DevOps.md index ca59d92f2cb..c3f4671bfa8 100644 --- a/docs/DevOps.md +++ b/docs/DevOps.md @@ -73,7 +73,7 @@ Any warnings or errors will be printed out to the console. Here are the steps to create a new release. -1. Update the version number in [`MainApp.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/MainApp.java). +1. Update the version number in [`MainApp.java`](https://github.com/AY2122S1-CS2103T-W15-3/tp/blob/master/src/main/java/seedu/address/MainApp.java). 1. Generate a fat JAR file using Gradle (i.e., `gradlew shadowJar`). 1. Tag the repo with the version number. e.g. `v0.1` 1. [Create a new release using GitHub](https://help.github.com/articles/creating-releases/). Upload the JAR file you created. diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 46eae8ee565..e9b59a16305 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -2,14 +2,33 @@ layout: page title: Developer Guide --- + +SoConnect is a **desktop app for SoC students to manage contacts of _Professors_ and _Teaching Assistants_, +as well as to keep track of noteworthy events, optimized for use via a _Command Line Interface (CLI)_** while still having +the benefits of a _Graphical User Interface (GUI)_. + +This developer guide is targeted at current and potential developers and testers of the SoConnect project. The purpose of this guide is to outline the architecture and some implementation details of SoConnect, so that developers and testers are aware of how to navigate this project. + * Table of Contents {:toc} +------------------- + +## **How to use the Developer Guide** + +* You can click on the titles in the Table of Contents to jump the section that you are interested in. +* You will find these icons in this developer guide: + * **:bulb: Tip** provides additional information that might be useful to you. + * **:information_source: Note** provides supplementary information that helps you to understand this Developer Guide. + * **:exclamation: Caution** cautions you against certain actions that will lead to undesirable consequences. +* You can find explanations of _italicized_ words in the [Glossary](#glossary). + + -------------------------------------------------------------------------------------------------------------------- ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +* The Calendar UI is inspired by another team project - [AY2122S1 CS2103T-F13-3](https://ay2122s1-cs2103t-f13-3.github.io/tp/). However, the implementation of the calendar UI is largely our own, other than a few instances of code reuse from the [CalendarFX Quick Start](https://dlsc.com/wp-content/html/calendarfx/manual.html#_quick_start) page. -------------------------------------------------------------------------------------------------------------------- @@ -23,20 +42,22 @@ 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-W15-3/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.
+This section will cover the design considerations and implementations of the various components of SoConnect, and how each component is related to other components. + ### 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 the SoConnect App. 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-W15-3/tp/tree/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/AY2122S1-CS2103T-W15-3/tp/tree/master/src/main/java/seedu/address/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. @@ -44,24 +65,24 @@ Given below is a quick overview of main components and how they interact with ea The rest of the App consists of four components. -* [**`UI`**](#ui-component): The UI of the App. +* [**`UI`**](#ui-component): The UI of the SoConnect app. * [**`Logic`**](#logic-component): The command executor. -* [**`Model`**](#model-component): Holds the data of the App in memory. +* [**`Model`**](#model-component): Holds the data of the app 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 issues the command `edelete 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. +* 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. +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. @@ -69,24 +90,26 @@ 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-W15-3/tp/blob/master/src/main/java/seedu/address/ui/Ui.java) + +Here's a (partial) class diagram of the `UI` component: ![Structure of the UI Component](images/UiClassDiagram.png) -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 consists of a `MainWindow` that is made up of several `UiPart` components, such as `ContactListPanel`, `EventListPanel`, `CalendarWindow` and the different components within `Other UI components`. The `Other UI components` contain the following classes: `HelpWindow`, `CommandBox`, `StatusBarFooter` and `ResultDisplay`. These classes are not drawn in the diagram for brevity. -The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/resources/view/MainWindow.fxml) +The `UI` component uses the JavaFx UI framework. Only the `CalendarWindow` component uses the [CalendarFX](https://github.com/dlsc-software-consulting-gmbh/CalendarFX) dependency. 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-W15-3/tp/blob/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](hhttps://github.com/AY2122S1-CS2103T-W15-3/tp/blob/master/src/main/resources/view/HelpWindow.fxml) The `UI` component, -* executes user commands using the `Logic` component. +* passes the user commands to the `Logic` component to be executed. * 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 `Contact` and `Event` objects 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) +The **API** of this component is specified in [`Logic.java`](https://github.com/AY2122S1-CS2103T-W15-3/tp/tree/master/src/main/java/seedu/address/logic/Logic.java) Here's a (partial) class diagram of the `Logic` component: @@ -94,15 +117,15 @@ 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. The result of the command execution is encapsulated as a `CommandResult` object which is returned back from `Logic`. +1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `CAddCommand`) which is executed by the `LogicManager`. +1. The command can communicate with the `Model` when it is executed (e.g. to add a contact). +1. The result of the command execution is encapsulated as a `CommandResult` object which is returned from `Logic`. -The Sequence Diagram below illustrates the interactions within the `Logic` component for the `execute("delete 1")` API call. +The Sequence Diagram below illustrates the interactions within the `Logic` component for the `execute("edelete 1-3")` API call. -![Interactions Inside the Logic Component for the `delete 1` Command](images/DeleteSequenceDiagram.png) +![Interactions Inside the Logic Component for the `edelete 1` Command](images/EDeleteSequenceDiagram.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. +
:information_source: **Note:** The lifeline for `EDeleteCommandParser` 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: @@ -110,43 +133,48 @@ Here are the other classes in `Logic` (omitted from the class diagram above) tha 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. -* 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. +* 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., `CAddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `CAddCommand`) which the `AddressBookParser` returns back as a `Command` object. +* All `XYZCommandParser` classes (e.g., `CAddCommandParser`, `EDeleteCommandParser`, ...) 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) - +The **API** of this component is specified in [`Model.java`](https://github.com/AY2122S1-CS2103T-W15-3/tp/tree/master/src/main/java/seedu/address/model/Model.java) +![SoConnect Model Component](images/ModelClassDiagram.png) 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 SoConnect data i.e., all `Contact` objects (which are contained in a `UniqueContactList` object) and all `Event` objects (which are contained in a `UniqueEventList` object). +* stores the currently 'selected' `Contact` and `Event` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` and `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 lists change. +* stores a `ModelDisplaySetting` object (not shown in the diagram) which will affect how the contacts and events will be displayed to the user. * 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.
+* stores a `ModelHistory` object (not shown in the diagram) which keeps track of all history instances of SoConnect. This is to facilitate the `undo` and `redo` features. +* 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 `Contact` and `Event` references. +This allows `AddressBook` to only require one `Tag` object per unique tag, +instead of each `Contact` or `Event` needing their own `Tag` objects.
+
### Storage component -**API** : [`Storage.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/storage/Storage.java) +The **API** of this component is specified in [`Storage.java`](https://github.com/AY2122S1-CS2103T-W15-3/tp/tree/master/src/main/java/seedu/address/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. +* saves both address book data and user preference data in json format, and reads them back into corresponding objects. * inherits from both `AddressBookStorage` and `UserPrefStorage`, which means it can be treated as either one (if only the functionality of only one is needed). -* depends on some classes in the `Model` component (because the `Storage` component's job is to save/retrieve objects that belong to the `Model`) +* 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.addressbook.commons` package. For example, the `Range` class, specified in [`Range.java`](https://github.com/AY2122S1-CS2103T-W15-3/tp/tree/master/src/main/java/seedu/address/commons/core/range/Range.java) encapsulates a range of indexes that are used by the `EDeleteCommand` and `CDeleteCommand` classes in logic. -------------------------------------------------------------------------------------------------------------------- @@ -154,45 +182,394 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa This section describes some noteworthy details on how certain features are implemented. -### \[Proposed\] Undo/redo feature -#### Proposed Implementation +### Mark Contacts feature + +This section details how the `cmark` command is implemented. This command allows the user to mark frequently used contacts, with the added feature of allowing the user to specify **more than one** contacts to mark. The marked contacts will appear at the **top** of the contact list and in **reverse order** in which their corresponding indexes are specified. + +#### Implementation + +Both `CMarkCommandParser` and `CMarkCommand` classes are involved in the execution of the `cmark` command. + +The `parse` method inside the `CMarkCommandParser` receives the user input, extracts the required index(es). It then creates a new List of `Index`(es) objects that will encapsulate the indexes of the contacts to be marked. + +
+:bulb: **Tip:** `CMarkCommandParser#parse` will throw a ParseException if the argument is empty, not an integer or is outside of the contact index range. +
+ +`CMarkCommandParser#parse` method will then return an `CMarkCommand` with the given List of `Index` object. + +Given below is one example usage scenario and explains how the `cmark` feature behaves at each step. You may also refer to the sequence diagram below. + +Step 1. The user enters `cmark 1 2` to mark the first and second contact displayed in the contact list. The arguments `1 2` are passed to the `CMarkCommandParser` through the `parse` method call. + +Step 2. The user input `1 2` will be subjected to checks by `String#trim` to ensure that argument provided is not empty. `Parserutil#parseMarkIndexes` will check for invalid arguments and create a list of `Index` object(s). + +Step 3. From this example, the List of `Indexes` created will contain two elements, both of which are `Indexes` and contain the integer value `1` and `2` respectively. + +Step 4. A new `CMarkCommand` object is returned to the `LogicManager`. + +Step 5. During the execution of the command, the `CMarkCommand` object gets the `filteredContactList` in the `Model` and gets the relevant contacts using the object created in step 3. For each contact identified, the command will check if the contact is originally marked, if so it will generate a message saying the particular contact is already marked. Otherwise, a new marked `Contact` containing the same details as the original contact is created and replaces the original contact in the `Model`. Thereafter, the list of contact in the model is rearranged using `Model#rearrangeContactsInOrder`, which will cause newly marked contacts to be placed at the top of the contact list. + +Step 6. A `CommandResult` with all newly marked contacts is returned and will be displayed to the user. + +#### Sequence Diagram + +The following sequence diagram shows how the `cmark` feature works for the example above: + +![CMarkSequenceDiagram](images/CMarkSequenceDiagram.png) + +#### Activity Diagram + +The following activity diagram summarizes what happens when the `cmark` feature is triggered: + +![CMarkActivityDiagram](images/CMarkActivityDiagram.png) + +#### Design Considerations + +**Aspect: Marking of contacts:** + +* **Alternative implementation 1:** CMarkCommand that does not place newly marked contacts at the top of the list. + * Pros: Easier to implement as there is no need to keep track of which contacts are marked and the order in which they are marked. + * Cons: Users still have to scroll through the entire contact list to find the marked contact(s). +* **Alternative implementation 2 (current choice):** CMmark replaces newly marked contacts at the top of the contact list. + * Pros: Easier for the users to differentiate the marked portion of the contact list from the unmarked portion. + * Cons: Harder to implement as there is a need to keep track of the order in which the contacts is marked and rearrange the contact list after each `cmark` command. + + +**Aspect: Allowing users to specify more than one index:** + +* **Alternative implementation 1:** Not allowing users to specify more than one index. + * Pros: Easier to implement as there is no need to keep track of the order in which the indexes are specified. + * Cons: User may have to scroll through or use `cmark` command to mark multiple contact(s). +* **Alternative implementation 2 (current choice):** Allowing users to specify more than one index. + * Pros: Command is more efficient as users can mark more than one contact at the same time. + * Cons: Harder to implement as more code is needed to facilitate the marking of multiple contacts in the correct order. + + +### Unmark Contacts feature + +This section details how the `cunmark` command is implemented. This command allows the user to unmark marked contacts, with the added feature of allowing the user to specify **more than one** contacts to unmark. The marked contacts will appear after all marked contacts and **in order** in which their corresponding indexes are specified. + +#### Implementation + +Both `CUnmarkCommandParser` and `CUnmarkCommand` classes are involved in the execution of the `cunmark` command. + +The `parse` method inside the `CUnmarkCommandParser` receives the user input, extracts the required index(es). It then creates a new List of `Index`(es) objects that will encapsulate the indexes of the marked contacts to be unmarked. + +
+:bulb: **Tip:** `CUnmarkCommandParser#parse` will throw a ParseException if the argument is empty, contains invalid values or the contact at the specifed index is not initially marked. +
+ +`CUnmarkCommandParser#parse` method will then return an `CUnmarkCommand` with the given List of `Index` object. + +Given below is one example usage scenario and explains how the `cunmark` feature behaves at each step. You may also refer to the sequence diagram below. + +Step 1. The user enters `cunmark 4 5` to unmark the fourth and fifth **marked** contact displayed in the contact list. The arguments `4 5` are passed to the `CUnmarkCommandParser` through the `parse` method call. + +Step 2. The user input `4 5` will be subjected to checks by `String#trim` to ensure that argument provided is not empty. `Parserutil#parseMarkIndexes` will check for invalid argument and create a list of `Index` from the argument. + +Step 3. From this example, the List of `Indexes` created will contain two elements both of which are `Indexes` and contains the integer value `4` and `5` respectively. + +Step 4. A new `CUnmarkCommand` object is returned to the `LogicManager`. + +Step 5. During the execution of the command, the `CUnmarkCommand` object retrieves the `filteredContactList` in the `Model` and get the relevant contacts using the object created in step 3. For each contact identified, the command will check if the contact is not marked, if so a message saying the contact is not marked will be generated. Otherwise, a new unmarked `Contact` object containing the same details as the original contact is created and replaces the original contact in `Model`. Thereafter, the list of contact in the model is rearranged using `Model#rearrangeContactsInOrder`, which will cause newly unmarked contacts to be placed below all marked contacts in the contact list. + +Step 6. A `CommandResult` with all newly unmarked contacts is returned and will be displayed to the user. + +#### Sequence Diagram + +The following sequence diagram shows how the `cunmark` feature works for the example above: + +![CUnmarkSequenceDiagram](images/CUnmarkSequenceDiagram.png) + +#### Activity Diagram + +The following activity diagram summarizes what happens when the `cunmark` feature is triggered: + +![CUnmarkActivityDiagram](images/CUnmarkActivityDiagram.png) + +#### Design Considerations + +**Aspect: Unmarking of contacts:** + +* **Alternative implementation 1:** CUnmarkCommand that does not replace newly unmarked contacts after all marked contacts. + * Pros: Easier to implement as there is no need to keep track of which the newly unmarked contacts. + * Cons: The marked contacts will no longer be at the top of the contact list, users would still have to scroll through the entire list of unmark and marked contacts to find a specific marked contact(s). +* **Alternative implementation 2 (current choice):** CUnmark replaces newly unmarked contacts below marked contacts. + * Pros: Easier for the users to differentiate the marked portion of the contact list from the unmarked portion. + * Cons: Harder to implement as there is a need to keep track of the order in which the contacts is unmarked and rearrange the contact list after each `cunmark` command. + + +**Aspect: Whether to allow users to specify more than one index:** + +* **Alternative implementation 1:** Only allow users to specify one `Index`. + * Pros: Easy to implement as the newly unmarked contacts is simply removed and added after all marked contacts. + * Cons: The user may want to unmark multiple contacts, which result in having to call `cunmark` command multiple times. +* **Alternative implementation 2 (current choice):** Allow users to specify one or more `Index`(es). + * Pros: This command is more efficient, users can now unmark more than one contacts at the same time, saving time from having to call `cunmark` repeatedly. + * Cons: More code will have to be written in order to facilitate unmarking multiple contacts and ensuring they are rearranged in the correct order. + + +### Delete Events feature + +This section details how an `Event` or multiple `Event` objects are deleted using the `edelete` command. + +The `edelete` command allows users to delete a single or an inclusive range of consecutive events from the current event list shown on SoConnect. The user needs to specify either an `Index` or a `Range` of event(s) to be deleted. Such event(s) would be removed from the `EventListPanel` in the _GUI_. + +#### Implementation + +We will use an example command: `edelete 1-3`. + +Given below is the usage scenario of the above example and explains how the `edelete` feature behaves at each step. + +Step 1: The user enters the command `edelete 1-3` to delete the events at indexes 1, 2 and 3. + +Step 2: The user input argument is passed to the `EDeleteCommandParser` object, which parses the input arguments and creates a `Range` object for the range . +`Range` object will only be created if the input arguments pass the checks of `ArgumentMultimap` and `ParserUtil` methods. + +Step 3: This `Range` object is used to construct a new `EDeleteCommand` object. `EDeleteCommand` object is then returned to `LogicManager` to be executed. + +Step 4: During the execution, the `EDeleteCommand` object repeatedly deletes each event from the most updated event list through the `Model#deleteEvent` method. +`Model#deleteEvent` method would then check if the event to be deleted is **linked to any contacts**. +If the event **has linked** contacts, `Model#deleteEvent` would **first unlink** the event and the linked contacts through `Model#unlinkContactsFromEvent` before deleting the event. +At the same time, a new list of `EventChanger` objects containing all the events to be deleted will be created. +The [Calendar UI implementation](#calendar-ui-feature) will discuss the use of this list. + +
:bulb: **Tip:** + +If the user only specifies one `Index` for `edelete`, a `Range` object is created with the same start and end `Index`. +
+ +#### Sequence Diagram + +The sequence diagram below shows how the execution of the example command flows: + +![Interactions Inside the Logic Component for the `edelete 1 - 3` Command](images/EDeleteSequenceDiagram.png) + +
:information_source: **Note:** + +The lifeline for `EDeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +
+ +#### Activity Diagram + +The activity diagram below shows how the execution of the `edelete` command flows: + +![EDeleteActivityDiagram](images/EDeleteActivityDiagram.png) + +#### Design considerations + +**Aspect: Type of user inputs:** + +* **Alternative 1 (current choice):** Either a single Index or a Range can be specified. + * Pros: Easy to implement. + * Cons: Unable to delete multiple ranges of events or events that are not ordered consecutively in the event list. + +* **Alternative 2:** Allow a mixture of multiple single indexes and multiple ranges + * Pros: Able to delete events more efficiently. + * Cons: We must ensure that the order of delete of the events is correct. It is more complex to keep track of the events to be deleted. + + +### List Events feature + +This section details how the `elist` command is implemented. This command allows the user to view all events, with the added feature of allowing the user to specify which _field_ to display. By default, all _fields_ will be displayed. + +#### Implementation + +As outlined in the [Logic component section](#logic-component), both `EListCommandParser` and `EListCommand` classes are involved in the execution of the `elist` command. + +The `parse` method inside the `EListCommandParser` receives the user input, extracts the required prefix(es). It then creates a new immutable `EventDisplaySetting` object that will encapsulate the visibility of the various _fields_ for all events in the _GUI_. + +* If no prefix is provided, the `parse` method will set all _fields_ of the `Event` class to be displayed. This is the default setting. +* If one or more prefix(es) is / are provided, `parse` will set the corresponding _field_(s) to be displayed, + sets the rest of the _fields_ to be hidden. + +
+:bulb: **Tip:** If values of prefixes given are not empty, `EListCommandParser#parse` throws a ParseException. +
+ +`EListCommandParser#parse` method will then return an `EListCommand` with the given `EventDisplaySetting` object. -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: +Given below is one example usage scenario and explains how the `elist` feature behaves at each step. You may also refer to the sequence diagram below. -* `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. +Step 1. The user enters `elist at/ end/` to display only the start and end timings of events. The arguments `at/ end/` are passed to the `EListCommandParser` through the `parse` method call. -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. +Step 2. The user input `at/ end/` will be subjected to checks by methods from [`ArgumentMultimap`](https://github.com/AY2122S1-CS2103T-W15-3/tp/tree/master/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java) to ensure that there are no incorrect arguments in the command. Examples of incorrect arguments include `1` or `at/3`. + +Step 3. A `EventDisplaySetting` object is created based on the input arguments. + * If there are no inputs, then the default `EventDisplaySetting.DEFAULT_SETTING` object is created. + * Since there are inputs for this example, an `EventDisplaySetting` with the `willDisplay` _fields_ for `startDateTime` and `endDateTime` set to true. All other _fields_ will be set to false (hidden from the user). `isViewingFull` will be set to false, since this is not a `view` command. + +Step 4. A new `EListCommand` object is returned to the `LogicManager`. + +Step 5. During the execution of the command, the `EListCommand` object will set update the `EventDisplaySetting` of the `Model` to the object created in step 3. Thereafter, the event cards are re-rendered, which will cause the `EventListPanel` in the UI component to update all the `EventCard` elements to display the events based on the new display settings. + +Step 6. A `CommandResult` with all events listed is returned and will be displayed to the user. + +#### Sequence Diagram + +The following sequence diagram shows how the `elist` feature works for the example: + +![EListSequenceDiagram](images/EListSequenceDiagram.png) + +#### Activity Diagram + +The following activity diagram summarizes what happens when the `elist` feature is triggered: + +![EListActivityDiagram](images/EListActivityDiagram.png) + +#### Design Considerations + +**Aspect: Whether to allow hiding of specific _fields_:** + +* **Alternative implementation 1:** EListCommand displays all _fields_. + * Pros: No need to check for valid prefixes. + * Cons: User may be interested in one _field_, but has to look through all the _fields_. +* **Alternative implementation 2 (current choice):** EListCommand allows users to choose which _fields_ to display. + * Pros: User can choose to hide long details about the event and focus on the details important to them. + * Cons: There is more room for mistakes, since it is more difficult to parse the user input correctly. + +**Aspect: Changing the display settings of events:** + +* **Alternative implementation 1:** Keep the settings directly in static fields in the `Event` class. + * Pros: Easy to implement and it is possible to change the settings even in the parser. + * Cons: It is difficult to test static variables since other tests may interfere with the state of the variables. Moreover, it will be impossible to implement a feature such as undo if the static fields are changed. This is unlike having a separate object like `EventDisplaySetting`, which is immutable and can be stored in history. In general, [manipulating global static variables is a bad idea](https://stackoverflow.com/questions/7026507/why-are-static-variables-considered-evil). + +* **Alternative implementation 2 (current choice):** Encapsulate the settings in a `EventDisplaySetting` class and update it in the `ModelManager`. + * Pros: The settings are immutable so the implementation is less prone to errors. + * Cons: More code will have to be written in order to facilitate the change in display settings. + +### Link Event feature +This section details how an `Event` object is linked to one or more `Contact` objects using the `elink` command. + +The `elink` command allows users to link an event to one or more contacts from the current event list and contact list shown in SoConnect. +The user needs to specify the `Index` of the event as well as the `Index` of the contacts to link. `Index` of the event must +come first, followed by that of contacts. To differentiate the `Index` of contact and event, the user needs to specify the +prefix `c/` right before every `Index` of the contact to be linked to the event. + +
+:bulb: **Tip:** Each link between an event and contact results in them having a bi-directional relationship. +
+ +#### Implementation + +The following operations are the main operations for `elink`. + +- `ELinkCommandParser#parse` - Parse the user inputs and create a `ELinkCommand` object to return. + +- `ELinkCommand#execute` - Links the event to the target contacts. + +We will use an example command `elink 1 c/1 c/2 c/3`, together with a sequence diagram, to explain how the `elink` feature works. + +#### Sequence Diagram + +![ELinkSequenceDiagram](images/ELinkSequenceDiagram.png) + +
:information_source: **Note:** The lifeline for `ELinkCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +
+ +Given below is the usage scenario from the example command `elink 1 c/1 c/2 c/3`, and explains how the `elink` feature behaves at each step. + +Step 1: The user enters `elink 1 c/1 c/2 c/3` to link the event at index `1` to the contacts at indexes `1`, `2` and `3`. +The argument `1 c/1 c/2 c/3` are passed to the `ELinkCommandParser` through the `parse` method call. + +Step 2: The argument `1 c/1 c/2 c/3` will be subjected to checks by methods from `ArgumentMultimap` and `ParserUtil` to ensure that there are no +incorrect arguments in the command. Examples of incorrect arguments include `1` or `c/2 1`. + +Step 3: A [HashSet](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/HashSet.html) of type `Index` +containing the indexes of contact is created. The index of event and the HashSet(https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/HashSet.html) is passed as arguments to construct an +`linkCommand` object. `ELinkCommand` object is then returned to `LogicManager` to be executed. + +Step 4: During the execution of the command, the `ELinkCommand` object links the event at index `1` to each of the contacts +through the `Model#linkEventAndContact` method call, by looping through the HashSet(https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/HashSet.html) containing the indexes of the contacts. + +Step 5. A `CommandResult` containing information about each link status will be displayed to the user. + +
+:bulb: **Tip:** If a user attempts to link an event and contact +that were already linked, the command will succeed with a `CommandResult` to let user know instead of throwing a `CommandException`. +
+ +#### Activity Diagram + +The following activity diagram summarizes what happens when the `elink` feature is triggered: + +![ELinkActivityDiagram](images/ELinkActivityDiagram.png) + +#### Design Considerations +**Aspect: The relationship of the link:** + +* **Alternative implementation 1:** The event has a reference to contacts that are linked to it but not the other way. + * Pros: Easy to implement. We only need to manage the relationship of the link in `Event` object. + * Cons: It is less efficient if we want to make changes to the contacts through other commands because we have to go through + all events to find out which events have links to the contact. + +* **Alternative implementation 2 (current choice):** Both event and contact have reference to each other if they are linked. + * Pros: More efficient. + * Cons: Have to enforce the bidirectional relationship strictly which is hard to implement and bug-prone. + +**Aspect: Whether to update the existing contact/event objects to show the link:** + +* **Alternative implementation 1:** Update the contact/event to have information about their linked events/contacts. + * Pros: Easy to implement. + * Cons: `Contact` and `Event` objects are no longer immutable. Increase in difficulty of testing. + +* **Alternative implementation 2 (current choice):** Creates new `Contact` and `Event` objects with link to each other and making them immutable. + * Pros: Less likely to have bugs. Works well with other commands like `undo` and `redo` because any changes in the link + will create new objects of `Contact` and `Event`, hence making it easier to store history of the contacts and events. + * Cons: May have performance issues in terms of memory usage because other commands like `EDeleteCommand` and `CEditCommand` may result in change in link relationship, + resulting in the creation of new `Contact` or `Event` objects. + + +### Undo/redo feature + +This section explains how `undo` and `redo` features are implemented. These features allow users to reverse the effect of +their previous command by using `undo`, and restore the previously undone action by using `redo`. + +#### Implementation + +The undo/redo mechanism is facilitated by `ModelHistory`, stored internally as `allHistory`, an ArrayList of all `HistoryInstance` of addressBook. + +![ModelHistoryDiagram](images/ModelHistory.png) + +- `HistoryInstance` is a nested class inside `ModelHistory` which keeps track of the current state of `AddressBook` and its `ModelDisplaySetting`. +- `allHistory` is managed by two pointers `currentSize` and `maxSize`. `currentSize` indicates the current point in history, while `maxSize` indicates the last point of history. Both of these pointers will be initialized to 0 when the modelHistory is first created. +- `Model History` implements the following operations: + * `commit()` — Saves the current history instance in the history. + * `undo()` — Restores the previous history instance book state from its history. + * `redo()` — Restores a previously undone history instance from its history. + +These operations are exposed in the `Model` _interface_ as `Model#commitHistory()`, `Model#undoHistory()` and `Model#redoHistory()` respectively. Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. -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. +Step 1. The user launches the application for the first time. A new model history will be initialized. The first version of `AddressBook` and `ModelDisplaySetting` will also create the first history instance in the history. Both current and maximum size of history will increment by one. ![UndoRedoState0](images/UndoRedoState0.png) -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. +Step 2. The user executes `edelete 5` command to delete the 5th event in the event list. The `edelete` command calls `Model#commitHistory()`, causing the modified state of the address book after the `edelete 5` command executes to be saved in `allHistory` as a new history instance. Both `currentSize` and `maxSize` are incremented by 1, pointing to the newly inserted history instance. ![UndoRedoState1](images/UndoRedoState1.png) -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`. +Step 3. The user executes `cadd n/David …​` to add a new contact. The `cadd` command also calls `Model#commitHistory()`, causing another modified state to be saved into `allHistory`. ![UndoRedoState2](images/UndoRedoState2.png) -
: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`. - +
:bulb: **Tip:** +- If a command fails its execution, it will not call `Model#commitHistory()`, so the address book state will not be saved into `allHistory`. +- For general commands (help, exit, calendar, undo, redo), `Model#commitHistory()` will not be called as they do not cause any changes to addressBook.
-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. +Step 4. The user now decides that adding the contact was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoHistory()`, which will shift the `currentSize` once to the left, pointing it to the previous history instance, and restores the address book and its displaySetting to that state. Pointer `maxSize` will not shift and will stay at the end point of the history. ![UndoRedoState3](images/UndoRedoState3.png) -
: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. - +
:bulb: **Tip:** If the `currentSize` is at index 0 or 1, it is pointing to an empty history or the initial HistoryInstance. Thus, there are no previous history instances to restore. The `undo` command uses `Model#isUndoable()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the undo action.
+#### Undo sequence diagram + The following sequence diagram shows how the undo operation works: ![UndoSequenceDiagram](images/UndoSequenceDiagram.png) @@ -201,43 +578,85 @@ The following sequence diagram shows how the undo operation works:
-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 `redo` command does the opposite — it calls `Model#redoHistory()`, which shifts the `currentSize` once to the right, pointing to the previously undone state, and restores the address book and its display setting to that state. -
:information_source: **Note:** If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone AddressBook states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. +
:bulb: **Tip:** If the `currentSize` is at the same point as `maxSize`, pointing to the latest history instance, then there are no undone AddressBook states to restore. The `redo` command uses `Model#isRedoable()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo.
-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. +Step 5. + +- If the user decides to `redo` the previous `undo` command, the pointer `currentSize` will again be shifted forward by one, pointing to the last history instance. The previously undone addressBook state is restored. ![UndoRedoState4](images/UndoRedoState4.png) -Step 6. The user executes `clear`, which calls `Model#commitAddressBook()`. Since the `currentStatePointer` is not pointing at the end of the `addressBookStateList`, all address book states after the `currentStatePointer` will be purged. Reason: It no longer makes sense to redo the `add n/David …​` command. This is the behavior that most modern desktop applications follow. +- If the user instead executes another command such as `eclear`, this will call `Model#commitHistory()`. This will create a new history instance right after the current instance. Both `currentSize` and `maxSize` will be reset to point to this new instance. Any history instance following after will be ignored. Reason: It no longer makes sense to redo the `cadd n/David …​` command. This is the behavior that most modern desktop applications follow. ![UndoRedoState5](images/UndoRedoState5.png) +#### Activity diagram + The following activity diagram summarizes what happens when a user executes a new command: - +![ActivityDiagram](images/UndoRedoActivityDiagram.png) -#### Design considerations: +#### Design considerations **Aspect: How undo & redo executes:** -* **Alternative 1 (current choice):** Saves the entire address book. - * Pros: Easy to implement. - * Cons: May have performance issues in terms of memory usage. +* **Alternative 1 (current choice):** Saves the entire address book and its display setting. + * Pros: Easy to implement. + * Cons: May have performance issues in terms of memory usage. * **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. + * Pros: Will use less memory (e.g. for `cdelete`, just save the contact being deleted). + * Cons: We must ensure that the implementation of each individual command are correct. + +### Calendar UI feature + +The calendar UI is implemented using the [CalendarFX](https://github.com/dlsc-software-consulting-gmbh/CalendarFX) framework. It can be opened using either the shortcut key `F2`, the file dropdown menu, or the command `calendar`. This feature is mainly facilitated by the `CalendarWindow` class. The `CalendarCommand` class handles the `calendar` command that the user may type and the `MainWindow` class handles the shortcut key and the dropdown menu option for showing the calendar. + +#### Design of CalendarWindow + +The `CalendarWindow` class extends from `UiPart`, just like the other UI components including the help window. The `CalendarWindow` class instance contains a table of events and the respective entries in the `Calendar` object. There are also final instances of `Calendar` and `CalendarView` objects in a `CalendarWindow` object. See the [CalendarFX manual](https://dlsc.com/wp-content/html/calendarfx/manual.html) for more information on `Calendar` and `CalendarView` classes. + +![Sequence diagram of CalendarWindow](images/CalendarSequenceDiagram.png) + +The sequence of how a calendar window is generated is shown in the UML sequence diagram above. The following are the textual descriptions of the diagram: + +1. The user intends to open the calendar. The `MainWindow` object captures this intention and calls the constructor of `CalendarWindow` and passes the list of events into it. +2. The `Calendar` and `CalendarView` objects are created and stored. A _hashmap_ of `Event` objects to `Entry` objects is created. (Not shown) +3. A time thread is created to constantly update the current time of the calendar every 10 seconds. +4. The `Calendar` object is populated with the entries of events from Step 1. The _hashmap_ is also concurrently being updated with the events and the associated entries. (Not shown) +5. The `CalendarView` object is updated to include the `Calendar` object, and also to change some configurations to simplify the user interface and prevent edits directly on the calendar. +6. The `StackPane` (see [`CalendarWindow.fxml`](https://github.com/AY2122S1-CS2103T-W15-3/tp/blob/master/src/main/resources/view/CalendarWindow.fxml)) is updated to include the new `CalendarView` user interface. The `CalendarWindow` object is now created and returned to `MainWindow`. + +#### Design considerations + +The user may leave the calendar window open and type in a new command to add, delete, edit or clear the events. In that case, there is a need to constantly update the calendar to reflect the new changes the user has made. This section discusses the implementation of the update and how the updates are optimized. + +**Aspect: How the calendar is updated constantly:** + +* **Alternative 1 (current choice):** Only updates the event(s) changed by the event commands. + * Pros: More optimized and faster update for large number of events. + * Cons: Harder to implement and prone to errors. + +
+:information_source: **Note:** It is important to discuss the `EventChanger` class from `Model` since the implementation of the update feature depends heavily on this class. +
+ +The `EventChanger` class contains references to up to 2 `Event` objects - an `oldEvent` and a `newEvent`. It also contains a boolean that is true if the user intends to clear all events. Creating the `EventChanger` object to be passed to the `CalendarWindow` to update the entries is simple, as it can be easily constructed using one of the _factory methods_: `addEventChanger`, `clearEventChanger`, `deleteEventChanger` and `editEventChanger`. See the class diagram below for a summary of the `EventChanger` class. -_{more aspects and alternatives to be added}_ +![Class Diagram of EventChanger](images/EventChangerClassDiagram.png) -### \[Proposed\] Data archiving +Upon the execution of any command, the list of event changers is returned in the `CommandResult`. The list is usually empty, except for the 4 types of commands listed above. The `updateCalendar` method of `CalendarWindow` is then called, which will update the `Calendar` object to remove the `oldEvent` and add the `newEvent` entries. The `Calendar` is cleared if the `EventChanger` is the `clearEventChanger`. This is when the _hashmap_ becomes useful, since the `Entry` objects in the calendar are unique and having the same `Event` associated to the `Entry` does not make the `Entry` objects equal. This will allow deletion of the `Entry` from the calendar. -_{Explain here how the data archiving feature will be implemented}_ +* **Alternative 2:** Create a new Calendar with all the events for each command executed. + * Pros: Easy to implement, less code is needed. + * Cons: Performance can be slowed down if there are many events, even if the user does not make any large change to the events. +This alternative implementation will cause the `MainWindow` class to constantly query the event list in the `AddressBook` through the `Logic` manager every time a command is executed. The queried list is then used to create a new `Calendar` which will be used by the `CalendarWindow` object to display the calendar to the user. However, the performance cost is not feasible if there are many events stored in the `AddressBook`. -------------------------------------------------------------------------------------------------------------------- @@ -255,73 +674,484 @@ _{Explain here how the data archiving feature will be implemented}_ ### Product scope -**Target user profile**: +**Target user:** [School of Computing](https://www.comp.nus.edu.sg) (SoC) students who: -* has a need to manage a significant number of contacts -* prefer desktop apps over other types -* can type fast -* prefers typing to mouse interactions -* is reasonably comfortable using CLI apps - -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app +* Need to manage large number of contacts and events +* Prefer desktop apps over other types +* Can type fast and prefer typing to mouse interactions +* Are reasonably comfortable using *CLI* apps +**Value proposition**: Manage contacts of peers, *Profs* and events such as classes and Co-Curricular Activities in +a single system faster than a typical mouse/*GUI* driven app while maintaining user-friendliness ### User stories Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` +#### Contacts + | Priority | As a …​ | I want to …​ | So that I can…​ | | -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | -| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App | -| `* * *` | user | add a new person | | -| `* * *` | user | delete a person | remove entries that I no longer need | -| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list | -| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident | -| `*` | user with many persons in the address book | sort persons by name | locate a person easily | - -*{More to be added}* +| `* * *` | hardworking student | add contact of my *TA*/*Profs* | save their contacts and ask them questions on my modules. | +| `* * *` | careless student | edit the contact of my *TA*/*Profs* | correct mistakes while adding contacts or update my *TA* contact details | +| `* * *` | senior SoC student | delete the contact of my *TA*/*Profs* | remove contact of my *TA* after I have completed the module | +| `* * *` | SoC student | view the contact of my *TA*/*Profs* | | +| `* * *` | year 4 SoC student with many contacts | search for contact of my *TA*/*Profs* | contact them when necessary | +| `* * *` | CS2103T student | list all the _telegram handles_ of my CS2103T project mates | add them to the project group | +| `* *` | year 4 SoC student with many contacts | sort the contacts of my *TA* | view the contacts based on the sorting settings | +| `* *` | careless student | undo my last action(s) | recover contacts I accidentally deleted/changed | +| `* *` | organized SoC student | categorize the contacts of students/*TA*/*Profs* by tags | view them separately | +| `* *` | SoC student who frequently contacts a *TA* | mark a contact as favorite and pin them to the top | view them easily when I start the app | +| `* *` | student with many contacts | add the profile picture of *TA*/*Profs* | more easily differentiate my contacts and remember their faces | +| `*` | first time user | import existing contacts from my phone | easily access all my past contacts using SoConnect | +| `*` | long term user | archive some contacts | still save old contacts without cluttering my current screen | + +#### Events + +| Priority | As a …​ | I want to …​ | So that I can…​ | +| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | +| `* * *` | SoC student | add details of event | keep track of the CCA dates and time | +| `* * *` | SoC student | delete details of event | remove events that have finished or are cancelled | +| `* * *` | SoC student | edit details of event | update event details | +| `* * *` | SoC student | view all CCA/events | have a rough overview of my schedule | +| `* * *` | SoC student | search for an event based on event name | easily refer to the event details | +| `* * *` | year 1 SoC student | list addresses of all lectures, tutorials and labs | +| `* *` | SoC student | sort the events by time | prepare for upcoming events | +| `* *` | SoC student with busy schedule | check if the new event clashes with any of my current events | better plan my timetable and avoid event clashes | +| `* *` | SoC student with busy schedule | see a weekly calendar | easily visualize my schedule for the week | +| `* *` | SoC student with many different events to manage | categorize my events with different tags like classes and CCAs | search related events | +| `* *` | SoC student with an important event coming up | mark an event as favorite and pin them to the top | easily see the most important events first when I start the app | +| `* *` | SoC student with many events and contacts to manage | link some contacts to some events | view the details of the participants/*TAs* in my events or lessons. | +| `*` | SoC student who uses other calendars | import and export my events to other calendars like Google Calendar | synchronize my events across my calendars | +| `*` | long term user | archive some events that have ended | still save details of past events without cluttering my main screen | +| `*` | SoC student with many commitments | have a reminder of upcoming events | | + +#### Personalisation and Others + +| Priority | As a …​ | I want to …​ | So that I can…​ | +| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | +| `* * *` | first time user | have a help message I can refer to | understand and start using basic features | +| `*` | first time user | set up my background | personalize the screen to my liking | ### 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 the `SoConnect Application System (SAS)` and the **Actor** is the `user`, unless specified otherwise) + +**1. Use case: UC1 - Add contact details** + +**Guarantees:** The contact will be stored in the system if and only if the user enters the correct inputs. + +***MSS*** + +1. User chooses to add *contact information*. + +2. SAS stores the contact and notifies the user that the contact has been successfully added. + + Use case ends. + +**Extensions** + +* 1a. SAS detects an error in the inputs. + + * 1a1. SAS requests for correct inputs. + + * 1a2. User enters new inputs. + + Steps 1a1 to 1a2 are repeated until the inputs entered are correct. + + Use case resumes from step 2. + + +* 1b. SAS detects a duplicate contact with the same name. + + * 1b1. SAS gives the user the option of adding the contact anyway. + + * 1b2. User proceeds to add the contact. + + Use case resumes from step 2. + + +* *a. At any time. user chooses not to add the contact. + + Use case ends. + + +**2. Use case: UC2 - Edit events** + +**Preconditions:** There is at least one event in the event list. + +**Guarantees:** The event will be updated accordingly if and only if the user enters the correct inputs. + +***MSS*** + +1. User wants to view the list of events. + +2. User decides on an event to edit. + +3. User edits the event. + +4. SAS updates the event accordingly and notifies user that the event has been successfully edited. + + Use case ends. + -**Use case: Delete a person** +**Extensions** + +* 3a. SAS detects an error in the inputs. + + * 3a1. SAS requests for correct inputs. + + * 3a2. User enters new inputs. + + Steps 3a1 to 3a2 are repeated until inputs entered are correct. + + Use case resumes from step 4. + + +* *a. User chooses not to edit the event. + + Use case ends. + + +**3. Use case: UC3 - Delete contacts** + +**Preconditions:** There is at least one contact in the contact list. + +**Guarantees:** The contact list will be updated according to which contact(s) are deleted if and only if the user enters the correct inputs. + +***MSS*** + +1. User wants to view the list of contacts. + +2. User decides on the contact(s) to delete. + +3. User deletes the contact(s). + +4. SAS deletes the specified contact(s), updates the contact list accordingly, and notifies user that the contact(s) has been successfully deleted. + + Use case ends. + + +**Extensions** + +* 3a. SAS detects an error in the inputs. + + * 3a1. SAS requests for correct inputs. + + * 3a2. User enters new inputs. + + Steps 3a1 to 3a2 are repeated until inputs entered are correct. + + Use case resumes from step 4. + + +* *a. User chooses not to delete the contact(s). + + Use case ends. + + +**4. Use case: UC4 - Find contact details** + +**Guarantees:** A filtered list of contacts that match the user keywords will be shown, if and only if the user enters the correct inputs. + +***MSS*** + +1. User decides on the keyword(s) to find. + +2. User enters specified keyword(s). + +3. SAS shows the matched contacts accordingly and notifies user of search success. + + Use case ends. + +**Extensions** + +* 2a. SAS fails to find any matched contacts. + + * 2a1. SAS outputs 0 matched contacts. + + * 2a2. User enters new keyword(s). + + Steps 2a1 to 2a2 are repeated until the user finds the contacts of interest. + + Use case resumes from step 3. + + +* *a. User chooses not to find contact(s). + + Use case ends. -**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 +**5. Use case: UC5 - Sort events** + +**Preconditions:** List of events to be sorted is displayed. + +**Guarantees:** The displayed list of events is sorted lexicographically according to the specified _field_, if the given _field_ is valid. + +***MSS*** + +1. User decides on a _field_ to sort by. + +2. User inputs the specific _field_. + +3. SAS sorts the list of events by the specified _field_ provided in step 2. + +4. SAS displays the sorted list of events. + + Use case ends + +**Extensions** + +* 2a. SAS detects that the input is an invalid _field_. + + * 2a1. SAS requests for a correct input. + + * 2a2. User enters a new input. + + Steps 2a1-2a2 are repeated until user enters a valid _field_. + + Use case resumes from step 3. + + +* 2b. SAS detects that the input contains more than one _field_. + + * 2b1. SAS sorts the list by the first _field_ entered. + + Use case resumes from step 4. + + +* *a. User chooses not to sort the list. Use case ends. + + +**6. Use case: UC6 - Delete events** + +**Preconditions:** There is at least one event in the event list. + +**Guarantees:** The event list will be updated according to which event(s) are deleted if and only if the user enters the correct inputs. + +***MSS*** + +1. User wants to view the list of events. + +2. User decides on the event(s) to delete. + +3. User deletes the event(s). + +4. SAS deletes the specified event(s), updates the event list accordingly, and notifies user that the event(s) has been successfully deleted. + + Use case ends. + **Extensions** -* 2a. The list is empty. +* 3a. SAS detects an error in the inputs. + + * 3a1. SAS requests for correct inputs. + + * 3a2. User enters new inputs. + + Steps 3a1 to 3a2 are repeated until inputs entered are correct. + + Use case resumes from step 4. + + +**7. Use case: UC7 - List event _fields_** + +**Preconditions:** There is at least one event in the event list. + +**Guarantees:** The displayed list only contains the _field_(s) of interest, if the given _field_(s) is / are valid. + +***MSS*** + +1. User decides on a _field_(s) to be listed. + +2. User inputs the specific _field_(s). + +3. SAS displays the list of events with only the _field_(s) specified shown. + + Use case ends + +**Extensions** + +* 2a. SAS detects that the input is an invalid _field_. + + * 2a1. SAS requests for a correct input. + + * 2a2. User enters a new input. + + Steps 2a1-2a2 are repeated until user enters a valid _field_. + + Use case resumes from step 3. + + +* 2b. SAS detects that the user did not provide a _field_. + + * 2b1. SAS displays the default list containing all the _fields_ of the events. + + Use case ends. + + +* *a. User chooses not to sort the list. Use case ends. -* 3a. The given index is invalid. - * 3a1. AddressBook shows an error message. +**8. Use case: UC8 - Link a specific event to multiple contacts** + +**Preconditions:** There is at least one event and one contact. + +**Guarantees:** The chosen event is linked to those chosen contacts. + +***MSS*** - Use case resumes at step 2. +1. User decides on which event and contacts to link. -*{More to be added}* +2. User inputs the event and contacts to link. + +3. SAS links the chosen event to every chosen contacts and notifies user that the event is successfully linked to those contacts. + + Use case ends. + +**Extensions** + +* 2a. SAS detects an error in the inputs. + + * 2a1. SAS requests for correct inputs. + + * 2a2. User enters new inputs. + + Steps 2a1 to 2a2 are repeated until the inputs entered are correct. + + Use case resumes from step 3. + +* *a. User chooses not to link event to contacts. + + Use case ends. + +**9. Use case: UC9 - Undo a command** + +**Guarantees:** SoConnect will be restored to its previous state in its history. + +***MSS*** + +1. User decides to undo a command. + +2. SAS identifies the previous state of SoConnect and restore it. SAS notifies the user that the previous command is undone. + + Use case ends. + +**Extensions** + +* 1a. SAS detects that SoConnect does not have a previous state in its history. + + * 1a1. SAS notifies the user that SoConnect is already at its original state. + + Use case ends. + +**10. Use case: UC10 - Redo a command** + +**Guarantees:** SoConnect will be restored to its previously undone state in its history. + +***MSS*** + +1. User decides to undo the previous undo command. + +2. SAS redoes the command and restores the previously undone state of SoConnect from its history. + + Use case ends. + +**Extensions** + +* 1a. SAS detects that SoConnect is already at its most recent state. + + * 1a1. SAS notifies the user that there is no previous undo command to redo. + + Use case ends. + +**11. Use case: UC11 - Mark contacts** + +**Guarantees:** The chosen contacts will be marked with a bookmark icon and moved to the start of the contact list. + +***MSS*** + +1. User decides on the contacts to mark. + +2. User inputs the indexes of the contacts to mark. + +3. SAS marks the chosen contacts and move them to the top of the list. + + Use case ends. + +**Extensions** + +* 2a. SAS detects an error in the inputs. + + * 2a1. SAS requests for correct inputs. + + * 2a2. User enters new inputs. + + Steps 2a1 to 2a2 are repeated until the inputs entered are correct. + + Use case resumes from step 3. + +* 2b. SAS detects some contacts that have already been marked. + + * 2b1. SAS informs the user the relevant contacts that have already been marked previously. + + * 2b2. SAS proceeds to mark the other contacts and move them to the top of the list. + + Use case ends. + +* *a. User chooses not to link event to contacts. + + Use case ends. ### 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. +2. Should be able to hold up to 1000 contacts 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. - -*{More to be added}* +4. Should not depend on Internet connection. +5. Only one user should be able to use the system at one time. ### 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. + + +* [**Main Success Scenario**](https://nus-cs2103-ay2122s1.github.io/website/schedule/week7/topics.html#requirements-specifying-requirements-use-cases-details) (MSS): Describes the most straightforward interaction for a given use case, which assumes that nothing goes wrong. + + +* **Command Line Interface** (CLI): Text-based user interface. + + +* **Graphical User Interface** (GUI): Graphic-based user interface. + + +* **Interface**: (For user interface) The place at which independent and often unrelated systems meet and act on or communicate with each other/ a way for the user to interact with the system. (For other use of interfaces such as `Model`) See the [Java Tutorials](https://docs.oracle.com/javase/tutorial/java/concepts/interface.html) for more details on what an interface in Java means. + + +* **Telegram handle**: Username of a [Telegram](https://telegram.org/) user. + + +* **Professor** (Prof): A lecturer who teaches a module in NUS. + + +* **Teaching Assistant** (TA): A student tutor employed to teach small group lessons in NUS. + + +* **Contact information**: Name and email of the contact. Phone number, address, telegram handle, video conferencing meeting link, and tag(s) are optional. + + +* **Field**: A particular detail of an event or contact. Examples of fields include the name of a contact, the description of an event or the tags of an event. + + +* **Hashmap**: A data structure that stores a table of keys and their corresponding values. In the implementation of Calendar UI, a hashmap is used to store the `Event` objects as keys and the `Entry` object as values. + + +* **Factory method**: A method to create an object without specifying the exact class, based on the creational design pattern. In the context of `EventChanger`, the factory methods create instances of subclasses of `EventChanger`, as seen in [`EventChanger.java`](https://github.com/AY2122S1-CS2103T-W15-3/tp/blob/master/src/main/java/seedu/address/model/event/EventChanger.java). -------------------------------------------------------------------------------------------------------------------- @@ -330,7 +1160,7 @@ Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unli Given below are instructions to test the app manually.
:information_source: **Note:** These instructions only provide a starting point for testers to work on; -testers are expected to do more *exploratory* testing. +testers are expected to do more exploratory testing.
@@ -340,7 +1170,8 @@ testers are expected to do more *exploratory* testing. 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. + 2. 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 @@ -349,29 +1180,143 @@ testers are expected to do more *exploratory* testing. 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 …​ }_ +### Adding a contact + +1. Test case: `cadd n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01`
+ Expected: A new contact named John Doe with the specified detail will be added to the end of the contact list. Success message displayed. + +2. Test case: `cadd n/Jane Doe p/1298701`
+ Expected: No contact added. Error details shown in the status message. + +3. Test case: `cadd n/Alex Yeoh e/alexYeoh@gmail.com`
+ Expected: No contact added. Error details shown in the status message. + +4. Other invalid `cadd` commands: `cadd n/`, `cadd n/John Doe e/`, `cadd n/* e/test@gmail.com`
+ Expected: No contact added. Error details shown in the status message. + +### Finding a contact +1. Test case: `cfind Alex`
+ Expected: A list of contacts with names that match`Alex` will be displayed. Success message displayed. + +2. Test case: `cfind alex`
+ Expected: Similar to previous. + +3. Test case: `cfind e/bernice`
+ Expected: A list of contacts with emails that match `bernice` will be displayed. Success message displayed. + +4. Test case: `cfind e/bernice a/geylang`
+ Expected: A list of contacts with emails that match `bernice` or with addresses that match `geylang` will be displayed. Success message displayed. + +5. Test case: `cfind a/`
+ Expected: No change in the displayed contact list. Error details displayed in the status message. + +6. Test case: `cfind`
+ Expected: No change in the displayed contact list. Error details displayed in the status message. + +7. Other invalid `cfind` commands: `cfind a/ e/`, `cfind a/alex e/`
+ Expected: No change in the displayed contact list. Error details displayed in the status message. + + +### Deleting an event + +Deleting an event while all events are being shown + +Prerequisites: List all events using the `elist` command. Multiple events in the list. + + 1. Test case: `edelete 1`
+ Expected: First event is deleted from the list. Details of the deleted event shown in the status message. Timestamp in the status bar is updated. + + 2. Test case: `edelete 0`
+ Expected: No event is deleted. Error details shown in the status message. Status bar remains the same. + + 3. Other incorrect delete commands to try: `edelete`, `edelete x` (where x is larger than the list size), `edelete a`
+ Expected: Similar to previous. -### Deleting a person + 4. Test case: `edelete 1-2`
+ Expected: First and second event are deleted from the list. Details of the deleted events shown in the status message. Timestamp in the status bar is updated. -1. Deleting a person while all persons are being shown + 5. Test case: `edelete 2-1` (invalid range)
+ Expected: No event is deleted. Error details shown in the status message. Status bar remains the same. + + 6. Test case: `edelete 1-x` (where x is larger than the list size)
+ Expected: Similar to previous. - 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. +### Listing all events - 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. +Listing all event with certain _fields_ shown. - 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. +Prerequisites: At least one event in the list. - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
- Expected: Similar to previous. + 1. Test case: `elist at/`
+ Expected: All events listed with only address displayed. All events listed shown in the status message. + + 1. Test case: `elist`
+ Expected: All events listed with all _fields_ displayed. All events listed shown in the status message. + + 1. Other incorrect `elist` commands to try: `elist 123`, `elist at/0000`, `elist xyz/` (where xyz is not a valid prefix)
+ Expected: No change in display. Error details displayed in the status message. + +### Linking an event to one or more contacts + + Prerequisites: There is at least one event and one contact in their respective list. + +1. Test case: `elink 1 c/1`
+ Expected: The event at index `1` is linked to the contact at index `1`. Success details shown in the status message. + +2. Test case: `elink 2 c/1 c/2 c/3`
+ Expected: The event at index `2` is linked to each of the contacts at indexes `1`, `2` and `3`. -1. _{ more test cases …​ }_ +3. Test case: `elink x c/1` (where x is an index larger than the event list size)
+ Expected: No change in display. Error details displayed in the status message. + +3. Test case: `elink 1 c/x` (where x is an index larger than the contact list size)
+ Expected: No change in display. Error details displayed in the status message. + +4. Other incorrect `elink` commands to try: `elink`, `elink 1`, `elink c/1`
+ Expected: No change in display. Error details displayed in the status message. + +### Undoing/Redoing a command + +1. Test case: `cadd n/Amy e/amy@mail.com p/1987911` followed by `undo`
+ Expected: A new contact named Amy is added after the first command is then removed after the `undo` command. Success messages displayed after each command. + +2. Test case: `cfind Alex` followed by `undo 12`
+ Expected: A filtered list with contact name Alex Yeoh will be displayed after the first command. A full contact list will be displayed after `undo`command. Success messages displayed after each command. + +3. Test case: `cdelete 1` followed by `undo` and `redo`
+ Expected: First contact in the contact list is removed from the displayed contact list after the first command. The contact is restored after the `undo` command, and is removed again after the `redo` command. Success messages displayed after each command. + +4. Test case: `undo` without any previous command
+ Expected: No change in display. Error details displayed in the status message. + +5. Test case: `redo` without any previous `undo` command
+ Expected: No change in display. Error details displayed in the status message. + +### GUI + +1. Test case: For any event with a linked contact, click on the linked contact (labeled in yellow)
+ Expected: Only linked contact(s) of the chosen event will be displayed in the contact list. Clicking on the same linked contact again will display the full contact list. + +2. Test case: Click on any field in the contact and event list (except for tag)
+ Expected: The chosen field will be copied to clipboard. Success details displayed in the status message. + +3. Test case: Click on any zoom or telegram link (underlined in blue)
+ Expected: The chosen link is opened in browser. Success details displayed in the status message. + ### Saving data -1. Dealing with missing/corrupted data files +Dealing with missing/corrupted data files + +Prerequisite: There should be a valid `soconnect.json` file saved in the data folder. + +
+:bulb: **Tip:** If there is no json file, start `soconnect.jar`, perform some changes to the default list of contacts or events (see [User Guide](https://ay2122s1-cs2103t-w15-3.github.io/tp/UserGuide.html)). A folder `data` with a file `soconnect.json` should be created. +
- 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ + 1. Test case: Edit the json file in order to corrupt it. For example, add an additional `{` to create some syntax error. Then, start `soconnect.jar`.
+ Expected: The event and contact list is empty. -1. _{ more test cases …​ }_ + 2. Test case: Delete the json file and start `soconnect.jar`.
+ Expected: The default event and contact list is loaded (see [User Guide](https://ay2122s1-cs2103t-w15-3.github.io/tp/UserGuide.html#quick-start) for an example). + diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 3716f3ca8a4..4a3d40975a2 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,192 +1,1140 @@ --- layout: page -title: User Guide +title: SoConnect 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. + + +Made by [School of Computing](https://www.comp.nus.edu.sg/) (SoC) students, for SoC students, SoConnect is a **desktop app for SoC students to manage contacts +of Professors and Teaching Assistants, and keep track of noteworthy events.** +With SoConnect, you can **save +your contacts and events in one location**, and even **link events to multiple contacts**, or **bookmark +your favorite contact**. + +SoConnect 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, managing your contacts will be +**very easy** with SoConnect. + +This user guide will help you to familiarise yourself with your SoConnect quickly and teach you the +full range of features it offers. + * Table of Contents {:toc} -------------------------------------------------------------------------------------------------------------------- -## Quick start +## How to use SoConnect User Guide -1. Ensure you have Java `11` or above installed in your Computer. +* You can click on the titles in the Table of Contents to jump to the section that you are interested in. +* You will find these icons in this user guide: + * **:bulb: Tip** provides additional information that might be useful to you. + * **:information_source: Note** provides supplementary information that helps you to understand this User Guide. + * **:exclamation: Caution** cautions you against certain actions that will lead to undesirable consequences. +* You can find explanations of _italicized_ words in the [Glossary](#glossary). +* You can refer to the [Command Summary](#command-summary) for a complete overview of all SoConnect features and _commands_. -1. Download the latest `addressbook.jar` from [here](https://github.com/se-edu/addressbook-level3/releases). -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +### How to read _commands_ in SoConnect -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) +You will see _commands_ throughout this User Guide and each of them has its own _Command Format_. -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: +This is one example of a _command_: - * **`list`** : Lists all contacts. +![Command Example](images/demo-screenshots/commandSyntax.png) - * **`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. +There are only 3 different parts of inputs within each _command_: +1. **_COMMAND_ WORD** - * **`delete`**`3` : Deletes the 3rd contact shown in the current list. + You need to specify the _command_ word to tell SoConnect what action you want to do. - * **`clear`** : Deletes all contacts. + For example, specifying `cedit` in the _command_ above will tell SoConnect to edit a contact for you. - * **`exit`** : Exits the app. +2. **_PARAMETER_** -1. Refer to the [Features](#features) below for details of each command. + The _parameter_ is the supplementary information that you need to provide for the action you want to do. --------------------------------------------------------------------------------------------------------------------- + Using the same example, specifying `2` tells SoConnect to edit the contact at **index 2** (the second contact in the list) to the name `Betsy Crower`. -## Features +3. **_PREFIX_** + + The *prefix* separates the different types of *parameters*. + + Each _prefix_ always ends with a `/`. See the [list of _prefixes_](#list-of-prefixes) for all the _prefixes_ that you can use in SoConnect. + + For example, if you use `n/`, SoConnect will recognise that the information following this _prefix_ should be a **name**. + +You may view the entire list of _commands_ that you can type in SoConnect [here](#features).
-**:information_source: Notes about the command format:**
+**:information_source: 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 `UPPER_CASE` are the _parameters_ that you should provide.
+ e.g. in `cadd n/NAME`, `NAME` is a _parameter_ that can be used as `cadd n/John Doe`. -* Items in square brackets are optional.
+* _Fields_ 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`. -* 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. +* _Fields_ 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. -* 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. +* Each pair of _prefixes_ and _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. -* 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.
+* 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 (unless otherwise stated).
e.g. if you specify `p/12341234 p/56785678`, only `p/56785678` 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 `help` and `exit`) will be **ignored**.
+ e.g. if the _command_ specifies `help 123`, it will be interpreted as `help`. + +* Date and time must follow **dd-MM-yyyy HH:mm** format (day-month-year hours:minutes). + e.g. if the date and time is 1 May 2021 6.30pm, you should specify it as `01-05-2021 18:30` + +
+ +### What happens if my _command_ is invalid? + +After you enter a _command_ in SoConnect, a success message will be displayed on the message box of SoConnect. + +
:bulb: **Tip:** + +Unsure of where to find the **message box**? Click [here](#overview-of-soconnect) to check out the +overview of different components in SoConnect. +
+ +For example, after entering [`clist`](#listing-all-contacts-clist), SoConnect will tell you that it has successfully listed all contacts. + +![Success Message](images/demo-screenshots/successMessage.png) + +
+ +However, if the _command_ you entered does not fulfill the _Command Format_ specified in +[Features](#features), an **error message** will be shown on the message box instead. +You can then correct your mistakes in the _command_ and try again. + +For example, if you enter [`cview`](#viewing-a-contact-cview) **without** specifying which contact to view, +SoConnect will tell you that the _Command Format_ is invalid. + +![Error Message](images/demo-screenshots/invalidCommand.png) + +-------------------------------------------------------------------------------------------------------------------- +## Quick start + +1. Ensure you have [Java 11](https://www.oracle.com/java/technologies/downloads/) or above installed on your computer. + +
:bulb: **Tip:** + + [Here](https://www.java.com/en/download/help/version_manual.html) is how you can check the Java Version installed on your computer. +
+ +2. Download the latest `soconnect.jar` from [here](https://github.com/AY2122S1-CS2103T-W15-3/tp/releases). + +3. Copy the file to the folder you want to use as the _home folder_ for your SoConnect. + + +![_Home Folder_](images/demo-screenshots/homeFolder.png) + + +4. Double-click the file to start SoConnect. This is how SoConnect looks like:
+ + ![Overview](images/demo-screenshots/overview.png) + +
:information_source: **Note:** + SoConnect comes with some sample contacts and events upon installation. +
+
:bulb: **Tip:** + If SoConnect does not start by double-clicking, you can check this + [alternative](#how-to-start-soconnect-using-terminal) way of starting SoConnect. +
+ +5. Type the _command_ in the _command_ box and **press Enter** to execute it. + +
:bulb: **Tip:** + Unsure of where to find the **_command_ box**? Click [here](#overview-of-soconnect) to check out the + overview of different components in SoConnect. +
+ + For example, typing `help` and **pressing Enter** will open the [help window](#help-window-help).
+ Here are some example _commands_ you can try: + + * `elist`: [Lists all events](#listing-all-events-elist). + + * `cadd n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01`: + [Adds a contact](#adding-a-contact-cadd) named `John Doe` to SoConnect. + + * `cdelete 3` : [Deletes the third contact](#deleting-a-contact-cdelete) shown in SoConnect. + + * `eclear` : [Clears all entries of events](#clearing-all-events-eclear) from SoConnect. + + * `exit` : [Exits](#exiting-soconnect-exit) SoConnect. + +6. Refer to the [Features](#features) below for details of each _command_. + +_See also: [What happens if my command is invalid](#what-happens-if-my-command-is-invalid)_ + +-------------------------------------------------------------------------------------------------------------------- + +## Overview of SoConnect + +This is what you will see when you open SoConnect! +![Labelled SoConnect](images/demo-screenshots/labelledSoconnectOverview.png) + +1. **Menu Bar** + + The Menu Bar consists of 2 tabs: `File` and `Help`. + + Upon clicking `File`, you can choose to: + 1. **Exit** SoConnect + 2. Open **Calendar** of SoConnect + + ![File Tab](images/demo-screenshots/fileTab.png) + +
:bulb: **Tip:** + Click the following links to see how to [exit SoConnect](#exiting-soconnect-exit) and [open the calendar](#calendar-window-calendar) using _commands_. +

+ + Upon clicking `Help`, you can choose to open the **Help Window** of SoConnect. + ![Help Tab](images/demo-screenshots/helpTab.png) + +
:bulb: **Tip:** + Check out how to open [the help window](#help-window-help) using _commands_. +
+ +2. **_Command_ Box** + + This is where you can type all your _commands_ to use the [features of SoConnect](#features). + +3. **Message Box** + + This is where the message from SoConnect is displayed after you have executed one _command_. + + _See Also: [What happens if my command is invalid?](#what-happens-if-my-command-is-invalid)_ + +5. **Contact List** + + Displays information of the contacts you saved in SoConnect. + + Refer to [Icons in Contact List](#icons-in-contact-list) to learn about the different _fields_ that each icon represents in Contact List. + +6. **Event list** + + Displays information of the events you saved in SoConnect. + + Refer to [Icons in Event List](#icons-in-event-list) to learn about the different _fields_ that each icon represents in Event List. + + + +### Icons in Contact List + +![Contact Card](images/demo-screenshots/contactCard.png) +You will always find these 2 information of each contact displayed in SoConnect Contact List: +1. Contact Index +2. Contact Name + +Here are the icons you might see under each contact: + +Icon | _Field_ +--------|------------------ +**![Bookmark Icon](images/demo-screenshots/bookmarkIcon.png)** | Bookmark* +**![Email Icon](images/demo-screenshots/emailIcon.png)** | Email Address +**![Phone Number Icon](images/demo-screenshots/phoneNumberIcon.png)** | Phone Number +**![Address Icon](images/demo-screenshots/addressIcon.png)** | Address +**![Telegram Icon](images/demo-screenshots/telegramIcon.png)** | Telegram Handle +**![Website Icon](images/demo-screenshots/websiteIcon.png)** | Links/ Websites +**![Event Icon](images/demo-screenshots/eventIcon.png)** | Linked Events +**![Tag Icon](images/demo-screenshots/tagIcon.png)** | Tag + +*The bookmark icon is only visible if the contact has been marked. +_See also: [Marking a contact](#marking-a-contact-cmark)_ + + +### Icons in Event List + +![Event Card](images/demo-screenshots/eventCard.png) +You will always find these 2 information of each event displayed in SoConnect Event List: +1. Event Index +2. Event Name + +Here are the icons you might see under each event: + +Icon | _Field_ +--------|------------------ +**![Bookmark Icon](images/demo-screenshots/bookmarkIcon.png)** | Bookmark* +**![Start Time Icon](images/demo-screenshots/startTimeIcon.png)** | Start Time +**![End Time Icon](images/demo-screenshots/endTimeIcon.png)** | End Time +**![Website Icon](images/demo-screenshots/websiteIcon.png)** | Links/ Websites +**![Description Icon](images/demo-screenshots/descriptionIcon.png)** | Description +**![Contact Icon](images/demo-screenshots/contactsIcon.png)** | Linked Contacts +**![Tag Icon](images/demo-screenshots/tagIcon.png)** | Tag + +*The bookmark icon is only visible if the event has been marked. +_See also: [Marking an event](#marking-an-event-emark)_ + +-------------------------------------------------------------------------------------------------------------------- + +## Features + +There are three main sections to SoConnect Features: +[Contact Management](#contact-management), +[Event Management](#event-management), and +[General](#general). + +For each feature, you are provided with: +* Function and Description of the feature +* _Command Format_ of the feature +* Examples of some usages of the feature (All examples use the **initial sample** of contacts and events) + +-------------------------------------------------------------------------------------------------------------------- + +### Contact Management + +This section details all the features and _commands_ available in SoConnect that can help you manage your contacts: + +* [Adding a contact](#adding-a-contact-cadd) +* [Clearing all contacts](#clearing-all-contacts-cclear) +* [Deleting a contact](#deleting-a-contact-cdelete) +* [Editing a contact](#editing-a-contact-cedit) +* [Finding contacts](#finding-contacts-cfind) +* [Listing all contacts](#listing-all-contacts-clist) + +* [Marking a contact](#marking-a-contact-cmark) +* [Unmarking a contact](#removing-mark-of-a-contact-cunmark) + +* [Viewing a contact](#viewing-a-contact-cview) + + +#### Adding a contact: `cadd` + +Adds a contact to SoConnect. + +**Format:** `cadd n/NAME e/EMAIL [p/PHONE_NUMBER] [a/ADDRESS] [th/TELEGRAM_HANDLE] [z/ZOOM] [t/TAG]…` + +
:bulb: **Tip:** + +* A contact can have any number of tags (including 0) +* You **cannot** add a contact with the **same name** as an existing contact. +
+ +**Examples:** + +Input | Expected Output +------|------------------ +`cadd n/Alex Doe e/e0123456@u.nus.edu a/COM1 #99-99 th/johnDoe99 t/Professor` | You should see this message in the message box: `New contact added: Alex Doe; Email: e0123456@u.nus.edu; Address: COM1 #99-99; Telegram: johnDoe99; Tags: [Professor]`

You should also see `Alex Doe` added **at the end** of your contact list: ![New Contact 1](images/demo-screenshots/caddEx1.png) +`cadd n/ Jon Cheng t/TA e/e7654321@u.nus.edu a/COM1-0201 p/87654321 t/Senior th/jonnyjohnny z/https://nus-sg.zoom.us/j/0123456789?pwd=ABCDEFGHIJKLMNOPDJFHISDFSDH` | You should see this message in the message box: `New contact added: Jon Cheng; Email: e7654321@u.nus.edu; Phone: 87654321; Address: COM1-0201; Zoom Link: https://nus-sg.zoom.us/j/0123456789?pwd=ABCDEFGHIJKLMNOPDJFHISDFSDH; Telegram: jonnyjohnny; Tags: [Senior][TA]`

You should also see `Jon Cheng` added **at the end** of your contact list: ![New Contact 2](images/demo-screenshots/caddEx2.png) + +*Index of the newly added contact will be one more than the previous number of contacts. + + +#### Clearing all contacts: `cclear` + +Clears **all** entries of contacts in SoConnect. + +**Format:** `cclear` + +
:bulb: **Tip:** + +This **will not change the events** saved in SoConnect. +
+ + +#### Deleting a contact: `cdelete` + +Deletes the specified contact(s) from SoConnect. + +**Format:** `cdelete INDEX1[-INDEX2]` + +* Deletes the contact(s): + * at the specified `INDEX1` or + * between the specified range from `INDEX1` to `INDEX2` inclusively (if you provide `INDEX2`). +* `INDEX1` and `INDEX2` refer to the index number shown in the displayed contact list. + +
:bulb: **Tip:** + +* `INDEX1` and `INDEX2` **must be a positive integer**. e.g. 1, 2, 3, … +* `INDEX1` and `INDEX2` must **not be greater** than the **number of contacts** in the contact list. +
+ +**Examples:** + +Input | Expected Output +--------|------------------ +[`clist`](#listing-all-contacts-clist) followed by `cdelete 2` | Deletes the second contact in the contact list.

You should see these messages in the message box:
1. After `clist`: `Listed all contacts`
2. After `cdelete 2`: `Deleted Contact: Bernice Yu; Email: berniceyu@example.com; Phone: 99272758; Address: Blk 30 Lorong 3 Serangoon Gardens, #07-18; Zoom Link: nus-sg.zoom.us/j/08382435376?pwd=Oow3u9N098nh8nsdLp0; Telegram: bbernicee; Tags: [TA][friends]` +[`cfind Bernice`](#finding-contacts-cfind) followed by `cdelete 1` | Deletes the first contact in the **results of the `cfind` _command_**.

You should see these messages in the message box:
1. After `cfind Bernice`: `1 contacts listed!`
2. After `cdelete 1`: `Deleted Contact: Bernice Yu; Email: berniceyu@example.com; Phone: 99272758; Address: Blk 30 Lorong 3 Serangoon Gardens, #07-18; Zoom Link: nus-sg.zoom.us/j/08382435376?pwd=Oow3u9N098nh8nsdLp0; Telegram: bbernicee; Tags: [TA][friends]` +`cdelete 3-5` | Deletes contacts from indexes 3 to 5 (inclusive) from the **currently displayed** contact list.

You should see these messages in the message box:
`Deleted Contact: Charlotte Oliveiro; Email: charlotte@example.com Deleted Contact: David Li; Email: lidavid@comp.nus.edu.sg; Address: COM1-B1-0931; Telegram: lidavid; Tags: [professor][CS2103T] Deleted Contact: Irfan Ibrahim; Email: irfan@example.com; Address: Blk 47 Tampines Street 20, #17-35; Telegram: irfanx; Tags: [classmates]` + + +#### Editing a contact: `cedit` + +Edits an **existing** contact in SoConnect. + +**Format:** `cedit INDEX [n/NAME] [e/EMAIL] [p/PHONE] [a/ADDRESS] [th/TELEGRAM_HANDLE] [z/ZOOM] [dt/TAG_DELETED]… [t/TAG_ADDED]… ` +* Edits the contact at the specified `INDEX`. +* `INDEX` refers to the index number shown in the displayed contact list. +* You must provide **at least one** of the optional _fields_. +* The input values you provide will be used to update the existing values. +* You can use `t/` to add a tag. +* You can remove a specific tag by typing `dt/` followed by the name of the tag that you wish to remove. +* You can remove all existing tags of a contact by typing `dt/*`. +* When editing tags, the tags to be deleted will be removed first, before new tags are added. + +
:bulb: **Tip:** + +* `INDEX` **must be a positive integer**. e.g. 1, 2, 3, … +* `INDEX` must **not be greater** than the **number of contacts** in the contact list. +* You **cannot** edit a contact to the **same name** as an existing contact. +
+ +**Examples:** + +Input | Expected Output +--------|------------------ +`cedit 2 p/91234567 e/agentX@thehightable.com` | Edits the **phone number** and **email address** of the second contact in the **currently displayed** contact list to be `91234567` and `agentX@thehightable.com` respectively.

You should see this message in the message box:
`Edited Contact: Bernice Yu; Email: agentX@thehightable.com; Phone: 91234567; Address: Blk 30 Lorong 3 Serangoon Gardens, #07-18; Zoom Link: nus-sg.zoom.us/j/08382435376?pwd=Oow3u9N098nh8nsdLp0; Telegram: bbernicee; Tags: [TA][friends]`

You should also the following changes: ![Edit Contact 1](images/demo-screenshots/ceditEx1.png) +`cedit 1 n/Betsy Crower dt/*` | Edits the **name** of the first contact in the **currently displayed** contact list to be `Betsy Crower` and **clears all existing tags**.

You should see this message in your message box: `Edited Contact: Betsy Crower; Email: alexyeoh@example.com; Phone: 87438807; Address: Blk 30 Geylang Street 29, #06-40; Telegram: yeoh_alex`

You should also see the following changes: ![Edit Contact 2](images/demo-screenshots/ceditEx2.png) + + +#### Finding contacts: `cfind` + +Finds all contacts that contain any of the given keywords that you specify. + +**Format:** `cfind [KEYWORD]… [e/KEYWORD…] [p/KEYWORD…] [a/KEYWORD…] [th/KEYWORD…] [z/KEYWORD…] [t/KEYWORD…]` + +
:bulb: **Tip:** + +There are **two** types of contact searches you can do in SoConnect: +1. If you **do not specify any optional _fields_** in front of the keywords you specify, e.g. `cfind KEYWORD1 KEYWORD2`, + + SoConnect will only search the names of the contacts using the keywords you provide. + +2. If you specified optional _field(s)_ before a keyword, e.g. `cfind e/KEYWORD1 p/KEYWORD2`, + + SoConnect will search the emails and phone numbers of the contacts using `KEYWORD1` and `KEYWORD2` respectively.
-### Viewing help : `help` +* You must provide **at least one keyword**. +* You can provide multiple keywords without specifying any optional _fields_ e.g. `cfind John David`. +* You can only **specify each optional _field_ once**. +* You can the keywords **in any order**. e.g. Both `Hans Bo` and `Bo Hans` will return the same result. +* Partial words can be matched e.g. `Han` will match `Hans`. +* The contact(s) matching at least one keyword you provide will be returned. + e.g. `Hans Bo` will return `Hans Gruber` and `Bo Yang`. -Shows a message explaning how to access the help page. +
:bulb: **Tip:** -![help message](images/helpMessage.png) +The search by `cfind` is case-insensitive. e.g. `hans` will match `Hans`. +
+ +**Examples:** + +Input | Expected Output +--------|------------------ +`cfind alex david` | Returns contacts of `Alex Yeoh` and `David Li`.

You should see this message in the message box:
`2 contacts listed!`

You should also see only these **2 contacts**: ![Find Contact 1](images/demo-screenshots/cfindEx1.png) +`cfind p/926 e/nus.edu` | Returns contacts with phone numbers that contain `926` and with emails that contain `nus.edu`.

You should see this message in the message box:
`2 contacts listed!`

You should also see only these **2 contacts**: ![Find Contact 2](images/demo-screenshots/cfindEx2.png) -Format: `help` +#### Listing all contacts: `clist` -### Adding a person: `add` +Shows **all contacts** in the SoConnect, with all available details by default. -Adds a person to the address book. +**Format:** `clist [e/] [p/] [a/] [th/] [z/] [t/]` -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +* Names of contacts are always shown. +* If you do not provide any optional _prefixes_, e.g `clist`, all available details of each contact will be shown. +* If you provide optional _prefixes_, it will only show the names and the _fields_ corresponding to specified _prefixes_ for each contact. +* You can provide more than one optional _prefix_. +* You can specify the optional _prefix_ **in any order**. e.g. both `clist e/ p/` and `clist p/ e/` will show only the names, email addresses and phone numbers of each contact. +* _Fields_ of a contact that have no value will not appear e.g. if a contact does not have a zoom link, typing `clist z/` will not display the zoom link of this contact.
:bulb: **Tip:** -A person can have any number of tags (including 0) + +**Do not** add extraneous values after each optional _prefix_ you specify.
-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` +**Examples:** + +Input | Expected Output +--------|------------------ +`clist` | Shows **all available details** of each contact in SoConnect.

You should see this message in the message box:
`Listed all contacts` +`clist e/ p/` | Shows **only the names, email addresses and phone numbers** (if available) of each contact in SoConnect.

You should see this message in the message box:
`Listed all contacts`

You should also see the contacts list displays **only the specified _fields_**: ![List Contact 1](images/demo-screenshots/clistEx.png) + + + +#### Marking a contact: `cmark` -### Listing all persons : `list` +Marks the specified contact(s). -Shows a list of all persons in the address book. +**Format:** `cmark INDEX [INDEX]...` -Format: `list` +* Marks the contact at `INDEX` and **pins it at the top** of the contact list. +* You can specify more than one `INDEX`, e.g `cmark 1 2`, the order in which the marked contacts appear will be in reverse order to the order you specify their corresponding indexes. +* `INDEX` refers to the index number shown in the **currently displayed** contact list. -### Editing a person : `edit` +
:bulb: **Tip:** -Edits an existing person in the address book. +* `INDEX` **must be a positive integer**, e.g. 1, 2, 3,… +* `INDEX` must **not be greater** than the **number of contacts** in the contact list. +
-Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +**Examples:** -* 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, …​ -* At least one of the optional fields must be provided. +Input | Expected Output +--------|------------------ +`cmark 2` | Marks the second contact of the **currently displayed** contact list in SoConnect.

You should see this message in the message box:
`Marked Contact: Bernice Yu; Email: berniceyu@example.com; Phone: 99272758; Address: Blk 30 Lorong 3 Serangoon Gardens, #07-18; Zoom Link: nus-sg.zoom.us/j/08382435376?pwd=Oow3u9N098nh8nsdLp0; Telegram: bbernicee; Tags: [TA][friends]`

You should see `Bernice Yu` **pinned to the top** of your contact list with the **Bookmark Icon**: ![Mark Contact](images/demo-screenshots/cmarkEx1.png) +`cmark 4 5` | Marks the fourth and fifth contacts of the **currently displayed** contact list in SoConnect.

You should see this message in the message box:
`Marked contact: Irfan Ibrahim; Email: irfan@example.com; Address: Blk 47 Tampines Street 20, #17-35; Telegram: irfanx; Tags: [classmates] Marked contact: David Li; Email: lidavid@comp.nus.edu.sg; Address: COM1-B1-0931; Telegram: lidavid; Tags: [professor][CS2103T]`

You should see `Irfan Ibrahim` and `David Li` **pinned to the top** of your contact list in that order with the **Bookmark Icon**: ![Mark Contact](images/demo-screenshots/cmarkEx2.png) + +_See also: [Unmarking a contact](#removing-mark-of-a-contact-cunmark)_ + + +#### Removing mark of a contact: `cunmark` + +Unmarks the specified contact(s). + +**Format:** `cunmark INDEX [INDEX]...` + +* Unmarks the contact at `INDEX1`. +* You may unmark **more than one contact at a time** by specifying multiple indexes, eg `cunmark 1 2`, the indexes in which the contacts appear will be **in the order in which** you specify them +* `INDEX` refers to the index number shown in the displayed contact list. + +
:bulb: **Tip:** + +* `INDEX` **must be a positive integer**, e.g. 1, 2, 3,… +* `INDEX` must **not be greater** than the **number of contacts** in the contact list. +* You must check that the contact indexed at `INDEX` is **initially marked**. +
+ +**Examples:** + +Input | Expected Output +--------|------------------ +[`cmark 2 3`](#marking-a-contact-cmark) followed by `cunmark 1` | Unmarks the first contact of the **currently displayed** contact list in SoConnect.

You should see these messages in the message box:
1. After `cmark 2 3`: `Marked contact: Charlotte Oliveiro; Email: charlotte@example.com Marked contact: Bernice Yu; Email: berniceyu@example.com; Phone: 99272758; Address: Blk 30 Lorong 3 Serangoon Gardens, #07-18; Zoom Link: nus-sg.zoom.us/j/08382435376?pwd=Oow3u9N098nh8nsdLp0; Telegram: bbernicee; Tags: [TA][friends]`
2. After `cunmark 1`: `Unmarked contact: Charlotte Oliveiro; Email: charlotte@example.com`

You should see `Charlotte Oliveiro` moved to the **bottom of your marked contact list** without the **Bookmark Icon**: ![Unmark Contact](images/demo-screenshots/cunmarkEx1.png) +[`cmark 1 2 3`](#marking-a-contact-cmark) followed by `cunmark 1 2` | Unmarks the first and second contact of the **currently displayed** contact list in SoConnect.

You should see these messages in the message box:
1. After `cmark 1 2 3`: `Marked contact: Charlotte Oliveiro; Email: charlotte@example.com Marked contact: Bernice Yu; Email: berniceyu@example.com; Phone: 99272758; Address: Blk 30 Lorong 3 Serangoon Gardens, #07-18; Zoom Link: nus-sg.zoom.us/j/08382435376?pwd=Oow3u9N098nh8nsdLp0; Telegram: bbernicee; Tags: [TA][friends] Marked contact: Alex Yeoh; Email: alexyeoh@example.com; Phone: 87438807; Address: Blk 30 Geylang Street 29, #06-40; Telegram: yeoh_alex; Tags: [friends]`
2. After `cunmark 1 2`: `Unmarked contact: Bernice Yu; Email: berniceyu@example.com; Phone: 99272758; Address: Blk 30 Lorong 3 Serangoon Gardens, #07-18; Zoom Link: nus-sg.zoom.us/j/08382435376?pwd=Oow3u9N098nh8nsdLp0; Telegram: bbernicee; Tags: [TA][friends] Unmarked contact: Charlotte Oliveiro; Email: charlotte@example.com`

You should see `Bernice Yu` and `Charlotte Oliveiro` moved to the **bottom of your marked contact list** without the **Bookmark Icon**: ![Unmark Contact](images/demo-screenshots/cunmarkEx2.png) + +_See also: [Marking a contact](#marking-a-contact-cmark)_ + + + +#### Viewing a contact: `cview` + +Views a contact with all details fully shown. + +**Format:** `cview INDEX` + +* displays only the contact at the specified `INDEX`. +* All truncated details of the contact you want to view will be expanded fully. +* `INDEX` refers to the index number shown in the displayed contact list. + +
+ +**:bulb: Tip:**
+* `INDEX` **must be a positive integer**. e.g. 1, 2, 3, …
+* `INDEX` must **not be greater** than the **number of contacts** in the contact list. + +
+ +**Examples:** + +Input | Expected Output +--------|------------------ +`cview 2` | Shows all details of the second contact of the **currently displayed** in SoConnect **fully**.
You should see this message in the message box:
`Viewing Contact: Bernice Yu; Email: berniceyu@example.com; Phone: 99272758; Address: Blk 30 Lorong 3 Serangoon Gardens, #07-18; Zoom Link: nus-sg.zoom.us/j/08382435376?pwd=Oow3u9N098nh8nsdLp0; Telegram: bbernicee; Tags: [TA][friends]`

You should see this change of your **viewed** contact: ![View Contact](images/demo-screenshots/cviewEx.png) + + +### Event Management + +This section details all the features and _commands_ available in SoConnect that can help you with managing your events: +* [Adding an event](#adding-an-event-eadd) +* [Clearing all event](#clearing-all-events-eclear) +* [Deleting an event](#deleting-an-event-edelete) +* [Editing an event](#editing-an-event-eedit) +* [Finding events](#finding-events-efind) +* [Linking an event to contacts](#linking-an-event-to-contacts-elink) +* [Listing all events](#listing-all-events-elist) +* [Marking an event](#marking-an-event-emark) +* [Removing mark of an event](#removing-mark-of-an-event-eunmark) +* [Sorting events](#sorting-events-esort) +* [Unlinking an event from contacts](#unlinking-an-event-from-contacts-eunlink) +* [Viewing an event](#viewing-an-event-eview) + + + +#### Adding an event: `eadd` + +Adds an event to SoConnect. + +**Format:** `eadd n/NAME at/START_TIME [end/END_TIME] [d/DESCRIPTION] [a/ADDRESS] [z/ZOOM] [t/TAG]…​` + +
:bulb: **Tip:** + +* An event can have any number of tags (including 0) +* You **cannot** add an event with the **same name** as an existing event. +
+ +
:information_source: **Note:** + +* Start time and End Time should be of format **dd-MM-yyyy HH:m** (date-MONTH-year HOUR:minute in 24-hour format). +* End Time should be **chronologically after** the Start Time. +
+ +**Examples:** + +Input | Expected Output +--------|------------------ +`eadd n/Summer Party at/12-12-2021 15:12 a/123, Clementi Rd, 1234665 t/fun` | You should see this message in the message box:
`New event added: Summer Party; Start: 12-12-2021 15:12; Address: 123, Clementi Rd, 1234665; Tags: [fun]`

You should also see `Summer Party` **at the end** of your event list: ![New Event](images/demo-screenshots/eaddEx.png) + +*Index of the newly added event will depend on your previous number of events. + + + +#### Clearing all events: `eclear` + +Clears all entries of events from SoConnect. + +**Format:** `eclear` + +
:bulb: **Tip:** +This will not change the contacts saved in SoConnect. +
+ + +#### Deleting an event: `edelete` + +Deletes the specified event(s) from SoConnect. + +**Format:** `edelete INDEX1[-INDEX2]` + +* Deletes the event(s) at: + * the specified `INDEX1` or + * between the specified range from `INDEX1` to `INDEX2` inclusively (if you provide `INDEX2`). +* `INDEX1` and `INDEX2` refer to the index numbers shown in the displayed event list. + +
:bulb: **Tip:** + +* `INDEX1` and `INDEX2` **must be a positive integer**. e.g. 1, 2, 3, … +* `INDEX1` and `INDEX2` must **not be greater** than the **number of events** in the event list. +
+ +**Examples:** + +Input | Expected Output +--------|------------------ +[`elist`](#listing-all-events-elist) followed by `edelete 2` | Deletes the second event from SoConnect.

You should see these messages in the message box:
1. After `elist`: `Listed all events`
2. After `edelete 2`: `Deleted Event: Basketball training; Start: 01-11-2021 20:00; End: 01-11-2021 21:00; Description: Meeting every week; Address: NUS Sport Centre; Tags: [CCA][Recurring]` +[`efind Class`](#finding-events-efind) followed by `edelete 1` | Deletes the first event from the **results of the `efind` _command_**.

You should see these messages in the message box:
1. After `efind class`: `1 events listed!`
2. After `edelete 1`: `Deleted Event: Dance class; Start: 13-11-2021 20:00; End: 13-11-2021 22:00; Description: Dancing is my passion. I like pole dancing.; Address: NUS UTown; Tags: [CCA][Recurring]` +`edelete 1-2` | Deletes events from index 1 to 2 from the **currently displayed** event list.

You should see these messages in the message box:
`Deleted Event: CS2103T project meeting; Start: 10-10-2021 21:00; End: 10-10-2021 22:00; Zoom Link: nus-sg.zoom.us/j/21342513543; Tags: [Recurring][CS2103T]`
`Deleted Event: Basketball training; Start: 01-11-2021 20:00; End: 01-11-2021 21:00; Description: Meeting every week; Address: NUS Sport Centre; Tags: [CCA][Recurring]` + + +#### Editing an event: `eedit` + +Edits an existing event in SoConnect. + +**Format:** `eedit INDEX [n/NAME] [at/START_TIME] [end/END_TIME] [d/DESCRIPTION] [a/ADDRESS] [z/ZOOM] [dt/TAG_DELETED]…​ [t/TAG_ADDED]…​` + +
:bulb: **Tip:** + +You must provide **at least one** of the optional _fields_. +
+ +* Edits the event at the specified `INDEX`. +* `INDEX` refers to the index number shown in the displayed event list. * 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. +* You can use `t/` to add a tag. +* You can remove a specific tag by typing `dt/` followed by the name of the tag that you wish to remove. +* You can remove all existing tags of an event by typing `dt/*`. +* When editing tags, the tags to be deleted will be removed first, before new tags are added. -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. +
:bulb: **Tip:** -### Locating persons by name: `find` +* `INDEX` **must be a positive integer**. e.g. 1, 2, 3, … +* Start time and End Time should be of format **dd-MM-yyyy HH:mm** (date-MONTH-year HOUR:minute in 24-hour format). +* End Time should be **chronologically after** the Start Time. +* You **cannot** edit an event to the **same name** as an existing event. +
-Finds persons whose names contain any of the given keywords. +**Examples:** -Format: `find KEYWORD [MORE_KEYWORDS]` +Input | Expected Output +--------|------------------ +`eedit 2 n/CS2103T Exam dt/CCA t/Hard` | Changes the name of the second event in the **currently displayed** event list to `CS2103T Exam`, deletes the tag `CCA` and adds the tag `Hard`.

You should see this message in the message box:
`Edited Event: CS2103T Exam; Start: 01-11-2021 20:00; End: 01-11-2021 21:00; Description: Meeting every week; Address: NUS Sport Centre; Tags: [Recurring][Hard]`

You should also see these changes: ![Edit Event](images/demo-screenshots/eeditEx.png) -* 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) -### Deleting a person : `delete` +#### Finding Events: `efind` -Deletes the specified person from the address book. +Finds all events that contain any of the given keywords based on your search type. -Format: `delete INDEX` +**Format:** `efind [KEYWORD]… [at/KEYWORD…] [end/KEYWORD…] [d/KEYWORD…] [a/KEYWORD…] [z/KEYWORD…] [t/KEYWORD…]` +
:bulb: **Tip:** -* 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, …​ +There are two types of event searches you can do in SoConnect: +1. If you **do not specify any optional _fields_ before your keyword(s)**, e.g. `efind KEYWORD1 KEYWORD2`, -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. + You will only search the names of the events based on the keyword(s) provided. -### Clearing all entries : `clear` +2. If you specify any _prefix_ before your keyword(s), e.g. `efind a/KEYWORD1 d/KEYWORD2`, -Clears all entries from the address book. + You will search the addresses and descriptions of the events based on `KEYWORD1` and `KEYWORD2` respectively. +
-Format: `clear` +* You need to provide **at least one keyword**. +* You can provide multiple keywords without specifying any _prefixes_. +* You can only **specify each optional _field_ once**. +* Partial words can be matched e.g. `Exa` will match `CS2103T Exam`. +* Events matching at least one keyword will be returned. + e.g. `Exam Hard` will return `Hard Exam`, `CS1101S Exams`. -### Exiting the program : `exit` +
:bulb: **Tip:** -Exits the program. +The search by `efind` is case-insensitive. e.g. `exams` will match `Exams`. +
-Format: `exit` +**Examples:** -### Saving the data +Input | Expected Output +--------|------------------ +`efind meet` | Displays events with names that contain `meet`.

You should see this message in the message box:
`1 events listed!`

You should also see only this **one event**: ![Find Event 1](images/demo-screenshots/efindEx1.png) +`efind t/CS2103T Intern` | Displays events with tags that contain `CS2103T` and `Intern`.

You should see this message in the message box:
`2 events listed!`

You should also see only these **two events**: ![Find Event 2](images/demo-screenshots/efindEx2.png) -AddressBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. + -### Editing the data file +#### Linking an event to contacts: `elink` + +Links the specified event to one or more contacts. + +**Format:** `elink EVENT_INDEX c/CONTACT_INDEX [c/CONTACT_INDEX]…` + +* Links the event at `EVENT_INDEX` to the contact(s) at `CONTACT_INDEX`. +* `EVENT_INDEX` refers to the index number shown in the displayed event list. +* `CONTACT_INDEX` refers to the index number shown in the displayed contact list. + +
:bulb: **Tip:** + +`EVENT_INDEX` and `CONTACT_INDEX` **must be a positive integer**, e.g. 1, 2, 3,… +
+ +**Examples:** + +Input | Expected Output +--------|------------------ +`elink 2 c/1 c/2` | Links the second event in the **currently displayed** event list to the contacts with index **1 and 2** in the **currently displayed** contact list.

You should see this in your SoConnect: ![Link](images/demo-screenshots/elinkEx.png) + +_See Also: [Unlinking an event from contacts](#unlinking-an-event-from-contacts-eunlink)_ + + +#### Listing all events: `elist` + +Displays all events in SoConnect, with all details by default. + +**Format:** `elist [at/] [end/] [d/] [a/] [z/] [t/]` + +* Event names are always displayed. +* When no optional _prefixes_ are provided, e.g. `elist` , all available details of each event will be displayed. +* When optional _prefixes_ are provided, only the names and the corresponding specified _fields_ for each event will be shown. +* You can provide more than one optional _prefixes_. +* The order of the _prefixes_ does not matter. e.g. both `elist d/ at/` and `elist at/ d/` will only show the names, descriptions and starting times of each event. +* _Fields_ of an event that have no value will not be shown. + +
:bulb: **Tip:** + +**Do not** add extraneous values after each optional _prefix_ you specify. +
+ +**Examples:** + +Input | Expected Output +--------|------------------ +`elist` | Shows **all events** in SoConnect with **all available details** for each event.

You should see this message in the message box:
`Listed all events` +`elist d/ at/` | Shows **all events** in SoConnect with **only their names, start date and time and description** (if available). You should see this message in the message box:
`Listed all events`

You should also see the event list displays all events with **only the specified _fields_**: ![List Events](images/demo-screenshots/elistEx.png) + + +#### Marking an event: `emark` + +Marks the specified event(s). + +**Format:** `emark INDEX [INDEX]…` + +* Marks the event at `INDEX` and **pins it to the top** of the event list. +* You can specify more than one `INDEX`, e.g. `emark 1 2`, the order in which the marked events appear will be in reverse order to the order you specify their corresponding indexes. +* `INDEX` refers to the index number shown in the **currently displayed** event list. + +
:bulb: **Tip:** + +`INDEX` **must be a positive integer**, e.g. 1, 2, 3,…
+`INDEX` must **not be greater** than the **number of events** in the event list. +
+ +**Examples:** + +Input | Expected Output +--------|------------------ +`emark 2` | Marks the second event of **currently displayed** event list in SoConnect.

_The expected display is similar to [marking a contact](#marking-a-contact-cmark)_ + +_See Also: [Removing mark of an event](#removing-mark-of-an-event-eunmark)_ + + +#### Removing mark of an event: `eunmark` + +Unmarks the specified event(s). + +**Format:** `eunmark INDEX [INDEX]...` + +* Unmarks the event at `INDEX`. +* You may unmark **more than one event** by specifying multiple indexes, eg `eunmark 1 2`, the indexes in which the events appear will be **in the order in which** you specify them. +* `INDEX` refers to the index number shown in the displayed event list. + +
:bulb: **Tip:** + +* `INDEX` **must be a positive integer**, e.g. 1, 2, 3,… +* `INDEX` must **not be greater** than the **number of events** in the event list. +* You must ensure that the event indexed at `INDEX` is **initially marked**. +
+ +**Examples:** + +Input | Expected Output +--------|------------------ +`eunmark 2` | Unmarks the second event of **currently displayed** event list in SoConnect.

_The expected display is similar to [Removing mark of a contact](#removing-mark-of-a-contact-cunmark)_ +`eunmark 2 4` | Unmarks the second and fourth event in SoConnect. + +_See Also: [Marking an event](#marking-an-event-emark)_ + + +#### Sorting events: `esort` + +Sorts all events by start time and displays all upcoming or ongoing events. + +**Format:** `esort` + +**Examples:** +![Sort example](images/demo-screenshots/sortResult.png) + +
:information_source: **Note:** + +Events that have ended **will not be shown** +
+ + +#### Unlinking an event from contacts: `eunlink` + +**Format:** `eunlink EVENT_INDEX c/CONTACT_INDEX [c/CONTACT_INDEX]…` + +Unlinks the specified event and the specified contact(s). + +**Examples:** + +Input | Expected Output +--------|------------------ +`eunlink 2 c/1` | Unlinks the second event in the **currently displayed** event list from the first contact in the **currently displayed** contact list.

You should see this in your SoConnect: ![unlink contact](images/demo-screenshots/eunlinkEx1.png) +`eunlink 2 c/*` | Unlinks the second event in the **currently displayed** event list from **all of its linked contacts**.

You should see this in your SoConnect: ![unlink all contacts](images/demo-screenshots/eunlinkEx2.png) + +_See Also: [Linking an event to contacts](#linking-an-event-to-contacts-elink)_ + + +#### Viewing an event: `eview` + +Views an event with all details fully shown. + +**Format:** `eview INDEX` + +* Views the event at the specified `INDEX`. +* `INDEX` refers to the index number shown in the displayed event list. +* All truncated details of the event will be shown fully. + +
:bulb: **Tip:** + +`INDEX` **must be a positive integer**. e.g. 1, 2, 3, … +
+ +**Examples:** + +Input | Expected Output +--------|------------------ +`eview 1` | Shows all details of the first event in the **currently displayed** event list **fully**.

_The expected display is similar to [Viewing a contact](#viewing-a-contact-cview)_ -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. + +### General + +This section details all the other features and _commands_ available in SoConnect that can enhance your SoConnect experience: +* [Calendar Window](#calendar-window-calendar) +* [Exiting SoConnect](#exiting-soconnect-exit) +* [Help Window](#help-window-help) +* [Redo a _command_](#redo-a-command-redo) +* [Undo a _command_](#undo-a-command-undo) + +#### Calendar Window: `calendar` + +Shows a calendar of all the events. + +**Format:** `calendar` + +* You can view the calendar in 4 different views: + * Daily + * Weekly + * Monthly + * Yearly + ![calendar](images/demo-screenshots/Calendar.png) + + +
:bulb: **Tip:** + +* Alternatively, you can view the calendar using the top menu bar via `File -> Calendar` or press `F2`. +* If an event does not have a specified end date and time, the calendar will treat the duration of the event as **one hour**. +
+ +
:information_source: **Note:** + +[Undo](#undo-a-command-undo) and [redo](#redo-a-command-redo) will not change the state of the calendar. +You should close the calendar window before performing any undo or redo operations. +
: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. +Any changes made in the calendar window will not be saved. +**Do not attempt to add new events using the calendar window.** +Doing so might result in a crash and your data may be lost. +
+ + + +#### Exiting SoConnect: `exit` + +Exits and closes SoConnect. + +**Format:** `exit` + +
:bulb: **Tip:** + +Alternatively, you can exit SoConnect using the top menu bar via `File -> Exit`. +
+ + +#### Help Window: `help` + +Displays a summary of all _commands_ in SoConnect User Guide. + +**Format:** `help` + +![help message](images/demo-screenshots/helpWindow.png) + +
:bulb: **Tip:** + +Alternatively, you can view the help window using the top menu bar via `Help -> Help` or press `F1`. +
+ + +#### Redo a _command_: `redo` + +Restores SoConnect to a previously undone state from its history. + +**Format:** `redo` + +**Examples:** + +Input | Expected Output +--------|------------------ +[`edelete 1`](#deleting-an-event-edelete) followed by [`undo`](#undo-a-command-undo) then `redo` | First **restores the deleted event** in the event list.
Then `redo` will **delete the same event again**. + +_See Also: [Undo a command](#undo-a-command-undo)_ + + +#### Undo a _command_: `undo` + +Restores SoConnect to its previous state from its history. + +**Format:** `undo` + +**Examples:** + +Input | Expected Output +--------|------------------ +[`cadd n/John Doe e/john@gmail.com`](#adding-a-contact-cadd) followed by `undo` | **Removes the added** contact from the contact list. + +_See Also: [Redo a command](#redo-a-command-redo)_ + +
:information_source: **Note:** + +[Undo](#undo-a-command-undo) and [redo](#redo-a-command-redo) will only work for _commands_ listed in the +[Contact Management](#contact-management) and [Event Management](#event-management) features section. + +_Commands_ listed in the [General](#general) section are not undoable.
-### Archiving data files `[coming in v2.0]` +-------------------------------------------------------------------------------------------------------------------- + +## SoConnect Saved Data + +### Saving the data + +SoConnect data is saved in the _hard disk_ automatically after any _command_ that changes the data. +There is no need to save manually. + +### Editing the data file -_Details coming soon ..._ +SoConnect data are saved as a _JSON file_ `[JAR file location]/data/soconnect.json`. +Advanced users are welcome to update data directly by editing that data file. + +
:exclamation: **Caution:** +If your changes to the data file make its format invalid, +SoConnect will discard all data and start with an empty data file at the next run. +
-------------------------------------------------------------------------------------------------------------------- -## FAQ +## Others + +### FAQ **Q**: How do I transfer my data to another Computer?
-**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous AddressBook home folder. +**A**: Install SoConnect in the other computer and copy over the contents from your previous SoConnect _JSON file_ to the +empty data file SoConnect creates on the other Computer. + + +#### Copying Details and Opening Hyperlinks + +![clickable links](images/demo-screenshots/clickableLinkExample.png) + +**Q**: How do I copy the email address of a contact?
+**A**: You can copy any specific details of a contact or an event just by clicking on that detail!
+As shown on the image above, clicking on the `email` of `Charlotte Oliveiro` will copy her Email Address. + +**Q**: Can SoConnect automatically open a hyperlink on my browser?
+**A**: You can open any hyperlinks that you have included in a contact or in an event. This includes +telegram handles and Zoom meeting links.
+ +
:bulb: **Tip:** + +Clickable hyperlinks are underlined in blue +
+ +Referring back to the same image, if you click on the zoom link saved under `Charlotte Oliveiro`, +SoConnect will help you open the link on your browser automatically. + + +#### Linked Contacts and Events + +**Q**: How do I view the contacts linked to a particular event?
+**A**: Click on the particular event card in the panel containing events. +Then click on the yellow boxes which are links to the contacts. +The linked contacts will be displayed on the contact panel on the left. +![View links of event](images/demo-screenshots/ClickLinksEvent.png) + +**Q**: How do I view the events linked to a particular contact?
+**A**: Click on the particular contact card in the panel containing contacts. +Then click on the yellow boxes which are links to the events. +The linked events will be displayed on the event panel on the right. +![View links of contact](images/demo-screenshots/ClickLinksContact.png) + +**Q**: What is the purpose of using links?
+**A**: Links are a form of **relationship between the contacts and the events** saved in SoConnect. +Typically, we link an event to a contact if the contact of a **participant** of the event. +For instance, you can link your *professor* to the lecture. + + +### How to start SoConnect using Terminal + +1. Open the terminal (For [MacOS](https://support.apple.com/en-sg/guide/terminal/welcome/mac) or Linux) + or Command Prompt (For [Windows](https://www.howtogeek.com/235101/10-ways-to-open-the-command-prompt-in-windows-10/)). + +2. Navigate to the folder containing `soconnect.jar`. + See the tutorial for [Windows](https://www.howtogeek.com/659411/how-to-change-directories-in-command-prompt-on-windows-10/), + [MacOS or Linux](https://www.macworld.com/article/221277/command-line-navigating-files-folders-mac-terminal.html) + (Linux uses the same _command_ for navigating folders). + +3. Enter the following _command_: `java -jar soconnect.jar`. The SoConnect window should open. + +
:bulb: **Tip:** + +* If you are still unable to open the file, + [check your Java version](https://www.java.com/en/download/help/version_manual.html) + again and make sure it is version 11. +* [Download Java 11](https://www.oracle.com/java/technologies/downloads/) if you have not done so. +
+ -------------------------------------------------------------------------------------------------------------------- +## List of _Prefixes_ + +**Contact Management** + +_Prefix_ | _Parameter_ Type +--------|------------------ +**`a/`** | Address +**`dt/`** | Tag to be deleted +**`e/`** | Email Address +**`n/`** | Name +**`p/`** | Phone Number +**`t/`** | Tag +**`th/`** | Telegram Handle +**`z/`** | Links/ Websites + +**Event Management** + +_Prefix_ | _Parameter_ Type +--------|------------------ +**`a/`** | Address +**`at/`** | Start Date and Time +**`c/`** | Contact index (for linking) +**`d/`** | Description +**`dt/`** | Tag to be deleted +**`end/`** | End Date and Time +**`n/`** | Name +**`t/`** | Tag +**`z/`** | Links/ Websites -## Command summary + +## _Command_ Summary + +**Contact Management** + +Action | Format, Examples +--------|------------------ +**[Add](#adding-a-contact-cadd)** | `cadd n/NAME e/EMAIL [p/PHONE_NUMBER] [a/ADDRESS] [th/TELEGRAM_HANDLE] [z/ZOOM] [t/TAG]…​`
e.g., `cadd n/Alex Doe e/e0123456@u.nus.edu a/COM1 #99-99 th/johnDoe99 t/Professor` +**[Clear](#clearing-all-contacts-cclear)** | `cclear` +**[Delete](#deleting-a-contact-cdelete)** | `cdelete INDEX1[-INDEX2]`
e.g. `cdelete 3`
e.g. `cdelete 1-5` +**[Edit](#editing-a-contact-cedit)** | `cedit INDEX [n/NAME] [e/EMAIL] [p/PHONE] [a/ADDRESS] [th/TELEGRAM_HANDLE] [z/ZOOM] [dt/TAG_DELETED]…​ [t/TAG_ADDED]…​​`
e.g.`cedit 2 p/91234567 e/agentX@thehightable.com`
e.g. `cedit 1 n/Betsy Crower dt/*` +**[Find](#finding-contacts-cfind)** | `cfind [KEYWORD]… [e/KEYWORD…] [p/KEYWORD…] [a/KEYWORD…] [th/KEYWORD…] [z/KEYWORD…] [t/KEYWORD…]`
e.g. `cfind alex david` +**[List](#listing-all-contacts-clist)** | `clist [e/] [p/] [a/] [th/] [z/] [t/]`
e.g. `clist`
e.g. `clist e/ p/` +**[Mark](#marking-a-contact-cmark)** | `cmark INDEX`
e.g. `cmark 2` +**[Remove mark](#removing-mark-of-a-contact-cunmark)** | `cunmark INDEX [INDEX]…`
e.g. `cunmark 2`
e.g. `cunmark 2 3` +**[View](#viewing-a-contact-cview)** | `cview INDEX`
e.g. `cview 2` + +**Event Management** + +Action | Format, Examples +--------|------------------ +**[Add](#adding-an-event-eadd)** | `eadd n/NAME at/START_TIME [end/END_TIME] [d/DESCRIPTION] [a/ADDRESS] [z/ZOOM] [t/TAG]…​ `
e.g., `eadd n/Summer Party at/12-12-2021 15:12 a/123, Clementi Rd, 1234665 t/fun` +**[Clear](#clearing-all-events-eclear)** | `eclear` +**[Delete](#deleting-an-event-edelete)** | `edelete INDEX1[-INDEX2]`
e.g., `edelete 3`
e.g., `edelete 1-5` +**[Edit](#editing-an-event-eedit)** | `eedit INDEX [n/NAME] [at/START_TIME] [end/END_TIME] [d/DESCRIPTION] [a/ADDRESS] [z/ZOOM] [dt/TAG_DELETED]…​ [t/TAG_ADDED]…​`
e.g.,`eedit 2 n/CS2103T Exam dt/CCA t/Hard`
e.g., `eedit 3 dt/*` +**[Find](#finding-events-efind)** | `efind [KEYWORDS]… [at/KEYWORD…] [end/KEYWORD…] [d/KEYWORD…] [a/KEYWORD…] [z/KEYWORD…] [t/KEYWORD…]`
e.g., `efind meet`
e.g., `efind t/CS2103T Intern` +**[Link](#linking-an-event-to-contacts-elink)** | `elink EVENT_INDEX c/CONTACT_INDEX [c/CONTACT_INDEX]...`
`elink 2 c/1 c/2` +**[List](#listing-all-events-elist)** | `elist [at/] [end/] [d/] [a/] [z/] [t/]`
e.g., `elist`
e.g., `elist d/ at/` +**[Mark](#marking-an-event-emark)** | `emark INDEX`
e.g. `emark 2` +**[Remove mark](#removing-mark-of-an-event-eunmark)** | `eunmark INDEX [INDEX]…`
e.g. `eunmark 2`
e.g. `eunmark 2 4` +**[Sort](#sorting-events-esort)** | `esort` +**[Unlink](#unlinking-an-event-from-contacts-eunlink)** | `eunlink EVENT_INDEX c/CONTACT_INDEX [c/CONTACT_INDEX]...`
e.g., `eunlink 2 c/1`
e.g., `eunlink 3 c/*` +**[View](#viewing-an-event-eview)** | `eview INDEX`
e.g. `eview 1` + +**General** 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` +**[Calendar](#calendar-window-calendar)** | `calendar` +**[Exit](#exiting-soconnect-exit)** | `exit` +**[Help](#help-window-help)** | `help` +**[Redo](#redo-a-command-redo)** | `redo` +**[Undo](#undo-a-command-undo)** | `undo` + +________________________________________________________________________________________________________________ + +## Glossary + +Word | Explanation +--------|------------------ +**Command Line Interface (CLI)** | Text-based application where users interact with the application by **typing in texts/ commands**. +**Command** | A string of words and characters you type to perform an action, each command has its own **_Command format_**. +**Command Format** | The structure that a _command_ must abide by for it to be executed. +**Parameter(s)** | Information supplied by the user to the application when executing certain _commands_. +**Prefix** | A tag, consisting of a slash - "/", with one or a few characters to denote information about a certain **_field_**. +**Field(s)** | The information type within each contact or event
For example, **Name** and **Address** of a contact are _fields_ of a contact. +**Graphical User Interface (GUI)** | How the application appears to the user on his/her screen. +**Hard Disk** | Device in a computer that is specialized in storing data permanently. +**Home Folder** | Folder which the application file is saved on the computer. +**JavaScript Object Notation (JSON) File** | The file that is used by the application to load and save data of the application in a human-readable format. diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..6b7b6a30a2a 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "SoConnect" theme: minima header_pages: @@ -8,7 +8,7 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2122S1-CS2103T-W15-3/tp" github_icon: "images/github-icon.png" plugins: diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..b6858eebe25 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: "SoConnect"; font-size: 32px; } } diff --git a/docs/diagrams/ArchitectureSequenceDiagram.puml b/docs/diagrams/ArchitectureSequenceDiagram.puml index ef81d18c337..a470d45582f 100644 --- a/docs/diagrams/ArchitectureSequenceDiagram.puml +++ b/docs/diagrams/ArchitectureSequenceDiagram.puml @@ -7,13 +7,13 @@ Participant ":Logic" as logic LOGIC_COLOR Participant ":Model" as model MODEL_COLOR Participant ":Storage" as storage STORAGE_COLOR -user -[USER_COLOR]> ui : "delete 1" +user -[USER_COLOR]> ui : "edelete 1" activate ui UI_COLOR -ui -[UI_COLOR]> logic : execute("delete 1") +ui -[UI_COLOR]> logic : execute("edelete 1") activate logic LOGIC_COLOR -logic -[LOGIC_COLOR]> model : deletePerson(p) +logic -[LOGIC_COLOR]> model : deleteEvent(e) activate model MODEL_COLOR model -[MODEL_COLOR]-> logic diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml index 5731f9cbaa1..109520731e9 100644 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ b/docs/diagrams/BetterModelClassDiagram.puml @@ -4,18 +4,14 @@ 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 +AddressBook *-right-> "1" UniqueContactList +AddressBook *-left-> "1" UniqueEventList +AddressBook *-down-> "1" UniqueTagList -UniqueTagList *-right-> "*" Tag -UniquePersonList -right-> Person +UniqueTagList *--> "*" Tag +UniqueContactList --> Contact +UniqueEventList --> Event -Person -up-> "*" Tag - -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address +Contact --> "*" Tag +Event --> "*" Tag @enduml diff --git a/docs/diagrams/CMarkActivityDiagram.puml b/docs/diagrams/CMarkActivityDiagram.puml new file mode 100644 index 00000000000..5e4c836ed3b --- /dev/null +++ b/docs/diagrams/CMarkActivityDiagram.puml @@ -0,0 +1,32 @@ +@startuml +'https://plantuml.com/sequence-diagram + +start +:User enters cmark command in the command box; +:CMarkCommandParser parses the command; +if () then ([arguments are valid]) + + :Generate list of indexes; + + if() then ([indexes not greater than length of contact list]) + : get contact corresponding to the index + in the contact list; + : create a new marked contact; + : replace the original contact with the marked contact; + : rearrange contacts in order; + + else ([else]) + :throw CommandException with + invalid contact displayed; + endif + +else ([else]) +:throw ParseException with invalid command +format message and command usage; +endif + +:returns feedback to user; + +stop + +@enduml diff --git a/docs/diagrams/CMarkSequenceDiagram.puml b/docs/diagrams/CMarkSequenceDiagram.puml new file mode 100644 index 00000000000..d76c5a2ee42 --- /dev/null +++ b/docs/diagrams/CMarkSequenceDiagram.puml @@ -0,0 +1,90 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":CMarkCommandParser" as CMarkCommandParser LOGIC_COLOR +participant "c:CMarkCommand" as CMarkCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +participant ":Contact" as Contact MODEL_COLOR +end box + +[-> LogicManager : execute("cmark 1 2") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("cmark 1 2") +activate AddressBookParser + +create CMarkCommandParser +AddressBookParser -> CMarkCommandParser +activate CMarkCommandParser + +CMarkCommandParser --> AddressBookParser +deactivate CMarkCommandParser + +AddressBookParser -> CMarkCommandParser : parse("1 2") +activate CMarkCommandParser + +create CMarkCommand +CMarkCommandParser -> CMarkCommand +activate CMarkCommand + +CMarkCommand --> CMarkCommandParser : c +deactivate CMarkCommand + +CMarkCommandParser --> AddressBookParser : c +deactivate CMarkCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +CMarkCommandParser -[hidden]-> AddressBookParser +destroy CMarkCommandParser + +AddressBookParser --> LogicManager : c +deactivate AddressBookParser + +LogicManager -> CMarkCommand : execute() +activate CMarkCommand + +CMarkCommand -> Model : getFilteredContactList() +activate Model +Model --> CMarkCommand +deactivate Model + +loop 2 times + CMarkCommand -> Contact : markContact() + activate Contact + + Contact --> CMarkCommand + deactivate Contact + + CMarkCommand -> Model : setContact() + activate Model + + Model --> CMarkCommand + deactivate Model + +end + +CMarkCommand -> Model : rearrangeContactsInOrder() +activate Model + +Model --> CMarkCommand +deactivate + +create CommandResult +CMarkCommand -> CommandResult +activate CommandResult + +CommandResult --> CMarkCommand +deactivate CommandResult + +CMarkCommand --> LogicManager : result +deactivate CMarkCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/CUnmarkActivityDiagram.puml b/docs/diagrams/CUnmarkActivityDiagram.puml new file mode 100644 index 00000000000..4b588687761 --- /dev/null +++ b/docs/diagrams/CUnmarkActivityDiagram.puml @@ -0,0 +1,37 @@ +@startuml +'https://plantuml.com/sequence-diagram + +start +:User enters cunmark command in the command box; +:CUnmarkCommandParser parses the command; +if () then ([arguments are valid]) + + :Generate list of indexes; + + if() then ([indexes not greater than length of contact list]) + : get contact corresponding to the index + in the contact list; + if() then ([contact is marked]) + : create a new unmarked contact; + : replace the original contact with the unmarked contact; + + else ([else]) + endif + + : rearrange contacts in order; + + else ([else])) + :throw CommandException with + invalid contact displayed; + endif + +else ([else]) +:throw ParseException with invalid command +format message and command usage; +endif + +:returns feedback to user; + +stop + +@enduml diff --git a/docs/diagrams/CUnmarkSequenceDiagram.puml b/docs/diagrams/CUnmarkSequenceDiagram.puml new file mode 100644 index 00000000000..8f08bb1c136 --- /dev/null +++ b/docs/diagrams/CUnmarkSequenceDiagram.puml @@ -0,0 +1,85 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":CUnmarkCommandParser" as CUnmarkCommandParser LOGIC_COLOR +participant "c:CUnmarkCommand" as CUnmarkCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +participant ":Contact" as Contact MODEL_COLOR +end box + +[-> LogicManager : execute("cunmark 4 5") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("cunmark 4 5") +activate AddressBookParser + +create CUnmarkCommandParser +AddressBookParser -> CUnmarkCommandParser +activate CUnmarkCommandParser + +CUnmarkCommandParser --> AddressBookParser +deactivate CUnmarkCommandParser + +AddressBookParser -> CUnmarkCommandParser : parse("4 5") +activate CUnmarkCommandParser + +create CUnmarkCommand +CUnmarkCommandParser -> CUnmarkCommand +activate CUnmarkCommand + +CUnmarkCommand --> CUnmarkCommandParser : c +deactivate CUnmarkCommand + +CUnmarkCommandParser --> AddressBookParser : c +deactivate CUnmarkCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +CUnmarkCommandParser -[hidden]-> AddressBookParser +destroy CUnmarkCommandParser + +AddressBookParser --> LogicManager : c +deactivate AddressBookParser + +LogicManager -> CUnmarkCommand : execute() +activate CUnmarkCommand + +CUnmarkCommand -> Model : getFilteredContactList() +activate Model +Model --> CUnmarkCommand +deactivate Model + +loop 2 times + CUnmarkCommand -> Contact : getIsMarked() + activate Contact + + Contact --> CUnmarkCommand : isMarked + deactivate Contact + +alt isMarked + CUnmarkCommand -> Contact : unmarkContact() + activate Contact + + Contact --> CUnmarkCommand + deactivate Contact + + CUnmarkCommand -> Model : setContact() + activate Model + + Model --> CUnmarkCommand + deactivate Model +end +end + +CUnmarkCommand -> Model : rearrangeContactsInOrder() +activate Model + +Model --> CUnmarkCommand +deactivate + +@enduml diff --git a/docs/diagrams/CalendarSequenceDiagram.puml b/docs/diagrams/CalendarSequenceDiagram.puml new file mode 100644 index 00000000000..dd37b4bae8f --- /dev/null +++ b/docs/diagrams/CalendarSequenceDiagram.puml @@ -0,0 +1,63 @@ +@startuml +!include style.puml + +actor User + +box UI UI_COLOR_T1 +participant ":CalendarWindow" as CalendarWindow UI_COLOR +participant ":CalendarView" as CalendarView UI_COLOR_T4 +participant ":Calendar" as Calendar UI_COLOR_T4 +participant ":CalendarSource" as CalendarSource UI_COLOR_T4 +participant "calendarui:StackPane" as CalendarUI UI_COLOR_T4 +end box + +create CalendarWindow +User -> CalendarWindow : CalendarWindow(events) +activate CalendarWindow + +create CalendarView +CalendarWindow -> CalendarView : new +activate CalendarView +CalendarView --> CalendarWindow +deactivate CalendarView + +create Calendar +CalendarWindow -> Calendar : new Calendar("Events") +activate Calendar +Calendar --> CalendarWindow +deactivate Calendar + +CalendarWindow -> CalendarWindow : createTimeThread() +note left: parallel thread to\nupdate calendar\nevery 10 seconds + +CalendarWindow -> CalendarWindow : createCalendar(events) + +activate CalendarWindow + +loop all events +CalendarWindow -> CalendarWindow : createEntry(event) +activate CalendarWindow +CalendarWindow -> Calendar : addEntry +deactivate CalendarWindow +end + +deactivate CalendarWindow + +CalendarWindow -> CalendarWindow : updateCalendarView() +activate CalendarWindow + +create CalendarSource +CalendarWindow -> CalendarSource : new +activate CalendarSource +return source +deactivate CalendarSource + +CalendarWindow -> CalendarSource : add(calendar) + +CalendarWindow -> CalendarUI : add calendarView +deactivate CalendarWindow + +CalendarWindow --> User : calendar window +deactivate CalendarWindow + +@enduml diff --git a/docs/diagrams/CommitActivityDiagram.puml b/docs/diagrams/CommitActivityDiagram.puml index 6a6b23a006f..64a784eee2f 100644 --- a/docs/diagrams/CommitActivityDiagram.puml +++ b/docs/diagrams/CommitActivityDiagram.puml @@ -4,12 +4,29 @@ start 'Since the beta syntax does not support placing the condition outside the 'diamond we place it as the true branch instead. +if () then ([UndoCommand]) + if () then ([model is undoable]) + :Restore previous + history instance; + else ([else]) + :throw ModelHistoryException; + endif -if () then ([command commits AddressBook]) - :Purge redundant states; - :Save AddressBook to - addressBookStateList; else ([else]) + if () then ([RedoCommand]) + if() then ([model is redoable]) + :Restore previously + undone history instance; + else ([else]) + : throw ModelHistoryException; + endif + else ([else]) + if () then ([Undoable Command]) + : Commit current history instance; + else ([else]) + endif + endif endif + stop @enduml diff --git a/docs/diagrams/DeleteSequenceDiagram.puml b/docs/diagrams/DeleteSequenceDiagram.puml deleted file mode 100644 index 1dc2311b245..00000000000 --- a/docs/diagrams/DeleteSequenceDiagram.puml +++ /dev/null @@ -1,69 +0,0 @@ -@startuml -!include style.puml - -box Logic LOGIC_COLOR_T1 -participant ":LogicManager" as LogicManager LOGIC_COLOR -participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR -participant ":DeleteCommandParser" as DeleteCommandParser LOGIC_COLOR -participant "d:DeleteCommand" as DeleteCommand 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("delete 1") -activate LogicManager - -LogicManager -> AddressBookParser : parseCommand("delete 1") -activate AddressBookParser - -create DeleteCommandParser -AddressBookParser -> DeleteCommandParser -activate DeleteCommandParser - -DeleteCommandParser --> AddressBookParser -deactivate DeleteCommandParser - -AddressBookParser -> DeleteCommandParser : parse("1") -activate DeleteCommandParser - -create DeleteCommand -DeleteCommandParser -> DeleteCommand -activate DeleteCommand - -DeleteCommand --> DeleteCommandParser : d -deactivate DeleteCommand - -DeleteCommandParser --> AddressBookParser : d -deactivate DeleteCommandParser -'Hidden arrow to position the destroy marker below the end of the activation bar. -DeleteCommandParser -[hidden]-> AddressBookParser -destroy DeleteCommandParser - -AddressBookParser --> LogicManager : d -deactivate AddressBookParser - -LogicManager -> DeleteCommand : execute() -activate DeleteCommand - -DeleteCommand -> Model : deletePerson(1) -activate Model - -Model --> DeleteCommand -deactivate Model - -create CommandResult -DeleteCommand -> CommandResult -activate CommandResult - -CommandResult --> DeleteCommand -deactivate CommandResult - -DeleteCommand --> LogicManager : result -deactivate DeleteCommand - -[<--LogicManager -deactivate LogicManager -@enduml diff --git a/docs/diagrams/EDeleteActivityDiagram.puml b/docs/diagrams/EDeleteActivityDiagram.puml new file mode 100644 index 00000000000..b13615bf78a --- /dev/null +++ b/docs/diagrams/EDeleteActivityDiagram.puml @@ -0,0 +1,37 @@ +@startuml +'https://plantuml.com/activity-diagram-beta + +start +:User enters edelete command in the command box; +:EDeleteCommandParser parses the command; +if () then ([arguments are valid] ) + if () then ( [argument is\na single INDEX]) + :Creates a RANGE with\nSTART INDEX = END INDEX = INPUT INDEX; + + else ([argument is a RANGE]) + + endif +:Creates EDeleteCommand object for execution; + + repeat + :Checks if the event\nat START INDEX is\nlinked to any contacts; + if () then ( [else]) + + else ([has linked\ncontact(s)]) + :Unlinks the event from the contact(s); + + endif + :Deletes the event at START INDEX; + :END INDEX = END INDEX - 1; + repeat while () is ( [END INDEX \n>= START INDEX]) + -> [else] ; + +else ( [else]) +:Throws ParseException with invalid command\nformat message and command usage; +endif + +:Returns feedback to user; + +stop + +@enduml diff --git a/docs/diagrams/EDeleteSequenceDiagram.puml b/docs/diagrams/EDeleteSequenceDiagram.puml new file mode 100644 index 00000000000..2f54d18000e --- /dev/null +++ b/docs/diagrams/EDeleteSequenceDiagram.puml @@ -0,0 +1,72 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":EDeleteCommandParser" as EDeleteCommandParser LOGIC_COLOR +participant "eD:EDeleteCommand" as EDeleteCommand 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("edelete 1-3") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("edelete 1-3") +activate AddressBookParser + +create EDeleteCommandParser +AddressBookParser -> EDeleteCommandParser +activate EDeleteCommandParser + +EDeleteCommandParser --> AddressBookParser +deactivate EDeleteCommandParser + +AddressBookParser -> EDeleteCommandParser : parse("1-3") +activate EDeleteCommandParser + +create EDeleteCommand +EDeleteCommandParser -> EDeleteCommand +activate EDeleteCommand + +EDeleteCommand --> EDeleteCommandParser : eD +deactivate EDeleteCommand + +EDeleteCommandParser --> AddressBookParser : eD +deactivate EDeleteCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +EDeleteCommandParser -[hidden]-> AddressBookParser +destroy EDeleteCommandParser + +AddressBookParser --> LogicManager : eD +deactivate AddressBookParser + +LogicManager -> EDeleteCommand : execute() +activate EDeleteCommand + +loop 3 times + EDeleteCommand -> Model : deleteEvent(1) + activate Model + + Model --> EDeleteCommand + deactivate Model +end + +create CommandResult +EDeleteCommand -> CommandResult +activate CommandResult + +CommandResult --> EDeleteCommand +deactivate CommandResult + +EDeleteCommand --> LogicManager : result +deactivate EDeleteCommand + +[<--LogicManager +deactivate LogicManager + +@enduml diff --git a/docs/diagrams/ELinkActivityDiagram.puml b/docs/diagrams/ELinkActivityDiagram.puml new file mode 100644 index 00000000000..77f8bb549a6 --- /dev/null +++ b/docs/diagrams/ELinkActivityDiagram.puml @@ -0,0 +1,29 @@ +@startuml +'https://plantuml.com/activity-diagram-beta + +start +:User enters elink command in the command box; +:ELinkCommandParser parses the command; +if () then ([arguments are valid] ) +:Creates ElinkCommand object for execution; + + repeat + :Checks if each of the contacts is linked to the event; + if () then ( [already linked]) + + else ([else]) + :Links the event and contact; + endif + repeat while () is ( [else]) + -> [linked to all contacts] ; + +else ( [else]) +:throws ParseException with invalid command +format message and command usage; +endif + +:returns feedback to user; + +stop + +@enduml diff --git a/docs/diagrams/ELinkSequenceDiagram.puml b/docs/diagrams/ELinkSequenceDiagram.puml new file mode 100644 index 00000000000..b8d0362a255 --- /dev/null +++ b/docs/diagrams/ELinkSequenceDiagram.puml @@ -0,0 +1,71 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":ELinkCommandParser" as ELinkCommandParser LOGIC_COLOR +participant "e:ELinkCommand" as ELinkCommand 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("elink 1 c/1 c/2 c/3") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("elink 1 c/1 c/2 c/3") +activate AddressBookParser + +create ELinkCommandParser +AddressBookParser -> ELinkCommandParser +activate ELinkCommandParser + +ELinkCommandParser --> AddressBookParser +deactivate ELinkCommandParser + +AddressBookParser -> ELinkCommandParser : parse("1 c/1 c/2 c/3") +activate ELinkCommandParser + +create ELinkCommand +ELinkCommandParser -> ELinkCommand +activate ELinkCommand + +ELinkCommand --> ELinkCommandParser : e +deactivate ELinkCommand + +ELinkCommandParser --> AddressBookParser : e +deactivate ELinkCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +ELinkCommandParser -[hidden]-> AddressBookParser +destroy ELinkCommandParser + +AddressBookParser --> LogicManager : e +deactivate AddressBookParser + +LogicManager -> ELinkCommand : execute() +activate ELinkCommand + +loop 3 times + ELinkCommand -> Model : linkEventAndContact(contact, event) + activate Model + + Model --> ELinkCommand + deactivate Model +end + +create CommandResult +ELinkCommand -> CommandResult +activate CommandResult + +CommandResult --> ELinkCommand +deactivate CommandResult + +ELinkCommand --> LogicManager : result +deactivate ELinkCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/EListActivityDiagram.puml b/docs/diagrams/EListActivityDiagram.puml new file mode 100644 index 00000000000..1d2baa93740 --- /dev/null +++ b/docs/diagrams/EListActivityDiagram.puml @@ -0,0 +1,26 @@ +@startuml +'https://plantuml.com/activity-diagram-beta + +start +:User enters elist command in the command box; +:EListCommandParser parses the command; +if () then ([valid arguments]) + + if () then ([prefix present]) + :set corresponding fields + to be displayed; + + else ([no prefix present]) + :set all fields to display; + endif + +else ([invalid arguments]) +:throw ParseException with invalid command +format message and command usage; +endif + +:returns feedback to user; + +stop + +@enduml diff --git a/docs/diagrams/EListSequenceDiagram.puml b/docs/diagrams/EListSequenceDiagram.puml new file mode 100644 index 00000000000..043f9e1b225 --- /dev/null +++ b/docs/diagrams/EListSequenceDiagram.puml @@ -0,0 +1,55 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":EListCommand" as eListCommand LOGIC_COLOR +participant ":EListCommandParser" as eListCommandParser LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +participant ":Event" as Event MODEL_COLOR +end box + +[-> LogicManager : execute(elist at/ end/) +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand(elist at/ end/) +activate AddressBookParser + +create eListCommandParser +AddressBookParser -> eListCommandParser +activate eListCommandParser + +eListCommandParser -> eListCommandParser : parse(at/ end/) + +eListCommandParser -> Event : setAllDisplayToFalse() \n setWillDisplayStartDateTime(true) \n setWillDisplayEndDateTime(true) + +create eListCommand +eListCommandParser -> eListCommand +activate eListCommand + +eListCommandParser --> AddressBookParser +deactivate eListCommandParser + +AddressBookParser --> LogicManager : eListCommand +deactivate AddressBookParser + +LogicManager -> eListCommand : execute() + +eListCommand -> Model : updateFilteredEventList(PREDICATE_HIDE_ALL_EVENTS) \n updateFilteredEventList(PREDICATE_SHOW_ALL_EVENTS); +activate Model + +Model --> eListCommand +deactivate Model + +eListCommand --> LogicManager : result +deactivate eListCommand +eListCommand -[hidden]-> LogicManager : result +destroy eListCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/EventChangerClassDiagram.puml b/docs/diagrams/EventChangerClassDiagram.puml new file mode 100644 index 00000000000..784dcf30ec8 --- /dev/null +++ b/docs/diagrams/EventChangerClassDiagram.puml @@ -0,0 +1,38 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR +show members +hide empty members + +class EventChanger { + {field} - isClear:boolean + {static} + addEventChanger(Event) + {static} + deleteEventChanger(Event) + {static} + editEventChanger(Event, Event) + {static} + clearEventChanger() +} + +EventChanger -> "0..2" Event + +class CalendarWindow UI_COLOR +CalendarWindow -right[dashed]-> EventChanger + +abstract class "{abstract}\nCommand" as Command LOGIC_COLOR +class EDeleteCommand LOGIC_COLOR +class EAddCommand LOGIC_COLOR +class EEditCommand LOGIC_COLOR +class EClearCommand LOGIC_COLOR + +EDeleteCommand --down[LOGIC_COLOR]|> Command +EAddCommand --down[LOGIC_COLOR]|> Command +EEditCommand --down[LOGIC_COLOR]|> Command +EClearCommand --down[LOGIC_COLOR]|> Command +EventChanger <-[LOGIC_COLOR,dashed]- EDeleteCommand +EventChanger <-[LOGIC_COLOR,dashed]- EClearCommand +EventChanger <-[LOGIC_COLOR,dashed]- EEditCommand +EventChanger <-[LOGIC_COLOR,dashed]- EAddCommand + + +@enduml diff --git a/docs/diagrams/LogicClassDiagram.puml b/docs/diagrams/LogicClassDiagram.puml index 6d14b17b361..ea65addb816 100644 --- a/docs/diagrams/LogicClassDiagram.puml +++ b/docs/diagrams/LogicClassDiagram.puml @@ -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 = CAddCommand, \nEFindCommand, etc Logic ..> CommandResult LogicManager .down.> CommandResult diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 1122257bd9a..1d434612007 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -15,14 +15,32 @@ Class ModelManager Class UserPrefs Class ReadOnlyUserPrefs +Package Contact <> { +Class UniqueContactList +Class Contact +Class Email +Class Phone +Class TelegramHandle +} -Class UniquePersonList -Class Person +Package Common <> { Class Address -Class Email Class Name -Class Phone +Class ZoomLink +} + +Package Event <> { +Class UniqueEventList +Class Event +Class EndDateTime +Class DateAndTime +Class Description +Class StartDateTime +} + +Package Tag <> { Class Tag +} } @@ -38,17 +56,35 @@ ModelManager -left-> "1" AddressBook ModelManager -right-> "1" UserPrefs UserPrefs .up.|> ReadOnlyUserPrefs -AddressBook *--> "1" UniquePersonList -UniquePersonList --> "~* all" Person -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address -Person *--> "*" Tag +AddressBook *--> "1" UniqueContactList +UniqueContactList --> "~* all" Contact +Contact *--> "1" Name +Contact *--> "0..1" Phone +Contact *--> "1" Email +Contact *--> "0..1" Address +Contact *--> "0..1" ZoomLink +Contact *--> "0..1" TelegramHandle +Contact *--> "*" Tag + +StartDateTime .down.|> DateAndTime +EndDateTime .down.|> DateAndTime + +AddressBook *--> "1" UniqueEventList +UniqueEventList --> "~* all" Event +Event *--> "1" Name +Event *--> "0..1" Address +Event *--> "0..1" ZoomLink +Event *--> "0..1" Description +Event *--> "1" StartDateTime +Event *--> "0..1" EndDateTime +Event *--> "*" Tag + +Tag -down[hidden]- Common +Address -up[hidden]- Name +Address -up[hidden]- ZoomLink +ZoomLink -up[hidden]- Name -Name -[hidden]right-> Phone -Phone -[hidden]right-> Address -Address -[hidden]right-> Email +ModelManager -->"~* filtered" Contact +ModelManager -->"~* filtered" Event -ModelManager -->"~* filtered" Person @enduml diff --git a/docs/diagrams/ModelHistory.puml b/docs/diagrams/ModelHistory.puml new file mode 100644 index 00000000000..e7f166b0caf --- /dev/null +++ b/docs/diagrams/ModelHistory.puml @@ -0,0 +1,21 @@ +@startuml +!include style.puml + +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR_T4 +skinparam classBackgroundColor MODEL_COLOR + +package Model { + Class ModelManager + Class ModelHistory + Class HistoryInstance + Class AddressBook + class ModelDisplaySetting +} + +ModelManager -> ModelHistory +ModelHistory -down-> HistoryInstance +HistoryInstance -down-> AddressBook +HistoryInstance -down-> ModelDisplaySetting + +@enduml diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index 85ac3ea2dee..c76582be55d 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -19,6 +19,7 @@ Interface AddressBookStorage <> Class JsonAddressBookStorage Class JsonSerializableAddressBook Class JsonAdaptedPerson +Class JsonAdaptedEvent Class JsonAdaptedTag } @@ -38,6 +39,8 @@ JsonUserPrefsStorage .up.|> UserPrefsStorage JsonAddressBookStorage .up.|> AddressBookStorage JsonAddressBookStorage ..> JsonSerializableAddressBook JsonSerializableAddressBook --> "*" JsonAdaptedPerson +JsonSerializableAddressBook --> "*" JsonAdaptedEvent JsonAdaptedPerson --> "*" JsonAdaptedTag +JsonAdaptedEvent --> "*" JsonAdaptedTag @enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index ecae4876432..1e88a949631 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -3,22 +3,26 @@ skinparam arrowThickness 1.1 skinparam arrowColor UI_COLOR_T4 skinparam classBackgroundColor UI_COLOR +'skinparam linetype ortho package UI <>{ Interface Ui <> Class "{abstract}\nUiPart" as UiPart Class UiManager Class MainWindow -Class HelpWindow -Class ResultDisplay -Class PersonListPanel -Class PersonCard -Class StatusBarFooter -Class CommandBox +Class CalendarWindow +Class ContactListPanel +Class EventListPanel +Class ContactCard +Class EventCard +package OtherUiComponents <> { +Class HiddenComponent #FFFFFF +} } package Model <> { -Class HiddenModel #FFFFFF +Class Event +Class Contact } package Logic <> { @@ -30,31 +34,30 @@ 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 +Ui -[hidden]---> OtherUiComponents +MainWindow *--> OtherUiComponents +MainWindow *--> "1" ContactListPanel +MainWindow *--> "1" EventListPanel +MainWindow *--> "0..1" CalendarWindow -PersonListPanel -down-> "*" PersonCard +ContactListPanel -down-> "*" ContactCard +EventListPanel -down-> "*" EventCard MainWindow -left-|> UiPart -ResultDisplay --|> UiPart -CommandBox --|> UiPart -PersonListPanel --|> UiPart -PersonCard --|> UiPart -StatusBarFooter --|> UiPart -HelpWindow --|> UiPart +ContactListPanel --|> UiPart +EventListPanel --|> UiPart +ContactCard --|> UiPart +EventCard --|> UiPart +CalendarWindow --|> UiPart +OtherUiComponents --|> UiPart -PersonCard ..> Model +ContactCard ..> Contact +EventCard ..> Event +CalendarWindow --> Event : calendar of > UiManager -right-> Logic MainWindow -left-> Logic -PersonListPanel -[hidden]left- HelpWindow -HelpWindow -[hidden]left- CommandBox -CommandBox -[hidden]left- ResultDisplay -ResultDisplay -[hidden]left- StatusBarFooter - -MainWindow -[hidden]-|> UiPart +'MainWindow -[hidden]-|> UiPart +ContactListPanel -[hidden]left- EventListPanel @enduml diff --git a/docs/diagrams/UndoRedoState0.puml b/docs/diagrams/UndoRedoState0.puml index 96e30744d24..397c0c33ace 100644 --- a/docs/diagrams/UndoRedoState0.puml +++ b/docs/diagrams/UndoRedoState0.puml @@ -3,18 +3,23 @@ skinparam ClassFontColor #000000 skinparam ClassBorderColor #000000 -title Initial state +title 1. Initial state -package States { - class State1 as "__ab0:AddressBook__" - class State2 as "__ab1:AddressBook__" - class State3 as "__ab2:AddressBook__" +package ModelHistory { + class State1 as "__hi0:HistoryInstance__" + class State2 as "__hi1:HistoryInstance__" + class State3 as "__hi2:HistoryInstance__" + class State4 as "__hi3:HistoryInstance__" } State1 -[hidden]right-> State2 State2 -[hidden]right-> State3 +State3 -[hidden]right-> State4 hide State2 hide State3 +hide State4 -class Pointer as "Current State" #FFFFF -Pointer -up-> State1 +class Pointer1 as "Current Size" #FFF +Pointer1 -up-> State1 +class Pointer2 as "Max Size" #FFF +Pointer2 -up-> State1 @end diff --git a/docs/diagrams/UndoRedoState1.puml b/docs/diagrams/UndoRedoState1.puml index 01fcb9b2b96..897c107c91f 100644 --- a/docs/diagrams/UndoRedoState1.puml +++ b/docs/diagrams/UndoRedoState1.puml @@ -3,20 +3,24 @@ skinparam ClassFontColor #000000 skinparam ClassBorderColor #000000 -title After command "delete 5" +title 2. After command "edelete 5" -package States <> { - class State1 as "__ab0:AddressBook__" - class State2 as "__ab1:AddressBook__" - class State3 as "__ab2:AddressBook__" +package ModelHistory { + class State1 as "__hi0:HistoryInstance__" + class State2 as "__hi1:HistoryInstance__" + class State3 as "__hi2:HistoryInstance__" + class State4 as "__hi3:HistoryInstance__" } State1 -[hidden]right-> State2 State2 -[hidden]right-> State3 +State3 -[hidden]right-> State4 hide State3 +hide State4 -class Pointer as "Current State" #FFFFF - -Pointer -up-> State2 +class Pointer1 as "Current Size" #FFF +Pointer1 -up-> State2 +class Pointer2 as "Max Size" #FFF +Pointer2 -up-> State2 @end diff --git a/docs/diagrams/UndoRedoState2.puml b/docs/diagrams/UndoRedoState2.puml index bccc230a5d1..459a5867fc6 100644 --- a/docs/diagrams/UndoRedoState2.puml +++ b/docs/diagrams/UndoRedoState2.puml @@ -3,18 +3,23 @@ skinparam ClassFontColor #000000 skinparam ClassBorderColor #000000 -title After command "add n/David" +title 3. After command "cadd n/David" -package States <> { - class State1 as "__ab0:AddressBook__" - class State2 as "__ab1:AddressBook__" - class State3 as "__ab2:AddressBook__" +package ModelHistory { + class State1 as "__hi0:HistoryInstance__" + class State2 as "__hi1:HistoryInstance__" + class State3 as "__hi2:HistoryInstance__" + class State4 as "__hi3:HistoryInstance__" } State1 -[hidden]right-> State2 State2 -[hidden]right-> State3 +State3 -[hidden]right-> State4 -class Pointer as "Current State" #FFFFF +hide State4 -Pointer -up-> State3 +class Pointer1 as "Current Size" #FFF +Pointer1 -up-> State3 +class Pointer2 as "Max Size" #FFF +Pointer2 -up-> State3 @end diff --git a/docs/diagrams/UndoRedoState3.puml b/docs/diagrams/UndoRedoState3.puml index ea29c9483e4..587a256fe20 100644 --- a/docs/diagrams/UndoRedoState3.puml +++ b/docs/diagrams/UndoRedoState3.puml @@ -3,18 +3,23 @@ skinparam ClassFontColor #000000 skinparam ClassBorderColor #000000 -title After command "undo" +title 4. After command "undo" -package States <> { - class State1 as "__ab0:AddressBook__" - class State2 as "__ab1:AddressBook__" - class State3 as "__ab2:AddressBook__" +package ModelHistory { + class State1 as "__hi0:HistoryInstance__" + class State2 as "__hi1:HistoryInstance__" + class State3 as "__hi2:HistoryInstance__" + class State4 as "__hi3:HistoryInstance__" } State1 -[hidden]right-> State2 State2 -[hidden]right-> State3 +State3 -[hidden]right-> State4 -class Pointer as "Current State" #FFFFF +hide State4 -Pointer -up-> State2 +class Pointer1 as "Current Size" #FFF +Pointer1 -up-> State2 +class Pointer2 as "Max Size" #FFF +Pointer2 -up-> State3 @end diff --git a/docs/diagrams/UndoRedoState4.puml b/docs/diagrams/UndoRedoState4.puml index 1b784cece80..6774b984bf4 100644 --- a/docs/diagrams/UndoRedoState4.puml +++ b/docs/diagrams/UndoRedoState4.puml @@ -3,18 +3,22 @@ skinparam ClassFontColor #000000 skinparam ClassBorderColor #000000 -title After command "list" +title 5a. After command "redo" -package States <> { - class State1 as "__ab0:AddressBook__" - class State2 as "__ab1:AddressBook__" - class State3 as "__ab2:AddressBook__" +package ModelHistory { + class State1 as "__hi0:HistoryInstance__" + class State2 as "__hi1:HistoryInstance__" + class State3 as "__hi2:HistoryInstance__" + class State4 as "__hi3:HistoryInstance__" } State1 -[hidden]right-> State2 State2 -[hidden]right-> State3 +State3 -[hidden]right-> State4 +hide State4 -class Pointer as "Current State" #FFFFF - -Pointer -up-> State2 +class Pointer1 as "Current Size" #FFF +Pointer1 -up-> State3 +class Pointer2 as "Max Size" #FFF +Pointer2 -up-> State3 @end diff --git a/docs/diagrams/UndoRedoState5.puml b/docs/diagrams/UndoRedoState5.puml index 88927be32bc..502404c0723 100644 --- a/docs/diagrams/UndoRedoState5.puml +++ b/docs/diagrams/UndoRedoState5.puml @@ -3,19 +3,23 @@ skinparam ClassFontColor #000000 skinparam ClassBorderColor #000000 -title After command "clear" +title 5b. After command "eclear" -package States <> { - class State1 as "__ab0:AddressBook__" - class State2 as "__ab1:AddressBook__" - class State3 as "__ab3:AddressBook__" +package ModelHistory { + class State1 as "__hi0:HistoryInstance__" + class State2 as "__hi1:HistoryInstance__" + class State3 as "__hi2:HistoryInstance__" #807f7d + class State4 as "__hi3:HistoryInstance__" } State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 +State2 -[hidden]right-> State4 +State4 -[hidden]right-> State3 -class Pointer as "Current State" #FFFFF +class Pointer1 as "Current Size" #FFF +Pointer1 -up-> State4 +class Pointer2 as "Max Size" #FFF +Pointer2 -up-> State4 -Pointer -up-> State3 -note right on link: State ab2 deleted. +note right on link: State hi2 cannot be accessed. @end diff --git a/docs/diagrams/UndoSequenceDiagram.puml b/docs/diagrams/UndoSequenceDiagram.puml index 410aab4e412..0591cfb0d99 100644 --- a/docs/diagrams/UndoSequenceDiagram.puml +++ b/docs/diagrams/UndoSequenceDiagram.puml @@ -1,4 +1,5 @@ @startuml + !include style.puml box Logic LOGIC_COLOR_T1 @@ -9,8 +10,9 @@ end box box Model MODEL_COLOR_T1 participant ":Model" as Model MODEL_COLOR -participant ":VersionedAddressBook" as VersionedAddressBook MODEL_COLOR +participant ":ModelHistory" as ModelHistory MODEL_COLOR end box + [-> LogicManager : execute(undo) activate LogicManager @@ -30,15 +32,14 @@ deactivate AddressBookParser LogicManager -> UndoCommand : execute() activate UndoCommand -UndoCommand -> Model : undoAddressBook() +UndoCommand -> Model : undoHistory() activate Model -Model -> VersionedAddressBook : undo() -activate VersionedAddressBook +Model -> ModelHistory : undo() +activate ModelHistory -VersionedAddressBook -> VersionedAddressBook :resetData(ReadOnlyAddressBook) -VersionedAddressBook --> Model : -deactivate VersionedAddressBook +ModelHistory --> Model : +deactivate ModelHistory Model --> UndoCommand deactivate Model @@ -50,4 +51,5 @@ destroy UndoCommand [<--LogicManager deactivate LogicManager + @enduml diff --git a/docs/diagrams/style.puml b/docs/diagrams/style.puml index fad8b0adeaa..0b802536023 100644 --- a/docs/diagrams/style.puml +++ b/docs/diagrams/style.puml @@ -41,6 +41,8 @@ skinparam Class { FontColor #FFFFFF BorderThickness 1 BorderColor #FFFFFF + AttributeFontColor #FFFFFF + AttributeIconSize 0 StereotypeFontColor #FFFFFF FontName Arial } diff --git a/docs/images/ArchitectureSequenceDiagram.png b/docs/images/ArchitectureSequenceDiagram.png index 2f1346869d0..5a7402a2fe6 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..e0696734a2c 100644 Binary files a/docs/images/BetterModelClassDiagram.png and b/docs/images/BetterModelClassDiagram.png differ diff --git a/docs/images/CMarkActivityDiagram.png b/docs/images/CMarkActivityDiagram.png new file mode 100644 index 00000000000..fcffb4d4aac Binary files /dev/null and b/docs/images/CMarkActivityDiagram.png differ diff --git a/docs/images/CMarkSequenceDiagram.png b/docs/images/CMarkSequenceDiagram.png new file mode 100644 index 00000000000..d96e96ecfa0 Binary files /dev/null and b/docs/images/CMarkSequenceDiagram.png differ diff --git a/docs/images/CUnmarkActivityDiagram.png b/docs/images/CUnmarkActivityDiagram.png new file mode 100644 index 00000000000..551e45c790e Binary files /dev/null and b/docs/images/CUnmarkActivityDiagram.png differ diff --git a/docs/images/CUnmarkSequenceDiagram.png b/docs/images/CUnmarkSequenceDiagram.png new file mode 100644 index 00000000000..51e4e17adb8 Binary files /dev/null and b/docs/images/CUnmarkSequenceDiagram.png differ diff --git a/docs/images/CalendarSequenceDiagram.png b/docs/images/CalendarSequenceDiagram.png new file mode 100644 index 00000000000..77556f5d4cc Binary files /dev/null and b/docs/images/CalendarSequenceDiagram.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/DeleteSequenceDiagram.png b/docs/images/DeleteSequenceDiagram.png deleted file mode 100644 index fa327b39618..00000000000 Binary files a/docs/images/DeleteSequenceDiagram.png and /dev/null differ diff --git a/docs/images/EDeleteActivityDiagram.png b/docs/images/EDeleteActivityDiagram.png new file mode 100644 index 00000000000..4f31dd10218 Binary files /dev/null and b/docs/images/EDeleteActivityDiagram.png differ diff --git a/docs/images/EDeleteSequenceDiagram.png b/docs/images/EDeleteSequenceDiagram.png new file mode 100644 index 00000000000..dc584468943 Binary files /dev/null and b/docs/images/EDeleteSequenceDiagram.png differ diff --git a/docs/images/ELinkActivityDiagram.png b/docs/images/ELinkActivityDiagram.png new file mode 100644 index 00000000000..0ceb9dd6c2a Binary files /dev/null and b/docs/images/ELinkActivityDiagram.png differ diff --git a/docs/images/ELinkSequenceDiagram.png b/docs/images/ELinkSequenceDiagram.png new file mode 100644 index 00000000000..5226ed6d7f4 Binary files /dev/null and b/docs/images/ELinkSequenceDiagram.png differ diff --git a/docs/images/EListActivityDiagram.png b/docs/images/EListActivityDiagram.png new file mode 100644 index 00000000000..4eaaf4565f9 Binary files /dev/null and b/docs/images/EListActivityDiagram.png differ diff --git a/docs/images/EListSequenceDiagram.png b/docs/images/EListSequenceDiagram.png new file mode 100644 index 00000000000..c6b4c855378 Binary files /dev/null and b/docs/images/EListSequenceDiagram.png differ diff --git a/docs/images/EventChangerClassDiagram.png b/docs/images/EventChangerClassDiagram.png new file mode 100644 index 00000000000..fc1f676794e Binary files /dev/null and b/docs/images/EventChangerClassDiagram.png differ diff --git a/docs/images/LogicClassDiagram.png b/docs/images/LogicClassDiagram.png index c3028aa1cda..bccf7cf608f 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..4c7311e2dc1 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/ModelHistory.png b/docs/images/ModelHistory.png new file mode 100644 index 00000000000..18062c20d75 Binary files /dev/null and b/docs/images/ModelHistory.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 82c66f8f16e..c872879697d 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 91488fd1a0f..75168984408 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..26a9ddbf17a 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/UndoRedoActivityDiagram.png b/docs/images/UndoRedoActivityDiagram.png new file mode 100644 index 00000000000..534b79bfe24 Binary files /dev/null and b/docs/images/UndoRedoActivityDiagram.png differ diff --git a/docs/images/UndoRedoState0.png b/docs/images/UndoRedoState0.png index 8f7538cd884..eadb61571d4 100644 Binary files a/docs/images/UndoRedoState0.png and b/docs/images/UndoRedoState0.png differ diff --git a/docs/images/UndoRedoState1.png b/docs/images/UndoRedoState1.png index df9908d0948..239c3d629db 100644 Binary files a/docs/images/UndoRedoState1.png and b/docs/images/UndoRedoState1.png differ diff --git a/docs/images/UndoRedoState2.png b/docs/images/UndoRedoState2.png index 36519c1015b..b4097379c00 100644 Binary files a/docs/images/UndoRedoState2.png and b/docs/images/UndoRedoState2.png differ diff --git a/docs/images/UndoRedoState3.png b/docs/images/UndoRedoState3.png index 19959d01712..c82ca28d193 100644 Binary files a/docs/images/UndoRedoState3.png and b/docs/images/UndoRedoState3.png differ diff --git a/docs/images/UndoRedoState4.png b/docs/images/UndoRedoState4.png index 4c623e4f2c5..53548380884 100644 Binary files a/docs/images/UndoRedoState4.png and b/docs/images/UndoRedoState4.png differ diff --git a/docs/images/UndoRedoState5.png b/docs/images/UndoRedoState5.png index 84ad2afa6bd..8174a6119d3 100644 Binary files a/docs/images/UndoRedoState5.png and b/docs/images/UndoRedoState5.png differ diff --git a/docs/images/UndoSequenceDiagram.png b/docs/images/UndoSequenceDiagram.png index 6addcd3a8d9..bc20308750b 100644 Binary files a/docs/images/UndoSequenceDiagram.png and b/docs/images/UndoSequenceDiagram.png differ diff --git a/docs/images/chunweii.png b/docs/images/chunweii.png new file mode 100644 index 00000000000..3b7799a9374 Binary files /dev/null and b/docs/images/chunweii.png differ diff --git a/docs/images/demo-screenshots/BookmarkContact.png b/docs/images/demo-screenshots/BookmarkContact.png new file mode 100644 index 00000000000..32990480ca0 Binary files /dev/null and b/docs/images/demo-screenshots/BookmarkContact.png differ diff --git a/docs/images/demo-screenshots/BookmarkEvents.png b/docs/images/demo-screenshots/BookmarkEvents.png new file mode 100644 index 00000000000..a36162ba268 Binary files /dev/null and b/docs/images/demo-screenshots/BookmarkEvents.png differ diff --git a/docs/images/demo-screenshots/Calendar.png b/docs/images/demo-screenshots/Calendar.png new file mode 100644 index 00000000000..16224e38f3b Binary files /dev/null and b/docs/images/demo-screenshots/Calendar.png differ diff --git a/docs/images/demo-screenshots/ClickLinksContact.png b/docs/images/demo-screenshots/ClickLinksContact.png new file mode 100644 index 00000000000..9d1cadeed3b Binary files /dev/null and b/docs/images/demo-screenshots/ClickLinksContact.png differ diff --git a/docs/images/demo-screenshots/ClickLinksEvent.png b/docs/images/demo-screenshots/ClickLinksEvent.png new file mode 100644 index 00000000000..f773bfc6af1 Binary files /dev/null and b/docs/images/demo-screenshots/ClickLinksEvent.png differ diff --git a/docs/images/demo-screenshots/UnmarkContacts.png b/docs/images/demo-screenshots/UnmarkContacts.png new file mode 100644 index 00000000000..e4d8ca26fe0 Binary files /dev/null and b/docs/images/demo-screenshots/UnmarkContacts.png differ diff --git a/docs/images/demo-screenshots/UnmarkEvents.png b/docs/images/demo-screenshots/UnmarkEvents.png new file mode 100644 index 00000000000..9bbbcb9dd08 Binary files /dev/null and b/docs/images/demo-screenshots/UnmarkEvents.png differ diff --git a/docs/images/demo-screenshots/addressIcon.png b/docs/images/demo-screenshots/addressIcon.png new file mode 100644 index 00000000000..292b1c5e5b8 Binary files /dev/null and b/docs/images/demo-screenshots/addressIcon.png differ diff --git a/docs/images/demo-screenshots/bookmarkIcon.png b/docs/images/demo-screenshots/bookmarkIcon.png new file mode 100644 index 00000000000..7e736ef607e Binary files /dev/null and b/docs/images/demo-screenshots/bookmarkIcon.png differ diff --git a/docs/images/demo-screenshots/caddEx1.png b/docs/images/demo-screenshots/caddEx1.png new file mode 100644 index 00000000000..7e58f88fcdc Binary files /dev/null and b/docs/images/demo-screenshots/caddEx1.png differ diff --git a/docs/images/demo-screenshots/caddEx2.png b/docs/images/demo-screenshots/caddEx2.png new file mode 100644 index 00000000000..2a6908be2bc Binary files /dev/null and b/docs/images/demo-screenshots/caddEx2.png differ diff --git a/docs/images/demo-screenshots/ceditEx1.png b/docs/images/demo-screenshots/ceditEx1.png new file mode 100644 index 00000000000..956e0bf9ee4 Binary files /dev/null and b/docs/images/demo-screenshots/ceditEx1.png differ diff --git a/docs/images/demo-screenshots/ceditEx2.png b/docs/images/demo-screenshots/ceditEx2.png new file mode 100644 index 00000000000..45346ddc462 Binary files /dev/null and b/docs/images/demo-screenshots/ceditEx2.png differ diff --git a/docs/images/demo-screenshots/cfindEx1.png b/docs/images/demo-screenshots/cfindEx1.png new file mode 100644 index 00000000000..7ddd86e15f3 Binary files /dev/null and b/docs/images/demo-screenshots/cfindEx1.png differ diff --git a/docs/images/demo-screenshots/cfindEx2.png b/docs/images/demo-screenshots/cfindEx2.png new file mode 100644 index 00000000000..0f27e6f4626 Binary files /dev/null and b/docs/images/demo-screenshots/cfindEx2.png differ diff --git a/docs/images/demo-screenshots/clickableLinkExample.png b/docs/images/demo-screenshots/clickableLinkExample.png new file mode 100644 index 00000000000..ae646253fd1 Binary files /dev/null and b/docs/images/demo-screenshots/clickableLinkExample.png differ diff --git a/docs/images/demo-screenshots/clistEx.png b/docs/images/demo-screenshots/clistEx.png new file mode 100644 index 00000000000..8d8fb5e3582 Binary files /dev/null and b/docs/images/demo-screenshots/clistEx.png differ diff --git a/docs/images/demo-screenshots/cmarkEx.png b/docs/images/demo-screenshots/cmarkEx.png new file mode 100644 index 00000000000..d1715be3dfa Binary files /dev/null and b/docs/images/demo-screenshots/cmarkEx.png differ diff --git a/docs/images/demo-screenshots/cmarkEx1.png b/docs/images/demo-screenshots/cmarkEx1.png new file mode 100644 index 00000000000..2d302846ca9 Binary files /dev/null and b/docs/images/demo-screenshots/cmarkEx1.png differ diff --git a/docs/images/demo-screenshots/cmarkEx2.png b/docs/images/demo-screenshots/cmarkEx2.png new file mode 100644 index 00000000000..ab56f22dae3 Binary files /dev/null and b/docs/images/demo-screenshots/cmarkEx2.png differ diff --git a/docs/images/demo-screenshots/commandSyntax.png b/docs/images/demo-screenshots/commandSyntax.png new file mode 100644 index 00000000000..7b6fdba37d0 Binary files /dev/null and b/docs/images/demo-screenshots/commandSyntax.png differ diff --git a/docs/images/demo-screenshots/contactCard.png b/docs/images/demo-screenshots/contactCard.png new file mode 100644 index 00000000000..57826c233e5 Binary files /dev/null and b/docs/images/demo-screenshots/contactCard.png differ diff --git a/docs/images/demo-screenshots/contactsIcon.png b/docs/images/demo-screenshots/contactsIcon.png new file mode 100644 index 00000000000..82d8e0fbf64 Binary files /dev/null and b/docs/images/demo-screenshots/contactsIcon.png differ diff --git a/docs/images/demo-screenshots/cunmarkEx1.png b/docs/images/demo-screenshots/cunmarkEx1.png new file mode 100644 index 00000000000..29f29a08978 Binary files /dev/null and b/docs/images/demo-screenshots/cunmarkEx1.png differ diff --git a/docs/images/demo-screenshots/cunmarkEx2.png b/docs/images/demo-screenshots/cunmarkEx2.png new file mode 100644 index 00000000000..a1ba4739463 Binary files /dev/null and b/docs/images/demo-screenshots/cunmarkEx2.png differ diff --git a/docs/images/demo-screenshots/cviewEx.png b/docs/images/demo-screenshots/cviewEx.png new file mode 100644 index 00000000000..9b13c4df67e Binary files /dev/null and b/docs/images/demo-screenshots/cviewEx.png differ diff --git a/docs/images/demo-screenshots/descriptionIcon.png b/docs/images/demo-screenshots/descriptionIcon.png new file mode 100644 index 00000000000..bc7bf0ea3e3 Binary files /dev/null and b/docs/images/demo-screenshots/descriptionIcon.png differ diff --git a/docs/images/demo-screenshots/eaddEx.png b/docs/images/demo-screenshots/eaddEx.png new file mode 100644 index 00000000000..44de8a2a70d Binary files /dev/null and b/docs/images/demo-screenshots/eaddEx.png differ diff --git a/docs/images/demo-screenshots/eeditEx.png b/docs/images/demo-screenshots/eeditEx.png new file mode 100644 index 00000000000..fb7dc6fef9d Binary files /dev/null and b/docs/images/demo-screenshots/eeditEx.png differ diff --git a/docs/images/demo-screenshots/efindEx1.png b/docs/images/demo-screenshots/efindEx1.png new file mode 100644 index 00000000000..ea4cd674100 Binary files /dev/null and b/docs/images/demo-screenshots/efindEx1.png differ diff --git a/docs/images/demo-screenshots/efindEx2.png b/docs/images/demo-screenshots/efindEx2.png new file mode 100644 index 00000000000..ab80fe8d041 Binary files /dev/null and b/docs/images/demo-screenshots/efindEx2.png differ diff --git a/docs/images/demo-screenshots/elinkEx.png b/docs/images/demo-screenshots/elinkEx.png new file mode 100644 index 00000000000..8a5ae6fe6ba Binary files /dev/null and b/docs/images/demo-screenshots/elinkEx.png differ diff --git a/docs/images/demo-screenshots/elistEx.png b/docs/images/demo-screenshots/elistEx.png new file mode 100644 index 00000000000..c45f4372a24 Binary files /dev/null and b/docs/images/demo-screenshots/elistEx.png differ diff --git a/docs/images/demo-screenshots/emailIcon.png b/docs/images/demo-screenshots/emailIcon.png new file mode 100644 index 00000000000..bb21667f584 Binary files /dev/null and b/docs/images/demo-screenshots/emailIcon.png differ diff --git a/docs/images/demo-screenshots/endTimeIcon.png b/docs/images/demo-screenshots/endTimeIcon.png new file mode 100644 index 00000000000..ead7561a603 Binary files /dev/null and b/docs/images/demo-screenshots/endTimeIcon.png differ diff --git a/docs/images/demo-screenshots/eunlinkEx1.png b/docs/images/demo-screenshots/eunlinkEx1.png new file mode 100644 index 00000000000..f83f27552d8 Binary files /dev/null and b/docs/images/demo-screenshots/eunlinkEx1.png differ diff --git a/docs/images/demo-screenshots/eunlinkEx2.png b/docs/images/demo-screenshots/eunlinkEx2.png new file mode 100644 index 00000000000..a15c121e6d8 Binary files /dev/null and b/docs/images/demo-screenshots/eunlinkEx2.png differ diff --git a/docs/images/demo-screenshots/eventCard.png b/docs/images/demo-screenshots/eventCard.png new file mode 100644 index 00000000000..9155f386e1c Binary files /dev/null and b/docs/images/demo-screenshots/eventCard.png differ diff --git a/docs/images/demo-screenshots/eventIcon.png b/docs/images/demo-screenshots/eventIcon.png new file mode 100644 index 00000000000..3b21f42c304 Binary files /dev/null and b/docs/images/demo-screenshots/eventIcon.png differ diff --git a/docs/images/demo-screenshots/fileTab.png b/docs/images/demo-screenshots/fileTab.png new file mode 100644 index 00000000000..7f563afbf0d Binary files /dev/null and b/docs/images/demo-screenshots/fileTab.png differ diff --git a/docs/images/demo-screenshots/helpTab.png b/docs/images/demo-screenshots/helpTab.png new file mode 100644 index 00000000000..a66cc15a98f Binary files /dev/null and b/docs/images/demo-screenshots/helpTab.png differ diff --git a/docs/images/demo-screenshots/helpWindow.png b/docs/images/demo-screenshots/helpWindow.png new file mode 100644 index 00000000000..8e5c8e7e709 Binary files /dev/null and b/docs/images/demo-screenshots/helpWindow.png differ diff --git a/docs/images/demo-screenshots/homeFolder.png b/docs/images/demo-screenshots/homeFolder.png new file mode 100644 index 00000000000..a8f04575d5e Binary files /dev/null and b/docs/images/demo-screenshots/homeFolder.png differ diff --git a/docs/images/demo-screenshots/invalidCommand.png b/docs/images/demo-screenshots/invalidCommand.png new file mode 100644 index 00000000000..c32b28b5c72 Binary files /dev/null and b/docs/images/demo-screenshots/invalidCommand.png differ diff --git a/docs/images/demo-screenshots/labelledSoconnectOverview.png b/docs/images/demo-screenshots/labelledSoconnectOverview.png new file mode 100644 index 00000000000..aaa1d01d460 Binary files /dev/null and b/docs/images/demo-screenshots/labelledSoconnectOverview.png differ diff --git a/docs/images/demo-screenshots/overview.png b/docs/images/demo-screenshots/overview.png new file mode 100644 index 00000000000..e019a45e2b8 Binary files /dev/null and b/docs/images/demo-screenshots/overview.png differ diff --git a/docs/images/demo-screenshots/phoneNumberIcon.png b/docs/images/demo-screenshots/phoneNumberIcon.png new file mode 100644 index 00000000000..a6554546e1d Binary files /dev/null and b/docs/images/demo-screenshots/phoneNumberIcon.png differ diff --git a/docs/images/demo-screenshots/sortResult.png b/docs/images/demo-screenshots/sortResult.png new file mode 100644 index 00000000000..e3dd1dc3aff Binary files /dev/null and b/docs/images/demo-screenshots/sortResult.png differ diff --git a/docs/images/demo-screenshots/startTimeIcon.png b/docs/images/demo-screenshots/startTimeIcon.png new file mode 100644 index 00000000000..b55937e6ac8 Binary files /dev/null and b/docs/images/demo-screenshots/startTimeIcon.png differ diff --git a/docs/images/demo-screenshots/successMessage.png b/docs/images/demo-screenshots/successMessage.png new file mode 100644 index 00000000000..8cb4043d2c1 Binary files /dev/null and b/docs/images/demo-screenshots/successMessage.png differ diff --git a/docs/images/demo-screenshots/tagIcon.png b/docs/images/demo-screenshots/tagIcon.png new file mode 100644 index 00000000000..8faedbbe37f Binary files /dev/null and b/docs/images/demo-screenshots/tagIcon.png differ diff --git a/docs/images/demo-screenshots/telegramIcon.png b/docs/images/demo-screenshots/telegramIcon.png new file mode 100644 index 00000000000..2b2059f25aa Binary files /dev/null and b/docs/images/demo-screenshots/telegramIcon.png differ diff --git a/docs/images/demo-screenshots/websiteIcon.png b/docs/images/demo-screenshots/websiteIcon.png new file mode 100644 index 00000000000..53771fdf8ad Binary files /dev/null and b/docs/images/demo-screenshots/websiteIcon.png differ diff --git a/docs/images/gordon25.png b/docs/images/gordon25.png new file mode 100644 index 00000000000..f3a09b311eb Binary files /dev/null and b/docs/images/gordon25.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png deleted file mode 100644 index b1f70470137..00000000000 Binary files a/docs/images/helpMessage.png and /dev/null differ diff --git a/docs/images/janjanchen.png b/docs/images/janjanchen.png new file mode 100644 index 00000000000..95abf543029 Binary files /dev/null and b/docs/images/janjanchen.png differ diff --git a/docs/images/pcgiang.png b/docs/images/pcgiang.png new file mode 100644 index 00000000000..f60a4469cc8 Binary files /dev/null and b/docs/images/pcgiang.png differ diff --git a/docs/images/xiangjunn.png b/docs/images/xiangjunn.png new file mode 100644 index 00000000000..10d170b867a Binary files /dev/null and b/docs/images/xiangjunn.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..0288f34f15c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,19 +1,20 @@ --- layout: page -title: AddressBook Level-3 +title: SoConnect --- -[![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) +[![Java CI](https://github.com/AY2122S1-CS2103T-W15-3/tp/actions/workflows/gradle.yml/badge.svg)](https://github.com/AY2122S1-CS2103T-W15-3/tp/actions/workflows/gradle.yml) +[![codecov](https://codecov.io/gh/AY2122S1-CS2103T-W15-3/tp/branch/master/graph/badge.svg?token=40MOICZDNE)](https://codecov.io/gh/AY2122S1-CS2103T-W15-3/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). -* 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. +**SoConnect is a desktop application for managing your contacts and events.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). + +* If you are interested in using SoConnect, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +* If you are interested about developing SoConnect, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. **Acknowledgements** -* Libraries used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5) +* Libraries used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5), [CalendarFX](https://github.com/dlsc-software-consulting-gmbh/CalendarFX) diff --git a/docs/team/chunweii.md b/docs/team/chunweii.md new file mode 100644 index 00000000000..50a808be3d5 --- /dev/null +++ b/docs/team/chunweii.md @@ -0,0 +1,53 @@ +--- +layout: page +title: Chun Wei's Project Portfolio Page +--- + +### Project: SoConnect + +SoConnect is a **desktop app for SoC students to manage contacts of Professors and Teaching Assistants, +as well as to keep track of noteworthy events, optimized for use via a _Command Line Interface (CLI)_** while still having +the benefits of a _Graphical User Interface (GUI)_. This project is part of the [CS2103T](https://nus-cs2103-ay2122s1.github.io/website/) team project requirements for AY2021/2022 Semester 1. + +The non-exhaustive list given below are my contributions to the project. All features come with their respective unit tests. + +* **New Feature**: Changed fields to `Contact` model (Previously known as `Person`). + * What it does: allows the user to save details about the telegram handle and zoom meeting link of the saved contact, and allowing the user to omit certain details about the contact. + * Justification: Given that the users are mainly School of Computing (SoC) students in NUS, the product would be significantly more relevant if it allows students to save details like telegram handles and Zoom meeting links of their friends, professors and tutors. Telegram and Zoom are widely used applications in NUS. Also, since students may not be able to obtain every single detail about a contact they want to save, for example, many tutors do not share their addresses and phone numbers. By making certain fields optional, our product becomes more suitable for SoC students to use. + * Highlights: This enhancement affects how certain commands such as add or edit will work, since these commands do not accept optional fields, except for tags, in the original Address Book 3 implementation. + +* **New Feature**: Added the [calendar user interface](../DeveloperGuide.html#calendar-ui-feature). + * What it does: Allows the user to view their saved events in a clean visual calendar user interface. An added feature is that while the calendar is open, the calendar will automatically update itself if the user make changes to the list of events. + * Justification: This feature improves the product significantly because a user may find it difficult to plan their appointments by just looking at the list of events in the right event list panel. + * Highlights: This enhancement requires the understanding of a third-party GUI framework. In addition, the implementation of the auto-update for the calendar is non-trivial, since a poor implementation can introduce coupling and turn the code ugly. + * Credits: [CalendarFX](https://github.com/dlsc-software-consulting-gmbh/CalendarFX). Inspiration is from team projects from other teams in the past and [present](https://ay2122s1-cs2103t-f13-3.github.io/tp/). + +* **New Feature**: Sorting and displaying upcoming events + * What it does: Allows the user to sort their events based on start time and display events that are ongoing or occuring soon. + * Justification: Having an unsorted list of events will make it difficult for the user to find out what is their next activity for the day or the week. + * Highlights: This is more than just a simple sorting by date and time, since there is also a need to filter out old events. Furthermore, it can interfere with other commands that will alter the order of the list, such as `emark`. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s1.github.io/tp-dashboard/?search=w15-3&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=chunweii&tabRepo=AY2122S1-CS2103T-W15-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code~other&authorshipIsBinaryFileTypeChecked=false) + +* **Project management**: + * Set up project dashboard and project milestones on GitHub. + * Oversaw the entire project as the team leader. + * Adopted the [long-lived feature branch workflow](https://github.com/nus-cs2103-AY2122S1/forum/issues/325#issuecomment-946409090) in [v1.2b](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/90) and the feature branch workflow in v1.3 onwards, to improve efficiency of the team contributions while maintaining protection of the master branch. + +* **Documentation**: + * User Guide: + * Added documentation for the features `calendar`([\#122](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/122)), and the instructions to use the user guide. ([\#236](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/236)) + * Add instructions on how to open the jar file in terminal. [\#162](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/162) + * Developer Guide: + * Added implementation details of the [calendar user interface](../DeveloperGuide.html#calendar-ui-feature) and heavily edited the [List Events feature](../DeveloperGuide.html#list-events-feature). + * Updated the [UI design section](../DeveloperGuide.html#ui-component). + * Added use case 11 in [#217](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/217). + +* **Community**: + * Notable PRs reviewed (with non-trivial review comments): [\#147](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/147), [\#135](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/135), [\#94](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/94). See others [here](https://github.com/AY2122S1-CS2103T-W15-3/tp/pulls?page=1&q=is%3Apr+is%3Aclosed+reviewed-by%3Achunweii). + * Contributed ideas and code to improve code quality for undo/redo feature and bug fix: [\#219](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/219) + * Contributed to forum discussions (examples: [1](https://github.com/nus-cs2103-AY2122S1/forum/issues/190#issuecomment-913172698), [2](https://github.com/nus-cs2103-AY2122S1/forum/issues/267#issuecomment-925130845)) + * Reported bugs and suggestions for other teams in the class (examples: [1](https://github.com/AY2122S1-CS2103T-T12-4/tp/issues/159), [2](https://github.com/AY2122S1-CS2103T-T12-4/tp/issues/163)) + +* **Tools**: + * Integrated a third party framework (CalendarFX) to the project ([\#42](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/122)) diff --git a/docs/team/gordon25.md b/docs/team/gordon25.md new file mode 100644 index 00000000000..939c81a5ede --- /dev/null +++ b/docs/team/gordon25.md @@ -0,0 +1,70 @@ +--- +layout: page +title: Gordon Yit Hongyao's Project Portfolio Page +--- + +### Project: SoConnect + +SoConnect is a **desktop app for SoC students to manage contacts of Professors and Teaching Assistants, as well as to keep track of noteworthy events, optimized for use via a _Command Line Interface (CLI)_** while still having the benefits of a _Graphical User Interface (GUI)_. + +This project is part of the [CS2103T](https://nus-cs2103-ay2122s1.github.io/website/) team project requirements for AY2021/2022 Semester 1. + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to mark contacts and events. + * What it does: allows the user to mark one or more contacts / events at a time. The marked contacts / events will appear at the top of the contact / event list. + * Justification: This feature improves the product significantly because it saves the user time from having to scroll through the list or using the `find` command to find contacts / events that they use frequently and the app should provide a convenient way for users to refer to them. + * Highlights: This enhancement affects existing commands commands to be added in future. It required an in-depth analysis of design alternatives. + +* **New Feature**: Added ability to unmark contacts and events. + * What it does: allows the users to unmark one or more marked contacts / events at a time. The newly unmarked contacts / events will appear after all marked contacts / events in the list. + * Justification: This feature improves the product significantly because it allows the user to keep the number of marked contacts / events small and unmark contacts / events they no longer reference frequently, saving them time from having to look through a long list of marked contacts / events. + * Highlights: This enhancement affects existing commands commands to be added in future. It required an in-depth analysis of design alternatives. + +
+ +* **New Feature**: Added ability to search contacts / events based on other fields + * What it does: allows the users to search for contacts / events based on other fields other than the the contact / event name. + * Justification: This feature improves the product significantly because it gives users more freedom in how they search for contacts / events. For example, they can for search for an event based on its location or description. + * Highlights: This enhancement affects existing commands commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. + +* **New Feature**: Added ability to list only certain fields of events + * What it does: allows the users to only list certain types of fields of each event. + * Justification: This feature improves the product significantly because it allows user to focus on certain fields of events they are interested in instead of looking at the entire list. + * Highlights: This enhancement affects existing commands commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s1.github.io/tp-dashboard/?search=w15-3&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=zoom&tabAuthor=janjanchen&tabRepo=AY2122S1-CS2103T-W15-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&zA=janjanchen&zR=AY2122S1-CS2103T-W15-3%2Ftp%5Bmaster%5D&zACS=199.78947368421052&zS=2021-09-17&zFS=w15-3&zU=2021-11-06&zMG=false&zFTF=commit&zFGS=groupByRepos&zFR=false) + +* **Project management**: + * Managed issue tracker, created issues, linked them to the relevant milestone, tags and project dashboard and assigned them to the relevant teammate based on the breakdown of work discussed in the team meeting. Where possible, included task descriptions in the issue for easy reference. + +* **Enhancements to existing features**: + * Updated contact search features to allow users to find by other fields: [\#135](https://github.com/AY2122S1-CS2103T-W15-3/tp/issues/135) + * Modified storage class to store events: [\#53](https://github.com/AY2122S1-CS2103T-W15-3/tp/issues/53) + +* **Documentation**: + * User Guide: + * Added documentation for the features `eedit`, `efind` and `edelete`: [\#29](https://github.com/AY2122S1-CS2103T-W15-3/tp/issues/29) + * Updated documentation for the features `cmark`, `cunmark`, added screenshots for `edelete`, `efind`, `elist`: [\#213](https://github.com/AY2122S1-CS2103T-W15-3/tp/issues/213) + * Developer Guide: + * Added implementation details for `elist` [\#132](https://github.com/AY2122S1-CS2103T-W15-3/tp/issues/132) + * Added implementation details for `cmark` and `cunmark` [\#217](https://github.com/AY2122S1-CS2103T-W15-3/tp/issues/217) + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#33](https://github.com/AY2122S1-CS2103T-W15-3/tp/issues/33), [\#145](https://github.com/AY2122S1-CS2103T-W15-3/tp/issues/145), [\#148](https://github.com/AY2122S1-CS2103T-W15-3/tp/issues/148) + + * Reported bugs and suggestions for other teams in the class : [\#169](https://github.com/AY2122S1-CS2103T-T13-3/tp/issues/169), [\#178](https://github.com/AY2122S1-CS2103T-T13-3/tp/issues/178), [\#181](https://github.com/AY2122S1-CS2103T-T13-3/tp/issues/181) + + +© 2021 GitHub, Inc. +Terms +Privacy +Security +Status +Docs +Contact GitHub +Pricing +API +Training +Blog +About diff --git a/docs/team/janjanchen.md b/docs/team/janjanchen.md new file mode 100644 index 00000000000..6b212b66d3f --- /dev/null +++ b/docs/team/janjanchen.md @@ -0,0 +1,65 @@ +--- +layout: page +title: Janice Chen's Project Portfolio Page +--- + +### Project: SoConnect + +SoConnect is a **desktop app for SoC students to manage contacts of Professors and Teaching Assistants, +as well as to keep track of noteworthy events, optimized for use via a _Command Line Interface (CLI)_** while still having +the benefits of a _Graphical User Interface (GUI)_. + +This project is part of the [CS2103T](https://nus-cs2103-ay2122s1.github.io/website/) team project requirements for AY2021/2022 Semester 1. + +Given below are my contributions to the project. + +* **New Feature**: Added the `Event` model, together with the ability to clear, delete and finding events. + * What it does: allows the user to save events that consists of details such as event date and time, description, address. + * Justification: This feature improves the product significantly because a user can save events next to the contact list using the same platform. This feature saves the trouble of the students from using multiple platforms to separately store the contacts and events. + * Highlights: This enhancement affects certain existing commands and how new commands should be implemented, since the model manager will need to take in both `Contact` and `Event`. `Event` and `Contact` also owns different fields, which adds more complexity in how the commands should work. + +* **New Feature**: Added the help window user interface + * What it does: allows the user to view the command summary of SoConnect in a different visual interface. + * Justification: This feature improves the product significantly because a user (especially new user) might find it challenging to remember all commands provided by SoConnect. + * Highlights: This enhancement requires the understanding of a third-party GUI framework. + * Credits: Text wrapping feature of help window is adapted from [@author:James_D](https://stackoverflow.com/questions/22732013/javafx-tablecolumn-text-wrapping) + +* **New Feature**: Viewing a specific contact or event + * What it does: allows the user to view a specific contact or event with all details fully shown + * Justification: This feature improves the product significantly because some information saved in the contact or event might be too long and the information will be truncated by default. + * Highlights: This enhancement requires new implementation of how to filter out all the other contacts/ events using index only, while changing the GUI framework setting to wrap text when it is in View Mode. Corresponding changes to the GUI setting is also needed to automatically disable View mode. + + + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s1.github.io/tp-dashboard/?search=w15-3&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=zoom&tabAuthor=janjanchen&tabRepo=AY2122S1-CS2103T-W15-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&zA=janjanchen&zR=AY2122S1-CS2103T-W15-3%2Ftp%5Bmaster%5D&zACS=199.78947368421052&zS=2021-09-17&zFS=w15-3&zU=2021-11-06&zMG=false&zFTF=commit&zFGS=groupByRepos&zFR=false) + + + +* **Project management**: + * Added documentation for `How to use SoConnect User Guide`, `Overview of SoConnect` , `List of Prefixes` and `Glossary` [\#139](https://github.com/AY2122S1-CS2103T-W15-3/tp/issues/139) and [\#236](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/236) + * Added screenshots for User Guide + * Changed logging and json file name [\#98](https://github.com/AY2122S1-CS2103T-W15-3/tp/issues/98) + + + +* **Enhancements to existing features**: + * Enable partial word search for events and contacts + * What it does: allows the user to search information on SoConnect without the need to specify the keyword fully + * Justification: This feature improves the product significantly because user does not need to know the full word they want to search for within the contact/ event list. + + + +* **Documentation**: + * User Guide: + * Added documentation for the features `cadd`, `cedit`, `clist` and `cfind` [\#11](https://github.com/AY2122S1-CS2103T-W15-3/tp/issues/11) + * Added documentation for the features `cview` and `eview` [\#78](https://github.com/AY2122S1-CS2103T-W15-3/tp/issues/78) + * Added documentation for the features `help` [\#139](https://github.com/AY2122S1-CS2103T-W15-3/tp/issues/139) + * Did cosmetic tweaks to the document. + * Developer Guide: + * Added implementation details of the `edelete` feature and `Model`[\#127](https://github.com/AY2122S1-CS2103T-W15-3/tp/issues/127) + + + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#117](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/117) + diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md deleted file mode 100644 index 773a07794e2..00000000000 --- a/docs/team/johndoe.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -layout: page -title: John Doe's Project Portfolio Page ---- - -### Project: AddressBook Level 3 - -AddressBook - Level 3 is a desktop address book application used for teaching Software Engineering principles. The user interacts 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 undo/redo previous commands. - * What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. - * Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. - * Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. - * Credits: *{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}* - -* **New Feature**: Added a history command that allows the user to navigate to previous commands using up/down keys. - -* **Code contributed**: [RepoSense link]() - -* **Project management**: - * Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub - -* **Enhancements to existing features**: - * Updated the GUI color scheme (Pull requests [\#33](), [\#34]()) - * Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests [\#36](), [\#38]()) - -* **Documentation**: - * User Guide: - * Added documentation for the features `delete` and `find` [\#72]() - * Did cosmetic tweaks to existing documentation of features `clear`, `exit`: [\#74]() - * Developer Guide: - * Added implementation details of the `delete` feature. - -* **Community**: - * PRs reviewed (with non-trivial review comments): [\#12](), [\#32](), [\#19](), [\#42]() - * Contributed to forum discussions (examples: [1](), [2](), [3](), [4]()) - * Reported bugs and suggestions for other teams in the class (examples: [1](), [2](), [3]()) - * Some parts of the history feature I added was adopted by several other class mates ([1](), [2]()) - -* **Tools**: - * Integrated a third party library (Natty) to the project ([\#42]()) - * Integrated a new Github plugin (CircleCI) to the team repo - -* _{you can add/remove categories in the list above}_ diff --git a/docs/team/pcgiang.md b/docs/team/pcgiang.md new file mode 100644 index 00000000000..39a414b76f0 --- /dev/null +++ b/docs/team/pcgiang.md @@ -0,0 +1,51 @@ +--- +layout: page +title: Pham Chau Giang's Project Portfolio Page +--- + +### Project: SoConnect + +SoConnect is a **desktop app for SoC students to manage contacts of Professors and Teaching Assistants, +as well as to keep track of noteworthy events, optimized for use via a _Command Line Interface (CLI)_** while still having +the benefits of a _Graphical User Interface (GUI)_. + +This project is part of the [CS2103T](https://nus-cs2103-ay2122s1.github.io/website/) team project requirements for AY2021/2022 Semester 1. + +Given below are my contributions to the project. + +* **New Feature**: Worked together with Chun Wei to add the ability to undo/redo previous commands. + * What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. + * Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. + * Highlights: This enhancement affects existing commands and commands to be added in future. The implementation was challenging as it requires an in-depth analysis of how commands interact and are displayed in order to come up with the most suited implementation. + + +* **New Feature**: Enabled fields to be copied. + * What it does: allows user to copy important information or open any links simply by clicking on a field label. + * Justification: This feature improves the product significantly because a user can conveniently get important information and use it elsewhere without having to manually copy the fields. + * Highlights: This enhancement requires understanding of GUI framework. + + +* **New Feature**: Added a feature that allow colourful tags for contacts and events. + * What it does: displays different tag names with different colours such that the user can easily identify the different tags names. + * Justification: This feature improves the UI of the product significantly and aids the user in differentiating the different tags. + * Highlights: This enhancement requires understanding of GUI framework. + + +* **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=pcgiang&tabRepo=AY2122S1-CS2103T-W15-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code~other&authorshipIsBinaryFileTypeChecked=false) + + +* **Project management**: + * Set up project repo + + +* **Documentation**: + * User Guide: + * Added documentation for the feature `undo` and `redo` [\#133](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/133) + * Checked and fixed bugs in Managing Events Section [\#236](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/236) + * Developer Guide: + * Added implementation details of the `undo/redo` feature [\#217](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/217) + * Added manual test cases [\#217](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/217) + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#132](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/132), [\#165](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/165) + diff --git a/docs/team/xiangjunn.md b/docs/team/xiangjunn.md new file mode 100644 index 00000000000..5e7c0b853e6 --- /dev/null +++ b/docs/team/xiangjunn.md @@ -0,0 +1,58 @@ +--- +layout: page +title: Xiang Jun's Project Portfolio Page +--- + +### Project: SoConnect + +SoConnect is a **desktop app for SoC students to manage contacts of Professors and Teaching Assistants, +as well as to keep track of noteworthy events, optimized for use via a _Command Line Interface (CLI)_** while still +having the benefits of a _Graphical User Interface (GUI)_. This project is part of the +[CS2103T](https://nus-cs2103-ay2122s1.github.io/website/) team project requirements for AY2021/2022 Semester 1. + +Given below are my notable contributions to the project. + +* **New Feature**: Added the ability to link an event to one or more contacts. + * What it does: allows the user to choose an event and link related contacts to it. + * Justification: An event is likely to have people that are hosting it. Therefore, the ability to view the contacts + of an event allows the user to easily contact the group of people that are hosting the event. This feature + connects contact and event together, making the product more cohesive. + * Highlights: This enhancement affects existing commands such as EDeleteCommand, CEditCommand and EClearCommand. + What these commands have in common is that they change the contacts/events in the contact/event list after + execution. This is problematic because these can result in change in the links as well. Therefore, the + implementation was challenging as it required changes to existing commands to take into account the links as well. + +* **New Feature**: Added the ability to delete a range of contacts. + * What it does: allows the user to delete either one contact or an inclusive range of consecutive contacts. + * Justification: It is troublesome for users to delete the contact one by one. Allowing them to delete a range of contacts + is more user-friendly. + * Highlights: This feature works well with the existing feature which can filter the contact list because user can delete the contacts from that filtered list with ease. + +* **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=xiangjunn&tabRepo=AY2122S1-CS2103T-W15-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code~other&authorshipIsBinaryFileTypeChecked=false) + +* **Project management**: + * Add branch protection rules to enforce certain requirements. + * Ensures that a branch has to pass CI before it can be merged into the `master` branch. + * Requires at least one reviewer's approval before pull request reviews can be merged. + * Ensures that a branch is up to date with `master` branch before it can be merged. + * Disallow push commits to `master` branch directly. + * Makes sure that team members are following the proper way of reviewing and merging pull requests. + * Facilitate closing of milestones and tagging the relevant commits with the correct version. + +* **Enhancements to existing features**: + * Improvements to the GUI (Pull requests [\#45](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/45), [\#149](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/149)) + * Enhance CDeleteCommand to allow for deleting over a range of values. Creates a range class for this purpose. (Pull requests [\#87](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/87)) + +* **Documentation**: + * User Guide: + * Added documentation for the features `cdelete`, `cclear`, `eadd` and `elist` (Pull request [\#19](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/19)) + * Added documentation for the feature `elink` (Pull request [\#131](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/131)) + * Developer Guide: + * Added documentation for product scope and user stories (Pull request [\#33](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/33)) + * Added implementation details of the `elink` feature, which include sequence diagram, activity diagram and design considerations. + * Added instruction for manual testing for `elink` feature. + * Added use cases for editing events and linking an event to one or more contacts + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#51](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/51), [\#142](https://github.com/AY2122S1-CS2103T-W15-3/tp/pull/142) + * Reported bugs and suggestions for other teams in the class (examples: [1](https://github.com/AY2122S1-CS2103-T16-2/tp/issues/160), [2](https://github.com/AY2122S1-CS2103-T16-2/tp/issues/145), [3](https://github.com/AY2122S1-CS2103-T16-2/tp/issues/159)) diff --git a/docs/tutorials/RemovingFields.md b/docs/tutorials/RemovingFields.md index f29169bc924..8fe957b1eec 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.address.model.common.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/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 4133aaa0151..a6ea2c089c2 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -48,7 +48,7 @@ public class MainApp extends Application { @Override public void init() throws Exception { - logger.info("=============================[ Initializing AddressBook ]==========================="); + logger.info("=============================[ Initializing SoConnect ]==========================="); super.init(); AppParameters appParameters = AppParameters.parse(getParameters()); @@ -173,9 +173,10 @@ public void start(Stage primaryStage) { @Override public void stop() { - logger.info("============================ [ Stopping Address Book ] ============================="); + logger.info("============================ [ Stopping SoConnect ] ============================="); try { storage.saveUserPrefs(model.getUserPrefs()); + storage.saveAddressBook(model.getAddressBook()); } catch (IOException e) { logger.severe("Failed to save preferences " + StringUtil.getDetails(e)); } diff --git a/src/main/java/seedu/address/commons/core/LogsCenter.java b/src/main/java/seedu/address/commons/core/LogsCenter.java index 431e7185e76..1f9e613a141 100644 --- a/src/main/java/seedu/address/commons/core/LogsCenter.java +++ b/src/main/java/seedu/address/commons/core/LogsCenter.java @@ -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 = "soconnect.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/address/commons/core/Messages.java b/src/main/java/seedu/address/commons/core/Messages.java index 1deb3a1e469..f66605533e7 100644 --- a/src/main/java/seedu/address/commons/core/Messages.java +++ b/src/main/java/seedu/address/commons/core/Messages.java @@ -7,7 +7,18 @@ 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!"; - + public static final String MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX = "The contact index provided is invalid"; + public static final String MESSAGE_INVALID_EVENT_DISPLAYED_INDEX = "The event index provided is invalid"; + public static final String MESSAGE_START_MORE_THAN_END_INDEX = "The start index should not be more than end index"; + public static final String MESSAGE_CONTACTS_LISTED_OVERVIEW = "%1$d contacts listed!"; + public static final String MESSAGE_EVENTS_LISTED_OVERVIEW = "%1$d events listed!"; + public static final String MESSAGE_CONTACT_LINK_OPENED = "Contact %1$s link opened in browser"; + public static final String MESSAGE_EVENT_LINK_OPENED = "Event %1$s link opened in browser"; + public static final String MESSAGE_CONTACT_LINK_NOT_FOUND = "Contact %1$s link not found!"; + public static final String MESSAGE_EVENT_LINK_NOT_FOUND = "Event %1$s link not found!"; + public static final String MESSAGE_DUPLICATE_INDEXES_PROVIDED = "There are duplicates in the indexes provided!"; + public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; + public static final String MESSAGE_INVALID_RANGE = "Range given is invalid"; + public static final String MESSAGE_CONTACT_FIELD_COPIED = "Contact %1$s copied to clipboard!"; + public static final String MESSAGE_EVENT_FIELD_COPIED = "Event %1$s copied to clipboard!"; } diff --git a/src/main/java/seedu/address/commons/core/index/Index.java b/src/main/java/seedu/address/commons/core/index/Index.java index 19536439c09..e1c8859d9c8 100644 --- a/src/main/java/seedu/address/commons/core/index/Index.java +++ b/src/main/java/seedu/address/commons/core/index/Index.java @@ -45,10 +45,25 @@ public static Index fromOneBased(int oneBasedIndex) { return new Index(oneBasedIndex - 1); } + /** + * Compares if this index is more than another index. + * + * @param index The index to compare to. + * @return true if this index is more than the index compared; false otherwise. + */ + public boolean isMoreThan(Index index) { + return this.zeroBasedIndex > index.zeroBasedIndex; + } + @Override public boolean equals(Object other) { return other == this // short circuit if same object || (other instanceof Index // instanceof handles nulls && zeroBasedIndex == ((Index) other).zeroBasedIndex); // state check } + + @Override + public int hashCode() { + return zeroBasedIndex; + } } diff --git a/src/main/java/seedu/address/commons/core/range/Range.java b/src/main/java/seedu/address/commons/core/range/Range.java new file mode 100644 index 00000000000..60b38f41965 --- /dev/null +++ b/src/main/java/seedu/address/commons/core/range/Range.java @@ -0,0 +1,49 @@ +package seedu.address.commons.core.range; + +import seedu.address.commons.core.index.Index; + +public class Range { + private Index start; + private Index end; + + /** + * Constructs an inclusive range from start index to end index. + * + * @param start An index representing the start. + * @param end An index representing the end. + */ + public Range(Index start, Index end) { + assert start.getZeroBased() >= 0; + assert end.getZeroBased() >= 0; + if (start.isMoreThan(end)) { + throw new IndexOutOfBoundsException(); + } + this.start = start; + this.end = end; + } + + public Index getStart() { + return start; + } + + public Index getEnd() { + return end; + } + + /** + * Creates a Range object with start and end indexes representing the same index. + * + * @param index The index to be converted. + */ + public static Range convertFromIndex(Index index) { + return new Range(index, index); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Range // instanceof handles nulls + && start.equals(((Range) other).start) + && (end.equals(((Range) other).end))); // state check + } +} diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/address/commons/util/StringUtil.java index 61cc8c9a1cb..57e4a341490 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/seedu/address/commons/util/StringUtil.java @@ -5,7 +5,7 @@ import java.io.PrintWriter; import java.io.StringWriter; -import java.util.Arrays; +import java.util.Locale; /** * Helper functions for handling strings. @@ -14,11 +14,12 @@ public class StringUtil { /** * Returns true if the {@code sentence} contains the {@code word}. - * Ignores case, but a full word match is required. + * Ignores case, and a full word match is not required. *
examples:
      *       containsWordIgnoreCase("ABc def", "abc") == true
      *       containsWordIgnoreCase("ABc def", "DEF") == true
-     *       containsWordIgnoreCase("ABc def", "AB") == false //not a full word match
+     *       containsWordIgnoreCase("ABc def", "AB") == true
+     *       containsWordIgnoreCase("ABc def", "Gh") == false
      *       
* @param sentence cannot be null * @param word cannot be null, cannot be empty, must be a single word @@ -27,15 +28,13 @@ public static boolean containsWordIgnoreCase(String sentence, String word) { requireNonNull(sentence); requireNonNull(word); - String preppedWord = word.trim(); + String preppedWord = word.trim().toLowerCase(Locale.ROOT); checkArgument(!preppedWord.isEmpty(), "Word parameter cannot be empty"); checkArgument(preppedWord.split("\\s+").length == 1, "Word parameter should be a single word"); - String preppedSentence = sentence; - String[] wordsInPreppedSentence = preppedSentence.split("\\s+"); + String preppedSentence = sentence.toLowerCase(Locale.ROOT); - return Arrays.stream(wordsInPreppedSentence) - .anyMatch(preppedWord::equalsIgnoreCase); + return preppedSentence.contains(preppedWord); } /** @@ -65,4 +64,18 @@ public static boolean isNonZeroUnsignedInteger(String s) { return false; } } + + /** + * Returns true if {@code s} represents a valid range; false otherwise. + */ + public static boolean isValidRange(String s) { + requireNonNull(s); + if (s.matches("\\d+-\\d+")) { + String[] indexes = s.split("-"); + int start = Integer.parseInt(indexes[0]); + int end = Integer.parseInt(indexes[1]); + return start > 0 && start <= end; + } + return false; + } } diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 92cd8fa605a..1ee6cbf7452 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -8,7 +8,10 @@ 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.address.model.contact.Contact; +import seedu.address.model.contact.ContactDisplaySetting; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventDisplaySetting; /** * API of the Logic component @@ -31,7 +34,10 @@ public interface Logic { ReadOnlyAddressBook getAddressBook(); /** Returns an unmodifiable view of the filtered list of persons */ - ObservableList getFilteredPersonList(); + ObservableList getFilteredContactList(); + + /** Returns an unmodifiable view of the filtered list of events */ + ObservableList getFilteredEventList(); /** * Returns the user prefs' address book file path. @@ -43,8 +49,30 @@ public interface Logic { */ GuiSettings getGuiSettings(); + /** + * Returns the display settings of the events. + */ + EventDisplaySetting getEventDisplaySetting(); + + /** + * Returns the display settings of the contacts. + */ + ContactDisplaySetting getContactDisplaySetting(); + /** * Set the user prefs' GUI settings. */ void setGuiSettings(GuiSettings guiSettings); + + /** Changes the filter to the model so that only contacts linked to {@code event} will be shown. */ + void filterContactsWithLinksToEvent(Event event); + + /** Changes the filter to the model so that only events linked to {@code contact} will be shown. */ + void filterEventsWithLinkToContact(Contact contact); + + /** Changes the filter of the contacts to show all contacts. */ + void resetFilterOfContacts(); + + /** Changes the filter of the events to show all events. */ + void resetFilterOfEvents(); } diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 9d9c6d15bdc..a3d7f211f43 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -9,12 +9,16 @@ import seedu.address.commons.core.LogsCenter; import seedu.address.logic.commands.Command; import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.Undoable; 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.model.contact.Contact; +import seedu.address.model.contact.ContactDisplaySetting; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventDisplaySetting; import seedu.address.storage.Storage; /** @@ -44,6 +48,9 @@ public CommandResult execute(String commandText) throws CommandException, ParseE CommandResult commandResult; Command command = addressBookParser.parseCommand(commandText); commandResult = command.execute(model); + if (command instanceof Undoable) { + model.commitHistory(); + } try { storage.saveAddressBook(model.getAddressBook()); @@ -60,8 +67,13 @@ public ReadOnlyAddressBook getAddressBook() { } @Override - public ObservableList getFilteredPersonList() { - return model.getFilteredPersonList(); + public ObservableList getFilteredContactList() { + return model.getFilteredContactList(); + } + + @Override + public ObservableList getFilteredEventList() { + return model.getFilteredEventList(); } @Override @@ -74,8 +86,38 @@ public GuiSettings getGuiSettings() { return model.getGuiSettings(); } + @Override + public EventDisplaySetting getEventDisplaySetting() { + return model.getEventDisplaySetting(); + } + + @Override + public ContactDisplaySetting getContactDisplaySetting() { + return model.getContactDisplaySetting(); + } + @Override public void setGuiSettings(GuiSettings guiSettings) { model.setGuiSettings(guiSettings); } + + @Override + public void filterContactsWithLinksToEvent(Event event) { + model.updateFilteredContactList(contact -> contact.getLinkedEvents().contains(event.getUuid())); + } + + @Override + public void filterEventsWithLinkToContact(Contact contact) { + model.updateFilteredEventList(event -> event.getLinkedContacts().contains(contact.getUuid())); + } + + @Override + public void resetFilterOfContacts() { + model.rerenderContactCards(true); + } + + @Override + public void resetFilterOfEvents() { + model.rerenderEventCards(true); + } } 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/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java deleted file mode 100644 index 9c86b1fa6e4..00000000000 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ /dev/null @@ -1,23 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import seedu.address.model.AddressBook; -import seedu.address.model.Model; - -/** - * Clears the address book. - */ -public class ClearCommand extends Command { - - public static final String COMMAND_WORD = "clear"; - public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; - - - @Override - public CommandResult execute(Model model) { - requireNonNull(model); - model.setAddressBook(new AddressBook()); - return new CommandResult(MESSAGE_SUCCESS); - } -} diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java index 92f900b7916..548d3ea4537 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/seedu/address/logic/commands/CommandResult.java @@ -2,8 +2,12 @@ import static java.util.Objects.requireNonNull; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; +import seedu.address.model.event.EventChanger; + /** * Represents the result of a command execution. */ @@ -11,19 +15,36 @@ public class CommandResult { private final String feedbackToUser; - /** Help information should be shown to the user. */ + /** + * Help information should be shown to the user. + */ private final boolean showHelp; - /** The application should exit. */ + /** + * The application should exit. + */ private final boolean exit; + /** + * The calendar should be shown to the user. + */ + private final boolean showCalendar; + + /** + * The list of all events to be changed. By default an empty list + */ + private final List eventChangerList; + /** * Constructs a {@code CommandResult} with the specified fields. */ - public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { + public CommandResult(String feedbackToUser, boolean showHelp, boolean exit, boolean showCalendar) { + assert numberOfTrueAtMost1(showHelp, exit, showCalendar) : "Invalid combination of boolean values."; this.feedbackToUser = requireNonNull(feedbackToUser); this.showHelp = showHelp; this.exit = exit; + this.showCalendar = showCalendar; + this.eventChangerList = new ArrayList<>(); } /** @@ -31,7 +52,19 @@ 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, false, false, false); + } + + /** + * Constructs a {@code CommandResult} with the specified {@code feedbackToUser}, + * and other fields set to their default value. + */ + public CommandResult(String feedbackToUser, List eventChangerList) { + this.feedbackToUser = requireNonNull(feedbackToUser); + this.showHelp = false; + this.exit = false; + this.showCalendar = false; + this.eventChangerList = requireNonNull(eventChangerList); } public String getFeedbackToUser() { @@ -46,6 +79,14 @@ public boolean isExit() { return exit; } + public boolean isShowCalendar() { + return showCalendar; + } + + public List getEventChangerList() { + return eventChangerList; + } + @Override public boolean equals(Object other) { if (other == this) { @@ -59,8 +100,10 @@ public boolean equals(Object other) { CommandResult otherCommandResult = (CommandResult) other; return feedbackToUser.equals(otherCommandResult.feedbackToUser) - && showHelp == otherCommandResult.showHelp - && exit == otherCommandResult.exit; + && showHelp == otherCommandResult.showHelp + && exit == otherCommandResult.exit + && showCalendar == otherCommandResult.showCalendar + && eventChangerList.equals(otherCommandResult.eventChangerList); } @Override @@ -68,4 +111,25 @@ public int hashCode() { return Objects.hash(feedbackToUser, showHelp, exit); } + /** + * For assertion checking. + */ + private boolean numberOfTrueAtMost1(boolean first, boolean second, boolean third) { + // Calculated using k-map to obtain disjunctive normal form. + return (!first && !second) || (!first && !third) || (!second && !third); + } + + /** + * For testing purposes. + */ + @Override + public String toString() { + return "CommandResult{" + + "feedbackToUser='" + feedbackToUser + '\'' + + ", showHelp=" + showHelp + + ", exit=" + exit + + ", showCalendar=" + showCalendar + + ", eventChangerList=" + eventChangerList + + '}'; + } } 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/commands/Undoable.java b/src/main/java/seedu/address/logic/commands/Undoable.java new file mode 100644 index 00000000000..4291f3e1325 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/Undoable.java @@ -0,0 +1,5 @@ +package seedu.address.logic.commands; + +/** Represents a command that can be undone. */ +public interface Undoable { +} diff --git a/src/main/java/seedu/address/logic/commands/contact/CAddCommand.java b/src/main/java/seedu/address/logic/commands/contact/CAddCommand.java new file mode 100644 index 00000000000..7285c04edaa --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/contact/CAddCommand.java @@ -0,0 +1,76 @@ +package seedu.address.logic.commands.contact; + +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.logic.parser.CliSyntax.PREFIX_TELEGRAM; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ZOOM; + +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.Undoable; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.contact.Contact; + +/** + * Adds a contact to the address book. + */ +public class CAddCommand extends Command implements Undoable { + + public static final String COMMAND_WORD = "cadd"; + public static final String PARAMETERS = PREFIX_NAME + "NAME " + + PREFIX_EMAIL + "EMAIL " + + "[" + PREFIX_PHONE + "PHONE_NUMBER] " + + "[" + PREFIX_ADDRESS + "ADDRESS] " + + "[" + PREFIX_TELEGRAM + "TELEGRAM_HANDLE] " + + "[" + PREFIX_ZOOM + "ZOOM] " + + "[" + PREFIX_TAG + "TAG]...\n"; + public static final String SYNTAX = COMMAND_WORD + " " + PARAMETERS; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a contact to SoConnect. \n" + + "Parameters: " + + PARAMETERS + + "Example: " + COMMAND_WORD + " " + + PREFIX_NAME + "John Doe " + + PREFIX_EMAIL + "e7654321@u.nus.edu " + + PREFIX_PHONE + "98765432 " + + PREFIX_ADDRESS + "COM1-0201 " + + PREFIX_TELEGRAM + "jonnyjonny " + + PREFIX_ZOOM + "https://nus-sg.zoom.us/j/0123456789?pwd=ABCDEFGHIJKLMNOPDJFHISDFSDH " + + PREFIX_TAG + "TA " + + PREFIX_TAG + "Senior"; + + public static final String MESSAGE_SUCCESS = "New contact added: %1$s"; + public static final String MESSAGE_DUPLICATE_CONTACT = "This contact already exists in the address book"; + + private final Contact toAdd; + + /** + * Creates an AddCommand to add the specified {@code Contact} + */ + public CAddCommand(Contact contact) { + requireNonNull(contact); + toAdd = contact; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + if (model.hasContact(toAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_CONTACT); + } + model.addContact(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 CAddCommand // instanceof handles nulls + && toAdd.equals(((CAddCommand) other).toAdd)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/contact/CClearCommand.java b/src/main/java/seedu/address/logic/commands/contact/CClearCommand.java new file mode 100644 index 00000000000..48d564293fe --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/contact/CClearCommand.java @@ -0,0 +1,30 @@ +package seedu.address.logic.commands.contact; + +import static java.util.Objects.requireNonNull; + +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.Undoable; +import seedu.address.model.Model; + +/** + * Clears all entries of contacts from SoConnect. + */ +public class CClearCommand extends Command implements Undoable { + + public static final String COMMAND_WORD = "cclear"; + + public static final String SYNTAX = COMMAND_WORD; + + public static final String MESSAGE_SUCCESS = "All contacts have been cleared!"; + + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.resetContacts(); + // re-render UI to remove all links + model.rerenderEventCards(true); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/contact/CDeleteCommand.java b/src/main/java/seedu/address/logic/commands/contact/CDeleteCommand.java new file mode 100644 index 00000000000..23d3340ee9a --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/contact/CDeleteCommand.java @@ -0,0 +1,78 @@ +package seedu.address.logic.commands.contact; + +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.commons.core.range.Range; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.Undoable; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.contact.Contact; + +/** + * Deletes a contact identified using it's displayed index from the address book. + */ +public class CDeleteCommand extends Command implements Undoable { + + public static final String COMMAND_WORD = "cdelete"; + public static final String PARAMETERS = "INDEX1[-INDEX2]"; + public static final String SYNTAX = COMMAND_WORD + " " + PARAMETERS; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the contact identified by the index number used in the displayed contact list.\n" + + "Parameters: " + PARAMETERS + " (both indexes must be a positive integer)\n" + + "Note: index must be a positive integer and INDEX1 must be smaller than or equal to INDEX2" + + " if the optional INDEX2 is included)\n" + + "Example 1: " + COMMAND_WORD + " 1\n" + + "Example 2: " + COMMAND_WORD + " 2-5"; + + public static final String MESSAGE_DELETE_CONTACT_SUCCESS = "Deleted Contact: %1$s"; + + private final Range targetRange; + + public CDeleteCommand(Range targetRange) { + this.targetRange = targetRange; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredContactList(); + + Index startIndex = targetRange.getStart(); + Index endIndex = targetRange.getEnd(); + int end = endIndex.getZeroBased(); + if (end >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX); + } + + String commandResult = ""; + int indexToDelete = startIndex.getZeroBased(); + // delete the same index starting from the start index, since after deleting a contact, + // the remaining contacts with larger indexes will have their index decreased by 1. Hence, + // the next bigger index will have the same index as the deleted contact. + for (int i = indexToDelete; i <= end; i++) { + Contact contactToDelete = lastShownList.get(indexToDelete); + model.deleteContact(contactToDelete); + commandResult += String.format(MESSAGE_DELETE_CONTACT_SUCCESS, contactToDelete); + if (i != end) { + commandResult += "\n"; + } + } + // rerender UI to update the links for events with links to deleted contact + model.rerenderEventCards(true); + return new CommandResult(commandResult); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof CDeleteCommand // instanceof handles nulls + && targetRange.equals(((CDeleteCommand) other).targetRange)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/contact/CEditCommand.java b/src/main/java/seedu/address/logic/commands/contact/CEditCommand.java new file mode 100644 index 00000000000..5b34fadf03e --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/contact/CEditCommand.java @@ -0,0 +1,338 @@ +package seedu.address.logic.commands.contact; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DELETE_TAG; +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.logic.parser.CliSyntax.PREFIX_TELEGRAM; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ZOOM; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_CONTACTS; + +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.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.Undoable; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.common.Address; +import seedu.address.model.common.Name; +import seedu.address.model.common.ZoomLink; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Phone; +import seedu.address.model.contact.TelegramHandle; +import seedu.address.model.tag.Tag; + +/** + * Edits the details of an existing contact in the address book. + */ +public class CEditCommand extends Command implements Undoable { + + public static final String COMMAND_WORD = "cedit"; + public static final String PARAMETERS = "INDEX (must be a positive integer) " + + "[" + PREFIX_NAME + "NAME] " + + "[" + PREFIX_PHONE + "PHONE] " + + "[" + PREFIX_EMAIL + "EMAIL] " + + "[" + PREFIX_ADDRESS + "ADDRESS] " + + "[" + PREFIX_TELEGRAM + "TELEGRAM] " + + "[" + PREFIX_ZOOM + "ZOOM] " + + "[" + PREFIX_TAG + "TAG]... " + + "[" + PREFIX_DELETE_TAG + "DELETE_TAG]...\n"; + public static final String SYNTAX = COMMAND_WORD + " " + PARAMETERS; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the contact identified " + + "by the index number used in the displayed contact list. " + + "Existing values will be overwritten by the input values.\n" + + "Parameters: " + PARAMETERS + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_PHONE + "91234567 " + + PREFIX_EMAIL + "johndoe@example.com"; + + public static final String MESSAGE_EDIT_CONTACT_SUCCESS = "Edited Contact: %1$s"; + public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; + public static final String MESSAGE_DUPLICATE_CONTACT = "This contact already exists in the address book."; + public static final String MESSAGE_TAG_TO_ADD_ALREADY_IN_ORIGINAL = "Contact already has %s tag.\n"; + public static final String MESSAGE_TAG_TO_DELETE_NOT_IN_ORIGINAL = + "Contact does not contain %s tag to delete.\n"; + + private final Index index; + private final EditContactDescriptor editContactDescriptor; + // to be displayed to user if user tries to delete a tag that does not exist + // or add a tag that already exists + private String infoMessage = ""; + + /** + * @param index of the contact in the filtered contact list to edit + * @param editContactDescriptor details to edit the contact with + */ + public CEditCommand(Index index, EditContactDescriptor editContactDescriptor) { + requireNonNull(index); + requireNonNull(editContactDescriptor); + + this.index = index; + this.editContactDescriptor = new EditContactDescriptor(editContactDescriptor); + } + + /** + * Creates and returns a {@code Contact} with the details of {@code contactToEdit} + * edited with {@code editContactDescriptor}. + */ + private Contact createEditedContact(Contact contactToEdit, EditContactDescriptor editContactDescriptor) { + assert contactToEdit != null; + + Name updatedName = editContactDescriptor.getName().orElse(contactToEdit.getName()); + Phone updatedPhone = editContactDescriptor.getPhone().orElse(contactToEdit.getPhone()); + Email updatedEmail = editContactDescriptor.getEmail().orElse(contactToEdit.getEmail()); + Address updatedAddress = editContactDescriptor.getAddress().orElse(contactToEdit.getAddress()); + TelegramHandle updatedTelegram = editContactDescriptor.getTelegramHandle() + .orElse(contactToEdit.getTelegramHandle()); + ZoomLink updatedZoomLink = editContactDescriptor.getZoomLink().orElse(contactToEdit.getZoomLink()); + Set updatedNewTags = editContactDescriptor.getTags().orElse(new HashSet<>()); + Set updatedDeletedTags = editContactDescriptor.getTagsToDelete().orElse(new HashSet<>()); + Set updatedTags = editContactDescriptor.isShouldDeleteAllTags() + ? updatedNewTags : addAndRemoveTags(updatedNewTags, updatedDeletedTags, contactToEdit.getTags()); + Contact updatedContact = new Contact(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedZoomLink, + updatedTelegram, updatedTags, contactToEdit.getUuid(), contactToEdit.getLinkedEvents(), + contactToEdit.getIsMarked()); + // update the edited contact to the hashmap that stores references to all contacts + Contact.addToMap(updatedContact); + return updatedContact; + } + + /** + * Creates and returns a {@code Set} with tags from {@code original} and {@code toAdd}, but + * tags in {@code toRemove} will be excluded. + */ + private Set addAndRemoveTags(Set toAdd, Set toRemove, Set original) { + Set updatedTags = new HashSet<>(original); + String result = "\nNote:\n"; + for (Tag tag : toAdd) { + if (!updatedTags.add(tag)) { // if the tag to delete is not in the original tags + result += String.format(MESSAGE_TAG_TO_ADD_ALREADY_IN_ORIGINAL, tag); + } + } + for (Tag tag : toRemove) { + if (!updatedTags.remove(tag)) { // if the tag to delete is not in the original tags + result += String.format(MESSAGE_TAG_TO_DELETE_NOT_IN_ORIGINAL, tag); + } + } + infoMessage = !result.equals("\nNote:\n") ? result : ""; + toRemove.forEach(updatedTags::remove); + updatedTags.addAll(toAdd); + return updatedTags; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredContactList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX); + } + + Contact contactToEdit = lastShownList.get(index.getZeroBased()); + Contact editedContact = createEditedContact(contactToEdit, editContactDescriptor); + + if (!contactToEdit.isSameContact(editedContact) && model.hasContact(editedContact)) { + throw new CommandException(MESSAGE_DUPLICATE_CONTACT); + } + + model.setContact(contactToEdit, editedContact); + model.updateFilteredContactList(PREDICATE_SHOW_ALL_CONTACTS); + // rerender UI to show latest change for events with links to edited contact + model.rerenderEventCards(true); + String result = String.format(MESSAGE_EDIT_CONTACT_SUCCESS, editedContact) + infoMessage; + return new CommandResult(result); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof CEditCommand)) { + return false; + } + + // state check + CEditCommand e = (CEditCommand) other; + return index.equals(e.index) + && editContactDescriptor.equals(e.editContactDescriptor); + } + + /** + * Stores the details to edit the contact with. Each non-empty field value will replace the + * corresponding field value of the contact. + */ + public static class EditContactDescriptor { + private Name name; + private Phone phone; + private Email email; + private Address address; + private TelegramHandle telegramHandle; + private ZoomLink zoomLink; + private Set tags; + private Set tagsToDelete; + private boolean shouldDeleteAllTags = false; + + public EditContactDescriptor() { + } + + /** + * Copy constructor. + * A defensive copy of {@code tags} is used internally. + */ + public EditContactDescriptor(EditContactDescriptor toCopy) { + setName(toCopy.name); + setPhone(toCopy.phone); + setEmail(toCopy.email); + setAddress(toCopy.address); + setTelegramHandle(toCopy.telegramHandle); + setZoomLink(toCopy.zoomLink); + setTags(toCopy.tags); + setTagsToDelete(toCopy.tagsToDelete); + setShouldDeleteAllTags(toCopy.shouldDeleteAllTags); + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(name, phone, email, address, telegramHandle, zoomLink, + tags, tagsToDelete) || shouldDeleteAllTags; + } + + public Optional getName() { + return Optional.ofNullable(name); + } + + public void setName(Name name) { + this.name = name; + } + + public Optional getPhone() { + return Optional.ofNullable(phone); + } + + public void setPhone(Phone phone) { + this.phone = phone; + } + + public Optional getEmail() { + return Optional.ofNullable(email); + } + + public void setEmail(Email email) { + this.email = email; + } + + public Optional
getAddress() { + return Optional.ofNullable(address); + } + + public void setAddress(Address address) { + this.address = address; + } + + public Optional getTelegramHandle() { + return Optional.ofNullable(telegramHandle); + } + + public void setTelegramHandle(TelegramHandle telegramHandle) { + this.telegramHandle = telegramHandle; + } + + public Optional getZoomLink() { + return Optional.ofNullable(zoomLink); + } + + public void setZoomLink(ZoomLink zoomLink) { + this.zoomLink = zoomLink; + } + + /** + * Returns an unmodifiable tag set to be added, 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(); + } + + /** + * 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 && !tags.isEmpty()) ? new HashSet<>(tags) : null; + } + + /** + * Returns an unmodifiable tag set to be removed, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code tagsToDelete} is null. + */ + public Optional> getTagsToDelete() { + return tagsToDelete != null ? Optional.of(Collections.unmodifiableSet((tagsToDelete))) : Optional.empty(); + } + + /** + * Sets {@code tagsToDelete} to this object's {@code tagsToDelete}. + * A defensive copy of {@code tagsToDelete} is used internally. + */ + public void setTagsToDelete(Set tagsToDelete) { + this.tagsToDelete = (tagsToDelete != null && !tagsToDelete.isEmpty()) ? new HashSet<>(tagsToDelete) : null; + } + + public boolean isShouldDeleteAllTags() { + return shouldDeleteAllTags; + } + + /** + * Sets the boolean condition of whether all tags should be cleared first. + */ + public void setShouldDeleteAllTags(boolean shouldDeleteAllTags) { + this.shouldDeleteAllTags = shouldDeleteAllTags; + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditContactDescriptor)) { + return false; + } + + // state check + EditContactDescriptor e = (EditContactDescriptor) other; + + return getName().equals(e.getName()) + && getPhone().equals(e.getPhone()) + && getEmail().equals(e.getEmail()) + && getAddress().equals(e.getAddress()) + && getTelegramHandle().equals(e.getTelegramHandle()) + && getZoomLink().equals(e.getZoomLink()) + && getTags().equals(e.getTags()) + && getTagsToDelete().equals(e.getTagsToDelete()); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/contact/CFindCommand.java b/src/main/java/seedu/address/logic/commands/contact/CFindCommand.java new file mode 100644 index 00000000000..e37e95811bb --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/contact/CFindCommand.java @@ -0,0 +1,66 @@ +package seedu.address.logic.commands.contact; + +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_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TELEGRAM; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ZOOM; + +import seedu.address.commons.core.Messages; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.Undoable; +import seedu.address.model.Model; +import seedu.address.model.contact.ContactContainsKeywordsPredicate; +import seedu.address.model.contact.ContactDisplaySetting; + +/** + * Finds and lists all persons in address book whose name contains any of the argument keywords. + * Keyword matching is case insensitive. + */ +public class CFindCommand extends Command implements Undoable { + + public static final String COMMAND_WORD = "cfind"; + public static final String PARAMETERS = "[KEYWORD]… " + + "[" + PREFIX_PHONE + "KEYWORD…] " + + "[" + PREFIX_EMAIL + "KEYWORD…] " + + "[" + PREFIX_ADDRESS + "KEYWORD…] " + + "[" + PREFIX_TELEGRAM + "KEYWORD…] " + + "[" + PREFIX_ZOOM + "KEYWORD…] " + + "[" + PREFIX_TAG + "KEYWORD…]\n"; + public static final String SYNTAX = COMMAND_WORD + " " + PARAMETERS; + + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all contacts whose fields contains any of the " + + "given keywords.\n" + + "At least one keyword must be present. " + + "For name search, keywords must follow directly after the command word\n" + + "Parameters: " + PARAMETERS + + "Example: " + COMMAND_WORD + " alice bob charlie " + + PREFIX_PHONE + "91234567 " + + PREFIX_EMAIL + "johndoe@example.com"; + + private final ContactContainsKeywordsPredicate predicate; + + public CFindCommand(ContactContainsKeywordsPredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.setContactDisplaySetting(ContactDisplaySetting.DEFAULT_SETTING); + model.updateFilteredContactList(predicate); + return new CommandResult( + String.format(Messages.MESSAGE_CONTACTS_LISTED_OVERVIEW, model.getFilteredContactList().size())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof CFindCommand // instanceof handles nulls + && predicate.equals(((CFindCommand) other).predicate)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/contact/CListCommand.java b/src/main/java/seedu/address/logic/commands/contact/CListCommand.java new file mode 100644 index 00000000000..5e3622c7a8a --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/contact/CListCommand.java @@ -0,0 +1,73 @@ +package seedu.address.logic.commands.contact; + +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_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TELEGRAM; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ZOOM; + +import java.util.Objects; + +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.Undoable; +import seedu.address.model.Model; +import seedu.address.model.contact.ContactDisplaySetting; + +/** + * Lists all persons in the address book to the user. + */ +public class CListCommand extends Command implements Undoable { + + public static final String COMMAND_WORD = "clist"; + public static final String PARAMETERS = "[" + PREFIX_PHONE + "] " + + "[" + PREFIX_EMAIL + "] " + + "[" + PREFIX_ADDRESS + "] " + + "[" + PREFIX_TELEGRAM + "] " + + "[" + PREFIX_ZOOM + "] " + + "[" + PREFIX_TAG + "] \n"; + public static final String SYNTAX = COMMAND_WORD + " " + PARAMETERS; + + public static final String MESSAGE_SUCCESS = "Listed all contacts"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Lists all the contacts on the screen with all" + + " details by default.\n" + + "Include optional parameters to filter details.\n" + + "Parameters should not be followed by any values.\n" //added this to warn users not to add any values. + + "Parameters: " + + PARAMETERS + + "Example: " + COMMAND_WORD + " " + PREFIX_EMAIL + " " + PREFIX_ZOOM; + + private final ContactDisplaySetting displaySetting; + + public CListCommand(ContactDisplaySetting displaySetting) { + this.displaySetting = displaySetting; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.setContactDisplaySetting(displaySetting); + model.rerenderContactCards(false); + return new CommandResult(MESSAGE_SUCCESS); + } + + @Override + public int hashCode() { + return Objects.hash(displaySetting); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + return other instanceof CListCommand + && ((CListCommand) other).displaySetting.equals(displaySetting); + } +} diff --git a/src/main/java/seedu/address/logic/commands/contact/CMarkCommand.java b/src/main/java/seedu/address/logic/commands/contact/CMarkCommand.java new file mode 100644 index 00000000000..42587d4061e --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/contact/CMarkCommand.java @@ -0,0 +1,90 @@ +package seedu.address.logic.commands.contact; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.Undoable; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.contact.Contact; + + +public class CMarkCommand extends Command implements Undoable { + public static final String COMMAND_WORD = "cmark"; + + public static final String PARAMETERS = "INDEX [INDEX]...\n"; + public static final String SYNTAX = COMMAND_WORD + " " + PARAMETERS; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Marks all contact(s) indexed at " + + "the specified index(es) and displays them at the top of the contact list.\n" + + "Parameters: " + PARAMETERS + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_SUCCESS = "Marked contact: %1$s"; + public static final String MESSAGE_ALREADY_MARKED = "Contact already marked: %1$s"; + + private final List indexesToMark; + + /** + * Class constructor, takes in a list of {@code index} + */ + public CMarkCommand(List indexes) { + requireNonNull(indexes); + this.indexesToMark = indexes; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + String commandResult = ""; + List lastShownList = model.getFilteredContactList(); + if (indexesToMark.stream().anyMatch(index -> index.getZeroBased() >= lastShownList.size())) { + throw new CommandException(Messages.MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX); + } + Collections.reverse(indexesToMark); + List contactsMarked = new ArrayList<>(); + for (Index index : indexesToMark) { + Contact contact = lastShownList.get(index.getZeroBased()); + commandResult += String.format("%s", generateCommandResultMessage(contact, contact.getIsMarked())); + Contact newContact = contact.markContact(); + model.setContact(contact, newContact); + contactsMarked.add(newContact); + } + model.rearrangeContactsInOrder(contactsMarked, true); + return new CommandResult(commandResult); + } + + /** + * Creates and returns a marked {@code Contact} with the details of {@code contactToMark} + */ + private static Contact createMarkedContact(Contact contactToMark) { + return new Contact(contactToMark.getName(), contactToMark.getPhone(), contactToMark.getEmail(), + contactToMark.getAddress(), contactToMark.getZoomLink(), contactToMark.getTelegramHandle(), + contactToMark.getTags(), contactToMark.getUuid(), contactToMark.getLinkedEvents(), true); + } + + private String generateCommandResultMessage(Contact contact, + boolean isAlreadyMarked) { + String message; + if (isAlreadyMarked) { + message = String.format(MESSAGE_ALREADY_MARKED, contact); + } else { + message = String.format(MESSAGE_SUCCESS, contact); + } + return message += "\n"; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof CMarkCommand // instanceof handles nulls + && indexesToMark.equals(((CMarkCommand) other).indexesToMark)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/contact/CUnmarkCommand.java b/src/main/java/seedu/address/logic/commands/contact/CUnmarkCommand.java new file mode 100644 index 00000000000..52d003d1b6d --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/contact/CUnmarkCommand.java @@ -0,0 +1,82 @@ +package seedu.address.logic.commands.contact; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.Undoable; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.contact.Contact; + +public class CUnmarkCommand extends Command implements Undoable { + + public static final String COMMAND_WORD = "cunmark"; + + public static final String PARAMETERS = "INDEX [INDEX]...\n"; + public static final String SYNTAX = COMMAND_WORD + " " + PARAMETERS; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Unmarks all contact(s) indexed at " + + "the specified index(es) and displays them after the marked contacts.\n" + + "Parameters: " + PARAMETERS + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_SUCCESS = "Unmarked contact: %1$s"; + public static final String MESSAGE_NOT_MARKED = "Contact not marked: %1$s"; + + private final List indexesToUnmark; + + /** + * Class constructor, takes in a list of {@code index} + */ + public CUnmarkCommand(List indexes) { + requireNonNull(indexes); + this.indexesToUnmark = indexes; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + String commandResult = ""; + List lastShownList = model.getFilteredContactList(); + if (indexesToUnmark.stream().anyMatch(index -> index.getZeroBased() >= lastShownList.size())) { + throw new CommandException(Messages.MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX); + } + Collections.reverse(indexesToUnmark); + List contactsUnmarked = new ArrayList<>(); + for (Index index : indexesToUnmark) { + Contact contact = lastShownList.get(index.getZeroBased()); + commandResult += String.format("%s", generateCommandResultMessage(contact, contact.getIsMarked())); + if (contact.getIsMarked()) { + Contact newContact = contact.unmarkContact(); + model.setContact(contact, newContact); + contactsUnmarked.add(newContact); + } + } + model.rearrangeContactsInOrder(contactsUnmarked, false); + return new CommandResult(commandResult); + } + + private String generateCommandResultMessage(Contact contact, boolean isAlreadyMarked) { + String message; + if (!isAlreadyMarked) { + message = String.format(MESSAGE_NOT_MARKED, contact); + } else { + message = String.format(MESSAGE_SUCCESS, contact); + } + return message += "\n"; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof CUnmarkCommand // instanceof handles nulls + && indexesToUnmark.equals(((CUnmarkCommand) other).indexesToUnmark)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/contact/CViewCommand.java b/src/main/java/seedu/address/logic/commands/contact/CViewCommand.java new file mode 100644 index 00000000000..582cb93f918 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/contact/CViewCommand.java @@ -0,0 +1,65 @@ +package seedu.address.logic.commands.contact; + +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.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.Undoable; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.ContactDisplaySetting; + + +/** + * Views one person in full detail in the SoConnect to the user. + */ +public class CViewCommand extends Command implements Undoable { + public static final String COMMAND_WORD = "cview"; + + public static final String SYNTAX = COMMAND_WORD + " INDEX"; + + public static final String MESSAGE_SUCCESS = "Viewing Contact: %1$s"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": View one contact of the index provided " + + "on the screen with all details.\n" + + "Parameters: INDEX\n" + + "Example: " + COMMAND_WORD + " 1"; + + private final Index index; + + public CViewCommand(Index index) { + this.index = index; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredContactList(); + + Index viewIndex = index; + int intIndex = viewIndex.getZeroBased(); + if (intIndex < 0 || intIndex >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX); + } + model.setContactDisplaySetting(new ContactDisplaySetting(true)); + model.updateContactListByIndex(viewIndex); + return new CommandResult(String.format(MESSAGE_SUCCESS, lastShownList.get(0))); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + return other instanceof CViewCommand + && ((CViewCommand) other).index.equals(index); + } +} diff --git a/src/main/java/seedu/address/logic/commands/event/EAddCommand.java b/src/main/java/seedu/address/logic/commands/event/EAddCommand.java new file mode 100644 index 00000000000..6abe93078b3 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/event/EAddCommand.java @@ -0,0 +1,85 @@ +package seedu.address.logic.commands.event; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_END_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_START_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ZOOM; + +import java.util.List; + +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.Undoable; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventChanger; + +/** + * Adds a event to the address book. + */ +public class EAddCommand extends Command implements Undoable { + + public static final String COMMAND_WORD = "eadd"; + public static final String PARAMETERS = "" + PREFIX_NAME + "NAME " + + "" + PREFIX_START_TIME + "START " + + "[" + PREFIX_END_TIME + "END] " + + "[" + PREFIX_DESCRIPTION + "DESCRIPTION] " + + "[" + PREFIX_ADDRESS + "ADDRESS] " + + "[" + PREFIX_ZOOM + "ZOOM] " + + "[" + PREFIX_TAG + "TAG]...\n"; + public static final String SYNTAX = COMMAND_WORD + " " + PARAMETERS; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds an event to SoConnect. \n" + + "Parameters: " + + PARAMETERS + + "Example: " + COMMAND_WORD + " " + + PREFIX_NAME + "Summer Party " + + PREFIX_START_TIME + "12-12-2021 15:12 " + + PREFIX_DESCRIPTION + "end of semester party " + + PREFIX_ADDRESS + "123, Clementi Rd, S1234665 " + + PREFIX_ZOOM + "http://zoomlink.com " + + PREFIX_TAG + "fun"; + + public static final String MESSAGE_SUCCESS = "New event added: %1$s"; + public static final String MESSAGE_DUPLICATE_EVENT = "This event already exists in the address book"; + public static final String MESSAGE_INVALID_DATE_TIME_RANGE = "Event start time cannot be later than end time."; + + private final Event toAdd; + + /** + * Creates an AddCommand to add the specified {@code Event} + * @param event the specified Event + */ + public EAddCommand(Event event) { + requireNonNull(event); + toAdd = event; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + if (model.hasEvent(toAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_EVENT); + } + + if (toAdd.getEndDateAndTime() != null && toAdd.getEndDateAndTime().isBefore(toAdd.getStartDateAndTime())) { + throw new CommandException(MESSAGE_INVALID_DATE_TIME_RANGE); + } + + model.addEvent(toAdd); + return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd), List.of(EventChanger.addEventChanger(toAdd))); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof EAddCommand // instanceof handles nulls + && toAdd.equals(((EAddCommand) other).toAdd)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/event/EClearCommand.java b/src/main/java/seedu/address/logic/commands/event/EClearCommand.java new file mode 100644 index 00000000000..69032fa17ed --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/event/EClearCommand.java @@ -0,0 +1,33 @@ +package seedu.address.logic.commands.event; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.Undoable; +import seedu.address.model.Model; +import seedu.address.model.event.EventChanger; + +/** + * Clears all events in SoConnect. + */ +public class EClearCommand extends Command implements Undoable { + + public static final String COMMAND_WORD = "eclear"; + + public static final String SYNTAX = COMMAND_WORD; + + public static final String MESSAGE_SUCCESS = "All events have been cleared!"; + + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.resetEvents(); + // rerender UI to remove all links + model.rerenderContactCards(true); + return new CommandResult(MESSAGE_SUCCESS, List.of(EventChanger.clearEventChanger())); + } +} diff --git a/src/main/java/seedu/address/logic/commands/event/EDeleteCommand.java b/src/main/java/seedu/address/logic/commands/event/EDeleteCommand.java new file mode 100644 index 00000000000..b26d011a4cc --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/event/EDeleteCommand.java @@ -0,0 +1,80 @@ +package seedu.address.logic.commands.event; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.core.range.Range; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.Undoable; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventChanger; + +/** + * Deletes an event identified using its displayed index from the SoConnect. + */ +public class EDeleteCommand extends Command implements Undoable { + + public static final String COMMAND_WORD = "edelete"; + public static final String PARAMETERS = "INDEX1[-INDEX2]"; + public static final String SYNTAX = COMMAND_WORD + " " + PARAMETERS; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the event identified by the index number used in the displayed event list.\n" + + "Parameters: " + PARAMETERS + " (both indexes must be a positive integer)\n" + + "Note: index must be a positive integer and INDEX1 must be smaller than or equal to INDEX2" + + " if the optional INDEX2 is included)\n" + + "Example 1: " + COMMAND_WORD + " 1\n" + + "Example 2: " + COMMAND_WORD + " 2-5"; + + public static final String MESSAGE_DELETE_EVENT_SUCCESS = "Deleted Event: %1$s"; + + private final Range targetRange; + + public EDeleteCommand(Range targetRange) { + this.targetRange = targetRange; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredEventList(); + + Index startIndex = targetRange.getStart(); + Index endIndex = targetRange.getEnd(); + int end = endIndex.getZeroBased(); + if (end >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_EVENT_DISPLAYED_INDEX); + } + + String commandResult = ""; + int indexToDelete = startIndex.getZeroBased(); + List eventChangerList = new ArrayList<>(); + for (int i = indexToDelete; i <= end; i++) { + Event eventToDelete = lastShownList.get(indexToDelete); + model.deleteEvent(eventToDelete); + eventChangerList.add(EventChanger.deleteEventChanger(eventToDelete)); + commandResult += String.format(MESSAGE_DELETE_EVENT_SUCCESS, eventToDelete); + if (i != end) { + commandResult += "\n"; + } + } + // rerender UI to update the links for contacts with links to deleted event + model.rerenderContactCards(true); + return new CommandResult(commandResult, eventChangerList); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof EDeleteCommand // instanceof handles nulls + && targetRange.equals(((EDeleteCommand) other).targetRange)); // state check + } +} + diff --git a/src/main/java/seedu/address/logic/commands/event/EEditCommand.java b/src/main/java/seedu/address/logic/commands/event/EEditCommand.java new file mode 100644 index 00000000000..562a10467fc --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/event/EEditCommand.java @@ -0,0 +1,339 @@ +package seedu.address.logic.commands.event; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DELETE_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_END_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_START_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ZOOM; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_EVENTS; + +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.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.Undoable; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.common.Address; +import seedu.address.model.common.Name; +import seedu.address.model.common.ZoomLink; +import seedu.address.model.event.Description; +import seedu.address.model.event.EndDateTime; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventChanger; +import seedu.address.model.event.StartDateTime; +import seedu.address.model.tag.Tag; + +/** + * Edits the details of an existing event in the address book. + */ +public class EEditCommand extends Command implements Undoable { + + public static final String COMMAND_WORD = "eedit"; + public static final String PARAMETERS = "INDEX (must be a positive integer) " + + "[" + PREFIX_NAME + "NAME] " + + "[" + PREFIX_START_TIME + "START] " + + "[" + PREFIX_END_TIME + "END] " + + "[" + PREFIX_DESCRIPTION + "DESCRIPTION] " + + "[" + PREFIX_ADDRESS + "ADDRESS] " + + "[" + PREFIX_ZOOM + "ZOOM] " + + "[" + PREFIX_TAG + "TAG]... " + + "[" + PREFIX_DELETE_TAG + "DELETE TAG]...\n"; + public static final String SYNTAX = COMMAND_WORD + " " + PARAMETERS; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the event identified " + + "by the index number used in the displayed event list. " + + "Existing values will be overwritten by the input values.\n" + + "Parameters: " + PARAMETERS + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_START_TIME + "01-10-2021 15:00 " + + PREFIX_ADDRESS + "12th Street"; + + public static final String MESSAGE_EDIT_EVENT_SUCCESS = "Edited Event: %1$s"; + public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; + public static final String MESSAGE_DUPLICATE_EVENT = "This event already exists in the address book."; + public static final String MESSAGE_INVALID_DATE_TIME_RANGE = "Event start time cannot be later than end time."; + public static final String MESSAGE_TAG_TO_ADD_ALREADY_IN_ORIGINAL = "Event already has %s tag.\n"; + public static final String MESSAGE_TAG_TO_DELETE_NOT_IN_ORIGINAL = + "Event does not contain %s tag to delete.\n"; + + private final Index index; + private final EditEventDescriptor editEventDescriptor; + // to be displayed to user if user tries to delete a tag that does not exist + // or add a tag that already exists + private String infoMessage = ""; + + /** + * @param index of the event in the filtered event list to edit + * @param editEventDescriptor details to edit the event with + */ + public EEditCommand(Index index, EditEventDescriptor editEventDescriptor) { + requireNonNull(index); + requireNonNull(editEventDescriptor); + + this.index = index; + this.editEventDescriptor = new EditEventDescriptor(editEventDescriptor); + } + + /** + * Creates and returns a {@code Event} with the details of {@code eventToEdit} + * edited with {@code editEventDescriptor}. + */ + private Event createEditedEvent(Event eventToEdit, EditEventDescriptor editEventDescriptor) { + assert eventToEdit != null; + + Name updatedName = editEventDescriptor.getName().orElse(eventToEdit.getName()); + StartDateTime updatedStartDateTime = editEventDescriptor.getStartDateTime() + .orElse(eventToEdit.getStartDateAndTime()); + EndDateTime updatedEndDateTime = editEventDescriptor.getEndDateTime() + .orElse(eventToEdit.getEndDateAndTime()); + Description updatedDescription = editEventDescriptor.getDescription().orElse(eventToEdit.getDescription()); + Address updatedAddress = editEventDescriptor.getAddress().orElse(eventToEdit.getAddress()); + ZoomLink updatedZoomLink = editEventDescriptor.getZoomLink().orElse(eventToEdit.getZoomLink()); + Set updatedNewTags = editEventDescriptor.getTags().orElse(new HashSet<>()); + Set updatedDeletedTags = editEventDescriptor.getTagsToDelete().orElse(new HashSet<>()); + Set updatedTags = editEventDescriptor.getShouldDeleteAllTags() + ? updatedNewTags : addAndRemoveTags(updatedNewTags, updatedDeletedTags, eventToEdit.getTags()); + Event updatedEvent = new Event(updatedName, updatedStartDateTime, updatedEndDateTime, updatedDescription, + updatedAddress, updatedZoomLink, updatedTags, eventToEdit.getUuid(), eventToEdit.getLinkedContacts(), + eventToEdit.getIsMarked()); + // update the edited event to the hashmap that stores references to all events + Event.addToMap(updatedEvent); + return updatedEvent; + } + + /** + * Creates and returns a {@code Set} with tags from {@code original} and {@code toAdd}, but + * tags in {@code toRemove} will be excluded. + */ + private Set addAndRemoveTags(Set toAdd, Set toRemove, Set original) { + Set updatedTags = new HashSet<>(original); + String result = "\nNote:\n"; + for (Tag tag : toAdd) { + if (!updatedTags.add(tag)) { // if the tag to delete is not in the original tags + result += String.format(MESSAGE_TAG_TO_ADD_ALREADY_IN_ORIGINAL, tag); + } + } + for (Tag tag : toRemove) { + if (!updatedTags.remove(tag)) { // if the tag to delete is not in the original tags + result += String.format(MESSAGE_TAG_TO_DELETE_NOT_IN_ORIGINAL, tag); + } + } + infoMessage = !result.equals("\nNote:\n") ? result : ""; + updatedTags.addAll(toAdd); + return updatedTags; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredEventList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_EVENT_DISPLAYED_INDEX); + } + + Event eventToEdit = lastShownList.get(index.getZeroBased()); + Event editedEvent = createEditedEvent(eventToEdit, editEventDescriptor); + + if (!eventToEdit.isSameEvent(editedEvent) && model.hasEvent(editedEvent)) { + throw new CommandException(MESSAGE_DUPLICATE_EVENT); + } + + if (editedEvent.getEndDateAndTime() != null + && editedEvent.getEndDateAndTime().isBefore(editedEvent.getStartDateAndTime())) { + throw new CommandException(MESSAGE_INVALID_DATE_TIME_RANGE); + } + + model.setEvent(eventToEdit, editedEvent); + model.updateFilteredEventList(PREDICATE_SHOW_ALL_EVENTS); + // rerender UI to show latest change for contacts with links to edited event + model.rerenderContactCards(true); + String result = String.format(MESSAGE_EDIT_EVENT_SUCCESS, editedEvent) + infoMessage; + return new CommandResult(result, List.of(EventChanger.editEventChanger(eventToEdit, editedEvent))); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EEditCommand)) { + return false; + } + + // state check + EEditCommand e = (EEditCommand) other; + return index.equals(e.index) + && editEventDescriptor.equals(e.editEventDescriptor); + } + + /** + * Stores the details to edit the event with. Each non-empty field value will replace the + * corresponding field value of the event. + */ + public static class EditEventDescriptor { + private Name name; + private StartDateTime start; + private EndDateTime end; + private Description description; + private Address address; + private ZoomLink zoom; + private Set tags; + private Set tagsToDelete; + private boolean shouldDeleteAllTags = false; + + public EditEventDescriptor() { + } + + /** + * Copy constructor. + * A defensive copy of {@code tags} is used internally. + */ + public EditEventDescriptor(EditEventDescriptor toCopy) { + setName(toCopy.name); + setStartDateTime(toCopy.start); + setEndDateTime(toCopy.end); + setDescription(toCopy.description); + setAddress(toCopy.address); + setZoomLink(toCopy.zoom); + setTags(toCopy.tags); + setTagsToDelete(toCopy.tagsToDelete); + setShouldDeleteAllTags(toCopy.shouldDeleteAllTags); + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(name, start, end, description, address, zoom, tags, tagsToDelete) + || shouldDeleteAllTags; + } + + public void setName(Name name) { + this.name = name; + } + + public Optional getName() { + return Optional.ofNullable(name); + } + + public void setStartDateTime(StartDateTime start) { + this.start = start; + } + + public Optional getStartDateTime() { + return Optional.ofNullable(start); + } + + public void setEndDateTime(EndDateTime end) { + this.end = end; + } + + public Optional getEndDateTime() { + return Optional.ofNullable(end); + } + + public void setDescription(Description description) { + this.description = description; + } + + public Optional getDescription() { + return Optional.ofNullable(description); + } + + public void setAddress(Address address) { + this.address = address; + } + + public Optional
getAddress() { + return Optional.ofNullable(address); + } + + public void setZoomLink(ZoomLink zoom) { + this.zoom = zoom; + } + + public Optional getZoomLink() { + return Optional.ofNullable(zoom); + } + + /** + * 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 && !tags.isEmpty()) ? new HashSet<>(tags) : null; + } + + public Optional> getTags() { + return Optional.ofNullable(tags); + } + /** + * Returns an unmodifiable tag set to be removed, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code tagsToDelete} is null. + */ + public Optional> getTagsToDelete() { + return tagsToDelete != null ? Optional.of(Collections.unmodifiableSet((tagsToDelete))) : Optional.empty(); + } + + /** + * Sets {@code tagsToDelete} to this object's {@code tagsToDelete}. + * A defensive copy of {@code tagsToDelete} is used internally. + */ + public void setTagsToDelete(Set tagsToDelete) { + this.tagsToDelete = (tagsToDelete != null && !tagsToDelete.isEmpty()) ? new HashSet<>(tagsToDelete) : null; + } + + public boolean getShouldDeleteAllTags() { + return shouldDeleteAllTags; + } + + /** + * Sets the boolean condition of whether all tags should be cleared first. + */ + public void setShouldDeleteAllTags(boolean shouldDeleteAllTags) { + this.shouldDeleteAllTags = shouldDeleteAllTags; + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditEventDescriptor)) { + return false; + } + + // state check + EditEventDescriptor e = (EditEventDescriptor) other; + + return getName().equals(e.getName()) + && getStartDateTime().equals(e.getStartDateTime()) + && getEndDateTime().equals(e.getEndDateTime()) + && getDescription().equals(e.getDescription()) + && getAddress().equals(e.getAddress()) + && getZoomLink().equals(e.getZoomLink()) + && getTags().equals(e.getTags()) + && getTagsToDelete().equals(e.getTagsToDelete()); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/event/EFindCommand.java b/src/main/java/seedu/address/logic/commands/event/EFindCommand.java new file mode 100644 index 00000000000..30e79c8392b --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/event/EFindCommand.java @@ -0,0 +1,66 @@ +package seedu.address.logic.commands.event; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_END_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_START_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ZOOM; + +import seedu.address.commons.core.Messages; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.Undoable; +import seedu.address.model.Model; +import seedu.address.model.event.EventContainsKeywordsPredicate; +import seedu.address.model.event.EventDisplaySetting; + +/** + * Finds and lists all events in SoConnect which have names containing any of the argument keywords. + * Keyword matching is case insensitive. + */ +public class EFindCommand extends Command implements Undoable { + + public static final String COMMAND_WORD = "efind"; + public static final String PARAMETERS = "[KEYWORD]… " + + "[" + PREFIX_START_TIME + "KEYWORD…] " + + "[" + PREFIX_END_TIME + "KEYWORD…] " + + "[" + PREFIX_ADDRESS + "KEYWORD…] " + + "[" + PREFIX_DESCRIPTION + "KEYWORD…] " + + "[" + PREFIX_ZOOM + "KEYWORD…] " + + "[" + PREFIX_TAG + "KEYWORD…]\n"; + public static final String SYNTAX = COMMAND_WORD + " " + PARAMETERS; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all events whose fields contains any of the " + + "given keywords.\n" + + "At least one keyword must be present. " + + "For name search, keywords must follow directly after the command word\n" + + "Parameters: " + PARAMETERS + + "Example: " + COMMAND_WORD + " cs 2103t " + + PREFIX_START_TIME + "2020-12-01 " + + PREFIX_EMAIL + "johndoe@example.com"; + + private final EventContainsKeywordsPredicate predicate; + + public EFindCommand(EventContainsKeywordsPredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.setEventDisplaySetting(EventDisplaySetting.DEFAULT_SETTING); + model.updateFilteredEventList(predicate); + return new CommandResult( + String.format(Messages.MESSAGE_EVENTS_LISTED_OVERVIEW, model.getFilteredEventList().size())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof EFindCommand // instanceof handles nulls + && predicate.equals(((EFindCommand) other).predicate)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/event/ELinkCommand.java b/src/main/java/seedu/address/logic/commands/event/ELinkCommand.java new file mode 100644 index 00000000000..64ed4423479 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/event/ELinkCommand.java @@ -0,0 +1,102 @@ +package seedu.address.logic.commands.event; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_CONTACT; + +import java.util.List; +import java.util.Set; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.Undoable; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.contact.Contact; +import seedu.address.model.event.Event; + +/** Links an event to a list of contacts. */ +public class ELinkCommand extends Command implements Undoable { + public static final String COMMAND_WORD = "elink"; + public static final String PARAMETERS = "EVENT_INDEX " + + PREFIX_CONTACT + "CONTACT_INDEX [" + PREFIX_CONTACT + "CONTACT_INDEX]...\n"; + public static final String SYNTAX = COMMAND_WORD + " " + PARAMETERS; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Links an event to one or more contacts. \n" + + "Parameters: " + + PARAMETERS + + "Example 1: elink 1 c/1\n" + + "Example 2: elink 3 c/1 c/2 c/3"; + public static final String MESSAGE_SUCCESS = "Successfully linked the event %s to the contact %s.\n"; + public static final String MESSAGE_ALREADY_LINKED = "Event %s is already linked to the contact %s.\n"; + + private final Index eventIndex; + private final Set contactIndexes; + + /** + * Creates an ELinkCommand to link the specified {@code Event} to a {@code Contact} + */ + public ELinkCommand(Index eventIndex, Set contactIndexes) { + assert !contactIndexes.isEmpty() : "Set of contact indices cannot be empty."; + this.eventIndex = eventIndex; + this.contactIndexes = contactIndexes; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + // check validity of command + List lastShownEventList = model.getFilteredEventList(); + List lastShownContactList = model.getFilteredContactList(); + checkCommandValidity(lastShownEventList, lastShownContactList); + + // execution of command + CommandResult commandResult = linkEventAndContacts(model, lastShownEventList, lastShownContactList); + + // rerender UI to show the links between event and each of the contacts + model.rerenderAllCards(); + + return commandResult; + } + + private void checkCommandValidity(List eventList, List contactList) throws CommandException { + if (eventIndex.getZeroBased() >= eventList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_EVENT_DISPLAYED_INDEX); + } + + for (Index contactIndex : contactIndexes) { + if (contactIndex.getZeroBased() >= contactList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX); + } + } + } + + private CommandResult linkEventAndContacts(Model model, List lastShownEventList, + List lastShownContactList) { + String commandResult = ""; + for (Index contactIndex : contactIndexes) { + // have to get the event from the list again because a new event replaces the index whenever + // a link occurs, hence cannot use the old reference of event. + Event eventToLink = lastShownEventList.get(eventIndex.getZeroBased()); + Contact contactToLink = lastShownContactList.get(contactIndex.getZeroBased()); + if (contactToLink.hasLinkTo(eventToLink)) { + assert eventToLink.hasLinkTo(contactToLink) : "Both should have links to each other"; + commandResult += String.format(MESSAGE_ALREADY_LINKED, eventToLink.getName(), contactToLink.getName()); + continue; + } + model.linkEventAndContact(eventToLink, contactToLink); + commandResult += String.format(MESSAGE_SUCCESS, eventToLink.getName(), contactToLink.getName()); + } + return new CommandResult(commandResult); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ELinkCommand // instanceof handles nulls + && eventIndex.equals(((ELinkCommand) other).eventIndex) + && contactIndexes.equals(((ELinkCommand) other).contactIndexes)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/event/EListCommand.java b/src/main/java/seedu/address/logic/commands/event/EListCommand.java new file mode 100644 index 00000000000..2ffa8599678 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/event/EListCommand.java @@ -0,0 +1,73 @@ +package seedu.address.logic.commands.event; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_END_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_START_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ZOOM; + +import java.util.Objects; + +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.Undoable; +import seedu.address.model.Model; +import seedu.address.model.event.EventDisplaySetting; + +/** + * Lists all events in the address book to the user. + */ +public class EListCommand extends Command implements Undoable { + + public static final String COMMAND_WORD = "elist"; + public static final String PARAMETERS = "[" + PREFIX_START_TIME + "] " + + "[" + PREFIX_END_TIME + "] " + + "[" + PREFIX_DESCRIPTION + "] " + + "[" + PREFIX_ADDRESS + "] " + + "[" + PREFIX_ZOOM + "] " + + "[" + PREFIX_TAG + "] \n"; + public static final String SYNTAX = COMMAND_WORD + " " + PARAMETERS; + + public static final String MESSAGE_SUCCESS = "Listed all events"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Lists all the events on the screen with all" + + " details by default.\n" + + "Include optional parameters to filter details.\n" + + "Parameters should not be followed by any values.\n" //added this to warn users not to add any values. + + "Parameters: " + + PARAMETERS + + "Example: " + COMMAND_WORD + " " + PREFIX_END_TIME + " " + PREFIX_ZOOM; + + public final EventDisplaySetting displaySetting; + + public EListCommand(EventDisplaySetting displaySetting) { + this.displaySetting = displaySetting; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.setEventDisplaySetting(displaySetting); + model.rerenderEventCards(false); + return new CommandResult(MESSAGE_SUCCESS); + } + + @Override + public int hashCode() { + return Objects.hash(displaySetting); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + return other instanceof EListCommand + && ((EListCommand) other).displaySetting.equals(displaySetting); + } +} diff --git a/src/main/java/seedu/address/logic/commands/event/EMarkCommand.java b/src/main/java/seedu/address/logic/commands/event/EMarkCommand.java new file mode 100644 index 00000000000..14c902b9b98 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/event/EMarkCommand.java @@ -0,0 +1,82 @@ +package seedu.address.logic.commands.event; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.Undoable; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.event.Event; + + +public class EMarkCommand extends Command implements Undoable { + + public static final String COMMAND_WORD = "emark"; + + public static final String PARAMETERS = "INDEX [INDEX]...\n"; + public static final String SYNTAX = COMMAND_WORD + " " + PARAMETERS; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Marks all event(s) indexed at " + + "the specified index(es) and displays them at the top of the event list.\n" + + "Parameters: " + PARAMETERS + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_SUCCESS = "Marked event: %1$s"; + public static final String MESSAGE_ALREADY_MARKED = "Event already marked: %1$s"; + + private final List indexesToMark; + + /** + * Class constryctor, takes in a list of {@code index} + */ + public EMarkCommand(List indexes) { + requireNonNull(indexes); + this.indexesToMark = indexes; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + String commandResult = ""; + List lastShownList = model.getFilteredEventList(); + if (indexesToMark.stream().anyMatch(index -> index.getZeroBased() >= lastShownList.size())) { + throw new CommandException(Messages.MESSAGE_INVALID_EVENT_DISPLAYED_INDEX); + } + Collections.reverse(indexesToMark); + List eventsMarked = new ArrayList<>(); + for (Index index : indexesToMark) { + Event event = lastShownList.get(index.getZeroBased()); + commandResult += String.format("%s", generateCommandResultMessage(event, event.getIsMarked())); + Event newEvent = event.markEvent(); + model.setEvent(event, newEvent); + eventsMarked.add(newEvent); + } + model.rearrangeEventsInOrder(eventsMarked, true); + return new CommandResult(commandResult); + } + + private String generateCommandResultMessage(Event event, + boolean isAlreadyMarked) { + String message; + if (isAlreadyMarked) { + message = String.format(MESSAGE_ALREADY_MARKED, event); + } else { + message = String.format(MESSAGE_SUCCESS, event); + } + return message += "\n"; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof EMarkCommand // instanceof handles nulls + && indexesToMark.equals(((EMarkCommand) other).indexesToMark)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/event/ESortCommand.java b/src/main/java/seedu/address/logic/commands/event/ESortCommand.java new file mode 100644 index 00000000000..7ce3ae15d8b --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/event/ESortCommand.java @@ -0,0 +1,25 @@ +package seedu.address.logic.commands.event; + +import static java.util.Objects.requireNonNull; + +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.Undoable; +import seedu.address.model.Model; + +/** Sorts the address book and show only the upcoming and ongoing events. */ +public class ESortCommand extends Command implements Undoable { + + public static final String COMMAND_WORD = "esort"; + + public static final String SYNTAX = COMMAND_WORD; + + public static final String MESSAGE_SUCCESS = "Showing upcoming events in sorted order."; + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.sortUpcomingFilteredEventList(); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/event/EUnlinkCommand.java b/src/main/java/seedu/address/logic/commands/event/EUnlinkCommand.java new file mode 100644 index 00000000000..1c87847d923 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/event/EUnlinkCommand.java @@ -0,0 +1,124 @@ +package seedu.address.logic.commands.event; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_CONTACT; + +import java.util.List; +import java.util.Set; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.Undoable; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.contact.Contact; +import seedu.address.model.event.Event; + +/** Unlinks an event from a list of contacts. */ +public class EUnlinkCommand extends Command implements Undoable { + public static final String COMMAND_WORD = "eunlink"; + + public static final String PARAMETERS = "EVENT_INDEX " + + PREFIX_CONTACT + "CONTACT_INDEX [" + PREFIX_CONTACT + "CONTACT_INDEX]...\n"; + public static final String SYNTAX = COMMAND_WORD + " " + PARAMETERS; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Unlinks an event from one or more contacts." + + " Include the argument \"c/*\" to clear all links.\n" + + "Parameters: " + + PARAMETERS + + "Example 1: eunlink 1 c/1\n" + + "Example 2: eunlink 3 c/1 c/2 c/3\n" + + "Example 3: eunlink 2 c/*"; + public static final String MESSAGE_SUCCESS = "Successfully unlinked the event %s from the contact %s\n"; + public static final String MESSAGE_SUCCESS_CLEAR_ALL = "Successfully unlinked the event %s from all contacts."; + public static final String MESSAGE_NOT_LINKED = "Event %s is already not linked to the contact %s.\n"; + + private final Index eventIndex; + private final Set contactIndexes; + private final boolean isClearAllLinks; + + /** + * Creates an EUnlinkCommand to remove the link from the specified {@code Event} to a {@code Contact} + */ + public EUnlinkCommand(Index eventIndex, Set contactIndexes, boolean isClearAllLinks) { + requireAllNonNull(eventIndex, contactIndexes); + assert (!contactIndexes.isEmpty() || isClearAllLinks) + && !(!contactIndexes.isEmpty() && isClearAllLinks) + : "Either the set is empty or isClearAllLinks is true, but not both"; + this.eventIndex = eventIndex; + this.contactIndexes = contactIndexes; + this.isClearAllLinks = isClearAllLinks; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + // check validity of command + List lastShownEventList = model.getFilteredEventList(); + List lastShownContactList = model.getFilteredContactList(); + checkCommandValidity(lastShownEventList, lastShownContactList); + + // execution of command + CommandResult commandResult; + Event eventToUnlink = lastShownEventList.get(eventIndex.getZeroBased()); + if (!isClearAllLinks) { + commandResult = unlinkEventAndContacts(model, lastShownEventList, lastShownContactList); + } else { + commandResult = unlinkEventAndAllContacts(eventToUnlink, model); + } + + // rerender UI to show the links between event and each of the contacts + model.rerenderAllCards(); + + return commandResult; + } + + private void checkCommandValidity(List eventList, List contactList) throws CommandException { + if (eventIndex.getZeroBased() >= eventList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_EVENT_DISPLAYED_INDEX); + } + + for (Index contactIndex : contactIndexes) { + if (contactIndex.getZeroBased() >= contactList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX); + } + } + } + + private CommandResult unlinkEventAndContacts(Model model, List lastShownEventList, + List lastShownContactList) { + String commandResult = ""; + for (Index contactIndex : contactIndexes) { + // have to get the event from the list again because a new event replaces the index whenever + // an unlink occurs, hence cannot use the old reference of event. + Event eventToUnlink = lastShownEventList.get(eventIndex.getZeroBased()); + Contact contactToUnlink = lastShownContactList.get(contactIndex.getZeroBased()); + if (!contactToUnlink.hasLinkTo(eventToUnlink)) { + assert !eventToUnlink.hasLinkTo(contactToUnlink) : "Both should not have links to each other"; + commandResult += String.format(MESSAGE_NOT_LINKED, eventToUnlink.getName(), contactToUnlink.getName()); + continue; + } + model.unlinkEventAndContact(eventToUnlink, contactToUnlink); + commandResult += String.format(MESSAGE_SUCCESS, eventToUnlink.getName(), contactToUnlink.getName()); + } + return new CommandResult(commandResult); + } + + private CommandResult unlinkEventAndAllContacts(Event eventToUnlink, Model model) { + model.unlinkAllContactsFromEvent(eventToUnlink); + return new CommandResult(String.format(MESSAGE_SUCCESS_CLEAR_ALL, eventToUnlink.getName())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof EUnlinkCommand // instanceof handles nulls + && eventIndex.equals(((EUnlinkCommand) other).eventIndex) + && contactIndexes.equals(((EUnlinkCommand) other).contactIndexes) + && isClearAllLinks == ((EUnlinkCommand) other).isClearAllLinks); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/event/EUnmarkCommand.java b/src/main/java/seedu/address/logic/commands/event/EUnmarkCommand.java new file mode 100644 index 00000000000..187d33a72f4 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/event/EUnmarkCommand.java @@ -0,0 +1,82 @@ +package seedu.address.logic.commands.event; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.Undoable; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.event.Event; + +public class EUnmarkCommand extends Command implements Undoable { + + public static final String COMMAND_WORD = "eunmark"; + + public static final String PARAMETERS = "INDEX [INDEX]...\n"; + public static final String SYNTAX = COMMAND_WORD + " " + PARAMETERS; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Unmarks all event(s) indexed at " + + "the specified index(es) and displays them after the marked events.\n" + + "Parameters: " + PARAMETERS + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_SUCCESS = "Unmarked event: %1$s"; + public static final String MESSAGE_NOT_MARKED = "Event not marked: %1$s"; + + private final List indexesToUnmark; + + /** + * Class constructor, takes in a list of {@code index} + */ + public EUnmarkCommand(List indexes) { + requireNonNull(indexes); + this.indexesToUnmark = indexes; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + String commandResult = ""; + List lastShownList = model.getFilteredEventList(); + if (indexesToUnmark.stream().anyMatch(index -> index.getZeroBased() >= lastShownList.size())) { + throw new CommandException(Messages.MESSAGE_INVALID_EVENT_DISPLAYED_INDEX); + } + Collections.reverse(indexesToUnmark); + List eventsUnmarked = new ArrayList<>(); + for (Index index : indexesToUnmark) { + Event event = lastShownList.get(index.getZeroBased()); + commandResult += String.format("%s", generateCommandResultMessage(event, event.getIsMarked())); + if (event.getIsMarked()) { + Event newEvent = event.unmarkEvent(); + model.setEvent(event, newEvent); + eventsUnmarked.add(newEvent); + } + } + model.rearrangeEventsInOrder(eventsUnmarked, false); + return new CommandResult(commandResult); + } + + private String generateCommandResultMessage(Event event, boolean isAlreadyMarked) { + String message; + if (!isAlreadyMarked) { + message = String.format(MESSAGE_NOT_MARKED, event); + } else { + message = String.format(MESSAGE_SUCCESS, event); + } + return message += "\n"; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof EUnmarkCommand // instanceof handles nulls + && indexesToUnmark.equals(((EUnmarkCommand) other).indexesToUnmark)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/event/EViewCommand.java b/src/main/java/seedu/address/logic/commands/event/EViewCommand.java new file mode 100644 index 00000000000..98ee123db84 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/event/EViewCommand.java @@ -0,0 +1,66 @@ +package seedu.address.logic.commands.event; + +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.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.Undoable; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventDisplaySetting; + + +/** + * Views one event in full detail in SoConnect to the user. + */ +public class EViewCommand extends Command implements Undoable { + public static final String COMMAND_WORD = "eview"; + + public static final String SYNTAX = COMMAND_WORD + " INDEX"; + + public static final String MESSAGE_SUCCESS = "Viewing Event: %1$s"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": View one event of the index provided " + + "on the screen with all details.\n" + + "Parameters: INDEX\n" + + "Example: " + COMMAND_WORD + " 1"; + + private final Index index; + + public EViewCommand(Index index) { + this.index = index; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredEventList(); + + Index viewIndex = index; + int intIndex = viewIndex.getZeroBased(); + if (intIndex < 0 || intIndex >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_EVENT_DISPLAYED_INDEX); + } + model.setEventDisplaySetting(new EventDisplaySetting(true)); + model.updateEventListByIndex(viewIndex); + return new CommandResult(String.format(MESSAGE_SUCCESS, lastShownList.get(0))); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + return other instanceof EViewCommand + && ((EViewCommand) other).index.equals(index); + } +} + diff --git a/src/main/java/seedu/address/logic/commands/general/CalendarCommand.java b/src/main/java/seedu/address/logic/commands/general/CalendarCommand.java new file mode 100644 index 00000000000..fe9fa69a61b --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/general/CalendarCommand.java @@ -0,0 +1,22 @@ +package seedu.address.logic.commands.general; + +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.model.Model; + +/** + * Represents the command to open a calendar window showing all events. + */ +public class CalendarCommand extends Command { + + public static final String COMMAND_WORD = "calendar"; + + public static final String SYNTAX = COMMAND_WORD; + + public static final String MESSAGE_SUCCESS = "Showing calendar."; + + @Override + public CommandResult execute(Model model) { + return new CommandResult(MESSAGE_SUCCESS, false, false, true); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/general/ExitCommand.java similarity index 65% rename from src/main/java/seedu/address/logic/commands/ExitCommand.java rename to src/main/java/seedu/address/logic/commands/general/ExitCommand.java index 3dd85a8ba90..d1681162df4 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/address/logic/commands/general/ExitCommand.java @@ -1,5 +1,7 @@ -package seedu.address.logic.commands; +package seedu.address.logic.commands.general; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; import seedu.address.model.Model; /** @@ -9,11 +11,13 @@ public class ExitCommand extends Command { public static final String COMMAND_WORD = "exit"; + public static final String SYNTAX = COMMAND_WORD; + public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Address Book as requested ..."; @Override public CommandResult execute(Model model) { - return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true); + return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true, false); } } diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/general/HelpCommand.java similarity index 72% rename from src/main/java/seedu/address/logic/commands/HelpCommand.java rename to src/main/java/seedu/address/logic/commands/general/HelpCommand.java index bf824f91bd0..df6d56b4d64 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/seedu/address/logic/commands/general/HelpCommand.java @@ -1,5 +1,7 @@ -package seedu.address.logic.commands; +package seedu.address.logic.commands.general; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; import seedu.address.model.Model; /** @@ -9,6 +11,8 @@ public class HelpCommand extends Command { public static final String COMMAND_WORD = "help"; + public static final String SYNTAX = COMMAND_WORD; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows program usage instructions.\n" + "Example: " + COMMAND_WORD; @@ -16,6 +20,6 @@ public class HelpCommand extends Command { @Override public CommandResult execute(Model model) { - return new CommandResult(SHOWING_HELP_MESSAGE, true, false); + return new CommandResult(SHOWING_HELP_MESSAGE, true, false, false); } } diff --git a/src/main/java/seedu/address/logic/commands/general/RedoCommand.java b/src/main/java/seedu/address/logic/commands/general/RedoCommand.java new file mode 100644 index 00000000000..b5b3536486f --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/general/RedoCommand.java @@ -0,0 +1,33 @@ +package seedu.address.logic.commands.general; + +import static java.util.Objects.requireNonNull; + +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Represents the command to redo a change by {@code UndoCommand}. + */ +public class RedoCommand extends Command { + public static final String COMMAND_WORD = "redo"; + + public static final String SYNTAX = COMMAND_WORD; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Restore the previously undone state of addressBook"; + + public static final String MESSAGE_SUCCESS = "Command redone"; + + public static final String MESSAGE_FAIL_TO_REDO = "No command to redo."; + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + if (!model.isRedoable()) { + throw new CommandException(MESSAGE_FAIL_TO_REDO); + } + model.redoHistory(); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/general/UndoCommand.java b/src/main/java/seedu/address/logic/commands/general/UndoCommand.java new file mode 100644 index 00000000000..cd06f1c7d9b --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/general/UndoCommand.java @@ -0,0 +1,33 @@ +package seedu.address.logic.commands.general; + +import static java.util.Objects.requireNonNull; + +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Represents the command to undo command changes. + */ +public class UndoCommand extends Command { + public static final String COMMAND_WORD = "undo"; + + public static final String SYNTAX = COMMAND_WORD; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Restore the previous state of addressBook"; + + public static final String MESSAGE_SUCCESS = "Command undone"; + + public static final String MESSAGE_FAIL_TO_UNDO = "No command to undo."; + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + if (!model.isUndoable()) { + throw new CommandException(MESSAGE_FAIL_TO_UNDO); + } + model.undoHistory(); + 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 index 1e466792b46..de7350994bb 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -6,15 +6,51 @@ 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.commands.contact.CAddCommand; +import seedu.address.logic.commands.contact.CClearCommand; +import seedu.address.logic.commands.contact.CDeleteCommand; +import seedu.address.logic.commands.contact.CEditCommand; +import seedu.address.logic.commands.contact.CFindCommand; +import seedu.address.logic.commands.contact.CListCommand; +import seedu.address.logic.commands.contact.CMarkCommand; +import seedu.address.logic.commands.contact.CUnmarkCommand; +import seedu.address.logic.commands.contact.CViewCommand; +import seedu.address.logic.commands.event.EAddCommand; +import seedu.address.logic.commands.event.EClearCommand; +import seedu.address.logic.commands.event.EDeleteCommand; +import seedu.address.logic.commands.event.EEditCommand; +import seedu.address.logic.commands.event.EFindCommand; +import seedu.address.logic.commands.event.ELinkCommand; +import seedu.address.logic.commands.event.EListCommand; +import seedu.address.logic.commands.event.EMarkCommand; +import seedu.address.logic.commands.event.ESortCommand; +import seedu.address.logic.commands.event.EUnlinkCommand; +import seedu.address.logic.commands.event.EUnmarkCommand; +import seedu.address.logic.commands.event.EViewCommand; +import seedu.address.logic.commands.general.CalendarCommand; +import seedu.address.logic.commands.general.ExitCommand; +import seedu.address.logic.commands.general.HelpCommand; +import seedu.address.logic.commands.general.RedoCommand; +import seedu.address.logic.commands.general.UndoCommand; +import seedu.address.logic.parser.contact.CAddCommandParser; +import seedu.address.logic.parser.contact.CDeleteCommandParser; +import seedu.address.logic.parser.contact.CEditCommandParser; +import seedu.address.logic.parser.contact.CFindCommandParser; +import seedu.address.logic.parser.contact.CListCommandParser; +import seedu.address.logic.parser.contact.CMarkCommandParser; +import seedu.address.logic.parser.contact.CUnmarkCommandParser; +import seedu.address.logic.parser.contact.CViewCommandParser; +import seedu.address.logic.parser.event.EAddCommandParser; +import seedu.address.logic.parser.event.EDeleteCommandParser; +import seedu.address.logic.parser.event.EEditCommandParser; +import seedu.address.logic.parser.event.EFindCommandParser; +import seedu.address.logic.parser.event.ELinkCommandParser; +import seedu.address.logic.parser.event.EListCommandParser; +import seedu.address.logic.parser.event.EMarkCommandParser; +import seedu.address.logic.parser.event.EUnlinkCommandParser; +import seedu.address.logic.parser.event.EUnmarkCommandParser; +import seedu.address.logic.parser.event.EViewCommandParser; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -44,23 +80,68 @@ public Command parseCommand(String userInput) throws ParseException { final String arguments = matcher.group("arguments"); switch (commandWord) { - case AddCommand.COMMAND_WORD: - return new AddCommandParser().parse(arguments); + case CAddCommand.COMMAND_WORD: + return new CAddCommandParser().parse(arguments); - case EditCommand.COMMAND_WORD: - return new EditCommandParser().parse(arguments); + case CEditCommand.COMMAND_WORD: + return new CEditCommandParser().parse(arguments); - case DeleteCommand.COMMAND_WORD: - return new DeleteCommandParser().parse(arguments); + case CDeleteCommand.COMMAND_WORD: + return new CDeleteCommandParser().parse(arguments); - case ClearCommand.COMMAND_WORD: - return new ClearCommand(); + case CClearCommand.COMMAND_WORD: + return new CClearCommand(); - case FindCommand.COMMAND_WORD: - return new FindCommandParser().parse(arguments); + case CFindCommand.COMMAND_WORD: + return new CFindCommandParser().parse(arguments); - case ListCommand.COMMAND_WORD: - return new ListCommand(); + case CListCommand.COMMAND_WORD: + return new CListCommandParser().parse(arguments); + + case CViewCommand.COMMAND_WORD: + return new CViewCommandParser().parse(arguments); + + case CMarkCommand.COMMAND_WORD: + return new CMarkCommandParser().parse(arguments); + + case CUnmarkCommand.COMMAND_WORD: + return new CUnmarkCommandParser().parse(arguments); + + case EAddCommand.COMMAND_WORD: + return new EAddCommandParser().parse(arguments); + + case EEditCommand.COMMAND_WORD: + return new EEditCommandParser().parse(arguments); + + case EDeleteCommand.COMMAND_WORD: + return new EDeleteCommandParser().parse(arguments); + + case EClearCommand.COMMAND_WORD: + return new EClearCommand(); + + case EFindCommand.COMMAND_WORD: + return new EFindCommandParser().parse(arguments); + + case EListCommand.COMMAND_WORD: + return new EListCommandParser().parse(arguments); + + case ELinkCommand.COMMAND_WORD: + return new ELinkCommandParser().parse(arguments); + + case EUnlinkCommand.COMMAND_WORD: + return new EUnlinkCommandParser().parse(arguments); + + case ESortCommand.COMMAND_WORD: + return new ESortCommand(); + + case EViewCommand.COMMAND_WORD: + return new EViewCommandParser().parse(arguments); + + case EMarkCommand.COMMAND_WORD: + return new EMarkCommandParser().parse(arguments); + + case EUnmarkCommand.COMMAND_WORD: + return new EUnmarkCommandParser().parse(arguments); case ExitCommand.COMMAND_WORD: return new ExitCommand(); @@ -68,6 +149,15 @@ public Command parseCommand(String userInput) throws ParseException { case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case CalendarCommand.COMMAND_WORD: + return new CalendarCommand(); + + case UndoCommand.COMMAND_WORD: + return new UndoCommand(); + + case RedoCommand.COMMAND_WORD: + return new RedoCommand(); + default: throw new ParseException(MESSAGE_UNKNOWN_COMMAND); } diff --git a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java b/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java index 954c8e18f8e..c0bac3f86c4 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java +++ b/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java @@ -57,4 +57,19 @@ public List getAllValues(Prefix prefix) { public String getPreamble() { return getValue(new Prefix("")).orElse(""); } + + /** + * Checks if a prefix present in this argument multimap has a non-empty value + */ + public boolean anyPrefixValueNotEmpty() { + return argMultimap.values().stream().anyMatch(ls -> ls.stream().anyMatch(str -> !str.isBlank())); + } + + /** + * Returns true if there are no prefixes in the argument multimap. + */ + public boolean noPrefixesPresent() { + return argMultimap.entrySet().stream() + .allMatch(prefixListEntry -> prefixListEntry.getKey().equals(new Prefix(""))); + } } diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..6243ec17ad1 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -11,5 +11,11 @@ public class CliSyntax { public static final Prefix PREFIX_EMAIL = new Prefix("e/"); public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); public static final Prefix PREFIX_TAG = new Prefix("t/"); - + public static final Prefix PREFIX_TELEGRAM = new Prefix("th/"); + public static final Prefix PREFIX_ZOOM = new Prefix("z/"); + public static final Prefix PREFIX_DELETE_TAG = new Prefix("dt/"); + public static final Prefix PREFIX_START_TIME = new Prefix("at/"); + public static final Prefix PREFIX_END_TIME = new Prefix("end/"); + public static final Prefix PREFIX_DESCRIPTION = new Prefix("d/"); + public static final Prefix PREFIX_CONTACT = new Prefix("c/"); } 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 index b117acb9c55..62d0bc280f3 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -1,18 +1,30 @@ package seedu.address.logic.parser; import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_INDEX; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_RANGE; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Set; +import seedu.address.commons.core.Messages; import seedu.address.commons.core.index.Index; +import seedu.address.commons.core.range.Range; 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.common.Address; +import seedu.address.model.common.Name; +import seedu.address.model.common.ZoomLink; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Phone; +import seedu.address.model.contact.TelegramHandle; +import seedu.address.model.event.DateAndTime; +import seedu.address.model.event.Description; +import seedu.address.model.event.EndDateTime; +import seedu.address.model.event.StartDateTime; import seedu.address.model.tag.Tag; /** @@ -20,7 +32,7 @@ */ 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 @@ -35,13 +47,64 @@ public static Index parseIndex(String oneBasedIndex) throws ParseException { return Index.fromOneBased(Integer.parseInt(trimmedIndex)); } + /** + * Parses parameter into a {@code Range} and returns it. Leading and trailing whitespaces will be trimmed. + * @throws ParseException if the specified range is invalid. + */ + public static Range parseRange(String range) throws ParseException { + String trimmedRange = range.trim(); + if (!StringUtil.isValidRange(trimmedRange)) { + throw new ParseException(MESSAGE_INVALID_RANGE); + } + String[] rangeArr = trimmedRange.split("-"); + Index start = parseIndex(rangeArr[0]); + Index end = parseIndex(rangeArr[1]); + return new Range(start, end); + } + + /** + * Parses parameter into a {@code Range} and returns it. Leading and trailing whitespaces will be trimmed. + * First, it will try to parse into an {@code Index}. If successful, the index will be converted to a + * Range from itself to itself. Else, it will parse into a Range. + * @throws ParseException if specified argument can neither be parsed into an Index nor a Range. + */ + public static Range parseDeleteArgument(String args) throws ParseException { + try { + Index index = parseIndex(args); + return Range.convertFromIndex(index); + } catch (ParseException pe) { + return parseRange(args); + } + } + + /** + * Parses parameter into a list of {@code Index} and returns it. Leading and trailing whitespaces will be trimmed. + * @throws ParseException if specified argument cannot be parsed into a Index. + */ + public static List parseMarkIndexes(String args) throws ParseException { + String[] stringIndexes = args.split("\\s+"); + List indexes = new ArrayList<>(); + for (String index : stringIndexes) { + index = index.trim(); + if (index.equals("")) { + continue; + } + Index parsedIndex = parseIndex(index.trim()); + if (indexes.contains(parsedIndex)) { + throw new ParseException(Messages.MESSAGE_DUPLICATE_INDEXES_PROVIDED); + } + indexes.add(parsedIndex); + } + return indexes; + } + /** * 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 { + public static Name parseContactName(String name) throws ParseException { requireNonNull(name); String trimmedName = name.trim(); if (!Name.isValidName(trimmedName)) { @@ -95,6 +158,99 @@ public static Email parseEmail(String email) throws ParseException { return new Email(trimmedEmail); } + /** + * Parses a {@code String telegramHandle} into an {@code TelegramHandle}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code telegramHandle} is invalid. + */ + public static TelegramHandle parseTelegram(String telegramHandle) throws ParseException { + requireNonNull(telegramHandle); + String trimmedTelegram = telegramHandle.trim(); + if (!TelegramHandle.isValidHandle(trimmedTelegram)) { + throw new ParseException(TelegramHandle.MESSAGE_CONSTRAINTS); + } + return new TelegramHandle(trimmedTelegram); + } + + /** + * 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 parseEventName(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 startDateTime} into a {@code StartDateTime}. + * Leading and trailing whitespaces will be trimmed. + * + * @param startDateTime when the event starts + * @throws ParseException if the given {@code startDateTime} is invalid. + */ + public static StartDateTime parseStartDateTime(String startDateTime) throws ParseException { + requireNonNull(startDateTime); + String trimmedStartDateTime = startDateTime.trim(); + if (!StartDateTime.isValidDateTime(trimmedStartDateTime)) { + throw new ParseException(DateAndTime.MESSAGE_CONSTRAINTS); + } + return new StartDateTime(trimmedStartDateTime); + } + + /** + * Parses a {@code String endDateTime} into a {@code EndDateTime}. + * Leading and trailing whitespaces will be trimmed. + * + * @param endDateTime when the event ends + * @throws ParseException if the given {@code endDateTime} is invalid. + */ + public static EndDateTime parseEndDateTime(String endDateTime) throws ParseException { + requireNonNull(endDateTime); + String trimmedEndDateTime = endDateTime.trim(); + if (!StartDateTime.isValidDateTime(trimmedEndDateTime)) { + throw new ParseException(DateAndTime.MESSAGE_CONSTRAINTS); + } + return new EndDateTime(trimmedEndDateTime); + } + + /** + * Parses a {@code String description} into a {@code Description}. + * Leading and trailing whitespaces will be trimmed. + * + */ + public static Description parseDescription(String description) throws ParseException { + requireNonNull(description); + String trimmedDescription = description.trim(); + if (!Description.isValidDescription(trimmedDescription)) { + throw new ParseException(Description.MESSAGE_CONSTRAINTS); + } + return new Description(trimmedDescription); + } + + /** + * Parses a {@code String zoomLink} into a {@code ZoomLink}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code zoomLink} is invalid. + */ + public static ZoomLink parseZoomLink(String zoomLink) throws ParseException { + requireNonNull(zoomLink); + String trimmedZoomLink = zoomLink.trim(); + if (!ZoomLink.isValidZoomLink(trimmedZoomLink)) { + throw new ParseException(ZoomLink.MESSAGE_CONSTRAINTS); + } + + return new ZoomLink(trimmedZoomLink); + + } + /** * Parses a {@code String tag} into a {@code Tag}. * Leading and trailing whitespaces will be trimmed. diff --git a/src/main/java/seedu/address/logic/parser/contact/CAddCommandParser.java b/src/main/java/seedu/address/logic/parser/contact/CAddCommandParser.java new file mode 100644 index 00000000000..cd476d26109 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/contact/CAddCommandParser.java @@ -0,0 +1,88 @@ +package seedu.address.logic.parser.contact; + +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 static seedu.address.logic.parser.CliSyntax.PREFIX_TELEGRAM; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ZOOM; + +import java.util.Set; +import java.util.stream.Stream; + +import seedu.address.logic.commands.contact.CAddCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.Prefix; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.common.Address; +import seedu.address.model.common.Name; +import seedu.address.model.common.ZoomLink; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Phone; +import seedu.address.model.contact.TelegramHandle; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new CAddCommand object + */ +public class CAddCommandParser 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 CAddCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_TELEGRAM, PREFIX_ADDRESS, PREFIX_ZOOM, PREFIX_TAG); + + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_EMAIL) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, CAddCommand.MESSAGE_USAGE)); + } + + // Compulsory fields + Name name = ParserUtil.parseContactName(argMultimap.getValue(PREFIX_NAME).get()); + Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); + + // Optional fields + Address address = null; + Phone phone = null; + TelegramHandle handle = null; + ZoomLink zoomLink = null; + if (arePrefixesPresent(argMultimap, PREFIX_ADDRESS)) { + address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); + } + if (arePrefixesPresent(argMultimap, PREFIX_PHONE)) { + phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); + } + if (arePrefixesPresent(argMultimap, PREFIX_TELEGRAM)) { + handle = ParserUtil.parseTelegram(argMultimap.getValue(PREFIX_TELEGRAM).get()); + } + if (arePrefixesPresent(argMultimap, PREFIX_ZOOM)) { + zoomLink = ParserUtil.parseZoomLink(argMultimap.getValue(PREFIX_ZOOM).get()); + } + + Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); + + Contact contact = new Contact(name, phone, email, address, zoomLink, handle, tagList); + + return new CAddCommand(contact); + } + + /** + * 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/contact/CDeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/contact/CDeleteCommandParser.java new file mode 100644 index 00000000000..b9e784b86f1 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/contact/CDeleteCommandParser.java @@ -0,0 +1,31 @@ +package seedu.address.logic.parser.contact; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.range.Range; +import seedu.address.logic.commands.contact.CDeleteCommand; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new CDeleteCommand object + */ +public class CDeleteCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the CDeleteCommand + * and returns a CDeleteCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public CDeleteCommand parse(String args) throws ParseException { + try { + Range range = ParserUtil.parseDeleteArgument(args); + return new CDeleteCommand(range); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, CDeleteCommand.MESSAGE_USAGE), pe); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/contact/CEditCommandParser.java b/src/main/java/seedu/address/logic/parser/contact/CEditCommandParser.java new file mode 100644 index 00000000000..9aecff568e2 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/contact/CEditCommandParser.java @@ -0,0 +1,101 @@ +package seedu.address.logic.parser.contact; + +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_DELETE_TAG; +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.logic.parser.CliSyntax.PREFIX_TELEGRAM; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ZOOM; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.contact.CEditCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new CEditCommand object + */ +public class CEditCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the CEditCommand + * and returns an CEditCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public CEditCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, + PREFIX_ZOOM, PREFIX_TELEGRAM, PREFIX_TAG, PREFIX_DELETE_TAG); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, CEditCommand.MESSAGE_USAGE), pe); + } + + CEditCommand.EditContactDescriptor editContactDescriptor = new CEditCommand.EditContactDescriptor(); + if (argMultimap.getValue(PREFIX_NAME).isPresent()) { + editContactDescriptor.setName(ParserUtil.parseContactName(argMultimap.getValue(PREFIX_NAME).get())); + } + if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { + editContactDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get())); + } + if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { + editContactDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get())); + } + if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { + editContactDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); + } + if (argMultimap.getValue(PREFIX_ZOOM).isPresent()) { + editContactDescriptor.setZoomLink(ParserUtil.parseZoomLink(argMultimap.getValue(PREFIX_ZOOM).get())); + } + if (argMultimap.getValue(PREFIX_TELEGRAM).isPresent()) { + editContactDescriptor.setTelegramHandle( + ParserUtil.parseTelegram(argMultimap.getValue(PREFIX_TELEGRAM).get())); + } + parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editContactDescriptor::setTags); + + // Check for delete all + if (argMultimap.getAllValues(PREFIX_DELETE_TAG).stream().anyMatch(arg -> arg.equals("*"))) { + editContactDescriptor.setShouldDeleteAllTags(true); + } + parseTagsForEdit(argMultimap.getAllValues(PREFIX_DELETE_TAG)).ifPresent(editContactDescriptor::setTagsToDelete); + + if (!editContactDescriptor.isAnyFieldEdited()) { + throw new ParseException(CEditCommand.MESSAGE_NOT_EDITED); + } + + return new CEditCommand(index, editContactDescriptor); + } + + /** + * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. + * If {@code tags} contain an asterisk, it will be ignored. + */ + private Optional> parseTagsForEdit(Collection tags) throws ParseException { + assert tags != null; + + if (tags.isEmpty()) { + return Optional.empty(); + } + // To handle case for dt/* + tags.remove("*"); + return Optional.of(ParserUtil.parseTags(tags)); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/contact/CFindCommandParser.java b/src/main/java/seedu/address/logic/parser/contact/CFindCommandParser.java new file mode 100644 index 00000000000..661c1d61773 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/contact/CFindCommandParser.java @@ -0,0 +1,104 @@ +package seedu.address.logic.parser.contact; + +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_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TELEGRAM; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ZOOM; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import seedu.address.logic.commands.contact.CFindCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.Prefix; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.contact.ContactContainsKeywordsPredicate; + +/** + * Parses input arguments and creates a new CFindCommand object + */ +public class CFindCommandParser implements Parser { + + public static final String MESSAGE_MISSING_KEYWORD = "There must be at least one keyword present for prefix '%s'."; + private ContactContainsKeywordsPredicate predicate; + + /** + * Parses the given {@code String} of arguments in the context of the CFindCommand + * and returns a CFindCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public CFindCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_TELEGRAM, PREFIX_ADDRESS, PREFIX_ZOOM, PREFIX_TAG); + + if (noPrefixesPresent(argMultimap) + && argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, CFindCommand.MESSAGE_USAGE)); + } + + predicate = new ContactContainsKeywordsPredicate(); + if (!argMultimap.getPreamble().isEmpty()) { + String[] nameKeywords = argMultimap.getPreamble().trim().split("\\s+"); + predicate = new ContactContainsKeywordsPredicate(Arrays.asList(nameKeywords)); + } + if (isPrefixValuePresent(argMultimap, PREFIX_PHONE)) { + predicate.setPhoneKeywords(getPrefixValueAndSplit(argMultimap, PREFIX_PHONE)); + } + if (isPrefixValuePresent(argMultimap, PREFIX_EMAIL)) { + predicate.setEmailKeywords(getPrefixValueAndSplit(argMultimap, PREFIX_EMAIL)); + } + if (isPrefixValuePresent(argMultimap, PREFIX_ADDRESS)) { + predicate.setAddressKeywords(getPrefixValueAndSplit(argMultimap, PREFIX_ADDRESS)); + } + if (isPrefixValuePresent(argMultimap, PREFIX_TELEGRAM)) { + predicate.setTelegramHandleKeyword(getPrefixValueAndSplit(argMultimap, PREFIX_TELEGRAM)); + } + if (isPrefixValuePresent(argMultimap, PREFIX_ZOOM)) { + predicate.setZoomLinkKeywords(getPrefixValueAndSplit(argMultimap, PREFIX_ZOOM)); + } + if (isPrefixValuePresent(argMultimap, PREFIX_TAG)) { + predicate.setTagKeywords(getPrefixValueAndSplit(argMultimap, PREFIX_TAG)); + } + return new CFindCommand(predicate); + } + + /** + * Returns true if none of the prefixes contains present {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean noPrefixesPresent(ArgumentMultimap argumentMultimap) { + return Stream.of(PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_TELEGRAM, PREFIX_ADDRESS, PREFIX_ZOOM, PREFIX_TAG) + .allMatch(prefix -> argumentMultimap.getValue(prefix).isEmpty()); + } + + /** + * Returns true if the given prefix value is present + * + * @param prefix the given prefix + * @return true if the value of the prefix is not empty, false otherwise + */ + private static boolean isPrefixValuePresent(ArgumentMultimap argumentMultimap, Prefix prefix) { + return argumentMultimap.getValue(prefix).isPresent(); + } + + private static List getPrefixValueAndSplit(ArgumentMultimap argumentMultimap, Prefix prefix) + throws ParseException { + requireNonNull(argumentMultimap.getValue(prefix)); //the prefix must not contain an empty value + String value = argumentMultimap.getValue(prefix).get(); + if (value.isEmpty()) { + throw new ParseException(String.format(MESSAGE_MISSING_KEYWORD, prefix)); + } + return Stream.of(value.split("\\s+")).collect(Collectors.toList()); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/contact/CListCommandParser.java b/src/main/java/seedu/address/logic/parser/contact/CListCommandParser.java new file mode 100644 index 00000000000..83207d96af8 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/contact/CListCommandParser.java @@ -0,0 +1,46 @@ +package seedu.address.logic.parser.contact; + +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_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TELEGRAM; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ZOOM; + +import seedu.address.logic.commands.contact.CListCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.contact.ContactDisplaySetting; + +public class CListCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the CListCommand + * and returns a CListCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public CListCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_TELEGRAM, PREFIX_ADDRESS, PREFIX_ZOOM, PREFIX_TAG); + if (argMultimap.anyPrefixValueNotEmpty() || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, CListCommand.MESSAGE_USAGE)); + } + ContactDisplaySetting displaySetting; + if (argMultimap.noPrefixesPresent()) { + displaySetting = ContactDisplaySetting.DEFAULT_SETTING; + } else { + displaySetting = new ContactDisplaySetting( + argMultimap.getValue(PREFIX_PHONE).isPresent(), + argMultimap.getValue(PREFIX_EMAIL).isPresent(), + argMultimap.getValue(PREFIX_TELEGRAM).isPresent(), + argMultimap.getValue(PREFIX_ADDRESS).isPresent(), + argMultimap.getValue(PREFIX_ZOOM).isPresent(), + argMultimap.getValue(PREFIX_TAG).isPresent() + ); + } + return new CListCommand(displaySetting); + } +} diff --git a/src/main/java/seedu/address/logic/parser/contact/CMarkCommandParser.java b/src/main/java/seedu/address/logic/parser/contact/CMarkCommandParser.java new file mode 100644 index 00000000000..a3441cdff8f --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/contact/CMarkCommandParser.java @@ -0,0 +1,32 @@ +package seedu.address.logic.parser.contact; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.contact.CMarkCommand; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +public class CMarkCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the CMarkCommand + * and returns a CMarkCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public CMarkCommand parse(String args) throws ParseException { + if (args.trim().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, CMarkCommand.MESSAGE_USAGE)); + } + try { + List indexes = ParserUtil.parseMarkIndexes(args); + return new CMarkCommand(indexes); + } catch (ParseException pe) { + throw new ParseException( + String.format(pe.getMessage(), CMarkCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/contact/CUnmarkCommandParser.java b/src/main/java/seedu/address/logic/parser/contact/CUnmarkCommandParser.java new file mode 100644 index 00000000000..5d6a975cd18 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/contact/CUnmarkCommandParser.java @@ -0,0 +1,32 @@ +package seedu.address.logic.parser.contact; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.contact.CUnmarkCommand; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +public class CUnmarkCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the CUnmarkCommand + * and returns a CUnmarkCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public CUnmarkCommand parse(String args) throws ParseException { + if (args.trim().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, CUnmarkCommand.MESSAGE_USAGE)); + } + try { + List indexes = ParserUtil.parseMarkIndexes(args); + return new CUnmarkCommand(indexes); + } catch (ParseException pe) { + throw new ParseException( + String.format(pe.getMessage(), CUnmarkCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/contact/CViewCommandParser.java b/src/main/java/seedu/address/logic/parser/contact/CViewCommandParser.java new file mode 100644 index 00000000000..339effd3c06 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/contact/CViewCommandParser.java @@ -0,0 +1,27 @@ +package seedu.address.logic.parser.contact; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.contact.CViewCommand; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +public class CViewCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the CViewCommand + * and returns an CViewCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + @Override + public CViewCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new CViewCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, CViewCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/event/EAddCommandParser.java b/src/main/java/seedu/address/logic/parser/event/EAddCommandParser.java new file mode 100644 index 00000000000..4a0d715b742 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/event/EAddCommandParser.java @@ -0,0 +1,88 @@ +package seedu.address.logic.parser.event; + + +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_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_END_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_START_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ZOOM; + +import java.util.Set; +import java.util.stream.Stream; + +import seedu.address.logic.commands.event.EAddCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.Prefix; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.common.Address; +import seedu.address.model.common.Name; +import seedu.address.model.common.ZoomLink; +import seedu.address.model.event.Description; +import seedu.address.model.event.EndDateTime; +import seedu.address.model.event.Event; +import seedu.address.model.event.StartDateTime; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new CAddCommand object + */ +public class EAddCommandParser 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 EAddCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_START_TIME, PREFIX_END_TIME, PREFIX_DESCRIPTION, + PREFIX_ADDRESS, PREFIX_ZOOM, PREFIX_TAG); + + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_START_TIME) || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EAddCommand.MESSAGE_USAGE)); + } + + // Compulsory fields + Name name = ParserUtil.parseEventName(argMultimap.getValue(PREFIX_NAME).get()); + StartDateTime start = ParserUtil.parseStartDateTime(argMultimap.getValue(PREFIX_START_TIME).get()); + + // Optional fields + EndDateTime end = null; + Description description = null; + Address address = null; + ZoomLink zoom = null; + if (arePrefixesPresent(argMultimap, PREFIX_END_TIME)) { + end = ParserUtil.parseEndDateTime(argMultimap.getValue(PREFIX_END_TIME).get()); + } + if (arePrefixesPresent(argMultimap, PREFIX_DESCRIPTION)) { + description = ParserUtil.parseDescription(argMultimap.getValue(PREFIX_DESCRIPTION).get()); + } + if (arePrefixesPresent(argMultimap, PREFIX_ADDRESS)) { + address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); + } + if (arePrefixesPresent(argMultimap, PREFIX_ZOOM)) { + zoom = ParserUtil.parseZoomLink(argMultimap.getValue(PREFIX_ZOOM).get()); + } + + Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); + + Event event = new Event(name, start, end, description, address, zoom, tagList); + + return new EAddCommand(event); + } + + /** + * 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/event/EDeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/event/EDeleteCommandParser.java new file mode 100644 index 00000000000..d902ed09bf5 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/event/EDeleteCommandParser.java @@ -0,0 +1,31 @@ +package seedu.address.logic.parser.event; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.range.Range; +import seedu.address.logic.commands.event.EDeleteCommand; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new EDeleteCommand object + */ +public class EDeleteCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the EDeleteCommand + * and returns a EDeleteCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public EDeleteCommand parse(String args) throws ParseException { + try { + Range range = ParserUtil.parseDeleteArgument(args); + return new EDeleteCommand(range); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, EDeleteCommand.MESSAGE_USAGE), pe); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/event/EEditCommandParser.java b/src/main/java/seedu/address/logic/parser/event/EEditCommandParser.java new file mode 100644 index 00000000000..ad19d1ab510 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/event/EEditCommandParser.java @@ -0,0 +1,103 @@ +package seedu.address.logic.parser.event; + +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_DELETE_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_END_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_START_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ZOOM; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.event.EEditCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new CEditCommand object + */ +public class EEditCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the EEditCommand + * and returns an EEditCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public EEditCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_START_TIME, PREFIX_END_TIME, PREFIX_DESCRIPTION, + PREFIX_ADDRESS, PREFIX_ZOOM, PREFIX_TAG, PREFIX_DELETE_TAG); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EEditCommand.MESSAGE_USAGE), pe); + } + + EEditCommand.EditEventDescriptor editEventDescriptor = new EEditCommand.EditEventDescriptor(); + if (argMultimap.getValue(PREFIX_NAME).isPresent()) { + editEventDescriptor.setName(ParserUtil.parseEventName(argMultimap.getValue(PREFIX_NAME).get())); + } + if (argMultimap.getValue(PREFIX_START_TIME).isPresent()) { + editEventDescriptor.setStartDateTime(ParserUtil + .parseStartDateTime(argMultimap.getValue(PREFIX_START_TIME).get())); + } + if (argMultimap.getValue(PREFIX_END_TIME).isPresent()) { + editEventDescriptor.setEndDateTime(ParserUtil + .parseEndDateTime(argMultimap.getValue(PREFIX_END_TIME).get())); + } + if (argMultimap.getValue(PREFIX_DESCRIPTION).isPresent()) { + editEventDescriptor.setDescription(ParserUtil + .parseDescription(argMultimap.getValue(PREFIX_DESCRIPTION).get())); + } + if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { + editEventDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); + } + if (argMultimap.getValue(PREFIX_ZOOM).isPresent()) { + editEventDescriptor.setZoomLink(ParserUtil.parseZoomLink(argMultimap.getValue(PREFIX_ZOOM).get())); + } + parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editEventDescriptor::setTags); + + // Check for delete all + if (argMultimap.getAllValues(PREFIX_DELETE_TAG).stream().anyMatch(arg -> arg.equals("*"))) { + editEventDescriptor.setShouldDeleteAllTags(true); + } + parseTagsForEdit(argMultimap.getAllValues(PREFIX_DELETE_TAG)).ifPresent(editEventDescriptor::setTagsToDelete); + + if (!editEventDescriptor.isAnyFieldEdited()) { + throw new ParseException(EEditCommand.MESSAGE_NOT_EDITED); + } + + return new EEditCommand(index, editEventDescriptor); + } + + /** + * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. + * If {@code tags} contain an asterisk, it will be ignored. + */ + private Optional> parseTagsForEdit(Collection tags) throws ParseException { + assert tags != null; + + if (tags.isEmpty()) { + return Optional.empty(); + } + // To handle case for dt/* + tags.remove("*"); + return Optional.of(ParserUtil.parseTags(tags)); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/event/EFindCommandParser.java b/src/main/java/seedu/address/logic/parser/event/EFindCommandParser.java new file mode 100644 index 00000000000..519fc830229 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/event/EFindCommandParser.java @@ -0,0 +1,102 @@ +package seedu.address.logic.parser.event; + +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_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_END_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_START_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ZOOM; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import seedu.address.logic.commands.event.EFindCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.Prefix; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.event.EventContainsKeywordsPredicate; + +/** + * Parses input arguments and creates a new EFindCommand object + */ +public class EFindCommandParser implements Parser { + + public static final String MESSAGE_MISSING_KEYWORD = "There must be at least one keyword present for prefix '%s'."; + private EventContainsKeywordsPredicate predicate; + + /** + * Parses the given {@code String} of arguments in the context of the EFindCommand + * and returns a EFindCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public EFindCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_START_TIME, PREFIX_END_TIME, + PREFIX_DESCRIPTION, PREFIX_ADDRESS, PREFIX_ZOOM, PREFIX_TAG); + + if (noPrefixesPresent(argMultimap) + && argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EFindCommand.MESSAGE_USAGE)); + } + + predicate = new EventContainsKeywordsPredicate(); + if (!argMultimap.getPreamble().isEmpty()) { + String[] nameKeywords = argMultimap.getPreamble().trim().split("\\s+"); + predicate = new EventContainsKeywordsPredicate(Arrays.asList(nameKeywords)); + } + if (isPrefixValuePresent(argMultimap, PREFIX_START_TIME)) { + predicate.setStartDateTimeKeywords(getPrefixValueAndSplit(argMultimap, PREFIX_START_TIME)); + } + if (isPrefixValuePresent(argMultimap, PREFIX_END_TIME)) { + predicate.setEndDateTimeKeywords(getPrefixValueAndSplit(argMultimap, PREFIX_END_TIME)); + } + if (isPrefixValuePresent(argMultimap, PREFIX_ADDRESS)) { + predicate.setAddressKeywords(getPrefixValueAndSplit(argMultimap, PREFIX_ADDRESS)); + } + if (isPrefixValuePresent(argMultimap, PREFIX_DESCRIPTION)) { + predicate.setDescriptionKeywords(getPrefixValueAndSplit(argMultimap, PREFIX_DESCRIPTION)); + } + if (isPrefixValuePresent(argMultimap, PREFIX_ZOOM)) { + predicate.setZoomLinkKeywords(getPrefixValueAndSplit(argMultimap, PREFIX_ZOOM)); + } + if (isPrefixValuePresent(argMultimap, PREFIX_TAG)) { + predicate.setTagKeywords(getPrefixValueAndSplit(argMultimap, PREFIX_TAG)); + } + return new EFindCommand(predicate); + } + /** + * Returns true if none of the prefixes contains present {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean noPrefixesPresent(ArgumentMultimap argumentMultimap) { + return Stream.of(PREFIX_START_TIME, PREFIX_END_TIME, + PREFIX_ADDRESS, PREFIX_DESCRIPTION, PREFIX_ZOOM, PREFIX_TAG) + .allMatch(prefix -> argumentMultimap.getValue(prefix).isEmpty()); + } + /** + * Returns true if the given prefix value is present + * + * @param prefix the given prefix + * @return true if the value of the prefix is not empty, false otherwise + */ + private static boolean isPrefixValuePresent(ArgumentMultimap argumentMultimap, Prefix prefix) { + return argumentMultimap.getValue(prefix).isPresent(); + } + + private static List getPrefixValueAndSplit(ArgumentMultimap argumentMultimap, Prefix prefix) + throws ParseException { + requireNonNull(argumentMultimap.getValue(prefix)); //the prefix must not contain an empty value + String value = argumentMultimap.getValue(prefix).get(); + if (value.isEmpty()) { + throw new ParseException(String.format(MESSAGE_MISSING_KEYWORD, prefix)); + } + return Stream.of(value.split("\\s+")).collect(Collectors.toList()); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/event/ELinkCommandParser.java b/src/main/java/seedu/address/logic/parser/event/ELinkCommandParser.java new file mode 100644 index 00000000000..88660fc9c6a --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/event/ELinkCommandParser.java @@ -0,0 +1,60 @@ +package seedu.address.logic.parser.event; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_CONTACT; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.event.ELinkCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.Prefix; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ELinkCommand object + */ +public class ELinkCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the ELinkCommand + * and returns an ELinkCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ELinkCommand parse(String args) throws ParseException { + + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_CONTACT); + if (!arePrefixesPresent(argMultimap, PREFIX_CONTACT) + || argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ELinkCommand.MESSAGE_USAGE)); + } + String index = argMultimap.getPreamble(); + try { + Index eventIndex = ParserUtil.parseIndex(index); + List listOfValues = argMultimap.getAllValues(PREFIX_CONTACT); + Set contactIndexes = new HashSet<>(); + for (String value : listOfValues) { + contactIndexes.add(ParserUtil.parseIndex(value)); + } + return new ELinkCommand(eventIndex, contactIndexes); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ELinkCommand.MESSAGE_USAGE), pe); + } + } + + /** + * 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/event/EListCommandParser.java b/src/main/java/seedu/address/logic/parser/event/EListCommandParser.java new file mode 100644 index 00000000000..b9c9d94d1a1 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/event/EListCommandParser.java @@ -0,0 +1,50 @@ +package seedu.address.logic.parser.event; + +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_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_END_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_START_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ZOOM; + +import seedu.address.logic.commands.event.EListCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.event.EventDisplaySetting; + +/** + * Parses input arguments and creates a new EListCommand object + */ +public class EListCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the EListCommand + * and returns an EListCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public EListCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_START_TIME, PREFIX_END_TIME, PREFIX_DESCRIPTION, + PREFIX_ADDRESS, PREFIX_ZOOM, PREFIX_TAG); + if (argMultimap.anyPrefixValueNotEmpty() || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EListCommand.MESSAGE_USAGE)); + } + EventDisplaySetting displaySetting; + if (argMultimap.noPrefixesPresent()) { + displaySetting = EventDisplaySetting.DEFAULT_SETTING; + } else { + displaySetting = new EventDisplaySetting( + argMultimap.getValue(PREFIX_START_TIME).isPresent(), + argMultimap.getValue(PREFIX_END_TIME).isPresent(), + argMultimap.getValue(PREFIX_DESCRIPTION).isPresent(), + argMultimap.getValue(PREFIX_ADDRESS).isPresent(), + argMultimap.getValue(PREFIX_ZOOM).isPresent(), + argMultimap.getValue(PREFIX_TAG).isPresent() + ); + } + return new EListCommand(displaySetting); + } +} diff --git a/src/main/java/seedu/address/logic/parser/event/EMarkCommandParser.java b/src/main/java/seedu/address/logic/parser/event/EMarkCommandParser.java new file mode 100644 index 00000000000..9f723d10908 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/event/EMarkCommandParser.java @@ -0,0 +1,27 @@ +package seedu.address.logic.parser.event; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.event.EMarkCommand; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +public class EMarkCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the EMarkCommand + * and returns a EMarkCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public EMarkCommand parse(String args) throws ParseException { + if (args.trim().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EMarkCommand.MESSAGE_USAGE)); + } + List indexes = ParserUtil.parseMarkIndexes(args); + return new EMarkCommand(indexes); + } +} diff --git a/src/main/java/seedu/address/logic/parser/event/EUnlinkCommandParser.java b/src/main/java/seedu/address/logic/parser/event/EUnlinkCommandParser.java new file mode 100644 index 00000000000..7c0904bd6fd --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/event/EUnlinkCommandParser.java @@ -0,0 +1,63 @@ +package seedu.address.logic.parser.event; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_CONTACT; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.event.EUnlinkCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.Prefix; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new EUnlinkCommand object + */ +public class EUnlinkCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the EUnlinkCommand + * and returns an EUnlinkCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public EUnlinkCommand parse(String args) throws ParseException { + + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_CONTACT); + if (!arePrefixesPresent(argMultimap, PREFIX_CONTACT) + || argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EUnlinkCommand.MESSAGE_USAGE)); + } + String index = argMultimap.getPreamble(); + try { + Index eventIndex = ParserUtil.parseIndex(index); + List listOfValues = argMultimap.getAllValues(PREFIX_CONTACT); + Set contactIndexes = new HashSet<>(); + for (String value : listOfValues) { + if (value.equals("*")) { // delete all links + return new EUnlinkCommand(eventIndex, Set.of(), true); + } + contactIndexes.add(ParserUtil.parseIndex(value)); + } + return new EUnlinkCommand(eventIndex, contactIndexes, false); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, EUnlinkCommand.MESSAGE_USAGE), pe); + } + } + + /** + * 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/event/EUnmarkCommandParser.java b/src/main/java/seedu/address/logic/parser/event/EUnmarkCommandParser.java new file mode 100644 index 00000000000..502237f734c --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/event/EUnmarkCommandParser.java @@ -0,0 +1,32 @@ +package seedu.address.logic.parser.event; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.event.EUnmarkCommand; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +public class EUnmarkCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the EUnmarkCommand + * and returns a EUnmarkCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public EUnmarkCommand parse(String args) throws ParseException { + if (args.trim().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EUnmarkCommand.MESSAGE_USAGE)); + } + try { + List indexes = ParserUtil.parseMarkIndexes(args); + return new EUnmarkCommand(indexes); + } catch (ParseException pe) { + throw new ParseException( + String.format(pe.getMessage(), EUnmarkCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/event/EViewCommandParser.java similarity index 54% rename from src/main/java/seedu/address/logic/parser/DeleteCommandParser.java rename to src/main/java/seedu/address/logic/parser/event/EViewCommandParser.java index 522b93081cc..fce195007ea 100644 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/event/EViewCommandParser.java @@ -1,29 +1,27 @@ -package seedu.address.logic.parser; +package seedu.address.logic.parser.event; 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.commands.event.EViewCommand; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; import seedu.address.logic.parser.exceptions.ParseException; -/** - * Parses input arguments and creates a new DeleteCommand object - */ -public class DeleteCommandParser implements Parser { - +public class EViewCommandParser implements Parser { /** - * Parses the given {@code String} of arguments in the context of the DeleteCommand - * and returns a DeleteCommand object for execution. + * Parses the given {@code String} of arguments in the context of the EViewCommand + * and returns an EViewCommand object for execution. * @throws ParseException if the user input does not conform the expected format */ - public DeleteCommand parse(String args) throws ParseException { + @Override + public EViewCommand parse(String args) throws ParseException { try { Index index = ParserUtil.parseIndex(args); - return new DeleteCommand(index); + return new EViewCommand(index); } catch (ParseException pe) { throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); + String.format(MESSAGE_INVALID_COMMAND_FORMAT, EViewCommand.MESSAGE_USAGE), pe); } } - } diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 1a943a0781a..14789f2a9fd 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -3,18 +3,24 @@ import static java.util.Objects.requireNonNull; import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; import javafx.collections.ObservableList; -import seedu.address.model.person.Person; -import seedu.address.model.person.UniquePersonList; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.UniqueContactList; +import seedu.address.model.event.Event; +import seedu.address.model.event.UniqueEventList; /** * Wraps all data at the address-book level - * Duplicates are not allowed (by .isSamePerson comparison) + * Duplicates are not allowed (by .isSameContact comparison) */ public class AddressBook implements ReadOnlyAddressBook { - private final UniquePersonList persons; + private final UniqueContactList contacts; + private final UniqueEventList events; /* * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication @@ -24,27 +30,56 @@ public class AddressBook implements ReadOnlyAddressBook { * among constructors. */ { - persons = new UniquePersonList(); + contacts = new UniqueContactList(); + events = new UniqueEventList(); } - public AddressBook() {} + public AddressBook() { + } /** - * Creates an AddressBook using the Persons in the {@code toBeCopied} + * Creates an AddressBook using the Contacts and Events in the {@code toBeCopied} */ public AddressBook(ReadOnlyAddressBook toBeCopied) { this(); resetData(toBeCopied); } + /** + * Create a copy of the current addressBook + * + * @return a copy of current addressBook + */ + public AddressBook copy() { + AddressBook toCopy = new AddressBook(); + toCopy.setContacts(contacts.copy()); + toCopy.setEvents(events.copy()); + return toCopy; + } + //// list overwrite operations /** - * Replaces the contents of the person list with {@code persons}. - * {@code persons} must not contain duplicate persons. + * Replaces the contents of the contact list with {@code contacts}. + * {@code contacts} must not contain duplicate contacts. + */ + public void setContacts(List contacts) { + this.contacts.setContacts(contacts); + } + + /** + * Replaces the contents of the event list with {@code events}. + * {@code events} must not contain duplicate events. */ - public void setPersons(List persons) { - this.persons.setPersons(persons); + public void setEvents(List events) { + this.events.setEvents(events); + } + + /** + * Resets the existing data of contacts of this {@code AddressBook}. + */ + public void resetContacts() { + this.contacts.resetContacts(); } /** @@ -53,68 +88,209 @@ public void setPersons(List persons) { public void resetData(ReadOnlyAddressBook newData) { requireNonNull(newData); - setPersons(newData.getPersonList()); + setContacts(newData.getContactList()); + setEvents(newData.getEventList()); + } + + /** + * Places marked {@code contacts} at the top of the list. + * Places the newly marked contacts or replaces newly unmarked contacts + * in the order specified in {@code contactsToRearrange} if specified. + */ + public void rearrangeContactsInOrder(List contactsToRearrange, boolean isMarked) { + contacts.rearrangeContactsInOrder(contactsToRearrange, isMarked); + } + + //// event-level operations + + /** + * Returns true if an event with the same name as {@code event} exists in the address book. + */ + public boolean hasEvent(Event event) { + requireNonNull(event); + return events.contains(event); + } + + /** + * Adds an event to the address book. + * The event must not already exist in the address book. + */ + public void addEvent(Event e) { + events.add(e); + Event.addToMap(e); + } + + /** + * Replaces the given event {@code target} in the list with {@code editedEvent}. + * {@code target} must exist in the address book. + * The event name of {@code editedEvent} must not be the same as another existing event in the address book. + */ + public void setEvent(Event target, Event editedEvent) { + requireNonNull(editedEvent); + + events.setEvent(target, editedEvent); + } + + /** + * Sorts the filtered event list to show all upcoming events. This will change the order of the filtered list + * and remove any events which have concluded. + */ + public void sortEvents() { + events.sortEvents(); + } + + /** + * Removes {@code key} from this {@code AddressBook}. + * {@code key} must exist in the address book. + */ + public void removeEvent(Event key) { + // unlink all the contacts linked to event before removing event + Event updatedEvent = unlinkContactsFromEvent(key); + events.remove(updatedEvent); } - //// person-level operations + /** + * Creates a link between the event and contact. + */ + public void linkEventAndContact(Event event, Contact contact) { + Event updatedEvent = event.linkTo(contact); + Contact updatedContact = contact.linkTo(event); + setEvent(event, updatedEvent); + setContact(contact, updatedContact); + } /** - * Returns true if a person with the same identity as {@code person} exists in the address book. + * Removes the link between the event and contact. */ - public boolean hasPerson(Person person) { - requireNonNull(person); - return persons.contains(person); + public void unlinkEventAndContact(Event event, Contact contact) { + Event updatedEvent = event.unlink(contact); + Contact updatedContact = contact.unlink(event); + setEvent(event, updatedEvent); + setContact(contact, updatedContact); } /** - * Adds a person to the address book. - * The person must not already exist in the address book. + * Unlink all the contacts linked to the given event {@code e}, but does not remove the stored links in the event. */ - public void addPerson(Person p) { - persons.add(p); + public Event unlinkContactsFromEvent(Event e) { + Set contactsUuid = e.getLinkedContacts(); + contactsUuid.iterator() + .forEachRemaining(contactUuid -> { + Contact linkedContact = Contact.findByUuid(contactUuid); + setContact(linkedContact, linkedContact.unlink(e)); + }); + Event updatedEvent = e.clearAllLinks(); + setEvent(e, updatedEvent); + return updatedEvent; } /** - * Replaces the given person {@code target} in the list with {@code editedPerson}. + * Resets the existing data of events of this {@code AddressBook}. + */ + public void resetEvents() { + this.events.resetEvents(); + } + + /** + * Places marked {@code events} at the top of the list. + * Places the newly marked events or replaces newly unmarked events + * in the order specified in {@code eventsToRearrange} if specified. + */ + public void rearrangeEventsInOrder(List eventsToRearrange, boolean isMarked) { + events.rearrangeEventsInOrder(eventsToRearrange, isMarked); + } + + //// contact-level operations + + /** + * Returns true if a contact with the same identity as {@code contact} exists in the address book. + */ + public boolean hasContact(Contact contact) { + requireNonNull(contact); + return contacts.contains(contact); + } + + /** + * Adds a contact to the address book. + * The contact must not already exist in the address book. + */ + public void addContact(Contact c) { + contacts.add(c); + Contact.addToMap(c); + } + + /** + * Replaces the given contact {@code target} in the list with {@code editedContact}. * {@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. + * The contact name of {@code editedContact} must not be the same as another existing contact in the address book. */ - public void setPerson(Person target, Person editedPerson) { - requireNonNull(editedPerson); + public void setContact(Contact target, Contact editedContact) { + requireNonNull(editedContact); - persons.setPerson(target, editedPerson); + contacts.setContact(target, editedContact); } /** * Removes {@code key} from this {@code AddressBook}. * {@code key} must exist in the address book. */ - public void removePerson(Person key) { - persons.remove(key); + public void removeContact(Contact key) { + // unlink all the events linked to contact before removing contact + Contact updatedContact = unlinkEventsFromContact(key); + contacts.remove(updatedContact); + } + + /** + * Unlink all the events linked to the given contact {@code c}, but does not remove the stored links in the contact. + */ + public Contact unlinkEventsFromContact(Contact c) { + Set eventsUuid = c.getLinkedEvents(); + eventsUuid.iterator() + .forEachRemaining(eventUuid -> { + Event linkedEvent = Event.findByUuid(eventUuid); + setEvent(linkedEvent, linkedEvent.unlink(c)); + }); + Contact updatedContact = c.clearAllLinks(); + setContact(c, updatedContact); + return updatedContact; + } + + /** + * Update the list of contacts and list of events in their respective UUID map. + */ + public void updateDataMaps() { + contacts.updateContactMap(); + events.updateEventMap(); } //// util methods @Override public String toString() { - return persons.asUnmodifiableObservableList().size() + " persons"; - // TODO: refine later + return contacts.asUnmodifiableObservableList().size() + " contacts\n" + + events.asUnmodifiableObservableList().size() + " events"; + } + + @Override + public ObservableList getContactList() { + return contacts.asUnmodifiableObservableList(); } @Override - public ObservableList getPersonList() { - return persons.asUnmodifiableObservableList(); + public ObservableList getEventList() { + return events.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)); + || (other instanceof AddressBook // instanceof handles nulls + && contacts.equals(((AddressBook) other).contacts) + && events.equals(((AddressBook) other).events)); } @Override public int hashCode() { - return persons.hashCode(); + return Objects.hash(contacts, events); } } diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..72d29638d20 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -1,18 +1,26 @@ package seedu.address.model; import java.nio.file.Path; +import java.util.List; import java.util.function.Predicate; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; -import seedu.address.model.person.Person; +import seedu.address.commons.core.index.Index; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.ContactDisplaySetting; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventDisplaySetting; /** * The API of the Model component. */ public interface Model { /** {@code Predicate} that always evaluate to true */ - Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; + Predicate PREDICATE_SHOW_ALL_CONTACTS = unused -> true; + Predicate PREDICATE_HIDE_ALL_CONTACTS = unused -> false; + Predicate PREDICATE_SHOW_ALL_EVENTS = unused -> true; + Predicate PREDICATE_HIDE_ALL_EVENTS = unused -> false; /** * Replaces user prefs data with the data in {@code userPrefs}. @@ -34,6 +42,26 @@ public interface Model { */ void setGuiSettings(GuiSettings guiSettings); + /** + * Returns the display settings of the events. + */ + EventDisplaySetting getEventDisplaySetting(); + + /** + * Sets the display settings of the events. + */ + void setEventDisplaySetting(EventDisplaySetting eventDisplaySetting); + + /** + * Returns the display settings of the contacts. + */ + ContactDisplaySetting getContactDisplaySetting(); + + /** + * Sets the display settings of the contacts. + */ + void setContactDisplaySetting(ContactDisplaySetting displaySetting); + /** * Returns the user prefs' address book file path. */ @@ -49,39 +77,176 @@ public interface Model { */ void setAddressBook(ReadOnlyAddressBook addressBook); - /** Returns the AddressBook */ + /** Returns the current AddressBook */ ReadOnlyAddressBook getAddressBook(); + /** Adds new state of AddressBook to its history list */ + void commitHistory(); + + /** Restores the previous addressBook state from its history */ + void undoHistory(); + + /** Restores the previously undone state from its history */ + void redoHistory(); + + /** Checks if the current state of addressBook is undoable */ + boolean isUndoable(); + + /** Check if the current state of addressBook is redoable */ + boolean isRedoable(); + + //=========== Contact Management ============================================================= + + /** + * Returns true if a contact with the same identity as {@code contact} exists in the address book. + */ + boolean hasContact(Contact contact); + + /** + * Deletes the given contact. + * The contact must exist in the address book. + */ + void deleteContact(Contact target); + + /** + * Adds the given contact. + * {@code contact} must not already exist in the address book. + */ + void addContact(Contact contact); + + /** + * Replaces the given contact {@code target} with {@code editedContact}. + * {@code target} must exist in the address book. + * The contact name of {@code editedContact} must not be the same as another existing contact in the address book. + */ + void setContact(Contact target, Contact editedContact); + + /** + * Remove all contacts from SoConnect. + */ + void resetContacts(); + + /** Returns an unmodifiable view of the filtered contact list */ + ObservableList getFilteredContactList(); + + /** + * Updates the filter of the filtered contact list to filter by the given {@code predicate}. + * @throws NullPointerException if {@code predicate} is null. + */ + void updateFilteredContactList(Predicate predicate); + /** - * Returns true if a person with the same identity as {@code person} exists in the address book. + * This will change the order of the filtered list, marked contacts will be placed at the top of the list. + * Places the newly marked contacts or replaces unmarked contacts + * in the order specified in {@code indexes} and depending on {@code isMark}. */ - boolean hasPerson(Person person); + void rearrangeContactsInOrder(List contacts, boolean isMark); /** - * Deletes the given person. - * The person must exist in the address book. + * Updates the filter of the filtered contact list to show the contact at {@code index}. + * @throws NullPointerException if {@code index} is null. */ - void deletePerson(Person target); + void updateContactListByIndex(Index index); + + //=========== Event Management ============================================================= + + /** + * Returns true if an event with the same name as {@code event} exists in the address book. + */ + boolean hasEvent(Event event); + + /** + * Deletes the given event. + * The event must exist in the address book. + */ + void deleteEvent(Event target); /** - * Adds the given person. - * {@code person} must not already exist in the address book. + * Adds the given event. + * {@code event} must not already exist in the address book. */ - void addPerson(Person person); + void addEvent(Event event); /** - * Replaces the given person {@code target} with {@code editedPerson}. + * Replaces the given event {@code target} with {@code editedEvent}. * {@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. + * The event name of {@code editedEvent} must not be the same as another existing event in the address book. */ - void setPerson(Person target, Person editedPerson); + void setEvent(Event target, Event editedEvent); + + /** Returns an unmodifiable view of the filtered event list */ + ObservableList getFilteredEventList(); - /** Returns an unmodifiable view of the filtered person list */ - ObservableList getFilteredPersonList(); + /** + * Removes all events from SoConnect. + */ + void resetEvents(); /** - * Updates the filter of the filtered person list to filter by the given {@code predicate}. + * Updates the filter of the filtered event list to filter by the given {@code predicate}. * @throws NullPointerException if {@code predicate} is null. */ - void updateFilteredPersonList(Predicate predicate); + void updateFilteredEventList(Predicate predicate); + + /** + * Links an event to a contact. Both the event and contact will have reference to each other. + * @param event The event to link to contact. + * @param contact The contact to link to event. + */ + void linkEventAndContact(Event event, Contact contact); + + /** + * Unlinks an event to a contact. Both references between the contact and the event will be removed. + * @param event The event to unlink from contact. + * @param contact The contact to unlink from event. + */ + void unlinkEventAndContact(Event event, Contact contact); + + /** Unlinks an {@code event} from all its linked contacts. Both references between the contact and the event + * will be removed. + */ + void unlinkAllContactsFromEvent(Event event); + + /** + * Sorts the filtered event list to show all upcoming events. This will change the order of the filtered list + * and remove any events which have concluded. + */ + void sortUpcomingFilteredEventList(); + + /** + * Updates the filter of the filtered event list to show the event at {@code index}. + * @throws NullPointerException if {@code index} is null. + */ + void updateEventListByIndex(Index index); + + /** + * Re-render contact cards in UI to show the most updated version. + * @param useBackSamePredicate whether the same predicate should be refreshed. + * Otherwise, the filter will be set to all contacts. + */ + void rerenderContactCards(boolean useBackSamePredicate); + + /** + * Re-render event cards in UI to show the most updated version. + * @param useBackSamePredicate whether the same predicate should be refreshed. + * Otherwise, the filter will be set to all contacts. + */ + void rerenderEventCards(boolean useBackSamePredicate); + + /** + * Re-render both contact and event cards in UI to show the most updated version. + */ + void rerenderAllCards(); + + /** + * This will change the order of the filtered list, marked events will be placed at the top of the list. + * Places the newly marked events or replaces unmarked events + * in the order specified in {@code indexes} and depending on {@code isMark}. + */ + void rearrangeEventsInOrder(List events, boolean isMark); + + /** + * Removes all links between contacts and events. + */ + void removeAllLinks(); } diff --git a/src/main/java/seedu/address/model/ModelDisplaySetting.java b/src/main/java/seedu/address/model/ModelDisplaySetting.java new file mode 100644 index 00000000000..acf675f615d --- /dev/null +++ b/src/main/java/seedu/address/model/ModelDisplaySetting.java @@ -0,0 +1,134 @@ +package seedu.address.model; + +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_CONTACTS; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_EVENTS; + +import java.util.function.Predicate; + +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.ContactDisplaySetting; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventDisplaySetting; + +/** + * Keep track of display setting of each model + */ +public class ModelDisplaySetting { + private final ContactDisplaySetting contactDisplaySetting; + private final EventDisplaySetting eventDisplaySetting; + private final Predicate contactDisplayPredicate; + private final Predicate eventDisplayPredicate; + + /** + * Constructor of a default model display setting that shows all fields and all contacts and events + */ + public ModelDisplaySetting() { + contactDisplaySetting = ContactDisplaySetting.DEFAULT_SETTING; + eventDisplaySetting = EventDisplaySetting.DEFAULT_SETTING; + contactDisplayPredicate = PREDICATE_SHOW_ALL_CONTACTS; + eventDisplayPredicate = PREDICATE_SHOW_ALL_EVENTS; + } + + /** + * Constructor of a customised display setting + * + * @param contactDisplaySetting contact display setting to be set + * @param eventDisplaySetting event display setting to be set + * @param contactDisplayPredicate contact display predicate to be set + * @param eventDisplayPredicate event display predicate to be set + */ + public ModelDisplaySetting( + ContactDisplaySetting contactDisplaySetting, EventDisplaySetting eventDisplaySetting, + Predicate contactDisplayPredicate, + Predicate eventDisplayPredicate) { + this.contactDisplaySetting = contactDisplaySetting; + this.eventDisplaySetting = eventDisplaySetting; + this.contactDisplayPredicate = contactDisplayPredicate; + this.eventDisplayPredicate = eventDisplayPredicate; + } + + public ContactDisplaySetting getContactDisplaySetting() { + return contactDisplaySetting; + } + + public EventDisplaySetting getEventDisplaySetting() { + return eventDisplaySetting; + } + + public Predicate getContactDisplayPredicate() { + return contactDisplayPredicate; + } + + public Predicate getEventDisplayPredicate() { + return eventDisplayPredicate; + } + + /** + * Create a copy of the current model display setting + * + * @return a copy of model display setting + */ + public ModelDisplaySetting copy() { + return new ModelDisplaySetting(contactDisplaySetting, eventDisplaySetting, contactDisplayPredicate, + eventDisplayPredicate); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (!(obj instanceof ModelDisplaySetting)) { + return false; + } + + ModelDisplaySetting other = (ModelDisplaySetting) obj; + return eventDisplaySetting.equals(other.eventDisplaySetting) + && contactDisplaySetting.equals(other.contactDisplaySetting); + } + + /** + * Returns a new {@code ModelDisplaySetting} object with the same properties, + * except for the {@code ContactDisplaySetting}. + */ + public ModelDisplaySetting differentContactDisplaySetting(ContactDisplaySetting displaySetting) { + return new ModelDisplaySetting(displaySetting, eventDisplaySetting, + contactDisplayPredicate, eventDisplayPredicate); + } + + /** + * Returns a new {@code ModelDisplaySetting} object with the same properties, + * except for the {@code EventDisplaySetting}. + */ + public ModelDisplaySetting differentEventDisplaySetting(EventDisplaySetting displaySetting) { + return new ModelDisplaySetting(contactDisplaySetting, displaySetting, + contactDisplayPredicate, eventDisplayPredicate); + } + + /** + * Returns a new {@code ModelDisplaySetting} object with the same properties, + * except for the {@code predicate} for contacts. + */ + public ModelDisplaySetting differentContactDisplayPredicate(Predicate predicate) { + return new ModelDisplaySetting(contactDisplaySetting, eventDisplaySetting, + predicate, eventDisplayPredicate); + } + + /** + * Returns a new {@code ModelDisplaySetting} object with the same properties, + * except for the {@code predicate} for events. + */ + public ModelDisplaySetting differentEventDisplayPredicate(Predicate predicate) { + return new ModelDisplaySetting(contactDisplaySetting, eventDisplaySetting, + contactDisplayPredicate, predicate); + } + + @Override + public String toString() { + return "ModelDisplaySetting{" + + "contactDisplaySetting=" + contactDisplaySetting + + ", eventDisplaySetting=" + eventDisplaySetting + + '}'; + } +} diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 0650c954f5c..a98ba8c4e76 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -4,6 +4,7 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; import java.nio.file.Path; +import java.util.List; import java.util.function.Predicate; import java.util.logging.Logger; @@ -11,7 +12,12 @@ import javafx.collections.transformation.FilteredList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; -import seedu.address.model.person.Person; +import seedu.address.commons.core.index.Index; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.ContactDisplaySetting; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventDisplaySetting; +import seedu.address.model.history.ModelHistory; /** * Represents the in-memory model of the address book data. @@ -19,9 +25,13 @@ public class ModelManager implements Model { private static final Logger logger = LogsCenter.getLogger(ModelManager.class); + private final ModelHistory modelHistory = new ModelHistory(); private final AddressBook addressBook; private final UserPrefs userPrefs; - private final FilteredList filteredPersons; + private final FilteredList filteredContacts; + private final FilteredList filteredEvents; + + private ModelDisplaySetting modelDisplaySetting = new ModelDisplaySetting(); /** * Initializes a ModelManager with the given addressBook and userPrefs. @@ -34,11 +44,17 @@ public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs this.addressBook = new AddressBook(addressBook); this.userPrefs = new UserPrefs(userPrefs); - filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + filteredContacts = new FilteredList<>(this.addressBook.getContactList()); + filteredEvents = new FilteredList<>(this.addressBook.getEventList()); + modelHistory.commit(addressBook, modelDisplaySetting); } + /** + * Initializes a ModelManager with the default addressBook and userPrefs. + */ public ModelManager() { this(new AddressBook(), new UserPrefs()); + modelHistory.commit(addressBook, modelDisplaySetting); } //=========== UserPrefs ================================================================================== @@ -65,6 +81,37 @@ public void setGuiSettings(GuiSettings guiSettings) { userPrefs.setGuiSettings(guiSettings); } + //=========== AddressBook Display Setting ======================================================================= + + /** + * Clear history of all display setting of model + */ + public void clearHistory() { + modelHistory.clearHistory(); + } + + public EventDisplaySetting getEventDisplaySetting() { + return modelDisplaySetting.getEventDisplaySetting(); + } + + public void setEventDisplaySetting(EventDisplaySetting eventDisplaySetting) { + requireNonNull(eventDisplaySetting); + modelDisplaySetting = modelDisplaySetting.differentEventDisplaySetting(eventDisplaySetting); + } + + @Override + public ContactDisplaySetting getContactDisplaySetting() { + return modelDisplaySetting.getContactDisplaySetting(); + } + + @Override + public void setContactDisplaySetting(ContactDisplaySetting displaySetting) { + requireNonNull(displaySetting); + modelDisplaySetting = modelDisplaySetting.differentContactDisplaySetting(displaySetting); + } + + //=========== AddressBook Storage ================================================================================ + @Override public Path getAddressBookFilePath() { return userPrefs.getAddressBookFilePath(); @@ -88,45 +135,190 @@ public ReadOnlyAddressBook getAddressBook() { return addressBook; } + //=========== Versioned AddressBook ================================================================================ + + @Override + public void commitHistory() { + modelHistory.commit(addressBook.copy(), modelDisplaySetting); + } + + @Override + public void undoHistory() { + ModelHistory.HistoryInstance instance = modelHistory.undo(); + addressBook.resetData(instance.getAddressBook()); + addressBook.updateDataMaps(); + modelDisplaySetting = instance.getDisplaySetting(); + filteredContacts.setPredicate(modelDisplaySetting.getContactDisplayPredicate()); + filteredEvents.setPredicate(modelDisplaySetting.getEventDisplayPredicate()); + rerenderAllCards(); + } + + @Override + public void redoHistory() { + ModelHistory.HistoryInstance instance = modelHistory.redo(); + addressBook.resetData(instance.getAddressBook()); + addressBook.updateDataMaps(); + modelDisplaySetting = instance.getDisplaySetting(); + filteredContacts.setPredicate(modelDisplaySetting.getContactDisplayPredicate()); + filteredEvents.setPredicate(modelDisplaySetting.getEventDisplayPredicate()); + rerenderAllCards(); + } + + @Override + public boolean isUndoable() { + return modelHistory.isUndoable(); + } + + @Override + public boolean isRedoable() { + return modelHistory.isRedoable(); + } + + //=========== Manage Contacts ====================== + + @Override + public boolean hasContact(Contact contact) { + requireNonNull(contact); + return addressBook.hasContact(contact); + } + + @Override + public void deleteContact(Contact target) { + addressBook.removeContact(target); + } + + @Override + public void addContact(Contact contact) { + addressBook.addContact(contact); + updateFilteredContactList(PREDICATE_SHOW_ALL_CONTACTS); + } + + @Override + public void setContact(Contact target, Contact editedContact) { + requireAllNonNull(target, editedContact); + addressBook.setContact(target, editedContact); + } + + @Override + public void resetContacts() { + // not necessary to remove links from contacts since they will be deleted, but just to be strict + // about the bidirectional relationship + removeAllLinks(); + + this.addressBook.resetContacts(); + } + + //=========== Manage Events ====================== + + @Override + public boolean hasEvent(Event event) { + requireNonNull(event); + return addressBook.hasEvent(event); + } + @Override - public boolean hasPerson(Person person) { - requireNonNull(person); - return addressBook.hasPerson(person); + public void deleteEvent(Event target) { + addressBook.removeEvent(target); } @Override - public void deletePerson(Person target) { - addressBook.removePerson(target); + public void addEvent(Event event) { + addressBook.addEvent(event); + updateFilteredEventList(PREDICATE_SHOW_ALL_EVENTS); } @Override - public void addPerson(Person person) { - addressBook.addPerson(person); - updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + public void setEvent(Event target, Event editedEvent) { + requireAllNonNull(target, editedEvent); + addressBook.setEvent(target, editedEvent); } @Override - public void setPerson(Person target, Person editedPerson) { - requireAllNonNull(target, editedPerson); + public void resetEvents() { + // not necessary to remove links from events since they will be deleted, but just to be strict + // about the bidirectional relationship + removeAllLinks(); - addressBook.setPerson(target, editedPerson); + this.addressBook.resetEvents(); } - //=========== Filtered Person List Accessors ============================================================= + //=========== Filtered Contact List Accessors ===================== /** - * Returns an unmodifiable view of the list of {@code Person} backed by the internal list of + * Returns an unmodifiable view of the list of {@code Contact} backed by the internal list of * {@code versionedAddressBook} */ @Override - public ObservableList getFilteredPersonList() { - return filteredPersons; + public ObservableList getFilteredContactList() { + return filteredContacts; } @Override - public void updateFilteredPersonList(Predicate predicate) { + public void updateFilteredContactList(Predicate predicate) { requireNonNull(predicate); - filteredPersons.setPredicate(predicate); + filteredContacts.setPredicate(predicate); + modelDisplaySetting = modelDisplaySetting.differentContactDisplayPredicate(predicate); + } + + @Override + public void updateContactListByIndex(Index index) { + requireNonNull(index); + Contact targetContact = filteredContacts.get(index.getZeroBased()); + Predicate predicate = curr -> curr.isSameContact(targetContact); + modelDisplaySetting = modelDisplaySetting.differentContactDisplayPredicate(predicate); + filteredContacts.setPredicate(predicate); + } + @Override + public void rearrangeContactsInOrder(List contacts, boolean isMarked) { + addressBook.rearrangeContactsInOrder(contacts, isMarked); + } + + //=========== Filtered Event List Accessors ======================= + /** + * Returns an unmodifiable view of the list of {@code Event} backed by the internal list of + * {@code versionedAddressBook} + */ + @Override + public ObservableList getFilteredEventList() { + return filteredEvents; + } + + @Override + public void updateFilteredEventList(Predicate predicate) { + requireNonNull(predicate); + filteredEvents.setPredicate(predicate); + modelDisplaySetting = modelDisplaySetting.differentEventDisplayPredicate(predicate); + } + + @Override + public void sortUpcomingFilteredEventList() { + Predicate currentPredicate = filteredEvents.getPredicate(); + // Remove events that have passed + addressBook.sortEvents(); + updateFilteredEventList(getNewPredicate(currentPredicate)); + } + + /** + * Returns the new predicate for displaying only upcoming and ongoing events + */ + private static Predicate getNewPredicate(Predicate originalPredicate) { + return event -> (originalPredicate == null || originalPredicate.test(event)) + && ((event.getEndDateAndTime() == null && event.getStartDateAndTime().isNotBeforeNow()) + || (event.getEndDateAndTime() != null && event.getEndDateAndTime().isNotBeforeNow())); + } + + @Override + public void updateEventListByIndex(Index index) { + requireNonNull(index); + Event targetEvent = filteredEvents.get(index.getZeroBased()); + Predicate predicate = curr -> curr.isSameEvent(targetEvent); + filteredEvents.setPredicate(predicate); + modelDisplaySetting = modelDisplaySetting.differentEventDisplayPredicate(predicate); + } + + @Override + public void rearrangeEventsInOrder(List events, boolean isMark) { + addressBook.rearrangeEventsInOrder(events, isMark); } @Override @@ -145,7 +337,55 @@ public boolean equals(Object obj) { ModelManager other = (ModelManager) obj; return addressBook.equals(other.addressBook) && userPrefs.equals(other.userPrefs) - && filteredPersons.equals(other.filteredPersons); + && filteredContacts.equals(other.filteredContacts) + && filteredEvents.equals(other.filteredEvents) + && modelDisplaySetting.equals(other.modelDisplaySetting); + } + + @Override + public void linkEventAndContact(Event event, Contact contact) { + requireAllNonNull(event, contact); + + addressBook.linkEventAndContact(event, contact); + } + + @Override + public void unlinkEventAndContact(Event event, Contact contact) { + requireAllNonNull(event, contact); + + addressBook.unlinkEventAndContact(event, contact); + } + + @Override + public void unlinkAllContactsFromEvent(Event event) { + addressBook.unlinkContactsFromEvent(event); } + @Override + public void rerenderContactCards(boolean useBackSamePredicate) { + ModelHistory.HistoryInstance historyInstance = modelHistory.getCurrentHistoryInstance(); + Predicate oldPred = historyInstance.getDisplaySetting().getContactDisplayPredicate(); + updateFilteredContactList(PREDICATE_HIDE_ALL_CONTACTS); // Hide first to update the contact cards. + updateFilteredContactList(useBackSamePredicate ? oldPred : PREDICATE_SHOW_ALL_CONTACTS); + } + + @Override + public void rerenderEventCards(boolean useBackSamePredicate) { + ModelHistory.HistoryInstance historyInstance = modelHistory.getCurrentHistoryInstance(); + Predicate oldPred = historyInstance.getDisplaySetting().getEventDisplayPredicate(); + updateFilteredEventList(PREDICATE_HIDE_ALL_EVENTS); // Hide first to update the event cards. + updateFilteredEventList(useBackSamePredicate ? oldPred : PREDICATE_SHOW_ALL_EVENTS); + } + + @Override + public void rerenderAllCards() { + rerenderContactCards(true); + rerenderEventCards(true); + } + + @Override + public void removeAllLinks() { + filteredEvents.forEach(event -> setEvent(event, event.clearAllLinks())); + filteredContacts.forEach(contact -> setContact(contact, contact.clearAllLinks())); + } } diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java index 6ddc2cd9a29..e791892c6d2 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java @@ -1,7 +1,8 @@ package seedu.address.model; import javafx.collections.ObservableList; -import seedu.address.model.person.Person; +import seedu.address.model.contact.Contact; +import seedu.address.model.event.Event; /** * Unmodifiable view of an address book @@ -9,9 +10,14 @@ public interface ReadOnlyAddressBook { /** - * Returns an unmodifiable view of the persons list. - * This list will not contain any duplicate persons. + * Returns an unmodifiable view of the contacts list. + * This list will not contain any duplicate contacts. */ - ObservableList getPersonList(); + ObservableList getContactList(); + /** + * Returns an unmodifiable view of the events list. + * This list will not contain any duplicate events. + */ + ObservableList getEventList(); } diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/address/model/UserPrefs.java index 25a5fd6eab9..dfbafb49f9a 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/seedu/address/model/UserPrefs.java @@ -14,7 +14,7 @@ public class UserPrefs implements ReadOnlyUserPrefs { private GuiSettings guiSettings = new GuiSettings(); - private Path addressBookFilePath = Paths.get("data" , "addressbook.json"); + private Path addressBookFilePath = Paths.get("data" , "soconnect.json"); /** * Creates a {@code UserPrefs} with default values. diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/common/Address.java similarity index 70% rename from src/main/java/seedu/address/model/person/Address.java rename to src/main/java/seedu/address/model/common/Address.java index 60472ca22a0..30080644d9d 100644 --- a/src/main/java/seedu/address/model/person/Address.java +++ b/src/main/java/seedu/address/model/common/Address.java @@ -1,10 +1,14 @@ -package seedu.address.model.person; +package seedu.address.model.common; import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; +import java.util.List; + +import seedu.address.commons.util.StringUtil; + /** - * Represents a Person's address in the address book. + * Represents address of an event or contact in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} */ public class Address { @@ -31,7 +35,7 @@ public Address(String address) { } /** - * Returns true if a given string is a valid email. + * Returns true if a given string is a valid address. */ public static boolean isValidAddress(String test) { return test.matches(VALIDATION_REGEX); @@ -49,6 +53,16 @@ public boolean equals(Object other) { && value.equals(((Address) other).value)); // state check } + /** + * Checks if this {@code value} contains any of {@code strings} + */ + public boolean containsString(List strings) { + requireNonNull(strings); + assert value != null : "the value should not be null"; + return strings.stream().anyMatch(string -> + StringUtil.containsWordIgnoreCase(value, string)); + } + @Override public int hashCode() { return value.hashCode(); diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/common/Name.java similarity index 75% rename from src/main/java/seedu/address/model/person/Name.java rename to src/main/java/seedu/address/model/common/Name.java index 79244d71cf7..8025f6450ac 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/common/Name.java @@ -1,10 +1,14 @@ -package seedu.address.model.person; +package seedu.address.model.common; import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; +import java.util.List; + +import seedu.address.commons.util.StringUtil; + /** - * Represents a Person's name in the address book. + * Represents the name of a contact or an event in SoConnect. * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} */ public class Name { @@ -51,6 +55,15 @@ public boolean equals(Object other) { && fullName.equals(((Name) other).fullName)); // state check } + /** + * Checks if this {@code fullName} contains any of keywords in {@code strings} + */ + public boolean containsString(List strings) { + requireNonNull(strings); + return strings.stream().anyMatch(string -> + StringUtil.containsWordIgnoreCase(fullName, string)); + } + @Override public int hashCode() { return fullName.hashCode(); diff --git a/src/main/java/seedu/address/model/common/ZoomLink.java b/src/main/java/seedu/address/model/common/ZoomLink.java new file mode 100644 index 00000000000..b253a33da10 --- /dev/null +++ b/src/main/java/seedu/address/model/common/ZoomLink.java @@ -0,0 +1,61 @@ +package seedu.address.model.common; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.util.List; + +import seedu.address.commons.util.StringUtil; + +/** + * Represents zoom link of a contact (usually a professor or tutor). + * Guarantees: immutable + */ +public class ZoomLink { + + public static final String MESSAGE_CONSTRAINTS = "Zoom link can take in any value and it should not be blank."; + + public static final String VALIDATION_REGEX = "^(?!\\s*$).+"; + + public final String link; + + /** + * Constructs a {@code ZoomLink}. + * + * @param link A valid zoom link. + */ + public ZoomLink(String link) { + requireNonNull(link); + checkArgument(isValidZoomLink(link), MESSAGE_CONSTRAINTS); + this.link = link; + } + + /** + * Returns true if a given string is a valid zoom link. + */ + public static boolean isValidZoomLink(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public String toString() { + return link; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ZoomLink // instanceof handles nulls + && link.equals(((ZoomLink) other).link)); // state check + } + + /** + * Checks if this {@code link} contains any keywords in {@code strings} + */ + public boolean containsString(List strings) { + requireNonNull(strings); + assert link != null : "the link should not be null"; + return strings.stream().anyMatch(string -> + StringUtil.containsWordIgnoreCase(link, string)); + } +} diff --git a/src/main/java/seedu/address/model/contact/Contact.java b/src/main/java/seedu/address/model/contact/Contact.java new file mode 100644 index 00000000000..5b3559181a6 --- /dev/null +++ b/src/main/java/seedu/address/model/contact/Contact.java @@ -0,0 +1,334 @@ +package seedu.address.model.contact; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; + +import seedu.address.model.common.Address; +import seedu.address.model.common.Name; +import seedu.address.model.common.ZoomLink; +import seedu.address.model.event.Event; +import seedu.address.model.tag.Tag; + +/** + * Represents a Contact in the address book. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Contact { + // stores references to all contacts, accessible by their unique UUIDs + private static HashMap map = new HashMap<>(); + + // Identity fields + private final Name name; + private final Phone phone; + private final Email email; + private final TelegramHandle telegramHandle; + private final UUID uuid; + + // Data fields + private final Address address; + private final ZoomLink zoomLink; + private final Set tags = new HashSet<>(); + private final Set linkedEvents = new HashSet<>(); + private final boolean isMarked; + + /** + * Name, email and tags must be present and not null. + */ + public Contact( + Name name, Phone phone, Email email, Address address, ZoomLink zoomLink, + TelegramHandle telegramHandle, Set tags) { + requireAllNonNull(name, email, tags); + this.name = name; + this.phone = phone; + this.email = email; + this.address = address; + this.tags.addAll(tags); + this.telegramHandle = telegramHandle; + this.zoomLink = zoomLink; + this.uuid = UUID.randomUUID(); // to generate a uuid to uniquely identify contact + this.isMarked = false; + } + + /** + * This constructor is for creating contact stored in storage. The contact stored in storage contains information + * of uuid and events linked to it, in addition to information about other fields. + * This constructor ensures that everytime the application loads the data from storage, the uuid of the contact + * stays the same and contains uuid of events that are linked. + */ + public Contact( + Name name, Phone phone, Email email, Address address, ZoomLink zoomLink, + TelegramHandle telegramHandle, Set tags, UUID uuid, Set linkedEvents, boolean isMarked) { + requireAllNonNull(name, email, tags, uuid, linkedEvents); + this.name = name; + this.phone = phone; + this.email = email; + this.address = address; + this.tags.addAll(tags); + this.telegramHandle = telegramHandle; + this.zoomLink = zoomLink; + this.uuid = uuid; + this.linkedEvents.addAll(linkedEvents); + this.isMarked = isMarked; + } + + /** + * Adds the contact to the hashmap that stores references to all contacts. + * If the hashmap already contains the UUID of the contact as key, the value associated to the + * key will be replaced to the contact passed as parameter to this method. + * + * @param contact The contact to be added. + */ + public static void addToMap(Contact contact) { + map.put(contact.getUuid(), contact); + } + + /** + * Returns a contact with the unique UUID that is passed in to the method. + * The UUID passed as parameter MUST be a valid UUID that is stored in the hashmap as a key. + * + * @param uuid The unique identifier for a contact in the hashmap. + */ + public static Contact findByUuid(UUID uuid) { + assert map.containsKey(uuid) : "The uuid must be valid and already in the hashmap as a key."; + return map.get(uuid); + } + + public Name getName() { + return name; + } + + public Phone getPhone() { + return phone; + } + + public Email getEmail() { + return email; + } + + public Address getAddress() { + return address; + } + + public TelegramHandle getTelegramHandle() { + return telegramHandle; + } + + public ZoomLink getZoomLink() { + return zoomLink; + } + + public UUID getUuid() { + return uuid; + } + + /** + * Returns an immutable tag set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public Set getTags() { + return Collections.unmodifiableSet(tags); + } + + /** + * Returns a unmodifiable set of UUIDs, each uniquely represents an event, that are linked to the contact object + * that calls this method. + */ + public Set getLinkedEvents() { + return Collections.unmodifiableSet(linkedEvents); + } + + public boolean getIsMarked() { + return isMarked; + } + + /** + * Checks if this {@code name} contains any keywords in {code strings} + */ + public boolean nameAnyMatch(List strings) { + return name.containsString(strings); + } + + /** + * Checks if this {@code email} contains any keywords in {code strings} + */ + public boolean emailAnyMatch(List strings) { + return email.containsString(strings); + } + + /** + * Checks if this {@code phone} contains any keywords in {code strings} + */ + public boolean phoneAnyMatch(List strings) { + return (phone != null) && phone.containsString(strings); + } + + /** + * Checks if this {@code address} contains any keywords in {code strings} + */ + public boolean addressAnyMatch(List strings) { + return (address != null) && address.containsString(strings); + } + + /** + * Checks if this {@code telegramHandle} contains any keywords in {code strings} + */ + public boolean telegramHandleAnyMatch(List strings) { + return (telegramHandle != null) && telegramHandle.containsString(strings); + } + + /** + * Checks if this {@code zoomLink} contains any keywords in {code strings} + */ + public boolean zoomLinkAnyMatch(List strings) { + return (zoomLink != null) && zoomLink.containsString(strings); + } + + /** + * Checks if at least one of the {@code tags} contains any keywords in {code strings} + */ + public boolean anyTagsContain(List strings) { + return tags.stream().anyMatch(tag -> tag.containsString(strings)); + } + + /** + * Returns true if both contacts have the same name. + * This defines a weaker notion of equality between two contacts. + */ + public boolean isSameContact(Contact otherContact) { + if (otherContact == this) { + return true; + } + + return otherContact != null + && otherContact.getName().equals(getName()); + } + + /** + * Checks if the contact is linked to a particular event. + */ + public boolean hasLinkTo(Event event) { + UUID eventUuid = event.getUuid(); + return linkedEvents.contains(eventUuid); + } + + /** + * Links the event to the contact object that calls this method. + * + * @param event The event to be linked with. + * @return The contact that has link to the event passed in as parameter. + */ + public Contact linkTo(Event event) { + Set updatedLinkedEvents = new HashSet<>(linkedEvents); + updatedLinkedEvents.add(event.getUuid()); + Contact updatedContact = new Contact(name, phone, email, address, zoomLink, telegramHandle, tags, + uuid, updatedLinkedEvents, isMarked); + addToMap(updatedContact); // must update the map to represent the latest changes + return updatedContact; + } + + /** + * Removes the link between the event and the contact object that calls this method. + * + * @param event The event to be unlinked. + * @return The contact that has no link to the event passed in as parameter. + */ + public Contact unlink(Event event) { + Set updatedLinkedEvents = new HashSet<>(linkedEvents); + updatedLinkedEvents.remove(event.getUuid()); + Contact updatedContact = new Contact(name, phone, email, address, zoomLink, telegramHandle, tags, + uuid, updatedLinkedEvents, isMarked); + addToMap(updatedContact); + return updatedContact; + } + + /** + * Removes all links to the contact object that calls this method. + * + * @return The contact that has no link to any contacts. + */ + public Contact clearAllLinks() { + Contact updatedContact = new Contact(name, phone, email, address, zoomLink, telegramHandle, tags, + uuid, new HashSet<>(), isMarked); + addToMap(updatedContact); + return updatedContact; + } + + /** + * Marks contact object that calls this method. + * + * @return The contact that has been marked. + */ + public Contact markContact() { + Contact updatedContact = + new Contact(name, phone, email, address, zoomLink, telegramHandle, tags, uuid, linkedEvents, true); + addToMap(updatedContact); + return updatedContact; + } + + /** + * Removes the mark in the contact object that calls this method. + * + * @return The contact that has been un-marked. + */ + public Contact unmarkContact() { + Contact updatedContact = + new Contact(name, phone, email, address, zoomLink, telegramHandle, tags, uuid, linkedEvents, false); + addToMap(updatedContact); + return updatedContact; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(getName()) + .append("; Email: ") + .append(getEmail()) + .append(getPhone() != null ? "; Phone: " + getPhone() : "") // optional + .append(getAddress() != null ? "; Address: " + getAddress() : "") // optional + .append(getZoomLink() != null ? "; Zoom Link: " + getZoomLink() : "") // optional + .append(getTelegramHandle() != null ? "; Telegram: " + getTelegramHandle() : ""); // optional + + Set tags = getTags(); + if (!tags.isEmpty()) { + builder.append("; Tags: "); + tags.forEach(builder::append); + } + return builder.toString(); + } + + /** + * Returns true if both contacts have the same details. + * This defines a stronger notion of equality between two contacts. + */ + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + Contact contact = (Contact) other; + return Objects.equals(getName(), contact.getName()) + && Objects.equals(getPhone(), contact.getPhone()) + && Objects.equals(getEmail(), contact.getEmail()) + && Objects.equals(getTelegramHandle(), contact.getTelegramHandle()) + && Objects.equals(getZoomLink(), contact.getZoomLink()) + && Objects.equals(getAddress(), contact.getAddress()) + && Objects.equals(getTags(), contact.getTags()) + && Objects.equals(getIsMarked(), contact.getIsMarked()); + } + + @Override + public int hashCode() { + return Objects.hash( + getName(), getPhone(), getEmail(), getTelegramHandle(), getZoomLink(), getAddress(), getTags()); + } +} diff --git a/src/main/java/seedu/address/model/contact/ContactContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/contact/ContactContainsKeywordsPredicate.java new file mode 100644 index 00000000000..4fbf853d345 --- /dev/null +++ b/src/main/java/seedu/address/model/contact/ContactContainsKeywordsPredicate.java @@ -0,0 +1,88 @@ +package seedu.address.model.contact; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +/** + * Tests that a {@code Contact} contains any of the keywords given. + */ +public class ContactContainsKeywordsPredicate implements Predicate { + private final List nameKeywords; + private List phoneKeywords = new ArrayList<>(); + private List emailKeywords = new ArrayList<>(); + private List telegramHandleKeywords = new ArrayList<>(); + private List addressKeywords = new ArrayList<>(); + private List zoomLinkKeywords = new ArrayList<>(); + private List tagKeywords = new ArrayList<>(); + + /** + * Creates a {@code ContactContainsKeywordsPredicate} object with the name keywords. + */ + public ContactContainsKeywordsPredicate(List nameKeywords) { + requireNonNull(nameKeywords); + this.nameKeywords = nameKeywords; + } + + /** + * Creates a {@code ContactContainsKeywordsPredicate} object with no keywords. + */ + public ContactContainsKeywordsPredicate() { + nameKeywords = new ArrayList<>(); + } + + public void setPhoneKeywords(List phoneKeywords) { + requireNonNull(phoneKeywords); + this.phoneKeywords = phoneKeywords; + } + + public void setEmailKeywords(List emailKeywords) { + requireNonNull(emailKeywords); + this.emailKeywords = emailKeywords; + } + + public void setAddressKeywords(List addressKeywords) { + requireNonNull(addressKeywords); + this.addressKeywords = addressKeywords; + } + + public void setTelegramHandleKeyword(List telegramHandleKeywords) { + requireNonNull(telegramHandleKeywords); + this.telegramHandleKeywords = telegramHandleKeywords; + } + + public void setZoomLinkKeywords(List zoomLinkKeywords) { + requireNonNull(zoomLinkKeywords); + this.zoomLinkKeywords = zoomLinkKeywords; + } + + public void setTagKeywords(List tagKeywords) { + requireNonNull(tagKeywords); + this.tagKeywords = tagKeywords; + } + + @Override + public boolean test(Contact contact) { // applied Law of Demeter, don't access contact fields' strings + requireNonNull(contact); + return contact.nameAnyMatch(nameKeywords) || contact.phoneAnyMatch(phoneKeywords) + || contact.emailAnyMatch(emailKeywords) || contact.addressAnyMatch(addressKeywords) + || contact.zoomLinkAnyMatch(zoomLinkKeywords) || contact.telegramHandleAnyMatch(telegramHandleKeywords) + || contact.anyTagsContain(tagKeywords); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ContactContainsKeywordsPredicate // instanceof handles nulls + && nameKeywords.equals(((ContactContainsKeywordsPredicate) other).nameKeywords) + && phoneKeywords.equals(((ContactContainsKeywordsPredicate) other).phoneKeywords) + && emailKeywords.equals(((ContactContainsKeywordsPredicate) other).emailKeywords) + && addressKeywords.equals(((ContactContainsKeywordsPredicate) other).addressKeywords) + && telegramHandleKeywords.equals(((ContactContainsKeywordsPredicate) other).telegramHandleKeywords) + && zoomLinkKeywords.equals(((ContactContainsKeywordsPredicate) other).zoomLinkKeywords) + && tagKeywords.equals(((ContactContainsKeywordsPredicate) other).tagKeywords)); // state check + } +} + diff --git a/src/main/java/seedu/address/model/contact/ContactDisplaySetting.java b/src/main/java/seedu/address/model/contact/ContactDisplaySetting.java new file mode 100644 index 00000000000..a6c36764a04 --- /dev/null +++ b/src/main/java/seedu/address/model/contact/ContactDisplaySetting.java @@ -0,0 +1,114 @@ +package seedu.address.model.contact; + +import java.util.Objects; + +/** + * Contains all the display settings for contacts in the model manager. + */ +public class ContactDisplaySetting { + public static final ContactDisplaySetting DEFAULT_SETTING = new ContactDisplaySetting(false); + private final boolean willDisplayPhone; + private final boolean willDisplayEmail; + private final boolean willDisplayTelegramHandle; + private final boolean willDisplayAddress; + private final boolean willDisplayZoomLink; + private final boolean willDisplayTags; + private final boolean isViewingFull; + + /** + * Creates a new {@code ContactDisplaySetting} with the given settings. + * This is usually used in {@code CListCommand}, where the {@code isViewingFull} is always false. + */ + public ContactDisplaySetting( + boolean willDisplayPhone, boolean willDisplayEmail, boolean willDisplayTelegramHandle, + boolean willDisplayAddress, + boolean willDisplayZoomLink, boolean willDisplayTags) { + this.willDisplayPhone = willDisplayPhone; + this.willDisplayEmail = willDisplayEmail; + this.willDisplayTelegramHandle = willDisplayTelegramHandle; + this.willDisplayAddress = willDisplayAddress; + this.willDisplayZoomLink = willDisplayZoomLink; + this.willDisplayTags = willDisplayTags; + this.isViewingFull = false; + } + + /** + * Creates a new {@code ContactDisplaySetting} with the given settings. + * By default, all willDisplayXXX fields will be true. + */ + public ContactDisplaySetting(boolean isViewingFull) { + willDisplayPhone = true; + willDisplayEmail = true; + willDisplayTelegramHandle = true; + willDisplayAddress = true; + willDisplayZoomLink = true; + willDisplayTags = true; + this.isViewingFull = isViewingFull; + } + + public boolean willDisplayAddress() { + return willDisplayAddress; + } + + public boolean willDisplayZoomLink() { + return willDisplayZoomLink; + } + + public boolean willDisplayTags() { + return willDisplayTags; + } + + public boolean isViewingFull() { + return isViewingFull; + } + + public boolean willDisplayPhone() { + return willDisplayPhone; + } + + public boolean willDisplayEmail() { + return willDisplayEmail; + } + + public boolean willDisplayTelegramHandle() { + return willDisplayTelegramHandle; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ContactDisplaySetting)) { + return false; + } + ContactDisplaySetting that = (ContactDisplaySetting) o; + return willDisplayPhone() == that.willDisplayPhone() && willDisplayEmail() == that.willDisplayEmail() + && willDisplayTelegramHandle() == that.willDisplayTelegramHandle() + && willDisplayAddress() == that.willDisplayAddress() + && willDisplayZoomLink() == that.willDisplayZoomLink() + && willDisplayTags() == that.willDisplayTags() + && isViewingFull() == that.isViewingFull(); + } + + @Override + public int hashCode() { + return Objects.hash( + willDisplayPhone(), willDisplayEmail(), willDisplayTelegramHandle(), + willDisplayAddress(), willDisplayZoomLink(), willDisplayTags(), isViewingFull()); + } + + /** For debugging/logging purposes. */ + @Override + public String toString() { + return "ContactDisplaySetting{" + + "willDisplayPhone=" + willDisplayPhone + + ", willDisplayEmail=" + willDisplayEmail + + ", willDisplayTelegramHandle=" + willDisplayTelegramHandle + + ", willDisplayAddress=" + willDisplayAddress + + ", willDisplayZoomLink=" + willDisplayZoomLink + + ", willDisplayTags=" + willDisplayTags + + ", isViewingFull=" + isViewingFull + + '}'; + } +} diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/contact/Email.java similarity index 84% rename from src/main/java/seedu/address/model/person/Email.java rename to src/main/java/seedu/address/model/contact/Email.java index f866e7133de..f781a6f3912 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/seedu/address/model/contact/Email.java @@ -1,10 +1,14 @@ -package seedu.address.model.person; +package seedu.address.model.contact; import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; +import java.util.List; + +import seedu.address.commons.util.StringUtil; + /** - * Represents a Person's email in the address book. + * Represents a Contact's email in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidEmail(String)} */ public class Email { @@ -21,6 +25,7 @@ public class Email { + " - 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 + "]" @@ -63,6 +68,16 @@ public boolean equals(Object other) { && value.equals(((Email) other).value)); // state check } + /** + * Checks if this {@code value} contains any keywords in {@code strings} + */ + public boolean containsString(List strings) { + requireNonNull(strings); + assert value != null : "the value should not be null"; + return strings.stream().anyMatch(string -> + StringUtil.containsWordIgnoreCase(value, string)); + } + @Override public int hashCode() { return value.hashCode(); diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/contact/Phone.java similarity index 72% rename from src/main/java/seedu/address/model/person/Phone.java rename to src/main/java/seedu/address/model/contact/Phone.java index 872c76b382f..95e8d7da915 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/seedu/address/model/contact/Phone.java @@ -1,10 +1,14 @@ -package seedu.address.model.person; +package seedu.address.model.contact; import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; +import java.util.List; + +import seedu.address.commons.util.StringUtil; + /** - * Represents a Person's phone number in the address book. + * Represents a Contact's phone number in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidPhone(String)} */ public class Phone { @@ -45,6 +49,16 @@ public boolean equals(Object other) { && value.equals(((Phone) other).value)); // state check } + /** + * Checks if this {@code value} contains any keywords in {@code strings} + * @param strings + */ + public boolean containsString(List strings) { + requireNonNull(strings); + return strings.stream().anyMatch(string -> + StringUtil.containsWordIgnoreCase(value, string)); + } + @Override public int hashCode() { return value.hashCode(); diff --git a/src/main/java/seedu/address/model/contact/TelegramHandle.java b/src/main/java/seedu/address/model/contact/TelegramHandle.java new file mode 100644 index 00000000000..b2051be8a3d --- /dev/null +++ b/src/main/java/seedu/address/model/contact/TelegramHandle.java @@ -0,0 +1,82 @@ +package seedu.address.model.contact; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.util.List; + +import seedu.address.commons.util.StringUtil; + +public class TelegramHandle { + + public static final String MESSAGE_CONSTRAINTS = + "Telegram handles should only contain alphanumeric characters or underscores, with minimum 5 characters."; + + /* + * At least 5 characters of a-z or A-Z or 0-9 or _. + */ + public static final String VALIDATION_REGEX = "[\\p{Alnum}_]{5,}"; + + /** + * The telegram username of a contact. + */ + public final String handle; + + /** + * The link to the telegram account of the handle. + */ + public final String link; + + /** + * Constructs a {@code TelegramHandle}. + * + * @param handle A valid Telegram handle. + */ + public TelegramHandle(String handle) { + requireNonNull(handle); + checkArgument(isValidHandle(handle), MESSAGE_CONSTRAINTS); + this.handle = handle; + link = "https://t.me/" + handle; + } + + /** + * Returns true if a given string is a valid Telegram handle. + */ + public static boolean isValidHandle(String test) { + return test.matches(VALIDATION_REGEX); + } + + /** + * Gets the link to the contact's telegram chat. + */ + public String getTelegramLink() { + return link; + } + + @Override + public String toString() { + return handle; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof TelegramHandle // instanceof handles nulls + && handle.equals(((TelegramHandle) other).handle)); // state check + } + + /** + * Checks if this {@code handle} contains any keywords in {@code strings} + */ + public boolean containsString(List strings) { + requireNonNull(strings); + assert handle != null : "the handle should not be null"; + return strings.stream().anyMatch(string -> + StringUtil.containsWordIgnoreCase(handle, string)); + } + + @Override + public int hashCode() { + return handle.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/contact/UniqueContactList.java b/src/main/java/seedu/address/model/contact/UniqueContactList.java new file mode 100644 index 00000000000..1e25b079189 --- /dev/null +++ b/src/main/java/seedu/address/model/contact/UniqueContactList.java @@ -0,0 +1,185 @@ +package seedu.address.model.contact; + + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.contact.exceptions.ContactNotFoundException; +import seedu.address.model.contact.exceptions.DuplicateContactException; + +/** + * A list of contacts that enforces uniqueness between its elements and does not allow nulls. + * A contact is considered unique by comparing using {@code Contact#isSameContact(Contact)}. As such, adding and + * updating of contacts uses Contact#isSameContact(Contact) for equality so as to ensure that the contact being added + * or updated is unique in terms of identity in the UniqueContactList. However, the removal of a contact uses + * Contact#equals(Object) so as to ensure that the contact with exactly the same fields will be removed. + *

+ * Supports a minimal set of list operations. + * + * @see Contact#isSameContact(Contact) + */ +public class UniqueContactList implements Iterable { + + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Returns true if the list contains an equivalent contact as the given argument. + */ + public boolean contains(Contact toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSameContact); + } + + /** + * Adds a contact to the list. + * The contact must not already exist in the list. + */ + public void add(Contact toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateContactException(); + } + internalList.add(toAdd); + } + + /** + * Replaces the contact {@code target} in the list with {@code editedContact}. + * {@code target} must exist in the list. + * The contact identity of {@code editedContact} must not be the same as another existing contact in the list. + */ + public void setContact(Contact target, Contact editedContact) { + requireAllNonNull(target, editedContact); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new ContactNotFoundException(); + } + + if (!target.isSameContact(editedContact) && contains(editedContact)) { + throw new DuplicateContactException(); + } + + internalList.set(index, editedContact); + } + + /** + * Removes the equivalent contact from the list. + * The contact must exist in the list. + */ + public void remove(Contact toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new ContactNotFoundException(); + } + } + + public void setContacts(UniqueContactList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code contacts}. + * {@code contacts} must not contain duplicate contacts. + */ + public void setContacts(List contacts) { + requireAllNonNull(contacts); + if (!contactsAreUnique(contacts)) { + throw new DuplicateContactException(); + } + + internalList.setAll(contacts); + } + + /** + * Remove all the contents of this list. + */ + public void resetContacts() { + internalList.clear(); + } + + /** + * Moves marked contacts to the top of the list. + * Places the newly marked contacts or replaces newly unmarked contacts + * in the order specified in {@code contacts} and + * based on {@code isMarked} which signals whether this method is called by + * CMarkCommand or otherwise. + */ + public void rearrangeContactsInOrder(List contacts, boolean isMarked) { + ObservableList tempList = FXCollections.observableArrayList(); + if (isMarked) { + tempList.addAll(contacts); + tempList.addAll(internalList.filtered(contact -> !contacts.contains(contact))); + } else { + tempList.addAll(internalList.filtered(Contact::getIsMarked)); + tempList.addAll(internalList.filtered(c -> !c.getIsMarked())); + } + internalList.clear(); + internalList.addAll(tempList); + } + + /** + * Update the UUID map in contacts. + */ + public void updateContactMap() { + for (Contact contact : internalList) { + Contact.addToMap(contact); + } + } + + /** + * Get a copy of uniqueContactList + * + * @return a copy of a UniqueContactList + */ + public ObservableList copy() { + List contactList = new ArrayList<>(internalList); + return FXCollections.observableArrayList(contactList); + } + + /** + * 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 UniqueContactList // instanceof handles nulls + && internalList.equals(((UniqueContactList) other).internalList)); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + /** + * Returns true if {@code contacts} contains only unique contacts. + */ + private boolean contactsAreUnique(List contacts) { + for (int i = 0; i < contacts.size() - 1; i++) { + for (int j = i + 1; j < contacts.size(); j++) { + if (contacts.get(i).isSameContact(contacts.get(j))) { + return false; + } + } + } + return true; + } +} diff --git a/src/main/java/seedu/address/model/contact/exceptions/ContactNotFoundException.java b/src/main/java/seedu/address/model/contact/exceptions/ContactNotFoundException.java new file mode 100644 index 00000000000..d63c8e5267d --- /dev/null +++ b/src/main/java/seedu/address/model/contact/exceptions/ContactNotFoundException.java @@ -0,0 +1,6 @@ +package seedu.address.model.contact.exceptions; + +/** + * Signals that the operation is unable to find the specified contact. + */ +public class ContactNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/contact/exceptions/DuplicateContactException.java b/src/main/java/seedu/address/model/contact/exceptions/DuplicateContactException.java new file mode 100644 index 00000000000..f7f35b383a9 --- /dev/null +++ b/src/main/java/seedu/address/model/contact/exceptions/DuplicateContactException.java @@ -0,0 +1,11 @@ +package seedu.address.model.contact.exceptions; + +/** + * Signals that the operation will result in duplicate Contacts (Contacts are considered duplicates if they have the + * same identity). + */ +public class DuplicateContactException extends RuntimeException { + public DuplicateContactException() { + super("Operation would result in duplicate contacts"); + } +} diff --git a/src/main/java/seedu/address/model/event/DateAndTime.java b/src/main/java/seedu/address/model/event/DateAndTime.java new file mode 100644 index 00000000000..82fa0703a74 --- /dev/null +++ b/src/main/java/seedu/address/model/event/DateAndTime.java @@ -0,0 +1,106 @@ +package seedu.address.model.event; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.format.ResolverStyle; +import java.util.List; + +import seedu.address.commons.util.StringUtil; + +/** + * Represents time of an event in the event list. + * Guarantees: immutable; + */ + +public class DateAndTime implements Comparable { + public static final String MESSAGE_CONSTRAINTS = + "Event date and time should be in dd-MM-yyyy HH:mm format and should meet the following requirements: \n" + + "1. Start dateTime should not be empty.\n" + + "2. Input should not contain any leading or trailing white space.\n" + + "3. Year input can be any 4 digits number. Month input ranges from 01 to 12. " + + "Date input ranges from 01 to 31 but the range may vary according to the month input. \n" + + "Example: 31-01-2021 is a valid date, but 31-02-2021 is invalid as February has only 28 or 29 days. \n" + + "4. Hour input ranges from 00 to 23. Minute input ranges from 00 to 59. \n" + + "5. Any single-digit input should start with a leading 0. \n" + + "Example: 02-10-2021 09:00, 13-05-1999 12:05 are valid inputs but 2-10-2021 09:00, " + + "13-05-1999 12:5 are invalid. \n"; + + public static final DateTimeFormatter DATE_TIME_FORMATTER = + DateTimeFormatter.ofPattern("dd-MM-uuuu HH:mm").withResolverStyle(ResolverStyle.STRICT); + public final LocalDateTime time; + + /** + * Constructs an {@code DateAndTime} + * + * @param time A valid start DateAndTime + */ + public DateAndTime(String time) { + requireNonNull(time); + checkArgument(isValidDateTime(time), MESSAGE_CONSTRAINTS); + this.time = LocalDateTime.parse(time, DATE_TIME_FORMATTER); + } + + /** + * Returns true if a given string is a valid DateAndTime. + */ + public static boolean isValidDateTime(String test) { + try { + DATE_TIME_FORMATTER.parse(test); + return true; + } catch (DateTimeParseException e) { + return false; + } + } + + /** + * Return LocalDateTime of a DateAndTime object + */ + public LocalDateTime getDateTime() { + return time; + } + + public boolean isBefore(DateAndTime d) { + return this.time.isBefore(d.getDateTime()); + } + + public boolean isNotBeforeNow() { + return !this.time.isBefore(LocalDateTime.now()); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DateAndTime // instanceof handles nulls + && time.equals(((DateAndTime) other).time)); // state check + } + + /** + * Returns a string representing a DateAndTime object. + * @return A string representation of a DateAndTime object. + */ + @Override + public String toString() { + return this.time.format(DATE_TIME_FORMATTER); + } + + @Override + public int compareTo(DateAndTime other) { + return this.time.compareTo(other.time); + } + + + /** + * Checks if this {@code time string} contains any keywords in {@code strings} + */ + public boolean containsString(List strings) { + requireNonNull(strings); + assert time != null : "time must not be null"; + return strings.stream().anyMatch(string -> + StringUtil.containsWordIgnoreCase(toString(), string)); + } + +} diff --git a/src/main/java/seedu/address/model/event/Description.java b/src/main/java/seedu/address/model/event/Description.java new file mode 100644 index 00000000000..6977a26fe64 --- /dev/null +++ b/src/main/java/seedu/address/model/event/Description.java @@ -0,0 +1,70 @@ +package seedu.address.model.event; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.util.List; + +import seedu.address.commons.util.StringUtil; + +/** + * Represents the description of an event in the address book. + * Guarantees: immutable; is always valid + */ +public class Description { + + public static final String MESSAGE_CONSTRAINTS = "Description can take any values, and it should not be blank"; + + /* + * The first character of the description 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 Description}. + * + * @param remark A valid description. + */ + public Description(String remark) { + requireNonNull(remark); + checkArgument(isValidDescription(remark), MESSAGE_CONSTRAINTS); + value = remark; + } + + /** + * Returns true if a given string is a valid description. + */ + public static boolean isValidDescription(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Description // instanceof handles nulls + && value.equals(((Description) other).value)); // state check + } + + @Override + public String toString() { + return value; + } + + /** + * Checks if this {@code value} contains any keywords in {@code strings} + */ + public boolean containsString(List strings) { + requireNonNull(strings); + assert value != null : "the value should not be null"; + return strings.stream().anyMatch(string -> + StringUtil.containsWordIgnoreCase(value, string)); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/event/EndDateTime.java b/src/main/java/seedu/address/model/event/EndDateTime.java new file mode 100644 index 00000000000..ed546f8bb05 --- /dev/null +++ b/src/main/java/seedu/address/model/event/EndDateTime.java @@ -0,0 +1,18 @@ +package seedu.address.model.event; + +/** + * Represents end time of an event in the event list. + * Guarantees: immutable; is valid as declared in {@link #isValidTime(String)} + */ + +public class EndDateTime extends DateAndTime { + + /** + * Constructs an {@code DateAndTime} + * + * @param time A valid DateAndTime + */ + public EndDateTime(String time) { + super(time); + } +} diff --git a/src/main/java/seedu/address/model/event/Event.java b/src/main/java/seedu/address/model/event/Event.java new file mode 100644 index 00000000000..17d78c96509 --- /dev/null +++ b/src/main/java/seedu/address/model/event/Event.java @@ -0,0 +1,329 @@ +package seedu.address.model.event; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; + +import seedu.address.model.common.Address; +import seedu.address.model.common.Name; +import seedu.address.model.common.ZoomLink; +import seedu.address.model.contact.Contact; +import seedu.address.model.tag.Tag; + +/** + * Represents an Event in SoConnect. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Event { + // stores references to all events, accessible by their unique UUIDs + private static final HashMap map = new HashMap<>(); + + // Identity fields + private final Name name; + private final StartDateTime startDateAndTime; + private final EndDateTime endDateAndTime; + private final Description description; + + // Data fields + private final Address address; + private final ZoomLink zoomLink; + private final UUID uuid; + + private final Set tags = new HashSet<>(); + private final Set linkedContacts = new HashSet<>(); + private final boolean isMarked; + + /** + * Name, startDateTime and tags must be present and not null. + */ + public Event(Name name, StartDateTime startDateAndTime, EndDateTime endDateAndTime, + Description description, Address address, ZoomLink zoomLink, Set tags) { + requireAllNonNull(name, startDateAndTime, tags); + this.name = name; + this.startDateAndTime = startDateAndTime; + this.endDateAndTime = endDateAndTime; + this.description = description; + this.address = address; + this.zoomLink = zoomLink; + this.tags.addAll(tags); + this.uuid = UUID.randomUUID(); + this.isMarked = false; + } + + + /** + * This constructor is for creating event stored in storage. The event stored in storage contains information + * of uuid and contacts linked to it, in addition to information about other fields. + * This constructor ensures that everytime the application loads the data from storage, the uuid of the event + * stays the same and contains uuid of contacts that are linked to it. + */ + public Event( + Name name, StartDateTime startDateAndTime, EndDateTime endDateAndTime, Description description, + Address address, ZoomLink zoomLink, Set tags, UUID uuid, Set linkedContacts, + boolean isMarked) { + requireAllNonNull(name, startDateAndTime, tags, uuid, linkedContacts); + this.name = name; + this.startDateAndTime = startDateAndTime; + this.endDateAndTime = endDateAndTime; + this.description = description; + this.address = address; + this.zoomLink = zoomLink; + this.tags.addAll(tags); + this.uuid = uuid; + this.linkedContacts.addAll(linkedContacts); + this.isMarked = isMarked; + } + + public Name getName() { + return name; + } + + public StartDateTime getStartDateAndTime() { + return startDateAndTime; + } + + public EndDateTime getEndDateAndTime() { + return endDateAndTime; + } + + public Description getDescription() { + return description; + } + + public Address getAddress() { + return address; + } + + public ZoomLink getZoomLink() { + return zoomLink; + } + + public UUID getUuid() { + return uuid; + } + + /** + * Returns an immutable tag set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public Set getTags() { + return Collections.unmodifiableSet(tags); + } + + /** + * Returns a unmodifiable set of UUIDs, each uniquely represents an contact, that are linked to the event object + * that calls this method. + */ + public Set getLinkedContacts() { + return Collections.unmodifiableSet(linkedContacts); + } + + public boolean getIsMarked() { + return isMarked; + } + + /** + * Checks if this {@code name} contains any keywords in {code strings} + */ + public boolean nameAnyMatch(List strings) { + return name.containsString(strings); + } + + /** + * Checks if this {@code startDateTime} contains any keywords in {code strings} + */ + public boolean startTimeAnyMatch(List strings) { + return startDateAndTime.containsString(strings); + } + + /** + * Checks if this {@code endDateTime} contains any keywords in {code strings} + */ + public boolean endTimeAnyMatch(List strings) { + return (endDateAndTime != null) && endDateAndTime.containsString(strings); + } + + /** + * Checks if this {@code address} contains any keywords in {code strings} + */ + public boolean addressAnyMatch(List strings) { + return (address != null) && address.containsString(strings); + } + + /** + * Checks if this {@code description} contains any keywords in {code strings} + */ + public boolean descriptionAnyMatch(List strings) { + return (description != null) && description.containsString(strings); + } + + /** + * Checks if this {@code zoomLink} contains any keywords in {code strings} + */ + public boolean zoomLinkAnyMatch(List strings) { + return (zoomLink != null) && zoomLink.containsString(strings); + } + + /** + * Checks if at least one of the {@code tags} contains any keywords in {code strings} + */ + public boolean anyTagsContain(List strings) { + return tags.stream().anyMatch(tag -> tag.containsString(strings)); + } + /** + * Returns true if both events have the same name. + * This defines a weaker notion of equality between two events. + */ + public boolean isSameEvent(Event otherEvent) { + if (otherEvent == this) { + return true; + } + + return otherEvent != null + && otherEvent.getName().equals(getName()); + } + + /** + * Checks if the event is linked to a particular contact. + */ + public boolean hasLinkTo(Contact contact) { + UUID contactUuid = contact.getUuid(); + return linkedContacts.contains(contactUuid); + } + + /** + * Links the contact to the event object that calls this method. + * @param contact The contact to be linked with. + * @return The event that has link to the contact passed in as parameter. + */ + public Event linkTo(Contact contact) { + Set updatedLinkedContacts = new HashSet<>(linkedContacts); + updatedLinkedContacts.add(contact.getUuid()); + Event updatedEvent = new Event(name, startDateAndTime, endDateAndTime, description, address, zoomLink, tags, + uuid, updatedLinkedContacts, isMarked); + addToMap(updatedEvent); // must update the map to represent the latest changes + return updatedEvent; + } + + /** + * Adds the event to the hashmap that stores references to all events. + * If the hashmap already contains the UUID of the event as key, the value associated to the + * key will be replaced to the event passed as parameter to this method. + * @param event The event to be added. + */ + public static void addToMap(Event event) { + map.put(event.getUuid(), event); + } + + /** + * Returns an event with the unique UUID that is passed in to the method. + * The UUID passed as parameter MUST be a valid UUID that is stored in the hashmap as a key. + * @param uuid The unique identifier for an event in the hashmap. + */ + public static Event findByUuid(UUID uuid) { + assert map.containsKey(uuid) : "The uuid must be valid and already in the hashmap as a key."; + return map.get(uuid); + } + + /** + * Removes the link between the contact and the event object that calls this method. + * @param contact The contact to be unlinked. + * @return The event that has link to the contact passed in as parameter. + */ + public Event unlink(Contact contact) { + Set updatedLinkedContacts = new HashSet<>(linkedContacts); + updatedLinkedContacts.remove(contact.getUuid()); + Event updatedEvent = new Event(name, startDateAndTime, endDateAndTime, description, address, zoomLink, tags, + uuid, updatedLinkedContacts, isMarked); + addToMap(updatedEvent); + return updatedEvent; + } + + /** + * Removes all links to the event object that calls this method. + * @return The event that has no link to any contacts. + */ + public Event clearAllLinks() { + Event updatedEvent = new Event(name, startDateAndTime, endDateAndTime, description, address, zoomLink, tags, + uuid, new HashSet<>(), isMarked); + addToMap(updatedEvent); + return updatedEvent; + } + + /** + * Marks an event object that calls this method as true. + * @return The marked event. + */ + public Event markEvent() { + Event updatedEvent = new Event(name, startDateAndTime, endDateAndTime, description, address, zoomLink, tags, + uuid, linkedContacts, true); + addToMap(updatedEvent); + return updatedEvent; + } + + /** + * Removes the mark an event object that calls this method as true. + * @return The un-marked event. + */ + public Event unmarkEvent() { + Event updatedEvent = new Event(name, startDateAndTime, endDateAndTime, description, address, zoomLink, tags, + uuid, linkedContacts, false); + addToMap(updatedEvent); + return updatedEvent; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(getName()) + .append("; Start: ") + .append(getStartDateAndTime()) + .append(getEndDateAndTime() != null ? "; End: " + getEndDateAndTime() : "") // optional + .append(getDescription() != null ? "; Description: " + getDescription() : "") // optional + .append(getAddress() != null ? "; Address: " + getAddress() : "") // optional + .append(getZoomLink() != null ? "; Zoom Link: " + getZoomLink() : ""); // optional + + Set tags = getTags(); + if (!tags.isEmpty()) { + builder.append("; Tags: "); + tags.forEach(builder::append); + } + return builder.toString(); + } + + /** + * Returns true if both events have the same details. + * This defines a stronger notion of equality between two events. + */ + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + Event event = (Event) other; + return Objects.equals(getName(), event.getName()) + && Objects.equals(getStartDateAndTime(), event.getStartDateAndTime()) + && Objects.equals(getEndDateAndTime(), event.getEndDateAndTime()) + && Objects.equals(getDescription(), event.getDescription()) + && Objects.equals(getAddress(), event.getAddress()) + && Objects.equals(getZoomLink(), event.getZoomLink()) + && Objects.equals(getTags(), event.getTags()) + && Objects.equals(getIsMarked(), event.getIsMarked()); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(name, startDateAndTime, endDateAndTime, description, + address, zoomLink, tags); + } +} diff --git a/src/main/java/seedu/address/model/event/EventChanger.java b/src/main/java/seedu/address/model/event/EventChanger.java new file mode 100644 index 00000000000..717964d7079 --- /dev/null +++ b/src/main/java/seedu/address/model/event/EventChanger.java @@ -0,0 +1,180 @@ +package seedu.address.model.event; + +import java.util.Objects; + +/** + * Contains the changes to an event so that it will be updated in the calendar UI. + */ +public abstract class EventChanger { + private final Event oldEvent; + private final Event newEvent; + + /** + * Creates an event changer with the following parameters. + * @param oldEvent the initial event. + * @param newEvent the edited or new event. + */ + private EventChanger(Event oldEvent, Event newEvent) { + this.oldEvent = oldEvent; + this.newEvent = newEvent; + } + + public static EventChanger clearEventChanger() { + return ClearEventChanger.CLEAR_EVENT_CHANGER; + } + + public static EventChanger deleteEventChanger(Event event) { + return new DeleteEventChanger(event); + } + + public static EventChanger editEventChanger(Event oldEvent, Event newEvent) { + return new EditEventChanger(oldEvent, newEvent); + } + + public static EventChanger addEventChanger(Event event) { + return new AddEventChanger(event); + } + + public Event getOldEvent() { + return oldEvent; + } + + public Event getNewEvent() { + return newEvent; + } + + public abstract boolean isClearing(); + + public abstract boolean isDeleting(); + + public abstract boolean isEditing(); + + public abstract boolean isAdding(); + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + EventChanger eventChanger = (EventChanger) other; + return isClearing() == eventChanger.isClearing() && Objects.equals(getOldEvent(), eventChanger.getOldEvent()) + && Objects.equals(getNewEvent(), eventChanger.getNewEvent()); + } + + @Override + public int hashCode() { + return Objects.hash(isClearing(), getOldEvent(), getNewEvent()); + } + + private static class ClearEventChanger extends EventChanger { + private static final ClearEventChanger CLEAR_EVENT_CHANGER = new ClearEventChanger(); + + private ClearEventChanger() { + super(null, null); + } + + @Override + public boolean isClearing() { + return true; + } + + @Override + public boolean isDeleting() { + return false; + } + + @Override + public boolean isEditing() { + return false; + } + + @Override + public boolean isAdding() { + return false; + } + } + + private static class DeleteEventChanger extends EventChanger { + + private DeleteEventChanger(Event oldEvent) { + super(oldEvent, null); + } + + @Override + public boolean isClearing() { + return false; + } + + @Override + public boolean isDeleting() { + return true; + } + + @Override + public boolean isEditing() { + return false; + } + + @Override + public boolean isAdding() { + return false; + } + } + + private static class EditEventChanger extends EventChanger { + + private EditEventChanger(Event oldEvent, Event newEvent) { + super(oldEvent, newEvent); + } + + @Override + public boolean isClearing() { + return false; + } + + @Override + public boolean isDeleting() { + return false; + } + + @Override + public boolean isEditing() { + return true; + } + + @Override + public boolean isAdding() { + return false; + } + } + + private static class AddEventChanger extends EventChanger { + + private AddEventChanger(Event event) { + super(null, event); + } + + @Override + public boolean isClearing() { + return false; + } + + @Override + public boolean isDeleting() { + return false; + } + + @Override + public boolean isEditing() { + return false; + } + + @Override + public boolean isAdding() { + return true; + } + } +} diff --git a/src/main/java/seedu/address/model/event/EventContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/event/EventContainsKeywordsPredicate.java new file mode 100644 index 00000000000..a8641b4d1f9 --- /dev/null +++ b/src/main/java/seedu/address/model/event/EventContainsKeywordsPredicate.java @@ -0,0 +1,83 @@ +package seedu.address.model.event; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +/** + * Tests that a {@code Event}'s {@code Name} matches any of the keywords given. + */ +public class EventContainsKeywordsPredicate implements Predicate { + private final List nameKeywords; + private List startDateTimeKeywords = new ArrayList<>(); + private List endDateTimeKeywords = new ArrayList<>(); + private List descriptionKeywords = new ArrayList<>(); + private List addressKeywords = new ArrayList<>(); + private List zoomLinkKeywords = new ArrayList<>(); + private List tagKeywords = new ArrayList<>(); + + /** + * Creates a {@code EventContainsKeywordsPredicate} object with the name keywords. + */ + public EventContainsKeywordsPredicate(List nameKeywords) { + requireNonNull(nameKeywords); + this.nameKeywords = nameKeywords; + } + + /** + * Creates a {@code EventContainsKeywordsPredicate} object with no keywords. + */ + public EventContainsKeywordsPredicate() { + nameKeywords = new ArrayList<>(); + } + + public void setStartDateTimeKeywords(List startDateTimeKeywords) { + requireNonNull(startDateTimeKeywords); + this.startDateTimeKeywords = startDateTimeKeywords; + } + public void setEndDateTimeKeywords(List endDateTimeKeywords) { + requireNonNull(endDateTimeKeywords); + this.endDateTimeKeywords = endDateTimeKeywords; + } + public void setAddressKeywords(List addressKeywords) { + requireNonNull(addressKeywords); + this.addressKeywords = addressKeywords; + } + public void setDescriptionKeywords(List descriptionKeywords) { + requireNonNull(descriptionKeywords); + this.descriptionKeywords = descriptionKeywords; + } + public void setZoomLinkKeywords(List zoomLinkKeywords) { + requireNonNull(zoomLinkKeywords); + this.zoomLinkKeywords = zoomLinkKeywords; + } + public void setTagKeywords(List tagKeywords) { + requireNonNull(tagKeywords); + this.tagKeywords = tagKeywords; + } + + @Override + public boolean test(Event event) { + requireNonNull(event); + return event.nameAnyMatch(nameKeywords) || event.startTimeAnyMatch(startDateTimeKeywords) + || event.endTimeAnyMatch(endDateTimeKeywords) || event.addressAnyMatch(addressKeywords) + || event.descriptionAnyMatch(descriptionKeywords) || event.zoomLinkAnyMatch(zoomLinkKeywords) + || event.anyTagsContain(tagKeywords); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof EventContainsKeywordsPredicate // instanceof handles nulls + && nameKeywords.equals(((EventContainsKeywordsPredicate) other).nameKeywords) + && startDateTimeKeywords.equals(((EventContainsKeywordsPredicate) other).startDateTimeKeywords) + && endDateTimeKeywords.equals(((EventContainsKeywordsPredicate) other).endDateTimeKeywords) + && addressKeywords.equals(((EventContainsKeywordsPredicate) other).addressKeywords) + && descriptionKeywords.equals(((EventContainsKeywordsPredicate) other).descriptionKeywords) + && zoomLinkKeywords.equals(((EventContainsKeywordsPredicate) other).zoomLinkKeywords) + && tagKeywords.equals(((EventContainsKeywordsPredicate) other).tagKeywords)); // state check + } +} + diff --git a/src/main/java/seedu/address/model/event/EventDisplaySetting.java b/src/main/java/seedu/address/model/event/EventDisplaySetting.java new file mode 100644 index 00000000000..31641aede6f --- /dev/null +++ b/src/main/java/seedu/address/model/event/EventDisplaySetting.java @@ -0,0 +1,114 @@ +package seedu.address.model.event; + +import java.util.Objects; + +/** + * Contains all the display settings for events in the model manager. + */ +public class EventDisplaySetting { + public static final EventDisplaySetting DEFAULT_SETTING = new EventDisplaySetting(false); + private final boolean willDisplayStartDateTime; + private final boolean willDisplayEndDateTime; + private final boolean willDisplayDescription; + private final boolean willDisplayAddress; + private final boolean willDisplayZoomLink; + private final boolean willDisplayTags; + private final boolean isViewingFull; + + /** + * Creates a new {@code EventDisplaySetting} with the given settings. + * This is usually used in {@code EListCommand}, where the {@code isViewingFull} is always false. + */ + public EventDisplaySetting( + boolean willDisplayStartDateTime, boolean willDisplayEndDateTime, boolean willDisplayDescription, + boolean willDisplayAddress, boolean willDisplayZoomLink, boolean willDisplayTags) { + this.willDisplayStartDateTime = willDisplayStartDateTime; + this.willDisplayEndDateTime = willDisplayEndDateTime; + this.willDisplayDescription = willDisplayDescription; + this.willDisplayAddress = willDisplayAddress; + this.willDisplayZoomLink = willDisplayZoomLink; + this.willDisplayTags = willDisplayTags; + this.isViewingFull = false; + } + + /** + * Creates a new {@code EventDisplaySetting} with the given settings. + * By default, all willDisplayXXX fields will be true. + */ + public EventDisplaySetting(boolean isViewingFull) { + willDisplayStartDateTime = true; + willDisplayEndDateTime = true; + willDisplayDescription = true; + willDisplayAddress = true; + willDisplayZoomLink = true; + willDisplayTags = true; + this.isViewingFull = isViewingFull; + } + + public boolean willDisplayStartDateTime() { + return willDisplayStartDateTime; + } + + public boolean willDisplayEndDateTime() { + return willDisplayEndDateTime; + } + + public boolean willDisplayDescription() { + return willDisplayDescription; + } + + public boolean willDisplayAddress() { + return willDisplayAddress; + } + + public boolean willDisplayZoomLink() { + return willDisplayZoomLink; + } + + public boolean willDisplayTags() { + return willDisplayTags; + } + + public boolean isViewingFull() { + return isViewingFull; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof EventDisplaySetting)) { + return false; + } + EventDisplaySetting that = (EventDisplaySetting) o; + return willDisplayStartDateTime() == that.willDisplayStartDateTime() + && willDisplayEndDateTime() == that.willDisplayEndDateTime() + && willDisplayDescription() == that.willDisplayDescription() + && willDisplayAddress() == that.willDisplayAddress() + && willDisplayZoomLink() == that.willDisplayZoomLink() + && willDisplayTags() == that.willDisplayTags() + && isViewingFull() == that.isViewingFull(); + } + + @Override + public int hashCode() { + return Objects.hash( + willDisplayStartDateTime(), willDisplayEndDateTime(), willDisplayDescription(), + willDisplayAddress(), willDisplayZoomLink(), willDisplayTags(), isViewingFull()); + } + + /** For debugging/logging purposes. */ + @Override + public String toString() { + return "EventDisplaySetting{" + + "willDisplayStartDateTime=" + willDisplayStartDateTime + + ", willDisplayEndDateTime=" + willDisplayEndDateTime + + ", willDisplayDescription=" + willDisplayDescription + + ", willDisplayAddress=" + willDisplayAddress + + ", willDisplayZoomLink=" + willDisplayZoomLink + + ", willDisplayTags=" + willDisplayTags + + ", isViewingFull=" + isViewingFull + + '}'; + } +} diff --git a/src/main/java/seedu/address/model/event/StartDateTime.java b/src/main/java/seedu/address/model/event/StartDateTime.java new file mode 100644 index 00000000000..83440226fb3 --- /dev/null +++ b/src/main/java/seedu/address/model/event/StartDateTime.java @@ -0,0 +1,17 @@ +package seedu.address.model.event; + +/** + * Represents start time of an event in the event list. + * Guarantees: immutable; is valid as declared in {@link #isValidTime(String)} + */ +public class StartDateTime extends DateAndTime { + + /** + * Constructs an {@code StartDateTime} + * + * @param time A valid DateAndTime + */ + public StartDateTime(String time) { + super(time); + } +} diff --git a/src/main/java/seedu/address/model/event/UniqueEventList.java b/src/main/java/seedu/address/model/event/UniqueEventList.java new file mode 100644 index 00000000000..ca9b9d9817f --- /dev/null +++ b/src/main/java/seedu/address/model/event/UniqueEventList.java @@ -0,0 +1,197 @@ +package seedu.address.model.event; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.event.exceptions.DuplicateEventException; +import seedu.address.model.event.exceptions.EventNotFoundException; +import seedu.address.model.event.exceptions.InvalidDateTimeRangeException; + +/** + * A list of events that enforces uniqueness between its elements and does not allow nulls. + * An event is considered unique by comparing using {@code Event#isSameEvent(Event)}. As such, adding and updating of + * events uses Event#isSameEvent(Event) for equality so as to ensure that the event being added or updated is + * unique in terms of event name in the UniqueEventList. However, the removal of an event uses Event#equals(Object) so + * as to ensure that the event with exactly the same fields will be removed. + *

+ * Supports a minimal set of list operations. + * + * @see Event#isSameEvent(Event) + */ +public class UniqueEventList implements Iterable { + + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Returns true if the list contains an equivalent event as the given argument. + */ + public boolean contains(Event toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSameEvent); + } + + /** + * Adds an event to the list. + * The event must not already exist in the list. + */ + public void add(Event toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateEventException(); + } + if (toAdd.getEndDateAndTime() != null && toAdd.getEndDateAndTime().isBefore(toAdd.getStartDateAndTime())) { + throw new InvalidDateTimeRangeException(); + } + internalList.add(toAdd); + } + + /** + * Replaces the event {@code target} in the list with {@code editedEvent}. + * {@code target} must exist in the list. + * The event name of {@code editedEvent} must not be the same as another existing event in the list. + */ + public void setEvent(Event target, Event editedEvent) { + requireAllNonNull(target, editedEvent); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new EventNotFoundException(); + } + if (!target.isSameEvent(editedEvent) && contains(editedEvent)) { + throw new DuplicateEventException(); + } + + if (editedEvent.getEndDateAndTime() != null + && editedEvent.getEndDateAndTime().isBefore(editedEvent.getStartDateAndTime())) { + throw new InvalidDateTimeRangeException(); + } + + internalList.set(index, editedEvent); + } + + /** + * Removes the equivalent event from the list. + * The event must exist in the list. + */ + public void remove(Event toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new EventNotFoundException(); + } + } + + public void setEvents(UniqueEventList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code events}. + * {@code events} must not contain duplicate events. + */ + public void setEvents(List events) { + requireAllNonNull(events); + if (!eventsAreUnique(events)) { + throw new DuplicateEventException(); + } + internalList.setAll(events); + } + + /** + * Sorts the events based on whether they are {@code isMarked} and {@code startDateTime} + */ + public void sortEvents() { + internalList.sort(Comparator.comparing(Event::getStartDateAndTime)); + internalList.sort(Comparator.comparing(Event::getIsMarked, Comparator.reverseOrder())); + } + + public void resetEvents() { + internalList.clear(); + } + + /** + * Moves marked events to the top of the list. + * Places the newly marked events or replaces newly unmarked events + * in the order specified in {@code events} and + * based on {@code isMarked} which signals whether this method is called by + * EMarkCommand or otherwise. + */ + public void rearrangeEventsInOrder(List events, boolean isMarked) { + ObservableList tempList = FXCollections.observableArrayList(); + if (isMarked) { + tempList.addAll(events); + tempList.addAll(internalList.filtered(event -> !events.contains(event))); + } else { + tempList.addAll(internalList.filtered(Event::getIsMarked)); + tempList.addAll(internalList.filtered(e -> !e.getIsMarked())); + } + internalList.clear(); + internalList.addAll(tempList); + } + + /** + * Update the UUID map in events. + */ + public void updateEventMap() { + for (Event event : internalList) { + Event.addToMap(event); + } + } + + /** + * Create a copy of a uniqueEventList + * + * @return a copy of a uniqueEventList + */ + public ObservableList copy() { + List eventList = new ArrayList<>(internalList); + return FXCollections.observableArrayList(eventList); + } + + /** + * 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 UniqueEventList // instanceof handles nulls + && internalList.equals(((UniqueEventList) other).internalList)); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + /** + * Returns true if {@code events} contains only unique events. + */ + private boolean eventsAreUnique(List events) { + for (int i = 0; i < events.size() - 1; i++) { + for (int j = i + 1; j < events.size(); j++) { + if (events.get(i).isSameEvent(events.get(j))) { + return false; + } + } + } + return true; + } +} diff --git a/src/main/java/seedu/address/model/event/exceptions/DuplicateEventException.java b/src/main/java/seedu/address/model/event/exceptions/DuplicateEventException.java new file mode 100644 index 00000000000..fb5c169b8e2 --- /dev/null +++ b/src/main/java/seedu/address/model/event/exceptions/DuplicateEventException.java @@ -0,0 +1,11 @@ +package seedu.address.model.event.exceptions; + +/** + * Signals that the operation will result in duplicate Events (Events are considered duplicates if they have the same + * details). + */ +public class DuplicateEventException extends RuntimeException { + public DuplicateEventException() { + super("Operation would result in duplicate events"); + } +} diff --git a/src/main/java/seedu/address/model/event/exceptions/EventNotFoundException.java b/src/main/java/seedu/address/model/event/exceptions/EventNotFoundException.java new file mode 100644 index 00000000000..57b7e515f9e --- /dev/null +++ b/src/main/java/seedu/address/model/event/exceptions/EventNotFoundException.java @@ -0,0 +1,7 @@ +package seedu.address.model.event.exceptions; + +/** + * Signals that the operation is unable to find the specified event. + */ +public class EventNotFoundException extends RuntimeException { +} diff --git a/src/main/java/seedu/address/model/event/exceptions/InvalidDateTimeRangeException.java b/src/main/java/seedu/address/model/event/exceptions/InvalidDateTimeRangeException.java new file mode 100644 index 00000000000..f67bb3364b4 --- /dev/null +++ b/src/main/java/seedu/address/model/event/exceptions/InvalidDateTimeRangeException.java @@ -0,0 +1,10 @@ +package seedu.address.model.event.exceptions; + +/** + * Signal that operation has invalid input for start and end date time + */ +public class InvalidDateTimeRangeException extends RuntimeException { + public InvalidDateTimeRangeException() { + super("Start date time cannot be after end date time"); + } +} diff --git a/src/main/java/seedu/address/model/history/ModelHistory.java b/src/main/java/seedu/address/model/history/ModelHistory.java new file mode 100644 index 00000000000..7628030bfd0 --- /dev/null +++ b/src/main/java/seedu/address/model/history/ModelHistory.java @@ -0,0 +1,121 @@ +package seedu.address.model.history; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import seedu.address.model.ModelDisplaySetting; +import seedu.address.model.ReadOnlyAddressBook; + +/** + * Stores the past history of the model manager. Meant for the undo and redo commands. + */ +public class ModelHistory { + + private final List allHistory = new ArrayList<>(); + private int currentSize = 0; // Size of the history/Number of undo commands allowed + private int maxSize = 0; // The last point of redo + // maxSize - currentSize = Number of redo commands allowed. + + public ModelHistory() {} + + /** Resets the history. */ + public void clearHistory() { + currentSize = 0; + maxSize = 0; + } + + /** Adds a commit to the history, with the given {@code AddressBook} and {@code ModelDisplaySetting}. */ + public void commit(ReadOnlyAddressBook addressBook, ModelDisplaySetting displaySetting) { + allHistory.add(currentSize, new HistoryInstance(addressBook, displaySetting)); + currentSize++; + maxSize = currentSize; + } + + /** Performs an undo operation to move the current pointer back one position. */ + public HistoryInstance undo() { + if (!isUndoable()) { + throw new ModelHistoryException("Trying to undo even though there is no history."); + } + // Moves pointer from the latest commit to the previous commit of the new state. + currentSize--; + return getCurrentHistoryInstance(); + } + + /** Performs a redo operation to move the current pointer forward by one position. */ + public HistoryInstance redo() { + if (!isRedoable()) { + throw new ModelHistoryException("Trying to redo even though it is impossible."); + } + currentSize++; + return getCurrentHistoryInstance(); + } + + /** Returns true if it is possible to perform an undo operation here. */ + public boolean isUndoable() { + return currentSize > 1; + } + + /** Returns true if it is possible to perform a redo operation here. */ + public boolean isRedoable() { + return maxSize > currentSize; + } + + public HistoryInstance getCurrentHistoryInstance() { + return allHistory.get(currentSize - 1); + } + + /** Encapsulates a point in history, with the address book and model display setting. */ + public static class HistoryInstance { + private final ModelDisplaySetting displaySetting; + private final ReadOnlyAddressBook addressBook; + + /** Creates a new instance of history. */ + public HistoryInstance(ReadOnlyAddressBook addressBook, ModelDisplaySetting displaySetting) { + requireAllNonNull(displaySetting, addressBook); + this.displaySetting = displaySetting; + this.addressBook = addressBook; + } + + public ModelDisplaySetting getDisplaySetting() { + return displaySetting; + } + + public ReadOnlyAddressBook getAddressBook() { + return addressBook; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof HistoryInstance)) { + return false; + } + HistoryInstance that = (HistoryInstance) o; + return getDisplaySetting().equals(that.getDisplaySetting()) && getAddressBook().equals( + that.getAddressBook()); + } + + @Override + public int hashCode() { + return Objects.hash(getDisplaySetting(), getAddressBook()); + } + } + + //// FOR TESTING (PACKAGE-PRIVATE ACCESS MODIFIER) + List getAllHistory() { + return allHistory; + } + + int getCurrentSize() { + return currentSize; + } + + int getMaxSize() { + return maxSize; + } +} diff --git a/src/main/java/seedu/address/model/history/ModelHistoryException.java b/src/main/java/seedu/address/model/history/ModelHistoryException.java new file mode 100644 index 00000000000..05cf7219fbe --- /dev/null +++ b/src/main/java/seedu/address/model/history/ModelHistoryException.java @@ -0,0 +1,10 @@ +package seedu.address.model.history; + +/** + * Represents an exception when there is an issue with the {@code ModelHistory}. + */ +public class ModelHistoryException extends RuntimeException { + public ModelHistoryException(String message) { + super(message); + } +} diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java deleted file mode 100644 index c9b5868427c..00000000000 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ /dev/null @@ -1,31 +0,0 @@ -package seedu.address.model.person; - -import java.util.List; -import java.util.function.Predicate; - -import seedu.address.commons.util.StringUtil; - -/** - * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. - */ -public class NameContainsKeywordsPredicate implements Predicate { - private final List keywords; - - public NameContainsKeywordsPredicate(List keywords) { - this.keywords = keywords; - } - - @Override - public boolean test(Person person) { - return keywords.stream() - .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof NameContainsKeywordsPredicate // instanceof handles nulls - && keywords.equals(((NameContainsKeywordsPredicate) other).keywords)); // state check - } - -} 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/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/tag/Colours.java b/src/main/java/seedu/address/model/tag/Colours.java new file mode 100644 index 00000000000..ff1de6f39ef --- /dev/null +++ b/src/main/java/seedu/address/model/tag/Colours.java @@ -0,0 +1,39 @@ +package seedu.address.model.tag; + +public class Colours { + private static final String COLOUR1 = "#CD5C5C"; // red + private static final String COLOUR2 = "#808F85"; //dark green + private static final String COLOUR3 = "#DDA0DD"; // light purple + private static final String COLOUR4 = "#5F9EA0"; // blue + private static final String COLOUR5 = "#FF7F50"; // coral + private static final String COLOUR6 = "#556B2F"; // olive green + private static final String COLOUR7 = "#F39C6B"; // orange + private static final String COLOUR8 = "#8FBC8F"; // light green + private static final String COLOUR9 = "#DB7093"; // pink + private static final String COLOUR10 = "#4B0082"; // purple + private static final String COLOUR11 = "#D2B48C"; // tan + private static final String COLOUR12 = "#F4A460"; // light orange + private static final String COLOUR13 = "#800000"; // maroon + private static final String COLOUR14 = "#00008B"; // dark blue + private static final String COLOUR15 = "#008080"; // teal + + private static int colourIndex = 0; + + private static final String[] COLOURS = + new String[]{COLOUR1, COLOUR2, COLOUR3, COLOUR4, COLOUR5, COLOUR6, COLOUR7, COLOUR8, COLOUR9, COLOUR10, + COLOUR11, COLOUR12, COLOUR13, COLOUR14, COLOUR15}; + + public static final int NUMBER_OF_COLOURS = COLOURS.length; + + /** + * Get a color for tag + * @return color code + */ + public static String getTagColour() { + colourIndex++; + if (colourIndex == NUMBER_OF_COLOURS) { + colourIndex = 0; + } + return COLOURS[colourIndex]; + } +} diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java index b0ea7e7dad7..82c5da938e7 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/seedu/address/model/tag/Tag.java @@ -3,6 +3,11 @@ import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; +import java.util.HashMap; +import java.util.List; + +import seedu.address.commons.util.StringUtil; + /** * Represents a Tag in the address book. * Guarantees: immutable; name is valid as declared in {@link #isValidTagName(String)} @@ -12,8 +17,12 @@ public class Tag { public static final String MESSAGE_CONSTRAINTS = "Tags names should be alphanumeric"; public static final String VALIDATION_REGEX = "\\p{Alnum}+"; + private static HashMap addedTagList = new HashMap<>(); + public final String tagName; + public final String tagColour; + /** * Constructs a {@code Tag}. * @@ -23,6 +32,12 @@ public Tag(String tagName) { requireNonNull(tagName); checkArgument(isValidTagName(tagName), MESSAGE_CONSTRAINTS); this.tagName = tagName; + if (addedTagList.containsKey(tagName)) { + this.tagColour = addedTagList.get(tagName); + } else { + this.tagColour = Colours.getTagColour(); + addedTagList.put(tagName, tagColour); + } } /** @@ -39,6 +54,15 @@ public boolean equals(Object other) { && tagName.equals(((Tag) other).tagName)); // state check } + /** + * Checks if this {@code tagName} contains any of keywords in {@code strings} + */ + public boolean containsString(List strings) { + requireNonNull(strings); + return strings.stream().anyMatch(string -> + StringUtil.containsWordIgnoreCase(tagName, string)); + } + @Override public int hashCode() { return tagName.hashCode(); @@ -47,6 +71,7 @@ public int hashCode() { /** * Format state as text for viewing. */ + @Override public String toString() { return '[' + tagName + ']'; } diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facf..914f90994c0 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -6,45 +6,81 @@ 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.common.Address; +import seedu.address.model.common.Name; +import seedu.address.model.common.ZoomLink; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Phone; +import seedu.address.model.contact.TelegramHandle; +import seedu.address.model.event.Description; +import seedu.address.model.event.EndDateTime; +import seedu.address.model.event.Event; +import seedu.address.model.event.StartDateTime; 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"), + public static Contact[] getSampleContacts() { + return new Contact[] { + new Contact(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"), + null, new TelegramHandle("yeoh_alex"), getTagSet("friends")), + new Contact(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 ZoomLink("nus-sg.zoom.us/j/08382435376?pwd=Oow3u9N098nh8nsdLp0"), + new TelegramHandle("bbernicee"), getTagSet("TA", "friends")), + new Contact(new Name("Charlotte Oliveiro"), null, new Email("charlotte@example.com"), + null, null, null, getTagSet()), + new Contact(new Name("David Li"), null, new Email("lidavid@comp.nus.edu.sg"), + new Address("COM1-B1-0931"), + null, new TelegramHandle("lidavid"), getTagSet("professor", "CS2103T")), + new Contact(new Name("Irfan Ibrahim"), null, 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")) + null, new TelegramHandle("irfanx"), getTagSet("classmates")), + new Contact(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), + null, + new ZoomLink("nus-sg.zoom.us/j/89157903482"), null, getTagSet("TA")) + }; + } + + public static Event[] getSampleEvents() { + return new Event[] { + new Event(new Name("CS2103T project meeting"), + new StartDateTime("10-10-2021 21:00"), new EndDateTime("10-10-2021 22:00"), null, + null, new ZoomLink("nus-sg.zoom.us/j/21342513543"), + Set.of(new Tag("Recurring"), new Tag("CS2103T"))), + new Event(new Name("Basketball training"), new StartDateTime("01-11-2021 20:00"), + new EndDateTime("01-11-2021 21:00"), new Description("Meeting every week"), + new Address("NUS Sport Centre"), null, + Set.of(new Tag("Recurring"), new Tag("CCA"))), + new Event(new Name("Google Interview"), new StartDateTime("10-11-2021 15:30"), + new EndDateTime("10-11-2021 16:00"), new Description("Revise on Data Structures and Algorithms." + + " Also read up on Computer Networks and Object-Oriented Programming. I can do this!"), + null, new ZoomLink("careers.google.com/student"), + Set.of(new Tag("Internship"))), + new Event(new Name("Dance class"), new StartDateTime("13-11-2021 20:00"), + new EndDateTime("13-11-2021 22:00"), new Description("Dancing is my passion. I like pole dancing."), + new Address("NUS UTown"), null, + Set.of(new Tag("Recurring"), new Tag("CCA"))) }; } public static ReadOnlyAddressBook getSampleAddressBook() { AddressBook sampleAb = new AddressBook(); - for (Person samplePerson : getSamplePersons()) { - sampleAb.addPerson(samplePerson); + Contact[] sampleContacts = getSampleContacts(); + Event[] sampleEvents = getSampleEvents(); + for (Contact sampleContact : sampleContacts) { + sampleAb.addContact(sampleContact); + } + for (Event sampleEvent : sampleEvents) { + sampleAb.addEvent(sampleEvent); } + // Link some contacts and events + sampleAb.linkEventAndContact(sampleAb.getEventList().get(1), sampleAb.getContactList().get(0)); + sampleAb.linkEventAndContact(sampleAb.getEventList().get(1), sampleAb.getContactList().get(4)); return sampleAb; } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedContact.java b/src/main/java/seedu/address/storage/JsonAdaptedContact.java new file mode 100644 index 00000000000..3a3143509c5 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedContact.java @@ -0,0 +1,150 @@ +package seedu.address.storage; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +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.common.Address; +import seedu.address.model.common.Name; +import seedu.address.model.common.ZoomLink; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Phone; +import seedu.address.model.contact.TelegramHandle; +import seedu.address.model.tag.Tag; + +/** + * Jackson-friendly version of {@link Contact}. + */ +class JsonAdaptedContact { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Contact's %s field is missing!"; + + private final String name; + private final String phone; + private final String email; + private final String address; + private final String telegramHandle; + private final String zoomLink; + private final String uuid; + private final List tagged = new ArrayList<>(); + private final List linkedEvents = new ArrayList<>(); + private final boolean isMarked; + + /** + * Constructs a {@code JsonAdaptedContact} with the given contact details. + */ + @JsonCreator + public JsonAdaptedContact(@JsonProperty("name") String name, @JsonProperty("phone") String phone, + @JsonProperty("email") String email, @JsonProperty("address") String address, + @JsonProperty("telegramHandle") String telegramHandle, @JsonProperty("zoomLink") String zoomLink, + @JsonProperty("tagged") List tagged, @JsonProperty("uuid") String uuid, + @JsonProperty("linkedEvents") List linkedEvents, + @JsonProperty("isMarked") boolean isMarked) { + this.name = name; + this.phone = phone; + this.email = email; + this.address = address; + this.telegramHandle = telegramHandle; + this.zoomLink = zoomLink; + this.uuid = uuid; + if (tagged != null) { + this.tagged.addAll(tagged); + } + if (linkedEvents != null) { + this.linkedEvents.addAll(linkedEvents); + } + this.isMarked = isMarked; + } + + /** + * Converts a given {@code Contact} into this class for Jackson use. + */ + public JsonAdaptedContact(Contact source) { + // compulsory fields + name = source.getName().fullName; + email = source.getEmail().value; + // optional fields + phone = source.getPhone() != null ? source.getPhone().value : null; + address = source.getAddress() != null ? source.getAddress().value : null; + telegramHandle = source.getTelegramHandle() != null ? source.getTelegramHandle().handle : null; + zoomLink = source.getZoomLink() != null ? source.getZoomLink().link : null; + uuid = source.getUuid().toString(); + tagged.addAll(source.getTags().stream() + .map(JsonAdaptedTag::new) + .collect(Collectors.toList())); + linkedEvents.addAll(source.getLinkedEvents().stream() + .map(UUID::toString) + .collect(Collectors.toList())); + isMarked = source.getIsMarked(); + } + + /** + * Converts this Jackson-friendly adapted contact object into the model's {@code Contact} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted contact. + */ + public Contact toModelType() throws IllegalValueException { + final List contactTags = new ArrayList<>(); + for (JsonAdaptedTag tag : tagged) { + contactTags.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 (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 (phone != null && !Phone.isValidPhone(phone)) { + throw new IllegalValueException(Phone.MESSAGE_CONSTRAINTS); + } + final Phone modelPhone = phone != null ? new Phone(phone) : null; + + + if (address != null && !Address.isValidAddress(address)) { + throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); + } + final Address modelAddress = address != null ? new Address(address) : null; + + if (telegramHandle != null && !TelegramHandle.isValidHandle(telegramHandle)) { + throw new IllegalValueException(TelegramHandle.MESSAGE_CONSTRAINTS); + } + final TelegramHandle modelTelegramHandle = telegramHandle != null ? new TelegramHandle(telegramHandle) : null; + + if (zoomLink != null && !ZoomLink.isValidZoomLink(zoomLink)) { + throw new IllegalValueException(ZoomLink.MESSAGE_CONSTRAINTS); + } + final ZoomLink modelZoomLink = zoomLink != null ? new ZoomLink(zoomLink) : null; + + if (uuid == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, UUID.class.getSimpleName())); + } + final UUID modelUuid = UUID.fromString(uuid); + + final Set modelTags = new HashSet<>(contactTags); + final Set modelLinkedEvents = new HashSet<>( + linkedEvents.stream().map(UUID::fromString).collect(Collectors.toList())); + + return new Contact(modelName, modelPhone, modelEmail, modelAddress, modelZoomLink, + modelTelegramHandle, modelTags, modelUuid, modelLinkedEvents, isMarked); + } + +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedEvent.java b/src/main/java/seedu/address/storage/JsonAdaptedEvent.java new file mode 100644 index 00000000000..95457c6ca7a --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedEvent.java @@ -0,0 +1,151 @@ +package seedu.address.storage; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +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.common.Address; +import seedu.address.model.common.Name; +import seedu.address.model.common.ZoomLink; +import seedu.address.model.event.Description; +import seedu.address.model.event.EndDateTime; +import seedu.address.model.event.Event; +import seedu.address.model.event.StartDateTime; +import seedu.address.model.tag.Tag; + +/** + * Jackson-friendly version of {@link Event}. + */ +class JsonAdaptedEvent { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Event's %s field is missing!"; + + private final String name; + private final String startDateTime; + private final String endDateTime; + private final String description; + private final String address; + private final String zoomLink; + private final String uuid; + private final List tagged = new ArrayList<>(); + private final List linkedContacts = new ArrayList<>(); + private final boolean isMarked; + + /** + * Constructs a {@code JsonAdaptedEvent} with the given event details. + */ + @JsonCreator + public JsonAdaptedEvent(@JsonProperty("name") String name, @JsonProperty("start") String startDateTime, + @JsonProperty("end") String endDateTime, @JsonProperty("description") String description, + @JsonProperty("address") String address, @JsonProperty("zoom") String zoomLink, + @JsonProperty("tagged") List tagged, + @JsonProperty("uuid") String uuid, + @JsonProperty("linkedContacts") List linkedContacts, + @JsonProperty("isMarked") boolean isMarked) { + this.name = name; + this.startDateTime = startDateTime; + this.endDateTime = endDateTime; + this.description = description; + this.address = address; + this.zoomLink = zoomLink; + this.uuid = uuid; + if (tagged != null) { + this.tagged.addAll(tagged); + } + if (linkedContacts != null) { + this.linkedContacts.addAll(linkedContacts); + } + this.isMarked = isMarked; + } + + /** + * Converts a given {@code Event} into this class for Jackson use. + */ + public JsonAdaptedEvent(Event source) { + // compulsory fields + name = source.getName().fullName; + startDateTime = source.getStartDateAndTime().toString(); + // optional fields + endDateTime = source.getEndDateAndTime() != null ? source.getEndDateAndTime().toString() : null; + description = source.getDescription() != null ? source.getDescription().value : null; + address = source.getAddress() != null ? source.getAddress().value : null; + zoomLink = source.getZoomLink() != null ? source.getZoomLink().link : null; + uuid = source.getUuid().toString(); + tagged.addAll(source.getTags().stream() + .map(JsonAdaptedTag::new) + .collect(Collectors.toList())); + linkedContacts.addAll(source.getLinkedContacts().stream() + .map(UUID::toString) + .collect(Collectors.toList())); + isMarked = source.getIsMarked(); + } + + /** + * Converts this Jackson-friendly adapted event object into the model's {@code Event} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted event. + */ + public Event toModelType() throws IllegalValueException { + final List eventTags = new ArrayList<>(); + for (JsonAdaptedTag tag : tagged) { + eventTags.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 (startDateTime == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + StartDateTime.class.getSimpleName())); + } + if (!StartDateTime.isValidDateTime(startDateTime)) { + throw new IllegalValueException(StartDateTime.MESSAGE_CONSTRAINTS); + } + final StartDateTime modelStartDateTime = new StartDateTime(startDateTime); + + if (endDateTime != null && !EndDateTime.isValidDateTime(endDateTime)) { + throw new IllegalValueException(EndDateTime.MESSAGE_CONSTRAINTS); + } + final EndDateTime modelEndDateTime = endDateTime == null ? null : new EndDateTime(endDateTime); + + if (description != null && !Description.isValidDescription(description)) { + throw new IllegalValueException(Description.MESSAGE_CONSTRAINTS); + } + final Description modelDescription = description == null ? null : new Description(description); + + if (address != null && !Address.isValidAddress(address)) { + throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); + } + final Address modelAddress = address == null ? null : new Address(address); + + if (zoomLink != null && !ZoomLink.isValidZoomLink(zoomLink)) { + throw new IllegalValueException(ZoomLink.MESSAGE_CONSTRAINTS); + } + final ZoomLink modelZoomLink = zoomLink == null ? null : new ZoomLink(zoomLink); + + if (uuid == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); + } + final UUID modelUuid = UUID.fromString(uuid); + + final Set modelTags = new HashSet<>(eventTags); + final Set modelLinkedContacts = new HashSet<>( + linkedContacts.stream().map(UUID::fromString).collect(Collectors.toList())); + + return new Event(modelName, modelStartDateTime, modelEndDateTime, modelDescription, modelAddress, modelZoomLink, + modelTags, modelUuid, modelLinkedContacts, isMarked); + } + +} 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 index dfab9daaa0d..58d34ad1a16 100644 --- a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java +++ b/src/main/java/seedu/address/storage/JsonAddressBookStorage.java @@ -65,11 +65,17 @@ public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException } /** - * Similar to {@link #saveAddressBook(ReadOnlyAddressBook)}. + * Similar to {@link AddressBookStorage#saveAddressBook(ReadOnlyAddressBook)}. * * @param filePath location of the data. Cannot be null. */ public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException { + if (addressBook == null) { + throw new NullPointerException("Address book is null."); + } + if (filePath == null) { + throw new NullPointerException("File path is null."); + } requireNonNull(addressBook); requireNonNull(filePath); diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java index 5efd834091d..54efa7cd9ca 100644 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java @@ -11,24 +11,37 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; +import seedu.address.model.contact.Contact; +import seedu.address.model.event.Event; + /** * 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)."; + public static final String MESSAGE_DUPLICATE_CONTACT = "Contacts list contains duplicate contact(s)."; + + public static final String MESSAGE_DUPLICATE_EVENT = "Events list contains duplicate event(s)."; - private final List persons = new ArrayList<>(); + private final List contacts = new ArrayList<>(); + + private final List events = new ArrayList<>(); /** - * Constructs a {@code JsonSerializableAddressBook} with the given persons. + * Constructs a {@code JsonSerializableAddressBook} with the given contacts and events. */ @JsonCreator - public JsonSerializableAddressBook(@JsonProperty("persons") List persons) { - this.persons.addAll(persons); + public JsonSerializableAddressBook(@JsonProperty("contacts") List contacts, + @JsonProperty("events") List events) { + if (contacts != null) { + this.contacts.addAll(contacts); + } + if (events != null) { + this.events.addAll(events); + } } /** @@ -37,7 +50,8 @@ public JsonSerializableAddressBook(@JsonProperty("persons") List { + private static final String FXML = "CalendarWindow.fxml"; + private static final Logger logger = LogsCenter.getLogger(CalendarWindow.class); + + private CalendarView calendarView = new CalendarView(); + private final Calendar calendarOfEvents = new Calendar("Events"); + + private final Map> mapOfCalendarEntries = new HashMap<>(); + + @FXML + private StackPane calendarUi; + + /** + * Creates a {@code CalendarWindow} with the given {@code ObservableList}. + */ + public CalendarWindow(ObservableList events) { + super(FXML, new Stage()); + createTimeThread(); + calendarView.setMinWidth(800); + createCalendar(events); + updateCalendarView(); + } + + private void updateCalendarView() { + // Adding calendar to the UI + CalendarSource calendarSource = new CalendarSource(); + calendarSource.getCalendars().add(calendarOfEvents); + calendarView.getCalendarSources().add(calendarSource); + + // Setting calendar view options + calendarView.setRequestedTime(LocalTime.now()); + calendarView.showWeekPage(); + calendarView.setShowAddCalendarButton(false); + calendarView.setShowPageToolBarControls(false); + calendarView.setShowPrintButton(false); + calendarView.setShowSearchField(false); + calendarView.setShowSourceTrayButton(false); + calendarView.setEntryContextMenuCallback(param -> null); + calendarView.setContextMenuCallback(param -> null); + calendarUi.getChildren().add(calendarView); + } + + /** Creates a calendar with all entries from {@code events}. */ + public void createCalendar(List events) { + calendarOfEvents.setReadOnly(true); + events.forEach(this::createEntry); + } + + /** + * Creates a new calendar entry and adds them to the calendar based on {@code event}. + */ + private void createEntry(Event event) { + if (event == null) { + return; + } + DateAndTime start = event.getStartDateAndTime(); + DateAndTime end = event.getEndDateAndTime(); + if (end == null) { + String newEndString = start.getDateTime().plusHours(1) + .format(DateAndTime.DATE_TIME_FORMATTER); + end = new EndDateTime(newEndString); + } + Entry entry = new Entry<>(); + entry.setTitle(event.getName().fullName); + entry.setMinimumDuration(Duration.ZERO); + entry.setInterval(start.time, end.time); + entry.setCalendar(calendarOfEvents); + mapOfCalendarEntries.put(event, entry); + } + + /** + * Deletes a calendar entry from the calendar the calendar based on {@code event}. + */ + private void deleteEntry(Event event) { + Entry entryToDelete = mapOfCalendarEntries.remove(event); + assert entryToDelete != null : "Entry must exist before it can be deleted"; + calendarOfEvents.removeEntry(entryToDelete); + } + + /** Deletes all entries in the calendar. */ + public void clearCalendar() { + calendarOfEvents.clear(); + } + + /** Updates the calendar according the events being changed. */ + public void updateCalendar(List eventChangerList) { + for (EventChanger eventChanger : eventChangerList) { + if (eventChanger.isClearing()) { + clearCalendar(); + return; + } + if (eventChanger.isAdding()) { + createEntry(eventChanger.getNewEvent()); + continue; + } + if (eventChanger.isEditing()) { + deleteEntry(eventChanger.getOldEvent()); + createEntry(eventChanger.getNewEvent()); + continue; + } + if (eventChanger.isDeleting()) { + deleteEntry(eventChanger.getOldEvent()); + } + } + } + + /** + * Shows the calendar window. + * @throws IllegalStateException + *

    + *
  • + * if this method is called on a thread other than the JavaFX Application Thread. + *
  • + *
  • + * if this method is called during animation or layout processing. + *
  • + *
  • + * if this method is called on the primary stage. + *
  • + *
  • + * if {@code dialogStage} is already showing. + *
  • + *
+ */ + public void show() { + logger.fine("Showing calendar."); + getRoot().show(); + getRoot().centerOnScreen(); + } + + /** + * Returns true if the calendar window is currently being shown. + */ + public boolean isShowing() { + return getRoot().isShowing(); + } + + /** + * Hides the calendar window. + */ + public void close() { + getRoot().close(); + } + + /** + * Adapted from CalendarFX developer manual. + * http://dlsc.com/wp-content/html/calendarfx/manual.html#_quick_start + */ + private void createTimeThread() { + Thread updateTimeThread = new Thread("Calendar: Update Time Thread") { + @Override + public void run() { + while (true) { + Platform.runLater(() -> { + calendarView.setToday(LocalDate.now()); + calendarView.setTime(LocalTime.now()); + }); + try { + // update every 10 seconds (equivalent to 10000 milliseconds) + sleep(10000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + }; + + updateTimeThread.setPriority(Thread.MIN_PRIORITY); + updateTimeThread.setDaemon(true); + updateTimeThread.start(); + } +} diff --git a/src/main/java/seedu/address/ui/ContactCard.java b/src/main/java/seedu/address/ui/ContactCard.java new file mode 100644 index 00000000000..c93f038e4a3 --- /dev/null +++ b/src/main/java/seedu/address/ui/ContactCard.java @@ -0,0 +1,288 @@ +package seedu.address.ui; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.awt.Desktop; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Comparator; +import java.util.UUID; +import java.util.logging.Logger; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.Messages; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.ContactDisplaySetting; +import seedu.address.model.event.Event; + + +/** + * An UI component that displays information of a {@code Contact}. + */ +public class ContactCard extends UiPart { + + private static final String FXML = "ContactListCard.fxml"; + + private static Logger logger = LogsCenter.getLogger(ContactCard.class); + + /** + * 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 Contact contact; + + private MainWindow mainWindow; + + private boolean isShowLinks = false; + + @FXML + private HBox cardPane; + + @FXML + private Label name; + + @FXML + private Label favourite; + + @FXML + private Label id; + + @FXML + private Label phone; + + @FXML + private Label address; + + @FXML + private Label email; + + @FXML + private Label telegramHandleTitle; + + @FXML + private Label telegramHandle; + + @FXML + private Label zoom; + + @FXML + private Label tagIcon; + + @FXML + private FlowPane tags; + + @FXML + private Label linkToEvent; + + @FXML + private FlowPane links; + + @FXML + private HBox linksHBox; + /** + * Creates a {@code ContactCard} with the given {@code Contact} and index to display. + */ + public ContactCard( + Contact contact, int displayedIndex, MainWindow mainWindow, + ContactDisplaySetting displaySetting) { + super(FXML); + requireAllNonNull(contact, displayedIndex, mainWindow); + this.mainWindow = mainWindow; + this.contact = contact; + boolean isViewMode = displaySetting.isViewingFull(); + + id.setText(displayedIndex + ". "); + name.setText(contact.getName().fullName); + name.setWrapText(isViewMode); + + // Compulsory fields + if (displaySetting.willDisplayEmail()) { + email.setText(contact.getEmail().value); + email.setManaged(true); + email.setVisible(true); + email.setWrapText(isViewMode); + } + // Optional fields + if (contact.getPhone() != null && displaySetting.willDisplayPhone()) { + phone.setText(contact.getPhone().value); + phone.setManaged(true); + phone.setVisible(true); + phone.setWrapText(isViewMode); + } + if (contact.getAddress() != null && displaySetting.willDisplayAddress()) { + address.setText(contact.getAddress().value); + address.setManaged(true); + address.setVisible(true); + address.setWrapText(isViewMode); + } + if (contact.getTelegramHandle() != null && displaySetting.willDisplayTelegramHandle()) { + telegramHandle.setText(contact.getTelegramHandle().handle); + telegramHandle.setManaged(true); + telegramHandle.setVisible(true); + telegramHandle.setWrapText(isViewMode); + } + if (contact.getZoomLink() != null && displaySetting.willDisplayZoomLink()) { + zoom.setText(contact.getZoomLink().link); + zoom.setManaged(true); + zoom.setVisible(true); + zoom.setWrapText(isViewMode); + } + if (displaySetting.willDisplayTags() && !contact.getTags().isEmpty()) { + contact.getTags().stream() + .sorted(Comparator.comparing(tag -> tag.tagName)) + .forEach(tag -> { + Label label = new Label(tag.tagName); + label.setStyle("-fx-background-color: " + tag.tagColour + ";"); + label.setWrapText(isViewMode); + tags.getChildren().add(label); + }); + tagIcon.setManaged(true); + tagIcon.setVisible(true); + tags.setManaged(true); + } + + if (!contact.getLinkedEvents().isEmpty()) { + contact.getLinkedEvents().stream() + .sorted(Comparator.comparing(UUID::toString)) + .forEach(eventUuid -> { + String eventName = Event.findByUuid(eventUuid).getName().toString(); + links.getChildren().add(new Label(eventName)); + }); + linkToEvent.setManaged(true); + linkToEvent.setVisible(true); + links.setManaged(true); + + } + + linkToEvent.addEventHandler(MouseEvent.MOUSE_CLICKED, this::toggleShowLinks); + links.addEventHandler(MouseEvent.MOUSE_CLICKED, this::toggleShowLinks); + + if (contact.getIsMarked()) { + favourite.setManaged(true); + favourite.setVisible(true); + } + } + + private void toggleShowLinks(MouseEvent e) { + if (isShowLinks) { + mainWindow.showAllEvents(); + } else { + mainWindow.showLinksOfContact(contact); + } + isShowLinks = !isShowLinks; + } + + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ContactCard)) { + return false; + } + + // state check + ContactCard card = (ContactCard) other; + return id.getText().equals(card.id.getText()) + && contact.equals(card.contact); + } + + /** + * Copies contact fields to the clipboard. + */ + private void copy(String fieldContent, String fieldName) { + final Clipboard clipboard = Clipboard.getSystemClipboard(); + final ClipboardContent content = new ClipboardContent(); + content.putString(fieldContent); + clipboard.setContent(content); + mainWindow.handleClick(String.format(Messages.MESSAGE_CONTACT_FIELD_COPIED, fieldName)); + logger.info(String.format(Messages.MESSAGE_CONTACT_FIELD_COPIED, fieldName)); + } + + /** + * Copies contact name to the clipboard. + */ + @FXML + private void copyName() { + copy(contact.getName().fullName, "name"); + } + + /** + * Copies contact email to the clipboard. + */ + @FXML + private void copyEmail() { + copy(contact.getEmail().value, "email"); + } + + /** + * Copies contact phone number to the clipboard. + */ + @FXML + private void copyPhone() { + copy(contact.getPhone().value, "phone"); + } + + /** + * Copies contact address to the clipboard. + */ + @FXML + private void copyAddress() { + copy(contact.getAddress().value, "address"); + } + + /** + * Open contact links in browser + */ + private void openLink(String link, String fieldName) { + try { + if (!link.matches("^http(s)?://.*$")) { + link = "http://" + link; + } + if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { + Desktop.getDesktop().browse(new URI(link)); + logger.info(String.format(Messages.MESSAGE_EVENT_LINK_OPENED, fieldName)); + mainWindow.handleClick(String.format(Messages.MESSAGE_EVENT_LINK_OPENED, fieldName)); + } else { + copy(link, fieldName); + logger.warning("Desktop does not support opening URL in browser. Copied link to clipboard"); + } + } catch (URISyntaxException | IOException e) { + logger.warning(String.format(Messages.MESSAGE_CONTACT_LINK_NOT_FOUND, fieldName)); + mainWindow.handleClick(String.format(Messages.MESSAGE_CONTACT_LINK_NOT_FOUND, fieldName)); + } + } + + /** + * Open contact zoom link in browser. + */ + @FXML + private void openZoomLink() { + openLink(contact.getZoomLink().link, "zoom"); + } + + /** + * Open telegram link in browser. + */ + @FXML + private void openTelegramHandle() { + openLink(contact.getTelegramHandle().link, "telegram"); + } +} diff --git a/src/main/java/seedu/address/ui/ContactCommandSummary.java b/src/main/java/seedu/address/ui/ContactCommandSummary.java new file mode 100644 index 00000000000..f296c413f8f --- /dev/null +++ b/src/main/java/seedu/address/ui/ContactCommandSummary.java @@ -0,0 +1,100 @@ +package seedu.address.ui; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.logic.commands.contact.CAddCommand; +import seedu.address.logic.commands.contact.CClearCommand; +import seedu.address.logic.commands.contact.CDeleteCommand; +import seedu.address.logic.commands.contact.CEditCommand; +import seedu.address.logic.commands.contact.CFindCommand; +import seedu.address.logic.commands.contact.CListCommand; +import seedu.address.logic.commands.contact.CMarkCommand; +import seedu.address.logic.commands.contact.CUnmarkCommand; +import seedu.address.logic.commands.contact.CViewCommand; + +public class ContactCommandSummary { + + public static final String ADD = "Add"; + public static final String MARK = "Mark"; + public static final String CLEAR = "Clear"; + public static final String DELETE = "Delete"; + public static final String EDIT = "Edit"; + public static final String FIND = "Find\n(at least one keyword must be present)"; + public static final String LIST = "List"; + public static final String REMOVE_MARK = "Remove mark"; + public static final String VIEW = "View"; + + /** Description of what the command does. */ + private StringProperty action; + + /** Format of a valid command. */ + private StringProperty format; + + + /** + * Constructs a ContactCommandSummary object. + * + * @param action Description of what the command does. + * @param format Format of a valid command. + */ + public ContactCommandSummary(String action, String format) { + this.action = new SimpleStringProperty(action); + this.format = new SimpleStringProperty(format); + } + + /** + * Returns the description of what the command does. + * + * @return Description of what the command does. + */ + public String getAction() { + return action.get(); + } + + /** + * Returns the format of a valid command. + * + * @return Format of a valid command. + */ + public String getFormat() { + return format.get(); + } + + + /** + * Returns the StringProperty object of action. + * @return StringProperty for action. + */ + public StringProperty actionProperty() { + return action; + } + + /** + * Returns the StringProperty object of format. + * @return StringProperty for format. + */ + public StringProperty formatProperty() { + return format; + } + + + /** + * Creates and returns an observable list of command actions and formats for contact management. + * + * @return An observable list of command actions and formats for contact management. + */ + public static ObservableList getContactCommandSummary() { + return FXCollections.observableArrayList( + new ContactCommandSummary(ADD, CAddCommand.SYNTAX), + new ContactCommandSummary(CLEAR, CClearCommand.SYNTAX), + new ContactCommandSummary(DELETE, CDeleteCommand.SYNTAX), + new ContactCommandSummary(EDIT, CEditCommand.SYNTAX), + new ContactCommandSummary(FIND, CFindCommand.SYNTAX), + new ContactCommandSummary(LIST, CListCommand.SYNTAX), + new ContactCommandSummary(MARK, CMarkCommand.SYNTAX), + new ContactCommandSummary(REMOVE_MARK, CUnmarkCommand.SYNTAX), + new ContactCommandSummary(VIEW, CViewCommand.SYNTAX)); + } +} diff --git a/src/main/java/seedu/address/ui/ContactListPanel.java b/src/main/java/seedu/address/ui/ContactListPanel.java new file mode 100644 index 00000000000..1e9f9b9c6da --- /dev/null +++ b/src/main/java/seedu/address/ui/ContactListPanel.java @@ -0,0 +1,62 @@ +package seedu.address.ui; + +import java.util.function.Supplier; +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.contact.Contact; +import seedu.address.model.contact.ContactDisplaySetting; + +/** + * Panel containing the list of contacts. + */ +public class ContactListPanel extends UiPart { + private static final String FXML = "ContactListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(ContactListPanel.class); + + @FXML + private ListView contactListView; + + /** + * Creates a {@code ContactListPanel} with the given {@code ObservableList}. + */ + public ContactListPanel(ObservableList contactList, MainWindow mainWindow, + Supplier displaySettingSupplier) { + super(FXML); + contactListView.setItems(contactList); + contactListView.setCellFactory(listView -> new ContactListViewCell(mainWindow, displaySettingSupplier)); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Contact} using a {@code ContactCard}. + */ + class ContactListViewCell extends ListCell { + private final MainWindow mainWindow; + private final Supplier displaySettingSupplier; + + public ContactListViewCell( + MainWindow mainWindow, Supplier displaySettingSupplier) { + this.mainWindow = mainWindow; + this.displaySettingSupplier = displaySettingSupplier; + } + + @Override + protected void updateItem(Contact contact, boolean empty) { + super.updateItem(contact, empty); + + if (empty || contact == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new ContactCard(contact, getIndex() + 1, mainWindow, + displaySettingSupplier.get()).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/EventCard.java b/src/main/java/seedu/address/ui/EventCard.java new file mode 100644 index 00000000000..f69a45a8e8e --- /dev/null +++ b/src/main/java/seedu/address/ui/EventCard.java @@ -0,0 +1,277 @@ +package seedu.address.ui; + +import java.awt.Desktop; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Comparator; +import java.util.UUID; +import java.util.logging.Logger; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.Messages; +import seedu.address.model.contact.Contact; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventDisplaySetting; + +/** + * An UI component that displays information of an {@code Event}. + */ +public class EventCard extends UiPart { + + private static final String FXML = "EventListCard.fxml"; + + private static Logger logger = LogsCenter.getLogger(EventCard.class); + + /** + * 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 final Event event; + + private MainWindow mainWindow; + + /** True only when the user is attempting to show all links of the {@code event}. */ + private boolean isShowLinks = false; + + @FXML + private Label id; + + @FXML + private Label eventName; + + @FXML + private Label favourite; + + @FXML + private Label from; + + @FXML + private Label to; + + @FXML + private Label address; + + @FXML + private Label zoom; + + @FXML + private Label description; + + @FXML + private Label tagIcon; + + @FXML + private FlowPane tags; + + @FXML + private Label linkToContact; + + @FXML + private FlowPane links; + + @FXML + private HBox linksHBox; + + /** + * Creates an {@code EventCard} with the given {@code Event} and index to display. + */ + public EventCard( + Event event, int displayedIndex, MainWindow mainWindow, + EventDisplaySetting eventDisplaySetting) { + super(FXML); + this.event = event; + this.mainWindow = mainWindow; + + boolean isViewMode = eventDisplaySetting.isViewingFull(); + + id.setText(displayedIndex + ". "); + // compulsory fields + eventName.setText(event.getName().fullName); + eventName.setWrapText(isViewMode); + + // Compulsory fields + if (eventDisplaySetting.willDisplayStartDateTime()) { + from.setText(event.getStartDateAndTime().toString()); + from.setManaged(true); + from.setVisible(true); + from.setWrapText(isViewMode); + } + // Optional fields + if (event.getEndDateAndTime() != null && eventDisplaySetting.willDisplayEndDateTime()) { + to.setText(event.getEndDateAndTime().toString()); + to.setManaged(true); + to.setVisible(true); + to.setWrapText(isViewMode); + } + if (event.getAddress() != null && eventDisplaySetting.willDisplayAddress()) { + address.setText(event.getAddress().value); + address.setManaged(true); + address.setVisible(true); + address.setWrapText(isViewMode); + } + if (event.getZoomLink() != null && eventDisplaySetting.willDisplayZoomLink()) { + zoom.setText(event.getZoomLink().link); + zoom.setManaged(true); + zoom.setVisible(true); + zoom.setWrapText(isViewMode); + } + if (event.getDescription() != null && eventDisplaySetting.willDisplayDescription()) { + description.setText(event.getDescription().value); + description.setManaged(true); + description.setVisible(true); + description.setWrapText(isViewMode); + } + + if (eventDisplaySetting.willDisplayTags() && !event.getTags().isEmpty()) { + event.getTags().stream() + .sorted(Comparator.comparing(tag -> tag.tagName)) + .forEach(tag -> { + Label label = new Label(tag.tagName); + label.setStyle("-fx-background-color: " + tag.tagColour + ";"); + label.setWrapText(isViewMode); + tags.getChildren().add(label); + }); + tagIcon.setManaged(true); + tagIcon.setVisible(true); + tags.setManaged(true); + } + + if (!event.getLinkedContacts().isEmpty()) { + event.getLinkedContacts().stream() + .sorted(Comparator.comparing(UUID::toString)) + .forEach(contactUuid -> links.getChildren() + .add(new Label(Contact.findByUuid(contactUuid).getName().toString()))); + linkToContact.setManaged(true); + linkToContact.setVisible(true); + links.setManaged(true); + linkToContact.addEventHandler(MouseEvent.MOUSE_CLICKED, this::toggleShowLinks); + links.addEventHandler(MouseEvent.MOUSE_CLICKED, this::toggleShowLinks); + } + if (event.getIsMarked()) { + favourite.setManaged(true); + favourite.setVisible(true); + } + } + + private void toggleShowLinks(MouseEvent e) { + if (isShowLinks) { + mainWindow.showAllContacts(); + } else { + mainWindow.showLinksOfEvent(event); + } + isShowLinks = !isShowLinks; + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EventCard)) { + return false; + } + + // state check + EventCard card = (EventCard) other; + return id.getText().equals(card.id.getText()) + && event.equals(card.event); + } + + /** + * Copies event fields to the clipboard. + */ + private void copy(String fieldContent, String fieldName) { + final Clipboard clipboard = Clipboard.getSystemClipboard(); + final ClipboardContent content = new ClipboardContent(); + content.putString(fieldContent); + clipboard.setContent(content); + mainWindow.handleClick(String.format(Messages.MESSAGE_EVENT_FIELD_COPIED, fieldName)); + logger.info(String.format(Messages.MESSAGE_EVENT_FIELD_COPIED, fieldName)); + } + + /** + * Copies event name to the clipboard. + */ + @FXML + private void copyName() { + copy(event.getName().fullName, "name"); + } + + /** + * Copies event start date time to the clipboard. + */ + @FXML + private void copyStartDateTime() { + copy(event.getStartDateAndTime().toString(), "start date time"); + } + + /** + * Copies contact end date time to the clipboard. + */ + @FXML + private void copyEndDateTime() { + copy(event.getEndDateAndTime().toString(), "end date time"); + } + + /** + * Copies event description to the clipboard. + */ + @FXML + private void copyDescription() { + copy(event.getDescription().value, "description"); + } + + /** + * Copies event address to the clipboard. + */ + @FXML + private void copyAddress() { + copy(event.getAddress().value, "address"); + } + + /** + * Open event links in browser. + */ + private void openLink(String link, String fieldName) { + try { + if (!link.matches("^http(s)?://.*$")) { + link = "http://" + link; + } + if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { + Desktop.getDesktop().browse(new URI(link)); + logger.info(String.format(Messages.MESSAGE_EVENT_LINK_OPENED, fieldName)); + mainWindow.handleClick(String.format(Messages.MESSAGE_EVENT_LINK_OPENED, fieldName)); + } else { + copy(link, fieldName); + logger.warning("Desktop does not support opening URL in browser. Copied link to clipboard"); + } + } catch (URISyntaxException | IOException e) { + logger.warning(String.format(Messages.MESSAGE_EVENT_LINK_NOT_FOUND, fieldName)); + mainWindow.handleClick(String.format(Messages.MESSAGE_EVENT_LINK_NOT_FOUND, fieldName)); + } + } + + /** + * Open event zoom link in browser. + */ + @FXML + private void openZoomLink() { + openLink(event.getZoomLink().link, "zoom"); + } +} diff --git a/src/main/java/seedu/address/ui/EventCommandSummary.java b/src/main/java/seedu/address/ui/EventCommandSummary.java new file mode 100644 index 00000000000..829185c5aef --- /dev/null +++ b/src/main/java/seedu/address/ui/EventCommandSummary.java @@ -0,0 +1,112 @@ +package seedu.address.ui; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.logic.commands.event.EAddCommand; +import seedu.address.logic.commands.event.EClearCommand; +import seedu.address.logic.commands.event.EDeleteCommand; +import seedu.address.logic.commands.event.EEditCommand; +import seedu.address.logic.commands.event.EFindCommand; +import seedu.address.logic.commands.event.ELinkCommand; +import seedu.address.logic.commands.event.EListCommand; +import seedu.address.logic.commands.event.EMarkCommand; +import seedu.address.logic.commands.event.ESortCommand; +import seedu.address.logic.commands.event.EUnlinkCommand; +import seedu.address.logic.commands.event.EUnmarkCommand; +import seedu.address.logic.commands.event.EViewCommand; + + + +public class EventCommandSummary { + + public static final String ADD = "Add"; + public static final String MARK = "Mark"; + public static final String CLEAR = "Clear"; + public static final String DELETE = "Delete"; + public static final String EDIT = "Edit"; + public static final String FIND = "Find\n(at least one keyword must be present)"; + public static final String LINK = "Link"; + public static final String LIST = "List"; + public static final String REMOVE_MARK = "Remove mark"; + public static final String SORT = "Sort"; + public static final String UNLINK = "Unlink"; + public static final String VIEW = "View"; + + /** Description of what the command does. */ + private StringProperty action; + + /** Format of a valid command. */ + private StringProperty format; + + + /** + * Constructs a EventCommandSummary object. + * + * @param action Description of what the command does. + * @param format Format of a valid command. + */ + public EventCommandSummary(String action, String format) { + this.action = new SimpleStringProperty(action); + this.format = new SimpleStringProperty(format); + } + + /** + * Returns the description of what the command does. + * + * @return Description of what the command does. + */ + public String getAction() { + return action.get(); + } + + /** + * Returns the format of a valid command. + * + * @return Format of a valid command. + */ + public String getFormat() { + return format.get(); + } + + + /** + * Returns the StringProperty object of action. + * @return StringProperty for action. + */ + public StringProperty actionProperty() { + return action; + } + + /** + * Returns the StringProperty object of format. + * @return StringProperty for format. + */ + public StringProperty formatProperty() { + return format; + } + + + /** + * Creates and returns an observable list of command actions and formats for event management. + * + * @return An observable list of command actions and formats for event management. + */ + public static ObservableList getEventCommandSummary() { + return FXCollections.observableArrayList( + new EventCommandSummary(ADD, EAddCommand.SYNTAX), + new EventCommandSummary(CLEAR, EClearCommand.SYNTAX), + new EventCommandSummary(DELETE, EDeleteCommand.SYNTAX), + new EventCommandSummary(EDIT, EEditCommand.SYNTAX), + new EventCommandSummary(FIND, EFindCommand.SYNTAX), + new EventCommandSummary(LINK, ELinkCommand.SYNTAX), + new EventCommandSummary(LIST, EListCommand.SYNTAX), + new EventCommandSummary(MARK, EMarkCommand.SYNTAX), + new EventCommandSummary(REMOVE_MARK, EUnmarkCommand.SYNTAX), + new EventCommandSummary(SORT, ESortCommand.SYNTAX), + new EventCommandSummary(UNLINK, EUnlinkCommand.SYNTAX), + new EventCommandSummary(VIEW, EViewCommand.SYNTAX)); + } +} + diff --git a/src/main/java/seedu/address/ui/EventListPanel.java b/src/main/java/seedu/address/ui/EventListPanel.java new file mode 100644 index 00000000000..c2a976b3235 --- /dev/null +++ b/src/main/java/seedu/address/ui/EventListPanel.java @@ -0,0 +1,62 @@ +package seedu.address.ui; + +import java.util.function.Supplier; +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.event.Event; +import seedu.address.model.event.EventDisplaySetting; + +/** + * Panel containing the list of Event. + */ +public class EventListPanel extends UiPart { + private static final String FXML = "EventListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(EventListPanel.class); + + @FXML + private ListView eventListView; + + /** + * Creates a {@code EventListPanel} with the given {@code ObservableList}. + */ + public EventListPanel( + ObservableList eventList, MainWindow mainWindow, Supplier displaySettingSupplier) { + super(FXML); + eventListView.setItems(eventList); + eventListView.setCellFactory(listView -> new EventListViewCell(mainWindow, displaySettingSupplier)); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Event} using a {@code EventCard}. + */ + class EventListViewCell extends ListCell { + private final MainWindow mainWindow; + private final Supplier displaySettingSupplier; + + public EventListViewCell( + MainWindow mainWindow, Supplier displaySettingSupplier) { + this.mainWindow = mainWindow; + this.displaySettingSupplier = displaySettingSupplier; + } + + @Override + protected void updateItem(Event event, boolean empty) { + super.updateItem(event, empty); + + if (empty || event == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new EventCard(event, getIndex() + 1, mainWindow, + displaySettingSupplier.get()).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/GeneralCommandSummary.java b/src/main/java/seedu/address/ui/GeneralCommandSummary.java new file mode 100644 index 00000000000..966ef7e5917 --- /dev/null +++ b/src/main/java/seedu/address/ui/GeneralCommandSummary.java @@ -0,0 +1,88 @@ +package seedu.address.ui; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.logic.commands.general.CalendarCommand; +import seedu.address.logic.commands.general.ExitCommand; +import seedu.address.logic.commands.general.HelpCommand; +import seedu.address.logic.commands.general.RedoCommand; +import seedu.address.logic.commands.general.UndoCommand; + + +public class GeneralCommandSummary { + + public static final String CALENDAR = "Calendar"; + public static final String EXIT = "Exit"; + public static final String HELP = "Help"; + public static final String UNDO = "Undo"; + public static final String REDO = "Redo"; + + private StringProperty action; + + /** Format of a valid command. */ + private StringProperty format; + + + /** + * Constructs a GeneralCommandSummary object. + * + * @param action Description of what the command does. + * @param format Format of a valid command. + */ + public GeneralCommandSummary(String action, String format) { + this.action = new SimpleStringProperty(action); + this.format = new SimpleStringProperty(format); + } + + /** + * Returns the description of what the command does. + * + * @return Description of what the command does. + */ + public String getAction() { + return action.get(); + } + + /** + * Returns the format of a valid command. + * + * @return Format of a valid command. + */ + public String getFormat() { + return format.get(); + } + + + /** + * Returns the StringProperty object of action. + * @return StringProperty for action. + */ + public StringProperty actionProperty() { + return action; + } + + /** + * Returns the StringProperty object of format. + * @return StringProperty for format. + */ + public StringProperty formatProperty() { + return format; + } + + + /** + * Creates and returns an observable list of command actions and formats for contact management. + * + * @return An observable list of command actions and formats for contact management. + */ + public static ObservableList getGeneralCommandSummary() { + return FXCollections.observableArrayList( + new GeneralCommandSummary(CALENDAR, CalendarCommand.SYNTAX), + new GeneralCommandSummary(EXIT, ExitCommand.SYNTAX), + new GeneralCommandSummary(HELP, HelpCommand.SYNTAX), + new GeneralCommandSummary(REDO, RedoCommand.SYNTAX), + new GeneralCommandSummary(UNDO, UndoCommand.SYNTAX)); + } +} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 9a665915949..9f23f84d70e 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/address/ui/HelpWindow.java @@ -2,12 +2,20 @@ import java.util.logging.Logger; +import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.Button; +import javafx.scene.control.Control; import javafx.scene.control.Label; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.input.Clipboard; import javafx.scene.input.ClipboardContent; +import javafx.scene.text.Text; import javafx.stage.Stage; +import javafx.util.Callback; import seedu.address.commons.core.LogsCenter; /** @@ -15,15 +23,66 @@ */ public class HelpWindow extends UiPart { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; - public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; + public static final String USERGUIDE_URL = "https://ay2122s1-cs2103t-w15-3.github.io/tp/UserGuide.html"; + public static final String HELP_MESSAGE = "For more help, visit SoConnect User Guide: " + USERGUIDE_URL; + public static final String INTRODUCTION = "Here are all the commands that you can use in SoConnect:"; + + //headers + public static final String CONTACT_TITLE = "Contact Management"; + public static final String EVENT_TITLE = "Event Management"; + public static final String GENERAL_TITLE = "General"; + public static final String ACTION_HEADER = "Action"; + public static final String FORMAT_HEADER = "Format"; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); private static final String FXML = "HelpWindow.fxml"; + private final ObservableList contactList = + ContactCommandSummary.getContactCommandSummary(); + private final ObservableList eventList = + EventCommandSummary.getEventCommandSummary(); + private final ObservableList generalList = + GeneralCommandSummary.getGeneralCommandSummary(); + @FXML - private Button copyButton; + private Label introduction; + + //tables + @FXML + private TableView contactTable; + @FXML + private TableView eventTable; + @FXML + private TableView generalTable; + + @FXML + private TableColumn contactAction; + @FXML + private TableColumn contactFormat; + + @FXML + private TableColumn eventAction; + @FXML + private TableColumn eventFormat; + + @FXML + private TableColumn generalAction; + @FXML + private TableColumn generalFormat; + + + //headers + @FXML + private Label contactTitle; + @FXML + private Label eventTitle; + @FXML + private Label generalTitle; + + //footer + @FXML + private Button copyButton; @FXML private Label helpMessage; @@ -34,7 +93,18 @@ public class HelpWindow extends UiPart { */ public HelpWindow(Stage root) { super(FXML, root); + introduction.setText(INTRODUCTION); + + contactTitle.setText(CONTACT_TITLE); + eventTitle.setText(EVENT_TITLE); + generalTitle.setText(GENERAL_TITLE); + + setCommandSummary(contactTable, contactAction, contactFormat, contactList); + setCommandSummary(eventTable, eventAction, eventFormat, eventList); + setCommandSummary(generalTable, generalAction, generalFormat, generalList); + helpMessage.setText(HELP_MESSAGE); + } /** @@ -44,6 +114,43 @@ public HelpWindow() { this(new Stage()); } + @SuppressWarnings("unchecked") + public void setCommandSummary(TableView table, TableColumn actionHeader, + TableColumn formatHeader, ObservableList list) { + table.setItems(list); + + actionHeader = new TableColumn(ACTION_HEADER); + formatHeader = new TableColumn<>(FORMAT_HEADER); + actionHeader.setCellValueFactory(new PropertyValueFactory(ACTION_HEADER)); + formatHeader.setCellValueFactory(new PropertyValueFactory(FORMAT_HEADER)); + + table.getColumns().setAll(actionHeader, formatHeader); + + formatHeader.setCellFactory(getCallback(formatHeader)); + actionHeader.setCellFactory(getCallback(actionHeader)); + + table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + } + + + //@@author janjanchen-reused + //Reused from https://stackoverflow.com/questions/22732013/javafx-tablecolumn-text-wrapping + // with minor modifications + public Callback, + TableCell> getCallback(TableColumn header) { + return tc -> { + TableCell cell = new TableCell<>(); + Text text = new Text(); + cell.setGraphic(text); + cell.setPrefHeight(Control.USE_COMPUTED_SIZE); + text.wrappingWidthProperty().bind(header.widthProperty()); + text.textProperty().bind(cell.itemProperty()); + return cell; + }; + } + //@@author + + /** * Shows the help window. * @throws IllegalStateException diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 9106c3aa6e5..50170b5d191 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -16,6 +16,8 @@ import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.contact.Contact; +import seedu.address.model.event.Event; /** * The Main Window. Provides the basic application layout containing @@ -27,13 +29,14 @@ public class MainWindow extends UiPart { private final Logger logger = LogsCenter.getLogger(getClass()); - private Stage primaryStage; - private Logic logic; - + private final Stage primaryStage; + private final Logic logic; + private final HelpWindow helpWindow; // Independent Ui parts residing in this Ui container - private PersonListPanel personListPanel; + private ContactListPanel contactListPanel; + private EventListPanel eventListPanel; private ResultDisplay resultDisplay; - private HelpWindow helpWindow; + private CalendarWindow calendarWindow; @FXML private StackPane commandBoxPlaceholder; @@ -42,7 +45,13 @@ public class MainWindow extends UiPart { private MenuItem helpMenuItem; @FXML - private StackPane personListPanelPlaceholder; + private MenuItem calendarItem; + + @FXML + private StackPane contactListPanelPlaceholder; + + @FXML + private StackPane eventListPanelPlaceholder; @FXML private StackPane resultDisplayPlaceholder; @@ -74,10 +83,12 @@ public Stage getPrimaryStage() { private void setAccelerators() { setAccelerator(helpMenuItem, KeyCombination.valueOf("F1")); + setAccelerator(calendarItem, KeyCombination.valueOf("F2")); } /** * Sets the accelerator of a MenuItem. + * * @param keyCombination the KeyCombination value of the accelerator */ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { @@ -110,8 +121,11 @@ 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()); + contactListPanel = new ContactListPanel(logic.getFilteredContactList(), this, logic::getContactDisplaySetting); + contactListPanelPlaceholder.getChildren().add(contactListPanel.getRoot()); + + eventListPanel = new EventListPanel(logic.getFilteredEventList(), this, logic::getEventDisplaySetting); + eventListPanelPlaceholder.getChildren().add(eventListPanel.getRoot()); resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); @@ -157,14 +171,55 @@ void show() { @FXML private void handleExit() { GuiSettings guiSettings = new GuiSettings(primaryStage.getWidth(), primaryStage.getHeight(), - (int) primaryStage.getX(), (int) primaryStage.getY()); + (int) primaryStage.getX(), (int) primaryStage.getY()); logic.setGuiSettings(guiSettings); helpWindow.hide(); primaryStage.hide(); + if (calendarWindow != null) { + calendarWindow.close(); + } } - public PersonListPanel getPersonListPanel() { - return personListPanel; + /** + * Shows the calendar panel to the user. + */ + @FXML + private void handleCalendar() { + if (calendarWindow != null && calendarWindow.isShowing()) { + calendarWindow.close(); + } + calendarWindow = new CalendarWindow(logic.getAddressBook().getEventList()); + calendarWindow.show(); + } + + /** + * Display result when user clicks on certain fields + * + * @param message Message displayed to user + */ + @FXML + public void handleClick(String message) { + resultDisplay.setFeedbackToUser(message); + } + + /** Filters the list of contacts to show the linked contacts of the {@code event}. */ + public void showLinksOfEvent(Event event) { + this.logic.filterContactsWithLinksToEvent(event); + } + + /** Filters the list of events to show the linked events of the {@code contact}. */ + public void showLinksOfContact(Contact contact) { + this.logic.filterEventsWithLinkToContact(contact); + } + + /** Changes the filter of the events so that all events will be displayed. */ + public void showAllEvents() { + this.logic.resetFilterOfEvents(); + } + + /** Changes the filter of the contacts so that all contacts will be displayed. */ + public void showAllContacts() { + this.logic.resetFilterOfContacts(); } /** @@ -186,6 +241,13 @@ private CommandResult executeCommand(String commandText) throws CommandException handleExit(); } + if (commandResult.isShowCalendar()) { + handleCalendar(); + } + + if (calendarWindow != null) { + calendarWindow.updateCalendar(commandResult.getEventChangerList()); + } return commandResult; } catch (CommandException | ParseException e) { logger.info("Invalid command: " + commandText); 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/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java index 882027e4537..21b34343ca2 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/address/ui/UiManager.java @@ -85,5 +85,4 @@ private void showFatalErrorDialogAndShutdown(String title, Throwable e) { Platform.exit(); System.exit(1); } - } diff --git a/src/main/resources/images/address.png b/src/main/resources/images/address.png new file mode 100644 index 00000000000..42d57c4c552 Binary files /dev/null and b/src/main/resources/images/address.png differ diff --git a/src/main/resources/images/bookmark.png b/src/main/resources/images/bookmark.png new file mode 100644 index 00000000000..053abe280d6 Binary files /dev/null and b/src/main/resources/images/bookmark.png differ diff --git a/src/main/resources/images/datetime.png b/src/main/resources/images/datetime.png new file mode 100644 index 00000000000..6890364dded Binary files /dev/null and b/src/main/resources/images/datetime.png differ diff --git a/src/main/resources/images/description.png b/src/main/resources/images/description.png new file mode 100644 index 00000000000..4f657c1ef59 Binary files /dev/null and b/src/main/resources/images/description.png differ diff --git a/src/main/resources/images/email.png b/src/main/resources/images/email.png new file mode 100644 index 00000000000..cbbbc7f29a7 Binary files /dev/null and b/src/main/resources/images/email.png differ diff --git a/src/main/resources/images/end.png b/src/main/resources/images/end.png new file mode 100644 index 00000000000..111d69d0c46 Binary files /dev/null and b/src/main/resources/images/end.png differ diff --git a/src/main/resources/images/event.png b/src/main/resources/images/event.png new file mode 100644 index 00000000000..73c34f48ff3 Binary files /dev/null and b/src/main/resources/images/event.png differ diff --git a/src/main/resources/images/eventBig.png b/src/main/resources/images/eventBig.png new file mode 100644 index 00000000000..1d4699effd6 Binary files /dev/null and b/src/main/resources/images/eventBig.png differ diff --git a/src/main/resources/images/group.png b/src/main/resources/images/group.png new file mode 100644 index 00000000000..4ced69238bb Binary files /dev/null and b/src/main/resources/images/group.png differ diff --git a/src/main/resources/images/name.png b/src/main/resources/images/name.png new file mode 100644 index 00000000000..18b036a815a Binary files /dev/null and b/src/main/resources/images/name.png differ diff --git a/src/main/resources/images/phone.png b/src/main/resources/images/phone.png new file mode 100644 index 00000000000..213df17a0c0 Binary files /dev/null and b/src/main/resources/images/phone.png differ diff --git a/src/main/resources/images/start.png b/src/main/resources/images/start.png new file mode 100644 index 00000000000..b0270990ea8 Binary files /dev/null and b/src/main/resources/images/start.png differ diff --git a/src/main/resources/images/tag.png b/src/main/resources/images/tag.png new file mode 100644 index 00000000000..9975f456960 Binary files /dev/null and b/src/main/resources/images/tag.png differ diff --git a/src/main/resources/images/telegram.png b/src/main/resources/images/telegram.png new file mode 100644 index 00000000000..7ebdaee94b5 Binary files /dev/null and b/src/main/resources/images/telegram.png differ diff --git a/src/main/resources/images/zoom.png b/src/main/resources/images/zoom.png new file mode 100644 index 00000000000..5e44f9e7776 Binary files /dev/null and b/src/main/resources/images/zoom.png differ diff --git a/src/main/resources/view/CalendarWindow.fxml b/src/main/resources/view/CalendarWindow.fxml new file mode 100644 index 00000000000..022e1ccc266 --- /dev/null +++ b/src/main/resources/view/CalendarWindow.fxml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/Clickable.css b/src/main/resources/view/Clickable.css new file mode 100644 index 00000000000..8a57eb1b58e --- /dev/null +++ b/src/main/resources/view/Clickable.css @@ -0,0 +1,8 @@ +#zoom, #telegramHandle { + -fx-text-fill: #1fc2de; + -fx-underline: true; +} + +.clickable:hover { + -fx-cursor: hand; +} diff --git a/src/main/resources/view/ContactListCard.fxml b/src/main/resources/view/ContactListCard.fxml new file mode 100644 index 00000000000..b3c72f6b362 --- /dev/null +++ b/src/main/resources/view/ContactListCard.fxml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/ContactListPanel.fxml b/src/main/resources/view/ContactListPanel.fxml new file mode 100644 index 00000000000..1007c86e01d --- /dev/null +++ b/src/main/resources/view/ContactListPanel.fxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 9ce9bcfb569..faf0bdd53a1 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -328,7 +328,7 @@ -fx-text-fill: white; } -#filterField, #personListPanel, #personWebpage { +#filterField, #contactListPanel, #personWebpage { -fx-effect: innershadow(gaussian, black, 10, 0, 0, 0); } @@ -337,12 +337,12 @@ -fx-background-radius: 0; } -#tags { +#tags, #links { -fx-hgap: 7; -fx-vgap: 3; } -#tags .label { +#tags .label, #links .label { -fx-text-fill: white; -fx-background-color: #3e7b91; -fx-padding: 1 3 1 3; @@ -350,3 +350,8 @@ -fx-background-radius: 2; -fx-font-size: 11; } + +#links .label { + -fx-text-fill: black; + -fx-background-color: #FDFD96; +} diff --git a/src/main/resources/view/EventListCard.fxml b/src/main/resources/view/EventListCard.fxml new file mode 100644 index 00000000000..e7f4a498fa0 --- /dev/null +++ b/src/main/resources/view/EventListCard.fxml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/PersonListPanel.fxml b/src/main/resources/view/EventListPanel.fxml similarity index 77% rename from src/main/resources/view/PersonListPanel.fxml rename to src/main/resources/view/EventListPanel.fxml index 8836d323cc5..ef61c334e39 100644 --- a/src/main/resources/view/PersonListPanel.fxml +++ b/src/main/resources/view/EventListPanel.fxml @@ -4,5 +4,5 @@ - + diff --git a/src/main/resources/view/Extensions.css b/src/main/resources/view/Extensions.css index bfe82a85964..ad76ad3bdcc 100644 --- a/src/main/resources/view/Extensions.css +++ b/src/main/resources/view/Extensions.css @@ -18,3 +18,63 @@ .tooltip-text { -fx-text-fill: white; } + +#contact, #event { + -fx-text-fill: white; + -fx-padding: 10 5 10 5; + -fx-background-insets: 10; +} + +#contact{ + -fx-graphic:url(../images/name.png); +} + +#email{ + -fx-graphic:url(../images/email.png); +} + +#phone{ + -fx-graphic:url(../images/phone.png); +} + +#address{ + -fx-graphic:url(../images/address.png); +} +#telegramHandle{ + -fx-graphic:url(../images/telegram.png); +} +#zoom{ + -fx-graphic:url(../images/zoom.png); +} + +#linkToEvent{ + -fx-graphic:url(../images/event.png); +} + +#tagIcon{ + -fx-graphic:url(../images/tag.png); +} + +#event{ + -fx-graphic:url(../images/eventBig.png); +} + +#from{ + -fx-graphic:url(../images/start.png); +} + +#to{ + -fx-graphic:url(../images/end.png); +} + +#description{ + -fx-graphic:url(../images/description.png); +} + +#linkToContact{ + -fx-graphic:url(../images/group.png); +} + +#favourite{ + -fx-graphic:url(../images/bookmark.png); +} diff --git a/src/main/resources/view/HelpWindow.css b/src/main/resources/view/HelpWindow.css index 8a5951e6df7..fa3b0284e05 100644 --- a/src/main/resources/view/HelpWindow.css +++ b/src/main/resources/view/HelpWindow.css @@ -1,3 +1,21 @@ -#copyButton, #helpMessage { +#introduction { + -fx-font-family: "Open Sans Extra Bold"; + -fx-padding: 3 3 3 3; +} + +#contactTitle, +#eventTitle, +#generalTitle { + -fx-font-family: "Open Sans Extra Bold"; + -fx-font-size: 13pt; + -fx-padding: 5 3 1 3; + -fx-underline: "true"; +} + +#copyButton, +#helpMessage { -fx-font-family: "Open Sans"; + -fx-padding: 3 3 3 3; } + + diff --git a/src/main/resources/view/HelpWindow.fxml b/src/main/resources/view/HelpWindow.fxml index c9a38f2b105..78e164b602a 100644 --- a/src/main/resources/view/HelpWindow.fxml +++ b/src/main/resources/view/HelpWindow.fxml @@ -1,45 +1,61 @@ - - - - - - - - + + + + + + + - - - - - - - - - - + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 32bcf2c8e70..73ea9b35a0a 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -6,13 +6,14 @@ - - + + + @@ -28,6 +29,7 @@ + @@ -47,12 +49,27 @@ - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml deleted file mode 100644 index f08ea32ad55..00000000000 --- a/src/main/resources/view/PersonListCard.fxml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/test/data/JsonAddressBookStorageTest/invalidAndValidContactAndEventAddressBook.json b/src/test/data/JsonAddressBookStorageTest/invalidAndValidContactAndEventAddressBook.json new file mode 100644 index 00000000000..95dbfb23c11 --- /dev/null +++ b/src/test/data/JsonAddressBookStorageTest/invalidAndValidContactAndEventAddressBook.json @@ -0,0 +1,38 @@ +{ + "contacts": [ { + "name": "Valid Contact", + "phone": "9482424", + "email": "hans@example.com", + "address": "4th street" + }, { + "name": "Contact With Invalid Phone Field", + "phone": "948asdf2424", + "email": "hans@example.com", + "address": "4th street" + } ], + "events": [ { + "name": "Valid Event", + "start": "2021-03-27 15:30", + "end": "2021-03-27 15:30", + "description": "blah blah blah", + "address": "4th street", + "zoom": "a valid zoom link", + "tagged": [ ] + }, { + "name": "Event With Invalid startDateTime Field", + "start": "rsvrthbtrdbrbs", + "end": "2021-03-27 15:30", + "description": "2021-03-27 15:30", + "address": "4th street", + "zoom": "http://zoomlink.com", + "tagged": [ ] + }, { + "name": "Event With Invalid endDateTime Field", + "start": "2021-03-27 15:30", + "end": "what time is this", + "description": "", + "address": "4th street", + "zoom": "http://zoomlink.com", + "tagged": [ ] + }] +} diff --git a/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json b/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json deleted file mode 100644 index 6a4d2b7181c..00000000000 --- a/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "persons": [ { - "name": "Valid Person", - "phone": "9482424", - "email": "hans@example.com", - "address": "4th street" - }, { - "name": "Person With Invalid Phone Field", - "phone": "948asdf2424", - "email": "hans@example.com", - "address": "4th street" - } ] -} diff --git a/src/test/data/JsonAddressBookStorageTest/invalidContactAndEventAddressBook.json b/src/test/data/JsonAddressBookStorageTest/invalidContactAndEventAddressBook.json new file mode 100644 index 00000000000..5052b9f35d8 --- /dev/null +++ b/src/test/data/JsonAddressBookStorageTest/invalidContactAndEventAddressBook.json @@ -0,0 +1,17 @@ +{ + "contacts": [ { + "name": "Contact with invalid name field: Ha!ns Mu@ster", + "phone": "9482424", + "email": "hans@example.com", + "address": "4th street" + } ], + "events": [ { + "name": "Event with invalid name field: !gtr@@@&&&&t4jfer", + "start": "2021-03-27 15:30", + "end": "2021-03-27 15:30", + "description": "", + "address": "4th street", + "zoom": "http://zoomlink.com", + "tagged": [ ] + } ] +} diff --git a/src/test/data/JsonAddressBookStorageTest/invalidPersonAddressBook.json b/src/test/data/JsonAddressBookStorageTest/invalidPersonAddressBook.json deleted file mode 100644 index ccd21f7d1a9..00000000000 --- a/src/test/data/JsonAddressBookStorageTest/invalidPersonAddressBook.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "persons": [ { - "name": "Person with invalid name field: Ha!ns Mu@ster", - "phone": "9482424", - "email": "hans@example.com", - "address": "4th street" - } ] -} diff --git a/src/test/data/JsonSerializableAddressBookTest/duplicateContactAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/duplicateContactAddressBook.json new file mode 100644 index 00000000000..f9757d0ddc3 --- /dev/null +++ b/src/test/data/JsonSerializableAddressBookTest/duplicateContactAddressBook.json @@ -0,0 +1,26 @@ +{ + "contacts": [ { + "name": "Alice Pauline", + "phone": "94351253", + "email": "alice@example.com", + "address": "123, Jurong West Ave 6, #08-111", + "tagged": [ "friends" ], + "uuid": "123e4567-e89b-12d3-a456-556642440001" + }, { + "name": "Alice Pauline", + "phone": "94351253", + "email": "pauline@example.com", + "address": "4th street", + "uuid": "123e4567-e89b-12d3-a456-556642440001" + } ], + "events": [ { + "name": "CS2103T lecture", + "start": "2021-03-27 15:30", + "end": "2021-03-27 15:30", + "description": "", + "address": "123, Jurong West Ave 6, #08-111", + "zoom": "http://zoomlink.com", + "tagged": [ "its lecture time" ], + "uuid": "123e4567-e89b-12d3-a456-556642440001" + } ] +} diff --git a/src/test/data/JsonSerializableAddressBookTest/duplicateEventAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/duplicateEventAddressBook.json new file mode 100644 index 00000000000..34e7be8ffa8 --- /dev/null +++ b/src/test/data/JsonSerializableAddressBookTest/duplicateEventAddressBook.json @@ -0,0 +1,48 @@ +{ + "contacts" : [ { + "name" : "Alice Pauline", + "phone" : "94351253", + "email" : "alice@example.com", + "address" : "123, Jurong West Ave 6, #08-111", + "telegramHandle" : null, + "zoomLink" : null, + "tagged" : [ "friends" ], + "uuid" : "a9dba5c9-4bce-481d-b2f5-d4820ecdd0a3", + "linkedEvents" : ["123e4567-e89b-12d3-a456-556642440002"], + "isMarked" : true + } ], + "events" : [ { + "name" : "CS2103 Midterms", + "description" : "I'm very unprepared", + "address" : "Zoom", + "tagged" : [ "exams" ], + "uuid" : "17e996a5-1f13-45fe-b9d2-f9517dff067f", + "linkedContacts" : [ ], + "isMarked" : true, + "startDateTime" : "20-10-2021 09:00", + "endDateTime" : "20-10-2021 11:00", + "zoomLink" : "nus-sg.edu/123%a" + }, { + "name" : "CS2103 Midterms", + "description" : "I'm very unprepared", + "address" : "Zoom", + "tagged" : [ "exams" ], + "uuid" : "17e996a5-1f13-45fe-b9d2-f9517dff067f", + "linkedContacts" : [ ], + "isMarked" : true, + "startDateTime" : "20-10-2021 09:00", + "endDateTime" : "20-10-2021 11:00", + "zoomLink" : "nus-sg.edu/123%a" + }, { + "name" : "CS2100 Consultation", + "description" : "Topics: Assembly Language", + "address" : "02-11 COM2", + "tagged" : [ "School" ], + "uuid" : "123e4567-e89b-12d3-a456-556642440002", + "linkedContacts" : [ "a9dba5c9-4bce-481d-b2f5-d4820ecdd0a3" ], + "isMarked" : false, + "startDateTime" : "17-10-2021 15:00", + "endDateTime" : "17-10-2021 16:00", + "zoomLink" : "https://nus-sg.zoom.us/j/0123456789?pwd=ABCDEFG" + } ] +} diff --git a/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json deleted file mode 100644 index 48831cc7674..00000000000 --- a/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "persons": [ { - "name": "Alice Pauline", - "phone": "94351253", - "email": "alice@example.com", - "address": "123, Jurong West Ave 6, #08-111", - "tagged": [ "friends" ] - }, { - "name": "Alice Pauline", - "phone": "94351253", - "email": "pauline@example.com", - "address": "4th street" - } ] -} diff --git a/src/test/data/JsonSerializableAddressBookTest/invalidContactAndEventAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/invalidContactAndEventAddressBook.json new file mode 100644 index 00000000000..9b698903e07 --- /dev/null +++ b/src/test/data/JsonSerializableAddressBookTest/invalidContactAndEventAddressBook.json @@ -0,0 +1,17 @@ +{ + "contacts": [ { + "name": "Hans Muster", + "phone": "9482424", + "email": "invalid@email!3e", + "address": "4th street" + } ], + "events": [ { + "name": "Summer vacation", + "start": "2021-03-27 15:30", + "end": "invalid end", + "description": "", + "address": "4th street", + "zoom": "http://zoomlink.com", + "tagged": [ ] + } ] +} diff --git a/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json deleted file mode 100644 index ad3f135ae42..00000000000 --- a/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "persons": [ { - "name": "Hans Muster", - "phone": "9482424", - "email": "invalid@email!3e", - "address": "4th street" - } ] -} diff --git a/src/test/data/JsonSerializableAddressBookTest/typicalContactsAndEventsAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/typicalContactsAndEventsAddressBook.json new file mode 100644 index 00000000000..ea68112ab43 --- /dev/null +++ b/src/test/data/JsonSerializableAddressBookTest/typicalContactsAndEventsAddressBook.json @@ -0,0 +1,148 @@ +{ + "_comment": "AddressBook save file which contains the same Contact values as in TypicalContacts#getTypicalAddressBook() and the same Event Values as in TypicalEvents#getTypicalAddressBook()", + "contacts" : [ { + "name" : "Alice Pauline", + "phone" : "94351253", + "email" : "alice@example.com", + "address" : "123, Jurong West Ave 6, #08-111", + "telegramHandle" : null, + "zoomLink" : null, + "tagged" : [ "friends" ], + "uuid" : "a9dba5c9-4bce-481d-b2f5-d4820ecdd0a3", + "linkedEvents" : ["123e4567-e89b-12d3-a456-556642440002"], + "isMarked" : true + }, { + "name" : "Benson Meier", + "phone" : "98765432", + "email" : "johnd@example.com", + "address" : "311, Clementi Ave 2, #02-25", + "telegramHandle" : "benson67", + "zoomLink" : null, + "tagged" : [ "owesMoney", "friends" ], + "uuid" : "bd187ae2-5818-471e-a44f-96b52cb896de", + "linkedEvents" : [ ], + "isMarked" : false + }, { + "name" : "Carl Kurz", + "phone" : "95352563", + "email" : "heinz@example.com", + "address" : "wall street", + "telegramHandle" : "carlcarl7", + "zoomLink" : "https://nus-sg.zoom.us/j/7453256545252", + "tagged" : ["friends"], + "uuid" : "f37d9480-6add-406c-a7e7-bbfc682c1860", + "linkedEvents" : [ ], + "isMarked" : false + }, { + "name" : "Daniel Meier", + "phone" : "87652533", + "email" : "cornelia@example.com", + "address" : "10th street", + "telegramHandle" : null, + "zoomLink" : "nus-sg.zoom.us/j/783624625626", + "tagged" : [ "friends" ], + "uuid" : "88d18865-efbe-4a2d-b6a6-b7f62b5c36e9", + "linkedEvents" : [ ], + "isMarked" : false + }, { + "name" : "Elle Meyer", + "phone" : "9482224", + "email" : "werner@example.com", + "address" : null, + "telegramHandle" : "ellelele", + "zoomLink" : null, + "tagged" : [ ], + "uuid" : "08289a5f-0254-4c2d-9dd6-241b865166b2", + "linkedEvents" : [ ], + "isMarked" : false + }, { + "name" : "Fiona Kunz", + "phone" : "93106433", + "email" : "lydia@example.com", + "address" : "little tokyo", + "telegramHandle" : null, + "zoomLink" : null, + "tagged" : [ ], + "uuid" : "3de33e74-c2d3-4701-8e54-d851c929f006", + "linkedEvents" : [ ], + "isMarked" : false + }, { + "name" : "George Best", + "phone" : null, + "email" : "george@example.com", + "address" : null, + "telegramHandle" : null, + "zoomLink" : null, + "tagged" : [ ], + "uuid" : "eb1438dc-f19d-4ab8-bde4-fd749ffe2575", + "linkedEvents" : [ ], + "isMarked" : false + } ], + "events" : [ { + "name" : "CS2103 Midterms", + "description" : "I'm very unprepared", + "address" : "Zoom", + "tagged" : [ "exams" ], + "uuid" : "17e996a5-1f13-45fe-b9d2-f9517dff067f", + "linkedContacts" : [ ], + "isMarked" : true, + "startDateTime" : "20-10-2021 09:00", + "endDateTime" : "20-10-2021 11:00", + "zoomLink" : "nus-sg.edu/123%a" + }, { + "name" : "CS2100 Consultation", + "description" : "Topics: Assembly Language", + "address" : "02-11 COM2", + "tagged" : [ "School" ], + "uuid" : "123e4567-e89b-12d3-a456-556642440002", + "linkedContacts" : [ "a9dba5c9-4bce-481d-b2f5-d4820ecdd0a3" ], + "isMarked" : false, + "startDateTime" : "17-10-2021 15:00", + "endDateTime" : "17-10-2021 16:00", + "zoomLink" : "https://nus-sg.zoom.us/j/0123456789?pwd=ABCDEFG" + }, { + "name" : "CS2101 Meeting", + "description" : "Need to prepare for OP2", + "address" : "Zoom", + "tagged" : [ "meeting" ], + "uuid" : "f784de18-adf9-4d0d-8657-8d77d7be9bd7", + "linkedContacts" : [ ], + "isMarked" : false, + "startDateTime" : "15-10-2021 21:00", + "endDateTime" : "15-10-2021 22:00", + "zoomLink" : "nus-sg.edu/pwdffha%a" + }, { + "name" : "Football Practice", + "description" : "Important", + "address" : "USC", + "tagged" : [ ], + "uuid" : "ff189b43-527f-4afc-905c-aad42e2c68ac", + "linkedContacts" : [ ], + "isMarked" : false, + "startDateTime" : "30-10-2021 09:00", + "endDateTime" : "30-10-2021 11:00", + "zoomLink" : "https://nus-sg.zoom.us/j/0123456789?pwd=ABCDEFG" + }, { + "name" : "Team Meeting", + "description" : "Important", + "address" : "Zoom", + "tagged" : [ "meeting" ], + "uuid" : "87ac6178-54e7-483a-ac9f-f598b6a27955", + "linkedContacts" : [ ], + "isMarked" : false, + "startDateTime" : "20-10-2021 19:00", + "endDateTime" : "20-10-2021 20:00", + "zoomLink" : "nus-edu.sg/123link" + }, { + "name" : "Birthday Party", + "description" : "Dress code: Pink!", + "address" : "#10-07 Baker Street", + "tagged" : [ "friends" ], + "uuid" : "1a245538-3cb4-4c9a-8d09-83188e6e4e0e", + "linkedContacts" : [ ], + "isMarked" : false, + "startDateTime" : "23-10-2021 20:00", + "endDateTime" : "24-10-2021 01:00", + "zoomLink" : "https://nus-sg.zoom.us/j/0123456789?pwd=ABCDEFG" + } ] +} diff --git a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json deleted file mode 100644 index f10eddee12e..00000000000 --- a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "_comment": "AddressBook save file which contains the same Person values as in TypicalPersons#getTypicalAddressBook()", - "persons" : [ { - "name" : "Alice Pauline", - "phone" : "94351253", - "email" : "alice@example.com", - "address" : "123, Jurong West Ave 6, #08-111", - "tagged" : [ "friends" ] - }, { - "name" : "Benson Meier", - "phone" : "98765432", - "email" : "johnd@example.com", - "address" : "311, Clementi Ave 2, #02-25", - "tagged" : [ "owesMoney", "friends" ] - }, { - "name" : "Carl Kurz", - "phone" : "95352563", - "email" : "heinz@example.com", - "address" : "wall street", - "tagged" : [ ] - }, { - "name" : "Daniel Meier", - "phone" : "87652533", - "email" : "cornelia@example.com", - "address" : "10th street", - "tagged" : [ "friends" ] - }, { - "name" : "Elle Meyer", - "phone" : "9482224", - "email" : "werner@example.com", - "address" : "michegan ave", - "tagged" : [ ] - }, { - "name" : "Fiona Kunz", - "phone" : "9482427", - "email" : "lydia@example.com", - "address" : "little tokyo", - "tagged" : [ ] - }, { - "name" : "George Best", - "phone" : "9482442", - "email" : "anna@example.com", - "address" : "4th street", - "tagged" : [ ] - } ] -} diff --git a/src/test/java/seedu/address/commons/core/index/IndexTest.java b/src/test/java/seedu/address/commons/core/index/IndexTest.java index a3ec6f8e747..c88f5f1c12c 100644 --- a/src/test/java/seedu/address/commons/core/index/IndexTest.java +++ b/src/test/java/seedu/address/commons/core/index/IndexTest.java @@ -39,22 +39,31 @@ public void createZeroBasedIndex() { @Test public void equals() { - final Index fifthPersonIndex = Index.fromOneBased(5); + final Index fifthIndex = Index.fromOneBased(5); // same values -> returns true - assertTrue(fifthPersonIndex.equals(Index.fromOneBased(5))); - assertTrue(fifthPersonIndex.equals(Index.fromZeroBased(4))); + assertTrue(fifthIndex.equals(Index.fromOneBased(5))); + assertTrue(fifthIndex.equals(Index.fromZeroBased(4))); // same object -> returns true - assertTrue(fifthPersonIndex.equals(fifthPersonIndex)); + assertTrue(fifthIndex.equals(fifthIndex)); // null -> returns false - assertFalse(fifthPersonIndex.equals(null)); + assertFalse(fifthIndex.equals(null)); // different types -> returns false - assertFalse(fifthPersonIndex.equals(5.0f)); + assertFalse(fifthIndex.equals(5.0f)); // different index -> returns false - assertFalse(fifthPersonIndex.equals(Index.fromOneBased(1))); + assertFalse(fifthIndex.equals(Index.fromOneBased(1))); + } + + @Test + public void isMoreThan() { + Index indexWithValueOne = Index.fromOneBased(1); + Index indexWithValueMaxInteger = Index.fromOneBased(Integer.MAX_VALUE); + assertTrue(indexWithValueMaxInteger.isMoreThan(indexWithValueOne)); + assertFalse(indexWithValueOne.isMoreThan(indexWithValueMaxInteger)); + assertFalse(indexWithValueOne.isMoreThan(indexWithValueOne)); } } diff --git a/src/test/java/seedu/address/commons/core/range/RangeTest.java b/src/test/java/seedu/address/commons/core/range/RangeTest.java new file mode 100644 index 00000000000..d934ad3f9a0 --- /dev/null +++ b/src/test/java/seedu/address/commons/core/range/RangeTest.java @@ -0,0 +1,73 @@ +package seedu.address.commons.core.range; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND; +import static seedu.address.testutil.TypicalIndexes.INDEX_THIRD; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; + +public class RangeTest { + + @Test + public void createRange() { + // valid range + final Range rangeOne = new Range(INDEX_FIRST, INDEX_SECOND); + assertTrue(rangeOne.getStart().equals(INDEX_FIRST)); + assertTrue(rangeOne.getEnd().equals(INDEX_SECOND)); + + // start and end index same + final Range rangeTwo = Range.convertFromIndex(INDEX_THIRD); + assertTrue(rangeTwo.getStart().equals(INDEX_THIRD)); + assertTrue(rangeTwo.getEnd().equals(INDEX_THIRD)); + + // start index more than end index + assertThrows(IndexOutOfBoundsException.class, () -> new Range(INDEX_THIRD, INDEX_FIRST)); + } + + @Test + public void testEquals() { + final Range range = new Range(INDEX_FIRST, INDEX_THIRD); + + // same values -> returns true + final Range rangeFromConstructor = new Range(INDEX_THIRD, INDEX_THIRD); + final Range rangeFromIndex = Range.convertFromIndex(INDEX_THIRD); + assertTrue(rangeFromConstructor.equals(rangeFromIndex)); + + // same object -> returns true + assertTrue(range.equals(range)); + + // null -> returns false + assertFalse(range.equals(null)); + + // different types -> returns false + assertFalse(range.equals(5.0f)); + + // different start index but same end index -> returns false + final Range rangeStartIndexDifferent = new Range(INDEX_SECOND, INDEX_THIRD); + assertFalse(range.equals(rangeStartIndexDifferent)); + + // same start index but different end index -> returns false + final Range rangeEndIndexDifferent = new Range(INDEX_FIRST, INDEX_SECOND); + assertFalse(range.equals(rangeStartIndexDifferent)); + } + + @Test + void getStart() { + final Range range = new Range(INDEX_FIRST, INDEX_SECOND); + Index start = range.getStart(); + assertEquals(start, INDEX_FIRST); + } + + @Test + void getEnd() { + final Range range = new Range(INDEX_FIRST, INDEX_SECOND); + Index end = range.getEnd(); + assertEquals(end, INDEX_SECOND); + } +} diff --git a/src/test/java/seedu/address/commons/util/JsonUtilTest.java b/src/test/java/seedu/address/commons/util/JsonUtilTest.java index d4907539dee..8d6aac1521b 100644 --- a/src/test/java/seedu/address/commons/util/JsonUtilTest.java +++ b/src/test/java/seedu/address/commons/util/JsonUtilTest.java @@ -38,8 +38,4 @@ public void deserializeObjectFromJsonFile_noExceptionThrown() throws IOException assertEquals(serializableTestClass.getListOfLocalDateTimes(), SerializableTestClass.getListTestValues()); assertEquals(serializableTestClass.getMapOfIntegerToString(), SerializableTestClass.getHashMapTestValues()); } - - //TODO: @Test jsonUtil_readJsonStringToObjectInstance_correctObject() - - //TODO: @Test jsonUtil_writeThenReadObjectToJson_correctObject() } diff --git a/src/test/java/seedu/address/commons/util/StringUtilTest.java b/src/test/java/seedu/address/commons/util/StringUtilTest.java index c56d407bf3f..ced067161fe 100644 --- a/src/test/java/seedu/address/commons/util/StringUtilTest.java +++ b/src/test/java/seedu/address/commons/util/StringUtilTest.java @@ -109,7 +109,7 @@ public void containsWordIgnoreCase_validInputs_correctResult() { assertFalse(StringUtil.containsWordIgnoreCase(" ", "123")); // Matches a partial word only - assertFalse(StringUtil.containsWordIgnoreCase("aaa bbb ccc", "bb")); // Sentence word bigger than query word + assertTrue(StringUtil.containsWordIgnoreCase("aaa bbb ccc", "bb")); // Partial match to second word assertFalse(StringUtil.containsWordIgnoreCase("aaa bbb ccc", "bbbb")); // Query word bigger than sentence word // Matches word in the sentence, different upper/lower case letters @@ -140,4 +140,34 @@ public void getDetails_nullGiven_throwsNullPointerException() { assertThrows(NullPointerException.class, () -> StringUtil.getDetails(null)); } + @Test + public void isValidRange() { + // EP: empty strings + assertFalse(StringUtil.isValidRange("")); // Boundary value + assertFalse(StringUtil.isValidRange(" ")); + + // EP: not a range + assertFalse(StringUtil.isValidRange("a")); + assertFalse(StringUtil.isValidRange("aaa")); + + // EP: start with zero + assertFalse(StringUtil.isValidRange("0-10")); + + // EP: signed ranges + assertFalse(StringUtil.isValidRange("-2--10")); + assertFalse(StringUtil.isValidRange("-2-10")); + + // EP: ranges with white space + assertFalse(StringUtil.isValidRange(" 2-5 ")); // Leading/trailing spaces + assertFalse(StringUtil.isValidRange("0 - 10")); // Spaces in the middle + + // EP: first number more than second number + assertFalse(StringUtil.isValidRange("10-5")); + assertFalse(StringUtil.isValidRange("2-1")); + + // EP: valid ranges, should return true + assertTrue(StringUtil.isValidRange("1-5")); // Boundary value + assertTrue(StringUtil.isValidRange("10-100")); + + } } diff --git a/src/test/java/seedu/address/logic/LogicManagerTest.java b/src/test/java/seedu/address/logic/LogicManagerTest.java index ad923ac249a..155843042dd 100644 --- a/src/test/java/seedu/address/logic/LogicManagerTest.java +++ b/src/test/java/seedu/address/logic/LogicManagerTest.java @@ -1,36 +1,53 @@ package seedu.address.logic; +import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX; import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; -import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.ADDRESS_DESC_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.EMAIL_DESC_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.NAME_DESC_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.PHONE_DESC_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.TELEGRAM_DESC_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.ZOOM_DESC_AMY; import static seedu.address.testutil.Assert.assertThrows; -import static seedu.address.testutil.TypicalPersons.AMY; +import static seedu.address.testutil.TypicalContacts.AMY; +import static seedu.address.testutil.TypicalContacts.BOB; +import static seedu.address.testutil.TypicalEvents.INTERVIEW; +import static seedu.address.testutil.TypicalEvents.TEAM_MEETING; import java.io.IOException; import java.nio.file.Path; +import java.util.function.Predicate; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import seedu.address.logic.commands.AddCommand; +import seedu.address.commons.core.GuiSettings; import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.ModelStub; +import seedu.address.logic.commands.contact.CAddCommand; +import seedu.address.logic.commands.contact.CListCommand; import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.commands.general.CalendarCommand; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.AddressBook; import seedu.address.model.Model; import seedu.address.model.ModelManager; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.UserPrefs; -import seedu.address.model.person.Person; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.ContactDisplaySetting; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventDisplaySetting; import seedu.address.storage.JsonAddressBookStorage; import seedu.address.storage.JsonUserPrefsStorage; import seedu.address.storage.StorageManager; -import seedu.address.testutil.PersonBuilder; +import seedu.address.testutil.ContactBuilder; public class LogicManagerTest { private static final IOException DUMMY_IO_EXCEPTION = new IOException("dummy exception"); @@ -40,13 +57,14 @@ public class LogicManagerTest { private Model model = new ModelManager(); private Logic logic; + private StorageManager storage; @BeforeEach public void setUp() { JsonAddressBookStorage addressBookStorage = new JsonAddressBookStorage(temporaryFolder.resolve("addressBook.json")); JsonUserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(temporaryFolder.resolve("userPrefs.json")); - StorageManager storage = new StorageManager(addressBookStorage, userPrefsStorage); + storage = new StorageManager(addressBookStorage, userPrefsStorage); logic = new LogicManager(model, storage); } @@ -58,14 +76,20 @@ public void execute_invalidCommandFormat_throwsParseException() { @Test public void execute_commandExecutionError_throwsCommandException() { - String deleteCommand = "delete 9"; - assertCommandException(deleteCommand, MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + String deleteCommand = "cdelete 9"; + assertCommandException(deleteCommand, MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX); + } + + @Test + public void execute_validUndoableCommand_success() throws Exception { + String cListCommand = CListCommand.COMMAND_WORD; + assertCommandSuccess(cListCommand, CListCommand.MESSAGE_SUCCESS, model); } @Test - public void execute_validCommand_success() throws Exception { - String listCommand = ListCommand.COMMAND_WORD; - assertCommandSuccess(listCommand, ListCommand.MESSAGE_SUCCESS, model); + public void execute_validNotUndoableCommand_success() throws Exception { + String calendarCommand = CalendarCommand.COMMAND_WORD; + assertCommandSuccess(calendarCommand, CalendarCommand.MESSAGE_SUCCESS, model); } @Test @@ -79,18 +103,117 @@ public void execute_storageThrowsIoException_throwsCommandException() { logic = new LogicManager(model, storage); // Execute add command - String addCommand = AddCommand.COMMAND_WORD + NAME_DESC_AMY + PHONE_DESC_AMY + EMAIL_DESC_AMY - + ADDRESS_DESC_AMY; - Person expectedPerson = new PersonBuilder(AMY).withTags().build(); + String addCommand = CAddCommand.COMMAND_WORD + NAME_DESC_AMY + PHONE_DESC_AMY + EMAIL_DESC_AMY + + ADDRESS_DESC_AMY + TELEGRAM_DESC_AMY + ZOOM_DESC_AMY; + Contact expectedContact = new ContactBuilder(AMY).withTags().build(); ModelManager expectedModel = new ModelManager(); - expectedModel.addPerson(expectedPerson); + expectedModel.addContact(expectedContact); String expectedMessage = LogicManager.FILE_OPS_ERROR_MESSAGE + DUMMY_IO_EXCEPTION; assertCommandFailure(addCommand, CommandException.class, expectedMessage, expectedModel); } @Test - public void getFilteredPersonList_modifyList_throwsUnsupportedOperationException() { - assertThrows(UnsupportedOperationException.class, () -> logic.getFilteredPersonList().remove(0)); + public void getFilteredContactList_modifyList_throwsUnsupportedOperationException() { + assertThrows(UnsupportedOperationException.class, () -> logic.getFilteredContactList().remove(0)); + } + + @Test + public void getFilteredEventList_modifyList_throwsUnsupportedOperationException() { + assertThrows(UnsupportedOperationException.class, () -> logic.getFilteredEventList().remove(0)); + } + + @Test + public void test_getAddressBook() { + Model newModel = new ModelStubWithAddressBook(new AddressBook()); + Logic newLogic = new LogicManager(newModel, storage); + assertEquals(new AddressBook(), newLogic.getAddressBook()); + AddressBook updatedAddressBook = new AddressBook(); + updatedAddressBook.addContact(AMY); + newModel.setAddressBook(updatedAddressBook); + assertEquals(newModel.getAddressBook(), newLogic.getAddressBook()); + } + + @Test + public void test_getAddressBookFilePath() { + assertEquals(new UserPrefs().getAddressBookFilePath(), logic.getAddressBookFilePath()); + } + + @Test + public void test_getGuiSettings() { + Model newModel = new ModelStubWithGuiSettings(new GuiSettings()); + Logic newLogic = new LogicManager(newModel, storage); + assertEquals(new GuiSettings(), newLogic.getGuiSettings()); + GuiSettings settings = new GuiSettings(1, 1, 1, 1); + newModel.setGuiSettings(settings); + assertEquals(newModel.getGuiSettings(), newLogic.getGuiSettings()); + } + + @Test + public void test_getContactDisplaySetting() { + assertEquals(ContactDisplaySetting.DEFAULT_SETTING, logic.getContactDisplaySetting()); + ContactDisplaySetting setting = new ContactDisplaySetting(true, true, true, false, false, false); + model.setContactDisplaySetting(setting); + assertEquals(setting, logic.getContactDisplaySetting()); + } + + @Test + public void test_getEventDisplaySetting() { + assertEquals(EventDisplaySetting.DEFAULT_SETTING, logic.getEventDisplaySetting()); + EventDisplaySetting setting = new EventDisplaySetting(true, true, true, false, false, false); + model.setEventDisplaySetting(setting); + assertEquals(setting, logic.getEventDisplaySetting()); + } + + @Test + public void test_setGuiSettings() { + GuiSettings guiSettings = new GuiSettings(1, 1, 1, 1); + assertNotEquals(guiSettings, logic.getGuiSettings()); + logic.setGuiSettings(guiSettings); + assertEquals(guiSettings, logic.getGuiSettings()); + } + + @Test + public void test_filterContactsWithLinksToEvent() { + ModelStubWithPredicate newModel = new ModelStubWithPredicate(); + Logic newLogic = new LogicManager(newModel, storage); + Event event = INTERVIEW.linkTo(AMY); + Contact c = AMY.linkTo(INTERVIEW); + Predicate predicate = contact -> contact.getLinkedEvents().contains(event.getUuid()); + newLogic.filterContactsWithLinksToEvent(event); + + assertNotEquals(predicate.test(c), newModel.contactPredicate.test(BOB)); + assertEquals(predicate.test(c), newModel.contactPredicate.test(c)); + } + + @Test + public void test_filterEventsWithLinkToContact() { + ModelStubWithPredicate newModel = new ModelStubWithPredicate(); + Logic newLogic = new LogicManager(newModel, storage); + Event e = INTERVIEW.linkTo(AMY); + Contact contact = AMY.linkTo(INTERVIEW); + Predicate predicate = event -> event.getLinkedContacts().contains(contact.getUuid()); + newLogic.filterEventsWithLinkToContact(contact); + + assertNotEquals(predicate.test(e), newModel.eventPredicate.test(TEAM_MEETING)); + assertEquals(predicate.test(e), newModel.eventPredicate.test(e)); + } + + @Test + public void test_resetFilterOfContacts() { + ModelStubWithBoolean newModel = new ModelStubWithBoolean(); + Logic newLogic = new LogicManager(newModel, storage); + assertFalse(newModel.contactBoolean); + newLogic.resetFilterOfContacts(); + assertTrue(newModel.contactBoolean); + } + + @Test + public void test_resetFilterOfEvents() { + ModelStubWithBoolean newModel = new ModelStubWithBoolean(); + Logic newLogic = new LogicManager(newModel, storage); + assertFalse(newModel.eventBoolean); + newLogic.resetFilterOfEvents(); + assertTrue(newModel.eventBoolean); } /** @@ -155,8 +278,96 @@ private JsonAddressBookIoExceptionThrowingStub(Path filePath) { } @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException { + public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) + throws IOException { throw DUMMY_IO_EXCEPTION; } } + + /** + * A Model stub that contains an address book. + */ + private class ModelStubWithAddressBook extends ModelStub { + private ReadOnlyAddressBook addressBook; + + ModelStubWithAddressBook(AddressBook addressBook) { + requireNonNull(addressBook); + this.addressBook = addressBook; + } + + @Override + public ReadOnlyAddressBook getAddressBook() { + return addressBook; + } + + @Override + public void setAddressBook(ReadOnlyAddressBook addressBook) { + requireNonNull(addressBook); + this.addressBook = addressBook; + } + } + + /** + * A Model stub that contains a GUI settings. + */ + private class ModelStubWithGuiSettings extends ModelStub { + private GuiSettings guiSettings; + + ModelStubWithGuiSettings(GuiSettings guiSettings) { + requireNonNull(guiSettings); + this.guiSettings = guiSettings; + } + + @Override + public GuiSettings getGuiSettings() { + return guiSettings; + } + + @Override + public void setGuiSettings(GuiSettings guiSettings) { + requireNonNull(guiSettings); + this.guiSettings = guiSettings; + } + } + + /** + * A Model stub that contains contact and event predicates. + */ + private class ModelStubWithPredicate extends ModelStub { + private Predicate contactPredicate; + private Predicate eventPredicate; + + @Override + public void updateFilteredContactList(Predicate predicate) { + contactPredicate = predicate; + } + + @Override + public void updateFilteredEventList(Predicate predicate) { + eventPredicate = predicate; + } + } + + /** + * A Model stub that contains two booleans. + */ + private class ModelStubWithBoolean extends ModelStub { + private boolean contactBoolean; + private boolean eventBoolean; + + public ModelStubWithBoolean() { + contactBoolean = false; + eventBoolean = false; + } + + @Override + public void rerenderContactCards(boolean useBackSamePredicate) { + contactBoolean = useBackSamePredicate; + } + + @Override + public void rerenderEventCards(boolean useBackSamePredicate) { + eventBoolean = useBackSamePredicate; + } + } } diff --git a/src/test/java/seedu/address/logic/LogicStub.java b/src/test/java/seedu/address/logic/LogicStub.java new file mode 100644 index 00000000000..568f6a58ba0 --- /dev/null +++ b/src/test/java/seedu/address/logic/LogicStub.java @@ -0,0 +1,35 @@ +package seedu.address.logic; + +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.Undoable; +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; + +/** + * Stub for LogicManager, without the storage capability. + */ +public class LogicStub extends LogicManager { + private final Model model; + private final AddressBookParser addressBookParser; + + /** Creates a simple stub for a logic manager without storage. */ + public LogicStub(Model model) { + super(model, null); + addressBookParser = new AddressBookParser(); + this.model = model; + } + + @Override + public CommandResult execute(String commandText) throws CommandException, ParseException { + CommandResult commandResult; + Command command = addressBookParser.parseCommand(commandText); + commandResult = command.execute(model); + if (command instanceof Undoable) { + model.commitHistory(); + } + return commandResult; + } +} diff --git a/src/test/java/seedu/address/logic/commands/AddCommandIntegrationTest.java b/src/test/java/seedu/address/logic/commands/AddCommandIntegrationTest.java deleted file mode 100644 index cb8714bb055..00000000000 --- a/src/test/java/seedu/address/logic/commands/AddCommandIntegrationTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package seedu.address.logic.commands; - -import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; -import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; -import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import seedu.address.model.Model; -import seedu.address.model.ModelManager; -import seedu.address.model.UserPrefs; -import seedu.address.model.person.Person; -import seedu.address.testutil.PersonBuilder; - -/** - * Contains integration tests (interaction with the Model) for {@code AddCommand}. - */ -public class AddCommandIntegrationTest { - - private Model model; - - @BeforeEach - public void setUp() { - model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); - } - - @Test - public void execute_newPerson_success() { - Person validPerson = new PersonBuilder().build(); - - Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); - expectedModel.addPerson(validPerson); - - assertCommandSuccess(new AddCommand(validPerson), model, - String.format(AddCommand.MESSAGE_SUCCESS, validPerson), expectedModel); - } - - @Test - public void execute_duplicatePerson_throwsCommandException() { - Person personInList = model.getAddressBook().getPersonList().get(0); - assertCommandFailure(new AddCommand(personInList), model, AddCommand.MESSAGE_DUPLICATE_PERSON); - } - -} diff --git a/src/test/java/seedu/address/logic/commands/AddCommandTest.java b/src/test/java/seedu/address/logic/commands/AddCommandTest.java deleted file mode 100644 index 5865713d5dd..00000000000 --- a/src/test/java/seedu/address/logic/commands/AddCommandTest.java +++ /dev/null @@ -1,194 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.testutil.Assert.assertThrows; - -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.function.Predicate; - -import org.junit.jupiter.api.Test; - -import javafx.collections.ObservableList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.AddressBook; -import seedu.address.model.Model; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.person.Person; -import seedu.address.testutil.PersonBuilder; - -public class AddCommandTest { - - @Test - public void constructor_nullPerson_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> new AddCommand(null)); - } - - @Test - public void execute_personAcceptedByModel_addSuccessful() throws Exception { - ModelStubAcceptingPersonAdded modelStub = new ModelStubAcceptingPersonAdded(); - Person validPerson = new PersonBuilder().build(); - - CommandResult commandResult = new AddCommand(validPerson).execute(modelStub); - - assertEquals(String.format(AddCommand.MESSAGE_SUCCESS, validPerson), commandResult.getFeedbackToUser()); - assertEquals(Arrays.asList(validPerson), modelStub.personsAdded); - } - - @Test - public void execute_duplicatePerson_throwsCommandException() { - Person validPerson = new PersonBuilder().build(); - AddCommand addCommand = new AddCommand(validPerson); - ModelStub modelStub = new ModelStubWithPerson(validPerson); - - assertThrows(CommandException.class, AddCommand.MESSAGE_DUPLICATE_PERSON, () -> addCommand.execute(modelStub)); - } - - @Test - public void equals() { - Person alice = new PersonBuilder().withName("Alice").build(); - Person bob = new PersonBuilder().withName("Bob").build(); - AddCommand addAliceCommand = new AddCommand(alice); - AddCommand addBobCommand = new AddCommand(bob); - - // same object -> returns true - assertTrue(addAliceCommand.equals(addAliceCommand)); - - // same values -> returns true - AddCommand addAliceCommandCopy = new AddCommand(alice); - assertTrue(addAliceCommand.equals(addAliceCommandCopy)); - - // different types -> returns false - assertFalse(addAliceCommand.equals(1)); - - // null -> returns false - assertFalse(addAliceCommand.equals(null)); - - // different person -> returns false - assertFalse(addAliceCommand.equals(addBobCommand)); - } - - /** - * A default model stub that have all of the methods failing. - */ - private class ModelStub implements Model { - @Override - public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { - throw new AssertionError("This method should not be called."); - } - - @Override - public ReadOnlyUserPrefs getUserPrefs() { - throw new AssertionError("This method should not be called."); - } - - @Override - public GuiSettings getGuiSettings() { - throw new AssertionError("This method should not be called."); - } - - @Override - public void setGuiSettings(GuiSettings guiSettings) { - throw new AssertionError("This method should not be called."); - } - - @Override - public Path getAddressBookFilePath() { - throw new AssertionError("This method should not be called."); - } - - @Override - public void setAddressBookFilePath(Path addressBookFilePath) { - throw new AssertionError("This method should not be called."); - } - - @Override - public void addPerson(Person person) { - throw new AssertionError("This method should not be called."); - } - - @Override - public void setAddressBook(ReadOnlyAddressBook newData) { - throw new AssertionError("This method should not be called."); - } - - @Override - public ReadOnlyAddressBook getAddressBook() { - throw new AssertionError("This method should not be called."); - } - - @Override - public boolean hasPerson(Person person) { - throw new AssertionError("This method should not be called."); - } - - @Override - public void deletePerson(Person target) { - throw new AssertionError("This method should not be called."); - } - - @Override - public void setPerson(Person target, Person editedPerson) { - throw new AssertionError("This method should not be called."); - } - - @Override - public ObservableList getFilteredPersonList() { - throw new AssertionError("This method should not be called."); - } - - @Override - public void updateFilteredPersonList(Predicate predicate) { - throw new AssertionError("This method should not be called."); - } - } - - /** - * A Model stub that contains a single person. - */ - private class ModelStubWithPerson extends ModelStub { - private final Person person; - - ModelStubWithPerson(Person person) { - requireNonNull(person); - this.person = person; - } - - @Override - public boolean hasPerson(Person person) { - requireNonNull(person); - return this.person.isSamePerson(person); - } - } - - /** - * A Model stub that always accept the person being added. - */ - private class ModelStubAcceptingPersonAdded extends ModelStub { - final ArrayList personsAdded = new ArrayList<>(); - - @Override - public boolean hasPerson(Person person) { - requireNonNull(person); - return personsAdded.stream().anyMatch(person::isSamePerson); - } - - @Override - public void addPerson(Person person) { - requireNonNull(person); - personsAdded.add(person); - } - - @Override - public ReadOnlyAddressBook getAddressBook() { - return new AddressBook(); - } - } - -} diff --git a/src/test/java/seedu/address/logic/commands/ClearCommandTest.java b/src/test/java/seedu/address/logic/commands/ClearCommandTest.java deleted file mode 100644 index 80d9110c03a..00000000000 --- a/src/test/java/seedu/address/logic/commands/ClearCommandTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package seedu.address.logic.commands; - -import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; -import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; - -import org.junit.jupiter.api.Test; - -import seedu.address.model.AddressBook; -import seedu.address.model.Model; -import seedu.address.model.ModelManager; -import seedu.address.model.UserPrefs; - -public class ClearCommandTest { - - @Test - public void execute_emptyAddressBook_success() { - Model model = new ModelManager(); - Model expectedModel = new ModelManager(); - - assertCommandSuccess(new ClearCommand(), model, ClearCommand.MESSAGE_SUCCESS, expectedModel); - } - - @Test - public void execute_nonEmptyAddressBook_success() { - Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); - Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); - expectedModel.setAddressBook(new AddressBook()); - - assertCommandSuccess(new ClearCommand(), model, ClearCommand.MESSAGE_SUCCESS, expectedModel); - } - -} diff --git a/src/test/java/seedu/address/logic/commands/CommandResultTest.java b/src/test/java/seedu/address/logic/commands/CommandResultTest.java index 4f3eb46e9ef..9cc7ad4f41d 100644 --- a/src/test/java/seedu/address/logic/commands/CommandResultTest.java +++ b/src/test/java/seedu/address/logic/commands/CommandResultTest.java @@ -5,16 +5,22 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.List; + import org.junit.jupiter.api.Test; +import seedu.address.model.event.EventChanger; +import seedu.address.testutil.TypicalEvents; + public class CommandResultTest { + private CommandResult commandResult = new CommandResult("feedback"); + @Test public void equals() { - CommandResult commandResult = new CommandResult("feedback"); // same values -> returns true assertTrue(commandResult.equals(new CommandResult("feedback"))); - assertTrue(commandResult.equals(new CommandResult("feedback", false, false))); + assertTrue(commandResult.equals(new CommandResult("feedback", false, false, false))); // same object -> returns true assertTrue(commandResult.equals(commandResult)); @@ -29,16 +35,17 @@ public void equals() { assertFalse(commandResult.equals(new CommandResult("different"))); // different showHelp value -> returns false - assertFalse(commandResult.equals(new CommandResult("feedback", true, false))); + assertFalse(commandResult.equals(new CommandResult("feedback", true, false, false))); // different exit value -> returns false - assertFalse(commandResult.equals(new CommandResult("feedback", false, true))); + assertFalse(commandResult.equals(new CommandResult("feedback", false, true, false))); + + // different showCalendar value -> return false + assertFalse(commandResult.equals(new CommandResult("feedback", false, false, true))); } @Test public void hashcode() { - CommandResult commandResult = new CommandResult("feedback"); - // same values -> returns same hashcode assertEquals(commandResult.hashCode(), new CommandResult("feedback").hashCode()); @@ -46,9 +53,70 @@ public void hashcode() { assertNotEquals(commandResult.hashCode(), new CommandResult("different").hashCode()); // different showHelp value -> returns different hashcode - assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", true, false).hashCode()); + assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", true, false, false).hashCode()); // different exit value -> returns different hashcode - assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", false, true).hashCode()); + assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", false, true, false).hashCode()); + + // different showCalendar value -> returns different hashcode + assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", false, true, false).hashCode()); + } + + @Test + public void getFeedbackToUser() { + assertEquals("feedback", commandResult.getFeedbackToUser()); + assertNotEquals("feedbacks", commandResult.getFeedbackToUser()); + } + + @Test + public void isShowHelp() { + assertFalse(commandResult.isShowHelp()); + + CommandResult commandResult1 = new CommandResult("feedback", true, false, false); + assertTrue(commandResult1.isShowHelp()); + + CommandResult commandResult2 = new CommandResult("feedback", false, true, false); + assertFalse(commandResult2.isShowHelp()); + + CommandResult commandResult3 = new CommandResult("feedback", List.of()); + assertFalse(commandResult3.isShowHelp()); + } + + @Test + public void isExit() { + assertFalse(commandResult.isExit()); + + CommandResult commandResult1 = new CommandResult("feedback", true, false, false); + assertFalse(commandResult1.isExit()); + + CommandResult commandResult2 = new CommandResult("feedback", false, true, false); + assertTrue(commandResult2.isExit()); + + CommandResult commandResult3 = new CommandResult("feedback", List.of()); + assertFalse(commandResult3.isExit()); + } + + @Test + public void isShowCalendar() { + assertFalse(commandResult.isShowCalendar()); + + CommandResult commandResult1 = new CommandResult("feedback", true, false, false); + assertFalse(commandResult1.isShowCalendar()); + + CommandResult commandResult2 = new CommandResult("feedback", false, false, true); + assertTrue(commandResult2.isShowCalendar()); + + CommandResult commandResult3 = new CommandResult("feedback", List.of()); + assertFalse(commandResult3.isShowCalendar()); + } + + @Test + public void getEventChangerList() { + assertEquals(0, commandResult.getEventChangerList().size()); + List eventChangerList = List.of( + EventChanger.editEventChanger(TypicalEvents.CS2101_MEETING, TypicalEvents.TEAM_MEETING), + EventChanger.editEventChanger(TypicalEvents.CS2103_MIDTERM_MARKED, TypicalEvents.CS2100_CONSULTATION)); + CommandResult commandResult1 = new CommandResult("feedback", eventChangerList); + assertEquals(eventChangerList, commandResult1.getEventChangerList()); } } diff --git a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java deleted file mode 100644 index 643a1d08069..00000000000 --- a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java +++ /dev/null @@ -1,128 +0,0 @@ -package seedu.address.logic.commands; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import static seedu.address.testutil.Assert.assertThrows; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.AddressBook; -import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; -import seedu.address.model.person.Person; -import seedu.address.testutil.EditPersonDescriptorBuilder; - -/** - * Contains helper methods for testing commands. - */ -public class CommandTestUtil { - - public static final String VALID_NAME_AMY = "Amy Bee"; - public static final String VALID_NAME_BOB = "Bob Choo"; - public static final String VALID_PHONE_AMY = "11111111"; - public static final String VALID_PHONE_BOB = "22222222"; - public static final String VALID_EMAIL_AMY = "amy@example.com"; - public static final String VALID_EMAIL_BOB = "bob@example.com"; - public static final String VALID_ADDRESS_AMY = "Block 312, Amy Street 1"; - public static final String VALID_ADDRESS_BOB = "Block 123, Bobby Street 3"; - public static final String VALID_TAG_HUSBAND = "husband"; - public static final String VALID_TAG_FRIEND = "friend"; - - public static final String NAME_DESC_AMY = " " + PREFIX_NAME + VALID_NAME_AMY; - public static final String NAME_DESC_BOB = " " + PREFIX_NAME + VALID_NAME_BOB; - public static final String PHONE_DESC_AMY = " " + PREFIX_PHONE + VALID_PHONE_AMY; - public static final String PHONE_DESC_BOB = " " + PREFIX_PHONE + VALID_PHONE_BOB; - public static final String EMAIL_DESC_AMY = " " + PREFIX_EMAIL + VALID_EMAIL_AMY; - public static final String EMAIL_DESC_BOB = " " + PREFIX_EMAIL + VALID_EMAIL_BOB; - public static final String ADDRESS_DESC_AMY = " " + PREFIX_ADDRESS + VALID_ADDRESS_AMY; - public static final String ADDRESS_DESC_BOB = " " + PREFIX_ADDRESS + VALID_ADDRESS_BOB; - public static final String TAG_DESC_FRIEND = " " + PREFIX_TAG + VALID_TAG_FRIEND; - public static final String TAG_DESC_HUSBAND = " " + PREFIX_TAG + VALID_TAG_HUSBAND; - - public static final String INVALID_NAME_DESC = " " + PREFIX_NAME + "James&"; // '&' not allowed in names - public static final String INVALID_PHONE_DESC = " " + PREFIX_PHONE + "911a"; // 'a' not allowed in phones - public static final String INVALID_EMAIL_DESC = " " + PREFIX_EMAIL + "bob!yahoo"; // missing '@' symbol - public static final String INVALID_ADDRESS_DESC = " " + PREFIX_ADDRESS; // empty string not allowed for addresses - public static final String INVALID_TAG_DESC = " " + PREFIX_TAG + "hubby*"; // '*' not allowed in tags - - public static final String PREAMBLE_WHITESPACE = "\t \r \n"; - public static final String PREAMBLE_NON_EMPTY = "NonEmptyPreamble"; - - public static final EditCommand.EditPersonDescriptor DESC_AMY; - public static final EditCommand.EditPersonDescriptor DESC_BOB; - - static { - DESC_AMY = new EditPersonDescriptorBuilder().withName(VALID_NAME_AMY) - .withPhone(VALID_PHONE_AMY).withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY) - .withTags(VALID_TAG_FRIEND).build(); - DESC_BOB = new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB) - .withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB) - .withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).build(); - } - - /** - * Executes the given {@code command}, confirms that
- * - the returned {@link CommandResult} matches {@code expectedCommandResult}
- * - the {@code actualModel} matches {@code expectedModel} - */ - public static void assertCommandSuccess(Command command, Model actualModel, CommandResult expectedCommandResult, - Model expectedModel) { - try { - CommandResult result = command.execute(actualModel); - assertEquals(expectedCommandResult, result); - assertEquals(expectedModel, actualModel); - } catch (CommandException ce) { - throw new AssertionError("Execution of command should not fail.", ce); - } - } - - /** - * Convenience wrapper to {@link #assertCommandSuccess(Command, Model, CommandResult, Model)} - * that takes a string {@code expectedMessage}. - */ - public static void assertCommandSuccess(Command command, Model actualModel, String expectedMessage, - Model expectedModel) { - CommandResult expectedCommandResult = new CommandResult(expectedMessage); - assertCommandSuccess(command, actualModel, expectedCommandResult, expectedModel); - } - - /** - * Executes the given {@code command}, confirms that
- * - a {@code CommandException} is thrown
- * - the CommandException message matches {@code expectedMessage}
- * - the address book, filtered person list and selected person in {@code actualModel} remain unchanged - */ - public static void assertCommandFailure(Command command, Model actualModel, String expectedMessage) { - // we are unable to defensively copy the model for comparison later, so we can - // only do so by copying its components. - AddressBook expectedAddressBook = new AddressBook(actualModel.getAddressBook()); - List expectedFilteredList = new ArrayList<>(actualModel.getFilteredPersonList()); - - assertThrows(CommandException.class, expectedMessage, () -> command.execute(actualModel)); - assertEquals(expectedAddressBook, actualModel.getAddressBook()); - assertEquals(expectedFilteredList, actualModel.getFilteredPersonList()); - } - /** - * Updates {@code model}'s filtered list to show only the person at the given {@code targetIndex} in the - * {@code model}'s address book. - */ - public static void showPersonAtIndex(Model model, Index targetIndex) { - assertTrue(targetIndex.getZeroBased() < model.getFilteredPersonList().size()); - - Person person = model.getFilteredPersonList().get(targetIndex.getZeroBased()); - final String[] splitName = person.getName().fullName.split("\\s+"); - model.updateFilteredPersonList(new NameContainsKeywordsPredicate(Arrays.asList(splitName[0]))); - - assertEquals(1, model.getFilteredPersonList().size()); - } - -} diff --git a/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java deleted file mode 100644 index 45a8c910ba1..00000000000 --- a/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java +++ /dev/null @@ -1,109 +0,0 @@ -package seedu.address.logic.commands; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; -import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; -import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; -import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; -import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; -import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; - -import org.junit.jupiter.api.Test; - -import seedu.address.commons.core.Messages; -import seedu.address.commons.core.index.Index; -import seedu.address.model.Model; -import seedu.address.model.ModelManager; -import seedu.address.model.UserPrefs; -import seedu.address.model.person.Person; - -/** - * Contains integration tests (interaction with the Model) and unit tests for - * {@code DeleteCommand}. - */ -public class DeleteCommandTest { - - private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); - - @Test - public void execute_validIndexUnfilteredList_success() { - Person personToDelete = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); - DeleteCommand deleteCommand = new DeleteCommand(INDEX_FIRST_PERSON); - - String expectedMessage = String.format(DeleteCommand.MESSAGE_DELETE_PERSON_SUCCESS, personToDelete); - - ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); - expectedModel.deletePerson(personToDelete); - - assertCommandSuccess(deleteCommand, model, expectedMessage, expectedModel); - } - - @Test - public void execute_invalidIndexUnfilteredList_throwsCommandException() { - Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); - DeleteCommand deleteCommand = new DeleteCommand(outOfBoundIndex); - - assertCommandFailure(deleteCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - @Test - public void execute_validIndexFilteredList_success() { - showPersonAtIndex(model, INDEX_FIRST_PERSON); - - Person personToDelete = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); - DeleteCommand deleteCommand = new DeleteCommand(INDEX_FIRST_PERSON); - - String expectedMessage = String.format(DeleteCommand.MESSAGE_DELETE_PERSON_SUCCESS, personToDelete); - - Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); - expectedModel.deletePerson(personToDelete); - showNoPerson(expectedModel); - - assertCommandSuccess(deleteCommand, model, expectedMessage, expectedModel); - } - - @Test - public void execute_invalidIndexFilteredList_throwsCommandException() { - showPersonAtIndex(model, INDEX_FIRST_PERSON); - - Index outOfBoundIndex = INDEX_SECOND_PERSON; - // ensures that outOfBoundIndex is still in bounds of address book list - assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getPersonList().size()); - - DeleteCommand deleteCommand = new DeleteCommand(outOfBoundIndex); - - assertCommandFailure(deleteCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - @Test - public void equals() { - DeleteCommand deleteFirstCommand = new DeleteCommand(INDEX_FIRST_PERSON); - DeleteCommand deleteSecondCommand = new DeleteCommand(INDEX_SECOND_PERSON); - - // same object -> returns true - assertTrue(deleteFirstCommand.equals(deleteFirstCommand)); - - // same values -> returns true - DeleteCommand deleteFirstCommandCopy = new DeleteCommand(INDEX_FIRST_PERSON); - assertTrue(deleteFirstCommand.equals(deleteFirstCommandCopy)); - - // different types -> returns false - assertFalse(deleteFirstCommand.equals(1)); - - // null -> returns false - assertFalse(deleteFirstCommand.equals(null)); - - // different person -> returns false - assertFalse(deleteFirstCommand.equals(deleteSecondCommand)); - } - - /** - * Updates {@code model}'s filtered list to show no one. - */ - private void showNoPerson(Model model) { - model.updateFilteredPersonList(p -> false); - - assertTrue(model.getFilteredPersonList().isEmpty()); - } -} diff --git a/src/test/java/seedu/address/logic/commands/EditCommandTest.java b/src/test/java/seedu/address/logic/commands/EditCommandTest.java deleted file mode 100644 index 214c6c2507b..00000000000 --- a/src/test/java/seedu/address/logic/commands/EditCommandTest.java +++ /dev/null @@ -1,173 +0,0 @@ -package seedu.address.logic.commands; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.logic.commands.CommandTestUtil.DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.DESC_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; -import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; -import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; -import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; -import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; -import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; -import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; - -import org.junit.jupiter.api.Test; - -import seedu.address.commons.core.Messages; -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; -import seedu.address.model.AddressBook; -import seedu.address.model.Model; -import seedu.address.model.ModelManager; -import seedu.address.model.UserPrefs; -import seedu.address.model.person.Person; -import seedu.address.testutil.EditPersonDescriptorBuilder; -import seedu.address.testutil.PersonBuilder; - -/** - * Contains integration tests (interaction with the Model) and unit tests for EditCommand. - */ -public class EditCommandTest { - - private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); - - @Test - public void execute_allFieldsSpecifiedUnfilteredList_success() { - Person editedPerson = new PersonBuilder().build(); - EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder(editedPerson).build(); - EditCommand editCommand = new EditCommand(INDEX_FIRST_PERSON, descriptor); - - String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, editedPerson); - - Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); - expectedModel.setPerson(model.getFilteredPersonList().get(0), editedPerson); - - assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); - } - - @Test - public void execute_someFieldsSpecifiedUnfilteredList_success() { - Index indexLastPerson = Index.fromOneBased(model.getFilteredPersonList().size()); - Person lastPerson = model.getFilteredPersonList().get(indexLastPerson.getZeroBased()); - - PersonBuilder personInList = new PersonBuilder(lastPerson); - Person editedPerson = personInList.withName(VALID_NAME_BOB).withPhone(VALID_PHONE_BOB) - .withTags(VALID_TAG_HUSBAND).build(); - - EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB) - .withPhone(VALID_PHONE_BOB).withTags(VALID_TAG_HUSBAND).build(); - EditCommand editCommand = new EditCommand(indexLastPerson, descriptor); - - String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, editedPerson); - - Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); - expectedModel.setPerson(lastPerson, editedPerson); - - assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); - } - - @Test - public void execute_noFieldSpecifiedUnfilteredList_success() { - EditCommand editCommand = new EditCommand(INDEX_FIRST_PERSON, new EditPersonDescriptor()); - Person editedPerson = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); - - String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, editedPerson); - - Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); - - assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); - } - - @Test - public void execute_filteredList_success() { - showPersonAtIndex(model, INDEX_FIRST_PERSON); - - Person personInFilteredList = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); - Person editedPerson = new PersonBuilder(personInFilteredList).withName(VALID_NAME_BOB).build(); - EditCommand editCommand = new EditCommand(INDEX_FIRST_PERSON, - new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB).build()); - - String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, editedPerson); - - Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); - expectedModel.setPerson(model.getFilteredPersonList().get(0), editedPerson); - - assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); - } - - @Test - public void execute_duplicatePersonUnfilteredList_failure() { - Person firstPerson = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); - EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder(firstPerson).build(); - EditCommand editCommand = new EditCommand(INDEX_SECOND_PERSON, descriptor); - - assertCommandFailure(editCommand, model, EditCommand.MESSAGE_DUPLICATE_PERSON); - } - - @Test - public void execute_duplicatePersonFilteredList_failure() { - showPersonAtIndex(model, INDEX_FIRST_PERSON); - - // edit person in filtered list into a duplicate in address book - Person personInList = model.getAddressBook().getPersonList().get(INDEX_SECOND_PERSON.getZeroBased()); - EditCommand editCommand = new EditCommand(INDEX_FIRST_PERSON, - new EditPersonDescriptorBuilder(personInList).build()); - - assertCommandFailure(editCommand, model, EditCommand.MESSAGE_DUPLICATE_PERSON); - } - - @Test - public void execute_invalidPersonIndexUnfilteredList_failure() { - Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); - EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB).build(); - EditCommand editCommand = new EditCommand(outOfBoundIndex, descriptor); - - assertCommandFailure(editCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - /** - * Edit filtered list where index is larger than size of filtered list, - * but smaller than size of address book - */ - @Test - public void execute_invalidPersonIndexFilteredList_failure() { - showPersonAtIndex(model, INDEX_FIRST_PERSON); - Index outOfBoundIndex = INDEX_SECOND_PERSON; - // ensures that outOfBoundIndex is still in bounds of address book list - assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getPersonList().size()); - - EditCommand editCommand = new EditCommand(outOfBoundIndex, - new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB).build()); - - assertCommandFailure(editCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - @Test - public void equals() { - final EditCommand standardCommand = new EditCommand(INDEX_FIRST_PERSON, DESC_AMY); - - // same values -> returns true - EditPersonDescriptor copyDescriptor = new EditPersonDescriptor(DESC_AMY); - EditCommand commandWithSameValues = new EditCommand(INDEX_FIRST_PERSON, copyDescriptor); - assertTrue(standardCommand.equals(commandWithSameValues)); - - // same object -> returns true - assertTrue(standardCommand.equals(standardCommand)); - - // null -> returns false - assertFalse(standardCommand.equals(null)); - - // different types -> returns false - assertFalse(standardCommand.equals(new ClearCommand())); - - // different index -> returns false - assertFalse(standardCommand.equals(new EditCommand(INDEX_SECOND_PERSON, DESC_AMY))); - - // different descriptor -> returns false - assertFalse(standardCommand.equals(new EditCommand(INDEX_FIRST_PERSON, DESC_BOB))); - } - -} diff --git a/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java b/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java deleted file mode 100644 index e0288792e72..00000000000 --- a/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package seedu.address.logic.commands; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.logic.commands.CommandTestUtil.DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.DESC_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; - -import org.junit.jupiter.api.Test; - -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; -import seedu.address.testutil.EditPersonDescriptorBuilder; - -public class EditPersonDescriptorTest { - - @Test - public void equals() { - // same values -> returns true - EditPersonDescriptor descriptorWithSameValues = new EditPersonDescriptor(DESC_AMY); - assertTrue(DESC_AMY.equals(descriptorWithSameValues)); - - // same object -> returns true - assertTrue(DESC_AMY.equals(DESC_AMY)); - - // null -> returns false - assertFalse(DESC_AMY.equals(null)); - - // different types -> returns false - assertFalse(DESC_AMY.equals(5)); - - // different values -> returns false - assertFalse(DESC_AMY.equals(DESC_BOB)); - - // different name -> returns false - EditPersonDescriptor editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withName(VALID_NAME_BOB).build(); - assertFalse(DESC_AMY.equals(editedAmy)); - - // different phone -> returns false - editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withPhone(VALID_PHONE_BOB).build(); - assertFalse(DESC_AMY.equals(editedAmy)); - - // different email -> returns false - editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withEmail(VALID_EMAIL_BOB).build(); - assertFalse(DESC_AMY.equals(editedAmy)); - - // different address -> returns false - editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withAddress(VALID_ADDRESS_BOB).build(); - assertFalse(DESC_AMY.equals(editedAmy)); - - // different tags -> returns false - editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withTags(VALID_TAG_HUSBAND).build(); - assertFalse(DESC_AMY.equals(editedAmy)); - } -} diff --git a/src/test/java/seedu/address/logic/commands/FindCommandTest.java b/src/test/java/seedu/address/logic/commands/FindCommandTest.java deleted file mode 100644 index 9b15db28bbb..00000000000 --- a/src/test/java/seedu/address/logic/commands/FindCommandTest.java +++ /dev/null @@ -1,83 +0,0 @@ -package seedu.address.logic.commands; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.commons.core.Messages.MESSAGE_PERSONS_LISTED_OVERVIEW; -import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; -import static seedu.address.testutil.TypicalPersons.CARL; -import static seedu.address.testutil.TypicalPersons.ELLE; -import static seedu.address.testutil.TypicalPersons.FIONA; -import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; - -import java.util.Arrays; -import java.util.Collections; - -import org.junit.jupiter.api.Test; - -import seedu.address.model.Model; -import seedu.address.model.ModelManager; -import seedu.address.model.UserPrefs; -import seedu.address.model.person.NameContainsKeywordsPredicate; - -/** - * Contains integration tests (interaction with the Model) for {@code FindCommand}. - */ -public class FindCommandTest { - private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); - private Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); - - @Test - public void equals() { - NameContainsKeywordsPredicate firstPredicate = - new NameContainsKeywordsPredicate(Collections.singletonList("first")); - NameContainsKeywordsPredicate secondPredicate = - new NameContainsKeywordsPredicate(Collections.singletonList("second")); - - FindCommand findFirstCommand = new FindCommand(firstPredicate); - FindCommand findSecondCommand = new FindCommand(secondPredicate); - - // same object -> returns true - assertTrue(findFirstCommand.equals(findFirstCommand)); - - // same values -> returns true - FindCommand findFirstCommandCopy = new FindCommand(firstPredicate); - assertTrue(findFirstCommand.equals(findFirstCommandCopy)); - - // different types -> returns false - assertFalse(findFirstCommand.equals(1)); - - // null -> returns false - assertFalse(findFirstCommand.equals(null)); - - // different person -> returns false - assertFalse(findFirstCommand.equals(findSecondCommand)); - } - - @Test - public void execute_zeroKeywords_noPersonFound() { - String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 0); - NameContainsKeywordsPredicate predicate = preparePredicate(" "); - FindCommand command = new FindCommand(predicate); - expectedModel.updateFilteredPersonList(predicate); - assertCommandSuccess(command, model, expectedMessage, expectedModel); - assertEquals(Collections.emptyList(), model.getFilteredPersonList()); - } - - @Test - public void execute_multipleKeywords_multiplePersonsFound() { - String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 3); - NameContainsKeywordsPredicate predicate = preparePredicate("Kurz Elle Kunz"); - FindCommand command = new FindCommand(predicate); - expectedModel.updateFilteredPersonList(predicate); - assertCommandSuccess(command, model, expectedMessage, expectedModel); - assertEquals(Arrays.asList(CARL, ELLE, FIONA), model.getFilteredPersonList()); - } - - /** - * Parses {@code userInput} into a {@code NameContainsKeywordsPredicate}. - */ - private NameContainsKeywordsPredicate preparePredicate(String userInput) { - return new NameContainsKeywordsPredicate(Arrays.asList(userInput.split("\\s+"))); - } -} diff --git a/src/test/java/seedu/address/logic/commands/ListCommandTest.java b/src/test/java/seedu/address/logic/commands/ListCommandTest.java deleted file mode 100644 index 435ff1f7275..00000000000 --- a/src/test/java/seedu/address/logic/commands/ListCommandTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package seedu.address.logic.commands; - -import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; -import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; -import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; -import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import seedu.address.model.Model; -import seedu.address.model.ModelManager; -import seedu.address.model.UserPrefs; - -/** - * Contains integration tests (interaction with the Model) and unit tests for ListCommand. - */ -public class ListCommandTest { - - private Model model; - private Model expectedModel; - - @BeforeEach - public void setUp() { - model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); - expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); - } - - @Test - public void execute_listIsNotFiltered_showsSameList() { - assertCommandSuccess(new ListCommand(), model, ListCommand.MESSAGE_SUCCESS, expectedModel); - } - - @Test - public void execute_listIsFiltered_showsEverything() { - showPersonAtIndex(model, INDEX_FIRST_PERSON); - assertCommandSuccess(new ListCommand(), model, ListCommand.MESSAGE_SUCCESS, expectedModel); - } -} diff --git a/src/test/java/seedu/address/logic/commands/ModelStub.java b/src/test/java/seedu/address/logic/commands/ModelStub.java new file mode 100644 index 00000000000..f4d74f48216 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/ModelStub.java @@ -0,0 +1,244 @@ +package seedu.address.logic.commands; + +import java.nio.file.Path; +import java.util.List; +import java.util.function.Predicate; + +import javafx.collections.ObservableList; +import seedu.address.commons.core.GuiSettings; +import seedu.address.commons.core.index.Index; +import seedu.address.model.Model; +import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyUserPrefs; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.ContactDisplaySetting; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventDisplaySetting; + +/** + * A default model stub that have all of the methods failing. + */ +public class ModelStub implements Model { + public static final String ERROR_MESSAGE = "This method should not be called."; + + @Override + public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public ReadOnlyUserPrefs getUserPrefs() { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public GuiSettings getGuiSettings() { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void setGuiSettings(GuiSettings guiSettings) { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public EventDisplaySetting getEventDisplaySetting() { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void setEventDisplaySetting(EventDisplaySetting eventDisplaySetting) { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public ContactDisplaySetting getContactDisplaySetting() { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void setContactDisplaySetting(ContactDisplaySetting displaySetting) { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public Path getAddressBookFilePath() { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void setAddressBookFilePath(Path addressBookFilePath) { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void setAddressBook(ReadOnlyAddressBook newData) { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public ReadOnlyAddressBook getAddressBook() { + throw new AssertionError(ERROR_MESSAGE); + } + + // manage versioned addressBook + @Override + public void commitHistory() { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void undoHistory() { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void redoHistory() { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public boolean isUndoable() { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public boolean isRedoable() { + throw new AssertionError(ERROR_MESSAGE); + } + + // manage contacts + @Override + public void addContact(Contact contact) { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public boolean hasContact(Contact contact) { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void deleteContact(Contact target) { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void setContact(Contact target, Contact editedContact) { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void resetContacts() { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public ObservableList getFilteredContactList() { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void updateFilteredContactList(Predicate predicate) { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void updateContactListByIndex(Index index) { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void rearrangeContactsInOrder(List contacts, boolean isMark) { + throw new AssertionError(ERROR_MESSAGE); + } + + // manage events + + @Override + public void addEvent(Event event) { + throw new AssertionError(ERROR_MESSAGE); + } + + + @Override + public boolean hasEvent(Event event) { + throw new AssertionError(ERROR_MESSAGE); + } + + + @Override + public void deleteEvent(Event target) { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void setEvent(Event target, Event editedEvent) { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public ObservableList getFilteredEventList() { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void resetEvents() { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void updateFilteredEventList(Predicate predicate) { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void sortUpcomingFilteredEventList() { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void updateEventListByIndex(Index index) { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void rearrangeEventsInOrder(List events, boolean isMark) { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void linkEventAndContact(Event event, Contact contact) { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void unlinkEventAndContact(Event event, Contact contact) { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void unlinkAllContactsFromEvent(Event event) { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void rerenderContactCards(boolean useBackSamePredicate) { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void rerenderEventCards(boolean useBackSamePredicate) { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void rerenderAllCards() { + throw new AssertionError(ERROR_MESSAGE); + } + + @Override + public void removeAllLinks() { + throw new AssertionError(ERROR_MESSAGE); + } +} diff --git a/src/test/java/seedu/address/logic/commands/contact/CAddCommandIntegrationTest.java b/src/test/java/seedu/address/logic/commands/contact/CAddCommandIntegrationTest.java new file mode 100644 index 00000000000..d66329a3bfc --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/contact/CAddCommandIntegrationTest.java @@ -0,0 +1,44 @@ +package seedu.address.logic.commands.contact; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalAddressBook.getTypicalAddressBook; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.contact.Contact; +import seedu.address.testutil.ContactBuilder; + +/** + * Contains integration tests (interaction with the Model) for {@code CAddCommand}. + */ +public class CAddCommandIntegrationTest { + + private Model model; + + @BeforeEach + public void setUp() { + model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + } + + @Test + public void execute_newContact_success() { + Contact validContact = new ContactBuilder().build(); + Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + expectedModel.addContact(validContact); + + assertCommandSuccess(new CAddCommand(validContact), model, + String.format(CAddCommand.MESSAGE_SUCCESS, validContact), expectedModel); + } + + @Test + public void execute_duplicateContact_throwsCommandException() { + Contact contactInList = model.getAddressBook().getContactList().get(0); + assertCommandFailure(new CAddCommand(contactInList), model, + CAddCommand.MESSAGE_DUPLICATE_CONTACT); + } + +} diff --git a/src/test/java/seedu/address/logic/commands/contact/CAddCommandTest.java b/src/test/java/seedu/address/logic/commands/contact/CAddCommandTest.java new file mode 100644 index 00000000000..3213682493a --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/contact/CAddCommandTest.java @@ -0,0 +1,118 @@ +package seedu.address.logic.commands.contact; + +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import java.util.ArrayList; +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.ModelStub; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.AddressBook; +import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.contact.Contact; +import seedu.address.testutil.ContactBuilder; + +public class CAddCommandTest { + + @Test + public void constructor_nullContact_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new CAddCommand(null)); + } + + @Test + public void execute_contactAcceptedByModel_addSuccessful() throws Exception { + ModelStubAcceptingContactAdded modelStub = new ModelStubAcceptingContactAdded(); + Contact validContact = new ContactBuilder().build(); + + CommandResult commandResult = new CAddCommand(validContact).execute(modelStub); + + assertEquals(String.format(CAddCommand.MESSAGE_SUCCESS, validContact), commandResult.getFeedbackToUser()); + assertEquals(Arrays.asList(validContact), modelStub.contactsAdded); + } + + @Test + public void execute_duplicateContact_throwsCommandException() { + Contact validContact = new ContactBuilder().build(); + CAddCommand addCommand = new CAddCommand(validContact); + ModelStub modelStub = new ModelStubWithContact(validContact); + + assertThrows(CommandException.class, CAddCommand.MESSAGE_DUPLICATE_CONTACT, () -> + addCommand.execute(modelStub)); + } + + @Test + public void equals() { + Contact alice = new ContactBuilder().withName("Alice").build(); + Contact bob = new ContactBuilder().withName("Bob").build(); + CAddCommand addAliceCommand = new CAddCommand(alice); + CAddCommand addBobCommand = new CAddCommand(bob); + + // same object -> returns true + assertTrue(addAliceCommand.equals(addAliceCommand)); + + // same values -> returns true + CAddCommand addAliceCommandCopy = new CAddCommand(alice); + assertTrue(addAliceCommand.equals(addAliceCommandCopy)); + + // different types -> returns false + assertFalse(addAliceCommand.equals(1)); + + // null -> returns false + assertFalse(addAliceCommand.equals(null)); + + // different contact -> returns false + assertFalse(addAliceCommand.equals(addBobCommand)); + } + + + /** + * A Model stub that contains a single contact. + */ + private class ModelStubWithContact extends ModelStub { + private final Contact contact; + + ModelStubWithContact(Contact contact) { + requireNonNull(contact); + this.contact = contact; + } + + @Override + public boolean hasContact(Contact contact) { + requireNonNull(contact); + return this.contact.isSameContact(contact); + } + + } + + /** + * A Model stub that always accept the contact being added. + */ + private class ModelStubAcceptingContactAdded extends ModelStub { + final ArrayList contactsAdded = new ArrayList<>(); + + @Override + public boolean hasContact(Contact contact) { + requireNonNull(contact); + return contactsAdded.stream().anyMatch(contact::isSameContact); + } + + @Override + public void addContact(Contact contact) { + requireNonNull(contact); + contactsAdded.add(contact); + } + + @Override + public ReadOnlyAddressBook getAddressBook() { + return new AddressBook(); + } + + } +} diff --git a/src/test/java/seedu/address/logic/commands/contact/CClearCommandTest.java b/src/test/java/seedu/address/logic/commands/contact/CClearCommandTest.java new file mode 100644 index 00000000000..89a97f7c315 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/contact/CClearCommandTest.java @@ -0,0 +1,31 @@ +package seedu.address.logic.commands.contact; + +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalAddressBook.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; + +public class CClearCommandTest { + + @Test + public void execute_emptyContactList_success() { + Model model = new ModelManager(); + Model expectedModel = new ModelManager(); + + assertCommandSuccess(new CClearCommand(), model, CClearCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void execute_nonEmptyContactList_success() { + Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + expectedModel.resetContacts(); + + assertCommandSuccess(new CClearCommand(), model, CClearCommand.MESSAGE_SUCCESS, expectedModel); + } + +} diff --git a/src/test/java/seedu/address/logic/commands/contact/CDeleteCommandIntegrationTest.java b/src/test/java/seedu/address/logic/commands/contact/CDeleteCommandIntegrationTest.java new file mode 100644 index 00000000000..1211813a0d7 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/contact/CDeleteCommandIntegrationTest.java @@ -0,0 +1,62 @@ +package seedu.address.logic.commands.contact; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalAddressBook.getTypicalAddressBook; +import static seedu.address.testutil.TypicalEvents.EXAM; +import static seedu.address.testutil.TypicalEvents.INTERVIEW; +import static seedu.address.testutil.TypicalRanges.RANGE_FIRST_TO_FIRST; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.contact.Contact; +import seedu.address.model.event.Event; + +/** + * Contains integration tests (interaction with the Model and checks if unlinking is done properly) + * for {@code CDeleteCommand}. + */ +public class CDeleteCommandIntegrationTest { + + private Model model; + + @BeforeEach + public void setUp() { + model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + } + + @Test + public void execute_deleteContact_success() { + Contact contactToDelete = model.getFilteredContactList().get(0); + Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + expectedModel.deleteContact(contactToDelete); + + assertCommandSuccess(new CDeleteCommand(RANGE_FIRST_TO_FIRST), model, + String.format(CDeleteCommand.MESSAGE_DELETE_CONTACT_SUCCESS, contactToDelete), expectedModel); + } + + @Test + public void execute_deleteContactWithLinks_success() throws CommandException { + // link contact to two events before deleting + Event event1ToLink = INTERVIEW; + Event event2ToLink = EXAM; + model.addEvent(event1ToLink); + model.addEvent(event2ToLink); + // due to contact immutability, have to get new contact from the contact list + model.linkEventAndContact(event1ToLink, model.getFilteredContactList().get(0)); + model.linkEventAndContact(event2ToLink, model.getFilteredContactList().get(0)); + Contact contactToDelete = model.getFilteredContactList().get(0); + assertTrue(contactToDelete.hasLinkTo(event1ToLink)); + assertTrue(contactToDelete.hasLinkTo(event2ToLink)); + CDeleteCommand cDeleteCommand = new CDeleteCommand(RANGE_FIRST_TO_FIRST); + cDeleteCommand.execute(model); + assertFalse(event1ToLink.hasLinkTo(contactToDelete)); + assertFalse(event2ToLink.hasLinkTo(contactToDelete)); + } +} diff --git a/src/test/java/seedu/address/logic/commands/contact/CDeleteCommandTest.java b/src/test/java/seedu/address/logic/commands/contact/CDeleteCommandTest.java new file mode 100644 index 00000000000..d243cbb357b --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/contact/CDeleteCommandTest.java @@ -0,0 +1,138 @@ +package seedu.address.logic.commands.contact; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.general.CommandTestUtil.showContactAtIndex; +import static seedu.address.testutil.TypicalAddressBook.getTypicalAddressBook; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND; +import static seedu.address.testutil.TypicalIndexes.INDEX_THIRD; +import static seedu.address.testutil.TypicalRanges.RANGE_FIRST_TO_FIRST; +import static seedu.address.testutil.TypicalRanges.RANGE_FIRST_TO_THIRD; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.core.range.Range; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.contact.Contact; + +/** + * Contains integration tests (interaction with the Model) and unit tests for + * {@code DeleteCommand}. + */ +public class CDeleteCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + private Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + + @Test + public void execute_validIndexUnfilteredList_success() { + Contact contactToDelete = model.getFilteredContactList().get(INDEX_FIRST.getZeroBased()); + CDeleteCommand cDeleteCommand = new CDeleteCommand(RANGE_FIRST_TO_FIRST); + + String expectedMessage = String.format(CDeleteCommand.MESSAGE_DELETE_CONTACT_SUCCESS, contactToDelete); + + expectedModel.deleteContact(contactToDelete); + + assertCommandSuccess(cDeleteCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndexUnfilteredList_throwsCommandException() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredContactList().size() + 1); + Range rangeOfIndexes = Range.convertFromIndex(outOfBoundIndex); + CDeleteCommand cDeleteCommand = new CDeleteCommand(rangeOfIndexes); + + assertCommandFailure(cDeleteCommand, model, Messages.MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX); + } + + @Test + public void execute_validIndexFilteredList_success() { + showContactAtIndex(model, INDEX_FIRST); + + Contact contactToDelete = model.getFilteredContactList().get(INDEX_FIRST.getZeroBased()); + CDeleteCommand cDeleteCommand = new CDeleteCommand(RANGE_FIRST_TO_FIRST); + + String expectedMessage = String.format(CDeleteCommand.MESSAGE_DELETE_CONTACT_SUCCESS, contactToDelete); + + expectedModel.deleteContact(contactToDelete); + showNoContact(expectedModel); + + assertCommandSuccess(cDeleteCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndexFilteredList_throwsCommandException() { + showContactAtIndex(model, INDEX_FIRST); + + Index outOfBoundIndex = INDEX_SECOND; + // ensures that outOfBoundIndex is still in bounds of address book list + assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getContactList().size()); + Range rangeOfIndexes = Range.convertFromIndex(outOfBoundIndex); + + CDeleteCommand cDeleteCommand = new CDeleteCommand(rangeOfIndexes); + + assertCommandFailure(cDeleteCommand, model, Messages.MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX); + } + + @Test + public void execute_validRangeUnfilteredList_success() { + Contact firstContactToDelete = model.getFilteredContactList().get(INDEX_FIRST.getZeroBased()); + Contact secondContactToDelete = model.getFilteredContactList().get(INDEX_SECOND.getZeroBased()); + Contact thirdContactToDelete = model.getFilteredContactList().get(INDEX_THIRD.getZeroBased()); + CDeleteCommand cDeleteCommand = new CDeleteCommand(RANGE_FIRST_TO_THIRD); + + String expectedMessage = String.format(CDeleteCommand.MESSAGE_DELETE_CONTACT_SUCCESS, firstContactToDelete) + + "\n" + + String.format(CDeleteCommand.MESSAGE_DELETE_CONTACT_SUCCESS, secondContactToDelete) + + "\n" + + String.format(CDeleteCommand.MESSAGE_DELETE_CONTACT_SUCCESS, thirdContactToDelete); + + expectedModel.deleteContact(firstContactToDelete); + expectedModel.deleteContact(secondContactToDelete); + expectedModel.deleteContact(thirdContactToDelete); + + assertCommandSuccess(cDeleteCommand, model, expectedMessage, expectedModel); + } + + @Test + public void equals() { + Range fromIndexOneToTwo = new Range(INDEX_FIRST, INDEX_SECOND); + Range fromIndexOneToThree = new Range(INDEX_FIRST, INDEX_THIRD); + CDeleteCommand deleteFirstCommand = new CDeleteCommand(fromIndexOneToTwo); + CDeleteCommand deleteSecondCommand = new CDeleteCommand(fromIndexOneToThree); + + // same object -> returns true + assertTrue(deleteFirstCommand.equals(deleteFirstCommand)); + + // same values -> returns true + CDeleteCommand deleteFirstCommandCopy = new CDeleteCommand(fromIndexOneToTwo); + assertTrue(deleteFirstCommand.equals(deleteFirstCommandCopy)); + + // different types -> returns false + assertFalse(deleteFirstCommand.equals(1)); + + // null -> returns false + assertFalse(deleteFirstCommand.equals(null)); + + // different contact -> returns false + assertFalse(deleteFirstCommand.equals(deleteSecondCommand)); + } + + /** + * Updates {@code model}'s filtered list to show no one. + */ + private void showNoContact(Model model) { + model.updateFilteredContactList(p -> false); + + assertTrue(model.getFilteredContactList().isEmpty()); + } +} diff --git a/src/test/java/seedu/address/logic/commands/contact/CEditCommandTest.java b/src/test/java/seedu/address/logic/commands/contact/CEditCommandTest.java new file mode 100644 index 00000000000..cec6af00efc --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/contact/CEditCommandTest.java @@ -0,0 +1,208 @@ +package seedu.address.logic.commands.contact; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.general.CommandTestUtil.DESC_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.DESC_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_NAME_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_PHONE_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_TAG_HUSBAND; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.general.CommandTestUtil.showContactAtIndex; +import static seedu.address.testutil.TypicalAddressBook.getTypicalAddressBook; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND; +import static seedu.address.testutil.TypicalIndexes.INDEX_THIRD; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.contact.CEditCommand.EditContactDescriptor; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.contact.Contact; +import seedu.address.model.tag.Tag; +import seedu.address.testutil.ContactBuilder; +import seedu.address.testutil.EditContactDescriptorBuilder; + +/** + * Contains integration tests (interaction with the Model) and unit tests for CEditCommand. + */ +public class CEditCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_allFieldsSpecifiedUnfilteredList_success() { + Contact editedContact = new ContactBuilder().build(); + EditContactDescriptor descriptor = new EditContactDescriptorBuilder(editedContact, + null, true).build(); + CEditCommand cEditCommand = new CEditCommand(INDEX_FIRST, descriptor); + + String expectedMessage = String.format(CEditCommand.MESSAGE_EDIT_CONTACT_SUCCESS, editedContact); + + Model expectedModel = new ModelManager(new AddressBook(getTypicalAddressBook()), new UserPrefs()); + expectedModel.setContact(model.getFilteredContactList().get(0), editedContact); + + assertCommandSuccess(cEditCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_someFieldsSpecifiedUnfilteredList_success() { + Index indexLastContact = Index.fromOneBased(model.getFilteredContactList().size()); + Contact lastContact = model.getFilteredContactList().get(indexLastContact.getZeroBased()); + + ContactBuilder contactInList = new ContactBuilder(lastContact); + Contact editedContact = contactInList.withName(VALID_NAME_BOB).withPhone(VALID_PHONE_BOB) + .withTags(VALID_TAG_HUSBAND).build(); + + EditContactDescriptor descriptor = new EditContactDescriptorBuilder().withName(VALID_NAME_BOB) + .withPhone(VALID_PHONE_BOB).withTags(VALID_TAG_HUSBAND).build(); + CEditCommand cEditCommand = new CEditCommand(indexLastContact, descriptor); + + String expectedMessage = String.format(CEditCommand.MESSAGE_EDIT_CONTACT_SUCCESS, editedContact); + + Model expectedModel = new ModelManager(new AddressBook(getTypicalAddressBook()), new UserPrefs()); + expectedModel.setContact(lastContact, editedContact); + + assertCommandSuccess(cEditCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_noFieldSpecifiedUnfilteredList_success() { + CEditCommand cEditCommand = new CEditCommand(INDEX_FIRST, new CEditCommand.EditContactDescriptor()); + Contact editedContact = model.getFilteredContactList().get(INDEX_FIRST.getZeroBased()); + + String expectedMessage = String.format(CEditCommand.MESSAGE_EDIT_CONTACT_SUCCESS, editedContact); + + Model expectedModel = new ModelManager(new AddressBook(getTypicalAddressBook()), new UserPrefs()); + + assertCommandSuccess(cEditCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_filteredList_success() { + showContactAtIndex(model, INDEX_FIRST); + + Contact contactInFilteredList = model.getFilteredContactList().get(INDEX_FIRST.getZeroBased()); + Contact editedContact = new ContactBuilder(contactInFilteredList).withName(VALID_NAME_BOB).build(); + CEditCommand cEditCommand = new CEditCommand(INDEX_FIRST, + new EditContactDescriptorBuilder().withName(VALID_NAME_BOB).build()); + + String expectedMessage = String.format(CEditCommand.MESSAGE_EDIT_CONTACT_SUCCESS, editedContact); + + Model expectedModel = new ModelManager(new AddressBook(getTypicalAddressBook()), new UserPrefs()); + expectedModel.setContact(model.getFilteredContactList().get(0), editedContact); + + assertCommandSuccess(cEditCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_duplicateContactUnfilteredList_failure() { + Contact firstContact = model.getFilteredContactList().get(INDEX_FIRST.getZeroBased()); + EditContactDescriptor descriptor = new EditContactDescriptorBuilder(firstContact, Set.of(), false).build(); + CEditCommand cEditCommand = new CEditCommand(INDEX_SECOND, descriptor); + + assertCommandFailure(cEditCommand, model, CEditCommand.MESSAGE_DUPLICATE_CONTACT); + } + + @Test + public void execute_duplicateContactFilteredList_failure() { + showContactAtIndex(model, INDEX_FIRST); + + // edit contact in filtered list into a duplicate in address book + Contact contactInList = getTypicalAddressBook().getContactList().get(INDEX_SECOND.getZeroBased()); + CEditCommand cEditCommand = new CEditCommand(INDEX_FIRST, + new EditContactDescriptorBuilder(contactInList, Set.of(), false).build()); + + assertCommandFailure(cEditCommand, model, CEditCommand.MESSAGE_DUPLICATE_CONTACT); + } + + @Test + public void execute_invalidContactIndexUnfilteredList_failure() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredContactList().size() + 1); + EditContactDescriptor descriptor = new EditContactDescriptorBuilder().withName(VALID_NAME_BOB).build(); + CEditCommand cEditCommand = new CEditCommand(outOfBoundIndex, descriptor); + + assertCommandFailure(cEditCommand, model, Messages.MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX); + } + + /** + * Edit filtered list where index is larger than size of filtered list, + * but smaller than size of address book + */ + @Test + public void execute_invalidContactIndexFilteredList_failure() { + showContactAtIndex(model, INDEX_FIRST); + Index outOfBoundIndex = INDEX_SECOND; + // ensures that outOfBoundIndex is still in bounds of address book list + assertTrue(outOfBoundIndex.getZeroBased() < getTypicalAddressBook().getContactList().size()); + + CEditCommand cEditCommand = new CEditCommand(outOfBoundIndex, + new EditContactDescriptorBuilder().withName(VALID_NAME_BOB).build()); + + assertCommandFailure(cEditCommand, model, Messages.MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX); + } + + @Test + public void execute_tagToDeleteNotInOriginalContact_success() { + Tag toDelete = new Tag("notInOriginal"); + Contact defaultContact = new ContactBuilder().withMarked(false).build(); // with friends and unmarked + Contact editedContact = new ContactBuilder().withTags().build(); + EditContactDescriptor descriptor = new EditContactDescriptorBuilder(editedContact, + Set.of(toDelete), false).build(); + // the index must not have any tags initially (check TypicalContacts) + CEditCommand cEditCommand = new CEditCommand(INDEX_THIRD, descriptor); + String expectedMessage = String.format(CEditCommand.MESSAGE_EDIT_CONTACT_SUCCESS, defaultContact) + + "\nNote:\n" + String.format(CEditCommand.MESSAGE_TAG_TO_DELETE_NOT_IN_ORIGINAL, toDelete); + Model expectedModel = new ModelManager(new AddressBook(getTypicalAddressBook()), new UserPrefs()); + expectedModel.setContact(model.getFilteredContactList().get(2), defaultContact); + assertCommandSuccess(cEditCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_tagToAddAlreadyInOriginalContact_success() { + Tag toAdd = new Tag("friends"); + Contact editedContact = new ContactBuilder().withTags("friends").build(); + EditContactDescriptor descriptor = new EditContactDescriptorBuilder(editedContact, + null, false).build(); + // the index must contain [friends] tag initially (check TypicalContacts) + CEditCommand cEditCommand = new CEditCommand(INDEX_FIRST, descriptor); + String expectedMessage = String.format(CEditCommand.MESSAGE_EDIT_CONTACT_SUCCESS, editedContact) + + "\nNote:\n" + String.format(CEditCommand.MESSAGE_TAG_TO_ADD_ALREADY_IN_ORIGINAL, toAdd); + Model expectedModel = new ModelManager(new AddressBook(getTypicalAddressBook()), new UserPrefs()); + expectedModel.setContact(model.getFilteredContactList().get(0), editedContact); + assertCommandSuccess(cEditCommand, model, expectedMessage, expectedModel); + } + + @Test + public void equals() { + final CEditCommand standardCommand = new CEditCommand(INDEX_FIRST, DESC_AMY); + + // same values -> returns true + EditContactDescriptor copyDescriptor = new EditContactDescriptor(DESC_AMY); + CEditCommand commandWithSameValues = new CEditCommand(INDEX_FIRST, copyDescriptor); + assertTrue(standardCommand.equals(commandWithSameValues)); + + // same object -> returns true + assertTrue(standardCommand.equals(standardCommand)); + + // null -> returns false + assertFalse(standardCommand.equals(null)); + + // different types -> returns false + assertFalse(standardCommand.equals(new CClearCommand())); + + // different index -> returns false + assertFalse(standardCommand.equals(new CEditCommand(INDEX_SECOND, DESC_AMY))); + + // different descriptor -> returns false + assertFalse(standardCommand.equals(new CEditCommand(INDEX_FIRST, DESC_BOB))); + } +} diff --git a/src/test/java/seedu/address/logic/commands/contact/CFindCommandTest.java b/src/test/java/seedu/address/logic/commands/contact/CFindCommandTest.java new file mode 100644 index 00000000000..33de6f3c078 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/contact/CFindCommandTest.java @@ -0,0 +1,95 @@ +package seedu.address.logic.commands.contact; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.commons.core.Messages.MESSAGE_CONTACTS_LISTED_OVERVIEW; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalAddressBook.getTypicalAddressBook; +import static seedu.address.testutil.TypicalContacts.CARL; +import static seedu.address.testutil.TypicalContacts.ELLE; +import static seedu.address.testutil.TypicalContacts.FIONA; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.contact.ContactContainsKeywordsPredicate; + +/** + * Contains integration tests (interaction with the Model) for {@code CFindCommand}. + */ +public class CFindCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void equals() { + String firstSearchPhrase = "first"; + String secondSearchPhrase = "second"; + ContactContainsKeywordsPredicate firstPredicate = + new ContactContainsKeywordsPredicate(Collections.singletonList(firstSearchPhrase)); + ContactContainsKeywordsPredicate secondPredicate = + new ContactContainsKeywordsPredicate(Collections.singletonList(secondSearchPhrase)); + + CFindCommand findFirstCommand = new CFindCommand(firstPredicate); + CFindCommand findSecondCommand = new CFindCommand(secondPredicate); + + // same object -> returns true + assertTrue(findFirstCommand.equals(findFirstCommand)); + + // same values -> returns true + CFindCommand findFirstCommandCopy = new CFindCommand(firstPredicate); + assertTrue(findFirstCommand.equals(findFirstCommandCopy)); + + // different types -> returns false + assertFalse(findFirstCommand.equals(1)); + + // null -> returns false + assertFalse(findFirstCommand.equals(null)); + + // different contact -> returns false + assertFalse(findFirstCommand.equals(findSecondCommand)); + } + + @Test + public void execute_zeroKeywords_noContactFound() { + String expectedMessage = String.format(MESSAGE_CONTACTS_LISTED_OVERVIEW, 0); + ContactContainsKeywordsPredicate predicate = preparePredicate(" "); + CFindCommand command = new CFindCommand(predicate); + expectedModel.updateFilteredContactList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Collections.emptyList(), model.getFilteredContactList()); + } + + @Test + public void execute_multipleCompleteKeywords_multipleContactsFound() { + String expectedMessage = String.format(MESSAGE_CONTACTS_LISTED_OVERVIEW, 3); + ContactContainsKeywordsPredicate predicate = preparePredicate("Kurz Elle Kunz"); + CFindCommand command = new CFindCommand(predicate); + expectedModel.updateFilteredContactList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(CARL, ELLE, FIONA), model.getFilteredContactList()); + } + + @Test + public void execute_multipleIncompleteKeywords_multipleContactsFound() { + String expectedMessage = String.format(MESSAGE_CONTACTS_LISTED_OVERVIEW, 3); + ContactContainsKeywordsPredicate predicate = preparePredicate("Ku Ell"); + CFindCommand command = new CFindCommand(predicate); + expectedModel.updateFilteredContactList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(CARL, ELLE, FIONA), model.getFilteredContactList()); + } + + /** + * Parses {@code userInput} into a {@code ContactContainsKeywordsPredicate}. + */ + private ContactContainsKeywordsPredicate preparePredicate(String userInput) { + return new ContactContainsKeywordsPredicate(Arrays.asList(userInput.split("\\s+"))); + } +} diff --git a/src/test/java/seedu/address/logic/commands/contact/CListCommandTest.java b/src/test/java/seedu/address/logic/commands/contact/CListCommandTest.java new file mode 100644 index 00000000000..2cdd7d409b7 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/contact/CListCommandTest.java @@ -0,0 +1,55 @@ +package seedu.address.logic.commands.contact; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.general.CommandTestUtil.showContactAtIndex; +import static seedu.address.model.contact.ContactDisplaySetting.DEFAULT_SETTING; +import static seedu.address.testutil.TypicalAddressBook.getTypicalAddressBook; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.contact.ContactDisplaySetting; + +/** + * Contains integration tests (interaction with the Model) and unit tests for ListCommand. + */ +public class CListCommandTest { + + private Model model; + private Model expectedModel; + + @BeforeEach + public void setUp() { + model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + } + + @Test + public void execute_listIsNotFiltered_showsSameList() { + assertCommandSuccess(new CListCommand(ContactDisplaySetting.DEFAULT_SETTING), model, + CListCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void execute_listIsFiltered_showsEverything() { + showContactAtIndex(model, INDEX_FIRST); + assertCommandSuccess(new CListCommand(ContactDisplaySetting.DEFAULT_SETTING), + model, CListCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void equal() { + CListCommand standardCommand = new CListCommand(DEFAULT_SETTING); + assertTrue(standardCommand.equals(standardCommand)); + assertTrue(standardCommand.equals(new CListCommand(DEFAULT_SETTING))); + assertFalse(standardCommand.equals(new CListCommand(new ContactDisplaySetting(false, false, + false, true, true, true)))); + assertFalse(standardCommand.equals(new CClearCommand())); + } +} diff --git a/src/test/java/seedu/address/logic/commands/contact/CMarkCommandTest.java b/src/test/java/seedu/address/logic/commands/contact/CMarkCommandTest.java new file mode 100644 index 00000000000..01a917e753a --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/contact/CMarkCommandTest.java @@ -0,0 +1,120 @@ +package seedu.address.logic.commands.contact; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalAddressBook.getTypicalAddressBook; +import static seedu.address.testutil.TypicalContacts.ALICE_MARKED; +import static seedu.address.testutil.TypicalContacts.BENSON; +import static seedu.address.testutil.TypicalContacts.CARL; +import static seedu.address.testutil.TypicalContacts.DANIEL; +import static seedu.address.testutil.TypicalContacts.ELLE; +import static seedu.address.testutil.TypicalContacts.FIONA; +import static seedu.address.testutil.TypicalContacts.GEORGE; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.CommandResult; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.contact.Contact; +import seedu.address.model.event.Event; +import seedu.address.testutil.ContactBuilder; +import seedu.address.testutil.TypicalEvents; + + +class CMarkCommandTest { + + private static final Contact ELLE_MARKED = new ContactBuilder(ELLE).withMarked(true).build(); + private Model expectedModel = new ModelManager(getAddressBookWith(getListWithMarkContact()), new UserPrefs()); + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + private List getListWithMarkContact() { + return new ArrayList<>(Arrays.asList(ELLE_MARKED, ALICE_MARKED, BENSON, CARL, DANIEL, FIONA, GEORGE)); + } + + public AddressBook getAddressBookWith(List contactList) { + AddressBook ab = new AddressBook(); + for (Contact contact : contactList) { + ab.addContact(contact); + } + for (Event event : TypicalEvents.getTypicalEvents()) { + ab.addEvent(event); + } + return ab; + } + + @Test + public void constructor_nullIndex_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new CMarkCommand(null)); + } + + @Test + public void execute_validIndexUnfilteredList_success() { + String expectedMessage = String.format(CMarkCommand.MESSAGE_SUCCESS, ELLE) + "\n"; + List indexes = List.of(Index.fromOneBased(5)); + CMarkCommand cMarkCommand = new CMarkCommand(indexes); + assertCommandSuccess(cMarkCommand, model, new CommandResult(expectedMessage), expectedModel); + } + + @Test + public void execute_invalidIndex_throwsCommandException() { + List outOfBoundIndex = List.of(Index.fromOneBased(model.getFilteredContactList().size() + 1)); + CMarkCommand cMarkCommand = new CMarkCommand(outOfBoundIndex); + assertCommandFailure(cMarkCommand, model, Messages.MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX); + + List veryLargeNumberWithDuplicateDigit = List.of(Index.fromOneBased(1111111111)); + CMarkCommand secondMarkCommand = new CMarkCommand(veryLargeNumberWithDuplicateDigit); + assertCommandFailure(secondMarkCommand, model, Messages.MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX); + } + + @Test + public void execute_contactAlreadyMarked() { + List indexes = List.of(Index.fromOneBased(1)); + model = expectedModel; + CMarkCommand cMarkCommand = new CMarkCommand(indexes); + String expectedMessage = String.format(CMarkCommand.MESSAGE_ALREADY_MARKED, ELLE) + "\n"; + assertCommandSuccess(cMarkCommand, model, expectedMessage, expectedModel); + } + + @Test + public void equals() { + Index first = Index.fromOneBased(1); + Index second = Index.fromOneBased(2); + + List firstIndexes = new ArrayList<>(); + firstIndexes.add(first); + List secondIndexes = new ArrayList<>(); + secondIndexes.add(second); + CMarkCommand markFirstCommand = new CMarkCommand(firstIndexes); + CMarkCommand markSecondCommand = new CMarkCommand(secondIndexes); + + // same object -> returns true + assertTrue(markFirstCommand.equals(markFirstCommand)); + + // same values -> returns true + CMarkCommand markFirstCommandCopy = new CMarkCommand(firstIndexes); + ; + assertTrue(markFirstCommand.equals(markFirstCommandCopy)); + + // different types -> returns false + assertFalse(markFirstCommand.equals(1)); + + // null -> returns false + assertFalse(markFirstCommand.equals(null)); + + // different Index -> returns false + assertFalse(markFirstCommand.equals(markSecondCommand)); + } + +} diff --git a/src/test/java/seedu/address/logic/commands/contact/CUnmarkCommandTest.java b/src/test/java/seedu/address/logic/commands/contact/CUnmarkCommandTest.java new file mode 100644 index 00000000000..e74d8bdc4af --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/contact/CUnmarkCommandTest.java @@ -0,0 +1,127 @@ +package seedu.address.logic.commands.contact; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalContacts.ALICE_MARKED; +import static seedu.address.testutil.TypicalContacts.BENSON; +import static seedu.address.testutil.TypicalContacts.CARL; +import static seedu.address.testutil.TypicalContacts.DANIEL; +import static seedu.address.testutil.TypicalContacts.ELLE; +import static seedu.address.testutil.TypicalContacts.FIONA; +import static seedu.address.testutil.TypicalContacts.GEORGE; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.CommandResult; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.contact.Contact; +import seedu.address.model.event.Event; +import seedu.address.testutil.ContactBuilder; +import seedu.address.testutil.TypicalEvents; + +class CUnmarkCommandTest { + + private static final Contact ELLE_MARKED = new ContactBuilder(ELLE).withMarked(true).build(); + private Model expectedModel; + private Model model; + + private List getListWithMarkContact() { + return new ArrayList<>(Arrays.asList(ELLE_MARKED, ALICE_MARKED, BENSON, CARL, DANIEL, FIONA, GEORGE)); + } + + private List getListAfterUnmark() { + // Alice is still marked + return new ArrayList<>(Arrays.asList(ALICE_MARKED, ELLE, BENSON, CARL, DANIEL, FIONA, GEORGE)); + } + + public AddressBook getAddressBookWith(List contactList) { + AddressBook ab = new AddressBook(); + for (Contact contact : contactList) { + ab.addContact(contact); + } + for (Event event : TypicalEvents.getTypicalEvents()) { + ab.addEvent(event); + } + return ab; + } + + @BeforeEach + private void setUp() { + expectedModel = new ModelManager(getAddressBookWith(getListAfterUnmark()), new UserPrefs()); + model = new ModelManager(getAddressBookWith(getListWithMarkContact()), new UserPrefs()); + } + + @Test + public void constructor_nullIndex_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new CUnmarkCommand(null)); + } + + @Test + public void execute_validIndexUnfilteredList_success() { + String expectedMessage = String.format(CUnmarkCommand.MESSAGE_SUCCESS, ELLE_MARKED) + "\n"; + List indexes = List.of(Index.fromOneBased(1)); + CUnmarkCommand cunmarkCommand = new CUnmarkCommand(indexes); + assertCommandSuccess(cunmarkCommand, model, new CommandResult(expectedMessage), expectedModel); + } + + @Test + public void execute_invalidIndex_throwsCommandException() { + List outOfBoundIndex = List.of(Index.fromOneBased(model.getFilteredContactList().size() + 1)); + CUnmarkCommand cunmarkCommand = new CUnmarkCommand(outOfBoundIndex); + + assertCommandFailure(cunmarkCommand, model, Messages.MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX); + } + + @Test + public void execute_contactNotMarked() { + model = new ModelManager(getAddressBookWith(getListAfterUnmark()), new UserPrefs()); + List indexes = List.of(Index.fromOneBased(2)); + CUnmarkCommand cunmarkCommand = new CUnmarkCommand(indexes); + String expectedMessage = String.format(CUnmarkCommand.MESSAGE_NOT_MARKED, ELLE) + "\n"; + assertCommandSuccess(cunmarkCommand, model, expectedMessage, expectedModel); + } + + @Test + public void equals() { + Index first = Index.fromOneBased(1); + Index second = Index.fromOneBased(2); + + List firstIndexes = new ArrayList<>(); + firstIndexes.add(first); + List secondIndexes = new ArrayList<>(); + secondIndexes.add(second); + CUnmarkCommand unmarkFirstCommand = new CUnmarkCommand(firstIndexes); + CUnmarkCommand unmarkSecondCommand = new CUnmarkCommand(secondIndexes); + + // same object -> returns true + assertTrue(unmarkFirstCommand.equals(unmarkFirstCommand)); + + // same values -> returns true + CUnmarkCommand unmarkFirstCommandCopy = new CUnmarkCommand(firstIndexes); + ; + assertTrue(unmarkFirstCommand.equals(unmarkFirstCommandCopy)); + + // different types -> returns false + assertFalse(unmarkFirstCommand.equals(1)); + + // null -> returns false + assertFalse(unmarkFirstCommand.equals(null)); + + // different Index -> returns false + assertFalse(unmarkFirstCommand.equals(unmarkSecondCommand)); + } + +} diff --git a/src/test/java/seedu/address/logic/commands/contact/CViewCommandTest.java b/src/test/java/seedu/address/logic/commands/contact/CViewCommandTest.java new file mode 100644 index 00000000000..a93f6ac35bb --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/contact/CViewCommandTest.java @@ -0,0 +1,97 @@ +package seedu.address.logic.commands.contact; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.general.CommandTestUtil.showContactAtIndex; +import static seedu.address.testutil.TypicalAddressBook.getTypicalAddressBook; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.ContactDisplaySetting; + + +public class CViewCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_validIndexUnfilteredList_success() { + Contact contactToView = model.getFilteredContactList().get(INDEX_FIRST.getZeroBased()); + CViewCommand cViewCommand = new CViewCommand(INDEX_FIRST); + + String expectedMessage = String.format(CViewCommand.MESSAGE_SUCCESS, contactToView); + + expectedModel.updateContactListByIndex(INDEX_FIRST); + expectedModel.setContactDisplaySetting(new ContactDisplaySetting(true)); + + assertCommandSuccess(cViewCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndexUnfilteredList_throwsCommandException() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredContactList().size() + 1); + CViewCommand cViewCommand = new CViewCommand(outOfBoundIndex); + + assertCommandFailure(cViewCommand, model, Messages.MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX); + } + + @Test + public void execute_validIndexFilteredList_success() { + showContactAtIndex(model, INDEX_FIRST); + + Contact contactToView = model.getFilteredContactList().get(INDEX_FIRST.getZeroBased()); + CViewCommand cViewCommand = new CViewCommand(INDEX_FIRST); + + String expectedMessage = String.format(CViewCommand.MESSAGE_SUCCESS, contactToView); + + expectedModel.updateContactListByIndex(INDEX_FIRST); + expectedModel.setContactDisplaySetting(new ContactDisplaySetting(true)); + + assertCommandSuccess(cViewCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndexFilteredList_throwsCommandException() { + showContactAtIndex(model, INDEX_FIRST); + Index outOfBoundIndex = INDEX_SECOND; + // ensures that outOfBoundIndex is still in bounds of address book list + assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getContactList().size()); + + CViewCommand cViewCommand = new CViewCommand(outOfBoundIndex); + + assertCommandFailure(cViewCommand, model, Messages.MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX); + } + + @Test + public void equals() { + CViewCommand viewFirstCommand = new CViewCommand(INDEX_FIRST); + CViewCommand viewSecondCommand = new CViewCommand(INDEX_SECOND); + + // same object -> returns true + assertTrue(viewFirstCommand.equals(viewFirstCommand)); + + // same values -> returns true + CViewCommand viewFirstCommandCopy = new CViewCommand(INDEX_FIRST); + assertTrue(viewFirstCommand.equals(viewFirstCommandCopy)); + + // different types -> returns false + assertFalse(viewFirstCommand.equals(1)); + + // null -> returns false + assertFalse(viewFirstCommand.equals(null)); + + // different contact -> returns false + assertFalse(viewFirstCommand.equals(viewSecondCommand)); + } +} diff --git a/src/test/java/seedu/address/logic/commands/contact/EditContactDescriptorTest.java b/src/test/java/seedu/address/logic/commands/contact/EditContactDescriptorTest.java new file mode 100644 index 00000000000..6858fb44ce6 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/contact/EditContactDescriptorTest.java @@ -0,0 +1,58 @@ +package seedu.address.logic.commands.contact; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.general.CommandTestUtil.DESC_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.DESC_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_ADDRESS_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_EMAIL_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_NAME_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_PHONE_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_TAG_HUSBAND; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.contact.CEditCommand.EditContactDescriptor; +import seedu.address.testutil.EditContactDescriptorBuilder; + +public class EditContactDescriptorTest { + + @Test + public void equals() { + // same values -> returns true + EditContactDescriptor descriptorWithSameValues = new EditContactDescriptor(DESC_AMY); + assertTrue(DESC_AMY.equals(descriptorWithSameValues)); + + // same object -> returns true + assertTrue(DESC_AMY.equals(DESC_AMY)); + + // null -> returns false + assertFalse(DESC_AMY.equals(null)); + + // different types -> returns false + assertFalse(DESC_AMY.equals(5)); + + // different values -> returns false + assertFalse(DESC_AMY.equals(DESC_BOB)); + + // different name -> returns false + EditContactDescriptor editedAmy = new EditContactDescriptorBuilder(DESC_AMY).withName(VALID_NAME_BOB).build(); + assertFalse(DESC_AMY.equals(editedAmy)); + + // different phone -> returns false + editedAmy = new EditContactDescriptorBuilder(DESC_AMY).withPhone(VALID_PHONE_BOB).build(); + assertFalse(DESC_AMY.equals(editedAmy)); + + // different email -> returns false + editedAmy = new EditContactDescriptorBuilder(DESC_AMY).withEmail(VALID_EMAIL_BOB).build(); + assertFalse(DESC_AMY.equals(editedAmy)); + + // different address -> returns false + editedAmy = new EditContactDescriptorBuilder(DESC_AMY).withAddress(VALID_ADDRESS_BOB).build(); + assertFalse(DESC_AMY.equals(editedAmy)); + + // different tags -> returns false + editedAmy = new EditContactDescriptorBuilder(DESC_AMY).withTags(VALID_TAG_HUSBAND).build(); + assertFalse(DESC_AMY.equals(editedAmy)); + } +} diff --git a/src/test/java/seedu/address/logic/commands/event/EAddCommandIntegrationTest.java b/src/test/java/seedu/address/logic/commands/event/EAddCommandIntegrationTest.java new file mode 100644 index 00000000000..ce9953bed73 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/event/EAddCommandIntegrationTest.java @@ -0,0 +1,54 @@ +package seedu.address.logic.commands.event; + +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalAddressBook.getTypicalAddressBook; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.CommandResult; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventChanger; +import seedu.address.testutil.EventBuilder; + +/** + * Contains integration tests (interaction with the Model) for {@code EAddCommand}. + */ +public class EAddCommandIntegrationTest { + + private Model model; + + private Model expectedModel; + + @BeforeEach + public void setUp() { + model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + } + + @Test + public void execute_newEvent_success() { + Event validEvent = new EventBuilder().build(); + + expectedModel.addEvent(validEvent); + + EventChanger eventChanger = EventChanger.addEventChanger(validEvent); + + assertCommandSuccess(new EAddCommand(validEvent), model, + new CommandResult(String.format(EAddCommand.MESSAGE_SUCCESS, validEvent), List.of(eventChanger)), + expectedModel); + } + + @Test + public void execute_duplicateEvent_throwsCommandException() { + Event eventInList = model.getAddressBook().getEventList().get(0); + assertCommandFailure(new EAddCommand(eventInList), model, EAddCommand.MESSAGE_DUPLICATE_EVENT); + } + +} diff --git a/src/test/java/seedu/address/logic/commands/event/EAddCommandTest.java b/src/test/java/seedu/address/logic/commands/event/EAddCommandTest.java new file mode 100644 index 00000000000..4bf4c609591 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/event/EAddCommandTest.java @@ -0,0 +1,130 @@ +package seedu.address.logic.commands.event; + +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import java.util.ArrayList; +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.ModelStub; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.AddressBook; +import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.event.Event; +import seedu.address.testutil.EventBuilder; + +class EAddCommandTest { + + @Test + public void constructor_nullEvent_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new EAddCommand(null)); + } + + @Test + public void execute_eventAcceptedByModel_addSuccessful() throws Exception { + ModelStubAcceptingEventAdded modelStub = new ModelStubAcceptingEventAdded(); + Event validEvent = new EventBuilder().build(); + + CommandResult commandResult = new EAddCommand(validEvent).execute(modelStub); + + assertEquals(String.format(EAddCommand.MESSAGE_SUCCESS, validEvent), commandResult.getFeedbackToUser()); + assertEquals(Arrays.asList(validEvent), modelStub.eventsAdded); + } + + @Test + public void execute_duplicateEvent_throwsCommandException() { + Event validEvent = new EventBuilder().build(); + EAddCommand addCommand = new EAddCommand(validEvent); + ModelStub modelStub = new ModelStubWithEvent(validEvent); + + assertThrows(CommandException.class, EAddCommand.MESSAGE_DUPLICATE_EVENT, () -> + addCommand.execute(modelStub)); + } + + @Test + public void execute_invalidDateTimeRange_throwsCommandException() { + Event event = new EventBuilder().withName("Outing").withStartDateAndTime("20-10-2021 20:00") + .withEndDateAndTime("20-10-2021 18:00").build(); + ModelStub modelStub = new ModelStubAcceptingEventAdded(); + + EAddCommand command = new EAddCommand(event); + + assertThrows(CommandException.class, EAddCommand.MESSAGE_INVALID_DATE_TIME_RANGE, () -> + command.execute(modelStub)); + } + + @Test + public void equals() { + Event lecture = new EventBuilder().withName("Lecture").build(); + Event exam = new EventBuilder().withName("Exam").build(); + EAddCommand addLectureCommand = new EAddCommand(lecture); + EAddCommand addExamCommand = new EAddCommand(exam); + + // same object -> returns true + assertTrue(addLectureCommand.equals(addLectureCommand)); + + // same values -> returns true + EAddCommand addLectureCommandCopy = new EAddCommand(lecture); + assertTrue(addLectureCommand.equals(addLectureCommandCopy)); + + // different types -> returns false + assertFalse(addLectureCommand.equals(1)); + + // null -> returns false + assertFalse(addLectureCommand.equals(null)); + + // different event -> returns false + assertFalse(addLectureCommand.equals(addExamCommand)); + } + + /** + * A Model stub that contains a single event. + */ + private class ModelStubWithEvent extends ModelStub { + private final Event event; + + ModelStubWithEvent(Event event) { + requireNonNull(event); + this.event = event; + } + + @Override + public boolean hasEvent(Event event) { + requireNonNull(event); + return this.event.isSameEvent(event); + } + } + + /** + * A Model stub that always accept the event being added. + */ + private class ModelStubAcceptingEventAdded extends ModelStub { + final ArrayList eventsAdded = new ArrayList<>(); + + @Override + public boolean hasEvent(Event event) { + requireNonNull(event); + return eventsAdded.stream().anyMatch(event::isSameEvent); + } + + @Override + public void addEvent(Event event) { + requireNonNull(event); + eventsAdded.add(event); + } + + @Override + public ReadOnlyAddressBook getAddressBook() { + return new AddressBook(); + } + + @Override + public void commitHistory() {} + } +} diff --git a/src/test/java/seedu/address/logic/commands/event/EClearCommandTest.java b/src/test/java/seedu/address/logic/commands/event/EClearCommandTest.java new file mode 100644 index 00000000000..a72a407745a --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/event/EClearCommandTest.java @@ -0,0 +1,39 @@ +package seedu.address.logic.commands.event; + +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalAddressBook.getTypicalAddressBook; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.CommandResult; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.event.EventChanger; +import seedu.address.testutil.TypicalContacts; + +public class EClearCommandTest { + private final CommandResult commandResult = new CommandResult( + EClearCommand.MESSAGE_SUCCESS, + List.of(EventChanger.clearEventChanger())); + + @Test + public void execute_emptyAddressBook_success() { + Model model = new ModelManager(); + Model expectedModel = new ModelManager(); + + assertCommandSuccess(new EClearCommand(), model, commandResult, expectedModel); + } + + @Test + public void execute_nonEmptyAddressBook_success() { + Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + expectedModel.setAddressBook(TypicalContacts.getTypicalAddressBook()); + + assertCommandSuccess(new EClearCommand(), model, commandResult, expectedModel); + } + +} diff --git a/src/test/java/seedu/address/logic/commands/event/EDeleteCommandIntegrationTest.java b/src/test/java/seedu/address/logic/commands/event/EDeleteCommandIntegrationTest.java new file mode 100644 index 00000000000..fc3caeb3d54 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/event/EDeleteCommandIntegrationTest.java @@ -0,0 +1,67 @@ +package seedu.address.logic.commands.event; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalAddressBook.getTypicalAddressBook; +import static seedu.address.testutil.TypicalContacts.AMY; +import static seedu.address.testutil.TypicalContacts.BOB; +import static seedu.address.testutil.TypicalRanges.RANGE_FIRST_TO_FIRST; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.contact.Contact; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventChanger; + +/** + * Contains integration tests (interaction with the Model and checks if unlinking is done properly) + * for {@code EDeleteCommand}. + */ +public class EDeleteCommandIntegrationTest { + private Model model; + + @BeforeEach + public void setUp() { + model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + } + + @Test + public void execute_deleteEvent_success() { + Event eventToDelete = model.getFilteredEventList().get(0); + Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + expectedModel.deleteEvent(eventToDelete); + EventChanger eventChanger = EventChanger.deleteEventChanger(eventToDelete); + CommandResult expectedMessage = new CommandResult( + String.format(EDeleteCommand.MESSAGE_DELETE_EVENT_SUCCESS, eventToDelete), List.of(eventChanger)); + assertCommandSuccess(new EDeleteCommand(RANGE_FIRST_TO_FIRST), model, + expectedMessage, expectedModel); + } + + @Test + public void execute_deleteEventWithLinks_success() throws CommandException { + // link event to two contacts before deleting + Contact contact1ToLink = AMY; + Contact contact2ToLink = BOB; + model.addContact(contact1ToLink); + model.addContact(contact2ToLink); + // due to event immutability, have to get new event from the event list + model.linkEventAndContact(model.getFilteredEventList().get(0), contact1ToLink); + model.linkEventAndContact(model.getFilteredEventList().get(0), contact2ToLink); + Event eventToDelete = model.getFilteredEventList().get(0); + assertTrue(eventToDelete.hasLinkTo(contact1ToLink)); + assertTrue(eventToDelete.hasLinkTo(contact2ToLink)); + EDeleteCommand cDeleteCommand = new EDeleteCommand(RANGE_FIRST_TO_FIRST); + cDeleteCommand.execute(model); + assertFalse(contact1ToLink.hasLinkTo(eventToDelete)); + assertFalse(contact2ToLink.hasLinkTo(eventToDelete)); + } +} diff --git a/src/test/java/seedu/address/logic/commands/event/EDeleteCommandTest.java b/src/test/java/seedu/address/logic/commands/event/EDeleteCommandTest.java new file mode 100644 index 00000000000..1e629a55e17 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/event/EDeleteCommandTest.java @@ -0,0 +1,147 @@ +package seedu.address.logic.commands.event; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.general.CommandTestUtil.showEventAtIndex; +import static seedu.address.testutil.TypicalAddressBook.getTypicalAddressBook; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND; +import static seedu.address.testutil.TypicalIndexes.INDEX_THIRD; +import static seedu.address.testutil.TypicalRanges.RANGE_FIRST_TO_FIRST; +import static seedu.address.testutil.TypicalRanges.RANGE_FIRST_TO_THIRD; +import static seedu.address.testutil.TypicalRanges.RANGE_SECOND_TO_THIRD; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.core.range.Range; +import seedu.address.logic.commands.CommandResult; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventChanger; + +/** + * Contains integration tests (interaction with the Model) and unit tests for + * {@code EDeleteCommand}. + */ +public class EDeleteCommandTest { + + private final Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private final Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + + @Test + public void execute_validIndexUnfilteredList_success() { + Event eventToDelete = model.getFilteredEventList().get(INDEX_FIRST.getZeroBased()); + EDeleteCommand eDeleteCommand = new EDeleteCommand(RANGE_FIRST_TO_FIRST); + + String expectedMessage = String.format(EDeleteCommand.MESSAGE_DELETE_EVENT_SUCCESS, eventToDelete); + EventChanger eventChanger = EventChanger.deleteEventChanger(eventToDelete); + expectedModel.deleteEvent(eventToDelete); + + assertCommandSuccess(eDeleteCommand, model, new CommandResult(expectedMessage, List.of(eventChanger)), + expectedModel); + } + + @Test + public void execute_invalidIndexUnfilteredList_throwsCommandException() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredEventList().size() + 1); + Range rangeOfIndexes = Range.convertFromIndex(outOfBoundIndex); + EDeleteCommand eDeleteCommand = new EDeleteCommand(rangeOfIndexes); + + assertCommandFailure(eDeleteCommand, model, Messages.MESSAGE_INVALID_EVENT_DISPLAYED_INDEX); + } + + @Test + public void execute_validIndexFilteredList_success() { + showEventAtIndex(model, INDEX_FIRST); + + Event eventToDelete = model.getFilteredEventList().get(INDEX_FIRST.getZeroBased()); + EDeleteCommand eDeleteCommand = new EDeleteCommand(RANGE_FIRST_TO_FIRST); + + String expectedMessage = String.format(EDeleteCommand.MESSAGE_DELETE_EVENT_SUCCESS, eventToDelete); + EventChanger eventChanger = EventChanger.deleteEventChanger(eventToDelete); + expectedModel.deleteEvent(eventToDelete); + showNoEvent(expectedModel); + + assertCommandSuccess(eDeleteCommand, model, new CommandResult(expectedMessage, List.of(eventChanger)), + expectedModel); + } + + @Test + public void execute_invalidIndexFilteredList_throwsCommandException() { + showEventAtIndex(model, INDEX_FIRST); + + Index outOfBoundIndex = INDEX_SECOND; + // ensures that outOfBoundIndex is still in bounds of address book list + assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getEventList().size()); + Range rangeOfIndexes = Range.convertFromIndex(outOfBoundIndex); + + EDeleteCommand eDeleteCommand = new EDeleteCommand(rangeOfIndexes); + + assertCommandFailure(eDeleteCommand, model, Messages.MESSAGE_INVALID_EVENT_DISPLAYED_INDEX); + } + + @Test + public void execute_validRangeUnfilteredList_success() { + Event firstEventToDelete = model.getFilteredEventList().get(INDEX_FIRST.getZeroBased()); + Event secondEventToDelete = model.getFilteredEventList().get(INDEX_SECOND.getZeroBased()); + Event thirdEventToDelete = model.getFilteredEventList().get(INDEX_THIRD.getZeroBased()); + EDeleteCommand eDeleteCommand = new EDeleteCommand(RANGE_FIRST_TO_THIRD); + + String expectedMessage = String.format(EDeleteCommand.MESSAGE_DELETE_EVENT_SUCCESS, firstEventToDelete) + + "\n" + + String.format(EDeleteCommand.MESSAGE_DELETE_EVENT_SUCCESS, secondEventToDelete) + + "\n" + + String.format(EDeleteCommand.MESSAGE_DELETE_EVENT_SUCCESS, thirdEventToDelete); + + EventChanger eventChangerOne = EventChanger.deleteEventChanger(firstEventToDelete); + EventChanger eventChangerTwo = EventChanger.deleteEventChanger(secondEventToDelete); + EventChanger eventChangerThree = EventChanger.deleteEventChanger(thirdEventToDelete); + expectedModel.deleteEvent(firstEventToDelete); + expectedModel.deleteEvent(secondEventToDelete); + expectedModel.deleteEvent(thirdEventToDelete); + + assertCommandSuccess(eDeleteCommand, model, + new CommandResult(expectedMessage, List.of(eventChangerOne, eventChangerTwo, eventChangerThree)), + expectedModel); + } + + @Test + public void equals() { + EDeleteCommand deleteFirstCommand = new EDeleteCommand(RANGE_FIRST_TO_FIRST); + EDeleteCommand deleteSecondToThirdCommand = new EDeleteCommand(RANGE_SECOND_TO_THIRD); + + // same object -> returns true + assertTrue(deleteFirstCommand.equals(deleteFirstCommand)); + + // same values -> returns true + EDeleteCommand deleteFirstCommandCopy = new EDeleteCommand(RANGE_FIRST_TO_FIRST); + assertTrue(deleteFirstCommand.equals(deleteFirstCommandCopy)); + + // different types -> returns false + assertFalse(deleteFirstCommand.equals(1)); + + // null -> returns false + assertFalse(deleteFirstCommand.equals(null)); + + // different event -> returns false + assertFalse(deleteFirstCommand.equals(deleteSecondToThirdCommand)); + } + + /** + * Updates {@code model}'s filtered list to show no one. + */ + private void showNoEvent(Model model) { + model.updateFilteredEventList(p -> false); + + assertTrue(model.getFilteredEventList().isEmpty()); + } +} + diff --git a/src/test/java/seedu/address/logic/commands/event/EEditCommandTest.java b/src/test/java/seedu/address/logic/commands/event/EEditCommandTest.java new file mode 100644 index 00000000000..d1764cedffd --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/event/EEditCommandTest.java @@ -0,0 +1,236 @@ +package seedu.address.logic.commands.event; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.general.CommandTestUtil.DESC_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.DESC_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_NAME_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_NAME_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_START_DATE_TIME_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_TAG_COOL; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.general.CommandTestUtil.showEventAtIndex; +import static seedu.address.testutil.TypicalAddressBook.getTypicalAddressBook; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST; +import static seedu.address.testutil.TypicalIndexes.INDEX_FOURTH; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND; + +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.event.EEditCommand.EditEventDescriptor; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventChanger; +import seedu.address.model.tag.Tag; +import seedu.address.testutil.EditEventDescriptorBuilder; +import seedu.address.testutil.EventBuilder; + +/** + * Contains integration tests (interaction with the Model) and unit tests for EEditCommand. + */ +class EEditCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + private Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + + @Test + public void execute_allFieldsSpecifiedUnfilteredList_success() { + Event editedEvent = new EventBuilder().build(); + EditEventDescriptor descriptor = new EditEventDescriptorBuilder(editedEvent, + null, true).build(); + EEditCommand eEditCommand = new EEditCommand(INDEX_FIRST, descriptor); + + String expectedMessage = String.format(EEditCommand.MESSAGE_EDIT_EVENT_SUCCESS, editedEvent); + + EventChanger eventChanger = EventChanger.editEventChanger(model.getFilteredEventList().get(0), editedEvent); + expectedModel.setEvent(model.getFilteredEventList().get(0), editedEvent); + + assertCommandSuccess(eEditCommand, model, new CommandResult(expectedMessage, List.of(eventChanger)), + expectedModel); + } + + @Test + public void execute_someFieldsSpecifiedUnfilteredList_success() { + Index indexLastEvent = Index.fromOneBased(model.getFilteredEventList().size()); + Event lastEvent = model.getFilteredEventList().get(indexLastEvent.getZeroBased()); + + EventBuilder eventInList = new EventBuilder(lastEvent); + Event editedEvent = eventInList.withName(VALID_NAME_TUTORIAL) + .withStartDateAndTime(VALID_START_DATE_TIME_TUTORIAL).withTags(VALID_TAG_COOL).build(); + + EditEventDescriptor descriptor = new EditEventDescriptorBuilder().withName(VALID_NAME_TUTORIAL) + .withStartDateTime(VALID_START_DATE_TIME_TUTORIAL).withDeleteAllTags(true) + .withTags(VALID_TAG_COOL).build(); + EEditCommand eEditCommand = new EEditCommand(indexLastEvent, descriptor); + + String expectedMessage = String.format(EEditCommand.MESSAGE_EDIT_EVENT_SUCCESS, editedEvent); + + expectedModel.setEvent(lastEvent, editedEvent); + + EventChanger eventChanger = EventChanger.editEventChanger(lastEvent, editedEvent); + + assertCommandSuccess(eEditCommand, model, new CommandResult(expectedMessage, List.of(eventChanger)), + expectedModel); + } + + @Test + public void execute_noFieldSpecifiedUnfilteredList_success() { + EEditCommand eEditCommand = new EEditCommand(INDEX_FIRST, new EditEventDescriptor()); + Event editedEvent = model.getFilteredEventList().get(INDEX_FIRST.getZeroBased()); + + String expectedMessage = String.format(EEditCommand.MESSAGE_EDIT_EVENT_SUCCESS, editedEvent); + + EventChanger eventChanger = EventChanger.editEventChanger(editedEvent, editedEvent); + + assertCommandSuccess(eEditCommand, model, new CommandResult(expectedMessage, List.of(eventChanger)), + expectedModel); + } + + @Test + public void execute_filteredList_success() { + showEventAtIndex(model, INDEX_FIRST); + + Event eventInFilteredList = model.getFilteredEventList().get(INDEX_FIRST.getZeroBased()); + Event editedEvent = new EventBuilder(eventInFilteredList).withName(VALID_NAME_EXAM).build(); + EEditCommand eEditCommand = new EEditCommand( + INDEX_FIRST, + new EditEventDescriptorBuilder().withName(VALID_NAME_EXAM).build()); + + String expectedMessage = String.format(EEditCommand.MESSAGE_EDIT_EVENT_SUCCESS, editedEvent); + + expectedModel.setEvent(model.getFilteredEventList().get(0), editedEvent); + + EventChanger eventChanger = EventChanger.editEventChanger(eventInFilteredList, editedEvent); + + assertCommandSuccess(eEditCommand, model, new CommandResult(expectedMessage, List.of(eventChanger)), + expectedModel); + } + + @Test + public void execute_duplicateEventUnfilteredList_failure() { + Event firstEvent = model.getFilteredEventList().get(INDEX_FIRST.getZeroBased()); + EditEventDescriptor descriptor = new EditEventDescriptorBuilder(firstEvent, Set.of(), + false).build(); + EEditCommand eEditCommand = new EEditCommand(INDEX_SECOND, descriptor); + + assertCommandFailure(eEditCommand, model, EEditCommand.MESSAGE_DUPLICATE_EVENT); + } + + @Test + public void execute_duplicateEventFilteredList_failure() { + showEventAtIndex(model, INDEX_FIRST); + + // edit event in filtered list into a duplicate in address book + Event eventInList = model.getAddressBook().getEventList().get(INDEX_SECOND.getZeroBased()); + EEditCommand eEditCommand = new EEditCommand( + INDEX_FIRST, + new EditEventDescriptorBuilder(eventInList, Set.of(), false).build()); + + assertCommandFailure(eEditCommand, model, EEditCommand.MESSAGE_DUPLICATE_EVENT); + } + + @Test + public void execute_invalidEventIndexUnfilteredList_failure() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredEventList().size() + 1); + EditEventDescriptor descriptor = new EditEventDescriptorBuilder().withName(VALID_NAME_EXAM) + .build(); + EEditCommand eEditCommand = new EEditCommand(outOfBoundIndex, descriptor); + + assertCommandFailure(eEditCommand, model, Messages.MESSAGE_INVALID_EVENT_DISPLAYED_INDEX); + } + + /** + * Edit filtered list where index is larger than size of filtered list, + * but smaller than size of address book + */ + @Test + public void execute_invalidEventIndexFilteredList_failure() { + showEventAtIndex(model, INDEX_FIRST); + Index outOfBoundIndex = INDEX_SECOND; + // ensures that outOfBoundIndex is still in bounds of address book list + assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getEventList().size()); + + EEditCommand eEditCommand = new EEditCommand( + outOfBoundIndex, + new EditEventDescriptorBuilder().withName(VALID_NAME_EXAM).build()); + + assertCommandFailure(eEditCommand, model, Messages.MESSAGE_INVALID_EVENT_DISPLAYED_INDEX); + } + + @Test + public void execute_invalidDateTimeRangeEvent_failure() { + EEditCommand eEditCommand = new EEditCommand(INDEX_FIRST, new EditEventDescriptorBuilder() + .withStartDateTime("20-10-2021 20:00").withEndDateTime("20-10-2021 18:00").build()); + assertCommandFailure(eEditCommand, model, EEditCommand.MESSAGE_INVALID_DATE_TIME_RANGE); + } + + @Test + public void execute_tagToDeleteNotInOriginalEvent_success() { + Tag toDelete = new Tag("notInOriginal"); + Event editedEvent = new EventBuilder().withMarked(false).build(); + EEditCommand.EditEventDescriptor descriptor = new EditEventDescriptorBuilder(editedEvent, + Set.of(toDelete), false).build(); + // the index must not have any tags initially (check TypicalEvents) + EEditCommand eEditCommand = new EEditCommand(INDEX_FOURTH, descriptor); + String expectedMessage = String.format(EEditCommand.MESSAGE_EDIT_EVENT_SUCCESS, editedEvent) + + "\nNote:\n" + String.format(EEditCommand.MESSAGE_TAG_TO_DELETE_NOT_IN_ORIGINAL, toDelete); + EventChanger eventChanger = EventChanger.editEventChanger(model.getFilteredEventList().get(3), editedEvent); + Model expectedModel = new ModelManager(new AddressBook(getTypicalAddressBook()), new UserPrefs()); + expectedModel.setEvent(model.getFilteredEventList().get(3), editedEvent); + assertCommandSuccess(eEditCommand, model, new CommandResult(expectedMessage, List.of(eventChanger)), + expectedModel); + } + + @Test + public void execute_tagToAddAlreadyInOriginalEvent_success() { + Tag toAdd = new Tag("exams"); + Event editedEvent = new EventBuilder().withTags("exams").build(); + EEditCommand.EditEventDescriptor descriptor = new EditEventDescriptorBuilder(editedEvent, + null, false).build(); + // the index must not have any tags initially (check TypicalEvents) + EEditCommand eEditCommand = new EEditCommand(INDEX_FIRST, descriptor); + String expectedMessage = String.format(EEditCommand.MESSAGE_EDIT_EVENT_SUCCESS, editedEvent) + + "\nNote:\n" + String.format(EEditCommand.MESSAGE_TAG_TO_ADD_ALREADY_IN_ORIGINAL, toAdd); + EventChanger eventChanger = EventChanger.editEventChanger(model.getFilteredEventList().get(0), editedEvent); + Model expectedModel = new ModelManager(new AddressBook(getTypicalAddressBook()), new UserPrefs()); + expectedModel.setEvent(model.getFilteredEventList().get(0), editedEvent); + assertCommandSuccess(eEditCommand, model, new CommandResult(expectedMessage, List.of(eventChanger)), + expectedModel); + } + + @Test + public void equals() { + final EEditCommand standardCommand = new EEditCommand(INDEX_FIRST, DESC_TUTORIAL); + + // same values -> returns true + EditEventDescriptor copyDescriptor = new EditEventDescriptor(DESC_TUTORIAL); + EEditCommand commandWithSameValues = new EEditCommand(INDEX_FIRST, copyDescriptor); + assertTrue(standardCommand.equals(commandWithSameValues)); + + // same object -> returns true + assertTrue(standardCommand.equals(standardCommand)); + + // null -> returns false + assertFalse(standardCommand.equals(null)); + + // different types -> returns false + assertFalse(standardCommand.equals(new EClearCommand())); + + // different index -> returns false + assertFalse(standardCommand.equals(new EEditCommand(INDEX_SECOND, DESC_TUTORIAL))); + + // different descriptor -> returns false + assertFalse(standardCommand.equals(new EEditCommand(INDEX_FIRST, DESC_EXAM))); + } +} diff --git a/src/test/java/seedu/address/logic/commands/event/EFindCommandTest.java b/src/test/java/seedu/address/logic/commands/event/EFindCommandTest.java new file mode 100644 index 00000000000..16977f5236b --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/event/EFindCommandTest.java @@ -0,0 +1,86 @@ +package seedu.address.logic.commands.event; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.commons.core.Messages.MESSAGE_EVENTS_LISTED_OVERVIEW; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalAddressBook.getTypicalAddressBook; +import static seedu.address.testutil.TypicalEvents.CS2101_MEETING; +import static seedu.address.testutil.TypicalEvents.CS2103_MIDTERM_MARKED; +import static seedu.address.testutil.TypicalEvents.FOOTBALL_PRACTICE; +import static seedu.address.testutil.TypicalEvents.TEAM_MEETING; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.event.EventContainsKeywordsPredicate; + +/** + * Contains integration tests (interaction with the Model) for {@code EFindCommand}. + */ +public class EFindCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + + @Test + public void equals() { + EventContainsKeywordsPredicate firstPredicate = + new EventContainsKeywordsPredicate(Collections.singletonList("first")); + EventContainsKeywordsPredicate secondPredicate = + new EventContainsKeywordsPredicate(Collections.singletonList("second")); + + EFindCommand findFirstCommand = new EFindCommand(firstPredicate); + EFindCommand findSecondCommand = new EFindCommand(secondPredicate); + + // same object -> returns true + assertTrue(findFirstCommand.equals(findFirstCommand)); + + // same values -> returns true + EFindCommand findFirstCommandCopy = new EFindCommand(firstPredicate); + assertTrue(findFirstCommand.equals(findFirstCommandCopy)); + + // different types -> returns false + assertFalse(findFirstCommand.equals(1)); + + // null -> returns false + assertFalse(findFirstCommand.equals(null)); + + // different event -> returns false + assertFalse(findFirstCommand.equals(findSecondCommand)); + } + + @Test + public void execute_zeroKeywords_noEventsFound() { + String expectedMessage = String.format(MESSAGE_EVENTS_LISTED_OVERVIEW, 0); + EventContainsKeywordsPredicate predicate = preparePredicate(" "); + EFindCommand command = new EFindCommand(predicate); + expectedModel.updateFilteredEventList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Collections.emptyList(), model.getFilteredEventList()); + } + + @Test + public void execute_multipleKeywords_multipleEventsFound() { + String expectedMessage = String.format(MESSAGE_EVENTS_LISTED_OVERVIEW, 4); + EventContainsKeywordsPredicate predicate = preparePredicate("2103 foot MEeT"); + EFindCommand command = new EFindCommand(predicate); + expectedModel.updateFilteredEventList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(CS2103_MIDTERM_MARKED, CS2101_MEETING, FOOTBALL_PRACTICE, TEAM_MEETING), + model.getFilteredEventList()); + } + + /** + * Parses {@code userInput} into a {@code EventContainsKeywordsPredicate}. + */ + private EventContainsKeywordsPredicate preparePredicate(String userInput) { + return new EventContainsKeywordsPredicate(Arrays.asList(userInput.split("\\s+"))); + } +} + diff --git a/src/test/java/seedu/address/logic/commands/event/ELinkCommandTest.java b/src/test/java/seedu/address/logic/commands/event/ELinkCommandTest.java new file mode 100644 index 00000000000..4227d385092 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/event/ELinkCommandTest.java @@ -0,0 +1,142 @@ +package seedu.address.logic.commands.event; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.contact.Contact; +import seedu.address.model.event.Event; +import seedu.address.testutil.TypicalAddressBook; + +class ELinkCommandTest { + private final AddressBook typicalAddressBook = TypicalAddressBook.getTypicalAddressBook(); + private final Model typicalModel = new ModelManager(typicalAddressBook, new UserPrefs()); + + private String generateSuccessfulLink(Event eventToLink, Set set) { + assert !set.isEmpty(); + StringBuilder result = new StringBuilder(); + for (Contact contact : set) { + String resultForEachLink = String.format(ELinkCommand.MESSAGE_SUCCESS, + eventToLink.getName(), contact.getName()); + result.append(resultForEachLink); + } + return result.toString(); + } + + private String generateAlreadyLinked(Event eventToLink, Set set) { + assert !set.isEmpty(); + StringBuilder result = new StringBuilder(); + for (Contact contact : set) { + String resultForEachLink = String.format(ELinkCommand.MESSAGE_ALREADY_LINKED, + eventToLink.getName(), contact.getName()); + result.append(resultForEachLink); + } + return result.toString(); + } + + @Test + public void execute_singleLink_success() { + // first index of event list and first index of contact list + ELinkCommand eLinkCommand = new ELinkCommand( + INDEX_FIRST, + Set.of(INDEX_FIRST)); + Event eventToLink = typicalModel.getFilteredEventList().get(0); + Contact contactToLink = typicalModel.getFilteredContactList().get(0); + Model newModel = new ModelManager(typicalAddressBook, new UserPrefs()); + newModel.linkEventAndContact( + newModel.getFilteredEventList().get(0), + newModel.getFilteredContactList().get(0)); + assertCommandSuccess( + eLinkCommand, typicalModel, generateSuccessfulLink(eventToLink, Set.of(contactToLink)), newModel); + } + + @Test + public void execute_multiLink_success() { + // first index of event list and set of first and second indexes of contact list + ELinkCommand eLinkCommand = new ELinkCommand( + INDEX_FIRST, + Set.of(INDEX_FIRST, INDEX_SECOND)); + Event eventToLink = typicalModel.getFilteredEventList().get(0); + Contact contact1ToLink = typicalModel.getFilteredContactList().get(0); + Contact contact2ToLink = typicalModel.getFilteredContactList().get(1); + Set setOfContacts = Set.of(contact1ToLink, contact2ToLink); + Model newModel = new ModelManager(typicalAddressBook, new UserPrefs()); + newModel.linkEventAndContact( + newModel.getFilteredEventList().get(0), + newModel.getFilteredContactList().get(0)); + newModel.linkEventAndContact( + newModel.getFilteredEventList().get(0), + newModel.getFilteredContactList().get(1)); + assertCommandSuccess( + eLinkCommand, typicalModel, generateSuccessfulLink(eventToLink, setOfContacts), + newModel); + } + + @Test + public void execute_alreadyLinked_successWithErrorMessage() { + // first index of event list and first index of contact list + ELinkCommand eLinkCommand = new ELinkCommand( + INDEX_FIRST, + Set.of(INDEX_FIRST)); + Event eventToLink = typicalModel.getFilteredEventList().get(0); + Contact contactToLink = typicalModel.getFilteredContactList().get(0); + typicalModel.linkEventAndContact(eventToLink, contactToLink); + Model newModel = new ModelManager(typicalAddressBook, new UserPrefs()); + newModel.linkEventAndContact( + newModel.getFilteredEventList().get(0), + newModel.getFilteredContactList().get(0)); + assertCommandSuccess( + eLinkCommand, typicalModel, generateAlreadyLinked(eventToLink, Set.of(contactToLink)), newModel); + } + + @Test + public void execute_invalidIndex_failure() { + ELinkCommand eLinkCommand = new ELinkCommand( + Index.fromZeroBased(100), + Set.of(INDEX_SECOND)); + assertCommandFailure(eLinkCommand, typicalModel, Messages.MESSAGE_INVALID_EVENT_DISPLAYED_INDEX); + ELinkCommand eLinkCommand2 = new ELinkCommand( + INDEX_FIRST, + Set.of(INDEX_SECOND, Index.fromZeroBased(101))); + assertCommandFailure(eLinkCommand2, typicalModel, Messages.MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX); + } + + @Test + public void testEquals() { + // first index of event list and first index of contact list + ELinkCommand eLinkCommand1 = new ELinkCommand(INDEX_FIRST, Set.of(INDEX_FIRST)); + Set setOfContactIndexes = new HashSet<>(); + setOfContactIndexes.add(INDEX_FIRST); + setOfContactIndexes.add(INDEX_SECOND); + + // first index of event list and the set of contact list indexes + ELinkCommand eLinkCommand2 = new ELinkCommand(INDEX_FIRST, setOfContactIndexes); + + // first index of event list and set of first and second indexes of contact list + ELinkCommand eLinkCommand3 = new ELinkCommand( + INDEX_FIRST, + Set.of(INDEX_FIRST, INDEX_SECOND)); + + // second index of event list and the set of contact list indexes + ELinkCommand eLinkCommand4 = new ELinkCommand(INDEX_SECOND, setOfContactIndexes); + + assertEquals(eLinkCommand1, eLinkCommand1); + assertNotEquals(eLinkCommand1, eLinkCommand2); + assertEquals(eLinkCommand2, eLinkCommand3); + assertNotEquals(eLinkCommand3, eLinkCommand4); + } +} diff --git a/src/test/java/seedu/address/logic/commands/event/EListCommandTest.java b/src/test/java/seedu/address/logic/commands/event/EListCommandTest.java new file mode 100644 index 00000000000..aa024d6c756 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/event/EListCommandTest.java @@ -0,0 +1,85 @@ +package seedu.address.logic.commands.event; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.general.CommandTestUtil.showEventAtIndex; +import static seedu.address.model.event.EventDisplaySetting.DEFAULT_SETTING; +import static seedu.address.testutil.TypicalAddressBook.getTypicalAddressBook; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST; + +import java.util.Objects; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.event.EventDisplaySetting; + +class EListCommandTest { + + private Model model; + private Model expectedModel; + + @BeforeEach + public void setUp() { + model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + } + + @Test + public void execute_listIsNotFiltered_showsSameList() { + expectedModel.setEventDisplaySetting(DEFAULT_SETTING); + assertCommandSuccess(new EListCommand(DEFAULT_SETTING), model, EListCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void execute_listIsFiltered_showsEverything() { + showEventAtIndex(model, INDEX_FIRST); + expectedModel.setEventDisplaySetting(DEFAULT_SETTING); + assertCommandSuccess(new EListCommand(DEFAULT_SETTING), model, EListCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void execute_listIsNotFiltered_showsSomeFields() { + EventDisplaySetting eventDisplaySetting = new EventDisplaySetting(true, false, false, false, true, false); + expectedModel.setEventDisplaySetting(eventDisplaySetting); + assertCommandSuccess(new EListCommand(eventDisplaySetting), model, EListCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void execute_listIsFiltered_showsSomeFields() { + showEventAtIndex(model, INDEX_FIRST); + EventDisplaySetting eventDisplaySetting = new EventDisplaySetting(true, false, false, false, true, false); + expectedModel.setEventDisplaySetting(eventDisplaySetting); + assertCommandSuccess(new EListCommand(eventDisplaySetting), model, EListCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void equal() { + EListCommand standardCommand = new EListCommand(DEFAULT_SETTING); + assertTrue(standardCommand.equals(standardCommand)); + assertTrue(standardCommand.equals(new EListCommand(DEFAULT_SETTING))); + assertFalse(standardCommand.equals(new EListCommand(new EventDisplaySetting(false, false, + false, true, true, true)))); + assertFalse(standardCommand.equals(new EClearCommand())); + + } + + @Test + public void hashCode_equal() { + EListCommand standardCommand = new EListCommand(DEFAULT_SETTING); + assertEquals(standardCommand.hashCode(), Objects.hash(DEFAULT_SETTING)); + EListCommand anotherCommand = new EListCommand( + new EventDisplaySetting(true, false, false, true, false, true) + ); + assertEquals(anotherCommand.hashCode(), Objects.hash( + new EventDisplaySetting(true, false, false, true, false, true) + )); + + } +} + diff --git a/src/test/java/seedu/address/logic/commands/event/EMarkCommandTest.java b/src/test/java/seedu/address/logic/commands/event/EMarkCommandTest.java new file mode 100644 index 00000000000..09128468ff8 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/event/EMarkCommandTest.java @@ -0,0 +1,121 @@ +package seedu.address.logic.commands.event; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalAddressBook.getTypicalAddressBook; +import static seedu.address.testutil.TypicalEvents.BIRTHDAY_PARTY; +import static seedu.address.testutil.TypicalEvents.CS2100_CONSULTATION; +import static seedu.address.testutil.TypicalEvents.CS2101_MEETING; +import static seedu.address.testutil.TypicalEvents.CS2103_MIDTERM_MARKED; +import static seedu.address.testutil.TypicalEvents.FOOTBALL_PRACTICE; +import static seedu.address.testutil.TypicalEvents.TEAM_MEETING; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.CommandResult; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.contact.Contact; +import seedu.address.model.event.Event; +import seedu.address.testutil.EventBuilder; +import seedu.address.testutil.TypicalContacts; + +class EMarkCommandTest { + + private static final Event TEAM_MEETING_MARKED = new EventBuilder(TEAM_MEETING).withMarked(true).build(); + + private Model expectedModel = new ModelManager(getAddressBookWith(getListWithMarkEvent()), new UserPrefs()); + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + private List getListWithMarkEvent() { + return new ArrayList<>(Arrays.asList(TEAM_MEETING_MARKED, CS2103_MIDTERM_MARKED, CS2100_CONSULTATION, + CS2101_MEETING, FOOTBALL_PRACTICE, BIRTHDAY_PARTY)); + } + + public AddressBook getAddressBookWith(List eventList) { + AddressBook ab = new AddressBook(); + for (Contact contact : TypicalContacts.getTypicalContacts()) { + ab.addContact(contact); + } + for (Event event : eventList) { + ab.addEvent(event); + } + return ab; + } + + @Test + public void constructor_nullIndex_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new EMarkCommand(null)); + } + + @Test + public void execute_validIndexUnfilteredList_success() { + String expectedMessage = String.format(EMarkCommand.MESSAGE_SUCCESS, TEAM_MEETING) + "\n"; + List indexes = List.of(Index.fromOneBased(5)); + EMarkCommand eMarkCommand = new EMarkCommand(indexes); + assertCommandSuccess(eMarkCommand, model, new CommandResult(expectedMessage), expectedModel); + } + + @Test + public void execute_invalidIndex_throwsCommandException() { + List outOfBoundIndex = List.of(Index.fromOneBased(model.getFilteredEventList().size() + 1)); + EMarkCommand eMarkCommand = new EMarkCommand(outOfBoundIndex); + assertCommandFailure(eMarkCommand, model, Messages.MESSAGE_INVALID_EVENT_DISPLAYED_INDEX); + + List veryLargeNumberWithDuplicateDigit = List.of(Index.fromOneBased(1111111111)); + EMarkCommand secondMarkCommand = new EMarkCommand(veryLargeNumberWithDuplicateDigit); + assertCommandFailure(secondMarkCommand, model, Messages.MESSAGE_INVALID_EVENT_DISPLAYED_INDEX); + } + + @Test + public void execute_eventAlreadyMarked() { + List indexes = List.of(Index.fromOneBased(1)); + model = expectedModel; + EMarkCommand eMarkCommand = new EMarkCommand(indexes); + String expectedMessage = String.format(EMarkCommand.MESSAGE_ALREADY_MARKED, TEAM_MEETING_MARKED) + "\n"; + assertCommandSuccess(eMarkCommand, model, expectedMessage, expectedModel); + } + + @Test + public void equals() { + Index first = Index.fromOneBased(1); + Index second = Index.fromOneBased(2); + + List firstIndexes = new ArrayList<>(); + firstIndexes.add(first); + List secondIndexes = new ArrayList<>(); + secondIndexes.add(second); + EMarkCommand markFirstCommand = new EMarkCommand(firstIndexes); + EMarkCommand markSecondCommand = new EMarkCommand(secondIndexes); + + // same object -> returns true + assertTrue(markFirstCommand.equals(markFirstCommand)); + + // same values -> returns true + EMarkCommand markFirstCommandCopy = new EMarkCommand(firstIndexes); + ; + assertTrue(markFirstCommand.equals(markFirstCommandCopy)); + + // different types -> returns false + assertFalse(markFirstCommand.equals(1)); + + // null -> returns false + assertFalse(markFirstCommand.equals(null)); + + // different Index -> returns false + assertFalse(markFirstCommand.equals(markSecondCommand)); + } + + +} diff --git a/src/test/java/seedu/address/logic/commands/event/ESortCommandTest.java b/src/test/java/seedu/address/logic/commands/event/ESortCommandTest.java new file mode 100644 index 00000000000..bfa7cd13aec --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/event/ESortCommandTest.java @@ -0,0 +1,87 @@ +package seedu.address.logic.commands.event; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.model.event.DateAndTime.DATE_TIME_FORMATTER; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.common.Name; +import seedu.address.model.event.EndDateTime; +import seedu.address.model.event.Event; +import seedu.address.model.event.StartDateTime; +import seedu.address.testutil.EventBuilder; + +class ESortCommandTest { + + private static Model setUpModel() { + UserPrefs userPrefs = new UserPrefs(); + AddressBook addressBook = new AddressBook(); + LocalDateTime timeNow = LocalDateTime.now(); + String beforeNow1 = timeNow.minusDays(10).format(DATE_TIME_FORMATTER); + String beforeNow2 = timeNow.minusHours(3).minusDays(1).format(DATE_TIME_FORMATTER); + String starting = timeNow.format(DATE_TIME_FORMATTER); + String after1 = timeNow.plusDays(7).plusHours(4).plusMinutes(30).format(DATE_TIME_FORMATTER); + String after2 = timeNow.plusYears(1).format(DATE_TIME_FORMATTER); + String after3 = timeNow.plusYears(1).format(DATE_TIME_FORMATTER); + Event[] events = new Event[] {new Event(new Name("Event 1"), new StartDateTime(after2), null, null, + null, null, Set.of()), + new Event(new Name("Event 2"), new StartDateTime(beforeNow2), new EndDateTime(starting), null, + null, null, Set.of()), + new Event(new Name("Event 3"), new StartDateTime(after3), null, null, + null, null, Set.of()), + new Event(new Name("Event 4"), new StartDateTime(starting), null, null, + null, null, Set.of()), + new Event(new Name("Event 5"), new StartDateTime(beforeNow1), new EndDateTime(after2), null, + null, null, Set.of()), + new Event(new Name("Event 6"), new StartDateTime(beforeNow1), new EndDateTime(after1), null, + null, null, Set.of()), + new Event(new Name("Event 7"), new StartDateTime(beforeNow1), new EndDateTime(beforeNow2), null, + null, null, Set.of()), + new Event(new Name("Event 8"), new StartDateTime(beforeNow2), null, null, + null, null, Set.of())}; + Model model = new ModelManager(addressBook, userPrefs); + for (Event e : events) { + model.addEvent(e); + } + return model; + } + + @Test + public void execute_emptyList() { + Model model = new ModelManager(); + ESortCommand command = new ESortCommand(); + assertCommandSuccess(command, model, ESortCommand.MESSAGE_SUCCESS, model); + } + + @Test + public void execute_nonEmptyList() { + Model model = setUpModel(); + List events = model.getFilteredEventList(); + ESortCommand command = new ESortCommand(); + List expectedList = List.of(events.get(4), events.get(5), events.get(0), events.get(2)); + command.execute(model); + assertEquals(model.getFilteredEventList(), expectedList); + } + + @Test + public void events_withMarked_sort() { + Model model = setUpModel(); + Event eventToMark = model.getFilteredEventList().get(0); + model.setEvent(eventToMark, new EventBuilder(eventToMark).withMarked(true).build()); + List events = model.getFilteredEventList(); + ESortCommand command = new ESortCommand(); + // Marked event still at the start + List expectedList = List.of(events.get(0), events.get(4), events.get(5), events.get(2)); + command.execute(model); + assertEquals(model.getFilteredEventList(), expectedList); + } +} diff --git a/src/test/java/seedu/address/logic/commands/event/EUnlinkCommandTest.java b/src/test/java/seedu/address/logic/commands/event/EUnlinkCommandTest.java new file mode 100644 index 00000000000..5d143383e9d --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/event/EUnlinkCommandTest.java @@ -0,0 +1,185 @@ +package seedu.address.logic.commands.event; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.contact.Contact; +import seedu.address.model.event.Event; +import seedu.address.testutil.TypicalAddressBook; +import seedu.address.testutil.TypicalIndexes; + +class EUnlinkCommandTest { + private final AddressBook typicalAddressBook = TypicalAddressBook.getTypicalAddressBook(); + private final Model typicalModel = new ModelManager(typicalAddressBook, new UserPrefs()); + + /** + * Links event 0 to contact 0 and 1. Links event 1 to contact 2. Links event 3 to contact 1. + */ + @BeforeEach + public void setUp() { + setUp(typicalModel); + } + + /** + * Links event 0 to contact 0, 1, 3. Links event 1 to contact 2. Links event 3 to contact 1. + */ + private void setUp(Model model) { + model.linkEventAndContact( + model.getFilteredEventList().get(0), + model.getFilteredContactList().get(0)); + model.linkEventAndContact( + model.getFilteredEventList().get(0), + model.getFilteredContactList().get(1)); + model.linkEventAndContact( + model.getFilteredEventList().get(0), + model.getFilteredContactList().get(3)); + model.linkEventAndContact( + model.getFilteredEventList().get(1), + model.getFilteredContactList().get(2)); + model.linkEventAndContact( + model.getFilteredEventList().get(2), + model.getFilteredContactList().get(1)); + } + + private String generateCommandResult(Event eventToUnlink, Set set) { + assert !set.isEmpty(); + StringBuilder result = new StringBuilder(); + for (Contact contact : set) { + String resultForEachUnlink = String.format(EUnlinkCommand.MESSAGE_SUCCESS, + eventToUnlink.getName(), contact.getName()); + result.append(resultForEachUnlink); + } + return result.toString(); + } + + @Test + public void execute_singleUnlink_success() { + EUnlinkCommand eUnlinkCommand = new EUnlinkCommand( + INDEX_FIRST, + Set.of(TypicalIndexes.INDEX_FIRST), false); + Event eventToUnlink = typicalModel.getFilteredEventList().get(0); + Contact contactToUnlink = typicalModel.getFilteredContactList().get(0); + Model newModel = new ModelManager(typicalModel.getAddressBook(), new UserPrefs()); + setUp(newModel); + newModel.unlinkEventAndContact( + newModel.getFilteredEventList().get(0), newModel.getFilteredContactList().get(0)); + String commandSuccessMessage = String.format(EUnlinkCommand.MESSAGE_SUCCESS, + eventToUnlink.getName(), contactToUnlink.getName()); + assertCommandSuccess(eUnlinkCommand, typicalModel, commandSuccessMessage, newModel); + } + + @Test + public void execute_multipleUnlink_success() { + EUnlinkCommand eUnlinkCommand = new EUnlinkCommand( + INDEX_FIRST, + Set.of(TypicalIndexes.INDEX_FIRST, TypicalIndexes.INDEX_SECOND), false); + Event eventToUnlink = typicalModel.getFilteredEventList().get(0); + Contact contact1ToUnlink = typicalModel.getFilteredContactList().get(0); + Contact contact2ToUnlink = typicalModel.getFilteredContactList().get(1); + Set setOfContacts = Set.of(contact1ToUnlink, contact2ToUnlink); + Model newModel = new ModelManager(typicalModel.getAddressBook(), new UserPrefs()); + setUp(newModel); + newModel.unlinkEventAndContact( + newModel.getFilteredEventList().get(0), newModel.getFilteredContactList().get(0)); + newModel.unlinkEventAndContact( + newModel.getFilteredEventList().get(0), newModel.getFilteredContactList().get(1)); + String commandSuccessMessage = generateCommandResult(eventToUnlink, setOfContacts); + assertCommandSuccess(eUnlinkCommand, typicalModel, commandSuccessMessage, newModel); + } + + @Test + public void execute_allUnlink_success() { + EUnlinkCommand eUnlinkCommand = new EUnlinkCommand( + INDEX_FIRST, + Set.of(), true); + Event eventToUnlink = typicalModel.getFilteredEventList().get(0); + Model newModel = new ModelManager(typicalModel.getAddressBook(), new UserPrefs()); + setUp(newModel); + newModel.unlinkEventAndContact( + newModel.getFilteredEventList().get(0), newModel.getFilteredContactList().get(0)); + newModel.unlinkEventAndContact( + newModel.getFilteredEventList().get(0), newModel.getFilteredContactList().get(1)); + newModel.unlinkEventAndContact( + newModel.getFilteredEventList().get(0), newModel.getFilteredContactList().get(3)); + String commandSuccessMessage = String.format( + EUnlinkCommand.MESSAGE_SUCCESS_CLEAR_ALL, + eventToUnlink.getName()); + assertCommandSuccess(eUnlinkCommand, typicalModel, commandSuccessMessage, newModel); + } + + @Test + public void execute_alreadyUnlinked_successWithErrorMessage() { + EUnlinkCommand eUnlinkCommand = new EUnlinkCommand( + INDEX_SECOND, + Set.of(INDEX_SECOND), false); + Event eventToUnlink = typicalModel.getFilteredEventList().get(1); + Contact contactToUnlink = typicalModel.getFilteredContactList().get(1); + Model newModel = new ModelManager(typicalModel.getAddressBook(), new UserPrefs()); + setUp(newModel); + newModel.unlinkEventAndContact( + newModel.getFilteredEventList().get(0), newModel.getFilteredContactList().get(0)); + String commandSuccessMessage = String.format(EUnlinkCommand.MESSAGE_NOT_LINKED, + eventToUnlink.getName(), contactToUnlink.getName()); + assertCommandSuccess(eUnlinkCommand, typicalModel, commandSuccessMessage, newModel); + } + + @Test + public void execute_invalidIndex_failure() { + EUnlinkCommand eUnlinkCommand = new EUnlinkCommand( + Index.fromZeroBased(100), + Set.of(), true); + assertCommandFailure(eUnlinkCommand, typicalModel, Messages.MESSAGE_INVALID_EVENT_DISPLAYED_INDEX); + EUnlinkCommand eUnlinkCommand2 = new EUnlinkCommand( + INDEX_FIRST, + Set.of(INDEX_SECOND, Index.fromZeroBased(101)), false); + assertCommandFailure(eUnlinkCommand2, typicalModel, Messages.MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX); + } + + @Test + public void testEquals() { + EUnlinkCommand eUnlinkCommand1 = new EUnlinkCommand(INDEX_FIRST, Set.of(), true); + assertEquals(eUnlinkCommand1, eUnlinkCommand1); + + EUnlinkCommand eUnlinkCommand2 = new EUnlinkCommand(INDEX_FIRST, new HashSet<>(), true); + assertEquals(eUnlinkCommand1, eUnlinkCommand2); + + EUnlinkCommand eUnlinkCommand3 = new EUnlinkCommand(INDEX_FIRST, + Set.of(INDEX_FIRST, INDEX_SECOND), false); + EUnlinkCommand eUnlinkCommand4 = new EUnlinkCommand(INDEX_SECOND, + Set.of(INDEX_FIRST, INDEX_SECOND), false); + Set indexSet = new HashSet<>(); + indexSet.add(INDEX_SECOND); + indexSet.add(INDEX_FIRST); + EUnlinkCommand eUnlinkCommand5 = new EUnlinkCommand(INDEX_SECOND, indexSet, false); + assertEquals(eUnlinkCommand5, eUnlinkCommand4); + assertNotEquals(eUnlinkCommand3, eUnlinkCommand5); + assertNotEquals(eUnlinkCommand4, eUnlinkCommand3); + assertNotEquals(eUnlinkCommand1, eUnlinkCommand5); + } + + @Test + public void constructor_invalid_failure() { + assertThrows(AssertionError.class, () -> new EUnlinkCommand(INDEX_FIRST, Set.of(), false)); + assertThrows( + AssertionError.class, () -> new EUnlinkCommand(INDEX_FIRST, Set.of(INDEX_FIRST), true)); + assertThrows(NullPointerException.class, () -> new EUnlinkCommand(INDEX_FIRST, null, false)); + assertThrows(NullPointerException.class, () -> new EUnlinkCommand(null, Set.of(), false)); + } +} diff --git a/src/test/java/seedu/address/logic/commands/event/EUnmarkCommandTest.java b/src/test/java/seedu/address/logic/commands/event/EUnmarkCommandTest.java new file mode 100644 index 00000000000..3cf1a0fd7f5 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/event/EUnmarkCommandTest.java @@ -0,0 +1,122 @@ +package seedu.address.logic.commands.event; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalEvents.BIRTHDAY_PARTY; +import static seedu.address.testutil.TypicalEvents.CS2100_CONSULTATION; +import static seedu.address.testutil.TypicalEvents.CS2101_MEETING; +import static seedu.address.testutil.TypicalEvents.CS2103_MIDTERM_MARKED; +import static seedu.address.testutil.TypicalEvents.FOOTBALL_PRACTICE; +import static seedu.address.testutil.TypicalEvents.TEAM_MEETING; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.CommandResult; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.event.Event; +import seedu.address.testutil.EventBuilder; + +class EUnmarkCommandTest { + + private static final Event TEAM_MEETING_MARKED = new EventBuilder(TEAM_MEETING).withMarked(true).build(); + + private Model expectedModel; + private Model model; + + private List getListWithMarkedEvent() { + return new ArrayList<>(Arrays.asList(TEAM_MEETING_MARKED, CS2103_MIDTERM_MARKED, CS2100_CONSULTATION, + CS2101_MEETING, FOOTBALL_PRACTICE, BIRTHDAY_PARTY)); + } + + private List getListAfterUnmark() { + return new ArrayList<>(Arrays.asList(CS2103_MIDTERM_MARKED, TEAM_MEETING, CS2100_CONSULTATION, + CS2101_MEETING, FOOTBALL_PRACTICE, BIRTHDAY_PARTY)); + } + + public static AddressBook getAddressBookWith(List eventList) { + AddressBook ab = new AddressBook(); + for (Event event : eventList) { + ab.addEvent(event); + } + return ab; + } + + @BeforeEach + private void setUp() { + expectedModel = new ModelManager(getAddressBookWith(getListAfterUnmark()), new UserPrefs()); + model = new ModelManager(getAddressBookWith(getListWithMarkedEvent()), new UserPrefs()); + } + + @Test + public void constructor_nullIndex_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new EUnmarkCommand(null)); + } + + @Test + public void execute_validIndexUnfilteredList_success() { + String expectedMessage = String.format(EUnmarkCommand.MESSAGE_SUCCESS, TEAM_MEETING_MARKED) + "\n"; + List indexes = List.of(Index.fromOneBased(1)); + EUnmarkCommand eUnmarkCommand = new EUnmarkCommand(indexes); + assertCommandSuccess(eUnmarkCommand, model, new CommandResult(expectedMessage), expectedModel); + } + + @Test + public void execute_invalidIndex_throwsCommandException() { + List outOfBoundIndex = List.of(Index.fromOneBased(model.getFilteredEventList().size() + 1)); + EUnmarkCommand eunmarkCommand = new EUnmarkCommand(outOfBoundIndex); + assertCommandFailure(eunmarkCommand, model, Messages.MESSAGE_INVALID_EVENT_DISPLAYED_INDEX); + } + + @Test + public void execute_eventNotMarked() { + List indexes = List.of(Index.fromOneBased(2)); + model = new ModelManager(getAddressBookWith(getListAfterUnmark()), new UserPrefs()); + EUnmarkCommand eunmarkCommand = new EUnmarkCommand(indexes); + String expectedMessage = String.format(EUnmarkCommand.MESSAGE_NOT_MARKED, TEAM_MEETING) + "\n"; + assertCommandSuccess(eunmarkCommand, model, expectedMessage, expectedModel); + } + + @Test + public void equals() { + Index first = Index.fromOneBased(1); + Index second = Index.fromOneBased(2); + + List firstIndexes = new ArrayList<>(); + firstIndexes.add(first); + List secondIndexes = new ArrayList<>(); + secondIndexes.add(second); + EUnmarkCommand unmarkFirstCommand = new EUnmarkCommand(firstIndexes); + EUnmarkCommand unmarkSecondCommand = new EUnmarkCommand(secondIndexes); + + // same object -> returns true + assertTrue(unmarkFirstCommand.equals(unmarkFirstCommand)); + + // same values -> returns true + EUnmarkCommand unmarkFirstCommandCopy = new EUnmarkCommand(firstIndexes); + ; + assertTrue(unmarkFirstCommand.equals(unmarkFirstCommandCopy)); + + // different types -> returns false + assertFalse(unmarkFirstCommand.equals(1)); + + // null -> returns false + assertFalse(unmarkFirstCommand.equals(null)); + + // different Index -> returns false + assertFalse(unmarkFirstCommand.equals(unmarkSecondCommand)); + } + +} diff --git a/src/test/java/seedu/address/logic/commands/event/EViewCommandTest.java b/src/test/java/seedu/address/logic/commands/event/EViewCommandTest.java new file mode 100644 index 00000000000..d79efefa03c --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/event/EViewCommandTest.java @@ -0,0 +1,97 @@ +package seedu.address.logic.commands.event; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.general.CommandTestUtil.showEventAtIndex; +import static seedu.address.testutil.TypicalAddressBook.getTypicalAddressBook; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventDisplaySetting; + + +public class EViewCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_validIndexUnfilteredList_success() { + Event eventToView = model.getFilteredEventList().get(INDEX_FIRST.getZeroBased()); + EViewCommand eViewCommand = new EViewCommand(INDEX_FIRST); + + String expectedMessage = String.format(EViewCommand.MESSAGE_SUCCESS, eventToView); + + expectedModel.updateEventListByIndex(INDEX_FIRST); + expectedModel.setEventDisplaySetting(new EventDisplaySetting(true)); + + assertCommandSuccess(eViewCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndexUnfilteredList_throwsCommandException() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredEventList().size() + 1); + EViewCommand eViewCommand = new EViewCommand(outOfBoundIndex); + + assertCommandFailure(eViewCommand, model, Messages.MESSAGE_INVALID_EVENT_DISPLAYED_INDEX); + } + + @Test + public void execute_validIndexFilteredList_success() { + showEventAtIndex(model, INDEX_FIRST); + + Event eventToView = model.getFilteredEventList().get(INDEX_FIRST.getZeroBased()); + EViewCommand eViewCommand = new EViewCommand(INDEX_FIRST); + + String expectedMessage = String.format(EViewCommand.MESSAGE_SUCCESS, eventToView); + + expectedModel.updateEventListByIndex(INDEX_FIRST); + expectedModel.setEventDisplaySetting(new EventDisplaySetting(true)); + + assertCommandSuccess(eViewCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndexFilteredList_throwsCommandException() { + showEventAtIndex(model, INDEX_FIRST); + Index outOfBoundIndex = INDEX_SECOND; + // ensures that outOfBoundIndex is still in bounds of address book list + assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getEventList().size()); + + EViewCommand eViewCommand = new EViewCommand(outOfBoundIndex); + + assertCommandFailure(eViewCommand, model, Messages.MESSAGE_INVALID_EVENT_DISPLAYED_INDEX); + } + + @Test + public void equals() { + EViewCommand viewFirstCommand = new EViewCommand(INDEX_FIRST); + EViewCommand viewSecondCommand = new EViewCommand(INDEX_SECOND); + + // same object -> returns true + assertTrue(viewFirstCommand.equals(viewFirstCommand)); + + // same values -> returns true + EViewCommand viewFirstCommandCopy = new EViewCommand(INDEX_FIRST); + assertTrue(viewFirstCommand.equals(viewFirstCommandCopy)); + + // different types -> returns false + assertFalse(viewFirstCommand.equals(1)); + + // null -> returns false + assertFalse(viewFirstCommand.equals(null)); + + // different event -> returns false + assertFalse(viewFirstCommand.equals(viewSecondCommand)); + } +} diff --git a/src/test/java/seedu/address/logic/commands/event/EditEventDescriptorTest.java b/src/test/java/seedu/address/logic/commands/event/EditEventDescriptorTest.java new file mode 100644 index 00000000000..626823b3348 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/event/EditEventDescriptorTest.java @@ -0,0 +1,67 @@ +package seedu.address.logic.commands.event; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.general.CommandTestUtil.DESC_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.DESC_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_ADDRESS_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_END_DATE_TIME_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_NAME_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_START_DATE_TIME_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_TAG_EXAMS; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.event.EEditCommand.EditEventDescriptor; +import seedu.address.testutil.EditEventDescriptorBuilder; + +public class EditEventDescriptorTest { + + @Test + public void isAnyFieldEdited_noFieldEdited() { + EditEventDescriptor emptyEditEventDescriptor = new EditEventDescriptor(); + assertFalse(emptyEditEventDescriptor.isAnyFieldEdited()); + } + + @Test + public void equals() { + // same values -> returns true + EditEventDescriptor descriptorWithSameValues = new EditEventDescriptor(DESC_TUTORIAL); + assertTrue(DESC_TUTORIAL.equals(descriptorWithSameValues)); + + // same object -> returns true + assertTrue(DESC_TUTORIAL.equals(DESC_TUTORIAL)); + + // null -> returns false + assertFalse(DESC_TUTORIAL.equals(null)); + + // different types -> returns false + assertFalse(DESC_TUTORIAL.equals(5)); + + // different values -> returns false + assertFalse(DESC_TUTORIAL.equals(DESC_EXAM)); + + // different name -> returns false + EditEventDescriptor editedTutorial = new EditEventDescriptorBuilder(DESC_TUTORIAL).withName(VALID_NAME_EXAM) + .build(); + assertFalse(DESC_TUTORIAL.equals(editedTutorial)); + + // different start time -> returns false + editedTutorial = new EditEventDescriptorBuilder(DESC_TUTORIAL).withStartDateTime(VALID_START_DATE_TIME_EXAM) + .build(); + assertFalse(DESC_TUTORIAL.equals(editedTutorial)); + + // different end time -> returns false + editedTutorial = new EditEventDescriptorBuilder(DESC_TUTORIAL).withEndDateTime(VALID_END_DATE_TIME_EXAM) + .build(); + assertFalse(DESC_TUTORIAL.equals(editedTutorial)); + + // different address -> returns false + editedTutorial = new EditEventDescriptorBuilder(DESC_TUTORIAL).withAddress(VALID_ADDRESS_EXAM).build(); + assertFalse(DESC_TUTORIAL.equals(editedTutorial)); + + // different tags -> returns false + editedTutorial = new EditEventDescriptorBuilder(DESC_TUTORIAL).withTags(VALID_TAG_EXAMS).build(); + assertFalse(DESC_TUTORIAL.equals(editedTutorial)); + } +} diff --git a/src/test/java/seedu/address/logic/commands/general/CalendarCommandTest.java b/src/test/java/seedu/address/logic/commands/general/CalendarCommandTest.java new file mode 100644 index 00000000000..6f2aac6f85b --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/general/CalendarCommandTest.java @@ -0,0 +1,21 @@ +package seedu.address.logic.commands.general; + +import static seedu.address.logic.commands.general.CalendarCommand.MESSAGE_SUCCESS; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.CommandResult; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; + +class CalendarCommandTest { + private Model model = new ModelManager(); + private Model expectedModel = new ModelManager(); + + @Test + void execute_calendar_success() { + CommandResult expectedCommandResult = new CommandResult(MESSAGE_SUCCESS, false, false, true); + assertCommandSuccess(new CalendarCommand(), model, expectedCommandResult, expectedModel); + } +} diff --git a/src/test/java/seedu/address/logic/commands/general/CommandTestUtil.java b/src/test/java/seedu/address/logic/commands/general/CommandTestUtil.java new file mode 100644 index 00000000000..ecb9dc6e2f2 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/general/CommandTestUtil.java @@ -0,0 +1,243 @@ +package seedu.address.logic.commands.general; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_CONTACT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DELETE_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_END_TIME; +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_START_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TELEGRAM; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ZOOM; +import static seedu.address.testutil.Assert.assertThrows; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.contact.CEditCommand.EditContactDescriptor; +import seedu.address.logic.commands.event.EEditCommand.EditEventDescriptor; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.ContactContainsKeywordsPredicate; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventContainsKeywordsPredicate; +import seedu.address.testutil.EditContactDescriptorBuilder; +import seedu.address.testutil.EditEventDescriptorBuilder; + + +/** + * Contains helper methods for testing commands. + */ +public class CommandTestUtil { + + //common + public static final String EMPTY_PREFIX_ADDRESS = " " + PREFIX_ADDRESS; + public static final String EMPTY_PREFIX_ZOOM = " " + PREFIX_ZOOM; + public static final String EMPTY_PREFIX_TAG = " " + PREFIX_TAG; + public static final String EMPTY_PREFIX_CONTACT = " " + PREFIX_CONTACT; + public static final String VALID_INDEX_ONE = "1"; + public static final String VALID_INDEX_TWO = "2"; + public static final String INVALID_INDEX = "0"; + + //for contacts + public static final String VALID_NAME_AMY = "Amy Bee"; + public static final String VALID_NAME_BOB = "Bob Choo"; + public static final String VALID_PHONE_AMY = "11111111"; + public static final String VALID_PHONE_BOB = "22222222"; + public static final String VALID_EMAIL_AMY = "amy@example.com"; + public static final String VALID_EMAIL_BOB = "bob@example.com"; + public static final String VALID_ADDRESS_AMY = "Block 312, Amy Street 1"; + public static final String VALID_ADDRESS_BOB = "Block 123, Bobby Street 3"; + public static final String VALID_TELEGRAM_AMY = "amyBeeBee"; + public static final String VALID_TELEGRAM_BOB = "Bobby"; + public static final String VALID_ZOOM_AMY = "https://nus-sg.zoom.us/j/0123456789?pwd=ABCDEFG"; + public static final String VALID_ZOOM_BOB = "https://nus-sg.zoom.us/j/0123456789?pwd=ABCDEFH"; + public static final String VALID_TAG_HUSBAND = "husband"; + public static final String VALID_TAG_FRIEND = "friend"; + + public static final String NAME_DESC_AMY = " " + PREFIX_NAME + VALID_NAME_AMY; + public static final String NAME_DESC_BOB = " " + PREFIX_NAME + VALID_NAME_BOB; + public static final String PHONE_DESC_AMY = " " + PREFIX_PHONE + VALID_PHONE_AMY; + public static final String PHONE_DESC_BOB = " " + PREFIX_PHONE + VALID_PHONE_BOB; + public static final String EMAIL_DESC_AMY = " " + PREFIX_EMAIL + VALID_EMAIL_AMY; + public static final String EMAIL_DESC_BOB = " " + PREFIX_EMAIL + VALID_EMAIL_BOB; + public static final String ADDRESS_DESC_AMY = " " + PREFIX_ADDRESS + VALID_ADDRESS_AMY; + public static final String ADDRESS_DESC_BOB = " " + PREFIX_ADDRESS + VALID_ADDRESS_BOB; + public static final String TELEGRAM_DESC_AMY = " " + PREFIX_TELEGRAM + VALID_TELEGRAM_AMY; + public static final String TELEGRAM_DESC_BOB = " " + PREFIX_TELEGRAM + VALID_TELEGRAM_BOB; + public static final String ZOOM_DESC_AMY = " " + PREFIX_ZOOM + VALID_ZOOM_AMY; + public static final String ZOOM_DESC_BOB = " " + PREFIX_ZOOM + VALID_ZOOM_BOB; + public static final String TAG_DESC_FRIEND = " " + PREFIX_TAG + VALID_TAG_FRIEND; + public static final String TAG_DESC_HUSBAND = " " + PREFIX_TAG + VALID_TAG_HUSBAND; + public static final String TAG_DESC_DELETEALL = " " + PREFIX_DELETE_TAG + "*"; + public static final String TAG_DESC_DELETEFRIEND = " " + PREFIX_DELETE_TAG + VALID_TAG_FRIEND; + public static final String TAG_DESC_DELETEHUSBAND = " " + PREFIX_DELETE_TAG + VALID_TAG_HUSBAND; + + public static final String EMPTY_PREFIX_PHONE = " " + PREFIX_PHONE; + public static final String EMPTY_PREFIX_EMAIL = " " + PREFIX_EMAIL; + public static final String EMPTY_PREFIX_TELEGRAM = " " + PREFIX_TELEGRAM; + + public static final String INVALID_NAME_DESC = " " + PREFIX_NAME + "James&"; // '&' not allowed in names + public static final String INVALID_PHONE_DESC = " " + PREFIX_PHONE + "911a"; // 'a' not allowed in phones + public static final String INVALID_EMAIL_DESC = " " + PREFIX_EMAIL + "bob!yahoo"; // missing '@' symbol + public static final String INVALID_ADDRESS_DESC = " " + PREFIX_ADDRESS; // empty string not allowed for addresses + public static final String INVALID_TELEGRAM_DESC = " " + PREFIX_TELEGRAM + "2103"; // Minimal 5 characters allowed + public static final String INVALID_ZOOM_DESC = " " + PREFIX_ZOOM + ""; // empty url not allowed + public static final String INVALID_TAG_DESC = " " + PREFIX_TAG + "hubby*"; // '*' not allowed in tags + + public static final String PREAMBLE_WHITESPACE = "\t \r \n"; + public static final String PREAMBLE_NON_EMPTY = "NonEmptyPreamble"; + + public static final EditContactDescriptor DESC_AMY; + public static final EditContactDescriptor DESC_BOB; + + static { + DESC_AMY = new EditContactDescriptorBuilder().withName(VALID_NAME_AMY) + .withPhone(VALID_PHONE_AMY).withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY) + .withZoomLink(VALID_ZOOM_AMY).withTelegram(VALID_TELEGRAM_AMY).withTags(VALID_TAG_FRIEND) + .withDeleteAllTags(true).build(); + DESC_BOB = new EditContactDescriptorBuilder().withName(VALID_NAME_BOB) + .withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB) + .withZoomLink(VALID_ZOOM_BOB).withTelegram(VALID_TELEGRAM_BOB) + .withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).withDeleteAllTags(true).build(); + } + + //for events + public static final String VALID_NAME_TUTORIAL = "CS2103T tutorial"; + public static final String VALID_NAME_EXAM = "CS2100 Midterms"; + public static final String VALID_START_DATE_TIME_TUTORIAL = "17-10-2021 15:00"; + public static final String VALID_END_DATE_TIME_TUTORIAL = "17-10-2021 16:00"; + public static final String VALID_START_DATE_TIME_EXAM = "20-10-2021 09:00"; + public static final String VALID_END_DATE_TIME_EXAM = "20-10-2021 11:00"; + public static final String VALID_DESCRIPTION_TUTORIAL = "Topics: Drawing UML diagrams"; + public static final String VALID_DESCRIPTION_EXAM = "I'm very unprepared"; + public static final String VALID_ADDRESS_TUTORIAL = "2-11 COM2"; + public static final String VALID_ADDRESS_EXAM = "Zoom"; + public static final String VALID_ZOOM_TUTORIAL = "https://nus-sg.zoom.us/j/0123456789?pwd=ABCDEFG"; + public static final String VALID_ZOOM_EXAM = "nus-sg.edu/123%a"; + public static final String VALID_TAG_COOL = "cool"; + public static final String VALID_TAG_EXAMS = "exams"; + + public static final String NAME_DESC_TUTORIAL = " " + PREFIX_NAME + VALID_NAME_TUTORIAL; + public static final String NAME_DESC_EXAM = " " + PREFIX_NAME + VALID_NAME_EXAM; + public static final String START_DATE_TIME_DESC_TUTORIAL = " " + PREFIX_START_TIME + VALID_START_DATE_TIME_TUTORIAL; + public static final String START_DATE_TIME_DESC_EXAM = " " + PREFIX_START_TIME + VALID_START_DATE_TIME_EXAM; + public static final String END_DATE_TIME_DESC_TUTORIAL = " " + PREFIX_END_TIME + VALID_END_DATE_TIME_TUTORIAL; + public static final String END_DATE_TIME_DESC_EXAM = " " + PREFIX_END_TIME + VALID_END_DATE_TIME_EXAM; + public static final String DESCRIPTION_DESC_TUTORIAL = " " + PREFIX_DESCRIPTION + VALID_DESCRIPTION_TUTORIAL; + public static final String DESCRIPTION_DESC_EXAM = " " + PREFIX_DESCRIPTION + VALID_DESCRIPTION_EXAM; + public static final String ADDRESS_DESC_TUTORIAL = " " + PREFIX_ADDRESS + VALID_ADDRESS_TUTORIAL; + public static final String ADDRESS_DESC_EXAM = " " + PREFIX_ADDRESS + VALID_ADDRESS_EXAM; + public static final String ZOOM_DESC_TUTORIAL = " " + PREFIX_ZOOM + VALID_ZOOM_TUTORIAL; + public static final String ZOOM_DESC_EXAM = " " + PREFIX_ZOOM + VALID_ZOOM_EXAM; + public static final String TAG_DESC_COOL = " " + PREFIX_TAG + VALID_TAG_COOL; + public static final String TAG_DESC_EXAMS = " " + PREFIX_TAG + VALID_TAG_EXAMS; + public static final String DELETE_TAG_DESC_EXAMS = " " + PREFIX_DELETE_TAG + VALID_TAG_EXAMS; + public static final String DELETE_TAG_DESC_COOL = " " + PREFIX_DELETE_TAG + VALID_TAG_COOL; + + public static final String EMPTY_PREFIX_START_DATE_TIME = " " + PREFIX_START_TIME; + public static final String EMPTY_PREFIX_END_DATE_TIME = " " + PREFIX_END_TIME; + public static final String EMPTY_PREFIX_DESCRIPTION = " " + PREFIX_DESCRIPTION; + + public static final String INVALID_EVENT_NAME_DESC = " " + PREFIX_NAME + "Hackathon&"; // '&' not allowed in names + public static final String INVALID_START_DATE_TIME_DESC = " " + PREFIX_START_TIME + "911a"; + // only takes in dd-MM-yyy HH:mm format + public static final String INVALID_END_DATE_TIME_DESC = " " + PREFIX_END_TIME + "bob!yahoo"; + // only takes in dd-MM-yyy HH:mm format + + + public static final EditEventDescriptor DESC_TUTORIAL = new EditEventDescriptorBuilder() + .withName(VALID_NAME_TUTORIAL).withStartDateTime(VALID_START_DATE_TIME_TUTORIAL) + .withEndDateTime(VALID_END_DATE_TIME_TUTORIAL).withDescription(VALID_DESCRIPTION_TUTORIAL) + .withAddress(VALID_ADDRESS_TUTORIAL).withZoomLink(VALID_ZOOM_TUTORIAL).withTags(VALID_TAG_COOL) + .withDeleteAllTags(true).build(); + public static final EditEventDescriptor DESC_EXAM = new EditEventDescriptorBuilder().withName(VALID_NAME_EXAM) + .withStartDateTime(VALID_START_DATE_TIME_EXAM).withEndDateTime(VALID_END_DATE_TIME_EXAM) + .withDescription(VALID_DESCRIPTION_EXAM).withAddress(VALID_ADDRESS_EXAM) + .withZoomLink(VALID_ZOOM_EXAM).withTags(VALID_TAG_EXAMS, VALID_TAG_COOL) + .withDeleteAllTags(true).build(); + + + + /** + * Executes the given {@code command}, confirms that
+ * - the returned {@link CommandResult} matches {@code expectedCommandResult}
+ * - the {@code actualModel} matches {@code expectedModel} + */ + public static void assertCommandSuccess(Command command, Model actualModel, CommandResult expectedCommandResult, + Model expectedModel) { + try { + CommandResult result = command.execute(actualModel); + assertEquals(expectedCommandResult, result); + assertEquals(expectedModel, actualModel); + } catch (CommandException ce) { + throw new AssertionError("Execution of command should not fail.", ce); + } + } + + /** + * Convenience wrapper to {@link #assertCommandSuccess(Command, Model, CommandResult, Model)} + * that takes a string {@code expectedMessage}. + */ + public static void assertCommandSuccess(Command command, Model actualModel, String expectedMessage, + Model expectedModel) { + CommandResult expectedCommandResult = new CommandResult(expectedMessage); + assertCommandSuccess(command, actualModel, expectedCommandResult, expectedModel); + } + + /** + * Executes the given {@code command}, confirms that
+ * - a {@code CommandException} is thrown
+ * - the CommandException message matches {@code expectedMessage}
+ * - the address book, filtered contact list and selected contact in {@code actualModel} remain unchanged + */ + public static void assertCommandFailure(Command command, Model actualModel, String expectedMessage) { + // we are unable to defensively copy the model for comparison later, so we can + // only do so by copying its components. + AddressBook expectedAddressBook = new AddressBook(actualModel.getAddressBook()); + List expectedFilteredList = new ArrayList<>(actualModel.getFilteredContactList()); + + assertThrows(CommandException.class, expectedMessage, () -> command.execute(actualModel)); + assertEquals(expectedAddressBook, actualModel.getAddressBook()); + assertEquals(expectedFilteredList, actualModel.getFilteredContactList()); + } + /** + * Updates {@code model}'s filtered list to show only the contact at the given {@code targetIndex} in the + * {@code model}'s address book. + */ + public static void showContactAtIndex(Model model, Index targetIndex) { + assertTrue(targetIndex.getZeroBased() < model.getFilteredContactList().size()); + + Contact contact = model.getFilteredContactList().get(targetIndex.getZeroBased()); + final String[] splitName = contact.getName().fullName.split("\\s+"); + model.updateFilteredContactList(new ContactContainsKeywordsPredicate(Arrays.asList(splitName[0]))); + + assertEquals(1, model.getFilteredContactList().size()); + } + + /** + * Updates {@code model}'s filtered list to show only the event at the given {@code targetIndex} in the + * {@code model}'s address book. + */ + public static void showEventAtIndex(Model model, Index targetIndex) { + assertTrue(targetIndex.getZeroBased() < model.getFilteredEventList().size()); + + Event event = model.getFilteredEventList().get(targetIndex.getZeroBased()); + final String[] splitName = event.getName().fullName.split("\\s+"); + model.updateFilteredEventList(new EventContainsKeywordsPredicate( + Arrays.asList(splitName[0]))); + + assertEquals(1, model.getFilteredEventList().size()); + } +} diff --git a/src/test/java/seedu/address/logic/commands/ExitCommandTest.java b/src/test/java/seedu/address/logic/commands/general/ExitCommandTest.java similarity index 58% rename from src/test/java/seedu/address/logic/commands/ExitCommandTest.java rename to src/test/java/seedu/address/logic/commands/general/ExitCommandTest.java index 9533c473875..3543ca9cfca 100644 --- a/src/test/java/seedu/address/logic/commands/ExitCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/general/ExitCommandTest.java @@ -1,10 +1,11 @@ -package seedu.address.logic.commands; +package seedu.address.logic.commands.general; -import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; -import static seedu.address.logic.commands.ExitCommand.MESSAGE_EXIT_ACKNOWLEDGEMENT; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.general.ExitCommand.MESSAGE_EXIT_ACKNOWLEDGEMENT; import org.junit.jupiter.api.Test; +import seedu.address.logic.commands.CommandResult; import seedu.address.model.Model; import seedu.address.model.ModelManager; @@ -14,7 +15,7 @@ public class ExitCommandTest { @Test public void execute_exit_success() { - CommandResult expectedCommandResult = new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true); + CommandResult expectedCommandResult = new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true, false); assertCommandSuccess(new ExitCommand(), model, expectedCommandResult, expectedModel); } } diff --git a/src/test/java/seedu/address/logic/commands/HelpCommandTest.java b/src/test/java/seedu/address/logic/commands/general/HelpCommandTest.java similarity index 59% rename from src/test/java/seedu/address/logic/commands/HelpCommandTest.java rename to src/test/java/seedu/address/logic/commands/general/HelpCommandTest.java index 4904fc4352e..b3f42267b23 100644 --- a/src/test/java/seedu/address/logic/commands/HelpCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/general/HelpCommandTest.java @@ -1,10 +1,11 @@ -package seedu.address.logic.commands; +package seedu.address.logic.commands.general; -import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; -import static seedu.address.logic.commands.HelpCommand.SHOWING_HELP_MESSAGE; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.general.HelpCommand.SHOWING_HELP_MESSAGE; import org.junit.jupiter.api.Test; +import seedu.address.logic.commands.CommandResult; import seedu.address.model.Model; import seedu.address.model.ModelManager; @@ -14,7 +15,7 @@ public class HelpCommandTest { @Test public void execute_help_success() { - CommandResult expectedCommandResult = new CommandResult(SHOWING_HELP_MESSAGE, true, false); + CommandResult expectedCommandResult = new CommandResult(SHOWING_HELP_MESSAGE, true, false, false); assertCommandSuccess(new HelpCommand(), model, expectedCommandResult, expectedModel); } } diff --git a/src/test/java/seedu/address/logic/commands/general/RedoCommandTest.java b/src/test/java/seedu/address/logic/commands/general/RedoCommandTest.java new file mode 100644 index 00000000000..4cd930f70f5 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/general/RedoCommandTest.java @@ -0,0 +1,68 @@ +package seedu.address.logic.commands.general; + +import static seedu.address.logic.commands.general.CommandTestUtil.EMAIL_DESC_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.NAME_DESC_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.NAME_DESC_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.START_DATE_TIME_DESC_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.general.RedoCommand.MESSAGE_FAIL_TO_REDO; +import static seedu.address.logic.commands.general.RedoCommand.MESSAGE_SUCCESS; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.Logic; +import seedu.address.logic.LogicStub; +import seedu.address.logic.commands.contact.CAddCommand; +import seedu.address.logic.commands.event.EAddCommand; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.testutil.TypicalAddressBook; + +class RedoCommandTest { + + private final RedoCommand redoCommand = new RedoCommand(); + private final AddressBook initialAddressBook = TypicalAddressBook.getTypicalAddressBook(); + private final Model model = new ModelManager(initialAddressBook, new UserPrefs()); + private final Logic logicManager = new LogicStub(model); + + @Test + public void execute_initial_failure() { + assertCommandFailure(redoCommand, model, MESSAGE_FAIL_TO_REDO); + } + + @Test + public void execute_withoutUndo_failure() throws CommandException, ParseException { + logicManager.execute(CAddCommand.COMMAND_WORD + NAME_DESC_AMY + EMAIL_DESC_AMY); + assertCommandFailure(redoCommand, model, MESSAGE_FAIL_TO_REDO); + } + + @Test + public void execute_withUndo_success() throws CommandException, ParseException { + logicManager.execute(CAddCommand.COMMAND_WORD + NAME_DESC_AMY + EMAIL_DESC_AMY); + Model expectedModel = new ModelManager(model.getAddressBook(), model.getUserPrefs()); + logicManager.execute(UndoCommand.COMMAND_WORD); + assertCommandSuccess(redoCommand, model, MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void execute_tooManyRedo_failure() throws CommandException, ParseException { + logicManager.execute(CAddCommand.COMMAND_WORD + NAME_DESC_AMY + EMAIL_DESC_AMY); + logicManager.execute(UndoCommand.COMMAND_WORD); + logicManager.execute(RedoCommand.COMMAND_WORD); + assertCommandFailure(redoCommand, model, MESSAGE_FAIL_TO_REDO); + } + + @Test + public void execute_undoAfterRedo_success() throws CommandException, ParseException { + Model expectedModel = new ModelManager(model.getAddressBook(), model.getUserPrefs()); + logicManager.execute(EAddCommand.COMMAND_WORD + NAME_DESC_TUTORIAL + START_DATE_TIME_DESC_TUTORIAL); + logicManager.execute(UndoCommand.COMMAND_WORD); + logicManager.execute(RedoCommand.COMMAND_WORD); + assertCommandSuccess(new UndoCommand(), model, UndoCommand.MESSAGE_SUCCESS, expectedModel); + } +} diff --git a/src/test/java/seedu/address/logic/commands/general/UndoCommandTest.java b/src/test/java/seedu/address/logic/commands/general/UndoCommandTest.java new file mode 100644 index 00000000000..7a2f1ffe547 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/general/UndoCommandTest.java @@ -0,0 +1,68 @@ +package seedu.address.logic.commands.general; + +import static seedu.address.logic.commands.general.CommandTestUtil.EMAIL_DESC_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.EMPTY_PREFIX_EMAIL; +import static seedu.address.logic.commands.general.CommandTestUtil.EMPTY_PREFIX_START_DATE_TIME; +import static seedu.address.logic.commands.general.CommandTestUtil.NAME_DESC_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.NAME_DESC_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.START_DATE_TIME_DESC_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.general.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.general.UndoCommand.MESSAGE_FAIL_TO_UNDO; +import static seedu.address.logic.commands.general.UndoCommand.MESSAGE_SUCCESS; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.Logic; +import seedu.address.logic.LogicStub; +import seedu.address.logic.commands.contact.CAddCommand; +import seedu.address.logic.commands.contact.CListCommand; +import seedu.address.logic.commands.event.EAddCommand; +import seedu.address.logic.commands.event.EListCommand; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.testutil.TypicalAddressBook; + +class UndoCommandTest { + + private final UndoCommand undoCommand = new UndoCommand(); + private final AddressBook initialAddressBook = TypicalAddressBook.getTypicalAddressBook(); + private final Model model = new ModelManager(initialAddressBook, new UserPrefs()); + private final Logic logicManager = new LogicStub(model); + + @Test + public void execute_initial_failure() { + assertCommandFailure(undoCommand, model, MESSAGE_FAIL_TO_UNDO); + } + + @Test + public void execute_notInitial_success() throws CommandException, ParseException { + Model expectedModel = new ModelManager(initialAddressBook, new UserPrefs()); + logicManager.execute(CAddCommand.COMMAND_WORD + NAME_DESC_AMY + EMAIL_DESC_AMY); + assertCommandSuccess(undoCommand, model, MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void execute_twice_success() throws CommandException, ParseException { + Model expectedModel2 = new ModelManager(initialAddressBook, new UserPrefs()); + logicManager.execute(EAddCommand.COMMAND_WORD + NAME_DESC_EXAM + START_DATE_TIME_DESC_EXAM); + Model expectedModel1 = new ModelManager(model.getAddressBook(), model.getUserPrefs()); + logicManager.execute(CAddCommand.COMMAND_WORD + NAME_DESC_AMY + EMAIL_DESC_AMY); + assertCommandSuccess(undoCommand, model, MESSAGE_SUCCESS, expectedModel1); + assertCommandSuccess(undoCommand, model, MESSAGE_SUCCESS, expectedModel2); + } + + @Test + public void execute_changeDisplay_success() throws CommandException, ParseException { + Model expectedModel = new ModelManager(initialAddressBook, model.getUserPrefs()); + Logic expectedLogicManager = new LogicStub(expectedModel); + expectedLogicManager.execute(CListCommand.COMMAND_WORD + EMPTY_PREFIX_EMAIL); + logicManager.execute(CListCommand.COMMAND_WORD + EMPTY_PREFIX_EMAIL); + logicManager.execute(EListCommand.COMMAND_WORD + EMPTY_PREFIX_START_DATE_TIME); + assertCommandSuccess(undoCommand, model, MESSAGE_SUCCESS, expectedModel); + } +} diff --git a/src/test/java/seedu/address/logic/commands/general/UndoRedoIntegrationTest.java b/src/test/java/seedu/address/logic/commands/general/UndoRedoIntegrationTest.java new file mode 100644 index 00000000000..a15c79d635e --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/general/UndoRedoIntegrationTest.java @@ -0,0 +1,187 @@ +package seedu.address.logic.commands.general; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.general.CommandTestUtil.EMAIL_DESC_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.EMPTY_PREFIX_CONTACT; +import static seedu.address.logic.commands.general.CommandTestUtil.EMPTY_PREFIX_DESCRIPTION; +import static seedu.address.logic.commands.general.CommandTestUtil.NAME_DESC_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_EMAIL_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_INDEX_ONE; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_INDEX_TWO; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_TAG_FRIEND; +import static seedu.address.testutil.TypicalAddressBook.getTypicalAddressBook; + +import java.util.List; +import java.util.function.Predicate; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.Logic; +import seedu.address.logic.LogicStub; +import seedu.address.logic.commands.contact.CAddCommand; +import seedu.address.logic.commands.contact.CDeleteCommand; +import seedu.address.logic.commands.contact.CFindCommand; +import seedu.address.logic.commands.event.ELinkCommand; +import seedu.address.logic.commands.event.EListCommand; +import seedu.address.logic.commands.event.EUnmarkCommand; +import seedu.address.logic.commands.event.EViewCommand; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.ContactContainsKeywordsPredicate; +import seedu.address.model.event.Event; + +class UndoRedoIntegrationTest { + // Usual commands that do not change the display settings + private static final String C_ADD_COMMAND = CAddCommand.COMMAND_WORD + EMAIL_DESC_AMY + + NAME_DESC_AMY + VALID_TAG_FRIEND; + private static final String C_DELETE_COMMAND = CDeleteCommand.COMMAND_WORD + " " + VALID_INDEX_TWO; + private static final String E_UNMARK_COMMAND = EUnmarkCommand.COMMAND_WORD + " " + VALID_INDEX_ONE; + private static final String E_LINK_COMMAND = + ELinkCommand.COMMAND_WORD + " " + VALID_INDEX_TWO + EMPTY_PREFIX_CONTACT + VALID_INDEX_TWO; + + // Commands that change the display settings (predicate) + private static final String C_FIND_COMMAND = CFindCommand.COMMAND_WORD + EMAIL_DESC_AMY; + private static final Predicate CONTACT_FIND_PREDICATE; + + // Commands that change the display settings (EventDisplaySetting) + private static final String E_LIST_COMMAND = EListCommand.COMMAND_WORD + EMPTY_PREFIX_DESCRIPTION; + private static final String E_VIEW_COMMAND = EViewCommand.COMMAND_WORD + " " + VALID_INDEX_TWO; + + // Undo and redo commands + private static final String UNDO_COMMAND = UndoCommand.COMMAND_WORD; + private static final String REDO_COMMAND = RedoCommand.COMMAND_WORD; + + // Commands that will give an error + private static final String INVALID_COMMAND = "a"; + private static final String INVALID_C_ADD_COMMAND = CAddCommand.COMMAND_WORD + NAME_DESC_AMY; + + static { + ContactContainsKeywordsPredicate predicate = new ContactContainsKeywordsPredicate(); + predicate.setEmailKeywords(List.of(VALID_EMAIL_AMY)); + CONTACT_FIND_PREDICATE = predicate; + } + + private final Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private final Logic logicManager = new LogicStub(model); + + private static Predicate getEViewPredicate(Event event) { + return curr -> curr.isSameEvent(event); + } + + private boolean runCommandSuccessful(String command) { + try { + logicManager.execute(command); + return true; + } catch (CommandException | ParseException ignored) { + return false; + } + } + + // Copies the entire model, except the history and the filtered list + private Model copyOfModel(Model model) { + Model result = new ModelManager(model.getAddressBook(), model.getUserPrefs()); + result.setEventDisplaySetting(model.getEventDisplaySetting()); + result.setContactDisplaySetting(model.getContactDisplaySetting()); + return result; + } + + @Test + public void typicalUsage_noChangeInDisplay() { + assertFalse(runCommandSuccessful(UNDO_COMMAND)); + assertFalse(runCommandSuccessful(REDO_COMMAND)); + + assertTrue(runCommandSuccessful(E_UNMARK_COMMAND)); + Model tempModel1 = copyOfModel(model); + + assertTrue(runCommandSuccessful(E_LINK_COMMAND)); + Model tempModel2 = copyOfModel(model); + + assertTrue(runCommandSuccessful(UNDO_COMMAND)); + assertEquals(model, tempModel1); + + assertTrue(runCommandSuccessful(UNDO_COMMAND)); + assertFalse(runCommandSuccessful(UNDO_COMMAND)); + + assertTrue(runCommandSuccessful(REDO_COMMAND)); + assertEquals(model, tempModel1); + + assertTrue(runCommandSuccessful(REDO_COMMAND)); + assertEquals(model, tempModel2); + } + + @Test + public void typicalUsage_changeInDisplay() { + assertTrue(runCommandSuccessful(C_ADD_COMMAND)); + Model tempModel1 = copyOfModel(model); + assertEquals(tempModel1, model); + + assertTrue(runCommandSuccessful(E_LIST_COMMAND)); + Model tempModel2 = copyOfModel(model); + assertEquals(tempModel2, model); + + assertTrue(runCommandSuccessful(C_FIND_COMMAND)); + Model tempModel3 = copyOfModel(model); + tempModel3.updateFilteredContactList(CONTACT_FIND_PREDICATE); + assertEquals(tempModel3, model); + + Event viewEvent = model.getFilteredEventList().get(1); + + assertTrue(runCommandSuccessful(E_VIEW_COMMAND)); + Model tempModel4 = copyOfModel(model); + tempModel4.updateFilteredContactList(CONTACT_FIND_PREDICATE); + tempModel4.updateFilteredEventList(getEViewPredicate(viewEvent)); + assertEquals(tempModel4, model); + + // Series of undo and redo + assertFalse(runCommandSuccessful(REDO_COMMAND)); + assertTrue(runCommandSuccessful(UNDO_COMMAND)); + assertEquals(tempModel3, model); + + assertTrue(runCommandSuccessful(REDO_COMMAND)); + assertEquals(tempModel4, model); + + assertTrue(runCommandSuccessful(UNDO_COMMAND)); + assertEquals(tempModel3, model); + + assertTrue(runCommandSuccessful(UNDO_COMMAND)); + assertEquals(tempModel2, model); + + assertTrue(runCommandSuccessful(UNDO_COMMAND)); + assertEquals(tempModel1, model); + + assertTrue(runCommandSuccessful(UNDO_COMMAND)); + assertEquals(new ModelManager(getTypicalAddressBook(), new UserPrefs()), model); + + assertFalse(runCommandSuccessful(UNDO_COMMAND)); + assertTrue(runCommandSuccessful(REDO_COMMAND)); + + assertTrue(runCommandSuccessful(C_DELETE_COMMAND)); + Model tempModel5 = copyOfModel(model); + + assertFalse(runCommandSuccessful(REDO_COMMAND)); + assertTrue(runCommandSuccessful(UNDO_COMMAND)); + assertTrue(runCommandSuccessful(REDO_COMMAND)); + assertEquals(tempModel5, model); + + } + + @Test + public void invalidCommands() { + assertFalse(runCommandSuccessful(INVALID_COMMAND)); + assertFalse(runCommandSuccessful(UNDO_COMMAND)); + assertFalse(runCommandSuccessful(REDO_COMMAND)); + assertTrue(runCommandSuccessful(E_LINK_COMMAND)); + Model tempModel = copyOfModel(model); + assertFalse(runCommandSuccessful(INVALID_C_ADD_COMMAND)); + assertTrue(runCommandSuccessful(UNDO_COMMAND)); + assertEquals(model, new ModelManager(getTypicalAddressBook(), new UserPrefs())); + assertTrue(runCommandSuccessful(REDO_COMMAND)); + assertEquals(model, tempModel); + } +} diff --git a/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java b/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java deleted file mode 100644 index 5cf487d7ebb..00000000000 --- a/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java +++ /dev/null @@ -1,141 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_BOB; -import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_BOB; -import static seedu.address.logic.commands.CommandTestUtil.INVALID_ADDRESS_DESC; -import static seedu.address.logic.commands.CommandTestUtil.INVALID_EMAIL_DESC; -import static seedu.address.logic.commands.CommandTestUtil.INVALID_NAME_DESC; -import static seedu.address.logic.commands.CommandTestUtil.INVALID_PHONE_DESC; -import static seedu.address.logic.commands.CommandTestUtil.INVALID_TAG_DESC; -import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_BOB; -import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_BOB; -import static seedu.address.logic.commands.CommandTestUtil.PREAMBLE_NON_EMPTY; -import static seedu.address.logic.commands.CommandTestUtil.PREAMBLE_WHITESPACE; -import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_FRIEND; -import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_HUSBAND; -import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_FRIEND; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; -import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; -import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; -import static seedu.address.testutil.TypicalPersons.AMY; -import static seedu.address.testutil.TypicalPersons.BOB; - -import org.junit.jupiter.api.Test; - -import seedu.address.logic.commands.AddCommand; -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; -import seedu.address.testutil.PersonBuilder; - -public class AddCommandParserTest { - private AddCommandParser parser = new AddCommandParser(); - - @Test - public void parse_allFieldsPresent_success() { - Person expectedPerson = new PersonBuilder(BOB).withTags(VALID_TAG_FRIEND).build(); - - // whitespace only preamble - assertParseSuccess(parser, PREAMBLE_WHITESPACE + NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB - + ADDRESS_DESC_BOB + TAG_DESC_FRIEND, new AddCommand(expectedPerson)); - - // multiple names - last name accepted - assertParseSuccess(parser, NAME_DESC_AMY + NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB - + ADDRESS_DESC_BOB + TAG_DESC_FRIEND, new AddCommand(expectedPerson)); - - // multiple phones - last phone accepted - assertParseSuccess(parser, NAME_DESC_BOB + PHONE_DESC_AMY + PHONE_DESC_BOB + EMAIL_DESC_BOB - + ADDRESS_DESC_BOB + TAG_DESC_FRIEND, new AddCommand(expectedPerson)); - - // multiple emails - last email accepted - assertParseSuccess(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_AMY + EMAIL_DESC_BOB - + ADDRESS_DESC_BOB + TAG_DESC_FRIEND, new AddCommand(expectedPerson)); - - // multiple addresses - last address accepted - assertParseSuccess(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_AMY - + ADDRESS_DESC_BOB + TAG_DESC_FRIEND, new AddCommand(expectedPerson)); - - // multiple tags - all accepted - Person expectedPersonMultipleTags = new PersonBuilder(BOB).withTags(VALID_TAG_FRIEND, VALID_TAG_HUSBAND) - .build(); - assertParseSuccess(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB - + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, new AddCommand(expectedPersonMultipleTags)); - } - - @Test - public void parse_optionalFieldsMissing_success() { - // zero tags - Person expectedPerson = new PersonBuilder(AMY).withTags().build(); - assertParseSuccess(parser, NAME_DESC_AMY + PHONE_DESC_AMY + EMAIL_DESC_AMY + ADDRESS_DESC_AMY, - new AddCommand(expectedPerson)); - } - - @Test - public void parse_compulsoryFieldMissing_failure() { - String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE); - - // missing name prefix - assertParseFailure(parser, VALID_NAME_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB, - expectedMessage); - - // missing phone prefix - assertParseFailure(parser, NAME_DESC_BOB + VALID_PHONE_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB, - expectedMessage); - - // missing email prefix - assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + VALID_EMAIL_BOB + ADDRESS_DESC_BOB, - expectedMessage); - - // missing address prefix - assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + VALID_ADDRESS_BOB, - expectedMessage); - - // all prefixes missing - assertParseFailure(parser, VALID_NAME_BOB + VALID_PHONE_BOB + VALID_EMAIL_BOB + VALID_ADDRESS_BOB, - expectedMessage); - } - - @Test - public void parse_invalidValue_failure() { - // invalid name - assertParseFailure(parser, INVALID_NAME_DESC + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB - + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Name.MESSAGE_CONSTRAINTS); - - // invalid phone - assertParseFailure(parser, NAME_DESC_BOB + INVALID_PHONE_DESC + EMAIL_DESC_BOB + ADDRESS_DESC_BOB - + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Phone.MESSAGE_CONSTRAINTS); - - // invalid email - assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + INVALID_EMAIL_DESC + ADDRESS_DESC_BOB - + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Email.MESSAGE_CONSTRAINTS); - - // invalid address - assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + INVALID_ADDRESS_DESC - + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Address.MESSAGE_CONSTRAINTS); - - // invalid tag - assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB - + INVALID_TAG_DESC + VALID_TAG_FRIEND, Tag.MESSAGE_CONSTRAINTS); - - // two invalid values, only first invalid value reported - assertParseFailure(parser, INVALID_NAME_DESC + PHONE_DESC_BOB + EMAIL_DESC_BOB + INVALID_ADDRESS_DESC, - Name.MESSAGE_CONSTRAINTS); - - // non-empty preamble - assertParseFailure(parser, PREAMBLE_NON_EMPTY + NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB - + ADDRESS_DESC_BOB + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, - String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); - } -} diff --git a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java index d9659205b57..d5e454fd748 100644 --- a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java +++ b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java @@ -5,7 +5,7 @@ import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; import static seedu.address.testutil.Assert.assertThrows; -import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST; import java.util.Arrays; import java.util.List; @@ -13,67 +13,181 @@ import org.junit.jupiter.api.Test; -import seedu.address.logic.commands.AddCommand; -import seedu.address.logic.commands.ClearCommand; -import seedu.address.logic.commands.DeleteCommand; -import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; -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.commons.core.range.Range; +import seedu.address.logic.commands.contact.CAddCommand; +import seedu.address.logic.commands.contact.CClearCommand; +import seedu.address.logic.commands.contact.CDeleteCommand; +import seedu.address.logic.commands.contact.CEditCommand; +import seedu.address.logic.commands.contact.CFindCommand; +import seedu.address.logic.commands.contact.CListCommand; +import seedu.address.logic.commands.contact.CMarkCommand; +import seedu.address.logic.commands.contact.CUnmarkCommand; +import seedu.address.logic.commands.contact.CViewCommand; +import seedu.address.logic.commands.event.EAddCommand; +import seedu.address.logic.commands.event.EClearCommand; +import seedu.address.logic.commands.event.EDeleteCommand; +import seedu.address.logic.commands.event.EEditCommand; +import seedu.address.logic.commands.event.EFindCommand; +import seedu.address.logic.commands.event.ELinkCommand; +import seedu.address.logic.commands.event.EListCommand; +import seedu.address.logic.commands.event.EMarkCommand; +import seedu.address.logic.commands.event.ESortCommand; +import seedu.address.logic.commands.event.EUnlinkCommand; +import seedu.address.logic.commands.event.EUnmarkCommand; +import seedu.address.logic.commands.event.EViewCommand; +import seedu.address.logic.commands.general.CalendarCommand; +import seedu.address.logic.commands.general.ExitCommand; +import seedu.address.logic.commands.general.HelpCommand; +import seedu.address.logic.commands.general.RedoCommand; +import seedu.address.logic.commands.general.UndoCommand; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; -import seedu.address.model.person.Person; -import seedu.address.testutil.EditPersonDescriptorBuilder; -import seedu.address.testutil.PersonBuilder; -import seedu.address.testutil.PersonUtil; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.ContactContainsKeywordsPredicate; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventContainsKeywordsPredicate; +import seedu.address.testutil.ContactBuilder; +import seedu.address.testutil.ContactUtil; +import seedu.address.testutil.EditContactDescriptorBuilder; +import seedu.address.testutil.EditEventDescriptorBuilder; +import seedu.address.testutil.EventBuilder; +import seedu.address.testutil.EventUtil; public class AddressBookParserTest { private final AddressBookParser parser = new AddressBookParser(); @Test - public void parseCommand_add() throws Exception { - Person person = new PersonBuilder().build(); - AddCommand command = (AddCommand) parser.parseCommand(PersonUtil.getAddCommand(person)); - assertEquals(new AddCommand(person), command); + public void parseCommand_cadd() throws Exception { + Contact contact = new ContactBuilder().withLinkedEvents().withRandomUuid().withMarked(false).build(); + CAddCommand command = (CAddCommand) parser.parseCommand(ContactUtil.getCAddCommand(contact)); + assertEquals(new CAddCommand(contact), command); } @Test - public void parseCommand_clear() throws Exception { - assertTrue(parser.parseCommand(ClearCommand.COMMAND_WORD) instanceof ClearCommand); - assertTrue(parser.parseCommand(ClearCommand.COMMAND_WORD + " 3") instanceof ClearCommand); + public void parseCommand_eadd() throws Exception { + Event event = new EventBuilder().withMarked(false).build(); + EAddCommand command = (EAddCommand) parser.parseCommand(EventUtil.getEAddCommand(event)); + assertEquals(new EAddCommand(event), command); } @Test - public void parseCommand_delete() throws Exception { - DeleteCommand command = (DeleteCommand) parser.parseCommand( - DeleteCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased()); - assertEquals(new DeleteCommand(INDEX_FIRST_PERSON), command); + public void parseCommand_cclear() throws Exception { + assertTrue(parser.parseCommand(CClearCommand.COMMAND_WORD) instanceof CClearCommand); + assertTrue(parser.parseCommand(CClearCommand.COMMAND_WORD + " 3") instanceof CClearCommand); } @Test - public void parseCommand_edit() throws Exception { - Person person = new PersonBuilder().build(); - EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder(person).build(); - EditCommand command = (EditCommand) parser.parseCommand(EditCommand.COMMAND_WORD + " " - + INDEX_FIRST_PERSON.getOneBased() + " " + PersonUtil.getEditPersonDescriptorDetails(descriptor)); - assertEquals(new EditCommand(INDEX_FIRST_PERSON, descriptor), command); + public void parseCommand_eclear() throws Exception { + assertTrue(parser.parseCommand(EClearCommand.COMMAND_WORD) instanceof EClearCommand); + assertTrue(parser.parseCommand(EClearCommand.COMMAND_WORD + " 2") instanceof EClearCommand); } @Test - public void parseCommand_exit() throws Exception { - assertTrue(parser.parseCommand(ExitCommand.COMMAND_WORD) instanceof ExitCommand); - assertTrue(parser.parseCommand(ExitCommand.COMMAND_WORD + " 3") instanceof ExitCommand); + public void parseCommand_cdelete() throws Exception { + CDeleteCommand command = (CDeleteCommand) parser.parseCommand( + CDeleteCommand.COMMAND_WORD + " " + INDEX_FIRST.getOneBased()); + Range rangeOfIndexes = Range.convertFromIndex(INDEX_FIRST); + assertEquals(new CDeleteCommand(rangeOfIndexes), command); + } + + @Test + public void parseCommand_edelete() throws Exception { + EDeleteCommand command = (EDeleteCommand) parser.parseCommand( + EDeleteCommand.COMMAND_WORD + " " + INDEX_FIRST.getOneBased()); + Range rangeOfIndexes = Range.convertFromIndex(INDEX_FIRST); + assertEquals(new EDeleteCommand(rangeOfIndexes), command); } @Test - public void parseCommand_find() throws Exception { + public void parseCommand_cedit() throws Exception { + Contact contact = new ContactBuilder().build(); + CEditCommand.EditContactDescriptor descriptor = new EditContactDescriptorBuilder(contact, + null, false).build(); + CEditCommand command = (CEditCommand) parser.parseCommand(CEditCommand.COMMAND_WORD + " " + + INDEX_FIRST.getOneBased() + " " + ContactUtil.getEditContactDescriptorDetails(descriptor)); + assertEquals(new CEditCommand(INDEX_FIRST, descriptor), command); + } + + @Test + public void parseCommand_eedit() throws Exception { + Event event = new EventBuilder().build(); + EEditCommand.EditEventDescriptor descriptor = new EditEventDescriptorBuilder(event, null, false).build(); + EEditCommand commmand = (EEditCommand) parser.parseCommand(EEditCommand.COMMAND_WORD + " " + + INDEX_FIRST.getOneBased() + " " + EventUtil.getEditEventDescriptorDetails(descriptor)); + assertEquals(new EEditCommand(INDEX_FIRST, descriptor), commmand); + } + + @Test + public void parseCommand_cfind() throws Exception { List keywords = Arrays.asList("foo", "bar", "baz"); - FindCommand command = (FindCommand) parser.parseCommand( - FindCommand.COMMAND_WORD + " " + keywords.stream().collect(Collectors.joining(" "))); - assertEquals(new FindCommand(new NameContainsKeywordsPredicate(keywords)), command); + CFindCommand command = (CFindCommand) parser.parseCommand( + CFindCommand.COMMAND_WORD + " " + keywords.stream().collect(Collectors.joining(" "))); + assertEquals(new CFindCommand(new ContactContainsKeywordsPredicate(keywords)), command); + } + + @Test + public void parseCommand_efind() throws Exception { + List keywords = Arrays.asList("foo", "bar", "baz"); + EFindCommand command = (EFindCommand) parser.parseCommand( + EFindCommand.COMMAND_WORD + " " + keywords.stream().collect(Collectors.joining(" "))); + assertEquals(new EFindCommand(new EventContainsKeywordsPredicate(keywords)), command); + } + + @Test + public void parseCommand_clist() throws Exception { + assertTrue(parser.parseCommand(CListCommand.COMMAND_WORD) instanceof CListCommand); + } + @Test + public void parseCommand_elist() throws Exception { + assertTrue(parser.parseCommand(EListCommand.COMMAND_WORD) instanceof EListCommand); + } + + @Test + public void parseCommand_cview() throws Exception { + assertTrue(parser.parseCommand(CViewCommand.COMMAND_WORD + " 1") instanceof CViewCommand); + } + @Test + public void parseCommand_eview() throws Exception { + assertTrue(parser.parseCommand(EViewCommand.COMMAND_WORD + " 1") instanceof EViewCommand); + } + + @Test + public void parseCommand_esort() throws Exception { + assertTrue(parser.parseCommand(ESortCommand.COMMAND_WORD) instanceof ESortCommand); + } + + @Test + public void parseCommand_cmark() throws Exception { + assertTrue(parser.parseCommand(CMarkCommand.COMMAND_WORD + " 1") instanceof CMarkCommand); + } + @Test + public void parseCommand_emark() throws Exception { + assertTrue(parser.parseCommand(EMarkCommand.COMMAND_WORD + " 1") instanceof EMarkCommand); + } + + @Test + public void parseCommand_cunmark() throws Exception { + assertTrue(parser.parseCommand(CUnmarkCommand.COMMAND_WORD + " 1") instanceof CUnmarkCommand); + } + @Test + public void parseCommand_eunmark() throws Exception { + assertTrue(parser.parseCommand(EUnmarkCommand.COMMAND_WORD + " 1") instanceof EUnmarkCommand); + } + + @Test + public void parseCommand_elink() throws Exception { + assertTrue(parser.parseCommand(ELinkCommand.COMMAND_WORD + " 1 c/3 c/4") instanceof ELinkCommand); + } + + @Test + public void parseCommand_eunlink() throws Exception { + assertTrue(parser.parseCommand(EUnlinkCommand.COMMAND_WORD + " 1 c/3 c/4") instanceof EUnlinkCommand); + } + + @Test + public void parseCommand_exit() throws Exception { + assertTrue(parser.parseCommand(ExitCommand.COMMAND_WORD) instanceof ExitCommand); + assertTrue(parser.parseCommand(ExitCommand.COMMAND_WORD + " 3") instanceof ExitCommand); } @Test @@ -83,9 +197,21 @@ public void parseCommand_help() throws Exception { } @Test - public void parseCommand_list() throws Exception { - assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD) instanceof ListCommand); - assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD + " 3") instanceof ListCommand); + public void parseCommand_calendar() throws Exception { + assertTrue(parser.parseCommand(CalendarCommand.COMMAND_WORD) instanceof CalendarCommand); + assertTrue(parser.parseCommand(CalendarCommand.COMMAND_WORD + " 4") instanceof CalendarCommand); + } + + @Test + public void parseCommand_undo() throws Exception { + assertTrue(parser.parseCommand(UndoCommand.COMMAND_WORD) instanceof UndoCommand); + assertTrue(parser.parseCommand(UndoCommand.COMMAND_WORD + " 1") instanceof UndoCommand); + } + + @Test + public void parseCommand_redo() throws Exception { + assertTrue(parser.parseCommand(RedoCommand.COMMAND_WORD) instanceof RedoCommand); + assertTrue(parser.parseCommand(RedoCommand.COMMAND_WORD + " 1") instanceof RedoCommand); } @Test diff --git a/src/test/java/seedu/address/logic/parser/DeleteCommandParserTest.java b/src/test/java/seedu/address/logic/parser/DeleteCommandParserTest.java deleted file mode 100644 index 27eaec84450..00000000000 --- a/src/test/java/seedu/address/logic/parser/DeleteCommandParserTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; -import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; -import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; - -import org.junit.jupiter.api.Test; - -import seedu.address.logic.commands.DeleteCommand; - -/** - * As we are only doing white-box testing, our test cases do not cover path variations - * outside of the DeleteCommand code. For example, inputs "1" and "1 abc" take the - * same path through the DeleteCommand, and therefore we test only one of them. - * The path variation for those two cases occur inside the ParserUtil, and - * therefore should be covered by the ParserUtilTest. - */ -public class DeleteCommandParserTest { - - private DeleteCommandParser parser = new DeleteCommandParser(); - - @Test - public void parse_validArgs_returnsDeleteCommand() { - assertParseSuccess(parser, "1", new DeleteCommand(INDEX_FIRST_PERSON)); - } - - @Test - public void parse_invalidArgs_throwsParseException() { - assertParseFailure(parser, "a", String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); - } -} diff --git a/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java b/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java deleted file mode 100644 index 2ff31522486..00000000000 --- a/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java +++ /dev/null @@ -1,211 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_BOB; -import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_BOB; -import static seedu.address.logic.commands.CommandTestUtil.INVALID_ADDRESS_DESC; -import static seedu.address.logic.commands.CommandTestUtil.INVALID_EMAIL_DESC; -import static seedu.address.logic.commands.CommandTestUtil.INVALID_NAME_DESC; -import static seedu.address.logic.commands.CommandTestUtil.INVALID_PHONE_DESC; -import static seedu.address.logic.commands.CommandTestUtil.INVALID_TAG_DESC; -import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_BOB; -import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_FRIEND; -import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_HUSBAND; -import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_AMY; -import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_AMY; -import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_AMY; -import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_AMY; -import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_FRIEND; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; -import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; -import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; -import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; -import static seedu.address.testutil.TypicalIndexes.INDEX_THIRD_PERSON; - -import org.junit.jupiter.api.Test; - -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; -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; -import seedu.address.testutil.EditPersonDescriptorBuilder; - -public class EditCommandParserTest { - - private static final String TAG_EMPTY = " " + PREFIX_TAG; - - private static final String MESSAGE_INVALID_FORMAT = - String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE); - - private EditCommandParser parser = new EditCommandParser(); - - @Test - public void parse_missingParts_failure() { - // no index specified - assertParseFailure(parser, VALID_NAME_AMY, MESSAGE_INVALID_FORMAT); - - // no field specified - assertParseFailure(parser, "1", EditCommand.MESSAGE_NOT_EDITED); - - // no index and no field specified - assertParseFailure(parser, "", MESSAGE_INVALID_FORMAT); - } - - @Test - public void parse_invalidPreamble_failure() { - // negative index - assertParseFailure(parser, "-5" + NAME_DESC_AMY, MESSAGE_INVALID_FORMAT); - - // zero index - assertParseFailure(parser, "0" + NAME_DESC_AMY, MESSAGE_INVALID_FORMAT); - - // invalid arguments being parsed as preamble - assertParseFailure(parser, "1 some random string", MESSAGE_INVALID_FORMAT); - - // invalid prefix being parsed as preamble - assertParseFailure(parser, "1 i/ string", MESSAGE_INVALID_FORMAT); - } - - @Test - public void parse_invalidValue_failure() { - assertParseFailure(parser, "1" + INVALID_NAME_DESC, Name.MESSAGE_CONSTRAINTS); // invalid name - assertParseFailure(parser, "1" + INVALID_PHONE_DESC, Phone.MESSAGE_CONSTRAINTS); // invalid phone - assertParseFailure(parser, "1" + INVALID_EMAIL_DESC, Email.MESSAGE_CONSTRAINTS); // invalid email - assertParseFailure(parser, "1" + INVALID_ADDRESS_DESC, Address.MESSAGE_CONSTRAINTS); // invalid address - assertParseFailure(parser, "1" + INVALID_TAG_DESC, Tag.MESSAGE_CONSTRAINTS); // invalid tag - - // invalid phone followed by valid email - assertParseFailure(parser, "1" + INVALID_PHONE_DESC + EMAIL_DESC_AMY, Phone.MESSAGE_CONSTRAINTS); - - // valid phone followed by invalid phone. The test case for invalid phone followed by valid phone - // is tested at {@code parse_invalidValueFollowedByValidValue_success()} - assertParseFailure(parser, "1" + PHONE_DESC_BOB + INVALID_PHONE_DESC, Phone.MESSAGE_CONSTRAINTS); - - // while parsing {@code PREFIX_TAG} alone will reset the tags of the {@code Person} being edited, - // parsing it together with a valid tag results in error - assertParseFailure(parser, "1" + TAG_DESC_FRIEND + TAG_DESC_HUSBAND + TAG_EMPTY, Tag.MESSAGE_CONSTRAINTS); - assertParseFailure(parser, "1" + TAG_DESC_FRIEND + TAG_EMPTY + TAG_DESC_HUSBAND, Tag.MESSAGE_CONSTRAINTS); - assertParseFailure(parser, "1" + TAG_EMPTY + TAG_DESC_FRIEND + TAG_DESC_HUSBAND, Tag.MESSAGE_CONSTRAINTS); - - // multiple invalid values, but only the first invalid value is captured - assertParseFailure(parser, "1" + INVALID_NAME_DESC + INVALID_EMAIL_DESC + VALID_ADDRESS_AMY + VALID_PHONE_AMY, - Name.MESSAGE_CONSTRAINTS); - } - - @Test - public void parse_allFieldsSpecified_success() { - Index targetIndex = INDEX_SECOND_PERSON; - String userInput = targetIndex.getOneBased() + PHONE_DESC_BOB + TAG_DESC_HUSBAND - + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + NAME_DESC_AMY + TAG_DESC_FRIEND; - - EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withName(VALID_NAME_AMY) - .withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY) - .withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).build(); - EditCommand expectedCommand = new EditCommand(targetIndex, descriptor); - - assertParseSuccess(parser, userInput, expectedCommand); - } - - @Test - public void parse_someFieldsSpecified_success() { - Index targetIndex = INDEX_FIRST_PERSON; - String userInput = targetIndex.getOneBased() + PHONE_DESC_BOB + EMAIL_DESC_AMY; - - EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withPhone(VALID_PHONE_BOB) - .withEmail(VALID_EMAIL_AMY).build(); - EditCommand expectedCommand = new EditCommand(targetIndex, descriptor); - - assertParseSuccess(parser, userInput, expectedCommand); - } - - @Test - public void parse_oneFieldSpecified_success() { - // name - Index targetIndex = INDEX_THIRD_PERSON; - String userInput = targetIndex.getOneBased() + NAME_DESC_AMY; - EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withName(VALID_NAME_AMY).build(); - EditCommand expectedCommand = new EditCommand(targetIndex, descriptor); - assertParseSuccess(parser, userInput, expectedCommand); - - // phone - userInput = targetIndex.getOneBased() + PHONE_DESC_AMY; - descriptor = new EditPersonDescriptorBuilder().withPhone(VALID_PHONE_AMY).build(); - expectedCommand = new EditCommand(targetIndex, descriptor); - assertParseSuccess(parser, userInput, expectedCommand); - - // email - userInput = targetIndex.getOneBased() + EMAIL_DESC_AMY; - descriptor = new EditPersonDescriptorBuilder().withEmail(VALID_EMAIL_AMY).build(); - expectedCommand = new EditCommand(targetIndex, descriptor); - assertParseSuccess(parser, userInput, expectedCommand); - - // address - userInput = targetIndex.getOneBased() + ADDRESS_DESC_AMY; - descriptor = new EditPersonDescriptorBuilder().withAddress(VALID_ADDRESS_AMY).build(); - expectedCommand = new EditCommand(targetIndex, descriptor); - assertParseSuccess(parser, userInput, expectedCommand); - - // tags - userInput = targetIndex.getOneBased() + TAG_DESC_FRIEND; - descriptor = new EditPersonDescriptorBuilder().withTags(VALID_TAG_FRIEND).build(); - expectedCommand = new EditCommand(targetIndex, descriptor); - assertParseSuccess(parser, userInput, expectedCommand); - } - - @Test - public void parse_multipleRepeatedFields_acceptsLast() { - Index targetIndex = INDEX_FIRST_PERSON; - String userInput = targetIndex.getOneBased() + PHONE_DESC_AMY + ADDRESS_DESC_AMY + EMAIL_DESC_AMY - + TAG_DESC_FRIEND + PHONE_DESC_AMY + ADDRESS_DESC_AMY + EMAIL_DESC_AMY + TAG_DESC_FRIEND - + PHONE_DESC_BOB + ADDRESS_DESC_BOB + EMAIL_DESC_BOB + TAG_DESC_HUSBAND; - - EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withPhone(VALID_PHONE_BOB) - .withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_FRIEND, VALID_TAG_HUSBAND) - .build(); - EditCommand expectedCommand = new EditCommand(targetIndex, descriptor); - - assertParseSuccess(parser, userInput, expectedCommand); - } - - @Test - public void parse_invalidValueFollowedByValidValue_success() { - // no other valid values specified - Index targetIndex = INDEX_FIRST_PERSON; - String userInput = targetIndex.getOneBased() + INVALID_PHONE_DESC + PHONE_DESC_BOB; - EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withPhone(VALID_PHONE_BOB).build(); - EditCommand expectedCommand = new EditCommand(targetIndex, descriptor); - assertParseSuccess(parser, userInput, expectedCommand); - - // other valid values specified - userInput = targetIndex.getOneBased() + EMAIL_DESC_BOB + INVALID_PHONE_DESC + ADDRESS_DESC_BOB - + PHONE_DESC_BOB; - descriptor = new EditPersonDescriptorBuilder().withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_BOB) - .withAddress(VALID_ADDRESS_BOB).build(); - expectedCommand = new EditCommand(targetIndex, descriptor); - assertParseSuccess(parser, userInput, expectedCommand); - } - - @Test - public void parse_resetTags_success() { - Index targetIndex = INDEX_THIRD_PERSON; - String userInput = targetIndex.getOneBased() + TAG_EMPTY; - - EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withTags().build(); - EditCommand expectedCommand = new EditCommand(targetIndex, descriptor); - - assertParseSuccess(parser, userInput, expectedCommand); - } -} diff --git a/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java b/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java deleted file mode 100644 index 70f4f0e79c4..00000000000 --- a/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; -import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; - -import java.util.Arrays; - -import org.junit.jupiter.api.Test; - -import seedu.address.logic.commands.FindCommand; -import seedu.address.model.person.NameContainsKeywordsPredicate; - -public class FindCommandParserTest { - - private FindCommandParser parser = new FindCommandParser(); - - @Test - public void parse_emptyArg_throwsParseException() { - assertParseFailure(parser, " ", String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); - } - - @Test - public void parse_validArgs_returnsFindCommand() { - // no leading and trailing whitespaces - FindCommand expectedFindCommand = - new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList("Alice", "Bob"))); - assertParseSuccess(parser, "Alice Bob", expectedFindCommand); - - // multiple whitespaces between keywords - assertParseSuccess(parser, " \n Alice \n \t Bob \t", expectedFindCommand); - } - -} diff --git a/src/test/java/seedu/address/logic/parser/ParserUtilTest.java b/src/test/java/seedu/address/logic/parser/ParserUtilTest.java index 4256788b1a7..e9532faf5b2 100644 --- a/src/test/java/seedu/address/logic/parser/ParserUtilTest.java +++ b/src/test/java/seedu/address/logic/parser/ParserUtilTest.java @@ -2,22 +2,28 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.logic.parser.ParserUtil.MESSAGE_INVALID_INDEX; import static seedu.address.testutil.Assert.assertThrows; -import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; import org.junit.jupiter.api.Test; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.core.range.Range; 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.common.Address; +import seedu.address.model.common.Name; +import seedu.address.model.common.ZoomLink; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Phone; +import seedu.address.model.contact.TelegramHandle; +import seedu.address.model.event.Description; +import seedu.address.model.event.EndDateTime; +import seedu.address.model.event.StartDateTime; import seedu.address.model.tag.Tag; public class ParserUtilTest { @@ -26,6 +32,13 @@ public class ParserUtilTest { private static final String INVALID_ADDRESS = " "; private static final String INVALID_EMAIL = "example.com"; private static final String INVALID_TAG = "#friend"; + private static final String INVALID_START_DATE_TIME = "20/11/2021 11:00"; + private static final String INVALID_END_DATE_TIME = "20/11/2021 15:00"; + private static final String INVALID_DESCRIPTION = " "; + private static final String INVALID_TELEGRAM = "my%Telegram"; + private static final String INVALID_ZOOM_LINK = ""; + private static final String INVALID_DELETE_ARGUMENT = "abc"; + private static final String INVALID_INDEX = "-1"; private static final String VALID_NAME = "Rachel Walker"; private static final String VALID_PHONE = "123456"; @@ -33,50 +46,40 @@ public class ParserUtilTest { private static final String VALID_EMAIL = "rachel@example.com"; private static final String VALID_TAG_1 = "friend"; private static final String VALID_TAG_2 = "neighbour"; + private static final String VALID_START_DATE_TIME = "20-11-2021 11:00"; + private static final String VALID_END_DATE_TIME = "20-11-2021 15:00"; + private static final String VALID_TELEGRAM = "my_Telegram"; + private static final String VALID_ZOOM_LINK = "my-zoom-link/tutorial.nus.edu"; + private static final String VALID_DESCRIPTION = "This is a description!"; + private static final String VALID_INDEX = "1"; + private static final String VALID_RANGE = "1-2"; + private static final String VALID_TWO_DIGIT_INDEX = "11"; + private static final String VALID_THREE_DIGIT_INDEX = "111"; + private static final String INDEX_WITH_ZERO_INFRONT = "01"; private static final String WHITESPACE = " \t\r\n"; - @Test - public void parseIndex_invalidInput_throwsParseException() { - assertThrows(ParseException.class, () -> ParserUtil.parseIndex("10 a")); - } - - @Test - public void parseIndex_outOfRangeInput_throwsParseException() { - assertThrows(ParseException.class, MESSAGE_INVALID_INDEX, () - -> ParserUtil.parseIndex(Long.toString(Integer.MAX_VALUE + 1))); - } - - @Test - public void parseIndex_validInput_success() throws Exception { - // No whitespaces - assertEquals(INDEX_FIRST_PERSON, ParserUtil.parseIndex("1")); - - // Leading and trailing whitespaces - assertEquals(INDEX_FIRST_PERSON, ParserUtil.parseIndex(" 1 ")); - } - @Test public void parseName_null_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> ParserUtil.parseName((String) null)); + assertThrows(NullPointerException.class, () -> ParserUtil.parseContactName((String) null)); } @Test public void parseName_invalidValue_throwsParseException() { - assertThrows(ParseException.class, () -> ParserUtil.parseName(INVALID_NAME)); + assertThrows(ParseException.class, () -> ParserUtil.parseContactName(INVALID_NAME)); } @Test public void parseName_validValueWithoutWhitespace_returnsName() throws Exception { Name expectedName = new Name(VALID_NAME); - assertEquals(expectedName, ParserUtil.parseName(VALID_NAME)); + assertEquals(expectedName, ParserUtil.parseContactName(VALID_NAME)); } @Test public void parseName_validValueWithWhitespace_returnsTrimmedName() throws Exception { String nameWithWhitespace = WHITESPACE + VALID_NAME + WHITESPACE; Name expectedName = new Name(VALID_NAME); - assertEquals(expectedName, ParserUtil.parseName(nameWithWhitespace)); + assertEquals(expectedName, ParserUtil.parseContactName(nameWithWhitespace)); } @Test @@ -193,4 +196,164 @@ public void parseTags_collectionWithValidTags_returnsTagSet() throws Exception { assertEquals(expectedTagSet, actualTagSet); } + + @Test + public void parseZoomLink_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> ParserUtil.parseZoomLink((String) null)); + } + + @Test + public void parseZoomLink_invalidValue_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.parseZoomLink(INVALID_ZOOM_LINK)); + } + + @Test + public void parseZoomLink_validValueWithoutWhitespace_returnsZoomLink() throws Exception { + ZoomLink expectedZoomLink = new ZoomLink(VALID_ZOOM_LINK); + assertEquals(expectedZoomLink, ParserUtil.parseZoomLink(VALID_ZOOM_LINK)); + } + + @Test + public void parseZoomLink_validValueWithWhitespace_returnsTrimmedZoomLink() throws Exception { + String zoomLinkWithWhitespace = WHITESPACE + VALID_ZOOM_LINK + WHITESPACE; + ZoomLink expectedZoomLink = new ZoomLink(VALID_ZOOM_LINK); + assertEquals(expectedZoomLink, ParserUtil.parseZoomLink(zoomLinkWithWhitespace)); + } + + @Test + public void parseTelegram_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> ParserUtil.parseTelegram((String) null)); + } + + @Test + public void parseTelegram_invalidValue_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.parseTelegram(INVALID_TELEGRAM)); + } + + @Test + public void parseTelegram_validValueWithoutWhitespace_returnsTelegram() throws Exception { + TelegramHandle expectedTelegramHandle = new TelegramHandle(VALID_TELEGRAM); + assertEquals(expectedTelegramHandle, ParserUtil.parseTelegram(VALID_TELEGRAM)); + } + + @Test + public void parseTelegram_validValueWithWhitespace_returnsTrimmedTelegram() throws Exception { + String telegramWithWhitespace = WHITESPACE + VALID_TELEGRAM + WHITESPACE; + TelegramHandle expectedTelegram = new TelegramHandle(VALID_TELEGRAM); + assertEquals(expectedTelegram, ParserUtil.parseTelegram(telegramWithWhitespace)); + } + + @Test + public void parseStartDateTime_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> ParserUtil.parseStartDateTime((String) null)); + } + + @Test + public void parseStartDateTime_invalidValue_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.parseStartDateTime(INVALID_START_DATE_TIME)); + } + + @Test + public void parseStartDateTime_validValueWithoutWhitespace_returnsStartDateTime() throws Exception { + StartDateTime expectedStartDateTime = new StartDateTime(VALID_START_DATE_TIME); + assertEquals(expectedStartDateTime, ParserUtil.parseStartDateTime(VALID_START_DATE_TIME)); + } + + @Test + public void parseStartDateTime_validValueWithWhitespace_returnsTrimmedStartDateTime() throws Exception { + String startDateTimeWithWhitespace = WHITESPACE + VALID_START_DATE_TIME + WHITESPACE; + StartDateTime expectedStartDateTime = new StartDateTime(VALID_START_DATE_TIME); + assertEquals(expectedStartDateTime, ParserUtil.parseStartDateTime(startDateTimeWithWhitespace)); + } + + + @Test + public void parseEndDateTime_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> ParserUtil.parseEndDateTime((String) null)); + } + + @Test + public void parseEndDateTime_invalidValue_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.parseEndDateTime(INVALID_END_DATE_TIME)); + } + + @Test + public void parseEndDateTime_validValueWithoutWhitespace_returnsEndDateTime() throws Exception { + EndDateTime expectedEndDateTime = new EndDateTime(VALID_END_DATE_TIME); + assertEquals(expectedEndDateTime, ParserUtil.parseEndDateTime(VALID_END_DATE_TIME)); + } + + @Test + public void parseEndDateTime_validValueWithWhitespace_returnsTrimmedEndDateTime() throws Exception { + String endDateTimeWithWhitespace = WHITESPACE + VALID_END_DATE_TIME + WHITESPACE; + EndDateTime expectedEndDateTime = new EndDateTime(VALID_END_DATE_TIME); + assertEquals(expectedEndDateTime, ParserUtil.parseEndDateTime(endDateTimeWithWhitespace)); + } + + @Test + public void parseDescription_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> ParserUtil.parseDescription((String) null)); + } + + @Test + public void parseDescription_invalidValue_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.parseDescription(INVALID_DESCRIPTION)); + } + + @Test + public void parseDescription_validValueWithoutWhitespace_returnsDescription() throws Exception { + Description expectedDescription = new Description(VALID_DESCRIPTION); + assertEquals(expectedDescription, ParserUtil.parseDescription(VALID_DESCRIPTION)); + } + + @Test + public void parseDescription_validValueWithWhitespace_returnsTrimmedDescription() throws Exception { + String descriptionWithWhitespace = WHITESPACE + VALID_DESCRIPTION + WHITESPACE; + Description expectedDescription = new Description(VALID_DESCRIPTION); + assertEquals(expectedDescription, ParserUtil.parseDescription(descriptionWithWhitespace)); + } + + @Test + public void parseIndex_validArguementWithWhiteSpace_returnsIndex() throws Exception { + Index expectedIndex = Index.fromOneBased(1); + String indexWithWhiteSpace = WHITESPACE + VALID_INDEX + WHITESPACE; + String indexWithZero = WHITESPACE + INDEX_WITH_ZERO_INFRONT; + assertEquals(expectedIndex, ParserUtil.parseIndex(indexWithWhiteSpace)); + assertEquals(expectedIndex, ParserUtil.parseIndex(indexWithZero)); + } + + @Test + public void parseIndex_invalidValue_throwsParseException() throws Exception { + assertThrows(ParseException.class, () -> ParserUtil.parseIndex(INVALID_INDEX)); + } + + @Test + public void parseDeleteArgument_validValueWithWhitespace_returnsRange() throws Exception { + String indexWithWhitespace = WHITESPACE + VALID_INDEX + WHITESPACE; + Range expectedRangeFromIndex = Range.convertFromIndex(Index.fromOneBased(1)); + assertEquals(expectedRangeFromIndex, ParserUtil.parseDeleteArgument(indexWithWhitespace)); + + String rangeWithWhitespace = WHITESPACE + VALID_RANGE + WHITESPACE; + Range expectedRange = new Range(Index.fromOneBased(1), Index.fromOneBased(2)); + assertEquals(expectedRange, ParserUtil.parseDeleteArgument(rangeWithWhitespace)); + } + + @Test + public void parseDeleteArgument_invalidValue_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.parseDeleteArgument(INVALID_DELETE_ARGUMENT)); + } + + @Test + public void parseMarkIndexes_validValueWithWhiteSpace_returnsListOfIndex() throws Exception { + String indexWithWhiteSpace = WHITESPACE + VALID_INDEX + WHITESPACE + WHITESPACE + VALID_TWO_DIGIT_INDEX + + WHITESPACE + VALID_THREE_DIGIT_INDEX + WHITESPACE; + List expectedIndexes = List.of(Index.fromOneBased(1), Index.fromOneBased(11), Index.fromOneBased(111)); + assertEquals(expectedIndexes, ParserUtil.parseMarkIndexes(indexWithWhiteSpace)); + } + + @Test + public void parseMarkIndexes_invalidValueWithWhiteSpace_returnsListOfIndex() throws Exception { + assertThrows(ParseException.class, () -> ParserUtil.parseDeleteArgument(INVALID_DELETE_ARGUMENT)); + } + } diff --git a/src/test/java/seedu/address/logic/parser/contact/CAddCommandParserTest.java b/src/test/java/seedu/address/logic/parser/contact/CAddCommandParserTest.java new file mode 100644 index 00000000000..d20fc6d19de --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/contact/CAddCommandParserTest.java @@ -0,0 +1,155 @@ +package seedu.address.logic.parser.contact; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.general.CommandTestUtil.ADDRESS_DESC_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.ADDRESS_DESC_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.EMAIL_DESC_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.EMAIL_DESC_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.INVALID_ADDRESS_DESC; +import static seedu.address.logic.commands.general.CommandTestUtil.INVALID_EMAIL_DESC; +import static seedu.address.logic.commands.general.CommandTestUtil.INVALID_NAME_DESC; +import static seedu.address.logic.commands.general.CommandTestUtil.INVALID_PHONE_DESC; +import static seedu.address.logic.commands.general.CommandTestUtil.INVALID_TAG_DESC; +import static seedu.address.logic.commands.general.CommandTestUtil.NAME_DESC_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.NAME_DESC_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.PHONE_DESC_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.PHONE_DESC_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.PREAMBLE_NON_EMPTY; +import static seedu.address.logic.commands.general.CommandTestUtil.PREAMBLE_WHITESPACE; +import static seedu.address.logic.commands.general.CommandTestUtil.TAG_DESC_FRIEND; +import static seedu.address.logic.commands.general.CommandTestUtil.TAG_DESC_HUSBAND; +import static seedu.address.logic.commands.general.CommandTestUtil.TELEGRAM_DESC_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.TELEGRAM_DESC_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_ADDRESS_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_EMAIL_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_NAME_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_PHONE_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_TAG_FRIEND; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_TAG_HUSBAND; +import static seedu.address.logic.commands.general.CommandTestUtil.ZOOM_DESC_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.ZOOM_DESC_BOB; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalContacts.AMY; +import static seedu.address.testutil.TypicalContacts.BOB; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.contact.CAddCommand; +import seedu.address.model.common.Address; +import seedu.address.model.common.Name; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Phone; +import seedu.address.model.tag.Tag; +import seedu.address.testutil.ContactBuilder; + +public class CAddCommandParserTest { + private CAddCommandParser parser = new CAddCommandParser(); + + @Test + public void parse_allFieldsPresent_success() { + Contact expectedContact = new ContactBuilder(BOB).withTags(VALID_TAG_FRIEND).withMarked(false).build(); + + // whitespace only preamble + assertParseSuccess(parser, PREAMBLE_WHITESPACE + NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + + ADDRESS_DESC_BOB + ZOOM_DESC_BOB + TELEGRAM_DESC_BOB + TAG_DESC_FRIEND, + new CAddCommand(expectedContact)); + + // multiple names - last name accepted + assertParseSuccess(parser, NAME_DESC_AMY + NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + + ADDRESS_DESC_BOB + ZOOM_DESC_BOB + TELEGRAM_DESC_BOB + TAG_DESC_FRIEND, + new CAddCommand(expectedContact)); + + // multiple phones - last phone accepted + assertParseSuccess(parser, NAME_DESC_BOB + PHONE_DESC_AMY + PHONE_DESC_BOB + EMAIL_DESC_BOB + + ADDRESS_DESC_BOB + ZOOM_DESC_BOB + TELEGRAM_DESC_BOB + TAG_DESC_FRIEND, + new CAddCommand(expectedContact)); + + // multiple emails - last email accepted + assertParseSuccess(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_AMY + EMAIL_DESC_BOB + + ADDRESS_DESC_BOB + ZOOM_DESC_BOB + TELEGRAM_DESC_BOB + TAG_DESC_FRIEND, + new CAddCommand(expectedContact)); + + // multiple addresses - last address accepted + assertParseSuccess(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_AMY + + ADDRESS_DESC_BOB + ZOOM_DESC_BOB + TELEGRAM_DESC_BOB + TAG_DESC_FRIEND, + new CAddCommand(expectedContact)); + + // multiple tags - all accepted + Contact expectedContactMultipleTags = new ContactBuilder(BOB).withTags(VALID_TAG_FRIEND, VALID_TAG_HUSBAND) + .withMarked(false).build(); + assertParseSuccess(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + TAG_DESC_HUSBAND + ZOOM_DESC_BOB + TELEGRAM_DESC_BOB + TAG_DESC_FRIEND, + new CAddCommand(expectedContactMultipleTags)); + } + + @Test + public void parse_optionalFieldsMissing_success() { + // zero tags + Contact expectedContact = new ContactBuilder(AMY).withTags().build(); + assertParseSuccess(parser, NAME_DESC_AMY + PHONE_DESC_AMY + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + + TELEGRAM_DESC_AMY + ZOOM_DESC_AMY, + new CAddCommand(expectedContact)); + + // Missing phone + Contact expectedContact2 = new ContactBuilder(BOB).withPhone(null).build(); + assertParseSuccess(parser, NAME_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + TELEGRAM_DESC_BOB + ZOOM_DESC_BOB + TAG_DESC_FRIEND + TAG_DESC_HUSBAND, + new CAddCommand(expectedContact2)); + + // Missing all optional fields + Contact expectedContact3 = new ContactBuilder(AMY).withAddress(null).withTags().withPhone(null) + .withTelegramHandle(null).withZoomLink(null).build(); + assertParseSuccess(parser, NAME_DESC_AMY + EMAIL_DESC_AMY, new CAddCommand(expectedContact3)); + } + + @Test + public void parse_compulsoryFieldMissing_failure() { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, CAddCommand.MESSAGE_USAGE); + + // missing name prefix + assertParseFailure(parser, VALID_NAME_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB, + expectedMessage); + + // missing email prefix + assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + VALID_EMAIL_BOB + ADDRESS_DESC_BOB, + expectedMessage); + + // all prefixes missing + assertParseFailure(parser, VALID_NAME_BOB + VALID_PHONE_BOB + VALID_EMAIL_BOB + VALID_ADDRESS_BOB, + expectedMessage); + } + + @Test + public void parse_invalidValue_failure() { + // invalid name + assertParseFailure(parser, INVALID_NAME_DESC + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Name.MESSAGE_CONSTRAINTS); + + // invalid phone + assertParseFailure(parser, NAME_DESC_BOB + INVALID_PHONE_DESC + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Phone.MESSAGE_CONSTRAINTS); + + // invalid email + assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + INVALID_EMAIL_DESC + ADDRESS_DESC_BOB + + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Email.MESSAGE_CONSTRAINTS); + + // invalid address + assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + INVALID_ADDRESS_DESC + + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Address.MESSAGE_CONSTRAINTS); + + // invalid tag + assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + INVALID_TAG_DESC + VALID_TAG_FRIEND, Tag.MESSAGE_CONSTRAINTS); + + // two invalid values, only first invalid value reported + assertParseFailure(parser, INVALID_NAME_DESC + PHONE_DESC_BOB + EMAIL_DESC_BOB + INVALID_ADDRESS_DESC, + Name.MESSAGE_CONSTRAINTS); + + // non-empty preamble + assertParseFailure(parser, PREAMBLE_NON_EMPTY + NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + + ADDRESS_DESC_BOB + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, CAddCommand.MESSAGE_USAGE)); + } +} diff --git a/src/test/java/seedu/address/logic/parser/contact/CDeleteCommandParserTest.java b/src/test/java/seedu/address/logic/parser/contact/CDeleteCommandParserTest.java new file mode 100644 index 00000000000..ed53a908b5a --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/contact/CDeleteCommandParserTest.java @@ -0,0 +1,34 @@ +package seedu.address.logic.parser.contact; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.range.Range; +import seedu.address.logic.commands.contact.CDeleteCommand; + +/** + * As we are only doing white-box testing, our test cases do not cover path variations + * outside of the CDeleteCommand code. For example, inputs "1" and "1 abc" take the + * same path through the CDeleteCommand, and therefore we test only one of them. + * The path variation for those two cases occur inside the ParserUtil, and + * therefore should be covered by the ParserUtilTest. + */ +public class CDeleteCommandParserTest { + + private CDeleteCommandParser parser = new CDeleteCommandParser(); + + @Test + public void parse_validArgs_returnsCDeleteCommand() { + Range rangeOfIndexes = Range.convertFromIndex(INDEX_FIRST); + assertParseSuccess(parser, "1", new CDeleteCommand(rangeOfIndexes)); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + assertParseFailure(parser, "a", String.format(MESSAGE_INVALID_COMMAND_FORMAT, CDeleteCommand.MESSAGE_USAGE)); + } +} diff --git a/src/test/java/seedu/address/logic/parser/contact/CEditCommandParserTest.java b/src/test/java/seedu/address/logic/parser/contact/CEditCommandParserTest.java new file mode 100644 index 00000000000..1d7abfc00eb --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/contact/CEditCommandParserTest.java @@ -0,0 +1,257 @@ +package seedu.address.logic.parser.contact; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.general.CommandTestUtil.ADDRESS_DESC_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.ADDRESS_DESC_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.EMAIL_DESC_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.EMAIL_DESC_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.INVALID_ADDRESS_DESC; +import static seedu.address.logic.commands.general.CommandTestUtil.INVALID_EMAIL_DESC; +import static seedu.address.logic.commands.general.CommandTestUtil.INVALID_NAME_DESC; +import static seedu.address.logic.commands.general.CommandTestUtil.INVALID_PHONE_DESC; +import static seedu.address.logic.commands.general.CommandTestUtil.INVALID_TAG_DESC; +import static seedu.address.logic.commands.general.CommandTestUtil.INVALID_TELEGRAM_DESC; +import static seedu.address.logic.commands.general.CommandTestUtil.INVALID_ZOOM_DESC; +import static seedu.address.logic.commands.general.CommandTestUtil.NAME_DESC_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.PHONE_DESC_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.PHONE_DESC_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.TAG_DESC_DELETEALL; +import static seedu.address.logic.commands.general.CommandTestUtil.TAG_DESC_DELETEFRIEND; +import static seedu.address.logic.commands.general.CommandTestUtil.TAG_DESC_DELETEHUSBAND; +import static seedu.address.logic.commands.general.CommandTestUtil.TAG_DESC_FRIEND; +import static seedu.address.logic.commands.general.CommandTestUtil.TAG_DESC_HUSBAND; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_ADDRESS_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_ADDRESS_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_EMAIL_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_EMAIL_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_NAME_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_PHONE_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_PHONE_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_TAG_FRIEND; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_TAG_HUSBAND; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DELETE_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND; +import static seedu.address.testutil.TypicalIndexes.INDEX_THIRD; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.contact.CEditCommand; +import seedu.address.logic.commands.contact.CEditCommand.EditContactDescriptor; +import seedu.address.model.common.Address; +import seedu.address.model.common.Name; +import seedu.address.model.common.ZoomLink; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Phone; +import seedu.address.model.contact.TelegramHandle; +import seedu.address.model.tag.Tag; +import seedu.address.testutil.EditContactDescriptorBuilder; + +public class CEditCommandParserTest { + + private static final String TAG_EMPTY = " " + PREFIX_TAG; + + private static final String MESSAGE_INVALID_FORMAT = + String.format(MESSAGE_INVALID_COMMAND_FORMAT, CEditCommand.MESSAGE_USAGE); + + private CEditCommandParser parser = new CEditCommandParser(); + + @Test + public void parse_missingParts_failure() { + // no index specified + assertParseFailure(parser, VALID_NAME_AMY, MESSAGE_INVALID_FORMAT); + + // no field specified + assertParseFailure(parser, "1", CEditCommand.MESSAGE_NOT_EDITED); + + // no index and no field specified + assertParseFailure(parser, "", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidPreamble_failure() { + // negative index + assertParseFailure(parser, "-5" + NAME_DESC_AMY, MESSAGE_INVALID_FORMAT); + + // zero index + assertParseFailure(parser, "0" + NAME_DESC_AMY, MESSAGE_INVALID_FORMAT); + + // invalid arguments being parsed as preamble + assertParseFailure(parser, "1 some random string", MESSAGE_INVALID_FORMAT); + + // invalid prefix being parsed as preamble + assertParseFailure(parser, "1 i/ string", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidValue_failure() { + assertParseFailure(parser, "1" + INVALID_NAME_DESC, Name.MESSAGE_CONSTRAINTS); // invalid name + assertParseFailure(parser, "1" + INVALID_PHONE_DESC, Phone.MESSAGE_CONSTRAINTS); // invalid phone + assertParseFailure(parser, "1" + INVALID_EMAIL_DESC, Email.MESSAGE_CONSTRAINTS); // invalid email + assertParseFailure(parser, "1" + INVALID_ADDRESS_DESC, Address.MESSAGE_CONSTRAINTS); // invalid address + assertParseFailure(parser, "1" + INVALID_TELEGRAM_DESC, + TelegramHandle.MESSAGE_CONSTRAINTS); // invalid telegram handle + assertParseFailure(parser, "1" + INVALID_ZOOM_DESC, ZoomLink.MESSAGE_CONSTRAINTS); // invalid zoom link + assertParseFailure(parser, "1" + INVALID_TAG_DESC, Tag.MESSAGE_CONSTRAINTS); // invalid tag + + // invalid phone followed by valid email + assertParseFailure(parser, "1" + INVALID_PHONE_DESC + EMAIL_DESC_AMY, Phone.MESSAGE_CONSTRAINTS); + + // valid phone followed by invalid phone. The test case for invalid phone followed by valid phone + // is tested at {@code parse_invalidValueFollowedByValidValue_success()} + assertParseFailure(parser, "1" + PHONE_DESC_BOB + INVALID_PHONE_DESC, Phone.MESSAGE_CONSTRAINTS); + + // parsing {@code PREFIX_TAG} alone will result in error + assertParseFailure(parser, "1" + TAG_DESC_FRIEND + TAG_DESC_HUSBAND + TAG_EMPTY, Tag.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, "1" + TAG_DESC_FRIEND + TAG_EMPTY + TAG_DESC_HUSBAND, Tag.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, "1" + TAG_EMPTY, Tag.MESSAGE_CONSTRAINTS); + + // multiple invalid values, but only the first invalid value is captured + assertParseFailure(parser, "1" + INVALID_NAME_DESC + INVALID_EMAIL_DESC + VALID_ADDRESS_AMY + VALID_PHONE_AMY, + Name.MESSAGE_CONSTRAINTS); + } + + @Test + public void parse_allFieldsSpecified_success() { + Index targetIndex = INDEX_SECOND; + String userInput = targetIndex.getOneBased() + PHONE_DESC_BOB + TAG_DESC_HUSBAND + + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + NAME_DESC_AMY + TAG_DESC_FRIEND; + + CEditCommand.EditContactDescriptor descriptor = new EditContactDescriptorBuilder().withName(VALID_NAME_AMY) + .withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY) + .withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).build(); + CEditCommand expectedCommand = new CEditCommand(targetIndex, descriptor); + + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_someFieldsSpecified_success() { + Index targetIndex = INDEX_FIRST; + String userInput = targetIndex.getOneBased() + PHONE_DESC_BOB + EMAIL_DESC_AMY; + + EditContactDescriptor descriptor = new EditContactDescriptorBuilder().withPhone(VALID_PHONE_BOB) + .withEmail(VALID_EMAIL_AMY).build(); + CEditCommand expectedCommand = new CEditCommand(targetIndex, descriptor); + + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_oneFieldSpecified_success() { + // name + Index targetIndex = INDEX_THIRD; + String userInput = targetIndex.getOneBased() + NAME_DESC_AMY; + CEditCommand.EditContactDescriptor descriptor = + new EditContactDescriptorBuilder().withName(VALID_NAME_AMY).build(); + CEditCommand expectedCommand = new CEditCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + + // phone + userInput = targetIndex.getOneBased() + PHONE_DESC_AMY; + descriptor = new EditContactDescriptorBuilder().withPhone(VALID_PHONE_AMY).build(); + expectedCommand = new CEditCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + + // email + userInput = targetIndex.getOneBased() + EMAIL_DESC_AMY; + descriptor = new EditContactDescriptorBuilder().withEmail(VALID_EMAIL_AMY).build(); + expectedCommand = new CEditCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + + // address + userInput = targetIndex.getOneBased() + ADDRESS_DESC_AMY; + descriptor = new EditContactDescriptorBuilder().withAddress(VALID_ADDRESS_AMY).build(); + expectedCommand = new CEditCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + + // tags + userInput = targetIndex.getOneBased() + TAG_DESC_FRIEND; + descriptor = new EditContactDescriptorBuilder().withTags(VALID_TAG_FRIEND).build(); + expectedCommand = new CEditCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_multipleRepeatedFields_acceptsLast() { + Index targetIndex = INDEX_FIRST; + String userInput = targetIndex.getOneBased() + PHONE_DESC_AMY + ADDRESS_DESC_AMY + EMAIL_DESC_AMY + + TAG_DESC_FRIEND + PHONE_DESC_AMY + ADDRESS_DESC_AMY + EMAIL_DESC_AMY + TAG_DESC_FRIEND + + PHONE_DESC_BOB + ADDRESS_DESC_BOB + EMAIL_DESC_BOB + TAG_DESC_HUSBAND; + + CEditCommand.EditContactDescriptor descriptor = new EditContactDescriptorBuilder().withPhone(VALID_PHONE_BOB) + .withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_FRIEND, VALID_TAG_HUSBAND) + .build(); + CEditCommand expectedCommand = new CEditCommand(targetIndex, descriptor); + + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_invalidValueFollowedByValidValue_success() { + // no other valid values specified + Index targetIndex = INDEX_FIRST; + String userInput = targetIndex.getOneBased() + INVALID_PHONE_DESC + PHONE_DESC_BOB; + CEditCommand.EditContactDescriptor descriptor = + new EditContactDescriptorBuilder().withPhone(VALID_PHONE_BOB).build(); + CEditCommand expectedCommand = new CEditCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + + // other valid values specified + userInput = targetIndex.getOneBased() + EMAIL_DESC_BOB + INVALID_PHONE_DESC + ADDRESS_DESC_BOB + + PHONE_DESC_BOB; + descriptor = new EditContactDescriptorBuilder().withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_BOB) + .withAddress(VALID_ADDRESS_BOB).build(); + expectedCommand = new CEditCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_resetTags_success() { + Index targetIndex = INDEX_THIRD; + String userInput = targetIndex.getOneBased() + " " + PREFIX_DELETE_TAG + "*"; + + CEditCommand.EditContactDescriptor descriptor = new EditContactDescriptorBuilder() + .withDeleteAllTags(true).build(); + CEditCommand expectedCommand = new CEditCommand(targetIndex, descriptor); + + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_resetTagsThenAddTags_success() { + Index targetIndex = INDEX_THIRD; + String userInput = targetIndex.getOneBased() + TAG_DESC_DELETEALL + TAG_DESC_HUSBAND; + String altUserInput = targetIndex.getOneBased() + TAG_DESC_HUSBAND + TAG_DESC_DELETEALL; + + CEditCommand.EditContactDescriptor descriptor = new EditContactDescriptorBuilder() + .withDeleteAllTags(true).withTags(VALID_TAG_HUSBAND).build(); + CEditCommand expectedCommand = new CEditCommand(targetIndex, descriptor); + + assertParseSuccess(parser, userInput, expectedCommand); + assertParseSuccess(parser, altUserInput, expectedCommand); + } + + @Test + public void parse_deleteTags_success() { + Index targetIndex = INDEX_SECOND; + String userInput = targetIndex.getOneBased() + TAG_DESC_DELETEFRIEND; + String altInput = targetIndex.getOneBased() + TAG_DESC_DELETEFRIEND + TAG_DESC_DELETEHUSBAND; + + CEditCommand.EditContactDescriptor descriptor1 = new EditContactDescriptorBuilder() + .withTagsToDelete(VALID_TAG_FRIEND).build(); + CEditCommand expectedCommand1 = new CEditCommand(targetIndex, descriptor1); + + assertParseSuccess(parser, userInput, expectedCommand1); + + CEditCommand.EditContactDescriptor descriptor2 = new EditContactDescriptorBuilder() + .withTagsToDelete(VALID_TAG_FRIEND, VALID_TAG_HUSBAND).build(); + CEditCommand expectedCommand2 = new CEditCommand(targetIndex, descriptor2); + + assertParseSuccess(parser, altInput, expectedCommand2); + } +} diff --git a/src/test/java/seedu/address/logic/parser/contact/CFindCommandParserTest.java b/src/test/java/seedu/address/logic/parser/contact/CFindCommandParserTest.java new file mode 100644 index 00000000000..d43553f6b72 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/contact/CFindCommandParserTest.java @@ -0,0 +1,183 @@ +package seedu.address.logic.parser.contact; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.contact.CFindCommand; +import seedu.address.model.contact.ContactContainsKeywordsPredicate; + +public class CFindCommandParserTest { + + private CFindCommandParser parser = new CFindCommandParser(); + + @Test + public void parse_emptyArg_throwsParseException() { + assertParseFailure(parser, " ", String.format(MESSAGE_INVALID_COMMAND_FORMAT, CFindCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_emptyPrefix_throwsParseException() { + // empty email prefix + assertParseFailure(parser, " e/", String.format(CFindCommandParser.MESSAGE_MISSING_KEYWORD, "e/")); + + // empty address prefix + assertParseFailure(parser, " a/", String.format(CFindCommandParser.MESSAGE_MISSING_KEYWORD, "a/")); + + // empty phone prefix + assertParseFailure(parser, " p/", String.format(CFindCommandParser.MESSAGE_MISSING_KEYWORD, "p/")); + + // empty zoom link prefix + assertParseFailure(parser, " z/", String.format(CFindCommandParser.MESSAGE_MISSING_KEYWORD, "z/")); + + // empty telegram handle prefix + assertParseFailure(parser, " th/", String.format(CFindCommandParser.MESSAGE_MISSING_KEYWORD, "th/")); + + // empty tag prefix + assertParseFailure(parser, " t/", String.format(CFindCommandParser.MESSAGE_MISSING_KEYWORD, "t/")); + } + + @Test + public void parse_prefixSyntaxButNotAPrefix_returnsCFindCommand() { + CFindCommand expectedCFindCommand = + new CFindCommand(new ContactContainsKeywordsPredicate(Arrays.asList("w/abc"))); + assertParseSuccess(parser, " w/abc", expectedCFindCommand); + } + + @Test + public void parse_validArgsWithNoPrefix_returnsCFindCommand() { + // no leading and trailing whitespaces + CFindCommand expectedCFindCommand = + new CFindCommand(new ContactContainsKeywordsPredicate(Arrays.asList("Alice", "Bob"))); + assertParseSuccess(parser, "Alice Bob", expectedCFindCommand); + + // multiple whitespaces between keywords + assertParseSuccess(parser, " \n Alice \n \t Bob \t", expectedCFindCommand); + } + + @Test + public void parse_validArgsWithEmailPrefix_returnsCFindCommand() { + // with one prefix + ContactContainsKeywordsPredicate predicate1 = new ContactContainsKeywordsPredicate(); + predicate1.setEmailKeywords(List.of("Alice", "Bob")); + CFindCommand expectedCFindCommand1 = new CFindCommand(predicate1); + assertParseSuccess(parser, " e/Alice Bob", expectedCFindCommand1); + + // with one prefix and two keywords before the prefix + ContactContainsKeywordsPredicate predicate2 = new ContactContainsKeywordsPredicate( + Arrays.asList("Charlie", "Darlie")); + predicate2.setEmailKeywords(List.of("Alice", "Bob")); + CFindCommand expectedCFindCommand2 = new CFindCommand(predicate2); + assertParseSuccess(parser, " Charlie Darlie e/Alice Bob", expectedCFindCommand2); + } + + @Test + public void parse_validArgsWithAddressPrefix_returnsCFindCommand() { + // with one prefix + ContactContainsKeywordsPredicate predicate1 = new ContactContainsKeywordsPredicate(); + predicate1.setAddressKeywords(List.of("jurong")); + CFindCommand expectedCFindCommand1 = new CFindCommand(predicate1); + assertParseSuccess(parser, " a/jurong", expectedCFindCommand1); + + // with one prefix and two keywords before the prefix + ContactContainsKeywordsPredicate predicate2 = new ContactContainsKeywordsPredicate( + Arrays.asList("Charlie", "Darlie")); + predicate2.setAddressKeywords(List.of("changi")); + CFindCommand expectedCFindCommand2 = new CFindCommand(predicate2); + assertParseSuccess(parser, " Charlie Darlie a/changi", expectedCFindCommand2); + } + + @Test + public void parse_validArgsWithPhonePrefix_returnsCFindCommand() { + // with one prefix + ContactContainsKeywordsPredicate predicate1 = new ContactContainsKeywordsPredicate(); + predicate1.setPhoneKeywords(List.of("123")); + CFindCommand expectedCFindCommand1 = new CFindCommand(predicate1); + assertParseSuccess(parser, " p/123", expectedCFindCommand1); + + // with one prefix and two keywords before the prefix + ContactContainsKeywordsPredicate predicate2 = new ContactContainsKeywordsPredicate( + Arrays.asList("Charlie", "Dar")); + predicate2.setPhoneKeywords(List.of("999")); + CFindCommand expectedCFindCommand2 = new CFindCommand(predicate2); + assertParseSuccess(parser, " Charlie Dar p/999", expectedCFindCommand2); + } + + @Test + public void parse_validArgsWithZoomLinkPrefix_returnsCFindCommand() { + // with one prefix + ContactContainsKeywordsPredicate predicate1 = new ContactContainsKeywordsPredicate(); + predicate1.setZoomLinkKeywords(List.of("nus")); + CFindCommand expectedCFindCommand1 = new CFindCommand(predicate1); + assertParseSuccess(parser, " z/nus", expectedCFindCommand1); + + // with one prefix and two keywords before the prefix + ContactContainsKeywordsPredicate predicate2 = new ContactContainsKeywordsPredicate( + Arrays.asList("Charlie", "Darlie")); + predicate2.setZoomLinkKeywords(List.of("zoom")); + CFindCommand expectedCFindCommand2 = new CFindCommand(predicate2); + assertParseSuccess(parser, " Charlie Darlie z/zoom", expectedCFindCommand2); + } + + @Test + public void parse_validArgsWithTelegramHandlePrefix_returnsCFindCommand() { + // with one prefix + ContactContainsKeywordsPredicate predicate1 = new ContactContainsKeywordsPredicate(); + predicate1.setTelegramHandleKeyword(List.of("Alice", "Bob")); + CFindCommand expectedCFindCommand1 = new CFindCommand(predicate1); + assertParseSuccess(parser, " th/Alice Bob", expectedCFindCommand1); + + // with one prefix and two keywords before the prefix + ContactContainsKeywordsPredicate predicate2 = new ContactContainsKeywordsPredicate( + Arrays.asList("Charlie", "Darlie")); + predicate2.setTelegramHandleKeyword(List.of("Alice", "Bob")); + CFindCommand expectedCFindCommand2 = new CFindCommand(predicate2); + assertParseSuccess(parser, " Charlie Darlie th/Alice Bob", expectedCFindCommand2); + } + + @Test + public void parse_validArgsWithTagPrefix_returnsCFindCommand() { + // with one prefix + ContactContainsKeywordsPredicate predicate1 = new ContactContainsKeywordsPredicate(); + predicate1.setTagKeywords(List.of("friends", "tut")); + CFindCommand expectedCFindCommand1 = new CFindCommand(predicate1); + assertParseSuccess(parser, " t/friends tut", expectedCFindCommand1); + + // with one prefix and two keywords before the prefix + ContactContainsKeywordsPredicate predicate2 = new ContactContainsKeywordsPredicate( + Arrays.asList("Charlie", "Darlie")); + predicate2.setTagKeywords(List.of("Ali", "Bo")); + CFindCommand expectedCFindCommand2 = new CFindCommand(predicate2); + assertParseSuccess(parser, " Charlie Darlie t/Ali Bo", expectedCFindCommand2); + } + + @Test + public void parse_validArgsWithAllPrefixes_returnsCFindCommand() { + ContactContainsKeywordsPredicate predicate = new ContactContainsKeywordsPredicate(); + predicate.setEmailKeywords(List.of("email")); + predicate.setAddressKeywords(List.of("address")); + predicate.setPhoneKeywords(List.of("phone")); + predicate.setZoomLinkKeywords(List.of("zoomlink")); + predicate.setTelegramHandleKeyword(List.of("telegramhandle")); + predicate.setTagKeywords(List.of("tag")); + CFindCommand expectedCFindCommand = new CFindCommand(predicate); + assertParseSuccess(parser, " e/email a/address p/phone z/zoomlink th/telegramhandle t/tag", + expectedCFindCommand); + // swapping around the prefixes should work as well + assertParseSuccess(parser, " a/address p/phone t/tag z/zoomlink e/email th/telegramhandle", + expectedCFindCommand); + } + + @Test + public void parse_allPrefixesWithNoWhitespaces_returnsCFindCommand() { + ContactContainsKeywordsPredicate predicate = new ContactContainsKeywordsPredicate(); + predicate.setEmailKeywords(List.of("a/p/z/th/t/")); + CFindCommand expectedCFindCommand = new CFindCommand(predicate); + assertParseSuccess(parser, " e/a/p/z/th/t/", expectedCFindCommand); + } +} diff --git a/src/test/java/seedu/address/logic/parser/contact/CListCommandParserTest.java b/src/test/java/seedu/address/logic/parser/contact/CListCommandParserTest.java new file mode 100644 index 00000000000..7d89acdeaad --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/contact/CListCommandParserTest.java @@ -0,0 +1,79 @@ +package seedu.address.logic.parser.contact; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.general.CommandTestUtil.EMPTY_PREFIX_ADDRESS; +import static seedu.address.logic.commands.general.CommandTestUtil.EMPTY_PREFIX_EMAIL; +import static seedu.address.logic.commands.general.CommandTestUtil.EMPTY_PREFIX_PHONE; +import static seedu.address.logic.commands.general.CommandTestUtil.EMPTY_PREFIX_TAG; +import static seedu.address.logic.commands.general.CommandTestUtil.EMPTY_PREFIX_TELEGRAM; +import static seedu.address.logic.commands.general.CommandTestUtil.EMPTY_PREFIX_ZOOM; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ZOOM; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.model.contact.ContactDisplaySetting.DEFAULT_SETTING; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.contact.CListCommand; +import seedu.address.model.contact.ContactDisplaySetting; + +public class CListCommandParserTest { + private static final String MESSAGE_INVALID_FORMAT = String.format( + MESSAGE_INVALID_COMMAND_FORMAT, + CListCommand.MESSAGE_USAGE); + + private final CListCommandParser parser = new CListCommandParser(); + + @Test + public void parse_invalidArguments_failure() { + // Preamble provided + assertParseFailure(parser, "Hello", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, "Hello " + EMPTY_PREFIX_ADDRESS, MESSAGE_INVALID_FORMAT); + + // Invalid parameters + assertParseFailure(parser, "g/", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, "g/ " + PREFIX_ZOOM, MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, PREFIX_ZOOM + "g/ ", MESSAGE_INVALID_FORMAT); + + // Additional arguments after prefix + assertParseFailure(parser, EMPTY_PREFIX_PHONE + "123", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, EMPTY_PREFIX_EMAIL + "123", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, EMPTY_PREFIX_ADDRESS + "123", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, EMPTY_PREFIX_TELEGRAM + "123", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, EMPTY_PREFIX_ZOOM + "123", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, EMPTY_PREFIX_TAG + "123", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, EMPTY_PREFIX_ZOOM + " " + PREFIX_ADDRESS + "123", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_noArguments_success() { + assertParseSuccess(parser, "", new CListCommand(DEFAULT_SETTING)); + } + + @Test + public void parse_validArguments_success() { + // Only display phone + ContactDisplaySetting c1 = new ContactDisplaySetting(true, false, false, false, false, false); + assertParseSuccess(parser, EMPTY_PREFIX_PHONE, new CListCommand(c1)); + // Only display email + ContactDisplaySetting c2 = new ContactDisplaySetting(false, true, false, false, false, false); + assertParseSuccess(parser, EMPTY_PREFIX_EMAIL, new CListCommand(c2)); + // Only display address + ContactDisplaySetting c3 = new ContactDisplaySetting(false, false, false, true, false, false); + assertParseSuccess(parser, EMPTY_PREFIX_ADDRESS, new CListCommand(c3)); + // Only display telegram + ContactDisplaySetting c4 = new ContactDisplaySetting(false, false, true, false, false, false); + assertParseSuccess(parser, EMPTY_PREFIX_TELEGRAM, new CListCommand(c4)); + // Only display zoom + ContactDisplaySetting c5 = new ContactDisplaySetting(false, false, false, false, true, false); + assertParseSuccess(parser, EMPTY_PREFIX_ZOOM, new CListCommand(c5)); + // Only display tags + ContactDisplaySetting c6 = new ContactDisplaySetting(false, false, false, false, false, true); + assertParseSuccess(parser, EMPTY_PREFIX_TAG, new CListCommand(c6)); + + // Multiple filters + ContactDisplaySetting c7 = new ContactDisplaySetting(true, false, false, false, false, true); + assertParseSuccess(parser, EMPTY_PREFIX_TAG + EMPTY_PREFIX_PHONE, new CListCommand(c7)); + } +} diff --git a/src/test/java/seedu/address/logic/parser/contact/CMarkCommandParserTest.java b/src/test/java/seedu/address/logic/parser/contact/CMarkCommandParserTest.java new file mode 100644 index 00000000000..1a8e81399c3 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/contact/CMarkCommandParserTest.java @@ -0,0 +1,36 @@ +package seedu.address.logic.parser.contact; + +import static seedu.address.commons.core.Messages.MESSAGE_DUPLICATE_INDEXES_PROVIDED; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_INDEX; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.contact.CMarkCommand; + + +class CMarkCommandParserTest { + private CMarkCommandParser parser = new CMarkCommandParser(); + + @Test + public void parse_validArgs_returnsBookmarkCommand() { + List indexes = new ArrayList<>(); + indexes.addAll(Arrays.asList(Index.fromOneBased(1), Index.fromOneBased(2), Index.fromOneBased(8))); + assertParseSuccess(parser, "1 2 8", new CMarkCommand(indexes)); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + assertParseFailure(parser, " ", String.format(MESSAGE_INVALID_COMMAND_FORMAT, + CMarkCommand.MESSAGE_USAGE)); //EP empty argument + assertParseFailure(parser, "a", MESSAGE_INVALID_INDEX); //EP non-integer argument + assertParseFailure(parser, "1 1 2", MESSAGE_DUPLICATE_INDEXES_PROVIDED); + } +} diff --git a/src/test/java/seedu/address/logic/parser/contact/CUnmarkCommandParserTest.java b/src/test/java/seedu/address/logic/parser/contact/CUnmarkCommandParserTest.java new file mode 100644 index 00000000000..06e55b5ac41 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/contact/CUnmarkCommandParserTest.java @@ -0,0 +1,36 @@ +package seedu.address.logic.parser.contact; + +import static seedu.address.commons.core.Messages.MESSAGE_DUPLICATE_INDEXES_PROVIDED; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_INDEX; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.contact.CUnmarkCommand; + + +class CUnmarkCommandParserTest { + private CUnmarkCommandParser parser = new CUnmarkCommandParser(); + + @Test + public void parse_validArgs_returnsUnmarkCommand() { + List indexes = new ArrayList<>(); + indexes.addAll(Arrays.asList(Index.fromOneBased(1), Index.fromOneBased(2), Index.fromOneBased(8))); + assertParseSuccess(parser, "1 2 8", new CUnmarkCommand(indexes)); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + assertParseFailure(parser, " ", String.format(MESSAGE_INVALID_COMMAND_FORMAT, + CUnmarkCommand.MESSAGE_USAGE)); //EP empty argument + assertParseFailure(parser, "a", MESSAGE_INVALID_INDEX); //EP non-integer argument + assertParseFailure(parser, "1 1 2", MESSAGE_DUPLICATE_INDEXES_PROVIDED); + } +} diff --git a/src/test/java/seedu/address/logic/parser/contact/CViewCommandParserTest.java b/src/test/java/seedu/address/logic/parser/contact/CViewCommandParserTest.java new file mode 100644 index 00000000000..95f79ce899b --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/contact/CViewCommandParserTest.java @@ -0,0 +1,49 @@ +package seedu.address.logic.parser.contact; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.contact.CViewCommand; + +public class CViewCommandParserTest { + + private CViewCommandParser parser = new CViewCommandParser(); + + @Test + public void parse_emptyArg_throwsParseException() { + assertParseFailure(parser, " ", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, CViewCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_validArgs_returnsViewCommand() { + // no leading and trailing whitespaces and only one valid index + CViewCommand expectedCViewCommand = + new CViewCommand(INDEX_FIRST); + assertParseSuccess(parser, "1", expectedCViewCommand); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + // multiple indexes + assertParseFailure(parser, "1 2 3", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, CViewCommand.MESSAGE_USAGE)); + + //no index specified + assertParseFailure(parser, "", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, CViewCommand.MESSAGE_USAGE)); + + //zero index + assertParseFailure(parser, "0", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, CViewCommand.MESSAGE_USAGE)); + + //negative index + assertParseFailure(parser, "-10", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, CViewCommand.MESSAGE_USAGE)); + + } +} diff --git a/src/test/java/seedu/address/logic/parser/event/EAddCommandParserTest.java b/src/test/java/seedu/address/logic/parser/event/EAddCommandParserTest.java new file mode 100644 index 00000000000..e0691ea33db --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/event/EAddCommandParserTest.java @@ -0,0 +1,152 @@ +package seedu.address.logic.parser.event; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.general.CommandTestUtil.ADDRESS_DESC_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.ADDRESS_DESC_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.DESCRIPTION_DESC_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.END_DATE_TIME_DESC_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.END_DATE_TIME_DESC_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.INVALID_ADDRESS_DESC; +import static seedu.address.logic.commands.general.CommandTestUtil.INVALID_END_DATE_TIME_DESC; +import static seedu.address.logic.commands.general.CommandTestUtil.INVALID_NAME_DESC; +import static seedu.address.logic.commands.general.CommandTestUtil.INVALID_START_DATE_TIME_DESC; +import static seedu.address.logic.commands.general.CommandTestUtil.INVALID_TAG_DESC; +import static seedu.address.logic.commands.general.CommandTestUtil.NAME_DESC_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.NAME_DESC_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.PREAMBLE_NON_EMPTY; +import static seedu.address.logic.commands.general.CommandTestUtil.PREAMBLE_WHITESPACE; +import static seedu.address.logic.commands.general.CommandTestUtil.START_DATE_TIME_DESC_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.START_DATE_TIME_DESC_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.TAG_DESC_COOL; +import static seedu.address.logic.commands.general.CommandTestUtil.TAG_DESC_EXAMS; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_ADDRESS_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_END_DATE_TIME_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_NAME_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_START_DATE_TIME_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_TAG_COOL; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_TAG_EXAMS; +import static seedu.address.logic.commands.general.CommandTestUtil.ZOOM_DESC_EXAM; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalEvents.EXAM; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.event.EAddCommand; +import seedu.address.model.common.Address; +import seedu.address.model.common.Name; +import seedu.address.model.event.EndDateTime; +import seedu.address.model.event.Event; +import seedu.address.model.event.StartDateTime; +import seedu.address.model.tag.Tag; +import seedu.address.testutil.EventBuilder; + +class EAddCommandParserTest { + private EAddCommandParser parser = new EAddCommandParser(); + + @Test + public void parse_allFieldsPresent_success() { + Event expectedEvent = new EventBuilder(EXAM).withTags(VALID_TAG_EXAMS).withMarked(false).build(); + + // whitespace only preamble + assertParseSuccess(parser, PREAMBLE_WHITESPACE + NAME_DESC_EXAM + START_DATE_TIME_DESC_EXAM + + END_DATE_TIME_DESC_EXAM + DESCRIPTION_DESC_EXAM + ADDRESS_DESC_EXAM + ZOOM_DESC_EXAM + + TAG_DESC_EXAMS, + new EAddCommand(expectedEvent)); + + // multiple names - last name accepted + assertParseSuccess(parser, NAME_DESC_TUTORIAL + NAME_DESC_EXAM + START_DATE_TIME_DESC_EXAM + + END_DATE_TIME_DESC_EXAM + DESCRIPTION_DESC_EXAM + ADDRESS_DESC_EXAM + + ZOOM_DESC_EXAM + TAG_DESC_EXAMS, + new EAddCommand(expectedEvent)); + + // multiple start time - last start time accepted + assertParseSuccess(parser, NAME_DESC_EXAM + START_DATE_TIME_DESC_TUTORIAL + START_DATE_TIME_DESC_EXAM + + END_DATE_TIME_DESC_EXAM + DESCRIPTION_DESC_EXAM + ADDRESS_DESC_EXAM + + ZOOM_DESC_EXAM + TAG_DESC_EXAMS, + new EAddCommand(expectedEvent)); + + // multiple end time - last end time accepted + assertParseSuccess(parser, NAME_DESC_EXAM + START_DATE_TIME_DESC_EXAM + END_DATE_TIME_DESC_TUTORIAL + + END_DATE_TIME_DESC_EXAM + DESCRIPTION_DESC_EXAM + ADDRESS_DESC_EXAM + + ZOOM_DESC_EXAM + TAG_DESC_EXAMS, + new EAddCommand(expectedEvent)); + + // multiple addresses - last address accepted + assertParseSuccess(parser, NAME_DESC_EXAM + START_DATE_TIME_DESC_EXAM + END_DATE_TIME_DESC_EXAM + + DESCRIPTION_DESC_EXAM + ADDRESS_DESC_TUTORIAL + ADDRESS_DESC_EXAM + + ZOOM_DESC_EXAM + TAG_DESC_EXAMS, + new EAddCommand(expectedEvent)); + + // multiple tags - all accepted + Event expectedEventMultipleTags = new EventBuilder(EXAM).withTags(VALID_TAG_EXAMS, VALID_TAG_COOL) + .withMarked(false).build(); + + assertParseSuccess(parser, NAME_DESC_EXAM + START_DATE_TIME_DESC_EXAM + END_DATE_TIME_DESC_EXAM + + DESCRIPTION_DESC_EXAM + ADDRESS_DESC_EXAM + + ZOOM_DESC_EXAM + TAG_DESC_EXAMS + TAG_DESC_COOL, + new EAddCommand(expectedEventMultipleTags)); + } + + @Test + public void parse_optionalFieldsMissing_success() { + // zero tags + Event expectedEvent = new EventBuilder(EXAM).withTags().withMarked(false).build(); + assertParseSuccess(parser, NAME_DESC_EXAM + START_DATE_TIME_DESC_EXAM + END_DATE_TIME_DESC_EXAM + + DESCRIPTION_DESC_EXAM + ADDRESS_DESC_EXAM + ZOOM_DESC_EXAM, + new EAddCommand(expectedEvent)); + } + + @Test + public void parse_compulsoryFieldMissing_failure() { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, EAddCommand.MESSAGE_USAGE); + + // missing name prefix + assertParseFailure(parser, VALID_NAME_EXAM + START_DATE_TIME_DESC_EXAM + END_DATE_TIME_DESC_EXAM + + DESCRIPTION_DESC_EXAM + ADDRESS_DESC_EXAM + ZOOM_DESC_EXAM, + expectedMessage); + + // missing start time prefix + assertParseFailure(parser, NAME_DESC_EXAM + VALID_START_DATE_TIME_EXAM + END_DATE_TIME_DESC_EXAM + + DESCRIPTION_DESC_EXAM + ADDRESS_DESC_EXAM + ZOOM_DESC_EXAM, + expectedMessage); + + // all prefixes missing + assertParseFailure(parser, VALID_NAME_EXAM + VALID_START_DATE_TIME_EXAM + VALID_END_DATE_TIME_EXAM + + VALID_ADDRESS_EXAM + ZOOM_DESC_EXAM, + expectedMessage); + } + + @Test + public void parse_invalidValue_failure() { + // invalid name + assertParseFailure(parser, INVALID_NAME_DESC + START_DATE_TIME_DESC_EXAM + END_DATE_TIME_DESC_EXAM + + ADDRESS_DESC_EXAM + TAG_DESC_EXAMS + TAG_DESC_COOL, Name.MESSAGE_CONSTRAINTS); + + // invalid start time + assertParseFailure(parser, NAME_DESC_EXAM + INVALID_START_DATE_TIME_DESC + END_DATE_TIME_DESC_EXAM + + ADDRESS_DESC_EXAM + TAG_DESC_EXAMS + TAG_DESC_COOL, StartDateTime.MESSAGE_CONSTRAINTS); + + // invalid end time + assertParseFailure(parser, NAME_DESC_EXAM + START_DATE_TIME_DESC_EXAM + INVALID_END_DATE_TIME_DESC + + ADDRESS_DESC_EXAM + TAG_DESC_EXAMS + TAG_DESC_COOL, EndDateTime.MESSAGE_CONSTRAINTS); + + // invalid address + assertParseFailure(parser, NAME_DESC_EXAM + START_DATE_TIME_DESC_EXAM + END_DATE_TIME_DESC_EXAM + + DESCRIPTION_DESC_EXAM + INVALID_ADDRESS_DESC + TAG_DESC_EXAMS + TAG_DESC_COOL, + Address.MESSAGE_CONSTRAINTS); + + // invalid tag + assertParseFailure(parser, NAME_DESC_EXAM + START_DATE_TIME_DESC_EXAM + END_DATE_TIME_DESC_EXAM + + ADDRESS_DESC_EXAM + INVALID_TAG_DESC + VALID_TAG_EXAMS, Tag.MESSAGE_CONSTRAINTS); + + // two invalid values, only first invalid value reported + assertParseFailure(parser, INVALID_NAME_DESC + START_DATE_TIME_DESC_EXAM + END_DATE_TIME_DESC_EXAM + + INVALID_ADDRESS_DESC, Name.MESSAGE_CONSTRAINTS); + + // non-empty preamble + assertParseFailure(parser, PREAMBLE_NON_EMPTY + NAME_DESC_EXAM + START_DATE_TIME_DESC_EXAM + + END_DATE_TIME_DESC_EXAM + ADDRESS_DESC_EXAM + TAG_DESC_EXAMS + TAG_DESC_COOL, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, EAddCommand.MESSAGE_USAGE)); + } +} diff --git a/src/test/java/seedu/address/logic/parser/event/EDeleteCommandParserTest.java b/src/test/java/seedu/address/logic/parser/event/EDeleteCommandParserTest.java new file mode 100644 index 00000000000..667a677472c --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/event/EDeleteCommandParserTest.java @@ -0,0 +1,34 @@ +package seedu.address.logic.parser.event; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.range.Range; +import seedu.address.logic.commands.event.EDeleteCommand; + +/** + * As we are only doing white-box testing, our test cases do not cover path variations + * outside of the EDeleteCommand code. For example, inputs "1" and "1 abc" take the + * same path through the EDeleteCommand, and therefore we test only one of them. + * The path variation for those two cases occur inside the ParserUtil, and + * therefore should be covered by the ParserUtilTest. + */ +public class EDeleteCommandParserTest { + + private EDeleteCommandParser parser = new EDeleteCommandParser(); + + @Test + public void parse_validArgs_returnsEDeleteCommand() { + Range rangeOfIndexes = Range.convertFromIndex(INDEX_FIRST); + assertParseSuccess(parser, "1", new EDeleteCommand(rangeOfIndexes)); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + assertParseFailure(parser, "a", String.format(MESSAGE_INVALID_COMMAND_FORMAT, EDeleteCommand.MESSAGE_USAGE)); + } +} diff --git a/src/test/java/seedu/address/logic/parser/event/EEditCommandParserTest.java b/src/test/java/seedu/address/logic/parser/event/EEditCommandParserTest.java new file mode 100644 index 00000000000..34e3f0a8304 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/event/EEditCommandParserTest.java @@ -0,0 +1,262 @@ +package seedu.address.logic.parser.event; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.general.CommandTestUtil.ADDRESS_DESC_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.ADDRESS_DESC_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.DELETE_TAG_DESC_COOL; +import static seedu.address.logic.commands.general.CommandTestUtil.DELETE_TAG_DESC_EXAMS; +import static seedu.address.logic.commands.general.CommandTestUtil.END_DATE_TIME_DESC_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.END_DATE_TIME_DESC_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.INVALID_ADDRESS_DESC; +import static seedu.address.logic.commands.general.CommandTestUtil.INVALID_END_DATE_TIME_DESC; +import static seedu.address.logic.commands.general.CommandTestUtil.INVALID_NAME_DESC; +import static seedu.address.logic.commands.general.CommandTestUtil.INVALID_START_DATE_TIME_DESC; +import static seedu.address.logic.commands.general.CommandTestUtil.INVALID_TAG_DESC; +import static seedu.address.logic.commands.general.CommandTestUtil.INVALID_ZOOM_DESC; +import static seedu.address.logic.commands.general.CommandTestUtil.NAME_DESC_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.START_DATE_TIME_DESC_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.START_DATE_TIME_DESC_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.TAG_DESC_COOL; +import static seedu.address.logic.commands.general.CommandTestUtil.TAG_DESC_DELETEALL; +import static seedu.address.logic.commands.general.CommandTestUtil.TAG_DESC_EXAMS; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_ADDRESS_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_ADDRESS_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_END_DATE_TIME_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_END_DATE_TIME_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_NAME_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_START_DATE_TIME_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_START_DATE_TIME_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_TAG_COOL; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_TAG_EXAMS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND; +import static seedu.address.testutil.TypicalIndexes.INDEX_THIRD; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.event.EEditCommand; +import seedu.address.logic.commands.event.EEditCommand.EditEventDescriptor; +import seedu.address.model.common.Address; +import seedu.address.model.common.Name; +import seedu.address.model.common.ZoomLink; +import seedu.address.model.event.EndDateTime; +import seedu.address.model.event.StartDateTime; +import seedu.address.model.tag.Tag; +import seedu.address.testutil.EditEventDescriptorBuilder; + +class EEditCommandParserTest { + + private static final String TAG_EMPTY = " " + PREFIX_TAG; + + private static final String MESSAGE_INVALID_FORMAT = + String.format(MESSAGE_INVALID_COMMAND_FORMAT, EEditCommand.MESSAGE_USAGE); + + private EEditCommandParser parser = new EEditCommandParser(); + + @Test + public void parse_missingParts_failure() { + // no index specified + assertParseFailure(parser, VALID_NAME_TUTORIAL, MESSAGE_INVALID_FORMAT); + + // no field specified + assertParseFailure(parser, "1", EEditCommand.MESSAGE_NOT_EDITED); + + // no index and no field specified + assertParseFailure(parser, "", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidPreamble_failure() { + // negative index + assertParseFailure(parser, "-5" + NAME_DESC_TUTORIAL, MESSAGE_INVALID_FORMAT); + + // zero index + assertParseFailure(parser, "0" + NAME_DESC_TUTORIAL, MESSAGE_INVALID_FORMAT); + + // invalid arguments being parsed as preamble + assertParseFailure(parser, "1 some random string", MESSAGE_INVALID_FORMAT); + + // invalid prefix being parsed as preamble + assertParseFailure(parser, "1 i/ string", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidValue_failure() { + assertParseFailure(parser, "1" + INVALID_NAME_DESC, Name.MESSAGE_CONSTRAINTS); // invalid name + assertParseFailure(parser, "1" + INVALID_START_DATE_TIME_DESC, StartDateTime.MESSAGE_CONSTRAINTS); + // invalid start date time + assertParseFailure(parser, "1" + INVALID_END_DATE_TIME_DESC, EndDateTime.MESSAGE_CONSTRAINTS); + // invalid end date time + assertParseFailure(parser, "1" + INVALID_ADDRESS_DESC, Address.MESSAGE_CONSTRAINTS); // invalid address + assertParseFailure(parser, "1" + INVALID_ZOOM_DESC, ZoomLink.MESSAGE_CONSTRAINTS); // invalid zoom link + assertParseFailure(parser, "1" + INVALID_TAG_DESC, Tag.MESSAGE_CONSTRAINTS); // invalid tag + + // invalid start time followed by valid end time + assertParseFailure(parser, "1" + INVALID_START_DATE_TIME_DESC + END_DATE_TIME_DESC_TUTORIAL, + StartDateTime.MESSAGE_CONSTRAINTS); + + // valid start time followed by invalid start time. The test case for invalid start time followed by valid + // start time + // is tested at {@code parse_invalidValueFollowedByValidValue_success()} + assertParseFailure(parser, "1" + START_DATE_TIME_DESC_EXAM + INVALID_START_DATE_TIME_DESC, + StartDateTime.MESSAGE_CONSTRAINTS); + + // parsing {@code PREFIX_TAG} alone will result in error + assertParseFailure(parser, "1" + TAG_DESC_COOL + TAG_DESC_EXAMS + TAG_EMPTY, Tag.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, "1" + TAG_DESC_COOL + TAG_EMPTY + TAG_DESC_EXAMS, Tag.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, "1" + TAG_EMPTY, Tag.MESSAGE_CONSTRAINTS); + + // multiple invalid values, but only the first invalid value is captured + assertParseFailure(parser, "1" + INVALID_NAME_DESC + INVALID_END_DATE_TIME_DESC + VALID_ADDRESS_TUTORIAL + + VALID_START_DATE_TIME_TUTORIAL, Name.MESSAGE_CONSTRAINTS); + } + + @Test + public void parse_allFieldsSpecified_success() { + Index targetIndex = INDEX_SECOND; + String userInput = targetIndex.getOneBased() + START_DATE_TIME_DESC_TUTORIAL + + END_DATE_TIME_DESC_TUTORIAL + ADDRESS_DESC_TUTORIAL + NAME_DESC_TUTORIAL + TAG_DESC_COOL; + + EditEventDescriptor descriptor = new EditEventDescriptorBuilder().withName(VALID_NAME_TUTORIAL) + .withStartDateTime(VALID_START_DATE_TIME_TUTORIAL).withEndDateTime(VALID_END_DATE_TIME_TUTORIAL) + .withAddress(VALID_ADDRESS_TUTORIAL).withTags(VALID_TAG_COOL).build(); + EEditCommand expectedCommand = new EEditCommand(targetIndex, descriptor); + + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_someFieldsSpecified_success() { + Index targetIndex = INDEX_FIRST; + String userInput = targetIndex.getOneBased() + START_DATE_TIME_DESC_EXAM + END_DATE_TIME_DESC_TUTORIAL; + + EditEventDescriptor descriptor = new EditEventDescriptorBuilder().withStartDateTime(VALID_START_DATE_TIME_EXAM) + .withEndDateTime(VALID_END_DATE_TIME_TUTORIAL).build(); + EEditCommand expectedCommand = new EEditCommand(targetIndex, descriptor); + + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_oneFieldSpecified_success() { + // name + Index targetIndex = INDEX_THIRD; + String userInput = targetIndex.getOneBased() + NAME_DESC_TUTORIAL; + EditEventDescriptor descriptor = + new EditEventDescriptorBuilder().withName(VALID_NAME_TUTORIAL).build(); + EEditCommand expectedCommand = new EEditCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + + // start time + userInput = targetIndex.getOneBased() + START_DATE_TIME_DESC_TUTORIAL; + descriptor = new EditEventDescriptorBuilder().withStartDateTime(VALID_START_DATE_TIME_TUTORIAL).build(); + expectedCommand = new EEditCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + + // end time + userInput = targetIndex.getOneBased() + END_DATE_TIME_DESC_TUTORIAL; + descriptor = new EditEventDescriptorBuilder().withEndDateTime(VALID_END_DATE_TIME_TUTORIAL).build(); + expectedCommand = new EEditCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + + // address + userInput = targetIndex.getOneBased() + ADDRESS_DESC_TUTORIAL; + descriptor = new EditEventDescriptorBuilder().withAddress(VALID_ADDRESS_TUTORIAL).build(); + expectedCommand = new EEditCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + + // tags + userInput = targetIndex.getOneBased() + TAG_DESC_COOL; + descriptor = new EditEventDescriptorBuilder().withTags(VALID_TAG_COOL).build(); + expectedCommand = new EEditCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_multipleRepeatedFields_acceptsLast() { + Index targetIndex = INDEX_FIRST; + String userInput = targetIndex.getOneBased() + START_DATE_TIME_DESC_TUTORIAL + ADDRESS_DESC_TUTORIAL + + END_DATE_TIME_DESC_TUTORIAL + TAG_DESC_COOL + START_DATE_TIME_DESC_TUTORIAL + ADDRESS_DESC_TUTORIAL + + END_DATE_TIME_DESC_TUTORIAL + TAG_DESC_COOL + START_DATE_TIME_DESC_EXAM + ADDRESS_DESC_EXAM + + END_DATE_TIME_DESC_EXAM + TAG_DESC_EXAMS; + + EditEventDescriptor descriptor = new EditEventDescriptorBuilder().withStartDateTime(VALID_START_DATE_TIME_EXAM) + .withEndDateTime(VALID_END_DATE_TIME_EXAM).withAddress(VALID_ADDRESS_EXAM) + .withTags(VALID_TAG_COOL, VALID_TAG_EXAMS).build(); + EEditCommand expectedCommand = new EEditCommand(targetIndex, descriptor); + + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_invalidValueFollowedByValidValue_success() { + // no other valid values specified + Index targetIndex = INDEX_FIRST; + String userInput = targetIndex.getOneBased() + INVALID_START_DATE_TIME_DESC + START_DATE_TIME_DESC_EXAM; + EditEventDescriptor descriptor = + new EditEventDescriptorBuilder().withStartDateTime(VALID_START_DATE_TIME_EXAM).build(); + EEditCommand expectedCommand = new EEditCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + + // other valid values specified + userInput = targetIndex.getOneBased() + END_DATE_TIME_DESC_EXAM + INVALID_START_DATE_TIME_DESC + + ADDRESS_DESC_EXAM + START_DATE_TIME_DESC_EXAM; + descriptor = new EditEventDescriptorBuilder().withStartDateTime(VALID_START_DATE_TIME_EXAM) + .withEndDateTime(VALID_END_DATE_TIME_EXAM).withAddress(VALID_ADDRESS_EXAM).build(); + expectedCommand = new EEditCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_resetTags_success() { + Index targetIndex = INDEX_THIRD; + String userInput = targetIndex.getOneBased() + TAG_DESC_DELETEALL; + + EditEventDescriptor descriptor = new EditEventDescriptorBuilder() + .withDeleteAllTags(true).build(); + EEditCommand expectedCommand = new EEditCommand(targetIndex, descriptor); + + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_resetTagsThenAddTags_success() { + Index targetIndex = INDEX_THIRD; + // user types delete all tags before adding new tag + String userInput = targetIndex.getOneBased() + TAG_DESC_DELETEALL + TAG_DESC_EXAMS; + // user types add new tag before deleting all tag + String altUserInput = targetIndex.getOneBased() + TAG_DESC_EXAMS + TAG_DESC_DELETEALL; + + EditEventDescriptor descriptor = new EditEventDescriptorBuilder() + .withDeleteAllTags(true).withTags(VALID_TAG_EXAMS).build(); + EEditCommand expectedCommand = new EEditCommand(targetIndex, descriptor); + + assertParseSuccess(parser, userInput, expectedCommand); + assertParseSuccess(parser, altUserInput, expectedCommand); + } + + @Test + public void parse_deleteTags_success() { + Index targetIndex = INDEX_SECOND; + // user deletes cool tag + String userDeleteOneTag = targetIndex.getOneBased() + DELETE_TAG_DESC_COOL; + // user deletes cool tag, then deletes exams tag + String userDeleteTwoTags = targetIndex.getOneBased() + DELETE_TAG_DESC_COOL + DELETE_TAG_DESC_EXAMS; + + EditEventDescriptor descriptorOneTagDeleted = new EditEventDescriptorBuilder() + .withTagsToDelete(VALID_TAG_COOL).build(); + EEditCommand commandToDeleteOneTag = new EEditCommand(targetIndex, descriptorOneTagDeleted); + + assertParseSuccess(parser, userDeleteOneTag, commandToDeleteOneTag); + + EditEventDescriptor descriptorTwoTagsDeleted = new EditEventDescriptorBuilder() + .withTagsToDelete(VALID_TAG_COOL, VALID_TAG_EXAMS).build(); + EEditCommand commandToDeleteTwoTags = new EEditCommand(targetIndex, descriptorTwoTagsDeleted); + + assertParseSuccess(parser, userDeleteTwoTags, commandToDeleteTwoTags); + } +} diff --git a/src/test/java/seedu/address/logic/parser/event/EFindCommandParserTest.java b/src/test/java/seedu/address/logic/parser/event/EFindCommandParserTest.java new file mode 100644 index 00000000000..abf60977476 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/event/EFindCommandParserTest.java @@ -0,0 +1,183 @@ +package seedu.address.logic.parser.event; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.event.EFindCommand; +import seedu.address.model.event.EventContainsKeywordsPredicate; + +public class EFindCommandParserTest { + + private EFindCommandParser parser = new EFindCommandParser(); + + @Test + public void parse_emptyArg_throwsParseException() { + assertParseFailure(parser, " ", String.format(MESSAGE_INVALID_COMMAND_FORMAT, EFindCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_emptyPrefix_throwsParseException() { + // empty start time prefix + assertParseFailure(parser, " at/", String.format(EFindCommandParser.MESSAGE_MISSING_KEYWORD, "at/")); + + // empty end time prefix + assertParseFailure(parser, " end/", String.format(EFindCommandParser.MESSAGE_MISSING_KEYWORD, "end/")); + + // empty description prefix + assertParseFailure(parser, " d/", String.format(EFindCommandParser.MESSAGE_MISSING_KEYWORD, "d/")); + + // empty address prefix + assertParseFailure(parser, " a/", String.format(EFindCommandParser.MESSAGE_MISSING_KEYWORD, "a/")); + + // empty zoom link prefix + assertParseFailure(parser, " z/", String.format(EFindCommandParser.MESSAGE_MISSING_KEYWORD, "z/")); + + // empty tag prefix + assertParseFailure(parser, " t/", String.format(EFindCommandParser.MESSAGE_MISSING_KEYWORD, "t/")); + } + + @Test + public void parse_prefixSyntaxButNotAPrefix_returnsEFindCommand() { + EFindCommand expectedEFindCommand = + new EFindCommand(new EventContainsKeywordsPredicate(Arrays.asList("w/abc"))); + assertParseSuccess(parser, " w/abc", expectedEFindCommand); + } + + @Test + public void parse_validArgsWithNoPrefix_returnsEFindCommand() { + // no leading and trailing whitespaces + EFindCommand expectedEFindCommand = + new EFindCommand(new EventContainsKeywordsPredicate(Arrays.asList("Alice", "Bob"))); + assertParseSuccess(parser, "Alice Bob", expectedEFindCommand); + + // multiple whitespaces between keywords + assertParseSuccess(parser, " \n Alice \n \t Bob \t", expectedEFindCommand); + } + + @Test + public void parse_validArgsWithStartTimePrefix_returnsEFindCommand() { + // with one prefix + EventContainsKeywordsPredicate predicate1 = new EventContainsKeywordsPredicate(); + predicate1.setStartDateTimeKeywords(List.of("2021")); + EFindCommand expectedEFindCommand1 = new EFindCommand(predicate1); + assertParseSuccess(parser, " at/2021", expectedEFindCommand1); + + // with one prefix and two keywords before the prefix + EventContainsKeywordsPredicate predicate2 = new EventContainsKeywordsPredicate( + Arrays.asList("Charlie", "Darlie")); + predicate2.setStartDateTimeKeywords(List.of("31")); + EFindCommand expectedEFindCommand2 = new EFindCommand(predicate2); + assertParseSuccess(parser, " Charlie Darlie at/31", expectedEFindCommand2); + } + + @Test + public void parse_validArgsWithEndTimePrefix_returnsEFindCommand() { + // with one prefix + EventContainsKeywordsPredicate predicate1 = new EventContainsKeywordsPredicate(); + predicate1.setEndDateTimeKeywords(List.of("2021")); + EFindCommand expectedEFindCommand1 = new EFindCommand(predicate1); + assertParseSuccess(parser, " end/2021", expectedEFindCommand1); + + // with one prefix and two keywords before the prefix + EventContainsKeywordsPredicate predicate2 = new EventContainsKeywordsPredicate( + Arrays.asList("Charlie", "Darlie")); + predicate2.setEndDateTimeKeywords(List.of("31")); + EFindCommand expectedEFindCommand2 = new EFindCommand(predicate2); + assertParseSuccess(parser, " Charlie Darlie end/31", expectedEFindCommand2); + } + + @Test + public void parse_validArgsWithDescriptionPrefix_returnsEFindCommand() { + // with one prefix + EventContainsKeywordsPredicate predicate1 = new EventContainsKeywordsPredicate(); + predicate1.setDescriptionKeywords(List.of("summer")); + EFindCommand expectedEFindCommand1 = new EFindCommand(predicate1); + assertParseSuccess(parser, " d/summer", expectedEFindCommand1); + + // with one prefix and two keywords before the prefix + EventContainsKeywordsPredicate predicate2 = new EventContainsKeywordsPredicate( + Arrays.asList("Charlie", "Darlie")); + predicate2.setDescriptionKeywords(List.of("winter")); + EFindCommand expectedEFindCommand2 = new EFindCommand(predicate2); + assertParseSuccess(parser, " Charlie Darlie d/winter", expectedEFindCommand2); + } + + @Test + public void parse_validArgsWithAddressPrefix_returnsEFindCommand() { + // with one prefix + EventContainsKeywordsPredicate predicate1 = new EventContainsKeywordsPredicate(); + predicate1.setAddressKeywords(List.of("sch")); + EFindCommand expectedEFindCommand1 = new EFindCommand(predicate1); + assertParseSuccess(parser, " a/sch", expectedEFindCommand1); + + // with one prefix and two keywords before the prefix + EventContainsKeywordsPredicate predicate2 = new EventContainsKeywordsPredicate( + Arrays.asList("Charlie", "Darlie")); + predicate2.setAddressKeywords(List.of("zoom")); + EFindCommand expectedEFindCommand2 = new EFindCommand(predicate2); + assertParseSuccess(parser, " Charlie Darlie a/zoom", expectedEFindCommand2); + } + + @Test + public void parse_validArgsWithZoomLinkPrefix_returnsEFindCommand() { + // with one prefix + EventContainsKeywordsPredicate predicate1 = new EventContainsKeywordsPredicate(); + predicate1.setZoomLinkKeywords(List.of("sg")); + EFindCommand expectedEFindCommand1 = new EFindCommand(predicate1); + assertParseSuccess(parser, " z/sg", expectedEFindCommand1); + + // with one prefix and two keywords before the prefix + EventContainsKeywordsPredicate predicate2 = new EventContainsKeywordsPredicate( + Arrays.asList("Charlie", "Darlie")); + predicate2.setZoomLinkKeywords(List.of(".com")); + EFindCommand expectedEFindCommand2 = new EFindCommand(predicate2); + assertParseSuccess(parser, " Charlie Darlie z/.com", expectedEFindCommand2); + } + + @Test + public void parse_validArgsWithTagPrefix_returnsEFindCommand() { + // with one prefix + EventContainsKeywordsPredicate predicate1 = new EventContainsKeywordsPredicate(); + predicate1.setTagKeywords(List.of("event")); + EFindCommand expectedEFindCommand1 = new EFindCommand(predicate1); + assertParseSuccess(parser, " t/event", expectedEFindCommand1); + + // with one prefix and two keywords before the prefix + EventContainsKeywordsPredicate predicate2 = new EventContainsKeywordsPredicate( + Arrays.asList("Charlie", "Darlie")); + predicate2.setTagKeywords(List.of("mid")); + EFindCommand expectedEFindCommand2 = new EFindCommand(predicate2); + assertParseSuccess(parser, " Charlie Darlie t/mid", expectedEFindCommand2); + } + + @Test + public void parse_validArgsWithAllPrefixes_returnsEFindCommand() { + EventContainsKeywordsPredicate predicate = new EventContainsKeywordsPredicate(); + predicate.setStartDateTimeKeywords(List.of("start")); + predicate.setEndDateTimeKeywords(List.of("end")); + predicate.setDescriptionKeywords(List.of("description")); + predicate.setAddressKeywords(List.of("address")); + predicate.setZoomLinkKeywords(List.of("zoomlink")); + predicate.setTagKeywords(List.of("tag")); + EFindCommand expectedEFindCommand = new EFindCommand(predicate); + assertParseSuccess(parser, " at/start end/end d/description a/address z/zoomlink t/tag", + expectedEFindCommand); + // swapping around the prefixes should work as well + assertParseSuccess(parser, " a/address end/end t/tag z/zoomlink at/start d/description", + expectedEFindCommand); + } + + @Test + public void parse_allPrefixesWithNoWhitespaces_returnsEFindCommand() { + EventContainsKeywordsPredicate predicate = new EventContainsKeywordsPredicate(); + predicate.setStartDateTimeKeywords(List.of("end/d/a/z/t/")); + EFindCommand expectedEFindCommand = new EFindCommand(predicate); + assertParseSuccess(parser, " at/end/d/a/z/t/", expectedEFindCommand); + } +} diff --git a/src/test/java/seedu/address/logic/parser/event/ELinkCommandParserTest.java b/src/test/java/seedu/address/logic/parser/event/ELinkCommandParserTest.java new file mode 100644 index 00000000000..2508793f9c8 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/event/ELinkCommandParserTest.java @@ -0,0 +1,68 @@ +package seedu.address.logic.parser.event; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.general.CommandTestUtil.EMPTY_PREFIX_CONTACT; +import static seedu.address.logic.commands.general.CommandTestUtil.PREAMBLE_WHITESPACE; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_INDEX_ONE; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_INDEX_TWO; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.event.ELinkCommand; + +class ELinkCommandParserTest { + private static final String MESSAGE_INVALID_FORMAT = String.format( + MESSAGE_INVALID_COMMAND_FORMAT, + ELinkCommand.MESSAGE_USAGE); + private final ELinkCommandParser parser = new ELinkCommandParser(); + + @Test + public void parse_multipleContacts_success() { + // whitespace only preamble + Set contactIndexes = new HashSet<>(); + contactIndexes.add(INDEX_FIRST); + contactIndexes.add(INDEX_SECOND); + assertParseSuccess(parser, PREAMBLE_WHITESPACE + VALID_INDEX_ONE + EMPTY_PREFIX_CONTACT + + VALID_INDEX_ONE + EMPTY_PREFIX_CONTACT + VALID_INDEX_TWO, + new ELinkCommand(INDEX_FIRST, contactIndexes)); + } + + @Test + public void parse_singleContact_success() { + // whitespace only preamble + Set contactIndexes = new HashSet<>(); + contactIndexes.add(INDEX_FIRST); + assertParseSuccess(parser, PREAMBLE_WHITESPACE + VALID_INDEX_ONE + EMPTY_PREFIX_CONTACT + + VALID_INDEX_ONE, + new ELinkCommand(INDEX_FIRST, contactIndexes)); + } + + @Test + public void parse_invalidInput_failure() { + // No contacts to link + assertParseFailure(parser, PREAMBLE_WHITESPACE + VALID_INDEX_ONE, MESSAGE_INVALID_FORMAT); + + // No arguments + assertParseFailure(parser, PREAMBLE_WHITESPACE, MESSAGE_INVALID_FORMAT); + + // No event index + assertParseFailure(parser, PREAMBLE_WHITESPACE + EMPTY_PREFIX_CONTACT + + VALID_INDEX_ONE, MESSAGE_INVALID_FORMAT); + + // Invalid index + assertParseFailure(parser, PREAMBLE_WHITESPACE + "a" + EMPTY_PREFIX_CONTACT + + VALID_INDEX_ONE, MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, PREAMBLE_WHITESPACE + VALID_INDEX_ONE + EMPTY_PREFIX_CONTACT + + VALID_INDEX_ONE + EMPTY_PREFIX_CONTACT + "a", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, PREAMBLE_WHITESPACE + VALID_INDEX_ONE + EMPTY_PREFIX_CONTACT + + "*", MESSAGE_INVALID_FORMAT); + } +} diff --git a/src/test/java/seedu/address/logic/parser/event/EListCommandParserTest.java b/src/test/java/seedu/address/logic/parser/event/EListCommandParserTest.java new file mode 100644 index 00000000000..336f3075832 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/event/EListCommandParserTest.java @@ -0,0 +1,86 @@ +package seedu.address.logic.parser.event; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.general.CommandTestUtil.EMPTY_PREFIX_ADDRESS; +import static seedu.address.logic.commands.general.CommandTestUtil.EMPTY_PREFIX_DESCRIPTION; +import static seedu.address.logic.commands.general.CommandTestUtil.EMPTY_PREFIX_END_DATE_TIME; +import static seedu.address.logic.commands.general.CommandTestUtil.EMPTY_PREFIX_START_DATE_TIME; +import static seedu.address.logic.commands.general.CommandTestUtil.EMPTY_PREFIX_TAG; +import static seedu.address.logic.commands.general.CommandTestUtil.EMPTY_PREFIX_ZOOM; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ZOOM; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.model.event.EventDisplaySetting.DEFAULT_SETTING; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.event.EListCommand; +import seedu.address.model.event.EventDisplaySetting; + +public class EListCommandParserTest { + private static final String MESSAGE_INVALID_FORMAT = String.format( + MESSAGE_INVALID_COMMAND_FORMAT, + EListCommand.MESSAGE_USAGE); + + private final EListCommandParser parser = new EListCommandParser(); + + @Test + public void parse_invalidArguments_failure() { + // Preamble provided + assertParseFailure(parser, "Hello", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, "Hello " + EMPTY_PREFIX_ADDRESS, MESSAGE_INVALID_FORMAT); + + // Invalid parameters + assertParseFailure(parser, "g/", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, "g/ " + PREFIX_ZOOM, MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, PREFIX_ZOOM + "g/ ", MESSAGE_INVALID_FORMAT); + + // Additional arguments after prefix + assertParseFailure(parser, EMPTY_PREFIX_START_DATE_TIME + "123", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, EMPTY_PREFIX_END_DATE_TIME + "123", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, EMPTY_PREFIX_ADDRESS + "123", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, EMPTY_PREFIX_DESCRIPTION + "123", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, EMPTY_PREFIX_ZOOM + "123", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, EMPTY_PREFIX_TAG + "123", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, EMPTY_PREFIX_ZOOM + " " + PREFIX_ADDRESS + "123", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_noArguments_success() { + assertParseSuccess(parser, "", new EListCommand(DEFAULT_SETTING)); + } + + @Test + public void parse_validArguments_success() { + + // only display start time + EventDisplaySetting displaySetting1 = new EventDisplaySetting(true, false, false, false, false, false); + assertParseSuccess(parser, EMPTY_PREFIX_START_DATE_TIME, new EListCommand(displaySetting1)); + + // only display end time + EventDisplaySetting displaySetting2 = new EventDisplaySetting(false, true, false, false, false, false); + assertParseSuccess(parser, EMPTY_PREFIX_END_DATE_TIME, new EListCommand(displaySetting2)); + + // only display address + EventDisplaySetting displaySetting3 = new EventDisplaySetting(false, false, false, true, false, false); + assertParseSuccess(parser, EMPTY_PREFIX_ADDRESS, new EListCommand(displaySetting3)); + + //only display description + EventDisplaySetting displaySetting4 = new EventDisplaySetting(false, false, true, false, false, false); + assertParseSuccess(parser, EMPTY_PREFIX_DESCRIPTION, new EListCommand(displaySetting4)); + + //only display zoom link + EventDisplaySetting displaySetting5 = new EventDisplaySetting(false, false, false, false, true, false); + assertParseSuccess(parser, EMPTY_PREFIX_ZOOM, new EListCommand(displaySetting5)); + + //only display tags + EventDisplaySetting displaySetting6 = new EventDisplaySetting(false, false, false, false, false, true); + assertParseSuccess(parser, EMPTY_PREFIX_TAG, new EListCommand(displaySetting6)); + + // display multiple fields + EventDisplaySetting displaySetting7 = new EventDisplaySetting(true, false, false, false, false, true); + assertParseSuccess(parser, EMPTY_PREFIX_TAG + EMPTY_PREFIX_START_DATE_TIME, + new EListCommand(displaySetting7)); + } +} diff --git a/src/test/java/seedu/address/logic/parser/event/EMarkCommandParserTest.java b/src/test/java/seedu/address/logic/parser/event/EMarkCommandParserTest.java new file mode 100644 index 00000000000..8e5ad98db5d --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/event/EMarkCommandParserTest.java @@ -0,0 +1,35 @@ +package seedu.address.logic.parser.event; + +import static seedu.address.commons.core.Messages.MESSAGE_DUPLICATE_INDEXES_PROVIDED; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_INDEX; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.event.EMarkCommand; + +class EMarkCommandParserTest { + private EMarkCommandParser parser = new EMarkCommandParser(); + + @Test + public void parse_validArgs_returnsMarkCommand() { + List indexes = new ArrayList<>(); + indexes.addAll(Arrays.asList(Index.fromOneBased(1), Index.fromOneBased(2), Index.fromOneBased(8))); + assertParseSuccess(parser, "1 2 8", new EMarkCommand(indexes)); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + assertParseFailure(parser, " ", String.format(MESSAGE_INVALID_COMMAND_FORMAT, + EMarkCommand.MESSAGE_USAGE)); //EP: empty argument + assertParseFailure(parser, "a", MESSAGE_INVALID_INDEX); //EP non-integer argument + assertParseFailure(parser, "1 1 2", MESSAGE_DUPLICATE_INDEXES_PROVIDED); + } +} diff --git a/src/test/java/seedu/address/logic/parser/event/EUnlinkCommandParserTest.java b/src/test/java/seedu/address/logic/parser/event/EUnlinkCommandParserTest.java new file mode 100644 index 00000000000..75c8c398a58 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/event/EUnlinkCommandParserTest.java @@ -0,0 +1,75 @@ +package seedu.address.logic.parser.event; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.general.CommandTestUtil.EMPTY_PREFIX_CONTACT; +import static seedu.address.logic.commands.general.CommandTestUtil.PREAMBLE_WHITESPACE; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_INDEX_ONE; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_INDEX_TWO; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.event.EUnlinkCommand; + +class EUnlinkCommandParserTest { + private static final String MESSAGE_INVALID_FORMAT = String.format( + MESSAGE_INVALID_COMMAND_FORMAT, + EUnlinkCommand.MESSAGE_USAGE); + private final EUnlinkCommandParser parser = new EUnlinkCommandParser(); + + @Test + public void parse_multipleContacts_success() { + // whitespace only preamble + Set contactIndexes = new HashSet<>(); + contactIndexes.add(INDEX_FIRST); + contactIndexes.add(INDEX_SECOND); + assertParseSuccess(parser, PREAMBLE_WHITESPACE + VALID_INDEX_ONE + EMPTY_PREFIX_CONTACT + + VALID_INDEX_ONE + EMPTY_PREFIX_CONTACT + VALID_INDEX_TWO, + new EUnlinkCommand(INDEX_FIRST, contactIndexes, false)); + } + + @Test + public void parse_singleContact_success() { + // whitespace only preamble + Set contactIndexes = new HashSet<>(); + contactIndexes.add(INDEX_FIRST); + assertParseSuccess(parser, PREAMBLE_WHITESPACE + VALID_INDEX_ONE + EMPTY_PREFIX_CONTACT + + VALID_INDEX_ONE, + new EUnlinkCommand(INDEX_FIRST, contactIndexes, false)); + } + + @Test + public void parse_allContacts_success() { + // whitespace only preamble + Set contactIndexes = new HashSet<>(); + assertParseSuccess(parser, PREAMBLE_WHITESPACE + VALID_INDEX_ONE + EMPTY_PREFIX_CONTACT + + "*", + new EUnlinkCommand(INDEX_FIRST, contactIndexes, true)); + } + + @Test + public void parse_invalidInput_failure() { + // No contacts to unlink + assertParseFailure(parser, PREAMBLE_WHITESPACE + VALID_INDEX_ONE, MESSAGE_INVALID_FORMAT); + + // No arguments + assertParseFailure(parser, PREAMBLE_WHITESPACE, MESSAGE_INVALID_FORMAT); + + // No event index + assertParseFailure(parser, PREAMBLE_WHITESPACE + EMPTY_PREFIX_CONTACT + + "*", MESSAGE_INVALID_FORMAT); + + // Invalid index + assertParseFailure(parser, PREAMBLE_WHITESPACE + "a" + EMPTY_PREFIX_CONTACT + + "*", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, PREAMBLE_WHITESPACE + VALID_INDEX_ONE + EMPTY_PREFIX_CONTACT + + VALID_INDEX_ONE + EMPTY_PREFIX_CONTACT + "a", MESSAGE_INVALID_FORMAT); + } +} diff --git a/src/test/java/seedu/address/logic/parser/event/EUnmarkCommandParserTest.java b/src/test/java/seedu/address/logic/parser/event/EUnmarkCommandParserTest.java new file mode 100644 index 00000000000..fb509b974d1 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/event/EUnmarkCommandParserTest.java @@ -0,0 +1,35 @@ +package seedu.address.logic.parser.event; + +import static seedu.address.commons.core.Messages.MESSAGE_DUPLICATE_INDEXES_PROVIDED; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_INDEX; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.event.EUnmarkCommand; + +class EUnmarkCommandParserTest { + private EUnmarkCommandParser parser = new EUnmarkCommandParser(); + + @Test + public void parse_validArgs_returnsUnmarkCommand() { + List indexes = new ArrayList<>(); + indexes.addAll(Arrays.asList(Index.fromOneBased(1), Index.fromOneBased(2), Index.fromOneBased(8))); + assertParseSuccess(parser, "1 2 8", new EUnmarkCommand(indexes)); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + assertParseFailure(parser, " ", String.format(MESSAGE_INVALID_COMMAND_FORMAT, + EUnmarkCommand.MESSAGE_USAGE)); //EP empty argument + assertParseFailure(parser, "a", MESSAGE_INVALID_INDEX); //EP non-integer argument + assertParseFailure(parser, "1 1 2", MESSAGE_DUPLICATE_INDEXES_PROVIDED); + } +} diff --git a/src/test/java/seedu/address/logic/parser/event/EViewCommandParserTest.java b/src/test/java/seedu/address/logic/parser/event/EViewCommandParserTest.java new file mode 100644 index 00000000000..b30c7ec354e --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/event/EViewCommandParserTest.java @@ -0,0 +1,48 @@ +package seedu.address.logic.parser.event; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.event.EViewCommand; + +public class EViewCommandParserTest { + private EViewCommandParser parser = new EViewCommandParser(); + + @Test + public void parse_emptyArg_throwsParseException() { + assertParseFailure(parser, " ", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, EViewCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_validArgs_returnsViewCommand() { + // no leading and trailing whitespaces and only one valid index + EViewCommand expectedEViewCommand = + new EViewCommand(INDEX_FIRST); + assertParseSuccess(parser, "1", expectedEViewCommand); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + // multiple indexes + assertParseFailure(parser, "1 2 3", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, EViewCommand.MESSAGE_USAGE)); + + //no index specified + assertParseFailure(parser, "", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, EViewCommand.MESSAGE_USAGE)); + + //zero index + assertParseFailure(parser, "0", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, EViewCommand.MESSAGE_USAGE)); + + //negative index + assertParseFailure(parser, "-10", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, EViewCommand.MESSAGE_USAGE)); + + } +} diff --git a/src/test/java/seedu/address/model/AddressBookTest.java b/src/test/java/seedu/address/model/AddressBookTest.java index 87782528ecd..de860ffb91e 100644 --- a/src/test/java/seedu/address/model/AddressBookTest.java +++ b/src/test/java/seedu/address/model/AddressBookTest.java @@ -3,12 +3,15 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_ADDRESS_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_TAG_HUSBAND; import static seedu.address.testutil.Assert.assertThrows; -import static seedu.address.testutil.TypicalPersons.ALICE; -import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalAddressBook.getTypicalAddressBook; +import static seedu.address.testutil.TypicalContacts.ALICE_MARKED; +import static seedu.address.testutil.TypicalEvents.INTERVIEW; +import static seedu.address.testutil.TypicalEvents.TEAM_MEETING; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -18,9 +21,12 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import seedu.address.model.person.Person; -import seedu.address.model.person.exceptions.DuplicatePersonException; -import seedu.address.testutil.PersonBuilder; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.exceptions.DuplicateContactException; +import seedu.address.model.event.Event; +import seedu.address.model.event.exceptions.DuplicateEventException; +import seedu.address.testutil.ContactBuilder; +import seedu.address.testutil.EventBuilder; public class AddressBookTest { @@ -28,7 +34,8 @@ public class AddressBookTest { @Test public void constructor() { - assertEquals(Collections.emptyList(), addressBook.getPersonList()); + assertEquals(Collections.emptyList(), addressBook.getContactList()); + assertEquals(Collections.emptyList(), addressBook.getEventList()); } @Test @@ -44,59 +51,113 @@ public void resetData_withValidReadOnlyAddressBook_replacesData() { } @Test - public void resetData_withDuplicatePersons_throwsDuplicatePersonException() { - // Two persons with the same identity fields - Person editedAlice = new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND) - .build(); - List newPersons = Arrays.asList(ALICE, editedAlice); - AddressBookStub newData = new AddressBookStub(newPersons); + public void resetData_withDuplicateContacts_throwsDuplicateContactException() { + // Two contacts with the same identity fields + Contact editedAlice = new ContactBuilder(ALICE_MARKED).withAddress(VALID_ADDRESS_BOB) + .withTags(VALID_TAG_HUSBAND).build(); + List newContacts = Arrays.asList(ALICE_MARKED, editedAlice); + List newEvents = new ArrayList<>(); //empty event list + AddressBookStub newData = new AddressBookStub(newContacts, newEvents); + + assertThrows(DuplicateContactException.class, () -> addressBook.resetData(newData)); + } - assertThrows(DuplicatePersonException.class, () -> addressBook.resetData(newData)); + @Test + public void resetData_withDuplicateEvents_throwsDuplicateEventException() { + // Two events with the same name fields + Event editedEvent = new EventBuilder(TEAM_MEETING).withAddress("New Address").build(); + List newEvents = Arrays.asList(TEAM_MEETING, editedEvent); + List newContacts = new ArrayList<>(); + AddressBookStub newData = new AddressBookStub(newContacts, newEvents); + + assertThrows(DuplicateEventException.class, () -> addressBook.resetData(newData)); } @Test - public void hasPerson_nullPerson_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> addressBook.hasPerson(null)); + public void hasContact_nullContact_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> addressBook.hasContact(null)); } @Test - public void hasPerson_personNotInAddressBook_returnsFalse() { - assertFalse(addressBook.hasPerson(ALICE)); + public void hasEvent_nullEvent_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> addressBook.hasEvent(null)); } @Test - public void hasPerson_personInAddressBook_returnsTrue() { - addressBook.addPerson(ALICE); - assertTrue(addressBook.hasPerson(ALICE)); + public void hasContact_contactNotInAddressBook_returnsFalse() { + assertFalse(addressBook.hasContact(ALICE_MARKED)); } @Test - public void hasPerson_personWithSameIdentityFieldsInAddressBook_returnsTrue() { - addressBook.addPerson(ALICE); - Person editedAlice = new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND) + public void hasEvent_eventNotInAddressBook_returnsFalse() { + assertFalse(addressBook.hasEvent(INTERVIEW)); + } + + @Test + public void hasContact_contactInAddressBook_returnsTrue() { + addressBook.addContact(ALICE_MARKED); + assertTrue(addressBook.hasContact(ALICE_MARKED)); + } + + @Test + public void hasEvent_eventInAddressBook_returnsTrue() { + addressBook.addEvent(INTERVIEW); + assertTrue(addressBook.hasEvent(INTERVIEW)); + } + + @Test + public void hasContact_contactWithSameIdentityFieldsInAddressBook_returnsTrue() { + addressBook.addContact(ALICE_MARKED); + Contact editedAlice = + new ContactBuilder(ALICE_MARKED).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND) .build(); - assertTrue(addressBook.hasPerson(editedAlice)); + assertTrue(addressBook.hasContact(editedAlice)); } @Test - public void getPersonList_modifyList_throwsUnsupportedOperationException() { - assertThrows(UnsupportedOperationException.class, () -> addressBook.getPersonList().remove(0)); + public void hasEvent_eventWithSameNameFieldsInAddressBook_returnsTrue() { + addressBook.addEvent(INTERVIEW); + Event editedEvent = new EventBuilder(INTERVIEW).withAddress("Google Office") + .withStartDateAndTime("28-10-2021 11:00").build(); + assertTrue(addressBook.hasEvent(editedEvent)); + } + + @Test + public void getContactList_modifyList_throwsUnsupportedOperationException() { + assertThrows(UnsupportedOperationException.class, () -> addressBook.getContactList().remove(0)); + } + + @Test + public void getEventList_modifyList_throwsUnsupportedOperationException() { + assertThrows(UnsupportedOperationException.class, () -> addressBook.getEventList().remove(0)); + } + + @Test + public void copy_success() { + AddressBook copy = addressBook.copy(); + assertEquals(addressBook, copy); } /** - * A stub ReadOnlyAddressBook whose persons list can violate interface constraints. + * A stub ReadOnlyAddressBook whose contacts list and event list can violate interface constraints. */ private static class AddressBookStub implements ReadOnlyAddressBook { - private final ObservableList persons = FXCollections.observableArrayList(); + private final ObservableList contacts = FXCollections.observableArrayList(); + private final ObservableList events = FXCollections.observableArrayList(); - AddressBookStub(Collection persons) { - this.persons.setAll(persons); + AddressBookStub(Collection contacts, Collection events) { + this.contacts.setAll(contacts); + this.events.setAll(events); } @Override - public ObservableList getPersonList() { - return persons; + public ObservableList getContactList() { + return contacts; } - } + @Override + public ObservableList getEventList() { + return events; + } + } } diff --git a/src/test/java/seedu/address/model/ModelDisplaySettingTest.java b/src/test/java/seedu/address/model/ModelDisplaySettingTest.java new file mode 100644 index 00000000000..2544a32ea14 --- /dev/null +++ b/src/test/java/seedu/address/model/ModelDisplaySettingTest.java @@ -0,0 +1,69 @@ +package seedu.address.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static seedu.address.model.Model.PREDICATE_HIDE_ALL_CONTACTS; +import static seedu.address.model.Model.PREDICATE_HIDE_ALL_EVENTS; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_CONTACTS; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_EVENTS; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.contact.ContactDisplaySetting; +import seedu.address.model.event.EventDisplaySetting; + +class ModelDisplaySettingTest { + + private ModelDisplaySetting displaySetting = new ModelDisplaySetting(); + + @Test + public void constructor() { + assertEquals(ContactDisplaySetting.DEFAULT_SETTING, displaySetting.getContactDisplaySetting()); + assertEquals(EventDisplaySetting.DEFAULT_SETTING, displaySetting.getEventDisplaySetting()); + assertEquals(PREDICATE_SHOW_ALL_CONTACTS, displaySetting.getContactDisplayPredicate()); + assertEquals(PREDICATE_SHOW_ALL_EVENTS, displaySetting.getEventDisplayPredicate()); + } + + @Test + void copy() { + ModelDisplaySetting copiedSetting = displaySetting.copy(); + assertEquals(displaySetting.getContactDisplaySetting(), copiedSetting.getContactDisplaySetting()); + assertEquals(displaySetting.getEventDisplaySetting(), copiedSetting.getEventDisplaySetting()); + assertEquals(displaySetting.getContactDisplayPredicate(), copiedSetting.getContactDisplayPredicate()); + assertEquals(displaySetting.getEventDisplayPredicate(), copiedSetting.getEventDisplayPredicate()); + } + + @Test + void testEquals() { + ModelDisplaySetting anotherSetting = new ModelDisplaySetting(); + assertEquals(anotherSetting, displaySetting); + } + + @Test + void differentContactDisplaySetting() { + ModelDisplaySetting differentSetting = displaySetting + .differentContactDisplaySetting(new ContactDisplaySetting(true)); + assertNotEquals(displaySetting, differentSetting); + } + + @Test + void differentEventDisplaySetting() { + ModelDisplaySetting differentSetting = displaySetting + .differentEventDisplaySetting(new EventDisplaySetting(true)); + assertNotEquals(displaySetting, differentSetting); + } + + @Test + void differentContactDisplayPredicate() { + ModelDisplaySetting differentSetting = displaySetting + .differentContactDisplayPredicate(PREDICATE_HIDE_ALL_CONTACTS); + assertNotEquals(displaySetting.getContactDisplayPredicate(), differentSetting.getContactDisplayPredicate()); + } + + @Test + void differentEventDisplayPredicate() { + ModelDisplaySetting differentSetting = displaySetting + .differentEventDisplayPredicate(PREDICATE_HIDE_ALL_EVENTS); + assertNotEquals(displaySetting.getEventDisplayPredicate(), differentSetting.getEventDisplayPredicate()); + } +} diff --git a/src/test/java/seedu/address/model/ModelManagerTest.java b/src/test/java/seedu/address/model/ModelManagerTest.java index 2cf1418d116..750d6d5a69b 100644 --- a/src/test/java/seedu/address/model/ModelManagerTest.java +++ b/src/test/java/seedu/address/model/ModelManagerTest.java @@ -2,11 +2,16 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_CONTACTS; import static seedu.address.testutil.Assert.assertThrows; -import static seedu.address.testutil.TypicalPersons.ALICE; -import static seedu.address.testutil.TypicalPersons.BENSON; +import static seedu.address.testutil.TypicalContacts.ALICE_MARKED; +import static seedu.address.testutil.TypicalContacts.AMY; +import static seedu.address.testutil.TypicalContacts.BENSON; +import static seedu.address.testutil.TypicalEvents.CS2100_CONSULTATION; +import static seedu.address.testutil.TypicalEvents.INTERVIEW; +import static seedu.address.testutil.TypicalEvents.TUTORIAL; import java.nio.file.Path; import java.nio.file.Paths; @@ -15,7 +20,10 @@ import org.junit.jupiter.api.Test; import seedu.address.commons.core.GuiSettings; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.contact.ContactContainsKeywordsPredicate; +import seedu.address.model.contact.ContactDisplaySetting; +import seedu.address.model.event.EventDisplaySetting; +import seedu.address.model.history.ModelHistoryException; import seedu.address.testutil.AddressBookBuilder; public class ModelManagerTest { @@ -27,6 +35,8 @@ public void constructor() { assertEquals(new UserPrefs(), modelManager.getUserPrefs()); assertEquals(new GuiSettings(), modelManager.getGuiSettings()); assertEquals(new AddressBook(), new AddressBook(modelManager.getAddressBook())); + assertEquals(ContactDisplaySetting.DEFAULT_SETTING, modelManager.getContactDisplaySetting()); + assertEquals(EventDisplaySetting.DEFAULT_SETTING, modelManager.getEventDisplaySetting()); } @Test @@ -60,6 +70,99 @@ public void setGuiSettings_validGuiSettings_setsGuiSettings() { assertEquals(guiSettings, modelManager.getGuiSettings()); } + @Test + public void setEventDisplaySetting_nullSetting_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> modelManager.setEventDisplaySetting(null)); + } + + @Test + public void setEventDisplaySetting_validSetting_setSuccess() { + EventDisplaySetting setting = new EventDisplaySetting(true); + modelManager.setEventDisplaySetting(setting); + assertEquals(setting, modelManager.getEventDisplaySetting()); + } + + @Test + public void setContactDisplaySetting_nullSetting_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> modelManager.setContactDisplaySetting(null)); + } + + @Test + public void setContactDisplaySetting_validSetting_setSuccess() { + ContactDisplaySetting setting = new ContactDisplaySetting(true); + modelManager.setContactDisplaySetting(setting); + assertEquals(setting, modelManager.getContactDisplaySetting()); + } + + @Test + public void commitHistory_success() { + modelManager.addContact(AMY); + modelManager.setContactDisplaySetting(new ContactDisplaySetting(true)); + modelManager.setEventDisplaySetting(new EventDisplaySetting(true)); + modelManager.commitHistory(); + assertTrue(modelManager.hasContact(AMY)); + assertEquals(new ContactDisplaySetting(true), modelManager.getContactDisplaySetting()); + assertEquals(new EventDisplaySetting(true), modelManager.getEventDisplaySetting()); + } + + @Test + public void isUndoable() { + assertTrue(modelManager.isUndoable()); + modelManager.addEvent(INTERVIEW); + assertTrue(modelManager.isUndoable()); + modelManager.undoHistory(); + assertFalse(modelManager.isUndoable()); + } + + @Test + public void undo_noHistory_failure() { + modelManager.clearHistory(); + assertThrows(ModelHistoryException.class, () -> modelManager.undoHistory()); + } + + @Test + public void undoHistory_addEvent_success() { + modelManager.setEventDisplaySetting(new EventDisplaySetting(true)); + modelManager.addEvent(TUTORIAL); + modelManager.commitHistory(); + modelManager.undoHistory(); + assertFalse(modelManager.hasEvent(TUTORIAL)); + assertNotEquals(modelManager.getEventDisplaySetting(), new EventDisplaySetting(true)); + } + + @Test + public void isRedoable() { + assertFalse(modelManager.isRedoable()); + modelManager.addEvent(CS2100_CONSULTATION); + modelManager.undoHistory(); + assertTrue(modelManager.isRedoable()); + modelManager.redoHistory(); + assertFalse(modelManager.isRedoable()); + } + @Test + public void redo_noUndo_failure() { + assertThrows(ModelHistoryException.class, () -> modelManager.redoHistory()); + } + + @Test + public void redo_clearHistory_failure() { + modelManager.addEvent(CS2100_CONSULTATION); + modelManager.commitHistory(); + modelManager.clearHistory(); + assertThrows(ModelHistoryException.class, () -> modelManager.redoHistory()); + } + + @Test + public void redo_singleUndo_success() { + modelManager.addEvent(INTERVIEW); + modelManager.commitHistory(); + modelManager.undoHistory(); + modelManager.redoHistory(); + assertTrue(modelManager.hasEvent(INTERVIEW)); + assertEquals(ContactDisplaySetting.DEFAULT_SETTING, modelManager.getContactDisplaySetting()); + assertEquals(EventDisplaySetting.DEFAULT_SETTING, modelManager.getEventDisplaySetting()); + } + @Test public void setAddressBookFilePath_nullPath_throwsNullPointerException() { assertThrows(NullPointerException.class, () -> modelManager.setAddressBookFilePath(null)); @@ -73,29 +176,50 @@ public void setAddressBookFilePath_validPath_setsAddressBookFilePath() { } @Test - public void hasPerson_nullPerson_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> modelManager.hasPerson(null)); + public void hasContact_nullContact_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> modelManager.hasContact(null)); + } + + @Test + public void hasContact_contactNotInAddressBook_returnsFalse() { + assertFalse(modelManager.hasContact(ALICE_MARKED)); } @Test - public void hasPerson_personNotInAddressBook_returnsFalse() { - assertFalse(modelManager.hasPerson(ALICE)); + public void hasContact_contactInAddressBook_returnsTrue() { + modelManager.addContact(ALICE_MARKED); + assertTrue(modelManager.hasContact(ALICE_MARKED)); } @Test - public void hasPerson_personInAddressBook_returnsTrue() { - modelManager.addPerson(ALICE); - assertTrue(modelManager.hasPerson(ALICE)); + public void getFilteredContactList_modifyList_throwsUnsupportedOperationException() { + assertThrows(UnsupportedOperationException.class, () -> modelManager.getFilteredContactList().remove(0)); } @Test - public void getFilteredPersonList_modifyList_throwsUnsupportedOperationException() { - assertThrows(UnsupportedOperationException.class, () -> modelManager.getFilteredPersonList().remove(0)); + public void hasEvent_nullEvent_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> modelManager.hasEvent(null)); + } + + @Test + public void hasEvent_eventNotInAddressBook_returnsFalse() { + assertFalse(modelManager.hasEvent(TUTORIAL)); + } + + @Test + public void hasEvent_contactInAddressBook_returnsTrue() { + modelManager.addEvent(TUTORIAL); + assertTrue(modelManager.hasEvent(TUTORIAL)); + } + + @Test + public void getFilteredEventList_modifyList_throwsUnsupportedOperationException() { + assertThrows(UnsupportedOperationException.class, () -> modelManager.getFilteredEventList().remove(1)); } @Test public void equals() { - AddressBook addressBook = new AddressBookBuilder().withPerson(ALICE).withPerson(BENSON).build(); + AddressBook addressBook = new AddressBookBuilder().withContact(ALICE_MARKED).withContact(BENSON).build(); AddressBook differentAddressBook = new AddressBook(); UserPrefs userPrefs = new UserPrefs(); @@ -116,17 +240,25 @@ public void equals() { // different addressBook -> returns false assertFalse(modelManager.equals(new ModelManager(differentAddressBook, userPrefs))); - // different filteredList -> returns false - String[] keywords = ALICE.getName().fullName.split("\\s+"); - modelManager.updateFilteredPersonList(new NameContainsKeywordsPredicate(Arrays.asList(keywords))); + // different filteredContactList -> returns false + String[] keywords = ALICE_MARKED.getName().fullName.split("\\s+"); + modelManager.updateFilteredContactList(new ContactContainsKeywordsPredicate(Arrays.asList(keywords))); assertFalse(modelManager.equals(new ModelManager(addressBook, userPrefs))); // resets modelManager to initial state for upcoming tests - modelManager.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + modelManager.updateFilteredContactList(PREDICATE_SHOW_ALL_CONTACTS); // different userPrefs -> returns false UserPrefs differentUserPrefs = new UserPrefs(); differentUserPrefs.setAddressBookFilePath(Paths.get("differentFilePath")); assertFalse(modelManager.equals(new ModelManager(addressBook, differentUserPrefs))); + + // different ContactDisplaySetting + modelManager.setContactDisplaySetting(new ContactDisplaySetting(true)); + assertFalse(modelManager.equals(new ModelManager(addressBook, userPrefs))); + + // different EventDisplaySetting + modelManager.setEventDisplaySetting(new EventDisplaySetting(true)); + assertFalse(modelManager.equals(new ModelManager(addressBook, userPrefs))); } } diff --git a/src/test/java/seedu/address/model/person/AddressTest.java b/src/test/java/seedu/address/model/common/AddressTest.java similarity index 97% rename from src/test/java/seedu/address/model/person/AddressTest.java rename to src/test/java/seedu/address/model/common/AddressTest.java index dcd3be87b3a..31320c8df66 100644 --- a/src/test/java/seedu/address/model/person/AddressTest.java +++ b/src/test/java/seedu/address/model/common/AddressTest.java @@ -1,4 +1,4 @@ -package seedu.address.model.person; +package seedu.address.model.common; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/src/test/java/seedu/address/model/person/NameTest.java b/src/test/java/seedu/address/model/common/NameTest.java similarity index 72% rename from src/test/java/seedu/address/model/person/NameTest.java rename to src/test/java/seedu/address/model/common/NameTest.java index c9801392874..ed8720a9764 100644 --- a/src/test/java/seedu/address/model/person/NameTest.java +++ b/src/test/java/seedu/address/model/common/NameTest.java @@ -1,9 +1,12 @@ -package seedu.address.model.person; +package seedu.address.model.common; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.testutil.Assert.assertThrows; +import java.util.Arrays; +import java.util.List; + import org.junit.jupiter.api.Test; public class NameTest { @@ -19,6 +22,19 @@ public void constructor_invalidName_throwsIllegalArgumentException() { assertThrows(IllegalArgumentException.class, () -> new Name(invalidName)); } + @Test + public void containsString() { + Name alice = new Name("Alice"); + + // keywords contained in name + List listOfKeywordsContained = Arrays.asList("aLi", "icE"); + assertTrue(alice.containsString(listOfKeywordsContained)); + + //keywords not contained in name + List noKeywordsContained = Arrays.asList("BoB", "Pan", "Peter"); + assertFalse(alice.containsString(noKeywordsContained)); + } + @Test public void isValidName() { // null name diff --git a/src/test/java/seedu/address/model/common/ZoomLinkTest.java b/src/test/java/seedu/address/model/common/ZoomLinkTest.java new file mode 100644 index 00000000000..eb292217e50 --- /dev/null +++ b/src/test/java/seedu/address/model/common/ZoomLinkTest.java @@ -0,0 +1,79 @@ +package seedu.address.model.common; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +class ZoomLinkTest { + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new ZoomLink(null)); + } + @Test + public void constructor_invalidZoomLink_throwsIllegalArgumentException() { + String invalidZoomLink = ""; + assertThrows(IllegalArgumentException.class, () -> new ZoomLink(invalidZoomLink)); + } + + @Test + public void containsString() { + ZoomLink zoomLink = + new ZoomLink("https://nus-sg.zoom.us/j/83851237332?pwd=c1B0V2FtTXh1MWwyYVJ6TFAzczE4Zz09#success"); + + // keywords contained in zoomLink + List listOfKeywordsContained = Arrays.asList("nus", "ZOOM"); + assertTrue(zoomLink.containsString(listOfKeywordsContained)); + + //keywords not contained in zoomLink + List noKeywordsContained = Arrays.asList("BoB", "Pan", "Peter"); + assertFalse(zoomLink.containsString(noKeywordsContained)); + } + + @Test + public void isValidZoomLink() { + // null zoom link + assertThrows(NullPointerException.class, () -> ZoomLink.isValidZoomLink(null)); + + // blank zoom link + assertFalse(ZoomLink.isValidZoomLink("")); // empty string + assertFalse(ZoomLink.isValidZoomLink(" ")); // spaces only + + // valid zoom link + assertTrue(ZoomLink.isValidZoomLink("googlecom")); + assertTrue(ZoomLink.isValidZoomLink("http:peterjack")); + assertTrue(ZoomLink.isValidZoomLink("https://PeterJack1190.example.com")); // valid header, body and end part + assertTrue(ZoomLink.isValidZoomLink("PeterJack1190.example.com%123")); // valid body and end part without header + assertTrue(ZoomLink.isValidZoomLink("www.PeterJack-1190.example.com")); // hyphen in body part + assertTrue(ZoomLink.isValidZoomLink("www.PeterJack/1190.example.com")); // '/' symbol in body part + assertTrue(ZoomLink.isValidZoomLink("PeterJack-1190.example.com")); // hyphen in local part + assertTrue(ZoomLink.isValidZoomLink("abc.co@m")); // end part can contain symbols + assertTrue(ZoomLink.isValidZoomLink("123.145")); // numeric body and end part + assertTrue(ZoomLink.isValidZoomLink("a1be.d@example1.com")); // end part contains mixture of alphanumeric + + // and special characters + assertTrue(ZoomLink.isValidZoomLink("peter/jack.very-very-very-long-example.com")); // long end part + assertTrue(ZoomLink.isValidZoomLink("if/you/dream/it-you/can-do/it.example.com")); // long body part + assertTrue(ZoomLink.isValidZoomLink("e1234567.u.nus.edu")); // more than one period in domain + assertTrue(ZoomLink.isValidZoomLink( + "https://nus-sg.zoom.us/j/83851237332?pwd=c1B0V2FtTXh1MWwyYVJ6TFAzczE4Zz09#success")); + } + + @Test + public void testEquals() { + ZoomLink link = new ZoomLink("http://zoomlink.com"); + ZoomLink sameLink = new ZoomLink("http://zoomlink.com"); + ZoomLink differentLink = new ZoomLink("http://123.com"); + + assertEquals(link, sameLink); + assertEquals(link, link); + assertNotEquals(link, differentLink); + + } +} diff --git a/src/test/java/seedu/address/model/contact/ContactContainsKeywordsPredicateTest.java b/src/test/java/seedu/address/model/contact/ContactContainsKeywordsPredicateTest.java new file mode 100644 index 00000000000..9bbca955a5b --- /dev/null +++ b/src/test/java/seedu/address/model/contact/ContactContainsKeywordsPredicateTest.java @@ -0,0 +1,104 @@ +package seedu.address.model.contact; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.TypicalContacts.ALICE_MARKED; +import static seedu.address.testutil.TypicalContacts.FIONA; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.testutil.ContactBuilder; + + +public class ContactContainsKeywordsPredicateTest { + + @Test + public void equals() { + List firstPredicateKeywordList = Collections.singletonList("first"); + List secondPredicateKeywordList = Arrays.asList("first", "second"); + + ContactContainsKeywordsPredicate firstPredicate = + new ContactContainsKeywordsPredicate(firstPredicateKeywordList); + ContactContainsKeywordsPredicate secondPredicate = + new ContactContainsKeywordsPredicate(secondPredicateKeywordList); + + // same object -> returns true + assertTrue(firstPredicate.equals(firstPredicate)); + + // same values -> returns true + ContactContainsKeywordsPredicate firstPredicateCopy = + new ContactContainsKeywordsPredicate(firstPredicateKeywordList); + assertTrue(firstPredicate.equals(firstPredicateCopy)); + + // different types -> returns false + assertFalse(firstPredicate.equals(1)); + + // null -> returns false + assertFalse(firstPredicate.equals(null)); + + // different contact -> returns false + assertFalse(firstPredicate.equals(secondPredicate)); + } + + @Test + public void test_fieldContainsKeywords_returnsTrue() { + // One keyword matches name field + ContactContainsKeywordsPredicate predicate = + new ContactContainsKeywordsPredicate(Collections.singletonList("Alice")); + assertTrue(predicate.test(new ContactBuilder().withName("Alice Bob").build())); + + // Multiple keywords match name field + predicate = new ContactContainsKeywordsPredicate(Arrays.asList("Alice", "Bob")); + assertTrue(predicate.test(new ContactBuilder().withName("Alice Bob").build())); + + // Only one matching keyword to name field + predicate = new ContactContainsKeywordsPredicate(Arrays.asList("Bob", "Carol")); + assertTrue(predicate.test(new ContactBuilder().withName("Alice Carol").build())); + + // Mixed-case keywords match name field + predicate = new ContactContainsKeywordsPredicate(Arrays.asList("aLIce", "bOB")); + assertTrue(predicate.test(new ContactBuilder().withName("Alice Bob").build())); + + // Keyword matches address field + predicate = new ContactContainsKeywordsPredicate(); + predicate.setAddressKeywords(Arrays.asList("JURONG", "TOK")); + // Both predicate test on ALICE and BENSON should return true + assertEquals(predicate.test(ALICE_MARKED), predicate.test(FIONA)); + + // keyword match phone number, address and one tag + predicate = new ContactContainsKeywordsPredicate(Arrays.asList("Alex")); + predicate.setPhoneKeywords(Arrays.asList("12345")); + predicate.setEmailKeywords(Arrays.asList("alex@email.com")); + predicate.setAddressKeywords(Arrays.asList("MAIN")); + predicate.setTagKeywords(Arrays.asList("smart", "cca", "friend")); + predicate.setTelegramHandleKeyword(Arrays.asList("alexx")); + predicate.setZoomLinkKeywords(Arrays.asList("meeting.com/123456")); + assertTrue(predicate.test(new ContactBuilder().withName("Alice").withPhone("12345") + .withEmail("alice@email.com").withAddress("Main Street").withTags("friends").build())); + + } + + @Test + public void test_fieldDoesNotContainKeywords_returnsFalse() { + // Zero keywords + ContactContainsKeywordsPredicate predicate = + new ContactContainsKeywordsPredicate(Collections.emptyList()); + assertFalse(predicate.test(new ContactBuilder().withName("Alice").build())); + + // Non-matching keyword to name field + predicate = new ContactContainsKeywordsPredicate(Arrays.asList("Carol")); + assertFalse(predicate.test(new ContactBuilder().withName("Alice Bob").build())); + + // no field match the keywords + predicate = new ContactContainsKeywordsPredicate(Arrays.asList("22345")); + predicate.setEmailKeywords(Arrays.asList("alex@email.com")); + predicate.setTagKeywords(Arrays.asList("smart", "cca")); + assertFalse(predicate.test(new ContactBuilder().withName("Alice").withPhone("12345") + .withEmail("alice@email.com").withAddress("Main Street").withTags("friends").build())); + } +} diff --git a/src/test/java/seedu/address/model/contact/ContactDisplaySettingTest.java b/src/test/java/seedu/address/model/contact/ContactDisplaySettingTest.java new file mode 100644 index 00000000000..ca2eff53601 --- /dev/null +++ b/src/test/java/seedu/address/model/contact/ContactDisplaySettingTest.java @@ -0,0 +1,76 @@ +package seedu.address.model.contact; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class ContactDisplaySettingTest { + + @Test + public void testEquals() { + ContactDisplaySetting contactDisplaySetting1 = new ContactDisplaySetting(false, true, + false, true, false, true); + ContactDisplaySetting contactDisplaySetting1Copy = new ContactDisplaySetting(false, true, + false, true, false, true); + + ContactDisplaySetting contactDisplaySetting2 = new ContactDisplaySetting(true); + ContactDisplaySetting contactDisplaySetting3 = new ContactDisplaySetting(true, true, false, true, false, true); + + //same setting -> equals + assertEquals(contactDisplaySetting1, contactDisplaySetting1Copy); + + assertNotEquals(contactDisplaySetting1, contactDisplaySetting2); + assertNotEquals(contactDisplaySetting1, contactDisplaySetting3); + assertNotEquals(ContactDisplaySetting.DEFAULT_SETTING, contactDisplaySetting3); + + //different object type -> returns false + assertFalse(contactDisplaySetting1.equals(1)); + + //same object type -> returns true + assertTrue(contactDisplaySetting1.equals(contactDisplaySetting1)); + } + + @Test + public void testHashCode() { + ContactDisplaySetting contactDisplaySetting1 = new ContactDisplaySetting(false, true, + false, true, false, true); + ContactDisplaySetting contactDisplaySetting2 = new ContactDisplaySetting(false, true, + false, true, false, true); + ContactDisplaySetting contactDisplaySetting3 = new ContactDisplaySetting(false); + assertEqualHash(contactDisplaySetting1, contactDisplaySetting2); + assertEqualHash(contactDisplaySetting3, ContactDisplaySetting.DEFAULT_SETTING); + } + + @Test + public void testToString() { + String expectedValue = "ContactDisplaySetting{" + + "willDisplayPhone=true" + + ", willDisplayEmail=true" + + ", willDisplayTelegramHandle=true" + + ", willDisplayAddress=true" + + ", willDisplayZoomLink=true" + + ", willDisplayTags=true" + + ", isViewingFull=false" + + '}'; + + String expectedValueView = "ContactDisplaySetting{" + + "willDisplayPhone=true" + + ", willDisplayEmail=true" + + ", willDisplayTelegramHandle=true" + + ", willDisplayAddress=true" + + ", willDisplayZoomLink=true" + + ", willDisplayTags=true" + + ", isViewingFull=true" + + '}'; + + assertEquals(expectedValue, ContactDisplaySetting.DEFAULT_SETTING.toString()); + assertEquals(expectedValueView, new ContactDisplaySetting(true).toString()); + } + + private void assertEqualHash(ContactDisplaySetting c1, ContactDisplaySetting c2) { + assertEquals(c1.hashCode(), c2.hashCode()); + } +} diff --git a/src/test/java/seedu/address/model/contact/ContactTest.java b/src/test/java/seedu/address/model/contact/ContactTest.java new file mode 100644 index 00000000000..fb8bf4d5c90 --- /dev/null +++ b/src/test/java/seedu/address/model/contact/ContactTest.java @@ -0,0 +1,135 @@ +package seedu.address.model.contact; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_ADDRESS_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_EMAIL_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_NAME_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_PHONE_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_TAG_HUSBAND; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalContacts.ALICE_MARKED; +import static seedu.address.testutil.TypicalContacts.BOB; +import static seedu.address.testutil.TypicalEvents.BIRTHDAY_PARTY; +import static seedu.address.testutil.TypicalEvents.CS2101_MEETING; + +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +import seedu.address.testutil.ContactBuilder; + +public class ContactTest { + + @Test + public void asObservableList_modifyList_throwsUnsupportedOperationException() { + Contact contact = new ContactBuilder().build(); + assertThrows(UnsupportedOperationException.class, () -> contact.getTags().remove(0)); + } + + @Test + public void isSameContact() { + // same object -> returns true + assertTrue(ALICE_MARKED.isSameContact(ALICE_MARKED)); + + // null -> returns false + assertFalse(ALICE_MARKED.isSameContact(null)); + + // same name, all other attributes different -> returns true + Contact editedAlice = new ContactBuilder(ALICE_MARKED).withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_BOB) + .withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND).build(); + assertTrue(ALICE_MARKED.isSameContact(editedAlice)); + + // different name, all other attributes same -> returns false + editedAlice = new ContactBuilder(ALICE_MARKED).withName(VALID_NAME_BOB).build(); + assertFalse(ALICE_MARKED.isSameContact(editedAlice)); + + // name differs in case, all other attributes same -> returns false + Contact editedBob = new ContactBuilder(BOB).withName(VALID_NAME_BOB.toLowerCase()).build(); + assertFalse(BOB.isSameContact(editedBob)); + + // name has trailing spaces, all other attributes same -> returns false + String nameWithTrailingSpaces = VALID_NAME_BOB + " "; + editedBob = new ContactBuilder(BOB).withName(nameWithTrailingSpaces).build(); + assertFalse(BOB.isSameContact(editedBob)); + } + + @Test + public void testLinkAndUnlink() { + Contact aliceCopy = new ContactBuilder(ALICE_MARKED).build(); + Contact expectedContact = aliceCopy.linkTo(CS2101_MEETING); + + //link cs2101 meeting to alice + assertTrue(expectedContact.hasLinkTo(CS2101_MEETING)); + assertEquals(expectedContact, ALICE_MARKED.linkTo(CS2101_MEETING)); + assertFalse(expectedContact.hasLinkTo(BIRTHDAY_PARTY)); + + //link birthday party to alice + expectedContact = expectedContact.linkTo(BIRTHDAY_PARTY); + assertTrue(expectedContact.hasLinkTo(BIRTHDAY_PARTY)); + assertEquals(expectedContact, ALICE_MARKED.linkTo(BIRTHDAY_PARTY).linkTo(CS2101_MEETING)); + + //unlink cs2101 meeting from alice + expectedContact = expectedContact.unlink(CS2101_MEETING); + assertFalse(expectedContact.hasLinkTo(CS2101_MEETING)); + assertEquals(expectedContact, ALICE_MARKED.linkTo(BIRTHDAY_PARTY)); + assertTrue(expectedContact.hasLinkTo(BIRTHDAY_PARTY)); + + //clear all links + expectedContact = expectedContact.clearAllLinks(); + assertEquals(expectedContact, ALICE_MARKED); + + } + + @Test + public void constructor_invalidInputs() { + assertThrows(NullPointerException.class, () -> new ContactBuilder().withUuid(null).build()); + assertThrows(NullPointerException.class, () -> new ContactBuilder().withName(null).build()); + assertThrows(NullPointerException.class, () -> new ContactBuilder().withEmail(null).build()); + assertThrows(NullPointerException.class, () -> new ContactBuilder().withLinkedEvents((UUID[]) null).build()); + assertThrows( + NullPointerException.class, () -> new ContactBuilder().withLinkedEvents(null, UUID.randomUUID()).build()); + assertThrows(NullPointerException.class, () -> new ContactBuilder().withTags((String[]) null).build()); + assertThrows(NullPointerException.class, () -> new ContactBuilder().withTags(null, "tag").build()); + } + + @Test + public void equals() { + // same values -> returns true + Contact aliceCopy = new ContactBuilder(ALICE_MARKED).build(); + assertTrue(ALICE_MARKED.equals(aliceCopy)); + + // same object -> returns true + assertTrue(ALICE_MARKED.equals(ALICE_MARKED)); + + // null -> returns false + assertFalse(ALICE_MARKED.equals(null)); + + // different type -> returns false + assertFalse(ALICE_MARKED.equals(5)); + + // different contact -> returns false + assertFalse(ALICE_MARKED.equals(BOB)); + + // different name -> returns false + Contact editedAlice = new ContactBuilder(ALICE_MARKED).withName(VALID_NAME_BOB).build(); + assertFalse(ALICE_MARKED.equals(editedAlice)); + + // different phone -> returns false + editedAlice = new ContactBuilder(ALICE_MARKED).withPhone(VALID_PHONE_BOB).build(); + assertFalse(ALICE_MARKED.equals(editedAlice)); + + // different email -> returns false + editedAlice = new ContactBuilder(ALICE_MARKED).withEmail(VALID_EMAIL_BOB).build(); + assertFalse(ALICE_MARKED.equals(editedAlice)); + + // different address -> returns false + editedAlice = new ContactBuilder(ALICE_MARKED).withAddress(VALID_ADDRESS_BOB).build(); + assertFalse(ALICE_MARKED.equals(editedAlice)); + + // different tags -> returns false + editedAlice = new ContactBuilder(ALICE_MARKED).withTags(VALID_TAG_HUSBAND).build(); + assertFalse(ALICE_MARKED.equals(editedAlice)); + } +} diff --git a/src/test/java/seedu/address/model/person/EmailTest.java b/src/test/java/seedu/address/model/contact/EmailTest.java similarity index 87% rename from src/test/java/seedu/address/model/person/EmailTest.java rename to src/test/java/seedu/address/model/contact/EmailTest.java index bbcc6c8c98e..68ad24c036b 100644 --- a/src/test/java/seedu/address/model/person/EmailTest.java +++ b/src/test/java/seedu/address/model/contact/EmailTest.java @@ -1,9 +1,12 @@ -package seedu.address.model.person; +package seedu.address.model.contact; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.testutil.Assert.assertThrows; +import java.util.Arrays; +import java.util.List; + import org.junit.jupiter.api.Test; public class EmailTest { @@ -19,6 +22,19 @@ public void constructor_invalidEmail_throwsIllegalArgumentException() { assertThrows(IllegalArgumentException.class, () -> new Email(invalidEmail)); } + @Test + public void containsString() { + Email email = new Email("peter_jack@example.com"); + + // keywords contained in email + List listOfKeywordsContained = Arrays.asList("pete", "@EXAMPLE"); + assertTrue(email.containsString(listOfKeywordsContained)); + + //keywords not contained in email + List noKeywordsContained = Arrays.asList("retep", "..", "yahoo"); + assertFalse(email.containsString(noKeywordsContained)); + } + @Test public void isValidEmail() { // null email diff --git a/src/test/java/seedu/address/model/person/PhoneTest.java b/src/test/java/seedu/address/model/contact/PhoneTest.java similarity index 72% rename from src/test/java/seedu/address/model/person/PhoneTest.java rename to src/test/java/seedu/address/model/contact/PhoneTest.java index 8dd52766a5f..5282a193e07 100644 --- a/src/test/java/seedu/address/model/person/PhoneTest.java +++ b/src/test/java/seedu/address/model/contact/PhoneTest.java @@ -1,9 +1,12 @@ -package seedu.address.model.person; +package seedu.address.model.contact; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.testutil.Assert.assertThrows; +import java.util.Arrays; +import java.util.List; + import org.junit.jupiter.api.Test; public class PhoneTest { @@ -19,6 +22,19 @@ public void constructor_invalidPhone_throwsIllegalArgumentException() { assertThrows(IllegalArgumentException.class, () -> new Phone(invalidPhone)); } + @Test + public void containsString() { + Phone phone = new Phone("91234567"); + + // keywords contained in phone + List listOfKeywordsContained = Arrays.asList("123", "912"); + assertTrue(phone.containsString(listOfKeywordsContained)); + + //keywords not contained in phone + List noKeywordsContained = Arrays.asList("999", "..", "0000000"); + assertFalse(phone.containsString(noKeywordsContained)); + } + @Test public void isValidPhone() { // null phone number diff --git a/src/test/java/seedu/address/model/contact/TelegramHandleTest.java b/src/test/java/seedu/address/model/contact/TelegramHandleTest.java new file mode 100644 index 00000000000..013c00eb9b3 --- /dev/null +++ b/src/test/java/seedu/address/model/contact/TelegramHandleTest.java @@ -0,0 +1,78 @@ +package seedu.address.model.contact; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +class TelegramHandleTest { + + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new TelegramHandle(null)); + } + + @Test + public void constructor_invalidHandle_throwsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> new TelegramHandle("")); + } + + @Test + public void containsString() { + TelegramHandle telegramHandle = new TelegramHandle("Beasty"); + + // keywords contained in telegram handle + List listOfKeywordsContained = Arrays.asList("beast", "TY"); + assertTrue(telegramHandle.containsString(listOfKeywordsContained)); + + //keywords not contained in telegram handle + List noKeywordsContained = Arrays.asList("retep", "@BEASTY", "yahoo"); + assertFalse(telegramHandle.containsString(noKeywordsContained)); + } + + @Test + void isValidHandle() { + // null + assertThrows(NullPointerException.class, () -> TelegramHandle.isValidHandle(null)); + + // invalid + assertFalse(TelegramHandle.isValidHandle("")); // empty string + assertFalse(TelegramHandle.isValidHandle(" ")); // spaces + assertFalse(TelegramHandle.isValidHandle("91a5")); // less than 5 characters + assertFalse(TelegramHandle.isValidHandle("tele man")); // spaces + assertFalse(TelegramHandle.isValidHandle("(^///^)3")); // illegal characters + + // valid + assertTrue(TelegramHandle.isValidHandle("beast")); // exactly 5 characters + assertTrue(TelegramHandle.isValidHandle("Beast_69")); // use of capital letters, numbers and underscore + } + + @Test + void getTelegramLink() { + TelegramHandle handle = new TelegramHandle("Beast"); + assertEquals(handle.getTelegramLink(), "https://t.me/Beast"); + } + + @Test + void testToString() { + TelegramHandle handle = new TelegramHandle("Beast"); + assertEquals(handle.toString(), "Beast"); + } + + @Test + void testEquals() { + TelegramHandle handle1 = new TelegramHandle("beast"); + TelegramHandle handle2 = new TelegramHandle("beast"); + TelegramHandle handle3 = new TelegramHandle("Beast"); + String hello = "Hello"; + assertEquals(handle1, handle2); // Same telegram handle + assertNotEquals(handle1, handle3); // Different handle + assertNotEquals(handle1, hello); // Different type + } +} diff --git a/src/test/java/seedu/address/model/contact/UniqueContactListTest.java b/src/test/java/seedu/address/model/contact/UniqueContactListTest.java new file mode 100644 index 00000000000..c406e46c760 --- /dev/null +++ b/src/test/java/seedu/address/model/contact/UniqueContactListTest.java @@ -0,0 +1,203 @@ +package seedu.address.model.contact; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_ADDRESS_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_TAG_HUSBAND; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalContacts.ALICE_MARKED; +import static seedu.address.testutil.TypicalContacts.BOB; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.contact.exceptions.ContactNotFoundException; +import seedu.address.model.contact.exceptions.DuplicateContactException; +import seedu.address.testutil.ContactBuilder; + +public class UniqueContactListTest { + + private final UniqueContactList uniqueContactList = new UniqueContactList(); + + @Test + public void contains_nullContact_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueContactList.contains(null)); + } + + @Test + public void contains_contactNotInList_returnsFalse() { + assertFalse(uniqueContactList.contains(ALICE_MARKED)); + } + + @Test + public void contains_contactInList_returnsTrue() { + uniqueContactList.add(ALICE_MARKED); + assertTrue(uniqueContactList.contains(ALICE_MARKED)); + } + + @Test + public void contains_contactWithSameIdentityFieldsInList_returnsTrue() { + uniqueContactList.add(ALICE_MARKED); + Contact editedAlice = + new ContactBuilder(ALICE_MARKED).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND) + .build(); + assertTrue(uniqueContactList.contains(editedAlice)); + } + + @Test + public void add_nullContact_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueContactList.add(null)); + } + + @Test + public void add_duplicateContact_throwsDuplicateContactException() { + uniqueContactList.add(ALICE_MARKED); + assertThrows(DuplicateContactException.class, () -> uniqueContactList.add(ALICE_MARKED)); + } + + @Test + public void setContact_nullTargetContact_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueContactList.setContact(null, ALICE_MARKED)); + } + + @Test + public void setContact_nullEditedContact_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueContactList.setContact(ALICE_MARKED, null)); + } + + @Test + public void setContact_targetContactNotInList_throwsContactNotFoundException() { + assertThrows(ContactNotFoundException.class, () -> uniqueContactList.setContact(ALICE_MARKED, ALICE_MARKED)); + } + + @Test + public void setContact_editedContactIsSameContact_success() { + uniqueContactList.add(ALICE_MARKED); + uniqueContactList.setContact(ALICE_MARKED, ALICE_MARKED); + UniqueContactList expectedUniqueContactList = new UniqueContactList(); + expectedUniqueContactList.add(ALICE_MARKED); + assertEquals(expectedUniqueContactList, uniqueContactList); + } + + @Test + public void setContact_editedContactHasSameIdentity_success() { + uniqueContactList.add(ALICE_MARKED); + Contact editedAlice = + new ContactBuilder(ALICE_MARKED).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND) + .build(); + uniqueContactList.setContact(ALICE_MARKED, editedAlice); + UniqueContactList expectedUniqueContactList = new UniqueContactList(); + expectedUniqueContactList.add(editedAlice); + assertEquals(expectedUniqueContactList, uniqueContactList); + } + + @Test + public void setContact_editedContactHasDifferentIdentity_success() { + uniqueContactList.add(ALICE_MARKED); + uniqueContactList.setContact(ALICE_MARKED, BOB); + UniqueContactList expectedUniqueContactList = new UniqueContactList(); + expectedUniqueContactList.add(BOB); + assertEquals(expectedUniqueContactList, uniqueContactList); + } + + @Test + public void setContact_editedContactHasNonUniqueIdentity_throwsDuplicateContactException() { + uniqueContactList.add(ALICE_MARKED); + uniqueContactList.add(BOB); + assertThrows(DuplicateContactException.class, () -> uniqueContactList.setContact(ALICE_MARKED, BOB)); + } + + @Test + public void remove_nullContact_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueContactList.remove(null)); + } + + @Test + public void remove_contactDoesNotExist_throwsContactNotFoundException() { + assertThrows(ContactNotFoundException.class, () -> uniqueContactList.remove(ALICE_MARKED)); + } + + @Test + public void remove_existingContact_removesContact() { + uniqueContactList.add(ALICE_MARKED); + uniqueContactList.remove(ALICE_MARKED); + UniqueContactList expectedUniqueContactList = new UniqueContactList(); + assertEquals(expectedUniqueContactList, uniqueContactList); + } + + @Test + public void setContacts_nullUniqueContactList_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueContactList.setContacts((UniqueContactList) null)); + } + + @Test + public void setContacts_uniqueContactList_replacesOwnListWithProvidedUniqueContactList() { + uniqueContactList.add(ALICE_MARKED); + UniqueContactList expectedUniqueContactList = new UniqueContactList(); + expectedUniqueContactList.add(BOB); + uniqueContactList.setContacts(expectedUniqueContactList); + assertEquals(expectedUniqueContactList, uniqueContactList); + } + + @Test + public void setContacts_nullList_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueContactList.setContacts((List) null)); + } + + @Test + public void setContacts_list_replacesOwnListWithProvidedList() { + uniqueContactList.add(ALICE_MARKED); + List contactList = Collections.singletonList(BOB); + uniqueContactList.setContacts(contactList); + UniqueContactList expectedUniqueContactList = new UniqueContactList(); + expectedUniqueContactList.add(BOB); + assertEquals(expectedUniqueContactList, uniqueContactList); + } + + @Test + public void setContacts_listWithDuplicateContacts_throwsDuplicateContactException() { + List listWithDuplicateContacts = Arrays.asList(ALICE_MARKED, ALICE_MARKED); + assertThrows(DuplicateContactException.class, () -> uniqueContactList.setContacts(listWithDuplicateContacts)); + } + + @Test + public void asUnmodifiableObservableList_modifyList_throwsUnsupportedOperationException() { + assertThrows(UnsupportedOperationException.class, () + -> uniqueContactList.asUnmodifiableObservableList().remove(0)); + } + + @Test + public void test_updateContactMap() { + uniqueContactList.add(ALICE_MARKED); + uniqueContactList.updateContactMap(); + assertEquals(ALICE_MARKED, Contact.findByUuid(ALICE_MARKED.getUuid())); + } + + @Test + public void test_hashCode() { + UniqueContactList uniqueContactListCopy = new UniqueContactList(); + assertEquals(uniqueContactList.hashCode(), uniqueContactListCopy.hashCode()); + + uniqueContactListCopy.add(ALICE_MARKED); + uniqueContactList.add(ALICE_MARKED); + assertEquals(uniqueContactList.hashCode(), uniqueContactListCopy.hashCode()); + + uniqueContactList.add(BOB); + assertNotEquals(uniqueContactList.hashCode(), uniqueContactListCopy.hashCode()); + } + + @Test + public void test_iterator() { + //empty uniqueContactList + assertFalse(uniqueContactList.iterator().hasNext()); + + //non-empty uniqueContactList + uniqueContactList.add(ALICE_MARKED); + assertTrue(uniqueContactList.iterator().hasNext()); + } +} diff --git a/src/test/java/seedu/address/model/event/DateAndTimeTest.java b/src/test/java/seedu/address/model/event/DateAndTimeTest.java new file mode 100644 index 00000000000..fe8b462b6d7 --- /dev/null +++ b/src/test/java/seedu/address/model/event/DateAndTimeTest.java @@ -0,0 +1,140 @@ +package seedu.address.model.event; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +class DateAndTimeTest { + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new DateAndTime(null)); + } + + @Test + public void constructor_invalidZoomLink_throwsIllegalArgumentException() { + String emptyDateTime = ""; + assertThrows(IllegalArgumentException.class, () -> new DateAndTime(emptyDateTime)); + } + + @Test + public void containsString() { + DateAndTime dateAndTime = new DateAndTime("01-12-2012 11:22"); + + // keywords contained in dateAndTime + List listOfKeywordsContained = Arrays.asList("01-10-20", "11:"); + assertTrue(dateAndTime.containsString(listOfKeywordsContained)); + + //keywords not contained in dateAndTime + List noKeywordsContained = Arrays.asList("morning", "11:3", "11:21"); + assertFalse(dateAndTime.containsString(noKeywordsContained)); + } + + @Test + public void isValidDateAndTime() { + // null DateAndTime + assertThrows(NullPointerException.class, () -> DateAndTime.isValidDateTime(null)); + + // blank DateAndTime + assertFalse(DateAndTime.isValidDateTime("")); // empty string + assertFalse(DateAndTime.isValidDateTime(" ")); // spaces only + + // missing parts + assertFalse(DateAndTime.isValidDateTime("11-10-2021")); // missing time + assertFalse(DateAndTime.isValidDateTime("11-10 12:10")); // missing year + assertFalse(DateAndTime.isValidDateTime("11-10")); // missing year and time + + // invalid date + assertFalse(DateAndTime.isValidDateTime("2010/12/21 12:10")); // invalid connector + assertFalse(DateAndTime.isValidDateTime(" 01-12-2012 12:05")); // leading space + assertFalse(DateAndTime.isValidDateTime("01-12-2012 12:05 ")); // trailing space + assertFalse(DateAndTime.isValidDateTime("2021 Oct 2 12:10")); // wrong date format + assertFalse(DateAndTime.isValidDateTime("%01-13-2012 10:15")); // include invalid symbol '%' + assertFalse(DateAndTime.isValidDateTime("1-13-2012 11:15")); // missing leading 0 + assertFalse(DateAndTime.isValidDateTime("01-12-20001 11:00")); // year can only take in 4-digit numbers + assertFalse(DateAndTime.isValidDateTime("00-12-2021 11:00")); // date must start from 0 + assertFalse(DateAndTime.isValidDateTime("31-02-2000 11:00")); // 31 Feb is not a valid date + assertFalse(DateAndTime.isValidDateTime("29-02-2021 11:11")); // invalid date 29th feb for non-leap year + + // invalid time + assertFalse(DateAndTime.isValidDateTime("01-12-2012 11:15pm")); // invalid time format + assertFalse(DateAndTime.isValidDateTime("01-12-2021 24:00")); // time must be between 00:00 and 23:59 + assertFalse(DateAndTime.isValidDateTime("01-12-2021 15:60")); // time must be between 00:00 and 23:59 + assertFalse(DateAndTime.isValidDateTime("01-12-2021 3:16")); // missing leading 0 for hour input + + + + // valid DateAndTime + assertTrue(DateAndTime.isValidDateTime("01-01-2021 11:00")); + assertTrue(DateAndTime.isValidDateTime("31-12-1090 10:05")); + assertTrue(DateAndTime.isValidDateTime("31-03-2012 11:22")); + assertTrue(DateAndTime.isValidDateTime("01-12-9000 11:00")); // year can take in any 4 digits number + assertTrue(DateAndTime.isValidDateTime("29-02-2024 11:11")); // leap year allows 29th feb + assertTrue(DateAndTime.isValidDateTime("20-10-2021 00:00")); + assertTrue(DateAndTime.isValidDateTime("20-10-2021 23:59")); + + } + + @Test + public void isBefore() { + DateAndTime firstDateTime = new DateAndTime("30-09-2021 22:50"); + DateAndTime secondDateTime = new DateAndTime("30-09-2021 22:55"); + DateAndTime thirdDateTime = new DateAndTime("30-10-2021 02:30"); + + assertTrue(firstDateTime.isBefore(secondDateTime)); + assertTrue(firstDateTime.isBefore(thirdDateTime)); + assertFalse(thirdDateTime.isBefore(secondDateTime)); + } + + @Test + public void compareTo() { + DateAndTime firstDateTime = new DateAndTime("30-09-2021 22:50"); + DateAndTime secondDateTime = new DateAndTime("30-09-2021 22:55"); + DateAndTime secondDuplicate = new DateAndTime("30-09-2021 22:55"); + DateAndTime thirdDateTime = new DateAndTime("30-10-2021 02:30"); + + assertTrue(firstDateTime.compareTo(secondDateTime) < 0); + assertTrue(firstDateTime.compareTo(thirdDateTime) < 0); + assertTrue(thirdDateTime.compareTo(firstDateTime) > 0); + assertEquals(0, secondDateTime.compareTo(secondDuplicate)); + } + + + + @Test + public void testEquals() { + DateAndTime dateTime = new DateAndTime("30-09-2021 22:50"); + DateAndTime dateTimeCopy = new DateAndTime("30-09-2021 22:50"); + DateAndTime differentDate = new DateAndTime("30-12-2021 22:55"); + DateAndTime differentTime = new DateAndTime("30-09-2021 00:30"); + DateAndTime differentDateTime = new DateAndTime("30-12-2021 00:30"); + + // same object -> returns true + assertTrue(dateTime.equals(dateTime)); + + // null -> returns false + assertFalse(dateTime.equals(null)); + + // same values -> returns true + assertTrue(dateTime.equals(dateTimeCopy)); + + // different type -> returns false + assertFalse(dateTime.equals(1)); + + // different date -> returns false + assertFalse(dateTime.equals(differentDate)); + + // different time -> returns false + assertFalse(dateTime.equals(differentTime)); + + // different date and time -> returns false + assertFalse(dateTime.equals(differentDateTime)); + + } +} + diff --git a/src/test/java/seedu/address/model/event/DescriptionTest.java b/src/test/java/seedu/address/model/event/DescriptionTest.java new file mode 100644 index 00000000000..aafc301e117 --- /dev/null +++ b/src/test/java/seedu/address/model/event/DescriptionTest.java @@ -0,0 +1,75 @@ +package seedu.address.model.event; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +class DescriptionTest { + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new Description(null)); + } + + @Test + public void constructor_invalidDescription_throwsIllegalArgumentException() { + String invalidDescription = ""; + assertThrows(IllegalArgumentException.class, () -> new Description(invalidDescription)); + } + + @Test + public void validDescription_returnsValueInString() { + Description description = new Description("description 1"); + String expectedValue = description.value; + + assertEquals(expectedValue, description.toString()); + } + + @Test + public void isValidDescription() { + // null description + assertThrows(NullPointerException.class, () -> Description.isValidDescription(null)); + + // invalid descriptions + assertFalse(Description.isValidDescription("")); // empty string + assertFalse(Description.isValidDescription(" ")); // spaces only + + // valid descriptions + assertTrue(Description.isValidDescription("This is a description.")); + assertTrue(Description.isValidDescription("This description contains %,?,()")); + assertTrue(Description.isValidDescription("-")); // one character + assertTrue(Description.isValidDescription("This is a very long description that serves no purpose except" + + " to test if long description is possible.")); // long description + } + + @Test + public void containsString() { + Description description = new Description("description 123"); + + // keywords contained in description + List listOfKeywordsContained = Arrays.asList("desc", " 23"); + assertTrue(description.containsString(listOfKeywordsContained)); + + //keywords not contained in description + List noKeywordsContained = Arrays.asList("dt", "blah", "descpt"); + assertFalse(description.containsString(noKeywordsContained)); + } + + @Test + public void testEquals() { + Description description = new Description("description 1"); + Description sameDescription = new Description("description 1"); + Description differentDescription = new Description("description 2"); + + assertEquals(description, sameDescription); + assertEquals(description, description); + assertNotEquals(description, differentDescription); + } + +} diff --git a/src/test/java/seedu/address/model/event/EndDateTimeTest.java b/src/test/java/seedu/address/model/event/EndDateTimeTest.java new file mode 100644 index 00000000000..ed02d86704d --- /dev/null +++ b/src/test/java/seedu/address/model/event/EndDateTimeTest.java @@ -0,0 +1,92 @@ +package seedu.address.model.event; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +class EndDateTimeTest { + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new EndDateTime(null)); + } + + @Test + public void constructor_invalidZoomLink_throwsIllegalArgumentException() { + String emptyDateTime = ""; + assertThrows(IllegalArgumentException.class, () -> new EndDateTime(emptyDateTime)); + } + + @Test + public void containsString() { + EndDateTime dateAndTime = new EndDateTime("01-12-2012 11:22"); + + // keywords contained in dateAndTime + List listOfKeywordsContained = Arrays.asList("01-10-20", "11:"); + assertTrue(dateAndTime.containsString(listOfKeywordsContained)); + + //keywords not contained in dateAndTime + List noKeywordsContained = Arrays.asList("morning", "11:3", "11:21"); + assertFalse(dateAndTime.containsString(noKeywordsContained)); + } + + @Test + public void isValidEndDateTime() { + // null EndDateTime + assertThrows(NullPointerException.class, () -> EndDateTime.isValidDateTime(null)); + + // blank EndDateTime + assertFalse(EndDateTime.isValidDateTime("")); // empty string + assertFalse(EndDateTime.isValidDateTime(" ")); // spaces only + + // missing parts + assertFalse(EndDateTime.isValidDateTime("11-10-2021")); // missing time + assertFalse(EndDateTime.isValidDateTime("11-10 12:10")); // missing year + assertFalse(EndDateTime.isValidDateTime("11-10")); // missing year and time + + // invalid parts + assertFalse(EndDateTime.isValidDateTime("2010/12/21 12:10")); // invalid connector + assertFalse(EndDateTime.isValidDateTime("2012-12-20 12:10")); // wrong order + assertFalse(EndDateTime.isValidDateTime("01-13-2012")); // wrong order of date and month + assertFalse(EndDateTime.isValidDateTime(" 01-13-2012 12:05")); // leading space + assertFalse(EndDateTime.isValidDateTime("01-13-2012 12:05 ")); // trailing space + assertFalse(EndDateTime.isValidDateTime("2021 Oct 2 12:10")); // wrong date format + assertFalse(EndDateTime.isValidDateTime("%01-13-2012 10:15")); // include invalid symbol '%' + assertFalse(EndDateTime.isValidDateTime("01-13-2012 11:15pm")); // invalid time format + assertFalse(EndDateTime.isValidDateTime("1-13-2012 11:15")); // missing leading 0 + + // valid EndDateTime + assertTrue(EndDateTime.isValidDateTime("01-12-2012 11:00")); + assertTrue(EndDateTime.isValidDateTime("01-12-2012 10:05")); + assertTrue(EndDateTime.isValidDateTime("01-12-2012 11:22")); + } + + @Test + public void isBefore() { + EndDateTime firstDateTime = new EndDateTime("30-09-2021 22:50"); + EndDateTime secondDateTime = new EndDateTime("30-09-2021 22:55"); + EndDateTime thirdDateTime = new EndDateTime("30-10-2021 02:30"); + + assertTrue(firstDateTime.isBefore(secondDateTime)); + assertTrue(firstDateTime.isBefore(thirdDateTime)); + assertFalse(thirdDateTime.isBefore(secondDateTime)); + } + + @Test + public void compareTo() { + DateAndTime firstDateTime = new DateAndTime("30-09-2021 22:50"); + DateAndTime secondDateTime = new DateAndTime("30-09-2021 22:55"); + DateAndTime secondDuplicate = new DateAndTime("30-09-2021 22:55"); + DateAndTime thirdDateTime = new DateAndTime("30-10-2021 02:30"); + + assertTrue(firstDateTime.compareTo(secondDateTime) < 0); + assertTrue(firstDateTime.compareTo(thirdDateTime) < 0); + assertTrue(thirdDateTime.compareTo(firstDateTime) > 0); + assertEquals(0, secondDateTime.compareTo(secondDuplicate)); + } +} diff --git a/src/test/java/seedu/address/model/event/EventChangerTest.java b/src/test/java/seedu/address/model/event/EventChangerTest.java new file mode 100644 index 00000000000..5c374913722 --- /dev/null +++ b/src/test/java/seedu/address/model/event/EventChangerTest.java @@ -0,0 +1,90 @@ +package seedu.address.model.event; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Objects; + +import org.junit.jupiter.api.Test; + +import seedu.address.testutil.TypicalEvents; + +class EventChangerTest { + private final EventChanger clearEventChanger = EventChanger.clearEventChanger(); + private final EventChanger deleteEventChanger = EventChanger.deleteEventChanger(TypicalEvents.CS2101_MEETING); + private final EventChanger addEventChanger = EventChanger.addEventChanger(TypicalEvents.CS2101_MEETING); + private final EventChanger editEventChanger = EventChanger.editEventChanger( + TypicalEvents.CS2101_MEETING, + TypicalEvents.TEAM_MEETING); + + @Test + public void clearEventChanger() { + assertEquals(clearEventChanger, EventChanger.clearEventChanger()); + assertNotEquals(clearEventChanger, deleteEventChanger); + + assertNull(clearEventChanger.getNewEvent()); + assertNull(clearEventChanger.getOldEvent()); + + assertTrue(clearEventChanger.isClearing()); + assertFalse(clearEventChanger.isAdding()); + assertFalse(clearEventChanger.isEditing()); + assertFalse(clearEventChanger.isDeleting()); + } + + @Test + public void deleteEventChanger() { + assertEquals(deleteEventChanger, EventChanger.deleteEventChanger(TypicalEvents.CS2101_MEETING)); + assertNotEquals(deleteEventChanger, editEventChanger); + + assertNull(deleteEventChanger.getNewEvent()); + assertEquals(deleteEventChanger.getOldEvent(), TypicalEvents.CS2101_MEETING); + + assertFalse(deleteEventChanger.isClearing()); + assertFalse(deleteEventChanger.isAdding()); + assertFalse(deleteEventChanger.isEditing()); + assertTrue(deleteEventChanger.isDeleting()); + } + + @Test + public void editEventChanger() { + assertEquals(editEventChanger, EventChanger.editEventChanger( + TypicalEvents.CS2101_MEETING, + TypicalEvents.TEAM_MEETING)); + assertNotEquals(editEventChanger, addEventChanger); + + assertEquals(editEventChanger.getNewEvent(), TypicalEvents.TEAM_MEETING); + assertEquals(editEventChanger.getOldEvent(), TypicalEvents.CS2101_MEETING); + + assertFalse(editEventChanger.isClearing()); + assertFalse(editEventChanger.isAdding()); + assertTrue(editEventChanger.isEditing()); + assertFalse(editEventChanger.isDeleting()); + } + + @Test + public void addEventChanger() { + assertEquals(addEventChanger, EventChanger.addEventChanger(TypicalEvents.CS2101_MEETING)); + assertNotEquals(addEventChanger, clearEventChanger); + + assertNull(addEventChanger.getOldEvent()); + assertEquals(addEventChanger.getNewEvent(), TypicalEvents.CS2101_MEETING); + + assertFalse(addEventChanger.isClearing()); + assertTrue(addEventChanger.isAdding()); + assertFalse(addEventChanger.isEditing()); + assertFalse(addEventChanger.isDeleting()); + } + + @Test + public void testHashCode() { + assertEquals(Objects.hash(true, null, null), clearEventChanger.hashCode()); + assertEquals(Objects.hash(false, TypicalEvents.CS2101_MEETING, null), deleteEventChanger.hashCode()); + assertEquals(Objects.hash(false, null, TypicalEvents.CS2101_MEETING), addEventChanger.hashCode()); + assertEquals( + Objects.hash(false, TypicalEvents.CS2101_MEETING, TypicalEvents.TEAM_MEETING), + editEventChanger.hashCode()); + } +} diff --git a/src/test/java/seedu/address/model/event/EventContainsKeywordsPredicateTest.java b/src/test/java/seedu/address/model/event/EventContainsKeywordsPredicateTest.java new file mode 100644 index 00000000000..89b7d3ab7b4 --- /dev/null +++ b/src/test/java/seedu/address/model/event/EventContainsKeywordsPredicateTest.java @@ -0,0 +1,114 @@ +package seedu.address.model.event; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.TypicalEvents.CS2100_CONSULTATION; +import static seedu.address.testutil.TypicalEvents.CS2103_MIDTERM_MARKED; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.testutil.EventBuilder; + +public class EventContainsKeywordsPredicateTest { + + @Test + public void equals() { + List firstPredicateKeywordList = Collections.singletonList("first"); + List secondPredicateKeywordList = Arrays.asList("first", "second"); + + EventContainsKeywordsPredicate firstPredicate = + new EventContainsKeywordsPredicate(firstPredicateKeywordList); + EventContainsKeywordsPredicate secondPredicate = + new EventContainsKeywordsPredicate(secondPredicateKeywordList); + + // same object -> returns true + assertTrue(firstPredicate.equals(firstPredicate)); + + // same values -> returns true + EventContainsKeywordsPredicate firstPredicateCopy = + new EventContainsKeywordsPredicate(firstPredicateKeywordList); + assertTrue(firstPredicate.equals(firstPredicateCopy)); + + // different types -> returns false + assertFalse(firstPredicate.equals(1)); + + // null -> returns false + assertFalse(firstPredicate.equals(null)); + + // different Event -> returns false + assertFalse(firstPredicate.equals(secondPredicate)); + } + + @Test + public void test_fieldContainsKeywords_returnsTrue() { + // One keyword matches name field + EventContainsKeywordsPredicate predicate = + new EventContainsKeywordsPredicate(Arrays.asList("CS2103T", "midterms")); + assertTrue(predicate.test(new EventBuilder().withName("CS2103T midterms").build())); + + // Multiple keywords match name field + predicate = new EventContainsKeywordsPredicate(Arrays.asList("cs2103t", "midterms")); + assertTrue(predicate.test(new EventBuilder().withName("cs2103t midterms").build())); + + // Only one matching keyword to name field + predicate = new EventContainsKeywordsPredicate(Arrays.asList("consultation", "weekly")); + assertTrue(predicate.test(new EventBuilder().withName("consultation weekly").build())); + + // Mixed-case keywords matching name field + predicate = new EventContainsKeywordsPredicate(Arrays.asList("CONsult", "WeEek")); + assertTrue(predicate.test(new EventBuilder().withName("CONsult WeEek").build())); + + // Keyword matches tag field + predicate = new EventContainsKeywordsPredicate(); + predicate.setTagKeywords(Arrays.asList("SCHOOL", "EXAM")); + // Both predicate test on CS2103T_MIDTERMS and CS2100_CONSULTATION should return true + assertEquals(predicate.test(CS2100_CONSULTATION), predicate.test(CS2103_MIDTERM_MARKED)); + + // Keywords match address and description, but does not match name + predicate = new EventContainsKeywordsPredicate(Arrays.asList("cs2222")); + predicate.setStartDateTimeKeywords(Arrays.asList("11-10-2021")); + predicate.setEndDateTimeKeywords(Arrays.asList("21-10-2021")); + predicate.setAddressKeywords(Arrays.asList("street")); + predicate.setDescriptionKeywords(Arrays.asList("DUEEET")); + predicate.setZoomLinkKeywords(Arrays.asList("zoom.com/12345678")); + assertTrue(predicate.test(new EventBuilder().withName("CS2103T midterms") + .withStartDateAndTime("01-10-2021 11:10") + .withEndDateAndTime("01-10-2021 12:10") + .withAddress("Main Street") + .withDescription("i can dueeet") + .build())); + } + + @Test + public void test_fieldDoesNotContainKeywords_returnsFalse() { + // Zero keywords + EventContainsKeywordsPredicate predicate = + new EventContainsKeywordsPredicate(Collections.emptyList()); + assertFalse(predicate.test(new EventBuilder().withName("CS2103T").build())); + + // Non-matching keyword for name field + predicate = new EventContainsKeywordsPredicate(Arrays.asList("Carol")); + assertFalse(predicate.test(new EventBuilder().withName("Alice Bob").build())); + + // no fields match the keyword + predicate = new EventContainsKeywordsPredicate(Arrays.asList("cs2222")); + predicate.setStartDateTimeKeywords(Arrays.asList("11-10-2021")); + predicate.setEndDateTimeKeywords(Arrays.asList("21-10-2021")); + predicate.setAddressKeywords(Arrays.asList("overseas")); + predicate.setDescriptionKeywords(Arrays.asList("DUEEEEEET")); + predicate.setZoomLinkKeywords(Arrays.asList("zoom.com/12345678")); + assertFalse(predicate.test(new EventBuilder().withName("CS2103T midterms") + .withStartDateAndTime("01-10-2021 11:10") + .withEndDateAndTime("01-10-2021 12:10") + .withAddress("Main Street") + .withDescription("i can dueeet") + .build())); + + } + +} diff --git a/src/test/java/seedu/address/model/event/EventDisplaySettingTest.java b/src/test/java/seedu/address/model/event/EventDisplaySettingTest.java new file mode 100644 index 00000000000..0b22cec0090 --- /dev/null +++ b/src/test/java/seedu/address/model/event/EventDisplaySettingTest.java @@ -0,0 +1,78 @@ +package seedu.address.model.event; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class EventDisplaySettingTest { + + @Test + public void testEquals() { + EventDisplaySetting eventDisplaySetting1 = new EventDisplaySetting(false, true, + false, true, false, true); + EventDisplaySetting eventDisplaySetting1Copy = new EventDisplaySetting(false, true, + false, true, false, true); + EventDisplaySetting eventDisplaySetting2 = new EventDisplaySetting(true); + EventDisplaySetting eventDisplaySetting3 = new EventDisplaySetting(true, true, + false, true, false, true); + + //same settings -> equals + assertEquals(eventDisplaySetting1, eventDisplaySetting1Copy); + + assertNotEquals(eventDisplaySetting1, eventDisplaySetting2); + assertNotEquals(eventDisplaySetting1, eventDisplaySetting3); + assertNotEquals(EventDisplaySetting.DEFAULT_SETTING, eventDisplaySetting2); + + //different object type -> returns false + assertFalse(eventDisplaySetting1.equals(1)); + + //same object type -> returns true + assertTrue(eventDisplaySetting1.equals(eventDisplaySetting1)); + } + + @Test + public void testHashCode() { + EventDisplaySetting eventDisplaySetting1 = new EventDisplaySetting(false, true, + false, true, false, true); + EventDisplaySetting eventDisplaySetting2 = new EventDisplaySetting(false, true, + false, true, false, true); + EventDisplaySetting eventDisplaySetting3 = new EventDisplaySetting(false); + assertEqualHash(eventDisplaySetting1, eventDisplaySetting2); + assertEqualHash(eventDisplaySetting3, EventDisplaySetting.DEFAULT_SETTING); + } + + @Test + public void testToString() { + String expectedValue = "EventDisplaySetting{" + + "willDisplayStartDateTime=true" + + ", willDisplayEndDateTime=true" + + ", willDisplayDescription=true" + + ", willDisplayAddress=true" + + ", willDisplayZoomLink=true" + + ", willDisplayTags=true" + + ", isViewingFull=false" + + '}'; + + String expectedValueView = "EventDisplaySetting{" + + "willDisplayStartDateTime=true" + + ", willDisplayEndDateTime=true" + + ", willDisplayDescription=true" + + ", willDisplayAddress=true" + + ", willDisplayZoomLink=true" + + ", willDisplayTags=true" + + ", isViewingFull=true" + + '}'; + + EventDisplaySetting eventDisplaySettingViewEnable = new EventDisplaySetting(true); + + assertEquals(expectedValue, EventDisplaySetting.DEFAULT_SETTING.toString()); + assertEquals(expectedValueView, eventDisplaySettingViewEnable.toString()); + } + + private void assertEqualHash(EventDisplaySetting e1, EventDisplaySetting e2) { + assertEquals(e1.hashCode(), e2.hashCode()); + } +} diff --git a/src/test/java/seedu/address/model/event/EventTest.java b/src/test/java/seedu/address/model/event/EventTest.java new file mode 100644 index 00000000000..6a640008da8 --- /dev/null +++ b/src/test/java/seedu/address/model/event/EventTest.java @@ -0,0 +1,135 @@ +package seedu.address.model.event; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_ADDRESS_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_NAME_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_START_DATE_TIME_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_TAG_EXAMS; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_ZOOM_EXAM; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalContacts.AMY; +import static seedu.address.testutil.TypicalContacts.BOB; +import static seedu.address.testutil.TypicalEvents.CS2103_MIDTERM_MARKED; +import static seedu.address.testutil.TypicalEvents.FOOTBALL_PRACTICE; + +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +import seedu.address.testutil.EventBuilder; + +class EventTest { + + @Test + public void asObservableList_modifyList_throwsUnsupportedOperationException() { + Event event = new EventBuilder().build(); + assertThrows(UnsupportedOperationException.class, () -> event.getTags().remove(0)); + } + + @Test + public void constructor_invalidInputs() { + assertThrows(NullPointerException.class, () -> new EventBuilder().withName(null).build()); + assertThrows(NullPointerException.class, () -> new EventBuilder().withStartDateAndTime(null).build()); + assertThrows(NullPointerException.class, () -> new EventBuilder().withUuid(null).build()); + assertThrows(NullPointerException.class, () -> new EventBuilder().withLinkedContacts((UUID[]) null).build()); + assertThrows( + NullPointerException.class, () -> new EventBuilder().withLinkedContacts(null, UUID.randomUUID()).build()); + assertThrows(NullPointerException.class, () -> new EventBuilder().withTags((String[]) null).build()); + assertThrows(NullPointerException.class, () -> new EventBuilder().withTags(null, "tag").build()); + } + + @Test + public void testIsSameEvent() { + // same object -> returns true + assertTrue(FOOTBALL_PRACTICE.isSameEvent(FOOTBALL_PRACTICE)); + + // null -> returns false + assertFalse(FOOTBALL_PRACTICE.isSameEvent(null)); + + // same name, all other attributes different -> returns true + Event editedPractice = new EventBuilder(FOOTBALL_PRACTICE).withZoomLink(VALID_ZOOM_EXAM) + .withStartDateAndTime(VALID_START_DATE_TIME_EXAM) + .withAddress(VALID_ADDRESS_EXAM).withTags(VALID_TAG_EXAMS).build(); + assertTrue(FOOTBALL_PRACTICE.isSameEvent(editedPractice)); + + // different name, all other attributes same -> returns false + editedPractice = new EventBuilder(FOOTBALL_PRACTICE).withName(VALID_NAME_EXAM).build(); + assertFalse(FOOTBALL_PRACTICE.isSameEvent(editedPractice)); + + // name differs in case, all other attributes same -> returns false + Event editedMidterm = new EventBuilder(CS2103_MIDTERM_MARKED).withName(VALID_NAME_EXAM.toLowerCase()).build(); + assertFalse(CS2103_MIDTERM_MARKED.isSameEvent(editedMidterm)); + + // name has trailing spaces, all other attributes same -> returns false + String nameWithTrailingSpaces = VALID_NAME_EXAM + " "; + editedMidterm = new EventBuilder(CS2103_MIDTERM_MARKED).withName(nameWithTrailingSpaces).build(); + assertFalse(CS2103_MIDTERM_MARKED.isSameEvent(editedMidterm)); + } + + @Test + public void equals() { + // same values -> returns true + Event midtermCopy = new EventBuilder(CS2103_MIDTERM_MARKED).build(); + assertTrue(CS2103_MIDTERM_MARKED.equals(midtermCopy)); + + // same object -> returns true + assertTrue(CS2103_MIDTERM_MARKED.equals(CS2103_MIDTERM_MARKED)); + + // null -> returns false + assertFalse(CS2103_MIDTERM_MARKED.equals(null)); + + // different type -> returns false + assertFalse(CS2103_MIDTERM_MARKED.equals(5)); + + // different event -> returns false + assertFalse(CS2103_MIDTERM_MARKED.equals(FOOTBALL_PRACTICE)); + + // different name -> returns false + Event editedPractice = new EventBuilder(FOOTBALL_PRACTICE).withName(VALID_NAME_EXAM).build(); + assertFalse(FOOTBALL_PRACTICE.equals(editedPractice)); + + // different address -> returns false + editedPractice = new EventBuilder(FOOTBALL_PRACTICE).withAddress(VALID_ADDRESS_EXAM).build(); + assertFalse(FOOTBALL_PRACTICE.equals(editedPractice)); + + // different email -> returns false + editedPractice = new EventBuilder(FOOTBALL_PRACTICE).withZoomLink(VALID_ZOOM_EXAM).build(); + assertFalse(FOOTBALL_PRACTICE.equals(editedPractice)); + + // different start time and date -> returns false + editedPractice = new EventBuilder(FOOTBALL_PRACTICE).withStartDateAndTime(VALID_START_DATE_TIME_EXAM).build(); + assertFalse(FOOTBALL_PRACTICE.equals(editedPractice)); + + // different tags -> returns false + editedPractice = new EventBuilder(FOOTBALL_PRACTICE).withTags(VALID_TAG_EXAMS).build(); + assertFalse(FOOTBALL_PRACTICE.equals(editedPractice)); + } + + @Test + void testLinkAndUnlink() { + Event midtermCopy = new EventBuilder(CS2103_MIDTERM_MARKED).build(); + Event expectedEvent = midtermCopy.linkTo(AMY); + + //link Amy to CS2103 Midterm + assertTrue(expectedEvent.hasLinkTo(AMY)); + assertEquals(expectedEvent, CS2103_MIDTERM_MARKED.linkTo(AMY)); + assertFalse(expectedEvent.hasLinkTo(BOB)); + + //link Bob to CS2103 Midterm + expectedEvent = expectedEvent.linkTo(BOB); + assertEquals(expectedEvent, CS2103_MIDTERM_MARKED.linkTo(AMY).linkTo(BOB)); + assertTrue(expectedEvent.hasLinkTo(BOB)); + + //unlink Amy from CS2103 Midterm + expectedEvent = expectedEvent.unlink(AMY); + assertTrue(expectedEvent.hasLinkTo(BOB)); + assertEquals(expectedEvent, CS2103_MIDTERM_MARKED.linkTo(BOB)); + assertFalse(expectedEvent.hasLinkTo(AMY)); + + expectedEvent = expectedEvent.clearAllLinks(); + assertEquals(expectedEvent, CS2103_MIDTERM_MARKED); + } + +} diff --git a/src/test/java/seedu/address/model/event/StartDateTimeTest.java b/src/test/java/seedu/address/model/event/StartDateTimeTest.java new file mode 100644 index 00000000000..7bdf17fe63a --- /dev/null +++ b/src/test/java/seedu/address/model/event/StartDateTimeTest.java @@ -0,0 +1,92 @@ +package seedu.address.model.event; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +class StartDateTimeTest { + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new StartDateTime(null)); + } + + @Test + public void constructor_invalidZoomLink_throwsIllegalArgumentException() { + String emptyDateTime = ""; + assertThrows(IllegalArgumentException.class, () -> new StartDateTime(emptyDateTime)); + } + + @Test + public void containsString() { + StartDateTime dateAndTime = new StartDateTime("01-12-2012 11:22"); + + // keywords contained in dateAndTime + List listOfKeywordsContained = Arrays.asList("01-10-20", "11:"); + assertTrue(dateAndTime.containsString(listOfKeywordsContained)); + + //keywords not contained in dateAndTime + List noKeywordsContained = Arrays.asList("morning", "11:3", "11:21"); + assertFalse(dateAndTime.containsString(noKeywordsContained)); + } + + @Test + public void isValidStartDateTime() { + // null StartDateTime + assertThrows(NullPointerException.class, () -> StartDateTime.isValidDateTime(null)); + + // blank StartDateTime + assertFalse(StartDateTime.isValidDateTime("")); // empty string + assertFalse(StartDateTime.isValidDateTime(" ")); // spaces only + + // missing parts + assertFalse(StartDateTime.isValidDateTime("11-10-2021")); // missing time + assertFalse(StartDateTime.isValidDateTime("11-10 12:10")); // missing year + assertFalse(StartDateTime.isValidDateTime("11-10")); // missing year and time + + // invalid parts + assertFalse(StartDateTime.isValidDateTime("2010/12/21 12:10")); // invalid connector + assertFalse(StartDateTime.isValidDateTime("2012-12-20 12:10")); // wrong order + assertFalse(StartDateTime.isValidDateTime("01-13-2012")); // wrong order of date and month + assertFalse(StartDateTime.isValidDateTime(" 01-13-2012 12:05")); // leading space + assertFalse(StartDateTime.isValidDateTime("01-13-2012 12:05 ")); // trailing space + assertFalse(StartDateTime.isValidDateTime("2021 Oct 2 12:10")); // wrong date format + assertFalse(StartDateTime.isValidDateTime("%01-13-2012 10:15")); // include invalid symbol '%' + assertFalse(StartDateTime.isValidDateTime("01-13-2012 11:15pm")); // invalid time format + assertFalse(StartDateTime.isValidDateTime("1-13-2012 11:15")); // missing leading 0 + + // valid StartDateTime + assertTrue(StartDateTime.isValidDateTime("01-12-2012 11:00")); + assertTrue(StartDateTime.isValidDateTime("01-12-2012 10:05")); + assertTrue(StartDateTime.isValidDateTime("01-12-2012 11:22")); + } + + @Test + public void isBefore() { + StartDateTime firstDateTime = new StartDateTime("30-09-2021 22:50"); + StartDateTime secondDateTime = new StartDateTime("30-09-2021 22:55"); + StartDateTime thirdDateTime = new StartDateTime("30-10-2021 02:30"); + + assertTrue(firstDateTime.isBefore(secondDateTime)); + assertTrue(firstDateTime.isBefore(thirdDateTime)); + assertFalse(thirdDateTime.isBefore(secondDateTime)); + } + + @Test + public void compareTo() { + StartDateTime firstDateTime = new StartDateTime("30-09-2021 22:50"); + StartDateTime secondDateTime = new StartDateTime("30-09-2021 22:55"); + StartDateTime secondDuplicate = new StartDateTime("30-09-2021 22:55"); + StartDateTime thirdDateTime = new StartDateTime("30-10-2021 02:30"); + + assertTrue(firstDateTime.compareTo(secondDateTime) < 0); + assertTrue(firstDateTime.compareTo(thirdDateTime) < 0); + assertTrue(thirdDateTime.compareTo(firstDateTime) > 0); + assertEquals(0, secondDateTime.compareTo(secondDuplicate)); + } +} diff --git a/src/test/java/seedu/address/model/event/UniqueEventListTest.java b/src/test/java/seedu/address/model/event/UniqueEventListTest.java new file mode 100644 index 00000000000..01f507031ef --- /dev/null +++ b/src/test/java/seedu/address/model/event/UniqueEventListTest.java @@ -0,0 +1,218 @@ +package seedu.address.model.event; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_ADDRESS_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_START_DATE_TIME_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_TAG_HUSBAND; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalEvents.BIRTHDAY_PARTY; +import static seedu.address.testutil.TypicalEvents.CS2100_CONSULTATION; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.event.exceptions.DuplicateEventException; +import seedu.address.model.event.exceptions.EventNotFoundException; +import seedu.address.model.event.exceptions.InvalidDateTimeRangeException; +import seedu.address.testutil.EventBuilder; + +class UniqueEventListTest { + + private final UniqueEventList uniqueEventList = new UniqueEventList(); + + @Test + public void contains_nullEvent_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueEventList.contains(null)); + } + + @Test + public void contains_eventNotInList_returnsFalse() { + assertFalse(uniqueEventList.contains(BIRTHDAY_PARTY)); + } + + @Test + public void contains_eventInList_returnsTrue() { + uniqueEventList.add(BIRTHDAY_PARTY); + assertTrue(uniqueEventList.contains(BIRTHDAY_PARTY)); + } + + @Test + public void contains_eventWithSameIdentityFieldsInList_returnsTrue() { + uniqueEventList.add(BIRTHDAY_PARTY); + Event editedBirthdayParty = new EventBuilder(BIRTHDAY_PARTY).withAddress(VALID_ADDRESS_TUTORIAL) + .withTags(VALID_TAG_HUSBAND).build(); + assertTrue(uniqueEventList.contains(editedBirthdayParty)); + } + + @Test + public void add_nullEvent_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueEventList.add(null)); + } + + @Test + public void add_duplicateEvent_throwsDuplicateEventException() { + uniqueEventList.add(BIRTHDAY_PARTY); + assertThrows(DuplicateEventException.class, () -> uniqueEventList.add(BIRTHDAY_PARTY)); + } + + @Test + public void addEvent_invalidDateTimeRange_throwsInvalidDateTimeRangeException() { + assertThrows(InvalidDateTimeRangeException.class, () -> uniqueEventList.add( + new EventBuilder(BIRTHDAY_PARTY).withEndDateAndTime(VALID_START_DATE_TIME_TUTORIAL).build())); + } + + @Test + public void setEvent_nullTargetEvent_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueEventList.setEvent(null, BIRTHDAY_PARTY)); + } + + @Test + public void setEvent_nullEditedEvent_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueEventList.setEvent(BIRTHDAY_PARTY, null)); + } + + @Test + public void setEvent_targetEventNotInList_throwsEventNotFoundException() { + assertThrows(EventNotFoundException.class, () -> uniqueEventList.setEvent(BIRTHDAY_PARTY, BIRTHDAY_PARTY)); + } + + @Test + public void setEvent_editedEventIsSameEvent_success() { + uniqueEventList.add(BIRTHDAY_PARTY); + uniqueEventList.setEvent(BIRTHDAY_PARTY, BIRTHDAY_PARTY); + UniqueEventList expectedUniqueEventList = new UniqueEventList(); + expectedUniqueEventList.add(BIRTHDAY_PARTY); + assertEquals(expectedUniqueEventList, uniqueEventList); + } + + @Test + public void setEvent_editedEventHasSameIdentity_success() { + uniqueEventList.add(BIRTHDAY_PARTY); + Event editedBirthdayParty = new EventBuilder(BIRTHDAY_PARTY).withAddress(VALID_ADDRESS_TUTORIAL) + .withTags(VALID_TAG_HUSBAND).build(); + uniqueEventList.setEvent(BIRTHDAY_PARTY, editedBirthdayParty); + UniqueEventList expectedUniqueEventList = new UniqueEventList(); + expectedUniqueEventList.add(editedBirthdayParty); + assertEquals(expectedUniqueEventList, uniqueEventList); + } + + @Test + public void setEvent_editedEventHasDifferentIdentity_success() { + uniqueEventList.add(BIRTHDAY_PARTY); + uniqueEventList.setEvent(BIRTHDAY_PARTY, CS2100_CONSULTATION); + UniqueEventList expectedUniqueEventList = new UniqueEventList(); + expectedUniqueEventList.add(CS2100_CONSULTATION); + assertEquals(expectedUniqueEventList, uniqueEventList); + } + + @Test + public void setEvent_editedEventHasNonUniqueIdentity_throwsDuplicateEventException() { + uniqueEventList.add(BIRTHDAY_PARTY); + uniqueEventList.add(CS2100_CONSULTATION); + assertThrows( + DuplicateEventException.class, () -> uniqueEventList.setEvent(BIRTHDAY_PARTY, CS2100_CONSULTATION)); + } + + @Test + public void setEvent_editedEventHasInvalidDateTimeRange_throwsInvalidDateTimeRangeException() { + uniqueEventList.add(BIRTHDAY_PARTY); + assertThrows(InvalidDateTimeRangeException.class, () -> uniqueEventList.setEvent(BIRTHDAY_PARTY, + new EventBuilder(BIRTHDAY_PARTY).withEndDateAndTime(VALID_START_DATE_TIME_TUTORIAL).build())); + } + + + @Test + public void remove_nullEvent_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueEventList.remove(null)); + } + + @Test + public void remove_eventDoesNotExist_throwsEventNotFoundException() { + assertThrows(EventNotFoundException.class, () -> uniqueEventList.remove(BIRTHDAY_PARTY)); + } + + @Test + public void remove_existingEvent_removesEvent() { + uniqueEventList.add(BIRTHDAY_PARTY); + uniqueEventList.remove(BIRTHDAY_PARTY); + UniqueEventList expectedUniqueEventList = new UniqueEventList(); + assertEquals(expectedUniqueEventList, uniqueEventList); + } + + @Test + public void setEvents_nullUniqueEventList_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueEventList.setEvents((UniqueEventList) null)); + } + + @Test + public void setEvents_uniqueEventList_replacesOwnListWithProvidedUniqueEventList() { + uniqueEventList.add(BIRTHDAY_PARTY); + UniqueEventList expectedUniqueEventList = new UniqueEventList(); + expectedUniqueEventList.add(CS2100_CONSULTATION); + uniqueEventList.setEvents(expectedUniqueEventList); + assertEquals(expectedUniqueEventList, uniqueEventList); + } + + @Test + public void setEvents_nullList_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueEventList.setEvents((List) null)); + } + + @Test + public void setEvents_list_replacesOwnListWithProvidedList() { + uniqueEventList.add(BIRTHDAY_PARTY); + List eventList = Collections.singletonList(CS2100_CONSULTATION); + uniqueEventList.setEvents(eventList); + UniqueEventList expectedUniqueEventList = new UniqueEventList(); + expectedUniqueEventList.add(CS2100_CONSULTATION); + assertEquals(expectedUniqueEventList, uniqueEventList); + } + + @Test + public void setEvents_listWithDuplicateEvents_throwsDuplicateEventException() { + List listWithDuplicateEvents = Arrays.asList(BIRTHDAY_PARTY, BIRTHDAY_PARTY); + assertThrows(DuplicateEventException.class, () -> uniqueEventList.setEvents(listWithDuplicateEvents)); + } + + @Test + public void asUnmodifiableObservableList_modifyList_throwsUnsupportedOperationException() { + assertThrows(UnsupportedOperationException.class, () + -> uniqueEventList.asUnmodifiableObservableList().remove(0)); + } + + @Test + public void test_updateEventMap() { + uniqueEventList.add(BIRTHDAY_PARTY); + uniqueEventList.updateEventMap(); + assertEquals(BIRTHDAY_PARTY, Event.findByUuid(BIRTHDAY_PARTY.getUuid())); + } + + @Test + public void test_hashCode() { + UniqueEventList uniqueEventListCopy = new UniqueEventList(); + assertEquals(uniqueEventList.hashCode(), uniqueEventListCopy.hashCode()); + + uniqueEventListCopy.add(BIRTHDAY_PARTY); + uniqueEventList.add(BIRTHDAY_PARTY); + assertEquals(uniqueEventList.hashCode(), uniqueEventListCopy.hashCode()); + + uniqueEventList.add(CS2100_CONSULTATION); + assertNotEquals(uniqueEventList.hashCode(), uniqueEventListCopy.hashCode()); + } + + @Test + public void test_iterator() { + //empty uniqueEventList + assertFalse(uniqueEventList.iterator().hasNext()); + + //non-empty uniqueEventList + uniqueEventList.add(BIRTHDAY_PARTY); + assertTrue(uniqueEventList.iterator().hasNext()); + } +} diff --git a/src/test/java/seedu/address/model/history/ModelHistoryTest.java b/src/test/java/seedu/address/model/history/ModelHistoryTest.java new file mode 100644 index 00000000000..f489e5dcd9c --- /dev/null +++ b/src/test/java/seedu/address/model/history/ModelHistoryTest.java @@ -0,0 +1,155 @@ +package seedu.address.model.history; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.model.Model.PREDICATE_HIDE_ALL_EVENTS; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_CONTACTS; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_EVENTS; +import static seedu.address.testutil.TypicalContacts.HOON; +import static seedu.address.testutil.TypicalEvents.TUTORIAL; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.AddressBook; +import seedu.address.model.ModelDisplaySetting; +import seedu.address.model.contact.ContactDisplaySetting; +import seedu.address.model.event.EventDisplaySetting; +import seedu.address.testutil.AddressBookBuilder; +import seedu.address.testutil.TypicalAddressBook; + +class ModelHistoryTest { + + private final ModelHistory history = new ModelHistory(); + + @Test + public void clearHistory_nonEmpty_success() { + ModelHistory anotherHistory = new ModelHistory(); + AddressBook ab = TypicalAddressBook.getTypicalAddressBook(); + anotherHistory.commit(ab, new ModelDisplaySetting()); + anotherHistory.commit(new AddressBookBuilder(ab).withContact(HOON).build(), new ModelDisplaySetting()); + anotherHistory.clearHistory(); + assertEquals(anotherHistory.getCurrentSize(), history.getCurrentSize()); + assertEquals(anotherHistory.getMaxSize(), history.getMaxSize()); + assertEquals(anotherHistory.getCurrentSize(), 0); + assertEquals(anotherHistory.getMaxSize(), 0); + } + + @Test + public void clearHistory_empty_success() { + history.clearHistory(); + assertEquals(history.getCurrentSize(), 0); + assertEquals(history.getMaxSize(), 0); + } + + @Test + public void commit_success() { + history.clearHistory(); + AddressBook ab = TypicalAddressBook.getTypicalAddressBook(); + history.commit(ab, new ModelDisplaySetting()); + history.commit(new AddressBookBuilder(ab).withContact(HOON).build(), new ModelDisplaySetting()); + assertEquals(history.getMaxSize(), 2); + assertEquals(history.getCurrentSize(), 2); + assertEquals(history.getAllHistory(), List.of(new ModelHistory.HistoryInstance( + ab, new ModelDisplaySetting() + ), new ModelHistory.HistoryInstance( + new AddressBookBuilder(ab).withContact(HOON).build(), new ModelDisplaySetting() + ))); + } + + @Test + public void undo_success() { + AddressBook ab = TypicalAddressBook.getTypicalAddressBook(); + history.commit(ab, new ModelDisplaySetting()); + history.commit(new AddressBookBuilder(ab).withContact(HOON).build(), new ModelDisplaySetting()); + assertEquals(history.undo(), new ModelHistory.HistoryInstance( + ab, new ModelDisplaySetting())); + assertEquals(history.getCurrentSize(), 1); + assertEquals(history.getMaxSize(), 2); + } + + @Test + public void undo_noHistory_failure() { + history.clearHistory(); + assertThrows(ModelHistoryException.class, () -> history.undo()); + } + + @Test + public void redo_singleUndo_success() { + AddressBook ab = TypicalAddressBook.getTypicalAddressBook(); + history.commit(ab, new ModelDisplaySetting()); + history.commit(new AddressBookBuilder(ab).withContact(HOON).build(), new ModelDisplaySetting()); + history.undo(); + assertEquals(history.redo(), new ModelHistory.HistoryInstance( + new AddressBookBuilder(ab).withContact(HOON).build(), new ModelDisplaySetting())); + assertEquals(history.getCurrentSize(), 2); + assertEquals(history.getMaxSize(), 2); + } + + @Test + public void redo_doubleUndo_success() { + AddressBook ab = TypicalAddressBook.getTypicalAddressBook(); + history.commit(ab, new ModelDisplaySetting()); + history.commit(new AddressBookBuilder(ab).withContact(HOON).build(), new ModelDisplaySetting()); + history.commit( + new AddressBookBuilder(ab).withContact(HOON).withEvent(TUTORIAL).build(), + new ModelDisplaySetting(ContactDisplaySetting.DEFAULT_SETTING, new EventDisplaySetting(true), + PREDICATE_SHOW_ALL_CONTACTS, PREDICATE_HIDE_ALL_EVENTS)); + history.undo(); + history.undo(); + assertEquals(history.redo(), new ModelHistory.HistoryInstance( + new AddressBookBuilder(ab).withContact(HOON).build(), new ModelDisplaySetting())); + assertEquals(history.getCurrentSize(), 2); + assertEquals(history.getMaxSize(), 3); + } + + @Test + public void redo_noUndo_failure() { + AddressBook ab = TypicalAddressBook.getTypicalAddressBook(); + history.commit(ab, new ModelDisplaySetting()); + history.commit(new AddressBookBuilder(ab).withContact(HOON).build(), new ModelDisplaySetting()); + assertThrows(ModelHistoryException.class, () -> history.redo()); + history.clearHistory(); + assertThrows(ModelHistoryException.class, () -> history.redo()); + } + + @Test + public void isUndoable() { + AddressBook ab = TypicalAddressBook.getTypicalAddressBook(); + history.commit(ab, new ModelDisplaySetting()); + history.commit(new AddressBookBuilder(ab).withContact(HOON).build(), new ModelDisplaySetting()); + assertTrue(history.isUndoable()); + history.undo(); + assertFalse(history.isUndoable()); + } + + @Test + public void isRedoable() { + AddressBook ab = TypicalAddressBook.getTypicalAddressBook(); + history.commit(ab, new ModelDisplaySetting()); + history.commit(new AddressBookBuilder(ab).withContact(HOON).build(), new ModelDisplaySetting()); + assertFalse(history.isRedoable()); + history.undo(); + assertTrue(history.isRedoable()); + } + + static class HistoryInstanceTest { + @Test + public void equal_test() { + ModelDisplaySetting displaySetting = + new ModelDisplaySetting(ContactDisplaySetting.DEFAULT_SETTING, EventDisplaySetting.DEFAULT_SETTING, + PREDICATE_SHOW_ALL_CONTACTS, PREDICATE_SHOW_ALL_EVENTS); + ModelHistory.HistoryInstance instance = new ModelHistory.HistoryInstance( + TypicalAddressBook.getTypicalAddressBook(), + displaySetting); + assertNotEquals(instance, null); + assertNotEquals(instance, 1); + assertEquals(instance, instance); + assertNotEquals(instance, new ModelHistory.HistoryInstance(new AddressBook(), displaySetting)); + } + } +} diff --git a/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java b/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java deleted file mode 100644 index f136664e017..00000000000 --- a/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package seedu.address.model.person; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import org.junit.jupiter.api.Test; - -import seedu.address.testutil.PersonBuilder; - -public class NameContainsKeywordsPredicateTest { - - @Test - public void equals() { - List firstPredicateKeywordList = Collections.singletonList("first"); - List secondPredicateKeywordList = Arrays.asList("first", "second"); - - NameContainsKeywordsPredicate firstPredicate = new NameContainsKeywordsPredicate(firstPredicateKeywordList); - NameContainsKeywordsPredicate secondPredicate = new NameContainsKeywordsPredicate(secondPredicateKeywordList); - - // same object -> returns true - assertTrue(firstPredicate.equals(firstPredicate)); - - // same values -> returns true - NameContainsKeywordsPredicate firstPredicateCopy = new NameContainsKeywordsPredicate(firstPredicateKeywordList); - assertTrue(firstPredicate.equals(firstPredicateCopy)); - - // different types -> returns false - assertFalse(firstPredicate.equals(1)); - - // null -> returns false - assertFalse(firstPredicate.equals(null)); - - // different person -> returns false - assertFalse(firstPredicate.equals(secondPredicate)); - } - - @Test - public void test_nameContainsKeywords_returnsTrue() { - // One keyword - NameContainsKeywordsPredicate predicate = new NameContainsKeywordsPredicate(Collections.singletonList("Alice")); - assertTrue(predicate.test(new PersonBuilder().withName("Alice Bob").build())); - - // Multiple keywords - predicate = new NameContainsKeywordsPredicate(Arrays.asList("Alice", "Bob")); - assertTrue(predicate.test(new PersonBuilder().withName("Alice Bob").build())); - - // Only one matching keyword - predicate = new NameContainsKeywordsPredicate(Arrays.asList("Bob", "Carol")); - assertTrue(predicate.test(new PersonBuilder().withName("Alice Carol").build())); - - // Mixed-case keywords - predicate = new NameContainsKeywordsPredicate(Arrays.asList("aLIce", "bOB")); - assertTrue(predicate.test(new PersonBuilder().withName("Alice Bob").build())); - } - - @Test - public void test_nameDoesNotContainKeywords_returnsFalse() { - // Zero keywords - NameContainsKeywordsPredicate predicate = new NameContainsKeywordsPredicate(Collections.emptyList()); - assertFalse(predicate.test(new PersonBuilder().withName("Alice").build())); - - // Non-matching keyword - predicate = new NameContainsKeywordsPredicate(Arrays.asList("Carol")); - assertFalse(predicate.test(new PersonBuilder().withName("Alice Bob").build())); - - // Keywords match phone, email and address, but does not match name - predicate = new NameContainsKeywordsPredicate(Arrays.asList("12345", "alice@email.com", "Main", "Street")); - assertFalse(predicate.test(new PersonBuilder().withName("Alice").withPhone("12345") - .withEmail("alice@email.com").withAddress("Main Street").build())); - } -} diff --git a/src/test/java/seedu/address/model/person/PersonTest.java b/src/test/java/seedu/address/model/person/PersonTest.java deleted file mode 100644 index b29c097cfd4..00000000000 --- a/src/test/java/seedu/address/model/person/PersonTest.java +++ /dev/null @@ -1,91 +0,0 @@ -package seedu.address.model.person; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; -import static seedu.address.testutil.Assert.assertThrows; -import static seedu.address.testutil.TypicalPersons.ALICE; -import static seedu.address.testutil.TypicalPersons.BOB; - -import org.junit.jupiter.api.Test; - -import seedu.address.testutil.PersonBuilder; - -public class PersonTest { - - @Test - public void asObservableList_modifyList_throwsUnsupportedOperationException() { - Person person = new PersonBuilder().build(); - assertThrows(UnsupportedOperationException.class, () -> person.getTags().remove(0)); - } - - @Test - public void isSamePerson() { - // same object -> returns true - assertTrue(ALICE.isSamePerson(ALICE)); - - // null -> returns false - assertFalse(ALICE.isSamePerson(null)); - - // same name, all other attributes different -> returns true - Person editedAlice = new PersonBuilder(ALICE).withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_BOB) - .withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND).build(); - assertTrue(ALICE.isSamePerson(editedAlice)); - - // different name, all other attributes same -> returns false - editedAlice = new PersonBuilder(ALICE).withName(VALID_NAME_BOB).build(); - assertFalse(ALICE.isSamePerson(editedAlice)); - - // name differs in case, all other attributes same -> returns false - Person editedBob = new PersonBuilder(BOB).withName(VALID_NAME_BOB.toLowerCase()).build(); - assertFalse(BOB.isSamePerson(editedBob)); - - // name has trailing spaces, all other attributes same -> returns false - String nameWithTrailingSpaces = VALID_NAME_BOB + " "; - editedBob = new PersonBuilder(BOB).withName(nameWithTrailingSpaces).build(); - assertFalse(BOB.isSamePerson(editedBob)); - } - - @Test - public void equals() { - // same values -> returns true - Person aliceCopy = new PersonBuilder(ALICE).build(); - assertTrue(ALICE.equals(aliceCopy)); - - // same object -> returns true - assertTrue(ALICE.equals(ALICE)); - - // null -> returns false - assertFalse(ALICE.equals(null)); - - // different type -> returns false - assertFalse(ALICE.equals(5)); - - // different person -> returns false - assertFalse(ALICE.equals(BOB)); - - // different name -> returns false - Person editedAlice = new PersonBuilder(ALICE).withName(VALID_NAME_BOB).build(); - assertFalse(ALICE.equals(editedAlice)); - - // different phone -> returns false - editedAlice = new PersonBuilder(ALICE).withPhone(VALID_PHONE_BOB).build(); - assertFalse(ALICE.equals(editedAlice)); - - // different email -> returns false - editedAlice = new PersonBuilder(ALICE).withEmail(VALID_EMAIL_BOB).build(); - assertFalse(ALICE.equals(editedAlice)); - - // different address -> returns false - editedAlice = new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).build(); - assertFalse(ALICE.equals(editedAlice)); - - // different tags -> returns false - editedAlice = new PersonBuilder(ALICE).withTags(VALID_TAG_HUSBAND).build(); - assertFalse(ALICE.equals(editedAlice)); - } -} diff --git a/src/test/java/seedu/address/model/person/UniquePersonListTest.java b/src/test/java/seedu/address/model/person/UniquePersonListTest.java deleted file mode 100644 index 1cc5fe9e0fe..00000000000 --- a/src/test/java/seedu/address/model/person/UniquePersonListTest.java +++ /dev/null @@ -1,170 +0,0 @@ -package seedu.address.model.person; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; -import static seedu.address.testutil.Assert.assertThrows; -import static seedu.address.testutil.TypicalPersons.ALICE; -import static seedu.address.testutil.TypicalPersons.BOB; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import org.junit.jupiter.api.Test; - -import seedu.address.model.person.exceptions.DuplicatePersonException; -import seedu.address.model.person.exceptions.PersonNotFoundException; -import seedu.address.testutil.PersonBuilder; - -public class UniquePersonListTest { - - private final UniquePersonList uniquePersonList = new UniquePersonList(); - - @Test - public void contains_nullPerson_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> uniquePersonList.contains(null)); - } - - @Test - public void contains_personNotInList_returnsFalse() { - assertFalse(uniquePersonList.contains(ALICE)); - } - - @Test - public void contains_personInList_returnsTrue() { - uniquePersonList.add(ALICE); - assertTrue(uniquePersonList.contains(ALICE)); - } - - @Test - public void contains_personWithSameIdentityFieldsInList_returnsTrue() { - uniquePersonList.add(ALICE); - Person editedAlice = new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND) - .build(); - assertTrue(uniquePersonList.contains(editedAlice)); - } - - @Test - public void add_nullPerson_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> uniquePersonList.add(null)); - } - - @Test - public void add_duplicatePerson_throwsDuplicatePersonException() { - uniquePersonList.add(ALICE); - assertThrows(DuplicatePersonException.class, () -> uniquePersonList.add(ALICE)); - } - - @Test - public void setPerson_nullTargetPerson_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> uniquePersonList.setPerson(null, ALICE)); - } - - @Test - public void setPerson_nullEditedPerson_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> uniquePersonList.setPerson(ALICE, null)); - } - - @Test - public void setPerson_targetPersonNotInList_throwsPersonNotFoundException() { - assertThrows(PersonNotFoundException.class, () -> uniquePersonList.setPerson(ALICE, ALICE)); - } - - @Test - public void setPerson_editedPersonIsSamePerson_success() { - uniquePersonList.add(ALICE); - uniquePersonList.setPerson(ALICE, ALICE); - UniquePersonList expectedUniquePersonList = new UniquePersonList(); - expectedUniquePersonList.add(ALICE); - assertEquals(expectedUniquePersonList, uniquePersonList); - } - - @Test - public void setPerson_editedPersonHasSameIdentity_success() { - uniquePersonList.add(ALICE); - Person editedAlice = new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND) - .build(); - uniquePersonList.setPerson(ALICE, editedAlice); - UniquePersonList expectedUniquePersonList = new UniquePersonList(); - expectedUniquePersonList.add(editedAlice); - assertEquals(expectedUniquePersonList, uniquePersonList); - } - - @Test - public void setPerson_editedPersonHasDifferentIdentity_success() { - uniquePersonList.add(ALICE); - uniquePersonList.setPerson(ALICE, BOB); - UniquePersonList expectedUniquePersonList = new UniquePersonList(); - expectedUniquePersonList.add(BOB); - assertEquals(expectedUniquePersonList, uniquePersonList); - } - - @Test - public void setPerson_editedPersonHasNonUniqueIdentity_throwsDuplicatePersonException() { - uniquePersonList.add(ALICE); - uniquePersonList.add(BOB); - assertThrows(DuplicatePersonException.class, () -> uniquePersonList.setPerson(ALICE, BOB)); - } - - @Test - public void remove_nullPerson_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> uniquePersonList.remove(null)); - } - - @Test - public void remove_personDoesNotExist_throwsPersonNotFoundException() { - assertThrows(PersonNotFoundException.class, () -> uniquePersonList.remove(ALICE)); - } - - @Test - public void remove_existingPerson_removesPerson() { - uniquePersonList.add(ALICE); - uniquePersonList.remove(ALICE); - UniquePersonList expectedUniquePersonList = new UniquePersonList(); - assertEquals(expectedUniquePersonList, uniquePersonList); - } - - @Test - public void setPersons_nullUniquePersonList_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> uniquePersonList.setPersons((UniquePersonList) null)); - } - - @Test - public void setPersons_uniquePersonList_replacesOwnListWithProvidedUniquePersonList() { - uniquePersonList.add(ALICE); - UniquePersonList expectedUniquePersonList = new UniquePersonList(); - expectedUniquePersonList.add(BOB); - uniquePersonList.setPersons(expectedUniquePersonList); - assertEquals(expectedUniquePersonList, uniquePersonList); - } - - @Test - public void setPersons_nullList_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> uniquePersonList.setPersons((List) null)); - } - - @Test - public void setPersons_list_replacesOwnListWithProvidedList() { - uniquePersonList.add(ALICE); - List personList = Collections.singletonList(BOB); - uniquePersonList.setPersons(personList); - UniquePersonList expectedUniquePersonList = new UniquePersonList(); - expectedUniquePersonList.add(BOB); - assertEquals(expectedUniquePersonList, uniquePersonList); - } - - @Test - public void setPersons_listWithDuplicatePersons_throwsDuplicatePersonException() { - List listWithDuplicatePersons = Arrays.asList(ALICE, ALICE); - assertThrows(DuplicatePersonException.class, () -> uniquePersonList.setPersons(listWithDuplicatePersons)); - } - - @Test - public void asUnmodifiableObservableList_modifyList_throwsUnsupportedOperationException() { - assertThrows(UnsupportedOperationException.class, () - -> uniquePersonList.asUnmodifiableObservableList().remove(0)); - } -} diff --git a/src/test/java/seedu/address/model/tag/ColoursTest.java b/src/test/java/seedu/address/model/tag/ColoursTest.java new file mode 100644 index 00000000000..78262187491 --- /dev/null +++ b/src/test/java/seedu/address/model/tag/ColoursTest.java @@ -0,0 +1,26 @@ +package seedu.address.model.tag; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import org.junit.jupiter.api.Test; + +class ColoursTest { + + @Test + void getTagColour() { + String colour1 = Colours.getTagColour(); + String colour2 = Colours.getTagColour(); + assertNotEquals(colour1, colour2); + } + + @Test + void getSameColour() { + String colour1 = Colours.getTagColour(); + String colour2 = ""; + for (int i = 0; i < Colours.NUMBER_OF_COLOURS; i++) { + colour2 = Colours.getTagColour(); + } + assertEquals(colour1, colour2); + } +} diff --git a/src/test/java/seedu/address/model/tag/TagTest.java b/src/test/java/seedu/address/model/tag/TagTest.java index 64d07d79ee2..bb57a553bf7 100644 --- a/src/test/java/seedu/address/model/tag/TagTest.java +++ b/src/test/java/seedu/address/model/tag/TagTest.java @@ -1,7 +1,12 @@ package seedu.address.model.tag; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.testutil.Assert.assertThrows; +import java.util.Arrays; +import java.util.List; + import org.junit.jupiter.api.Test; public class TagTest { @@ -17,10 +22,20 @@ public void constructor_invalidTagName_throwsIllegalArgumentException() { assertThrows(IllegalArgumentException.class, () -> new Tag(invalidTagName)); } + @Test + public void containsString() { + Tag tag = new Tag("recurring"); + + List listOfStringsKeywordsContained = Arrays.asList("RE", "curr"); + assertTrue(tag.containsString(listOfStringsKeywordsContained)); + + List noKeywordsContained = Arrays.asList("fri", "fam"); + assertFalse(tag.containsString(noKeywordsContained)); + + } @Test public void isValidTagName() { // null tag name assertThrows(NullPointerException.class, () -> Tag.isValidTagName(null)); } - } diff --git a/src/test/java/seedu/address/storage/JsonAdaptedContactTest.java b/src/test/java/seedu/address/storage/JsonAdaptedContactTest.java new file mode 100644 index 00000000000..bcf26dc9d31 --- /dev/null +++ b/src/test/java/seedu/address/storage/JsonAdaptedContactTest.java @@ -0,0 +1,160 @@ +package seedu.address.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.storage.JsonAdaptedContact.MISSING_FIELD_MESSAGE_FORMAT; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalContacts.CARL; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.common.Address; +import seedu.address.model.common.Name; +import seedu.address.model.common.ZoomLink; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Phone; +import seedu.address.model.contact.TelegramHandle; +import seedu.address.testutil.ContactBuilder; + +public class JsonAdaptedContactTest { + private static final String INVALID_NAME = "R@chel"; + private static final String INVALID_PHONE = "+651234"; + private static final String INVALID_ADDRESS = " "; + private static final String INVALID_EMAIL = "example.com"; + private static final String INVALID_TELEGRAM_HANDLE = "abc"; + private static final String INVALID_ZOOM_LINK = ""; + private static final String INVALID_TAG = "#friend"; + + private static final String VALID_NAME = CARL.getName().toString(); + private static final String VALID_PHONE = CARL.getPhone().toString(); + private static final String VALID_EMAIL = CARL.getEmail().toString(); + private static final String VALID_ADDRESS = CARL.getAddress().toString(); + private static final String VALID_TELEGRAM_HANDLE = CARL.getTelegramHandle().toString(); + private static final String VALID_ZOOM_LINK = CARL.getZoomLink().toString(); + private static final List VALID_TAGS = CARL.getTags().stream() + .map(JsonAdaptedTag::new) + .collect(Collectors.toList()); + private static final String VALID_UUID = UUID.randomUUID().toString(); + private static final List VALID_LINKED_EVENTS = new ArrayList<>(); + + @Test + public void toModelType_validContactDetails_returnsContact() throws IllegalValueException { + JsonAdaptedContact contact = new JsonAdaptedContact(CARL); + assertEquals(CARL, contact.toModelType()); + } + + @Test + public void toModelType_invalidName_throwsIllegalValueException() { + JsonAdaptedContact contact = + new JsonAdaptedContact(INVALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + VALID_TELEGRAM_HANDLE, VALID_ZOOM_LINK, VALID_TAGS, VALID_UUID, VALID_LINKED_EVENTS, + false); + String expectedMessage = Name.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, contact::toModelType); + } + + @Test + public void toModelType_nullName_throwsIllegalValueException() { + JsonAdaptedContact contact = new JsonAdaptedContact(null, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + VALID_TELEGRAM_HANDLE, VALID_ZOOM_LINK, VALID_TAGS, VALID_UUID, VALID_LINKED_EVENTS, false); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName()); + assertThrows(IllegalValueException.class, expectedMessage, contact::toModelType); + } + + @Test + public void toModelType_invalidPhone_throwsIllegalValueException() { + JsonAdaptedContact contact = + new JsonAdaptedContact(VALID_NAME, INVALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TELEGRAM_HANDLE, + VALID_ZOOM_LINK, VALID_TAGS, VALID_UUID, VALID_LINKED_EVENTS, false); + String expectedMessage = Phone.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, contact::toModelType); + } + + @Test + public void toModelType_nullPhone_returnsContact() throws IllegalValueException { + Contact contactWithNullPhone = new ContactBuilder().withPhone(null).build(); + JsonAdaptedContact contact = new JsonAdaptedContact(contactWithNullPhone); + assertEquals(contactWithNullPhone, contact.toModelType()); + } + + @Test + public void toModelType_invalidEmail_throwsIllegalValueException() { + JsonAdaptedContact contact = + new JsonAdaptedContact(VALID_NAME, VALID_PHONE, INVALID_EMAIL, VALID_ADDRESS, VALID_TELEGRAM_HANDLE, + VALID_ZOOM_LINK, VALID_TAGS, VALID_UUID, VALID_LINKED_EVENTS, false); + String expectedMessage = Email.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, contact::toModelType); + } + + @Test + public void toModelType_nullEmail_throwsIllegalValueException() { + JsonAdaptedContact contact = new JsonAdaptedContact(VALID_NAME, VALID_PHONE, null, VALID_ADDRESS, + VALID_TELEGRAM_HANDLE, VALID_ZOOM_LINK, VALID_TAGS, VALID_UUID, VALID_LINKED_EVENTS, false); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName()); + assertThrows(IllegalValueException.class, expectedMessage, contact::toModelType); + } + + @Test + public void toModelType_invalidAddress_throwsIllegalValueException() { + JsonAdaptedContact contact = + new JsonAdaptedContact(VALID_NAME, VALID_PHONE, VALID_EMAIL, INVALID_ADDRESS, VALID_TELEGRAM_HANDLE, + VALID_ZOOM_LINK, VALID_TAGS, VALID_UUID, VALID_LINKED_EVENTS, false); + String expectedMessage = Address.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, contact::toModelType); + } + + @Test + public void toModelType_nullAddress_returnsContact() throws IllegalValueException { + Contact contactWithNullAddress = new ContactBuilder().withAddress(null).build(); + JsonAdaptedContact contact = new JsonAdaptedContact(contactWithNullAddress); + assertEquals(contactWithNullAddress, contact.toModelType()); + } + + @Test + public void toModelType_invalidTelegramHandle_throwsIllegalValueException() { + JsonAdaptedContact contact = + new JsonAdaptedContact(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, INVALID_TELEGRAM_HANDLE, + VALID_ZOOM_LINK, VALID_TAGS, VALID_UUID, VALID_LINKED_EVENTS, false); + String expectedMessage = TelegramHandle.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, contact::toModelType); + } + + @Test + public void toModelType_nullTelegramHandle_returnsContact() throws IllegalValueException { + Contact contactWithNullTelegramHandle = new ContactBuilder().withTelegramHandle(null).build(); + JsonAdaptedContact contact = new JsonAdaptedContact(contactWithNullTelegramHandle); + assertEquals(contactWithNullTelegramHandle, contact.toModelType()); + } + + @Test + public void toModelType_invalidZoomLink_throwsIllegalValueException() { + JsonAdaptedContact contact = + new JsonAdaptedContact(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TELEGRAM_HANDLE, + INVALID_ZOOM_LINK, VALID_TAGS, VALID_UUID, VALID_LINKED_EVENTS, false); + String expectedMessage = ZoomLink.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, contact::toModelType); + } + + @Test + public void toModelType_nullZoomLink_returnsContact() throws IllegalValueException { + Contact contactWithNullZoomLink = new ContactBuilder().withZoomLink(null).build(); + JsonAdaptedContact contact = new JsonAdaptedContact(contactWithNullZoomLink); + assertEquals(contactWithNullZoomLink, contact.toModelType()); + } + + @Test + public void toModelType_invalidTags_throwsIllegalValueException() { + List invalidTags = new ArrayList<>(VALID_TAGS); + invalidTags.add(new JsonAdaptedTag(INVALID_TAG)); + JsonAdaptedContact contact = + new JsonAdaptedContact(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TELEGRAM_HANDLE, + VALID_ZOOM_LINK, invalidTags, VALID_UUID, VALID_LINKED_EVENTS, false); + assertThrows(IllegalValueException.class, contact::toModelType); + } +} diff --git a/src/test/java/seedu/address/storage/JsonAdaptedEventTest.java b/src/test/java/seedu/address/storage/JsonAdaptedEventTest.java new file mode 100644 index 00000000000..0cb8c6d1ae6 --- /dev/null +++ b/src/test/java/seedu/address/storage/JsonAdaptedEventTest.java @@ -0,0 +1,167 @@ +package seedu.address.storage; + + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.storage.JsonAdaptedEvent.MISSING_FIELD_MESSAGE_FORMAT; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalEvents.CS2103_MIDTERM_MARKED; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.common.Address; +import seedu.address.model.common.Name; +import seedu.address.model.common.ZoomLink; +import seedu.address.model.event.Description; +import seedu.address.model.event.EndDateTime; +import seedu.address.model.event.Event; +import seedu.address.model.event.StartDateTime; +import seedu.address.testutil.EventBuilder; + +class JsonAdaptedEventTest { + private static final String INVALID_NAME = "d@nce"; + private static final String INVALID_START_DATE_AND_TIME = "12/12/2021"; + private static final String INVALID_END_DATE_AND_TIME = "2021/12/12"; + private static final String INVALID_DESCRIPTION = " "; + private static final String INVALID_ADDRESS = " "; + private static final String INVALID_ZOOM_LINK = ""; + private static final String INVALID_TAG = "#summer"; + + private static final String VALID_NAME = CS2103_MIDTERM_MARKED.getName().toString(); + private static final String VALID_START_DATE_AND_TIME = CS2103_MIDTERM_MARKED.getStartDateAndTime().toString(); + private static final String VALID_END_DATE_AND_TIME = CS2103_MIDTERM_MARKED.getEndDateAndTime().toString(); + private static final String VALID_DESCRIPTION = CS2103_MIDTERM_MARKED.getDescription().toString(); + private static final String VALID_ADDRESS = CS2103_MIDTERM_MARKED.getAddress().toString(); + private static final String VALID_ZOOM_LINK = CS2103_MIDTERM_MARKED.getZoomLink().toString(); + private static final List VALID_TAGS = CS2103_MIDTERM_MARKED.getTags().stream() + .map(JsonAdaptedTag::new) + .collect(Collectors.toList()); + private static final String VALID_UUID = UUID.randomUUID().toString(); + private static final List VALID_LINKED_CONTACTS = new ArrayList<>(); + + @Test + public void toModelType_validEventDetails_returnsEvent() throws IllegalValueException { + JsonAdaptedEvent event = new JsonAdaptedEvent(CS2103_MIDTERM_MARKED); + assertEquals(CS2103_MIDTERM_MARKED, event.toModelType()); + } + + @Test + public void toModelType_invalidName_throwsIllegalValueException() { + JsonAdaptedEvent event = + new JsonAdaptedEvent(INVALID_NAME, VALID_START_DATE_AND_TIME, VALID_END_DATE_AND_TIME, VALID_DESCRIPTION, + VALID_ADDRESS, VALID_ZOOM_LINK, VALID_TAGS, VALID_UUID, VALID_LINKED_CONTACTS, + false); + String expectedMessage = Name.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, event::toModelType); + } + + @Test + public void toModelType_nullName_throwsIllegalValueException() { + JsonAdaptedEvent event = + new JsonAdaptedEvent(null, VALID_START_DATE_AND_TIME, VALID_END_DATE_AND_TIME, VALID_DESCRIPTION, + VALID_ADDRESS, VALID_ZOOM_LINK, VALID_TAGS, VALID_UUID, VALID_LINKED_CONTACTS, false); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName()); + assertThrows(IllegalValueException.class, expectedMessage, event::toModelType); + } + + @Test + public void toModelType_invalidStartDateTime_throwsIllegalValueException() { + JsonAdaptedEvent event = + new JsonAdaptedEvent(VALID_NAME, INVALID_START_DATE_AND_TIME, VALID_END_DATE_AND_TIME, VALID_DESCRIPTION, + VALID_ADDRESS, VALID_ZOOM_LINK, VALID_TAGS, VALID_UUID, VALID_LINKED_CONTACTS, false); + String expectedMessage = StartDateTime.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, event::toModelType); + } + + @Test + public void toModelType_nullStartDateTime_throwsIllegalValueException() { + JsonAdaptedEvent event = + new JsonAdaptedEvent(VALID_NAME, null, VALID_END_DATE_AND_TIME, VALID_DESCRIPTION, + VALID_ADDRESS, VALID_ZOOM_LINK, VALID_TAGS, VALID_UUID, VALID_LINKED_CONTACTS, false); + String expectedMessage = String.format( + JsonAdaptedEvent.MISSING_FIELD_MESSAGE_FORMAT, + StartDateTime.class.getSimpleName()); + assertThrows(IllegalValueException.class, expectedMessage, event::toModelType); + } + + @Test + public void toModelType_invalidEndDateTime_throwsIllegalValueException() { + JsonAdaptedEvent event = + new JsonAdaptedEvent(VALID_NAME, VALID_START_DATE_AND_TIME, INVALID_END_DATE_AND_TIME, VALID_DESCRIPTION, + VALID_ADDRESS, VALID_ZOOM_LINK, VALID_TAGS, VALID_UUID, VALID_LINKED_CONTACTS, false); + String expectedMessage = EndDateTime.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, event::toModelType); + } + + @Test + public void toModelType_nullEndDateTime_returnsEvent() throws IllegalValueException { + Event eventWithNullEndDateTime = new EventBuilder().withEndDateAndTime(null).build(); + JsonAdaptedEvent event = new JsonAdaptedEvent(eventWithNullEndDateTime); + assertEquals(eventWithNullEndDateTime, event.toModelType()); + } + + @Test + public void toModelType_invalidDescription_throwsIllegalValueException() { + JsonAdaptedEvent event = + new JsonAdaptedEvent(VALID_NAME, VALID_START_DATE_AND_TIME, VALID_END_DATE_AND_TIME, INVALID_DESCRIPTION, + VALID_ADDRESS, VALID_ZOOM_LINK, VALID_TAGS, VALID_UUID, VALID_LINKED_CONTACTS, false); + String expectedMessage = Description.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, event::toModelType); + } + + @Test + public void toModelType_nullDescription_returnsEvent() throws IllegalValueException { + Event eventWithNullDescription = new EventBuilder().withDescription(null).build(); + JsonAdaptedEvent event = new JsonAdaptedEvent(eventWithNullDescription); + assertEquals(eventWithNullDescription, event.toModelType()); + } + + @Test + public void toModelType_invalidAddress_throwsIllegalValueException() { + JsonAdaptedEvent event = + new JsonAdaptedEvent(VALID_NAME, VALID_START_DATE_AND_TIME, VALID_END_DATE_AND_TIME, VALID_DESCRIPTION, + INVALID_ADDRESS, VALID_ZOOM_LINK, VALID_TAGS, VALID_UUID, VALID_LINKED_CONTACTS, false); + String expectedMessage = Address.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, event::toModelType); + } + + @Test + public void toModelType_nullAddress_returnsEvent() throws IllegalValueException { + Event eventWithNullAddress = new EventBuilder().withAddress(null).build(); + JsonAdaptedEvent event = new JsonAdaptedEvent(eventWithNullAddress); + assertEquals(eventWithNullAddress, event.toModelType()); + } + + @Test + public void toModelType_invalidZoomLink_throwsIllegalValueException() { + JsonAdaptedEvent event = + new JsonAdaptedEvent(VALID_NAME, VALID_START_DATE_AND_TIME, VALID_END_DATE_AND_TIME, VALID_DESCRIPTION, + VALID_ADDRESS, INVALID_ZOOM_LINK, VALID_TAGS, VALID_UUID, VALID_LINKED_CONTACTS, false); + String expectedMessage = ZoomLink.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, event::toModelType); + } + + @Test + public void toModelType_nullZoomLink_returnsEvent() throws IllegalValueException { + Event eventWithNullZoomLink = new EventBuilder().withZoomLink(null).build(); + JsonAdaptedEvent event = new JsonAdaptedEvent(eventWithNullZoomLink); + assertEquals(eventWithNullZoomLink, event.toModelType()); + } + + @Test + public void toModelType_invalidTags_throwsIllegalValueException() { + List invalidTags = new ArrayList<>(VALID_TAGS); + invalidTags.add(new JsonAdaptedTag(INVALID_TAG)); + JsonAdaptedEvent event = + new JsonAdaptedEvent(VALID_NAME, VALID_START_DATE_AND_TIME, VALID_END_DATE_AND_TIME, VALID_DESCRIPTION, + VALID_ADDRESS, VALID_ZOOM_LINK, invalidTags, VALID_UUID, VALID_LINKED_CONTACTS, false); + assertThrows(IllegalValueException.class, event::toModelType); + } + +} + diff --git a/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java b/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java deleted file mode 100644 index 83b11331cdb..00000000000 --- a/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java +++ /dev/null @@ -1,110 +0,0 @@ -package seedu.address.storage; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static seedu.address.storage.JsonAdaptedPerson.MISSING_FIELD_MESSAGE_FORMAT; -import static seedu.address.testutil.Assert.assertThrows; -import static seedu.address.testutil.TypicalPersons.BENSON; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import org.junit.jupiter.api.Test; - -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.Phone; - -public class JsonAdaptedPersonTest { - private static final String INVALID_NAME = "R@chel"; - private static final String INVALID_PHONE = "+651234"; - private static final String INVALID_ADDRESS = " "; - private static final String INVALID_EMAIL = "example.com"; - private static final String INVALID_TAG = "#friend"; - - private static final String VALID_NAME = BENSON.getName().toString(); - private static final String VALID_PHONE = BENSON.getPhone().toString(); - private static final String VALID_EMAIL = BENSON.getEmail().toString(); - private static final String VALID_ADDRESS = BENSON.getAddress().toString(); - private static final List VALID_TAGS = BENSON.getTags().stream() - .map(JsonAdaptedTag::new) - .collect(Collectors.toList()); - - @Test - public void toModelType_validPersonDetails_returnsPerson() throws Exception { - JsonAdaptedPerson person = new JsonAdaptedPerson(BENSON); - assertEquals(BENSON, person.toModelType()); - } - - @Test - public void toModelType_invalidName_throwsIllegalValueException() { - JsonAdaptedPerson person = - new JsonAdaptedPerson(INVALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); - String expectedMessage = Name.MESSAGE_CONSTRAINTS; - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); - } - - @Test - public void toModelType_nullName_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(null, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); - String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName()); - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); - } - - @Test - public void toModelType_invalidPhone_throwsIllegalValueException() { - JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, INVALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); - String expectedMessage = Phone.MESSAGE_CONSTRAINTS; - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); - } - - @Test - public void toModelType_nullPhone_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, null, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); - String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName()); - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); - } - - @Test - public void toModelType_invalidEmail_throwsIllegalValueException() { - JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, INVALID_EMAIL, VALID_ADDRESS, VALID_TAGS); - String expectedMessage = Email.MESSAGE_CONSTRAINTS; - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); - } - - @Test - public void toModelType_nullEmail_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, null, VALID_ADDRESS, VALID_TAGS); - String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName()); - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); - } - - @Test - public void toModelType_invalidAddress_throwsIllegalValueException() { - JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, INVALID_ADDRESS, VALID_TAGS); - String expectedMessage = Address.MESSAGE_CONSTRAINTS; - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); - } - - @Test - public void toModelType_nullAddress_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, null, VALID_TAGS); - String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName()); - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); - } - - @Test - public void toModelType_invalidTags_throwsIllegalValueException() { - List invalidTags = new ArrayList<>(VALID_TAGS); - invalidTags.add(new JsonAdaptedTag(INVALID_TAG)); - JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, invalidTags); - assertThrows(IllegalValueException.class, person::toModelType); - } - -} diff --git a/src/test/java/seedu/address/storage/JsonAddressBookStorageTest.java b/src/test/java/seedu/address/storage/JsonAddressBookStorageTest.java index ac3c3af9566..e30fee1a07d 100644 --- a/src/test/java/seedu/address/storage/JsonAddressBookStorageTest.java +++ b/src/test/java/seedu/address/storage/JsonAddressBookStorageTest.java @@ -3,10 +3,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static seedu.address.testutil.Assert.assertThrows; -import static seedu.address.testutil.TypicalPersons.ALICE; -import static seedu.address.testutil.TypicalPersons.HOON; -import static seedu.address.testutil.TypicalPersons.IDA; -import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalAddressBook.getTypicalAddressBook; +import static seedu.address.testutil.TypicalContacts.ALICE_MARKED; +import static seedu.address.testutil.TypicalContacts.HOON; +import static seedu.address.testutil.TypicalContacts.IDA; import java.io.IOException; import java.nio.file.Path; @@ -20,7 +20,8 @@ import seedu.address.model.ReadOnlyAddressBook; public class JsonAddressBookStorageTest { - private static final Path TEST_DATA_FOLDER = Paths.get("src", "test", "data", "JsonAddressBookStorageTest"); + private static final Path TEST_DATA_FOLDER = Paths + .get("src", "test", "data", "JsonAddressBookStorageTest"); @TempDir public Path testFolder; @@ -36,8 +37,8 @@ private java.util.Optional readAddressBook(String filePath) private Path addToTestDataPathIfNotNull(String prefsFileInTestDataFolder) { return prefsFileInTestDataFolder != null - ? TEST_DATA_FOLDER.resolve(prefsFileInTestDataFolder) - : null; + ? TEST_DATA_FOLDER.resolve(prefsFileInTestDataFolder) + : null; } @Test @@ -51,13 +52,15 @@ public void read_notJsonFormat_exceptionThrown() { } @Test - public void readAddressBook_invalidPersonAddressBook_throwDataConversionException() { - assertThrows(DataConversionException.class, () -> readAddressBook("invalidPersonAddressBook.json")); + public void readAddressBook_invalidContactAddressBook_throwDataConversionException() { + assertThrows(DataConversionException.class, () -> + readAddressBook("invalidContactAndEventAddressBook.json")); } @Test - public void readAddressBook_invalidAndValidPersonAddressBook_throwDataConversionException() { - assertThrows(DataConversionException.class, () -> readAddressBook("invalidAndValidPersonAddressBook.json")); + public void readAddressBook_invalidAndValidContactAddressBook_throwDataConversionException() { + assertThrows(DataConversionException.class, () -> + readAddressBook("invalidAndValidContactAndEventAddressBook.json")); } @Test @@ -72,18 +75,17 @@ public void readAndSaveAddressBook_allInOrder_success() throws Exception { assertEquals(original, new AddressBook(readBack)); // Modify data, overwrite exiting file, and read back - original.addPerson(HOON); - original.removePerson(ALICE); + original.addContact(HOON); + original.removeContact(ALICE_MARKED); jsonAddressBookStorage.saveAddressBook(original, filePath); readBack = jsonAddressBookStorage.readAddressBook(filePath).get(); assertEquals(original, new AddressBook(readBack)); // Save and read without specifying file path - original.addPerson(IDA); + original.addContact(IDA); jsonAddressBookStorage.saveAddressBook(original); // file path not specified readBack = jsonAddressBookStorage.readAddressBook().get(); // file path not specified assertEquals(original, new AddressBook(readBack)); - } @Test @@ -97,7 +99,7 @@ public void saveAddressBook_nullAddressBook_throwsNullPointerException() { private void saveAddressBook(ReadOnlyAddressBook addressBook, String filePath) { try { new JsonAddressBookStorage(Paths.get(filePath)) - .saveAddressBook(addressBook, addToTestDataPathIfNotNull(filePath)); + .saveAddressBook(addressBook, addToTestDataPathIfNotNull(filePath)); } catch (IOException ioe) { throw new AssertionError("There should not be an error writing to the file.", ioe); } diff --git a/src/test/java/seedu/address/storage/JsonSerializableAddressBookTest.java b/src/test/java/seedu/address/storage/JsonSerializableAddressBookTest.java index 188c9058d20..a478b03c464 100644 --- a/src/test/java/seedu/address/storage/JsonSerializableAddressBookTest.java +++ b/src/test/java/seedu/address/storage/JsonSerializableAddressBookTest.java @@ -11,37 +11,55 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.commons.util.JsonUtil; import seedu.address.model.AddressBook; -import seedu.address.testutil.TypicalPersons; +import seedu.address.testutil.TypicalAddressBook; + public class JsonSerializableAddressBookTest { - private static final Path TEST_DATA_FOLDER = Paths.get("src", "test", "data", "JsonSerializableAddressBookTest"); - private static final Path TYPICAL_PERSONS_FILE = TEST_DATA_FOLDER.resolve("typicalPersonsAddressBook.json"); - private static final Path INVALID_PERSON_FILE = TEST_DATA_FOLDER.resolve("invalidPersonAddressBook.json"); - private static final Path DUPLICATE_PERSON_FILE = TEST_DATA_FOLDER.resolve("duplicatePersonAddressBook.json"); + private static final Path TEST_DATA_FOLDER = Paths + .get("src", "test", "data", "JsonSerializableAddressBookTest"); + private static final Path TYPICAL_CONTACTS_FILE = TEST_DATA_FOLDER + .resolve("typicalContactsAndEventsAddressBook.json"); + private static final Path INVALID_CONTACT_FILE = TEST_DATA_FOLDER + .resolve("invalidContactAndEventAddressBook.json"); + private static final Path DUPLICATE_CONTACT_FILE = TEST_DATA_FOLDER + .resolve("duplicateContactAddressBook.json"); + private static final Path DUPLICATE_EVENT_FILE = TEST_DATA_FOLDER + .resolve("duplicateEventAddressBook.json"); @Test - public void toModelType_typicalPersonsFile_success() throws Exception { - JsonSerializableAddressBook dataFromFile = JsonUtil.readJsonFile(TYPICAL_PERSONS_FILE, - JsonSerializableAddressBook.class).get(); + public void toModelType_typicalFile_success() throws Exception { + JsonSerializableAddressBook dataFromFile = JsonUtil.readJsonFile( + TYPICAL_CONTACTS_FILE, + JsonSerializableAddressBook.class).get(); AddressBook addressBookFromFile = dataFromFile.toModelType(); - AddressBook typicalPersonsAddressBook = TypicalPersons.getTypicalAddressBook(); - assertEquals(addressBookFromFile, typicalPersonsAddressBook); + AddressBook typicalAddressBook = TypicalAddressBook.getTypicalAddressBook(); + assertEquals(addressBookFromFile, typicalAddressBook); } @Test - public void toModelType_invalidPersonFile_throwsIllegalValueException() throws Exception { - JsonSerializableAddressBook dataFromFile = JsonUtil.readJsonFile(INVALID_PERSON_FILE, - JsonSerializableAddressBook.class).get(); + public void toModelType_invalidContactFile_throwsIllegalValueException() throws Exception { + JsonSerializableAddressBook dataFromFile = JsonUtil.readJsonFile( + INVALID_CONTACT_FILE, + JsonSerializableAddressBook.class).get(); assertThrows(IllegalValueException.class, dataFromFile::toModelType); } @Test - public void toModelType_duplicatePersons_throwsIllegalValueException() throws Exception { - JsonSerializableAddressBook dataFromFile = JsonUtil.readJsonFile(DUPLICATE_PERSON_FILE, - JsonSerializableAddressBook.class).get(); - assertThrows(IllegalValueException.class, JsonSerializableAddressBook.MESSAGE_DUPLICATE_PERSON, - dataFromFile::toModelType); + public void toModelType_duplicateContacts_throwsIllegalValueException() throws Exception { + JsonSerializableAddressBook dataFromFile = JsonUtil.readJsonFile( + DUPLICATE_CONTACT_FILE, + JsonSerializableAddressBook.class).get(); + assertThrows(IllegalValueException.class, JsonSerializableAddressBook.MESSAGE_DUPLICATE_CONTACT, + dataFromFile::toModelType); } + @Test + public void toModelType_duplicateEvents_throwsIllegalValueException() throws Exception { + JsonSerializableAddressBook dataFromFile = JsonUtil.readJsonFile( + DUPLICATE_EVENT_FILE, + JsonSerializableAddressBook.class).get(); + assertThrows(IllegalValueException.class, JsonSerializableAddressBook.MESSAGE_DUPLICATE_EVENT, + dataFromFile::toModelType); + } } diff --git a/src/test/java/seedu/address/storage/JsonUserPrefsStorageTest.java b/src/test/java/seedu/address/storage/JsonUserPrefsStorageTest.java index 16f33f4a6bb..5ff22134242 100644 --- a/src/test/java/seedu/address/storage/JsonUserPrefsStorageTest.java +++ b/src/test/java/seedu/address/storage/JsonUserPrefsStorageTest.java @@ -105,8 +105,8 @@ public void saveUserPrefs_allInOrder_success() throws DataConversionException, I UserPrefs original = new UserPrefs(); original.setGuiSettings(new GuiSettings(1200, 200, 0, 2)); - Path pefsFilePath = testFolder.resolve("TempPrefs.json"); - JsonUserPrefsStorage jsonUserPrefsStorage = new JsonUserPrefsStorage(pefsFilePath); + Path prefsFilePath = testFolder.resolve("TempPrefs.json"); + JsonUserPrefsStorage jsonUserPrefsStorage = new JsonUserPrefsStorage(prefsFilePath); //Try writing when the file doesn't exist jsonUserPrefsStorage.saveUserPrefs(original); @@ -120,4 +120,9 @@ public void saveUserPrefs_allInOrder_success() throws DataConversionException, I assertEquals(original, readBack); } + @Test + public void getPath_success() { + JsonUserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(testFolder.resolve("TypicalUserPref.json")); + assertEquals(userPrefsStorage.getUserPrefsFilePath(), testFolder.resolve("TypicalUserPref.json")); + } } diff --git a/src/test/java/seedu/address/storage/StorageManagerTest.java b/src/test/java/seedu/address/storage/StorageManagerTest.java index 99a16548970..00d1e24fd0b 100644 --- a/src/test/java/seedu/address/storage/StorageManagerTest.java +++ b/src/test/java/seedu/address/storage/StorageManagerTest.java @@ -2,7 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalAddressBook.getTypicalAddressBook; import java.nio.file.Path; @@ -65,4 +65,8 @@ public void getAddressBookFilePath() { assertNotNull(storageManager.getAddressBookFilePath()); } + @Test + public void getUserPrefsFilePath() { + assertNotNull(storageManager.getUserPrefsFilePath()); + } } diff --git a/src/test/java/seedu/address/testutil/AddressBookBuilder.java b/src/test/java/seedu/address/testutil/AddressBookBuilder.java index d53799fd110..2b91ca33b7f 100644 --- a/src/test/java/seedu/address/testutil/AddressBookBuilder.java +++ b/src/test/java/seedu/address/testutil/AddressBookBuilder.java @@ -1,12 +1,13 @@ package seedu.address.testutil; import seedu.address.model.AddressBook; -import seedu.address.model.person.Person; +import seedu.address.model.contact.Contact; +import seedu.address.model.event.Event; /** * A utility class to help with building Addressbook objects. * Example usage:
- * {@code AddressBook ab = new AddressBookBuilder().withPerson("John", "Doe").build();} + * {@code AddressBook ab = new AddressBookBuilder().withContact("John", "Doe").build();} */ public class AddressBookBuilder { @@ -17,14 +18,22 @@ public AddressBookBuilder() { } public AddressBookBuilder(AddressBook addressBook) { - this.addressBook = addressBook; + this.addressBook = addressBook.copy(); } /** - * Adds a new {@code Person} to the {@code AddressBook} that we are building. + * Adds a new {@code Contact} to the {@code AddressBook} that we are building. */ - public AddressBookBuilder withPerson(Person person) { - addressBook.addPerson(person); + public AddressBookBuilder withContact(Contact contact) { + addressBook.addContact(contact); + return this; + } + + /** + * Adds a new {@code Event} to the {@code AddressBook} that we are building. + */ + public AddressBookBuilder withEvent(Event event) { + addressBook.addEvent(event); return this; } diff --git a/src/test/java/seedu/address/testutil/ContactBuilder.java b/src/test/java/seedu/address/testutil/ContactBuilder.java new file mode 100644 index 00000000000..8152c982f91 --- /dev/null +++ b/src/test/java/seedu/address/testutil/ContactBuilder.java @@ -0,0 +1,175 @@ +package seedu.address.testutil; + +import static seedu.address.testutil.TypicalEvents.CS2100_CONSULTATION_UUID; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import seedu.address.model.common.Address; +import seedu.address.model.common.Name; +import seedu.address.model.common.ZoomLink; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Phone; +import seedu.address.model.contact.TelegramHandle; +import seedu.address.model.tag.Tag; +import seedu.address.model.util.SampleDataUtil; + +/** + * A utility class to help with building Contact objects. + */ +public class ContactBuilder { + + public static final String DEFAULT_NAME = "Amy Bee"; + public static final String DEFAULT_PHONE = "85355255"; + public static final String DEFAULT_EMAIL = "amy@gmail.com"; + public static final String DEFAULT_ADDRESS = "123, Jurong West Ave 6, #08-111"; + public static final String DEFAULT_TELEGRAM_HANDLE = "amyBeeBee"; + public static final String DEFAULT_ZOOM_LINK = "https://nus-sg.zoom.us/j/0123456789?pwd=ABCDEFG"; + public static final boolean DEFAULT_IS_MARKED = true; + public static final String DEFAULT_TAG = "friends"; + public static final UUID DEFAULT_UUID = TypicalContacts.ALICE_UUID; + public static final UUID DEFAULT_LINKED_EVENT = CS2100_CONSULTATION_UUID; + + private Name name; + private Phone phone; + private Email email; + private Address address; + private TelegramHandle telegramHandle; + private ZoomLink zoomLink; + private Set tags; + private UUID uuid; + private Set linkedEvents; + private boolean isMarked; + + /** + * Creates a {@code ContactBuilder} with the default details. + */ + public ContactBuilder() { + name = new Name(DEFAULT_NAME); + phone = new Phone(DEFAULT_PHONE); + email = new Email(DEFAULT_EMAIL); + address = new Address(DEFAULT_ADDRESS); + // adding of telegram handle and zoom link not implemented yet + telegramHandle = new TelegramHandle(DEFAULT_TELEGRAM_HANDLE); + zoomLink = new ZoomLink(DEFAULT_ZOOM_LINK); + tags = new HashSet<>(); + tags.add(new Tag(DEFAULT_TAG)); + uuid = DEFAULT_UUID; + linkedEvents = new HashSet<>(); + linkedEvents.add(DEFAULT_LINKED_EVENT); + isMarked = DEFAULT_IS_MARKED; + } + + /** + * Initializes the ContactBuilder with the data of {@code contactToCopy}. + */ + public ContactBuilder(Contact contactToCopy) { + name = contactToCopy.getName(); + phone = contactToCopy.getPhone(); + email = contactToCopy.getEmail(); + address = contactToCopy.getAddress(); + telegramHandle = contactToCopy.getTelegramHandle(); + zoomLink = contactToCopy.getZoomLink(); + tags = new HashSet<>(contactToCopy.getTags()); + uuid = contactToCopy.getUuid(); + linkedEvents = new HashSet<>(contactToCopy.getLinkedEvents()); + isMarked = contactToCopy.getIsMarked(); + } + + /** + * Sets the {@code Name} of the {@code Contact} that we are building. + */ + public ContactBuilder withName(String name) { + this.name = new Name(name); + return this; + } + + /** + * Parses the {@code tags} into a {@code Set} and set it to the {@code Contact} that we are building. + */ + public ContactBuilder withTags(String ... tags) { + this.tags = SampleDataUtil.getTagSet(tags); + return this; + } + + /** + * Sets the {@code Address} of the {@code Contact} that we are building. + */ + public ContactBuilder withAddress(String address) { + this.address = address != null ? new Address(address) : null; + return this; + } + + /** + * Sets the {@code Phone} of the {@code Contact} that we are building. + */ + public ContactBuilder withPhone(String phone) { + this.phone = phone != null ? new Phone(phone) : null; + return this; + } + + /** + * Sets the {@code Email} of the {@code Contact} that we are building. + */ + public ContactBuilder withEmail(String email) { + this.email = new Email(email); + return this; + } + + /** + * Sets the {@code TelegramHandle} of the {@code Contact} that we are building. + */ + public ContactBuilder withTelegramHandle(String telegramHandle) { + this.telegramHandle = telegramHandle != null ? new TelegramHandle(telegramHandle) : null; + return this; + } + + /** + * Sets the {@code ZoomLink} of the {@code Contact} that we are building. + */ + public ContactBuilder withZoomLink(String zoomLink) { + this.zoomLink = zoomLink != null ? new ZoomLink(zoomLink) : null; + return this; + } + + /** + * Sets the {@code UUID} of the {@code Contact} that we are building. + */ + public ContactBuilder withUuid(UUID uuid) { + this.uuid = uuid; + return this; + } + + /** + * Sets the {@code UUID} of the {@code contact} that we are building to a random value. + */ + public ContactBuilder withRandomUuid() { + this.uuid = UUID.randomUUID(); + return this; + } + + /** + * Parses the {@code events} into a {@code Set} and set it to the {@code Contact} that we are building. + */ + public ContactBuilder withLinkedEvents(UUID ...events) { + this.linkedEvents = Set.of(events); + return this; + } + + /** + * Sets the {@code isMarked} of the {@code Contact} that we are building to {@code isMarked}. + */ + public ContactBuilder withMarked(boolean isMarked) { + this.isMarked = isMarked; + return this; + } + + /** + * Creates an {@code Contact} from this {@code Contactbuilder}. + */ + public Contact build() { + return new Contact(name, phone, email, address, zoomLink, telegramHandle, tags, uuid, linkedEvents, isMarked); + } +} diff --git a/src/test/java/seedu/address/testutil/ContactUtil.java b/src/test/java/seedu/address/testutil/ContactUtil.java new file mode 100644 index 00000000000..d2fa5f9b92f --- /dev/null +++ b/src/test/java/seedu/address/testutil/ContactUtil.java @@ -0,0 +1,77 @@ +package seedu.address.testutil; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DELETE_TAG; +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.logic.parser.CliSyntax.PREFIX_TELEGRAM; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ZOOM; + +import java.util.Set; + +import seedu.address.logic.commands.contact.CAddCommand; +import seedu.address.logic.commands.contact.CEditCommand.EditContactDescriptor; +import seedu.address.model.contact.Contact; +import seedu.address.model.tag.Tag; + +/** + * A utility class for Contact. + */ +public class ContactUtil { + + /** + * Returns an add command string for adding the {@code contact}. + */ + public static String getCAddCommand(Contact contact) { + return CAddCommand.COMMAND_WORD + " " + getContactDetails(contact); + } + + /** + * Returns the part of command string for the given {@code contact}'s details. + */ + public static String getContactDetails(Contact contact) { + StringBuilder sb = new StringBuilder(); + sb.append(PREFIX_NAME + contact.getName().fullName + " "); + sb.append(PREFIX_PHONE + contact.getPhone().value + " "); + sb.append(PREFIX_EMAIL + contact.getEmail().value + " "); + sb.append(PREFIX_ADDRESS + contact.getAddress().value + " "); + sb.append(PREFIX_TELEGRAM + contact.getTelegramHandle().handle + " "); + sb.append(PREFIX_ZOOM + contact.getZoomLink().link + " "); + contact.getTags().stream().forEach( + s -> sb.append(PREFIX_TAG + s.tagName + " ") + ); + return sb.toString(); + } + + /** + * Returns the part of command string for the given {@code EditContactDescriptor}'s details. + */ + public static String getEditContactDescriptorDetails(EditContactDescriptor descriptor) { + StringBuilder sb = new StringBuilder(); + descriptor.getName().ifPresent(name -> sb.append(PREFIX_NAME).append(name.fullName).append(" ")); + descriptor.getPhone().ifPresent(phone -> sb.append(PREFIX_PHONE).append(phone.value).append(" ")); + descriptor.getEmail().ifPresent(email -> sb.append(PREFIX_EMAIL).append(email.value).append(" ")); + descriptor.getAddress().ifPresent(address -> sb.append(PREFIX_ADDRESS).append(address.value).append(" ")); + descriptor.getZoomLink().ifPresent(zoom -> sb.append(PREFIX_ZOOM).append(zoom.link).append(" ")); + descriptor.getTelegramHandle().ifPresent(telegramHandle -> sb.append(PREFIX_TELEGRAM) + .append(telegramHandle.handle).append(" ")); + if (descriptor.getTags().isPresent()) { + Set tags = descriptor.getTags().get(); + if (!tags.isEmpty()) { + tags.forEach(s -> sb.append(PREFIX_TAG).append(s.tagName).append(" ")); + } + } + if (descriptor.getTagsToDelete().isPresent()) { + Set tagsToDelete = descriptor.getTagsToDelete().get(); + if (!tagsToDelete.isEmpty()) { + tagsToDelete.forEach(s -> sb.append(PREFIX_DELETE_TAG).append(s.tagName).append(" ")); + } + } + if (descriptor.isShouldDeleteAllTags()) { + sb.append(PREFIX_DELETE_TAG).append("* "); + } + return sb.toString(); + } +} diff --git a/src/test/java/seedu/address/testutil/EditContactDescriptorBuilder.java b/src/test/java/seedu/address/testutil/EditContactDescriptorBuilder.java new file mode 100644 index 00000000000..70184bfdbb0 --- /dev/null +++ b/src/test/java/seedu/address/testutil/EditContactDescriptorBuilder.java @@ -0,0 +1,129 @@ +package seedu.address.testutil; + +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import seedu.address.logic.commands.contact.CEditCommand.EditContactDescriptor; +import seedu.address.model.common.Address; +import seedu.address.model.common.Name; +import seedu.address.model.common.ZoomLink; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Phone; +import seedu.address.model.contact.TelegramHandle; +import seedu.address.model.tag.Tag; + +/** + * A utility class to help with building EditContactDescriptor objects. + */ +public class EditContactDescriptorBuilder { + + private EditContactDescriptor descriptor; + + public EditContactDescriptorBuilder() { + descriptor = new EditContactDescriptor(); + } + + public EditContactDescriptorBuilder(EditContactDescriptor descriptor) { + this.descriptor = new EditContactDescriptor(descriptor); + } + + /** + * Returns an {@code EditContactDescriptor} with fields containing {@code contact}'s details + * and the set of tags to delete ({@code toDelete}). Also takes in a {@code shouldDeleteAllTags} + * to indicate if all tags should be deleted first. + */ + public EditContactDescriptorBuilder(Contact contact, Set toDelete, boolean shouldDeleteAllTags) { + descriptor = new EditContactDescriptor(); + descriptor.setName(contact.getName()); + descriptor.setPhone(contact.getPhone()); + descriptor.setEmail(contact.getEmail()); + descriptor.setAddress(contact.getAddress()); + descriptor.setZoomLink(contact.getZoomLink()); + descriptor.setTelegramHandle(contact.getTelegramHandle()); + descriptor.setTags(contact.getTags()); + descriptor.setTagsToDelete(toDelete); + descriptor.setShouldDeleteAllTags(shouldDeleteAllTags); + } + + /** + * Sets the {@code Name} of the {@code EditContactDescriptor} that we are building. + */ + public EditContactDescriptorBuilder withName(String name) { + descriptor.setName(new Name(name)); + return this; + } + + /** + * Sets the {@code Phone} of the {@code EditContactDescriptor} that we are building. + */ + public EditContactDescriptorBuilder withPhone(String phone) { + descriptor.setPhone(new Phone(phone)); + return this; + } + + /** + * Sets the {@code Email} of the {@code EditContactDescriptor} that we are building. + */ + public EditContactDescriptorBuilder withEmail(String email) { + descriptor.setEmail(new Email(email)); + return this; + } + + /** + * Sets the {@code Address} of the {@code EditContactDescriptor} that we are building. + */ + public EditContactDescriptorBuilder withAddress(String address) { + descriptor.setAddress(new Address(address)); + return this; + } + + /** + * Sets the {@code Telegram} of the {@code EditContactDescriptor} that we are building. + */ + public EditContactDescriptorBuilder withTelegram(String telegram) { + descriptor.setTelegramHandle(new TelegramHandle(telegram)); + return this; + } + + /** + * Sets the {@code ZoomLink} of the {@code EditContactDescriptor} that we are building. + */ + public EditContactDescriptorBuilder withZoomLink(String zoomLink) { + descriptor.setZoomLink(new ZoomLink(zoomLink)); + return this; + } + + /** + * Parses the {@code tags} into a {@code Set} and set it to the {@code EditContactDescriptor} + * that we are building. + */ + public EditContactDescriptorBuilder withTags(String... tags) { + Set tagSet = Stream.of(tags).map(Tag::new).collect(Collectors.toSet()); + descriptor.setTags(tagSet); + return this; + } + + /** + * Parses the {@code tags} into a {@code Set} and set it to the {@code EditContactDescriptor} + * that we are building for deletion. + */ + public EditContactDescriptorBuilder withTagsToDelete(String... tags) { + Set tagSet = Stream.of(tags).map(Tag::new).collect(Collectors.toSet()); + descriptor.setTagsToDelete(tagSet); + return this; + } + + /** + * Set the boolean {@code shouldDeleteAllTags} to the {@code EditContactDescriptor} that we are building. + */ + public EditContactDescriptorBuilder withDeleteAllTags(boolean shouldDeleteAllTags) { + descriptor.setShouldDeleteAllTags(shouldDeleteAllTags); + return this; + } + + public EditContactDescriptor build() { + return descriptor; + } +} diff --git a/src/test/java/seedu/address/testutil/EditEventDescriptorBuilder.java b/src/test/java/seedu/address/testutil/EditEventDescriptorBuilder.java new file mode 100644 index 00000000000..70ab5560c26 --- /dev/null +++ b/src/test/java/seedu/address/testutil/EditEventDescriptorBuilder.java @@ -0,0 +1,130 @@ +package seedu.address.testutil; + +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import seedu.address.logic.commands.event.EEditCommand.EditEventDescriptor; +import seedu.address.model.common.Address; +import seedu.address.model.common.Name; +import seedu.address.model.common.ZoomLink; +import seedu.address.model.event.Description; +import seedu.address.model.event.EndDateTime; +import seedu.address.model.event.Event; +import seedu.address.model.event.StartDateTime; +import seedu.address.model.tag.Tag; + +/** + * A utility class to help with building EditEventDescriptor objects. + */ +public class EditEventDescriptorBuilder { + + private EditEventDescriptor descriptor; + + public EditEventDescriptorBuilder() { + descriptor = new EditEventDescriptor(); + } + + public EditEventDescriptorBuilder(EditEventDescriptor descriptor) { + this.descriptor = new EditEventDescriptor(descriptor); + } + + /** + * Returns an {@code EditEventDescriptor} with fields containing {@code event}'s details + * and the set of tags to delete ({@code toDelete}). Also takes in a {@code shouldDeleteAllTags} + * to indicate if all tags should be deleted first. + */ + public EditEventDescriptorBuilder(Event event, Set toDelete, boolean shouldDeleteAllTags) { + descriptor = new EditEventDescriptor(); + descriptor.setName(event.getName()); + descriptor.setStartDateTime(event.getStartDateAndTime()); + descriptor.setEndDateTime(event.getEndDateAndTime()); + descriptor.setDescription(event.getDescription()); + descriptor.setAddress(event.getAddress()); + descriptor.setZoomLink(event.getZoomLink()); + descriptor.setTags(event.getTags()); + descriptor.setTagsToDelete(toDelete); + descriptor.setShouldDeleteAllTags(shouldDeleteAllTags); + } + + /** + * Sets the {@code Name} of the {@code EditEventDescriptor} that we are building. + */ + public EditEventDescriptorBuilder withName(String name) { + descriptor.setName(new Name(name)); + return this; + } + + /** + * Sets the {@code startDateTime} of the {@code EditEventDescriptor} that we are building. + */ + public EditEventDescriptorBuilder withStartDateTime(String startDateTime) { + descriptor.setStartDateTime(new StartDateTime(startDateTime)); + return this; + } + + /** + * Sets the {@code endDateTime} of the {@code EditEventDescriptor} that we are building. + */ + public EditEventDescriptorBuilder withEndDateTime(String endDateTime) { + descriptor.setEndDateTime(new EndDateTime(endDateTime)); + return this; + } + + + /** + * Sets the {@code Description} of the {@code EditEventDescriptor} that we are building. + */ + public EditEventDescriptorBuilder withDescription(String description) { + descriptor.setDescription(new Description(description)); + return this; + } + + /** + * Sets the {@code Address} of the {@code EditEventDescriptor} that we are building. + */ + public EditEventDescriptorBuilder withAddress(String address) { + descriptor.setAddress(new Address(address)); + return this; + } + + /** + * Sets the {@code ZoomLink} of the {@code EditEventDescriptor} that we are building. + */ + public EditEventDescriptorBuilder withZoomLink(String zoomLink) { + descriptor.setZoomLink(new ZoomLink(zoomLink)); + return this; + } + + /** + * Parses the {@code tags} into a {@code Set} and set it to the {@code EditEventDescriptor} + * that we are building. + */ + public EditEventDescriptorBuilder withTags(String... tags) { + Set tagSet = Stream.of(tags).map(Tag::new).collect(Collectors.toSet()); + descriptor.setTags(tagSet); + return this; + } + + /** + * Parses the {@code tags} into a {@code Set} and set it to the {@code EditEventDescriptor} + * that we are building for deletion. + */ + public EditEventDescriptorBuilder withTagsToDelete(String... tags) { + Set tagSet = Stream.of(tags).map(Tag::new).collect(Collectors.toSet()); + descriptor.setTagsToDelete(tagSet); + return this; + } + + /** + * Set the boolean {@code shouldDeleteAllTags} to the {@code EditEventDescriptor} that we are building. + */ + public EditEventDescriptorBuilder withDeleteAllTags(boolean shouldDeleteAllTags) { + descriptor.setShouldDeleteAllTags(shouldDeleteAllTags); + return this; + } + + public EditEventDescriptor build() { + return descriptor; + } +} diff --git a/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java b/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java deleted file mode 100644 index 4584bd5044e..00000000000 --- a/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java +++ /dev/null @@ -1,87 +0,0 @@ -package seedu.address.testutil; - -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; -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; - -/** - * A utility class to help with building EditPersonDescriptor objects. - */ -public class EditPersonDescriptorBuilder { - - private EditPersonDescriptor descriptor; - - public EditPersonDescriptorBuilder() { - descriptor = new EditPersonDescriptor(); - } - - public EditPersonDescriptorBuilder(EditPersonDescriptor descriptor) { - this.descriptor = new EditPersonDescriptor(descriptor); - } - - /** - * Returns an {@code EditPersonDescriptor} with fields containing {@code person}'s details - */ - public EditPersonDescriptorBuilder(Person person) { - descriptor = new EditPersonDescriptor(); - descriptor.setName(person.getName()); - descriptor.setPhone(person.getPhone()); - descriptor.setEmail(person.getEmail()); - descriptor.setAddress(person.getAddress()); - descriptor.setTags(person.getTags()); - } - - /** - * Sets the {@code Name} of the {@code EditPersonDescriptor} that we are building. - */ - public EditPersonDescriptorBuilder withName(String name) { - descriptor.setName(new Name(name)); - return this; - } - - /** - * Sets the {@code Phone} of the {@code EditPersonDescriptor} that we are building. - */ - public EditPersonDescriptorBuilder withPhone(String phone) { - descriptor.setPhone(new Phone(phone)); - return this; - } - - /** - * Sets the {@code Email} of the {@code EditPersonDescriptor} that we are building. - */ - public EditPersonDescriptorBuilder withEmail(String email) { - descriptor.setEmail(new Email(email)); - return this; - } - - /** - * Sets the {@code Address} of the {@code EditPersonDescriptor} that we are building. - */ - public EditPersonDescriptorBuilder withAddress(String address) { - descriptor.setAddress(new Address(address)); - return this; - } - - /** - * Parses the {@code tags} into a {@code Set} and set it to the {@code EditPersonDescriptor} - * that we are building. - */ - public EditPersonDescriptorBuilder withTags(String... tags) { - Set tagSet = Stream.of(tags).map(Tag::new).collect(Collectors.toSet()); - descriptor.setTags(tagSet); - return this; - } - - public EditPersonDescriptor build() { - return descriptor; - } -} diff --git a/src/test/java/seedu/address/testutil/EventBuilder.java b/src/test/java/seedu/address/testutil/EventBuilder.java new file mode 100644 index 00000000000..c3dd0b22c35 --- /dev/null +++ b/src/test/java/seedu/address/testutil/EventBuilder.java @@ -0,0 +1,176 @@ +package seedu.address.testutil; + +import static seedu.address.testutil.TypicalEvents.CS2103_MIDTERM_MARKED_UUID; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import seedu.address.model.common.Address; +import seedu.address.model.common.Name; +import seedu.address.model.common.ZoomLink; +import seedu.address.model.event.Description; +import seedu.address.model.event.EndDateTime; +import seedu.address.model.event.Event; +import seedu.address.model.event.StartDateTime; +import seedu.address.model.tag.Tag; +import seedu.address.model.util.SampleDataUtil; + +/** + * A utility class to help with building Event objects. + */ +public class EventBuilder { + public static final String DEFAULT_NAME = "Midterms"; + public static final String DEFAULT_START_DATE_AND_TIME = "13-10-2021 21:00"; + public static final String DEFAULT_END_DATE_AND_TIME = "13-10-2021 23:00"; + public static final String DEFAULT_DESCRIPTION = "Important"; + public static final String DEFAULT_ADDRESS = "COM2, SR1, #02-11"; + public static final String DEFAULT_ZOOM_LINK = "https://nus-sg.zoom.us/j/0123456789?pwd=ABCDEFG"; + public static final String DEFAULT_TAG = "exams"; + public static final UUID DEFAULT_UUID = CS2103_MIDTERM_MARKED_UUID; + public static final boolean DEFAULT_IS_MARKED = true; + + //Compulsory fields + private Name name; + private StartDateTime startDateAndTime; + + //Optional fields + private EndDateTime endDateAndTime; + private Description description; + private Address address; + private ZoomLink zoomLink; + private Set tags; + private UUID uuid; + private Set linkedContacts; + private boolean isMarked; + + /** + * Creates a {@code EventBuilder} with the default details. + */ + public EventBuilder() { + name = new Name(DEFAULT_NAME); + startDateAndTime = new StartDateTime(DEFAULT_START_DATE_AND_TIME); + endDateAndTime = new EndDateTime(DEFAULT_END_DATE_AND_TIME); + description = new Description(DEFAULT_DESCRIPTION); + address = new Address(DEFAULT_ADDRESS); + zoomLink = new ZoomLink(DEFAULT_ZOOM_LINK); + tags = new HashSet<>(); + tags.add(new Tag(DEFAULT_TAG)); + uuid = DEFAULT_UUID; + linkedContacts = new HashSet<>(); + isMarked = DEFAULT_IS_MARKED; + } + + /** + * Initializes the EventBuilder with the data of {@code eventToCopy}. + */ + public EventBuilder(Event eventToCopy) { + name = eventToCopy.getName(); + startDateAndTime = eventToCopy.getStartDateAndTime(); + endDateAndTime = eventToCopy.getEndDateAndTime(); + description = eventToCopy.getDescription(); + address = eventToCopy.getAddress(); + zoomLink = eventToCopy.getZoomLink(); + tags = new HashSet<>(eventToCopy.getTags()); + isMarked = eventToCopy.getIsMarked(); + uuid = eventToCopy.getUuid(); + linkedContacts = new HashSet<>(eventToCopy.getLinkedContacts()); + } + + /** + * Sets the {@code Name} of the {@code Event} that we are building. + */ + public EventBuilder withName(String name) { + this.name = new Name(name); + return this; + } + + /** + * Parses the {@code tags} into a {@code Set} and set it to the {@code Event} that we are building. + */ + public EventBuilder withTags(String ...tags) { + this.tags = SampleDataUtil.getTagSet(tags); + return this; + } + + /** + * Sets the {@code description} of the {@code Event} that we are building. + */ + public EventBuilder withStartDateAndTime(String startDateTime) { + this.startDateAndTime = new StartDateTime(startDateTime); + return this; + } + + /** + * Sets the {@code endDateTime} of the {@code Event} that we are building. + */ + public EventBuilder withEndDateAndTime(String endDateTime) { + this.endDateAndTime = endDateTime != null ? new EndDateTime(endDateTime) : null; + return this; + } + + /** + * Sets the {@code description} of the {@code Event} that we are building. + */ + public EventBuilder withDescription(String description) { + this.description = description != null ? new Description(description) : null; + return this; + } + + /** + * Sets the {@code Address} of the {@code Event} that we are building. + */ + public EventBuilder withAddress(String address) { + this.address = address != null ? new Address(address) : null; + return this; + } + + /** + * Sets the {@code ZoomLink} of the {@code Event} that we are building. + */ + public EventBuilder withZoomLink(String zoomLink) { + this.zoomLink = zoomLink != null ? new ZoomLink(zoomLink) : null; + return this; + } + + /** + * Sets the {@code UUID} of the {@code Event} that we are building. + */ + public EventBuilder withUuid(UUID uuid) { + this.uuid = uuid; + return this; + } + + /** + * Sets the {@code UUID} of the {@code Event} that we are building to a random one. + */ + public EventBuilder withRandomUuid() { + this.uuid = UUID.randomUUID(); + return this; + } + + /** + * Parses the {@code contacts} into a {@code Set} and set it to the {@code Event} that we are building. + */ + public EventBuilder withLinkedContacts(UUID ...contacts) { + this.linkedContacts = Set.of(contacts); + return this; + } + + /** + * Sets the {@code isMarked} of the {@code Event} that we are building to {@code isMarked}. + */ + public EventBuilder withMarked(boolean isMarked) { + this.isMarked = isMarked; + return this; + } + + /** + * Creates an {@code Event} from this {@code Eventbuilder}. + */ + public Event build() { + return new Event(name, startDateAndTime, endDateAndTime, description, address, zoomLink, + tags, uuid, linkedContacts, isMarked); + + } +} diff --git a/src/test/java/seedu/address/testutil/EventUtil.java b/src/test/java/seedu/address/testutil/EventUtil.java new file mode 100644 index 00000000000..bf7e74882e5 --- /dev/null +++ b/src/test/java/seedu/address/testutil/EventUtil.java @@ -0,0 +1,75 @@ +package seedu.address.testutil; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DELETE_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_END_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_START_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ZOOM; + +import java.util.Set; + +import seedu.address.logic.commands.event.EAddCommand; +import seedu.address.logic.commands.event.EEditCommand.EditEventDescriptor; +import seedu.address.model.event.Event; +import seedu.address.model.tag.Tag; + +/** + * A utility class for Events + */ +public class EventUtil { + + /** + * Returns an add command string for adding the {@code event}. + */ + public static String getEAddCommand(Event event) { + return EAddCommand.COMMAND_WORD + " " + getEventDetails(event); + } + + /** + * Returns the part of command string for the given {@code event}'s details. + */ + public static String getEventDetails(Event event) { + StringBuilder sb = new StringBuilder(); + sb.append(PREFIX_NAME + event.getName().fullName + " "); + sb.append(PREFIX_START_TIME + event.getStartDateAndTime().toString() + " "); + sb.append(PREFIX_END_TIME + event.getEndDateAndTime().toString() + " "); + sb.append(PREFIX_DESCRIPTION + event.getDescription().value + " "); + sb.append(PREFIX_ADDRESS + event.getAddress().value + " "); + sb.append(PREFIX_ZOOM + event.getZoomLink().link + " "); + event.getTags().stream().forEach(s -> sb.append(PREFIX_TAG + s.tagName + " ")); + return sb.toString(); + } + + /** + * Returns the part of command string for the given {@code EditEventDescriptor}'s details. + */ + public static String getEditEventDescriptorDetails(EditEventDescriptor descriptor) { + StringBuilder sb = new StringBuilder(); + descriptor.getName().ifPresent(name -> sb.append(PREFIX_NAME).append(name.fullName).append(" ")); + descriptor.getStartDateTime().ifPresent(time -> sb.append(PREFIX_START_TIME) + .append(time.toString()).append(" ")); + descriptor.getEndDateTime().ifPresent(time -> sb.append(PREFIX_END_TIME).append(time.toString()).append(" ")); + descriptor.getDescription().ifPresent(des -> sb.append(PREFIX_DESCRIPTION).append(des.value).append(" ")); + descriptor.getAddress().ifPresent(add -> sb.append(PREFIX_ADDRESS).append(add.value).append(" ")); + descriptor.getZoomLink().ifPresent(link -> sb.append(PREFIX_ZOOM).append(link.link).append(" ")); + if (descriptor.getTags().isPresent()) { + Set tags = descriptor.getTags().get(); + if (!tags.isEmpty()) { + tags.forEach(s -> sb.append(PREFIX_TAG).append(s.tagName).append(" ")); + } + } + if (descriptor.getTagsToDelete().isPresent()) { + Set tagsToDelete = descriptor.getTagsToDelete().get(); + if (!tagsToDelete.isEmpty()) { + tagsToDelete.forEach(s -> sb.append(PREFIX_DELETE_TAG).append(s.tagName).append(" ")); + } + } + if (descriptor.getShouldDeleteAllTags()) { + sb.append(PREFIX_DELETE_TAG).append("* "); + } + return sb.toString(); + } +} diff --git a/src/test/java/seedu/address/testutil/PersonBuilder.java b/src/test/java/seedu/address/testutil/PersonBuilder.java deleted file mode 100644 index 6be381d39ba..00000000000 --- a/src/test/java/seedu/address/testutil/PersonBuilder.java +++ /dev/null @@ -1,96 +0,0 @@ -package seedu.address.testutil; - -import java.util.HashSet; -import java.util.Set; - -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; -import seedu.address.model.util.SampleDataUtil; - -/** - * A utility class to help with building Person objects. - */ -public class PersonBuilder { - - public static final String DEFAULT_NAME = "Amy Bee"; - public static final String DEFAULT_PHONE = "85355255"; - public static final String DEFAULT_EMAIL = "amy@gmail.com"; - public static final String DEFAULT_ADDRESS = "123, Jurong West Ave 6, #08-111"; - - private Name name; - private Phone phone; - private Email email; - private Address address; - private Set tags; - - /** - * Creates a {@code PersonBuilder} with the default details. - */ - public PersonBuilder() { - name = new Name(DEFAULT_NAME); - phone = new Phone(DEFAULT_PHONE); - email = new Email(DEFAULT_EMAIL); - address = new Address(DEFAULT_ADDRESS); - tags = new HashSet<>(); - } - - /** - * Initializes the PersonBuilder with the data of {@code personToCopy}. - */ - public PersonBuilder(Person personToCopy) { - name = personToCopy.getName(); - phone = personToCopy.getPhone(); - email = personToCopy.getEmail(); - address = personToCopy.getAddress(); - tags = new HashSet<>(personToCopy.getTags()); - } - - /** - * Sets the {@code Name} of the {@code Person} that we are building. - */ - public PersonBuilder withName(String name) { - this.name = new Name(name); - return this; - } - - /** - * Parses the {@code tags} into a {@code Set} and set it to the {@code Person} that we are building. - */ - public PersonBuilder withTags(String ... tags) { - this.tags = SampleDataUtil.getTagSet(tags); - return this; - } - - /** - * Sets the {@code Address} of the {@code Person} that we are building. - */ - public PersonBuilder withAddress(String address) { - this.address = new Address(address); - return this; - } - - /** - * Sets the {@code Phone} of the {@code Person} that we are building. - */ - public PersonBuilder withPhone(String phone) { - this.phone = new Phone(phone); - return this; - } - - /** - * Sets the {@code Email} of the {@code Person} that we are building. - */ - public PersonBuilder withEmail(String email) { - this.email = new Email(email); - return this; - } - - public Person build() { - return new Person(name, phone, email, address, tags); - } - -} diff --git a/src/test/java/seedu/address/testutil/PersonUtil.java b/src/test/java/seedu/address/testutil/PersonUtil.java deleted file mode 100644 index 90849945183..00000000000 --- a/src/test/java/seedu/address/testutil/PersonUtil.java +++ /dev/null @@ -1,62 +0,0 @@ -package seedu.address.testutil; - -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 seedu.address.logic.commands.AddCommand; -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; -import seedu.address.model.person.Person; -import seedu.address.model.tag.Tag; - -/** - * A utility class for Person. - */ -public class PersonUtil { - - /** - * Returns an add command string for adding the {@code person}. - */ - public static String getAddCommand(Person person) { - return AddCommand.COMMAND_WORD + " " + getPersonDetails(person); - } - - /** - * Returns the part of command string for the given {@code person}'s details. - */ - public static String getPersonDetails(Person person) { - StringBuilder sb = new StringBuilder(); - sb.append(PREFIX_NAME + person.getName().fullName + " "); - sb.append(PREFIX_PHONE + person.getPhone().value + " "); - sb.append(PREFIX_EMAIL + person.getEmail().value + " "); - sb.append(PREFIX_ADDRESS + person.getAddress().value + " "); - person.getTags().stream().forEach( - s -> sb.append(PREFIX_TAG + s.tagName + " ") - ); - return sb.toString(); - } - - /** - * Returns the part of command string for the given {@code EditPersonDescriptor}'s details. - */ - public static String getEditPersonDescriptorDetails(EditPersonDescriptor descriptor) { - StringBuilder sb = new StringBuilder(); - descriptor.getName().ifPresent(name -> sb.append(PREFIX_NAME).append(name.fullName).append(" ")); - descriptor.getPhone().ifPresent(phone -> sb.append(PREFIX_PHONE).append(phone.value).append(" ")); - descriptor.getEmail().ifPresent(email -> sb.append(PREFIX_EMAIL).append(email.value).append(" ")); - descriptor.getAddress().ifPresent(address -> sb.append(PREFIX_ADDRESS).append(address.value).append(" ")); - if (descriptor.getTags().isPresent()) { - Set tags = descriptor.getTags().get(); - if (tags.isEmpty()) { - sb.append(PREFIX_TAG); - } else { - tags.forEach(s -> sb.append(PREFIX_TAG).append(s.tagName).append(" ")); - } - } - return sb.toString(); - } -} diff --git a/src/test/java/seedu/address/testutil/TestUtil.java b/src/test/java/seedu/address/testutil/TestUtil.java index 896d103eb0b..da2ceb837bd 100644 --- a/src/test/java/seedu/address/testutil/TestUtil.java +++ b/src/test/java/seedu/address/testutil/TestUtil.java @@ -7,7 +7,7 @@ import seedu.address.commons.core.index.Index; import seedu.address.model.Model; -import seedu.address.model.person.Person; +import seedu.address.model.contact.Contact; /** * A utility class for test cases. @@ -33,23 +33,23 @@ public static Path getFilePathInSandboxFolder(String fileName) { } /** - * Returns the middle index of the person in the {@code model}'s person list. + * Returns the middle index of the contact in the {@code model}'s contact list. */ public static Index getMidIndex(Model model) { - return Index.fromOneBased(model.getFilteredPersonList().size() / 2); + return Index.fromOneBased(model.getFilteredContactList().size() / 2); } /** - * Returns the last index of the person in the {@code model}'s person list. + * Returns the last index of the contact in the {@code model}'s contact list. */ public static Index getLastIndex(Model model) { - return Index.fromOneBased(model.getFilteredPersonList().size()); + return Index.fromOneBased(model.getFilteredContactList().size()); } /** - * Returns the person in the {@code model}'s person list at {@code index}. + * Returns the contact in the {@code model}'s contact list at {@code index}. */ - public static Person getPerson(Model model, Index index) { - return model.getFilteredPersonList().get(index.getZeroBased()); + public static Contact getContact(Model model, Index index) { + return model.getFilteredContactList().get(index.getZeroBased()); } } diff --git a/src/test/java/seedu/address/testutil/TypicalAddressBook.java b/src/test/java/seedu/address/testutil/TypicalAddressBook.java new file mode 100644 index 00000000000..2f176a38d7e --- /dev/null +++ b/src/test/java/seedu/address/testutil/TypicalAddressBook.java @@ -0,0 +1,26 @@ +package seedu.address.testutil; + +import static seedu.address.testutil.TypicalContacts.getTypicalContacts; +import static seedu.address.testutil.TypicalEvents.getTypicalEvents; + +import seedu.address.model.AddressBook; +import seedu.address.model.contact.Contact; +import seedu.address.model.event.Event; + +public class TypicalAddressBook { + private TypicalAddressBook() {} + + /** + * Returns an {@code AddressBook} with all the typical events and contacts. + */ + public static AddressBook getTypicalAddressBook() { + AddressBook ab = new AddressBook(); + for (Event event : getTypicalEvents()) { + ab.addEvent(event); + } + for (Contact contact : getTypicalContacts()) { + ab.addContact(contact); + } + return ab; + } +} diff --git a/src/test/java/seedu/address/testutil/TypicalContacts.java b/src/test/java/seedu/address/testutil/TypicalContacts.java new file mode 100644 index 00000000000..df082d21520 --- /dev/null +++ b/src/test/java/seedu/address/testutil/TypicalContacts.java @@ -0,0 +1,112 @@ +package seedu.address.testutil; + +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_ADDRESS_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_ADDRESS_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_EMAIL_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_EMAIL_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_NAME_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_NAME_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_PHONE_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_PHONE_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_TAG_FRIEND; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_TAG_HUSBAND; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_TELEGRAM_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_TELEGRAM_BOB; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_ZOOM_AMY; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_ZOOM_BOB; +import static seedu.address.testutil.TypicalEvents.CS2100_CONSULTATION_UUID; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import seedu.address.model.AddressBook; +import seedu.address.model.contact.Contact; + +/** + * A utility class containing a list of {@code Contact} objects to be used in tests. + */ +public class TypicalContacts { + + public static final UUID ALICE_UUID = UUID.fromString("a9dba5c9-4bce-481d-b2f5-d4820ecdd0a3"); + public static final Contact ALICE_MARKED = new ContactBuilder().withName("Alice Pauline") + .withAddress("123, Jurong West Ave 6, #08-111").withEmail("alice@example.com") + .withPhone("94351253").withTelegramHandle(null).withZoomLink(null).withUuid(ALICE_UUID) + .withLinkedEvents(CS2100_CONSULTATION_UUID).withTags("friends").withMarked(true).build(); + public static final Contact BENSON = new ContactBuilder().withName("Benson Meier") + .withAddress("311, Clementi Ave 2, #02-25").withTelegramHandle("benson67").withZoomLink(null) + .withEmail("johnd@example.com").withPhone("98765432").withMarked(false).withRandomUuid() + .withTags("owesMoney", "friends").withLinkedEvents().build(); + public static final Contact CARL = new ContactBuilder().withName("Carl Kurz").withPhone("95352563") + .withTelegramHandle("carlcarl7").withZoomLink("https://nus-sg.zoom.us/j/7453256545252") + .withEmail("heinz@example.com").withAddress("wall street").withRandomUuid().withMarked(false) + .withLinkedEvents().build(); + public static final Contact DANIEL = new ContactBuilder().withName("Daniel Meier").withPhone("87652533") + .withTelegramHandle(null).withZoomLink("nus-sg.zoom.us/j/783624625626") + .withEmail("cornelia@example.com").withAddress("10th street").withTags("friends") + .withLinkedEvents().withMarked(false).withRandomUuid().build(); + public static final Contact ELLE = new ContactBuilder().withName("Elle Meyer").withPhone("9482224") + .withTelegramHandle("ellelele").withZoomLink(null) + .withEmail("werner@example.com").withAddress(null).withTags() + .withLinkedEvents().withMarked(false).withRandomUuid().build(); + public static final Contact FIONA = new ContactBuilder().withName("Fiona Kunz").withPhone("93106433") + .withTelegramHandle(null).withZoomLink(null) + .withEmail("lydia@example.com").withAddress("little tokyo").withTags() + .withLinkedEvents().withMarked(false).withRandomUuid().build(); + public static final Contact GEORGE = new ContactBuilder().withName("George Best").withPhone(null) + .withTelegramHandle(null).withZoomLink(null).withLinkedEvents().withMarked(false).withRandomUuid() + .withEmail("george@example.com").withAddress(null).withTags().build(); + + // Manually added + public static final Contact HOON = new ContactBuilder().withName("Hoon Meier").withPhone("8482424") + .withEmail("stefan@example.com").withAddress("little india").withLinkedEvents().withMarked(false) + .withRandomUuid().build(); + public static final Contact IDA = new ContactBuilder().withName("Ida Mueller").withPhone("8482131") + .withEmail("hans@example.com").withAddress("chicago ave").withLinkedEvents().withMarked(false).withRandomUuid() + .build(); + + // Manually added + public static final Contact AMY = new ContactBuilder().withName(VALID_NAME_AMY).withPhone(VALID_PHONE_AMY) + .withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY).withTelegramHandle(VALID_TELEGRAM_AMY) + .withZoomLink(VALID_ZOOM_AMY).withTags(VALID_TAG_FRIEND).withMarked(false).withRandomUuid().withLinkedEvents() + .build(); + public static final Contact BOB = new ContactBuilder().withName(VALID_NAME_BOB).withPhone(VALID_PHONE_BOB) + .withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB).withZoomLink(VALID_ZOOM_BOB) + .withTelegramHandle(VALID_TELEGRAM_BOB).withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).withMarked(false) + .withRandomUuid().withLinkedEvents().build(); + + static { + Contact.addToMap(ALICE_MARKED); + Contact.addToMap(BENSON); + Contact.addToMap(CARL); + Contact.addToMap(DANIEL); + Contact.addToMap(ELLE); + Contact.addToMap(FIONA); + Contact.addToMap(GEORGE); + Contact.addToMap(HOON); + Contact.addToMap(IDA); + Contact.addToMap(AMY); + Contact.addToMap(BOB); + } + + private TypicalContacts() { + } // prevents instantiation + + public static List getTypicalContacts() { + return new ArrayList<>(Arrays.asList(ALICE_MARKED, BENSON, CARL, DANIEL, ELLE, FIONA, GEORGE)); + } + + /** + * Returns an {@code AddressBook} with all the typical contacts. + */ + public static AddressBook getTypicalAddressBook() { + AddressBook ab = new AddressBook(); + for (Contact contact : getTypicalContacts()) { + // build a new contact so that everytime this method is called, it returns new contacts + ab.addContact(new ContactBuilder(contact).build()); + } + return ab; + } + +} diff --git a/src/test/java/seedu/address/testutil/TypicalEvents.java b/src/test/java/seedu/address/testutil/TypicalEvents.java new file mode 100644 index 00000000000..8e3c2c4ead8 --- /dev/null +++ b/src/test/java/seedu/address/testutil/TypicalEvents.java @@ -0,0 +1,94 @@ +package seedu.address.testutil; + +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_ADDRESS_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_ADDRESS_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_DESCRIPTION_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_DESCRIPTION_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_END_DATE_TIME_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_END_DATE_TIME_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_NAME_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_NAME_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_START_DATE_TIME_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_START_DATE_TIME_TUTORIAL; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_TAG_COOL; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_TAG_EXAMS; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_ZOOM_EXAM; +import static seedu.address.logic.commands.general.CommandTestUtil.VALID_ZOOM_TUTORIAL; +import static seedu.address.testutil.TypicalContacts.ALICE_UUID; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import seedu.address.model.event.Event; + +public class TypicalEvents { + + public static final UUID CS2103_MIDTERM_MARKED_UUID = UUID.fromString("17e996a5-1f13-45fe-b9d2-f9517dff067f"); + public static final Event CS2103_MIDTERM_MARKED = new EventBuilder().withName("CS2103 Midterms") + .withAddress("Zoom").withZoomLink("nus-sg.edu/123%a").withDescription("I'm very unprepared") + .withStartDateAndTime("20-10-2021 09:00").withEndDateAndTime("20-10-2021 11:00") + .withTags("exams").withMarked(true).build(); + + public static final UUID CS2100_CONSULTATION_UUID = UUID.fromString("123e4567-e89b-12d3-a456-556642440002"); + public static final Event CS2100_CONSULTATION = new EventBuilder().withName("CS2100 Consultation") + .withAddress("02-11 COM2").withDescription("Topics: Assembly Language") + .withStartDateAndTime("17-10-2021 15:00").withEndDateAndTime("17-10-2021 16:00").withTags("School") + .withUuid(CS2100_CONSULTATION_UUID).withLinkedContacts(ALICE_UUID) + .withMarked(false).build(); + + public static final Event CS2101_MEETING = new EventBuilder().withName("CS2101 Meeting") + .withAddress("Zoom").withZoomLink("nus-sg.edu/pwdffha%a").withDescription("Need to prepare for OP2") + .withStartDateAndTime("15-10-2021 21:00").withEndDateAndTime("15-10-2021 22:00") + .withTags("meeting").withMarked(false).withRandomUuid().build(); + + public static final Event FOOTBALL_PRACTICE = new EventBuilder().withName("Football Practice") + .withAddress("USC").withStartDateAndTime("30-10-2021 09:00").withEndDateAndTime("30-10-2021 11:00") + .withMarked(false).withRandomUuid().withTags().build(); + + public static final Event TEAM_MEETING = new EventBuilder().withName("Team Meeting") + .withAddress("Zoom").withZoomLink("nus-edu.sg/123link") + .withStartDateAndTime("20-10-2021 19:00").withEndDateAndTime("20-10-2021 20:00") + .withTags("meeting").withMarked(false).withRandomUuid().build(); + + public static final Event BIRTHDAY_PARTY = new EventBuilder().withName("Birthday Party") + .withAddress("#10-07 Baker Street").withDescription("Dress code: Pink!") + .withStartDateAndTime("23-10-2021 20:00").withEndDateAndTime("24-10-2021 01:00") + .withTags("friends").withMarked(false).withRandomUuid().build(); + + // Manually added + public static final Event INTERVIEW = new EventBuilder().withName("Interview") + .withAddress("Zoom").withZoomLink("123kadsf-link-23.zoom.nus.edu") + .withStartDateAndTime("28-10-2021 10:00").withEndDateAndTime("28-10-2021 11:00").withTags("interview") + .build(); + public static final Event TUTORIAL = new EventBuilder().withName(VALID_NAME_TUTORIAL) + .withStartDateAndTime(VALID_START_DATE_TIME_TUTORIAL).withEndDateAndTime(VALID_END_DATE_TIME_TUTORIAL) + .withDescription(VALID_DESCRIPTION_TUTORIAL).withAddress(VALID_ADDRESS_TUTORIAL) + .withZoomLink(VALID_ZOOM_TUTORIAL).withTags(VALID_TAG_COOL).build(); + public static final Event EXAM = new EventBuilder().withName(VALID_NAME_EXAM) + .withStartDateAndTime(VALID_START_DATE_TIME_EXAM).withEndDateAndTime(VALID_END_DATE_TIME_EXAM) + .withDescription(VALID_DESCRIPTION_EXAM).withAddress(VALID_ADDRESS_EXAM) + .withZoomLink(VALID_ZOOM_EXAM).withTags(VALID_TAG_EXAMS, VALID_TAG_COOL).build(); + + static { + Event.addToMap(CS2103_MIDTERM_MARKED); + Event.addToMap(CS2100_CONSULTATION); + Event.addToMap(CS2101_MEETING); + Event.addToMap(FOOTBALL_PRACTICE); + Event.addToMap(TEAM_MEETING); + Event.addToMap(BIRTHDAY_PARTY); + Event.addToMap(TUTORIAL); + Event.addToMap(EXAM); + } + + private TypicalEvents() { + } + + public static List getTypicalEvents() { + return new ArrayList<>( + Arrays.asList(CS2103_MIDTERM_MARKED, CS2100_CONSULTATION, CS2101_MEETING, FOOTBALL_PRACTICE, + TEAM_MEETING, BIRTHDAY_PARTY)); + } + +} diff --git a/src/test/java/seedu/address/testutil/TypicalIndexes.java b/src/test/java/seedu/address/testutil/TypicalIndexes.java index 1e613937657..ce9eed33acc 100644 --- a/src/test/java/seedu/address/testutil/TypicalIndexes.java +++ b/src/test/java/seedu/address/testutil/TypicalIndexes.java @@ -6,7 +6,8 @@ * A utility class containing a list of {@code Index} objects to be used in tests. */ public class TypicalIndexes { - public static final Index INDEX_FIRST_PERSON = Index.fromOneBased(1); - public static final Index INDEX_SECOND_PERSON = Index.fromOneBased(2); - public static final Index INDEX_THIRD_PERSON = Index.fromOneBased(3); + public static final Index INDEX_FIRST = Index.fromOneBased(1); + public static final Index INDEX_SECOND = Index.fromOneBased(2); + public static final Index INDEX_THIRD = Index.fromOneBased(3); + public static final Index INDEX_FOURTH = Index.fromOneBased(4); } diff --git a/src/test/java/seedu/address/testutil/TypicalPersons.java b/src/test/java/seedu/address/testutil/TypicalPersons.java deleted file mode 100644 index fec76fb7129..00000000000 --- a/src/test/java/seedu/address/testutil/TypicalPersons.java +++ /dev/null @@ -1,76 +0,0 @@ -package seedu.address.testutil; - -import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_AMY; -import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_AMY; -import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_AMY; -import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_AMY; -import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_FRIEND; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import seedu.address.model.AddressBook; -import seedu.address.model.person.Person; - -/** - * A utility class containing a list of {@code Person} objects to be used in tests. - */ -public class TypicalPersons { - - public static final Person ALICE = new PersonBuilder().withName("Alice Pauline") - .withAddress("123, Jurong West Ave 6, #08-111").withEmail("alice@example.com") - .withPhone("94351253") - .withTags("friends").build(); - public static final Person BENSON = new PersonBuilder().withName("Benson Meier") - .withAddress("311, Clementi Ave 2, #02-25") - .withEmail("johnd@example.com").withPhone("98765432") - .withTags("owesMoney", "friends").build(); - public static final Person CARL = new PersonBuilder().withName("Carl Kurz").withPhone("95352563") - .withEmail("heinz@example.com").withAddress("wall street").build(); - public static final Person DANIEL = new PersonBuilder().withName("Daniel Meier").withPhone("87652533") - .withEmail("cornelia@example.com").withAddress("10th street").withTags("friends").build(); - public static final Person ELLE = new PersonBuilder().withName("Elle Meyer").withPhone("9482224") - .withEmail("werner@example.com").withAddress("michegan ave").build(); - public static final Person FIONA = new PersonBuilder().withName("Fiona Kunz").withPhone("9482427") - .withEmail("lydia@example.com").withAddress("little tokyo").build(); - public static final Person GEORGE = new PersonBuilder().withName("George Best").withPhone("9482442") - .withEmail("anna@example.com").withAddress("4th street").build(); - - // Manually added - public static final Person HOON = new PersonBuilder().withName("Hoon Meier").withPhone("8482424") - .withEmail("stefan@example.com").withAddress("little india").build(); - public static final Person IDA = new PersonBuilder().withName("Ida Mueller").withPhone("8482131") - .withEmail("hans@example.com").withAddress("chicago ave").build(); - - // Manually added - Person's details found in {@code CommandTestUtil} - public static final Person AMY = new PersonBuilder().withName(VALID_NAME_AMY).withPhone(VALID_PHONE_AMY) - .withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY).withTags(VALID_TAG_FRIEND).build(); - public static final Person BOB = new PersonBuilder().withName(VALID_NAME_BOB).withPhone(VALID_PHONE_BOB) - .withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND) - .build(); - - public static final String KEYWORD_MATCHING_MEIER = "Meier"; // A keyword that matches MEIER - - private TypicalPersons() {} // prevents instantiation - - /** - * Returns an {@code AddressBook} with all the typical persons. - */ - public static AddressBook getTypicalAddressBook() { - AddressBook ab = new AddressBook(); - for (Person person : getTypicalPersons()) { - ab.addPerson(person); - } - return ab; - } - - public static List getTypicalPersons() { - return new ArrayList<>(Arrays.asList(ALICE, BENSON, CARL, DANIEL, ELLE, FIONA, GEORGE)); - } -} diff --git a/src/test/java/seedu/address/testutil/TypicalRanges.java b/src/test/java/seedu/address/testutil/TypicalRanges.java new file mode 100644 index 00000000000..007c9b4cc17 --- /dev/null +++ b/src/test/java/seedu/address/testutil/TypicalRanges.java @@ -0,0 +1,17 @@ +package seedu.address.testutil; + +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND; +import static seedu.address.testutil.TypicalIndexes.INDEX_THIRD; + +import seedu.address.commons.core.range.Range; + +/** + * A utility class containing a list of {@code Range} objects to be used in tests. + */ +public class TypicalRanges { + public static final Range RANGE_FIRST_TO_SECOND = new Range(INDEX_FIRST, INDEX_SECOND); + public static final Range RANGE_SECOND_TO_THIRD = new Range(INDEX_SECOND, INDEX_THIRD); + public static final Range RANGE_FIRST_TO_THIRD = new Range(INDEX_FIRST, INDEX_THIRD); + public static final Range RANGE_FIRST_TO_FIRST = Range.convertFromIndex(INDEX_FIRST); +}