diff --git a/.gitignore b/.gitignore index 284c4ca7cd9..5e8ad61eb9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,12 @@ # Gradle build files /.gradle/ /build/ +/bin/ src/main/resources/docs/ +# VSCode files +/.vscode/ + # IDEA files /.idea/ /out/ diff --git a/README.md b/README.md index 16208adb9b6..49b74942d83 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,23 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) +# NUSMates + +[![CI Status](https://github.com/AY2425S2-CS2103T-T11-1/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2425S2-CS2103T-T11-1/tp/actions) + +[![codecov](https://codecov.io/gh/AY2425S2-CS2103T-T11-1/tp/graph/badge.svg?token=VA2F7WUH2X)](https://codecov.io/gh/AY2425S2-CS2103T-T11-1/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. +**NUSMates allows NUS undergraduate students to record the contact details of their fellow NUS undergraduate students.** With NUSMates, you can record NUS-specific contact information such as [year](#year), [major](#major), housing, and [modules](#module). +NUSMates also makes it seamless to record [module](#module) information using an [NUSMods link](#nusmods-link), helping you easily find friends who are taking the same [modules](#module) - so you can form project groups, share notes, or know who to reach out to for help. + +This is our team project for CS2103T; it is based on the AddressBook-Level3 (AB3) project created by the [SE-EDU initiative](https://se-education.org). In addition to the functionalities provided in AB3, we introduce features that can help NUS students better manage their contacts' information. + +## Project 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/#contributing-to-se-edu) for more info. + +## Selected features +* **NUS-Specific Information:** users can add NUS-specific contact information such as year, major, housing, and modules +* **Integration with [NUSMods](https://nusmods.com):** this allows users to seamlessly add a contact's timetable to their info using a NUSMods link. +* **Module Search:** we allow users to find coursemates for any of their courses making it easier to find teammates in advance. + +For the detailed documentation of this project, see the **[NUSMates Product Website](https://se-education.org/addressbook-level3)**. diff --git a/build.gradle b/build.gradle index 0db3743584e..fbc6b885efe 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,11 @@ dependencies { } shadowJar { - archiveFileName = 'addressbook.jar' + archiveFileName = 'nusmates.jar' +} + +run { + enableAssertions = true } defaultTasks 'clean', 'test' diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000000..e5969408ffe --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,10 @@ +# .gitignore for Jekyll. +# source: https://github.com/github/gitignore/blob/main/Jekyll.gitignore + +_site/ +.sass-cache/ +.jekyll-cache/ +.jekyll-metadata +# Ignore folders generated by Bundler +.bundle/ +vendor/ diff --git a/docs/AboutUs.md b/docs/AboutUs.md index ff3f04abd02..892ececdf48 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,59 +1,50 @@ --- layout: page +nav_order: 4 title: About Us --- -We are a team based in the [School of Computing, National University of Singapore](https://www.comp.nus.edu.sg). +# About Us -You can reach us at the email `seer[at]comp.nus.edu.sg` +We are CS2103T-T11 Team 1, a team based in the [School of Computing, National University of Singapore](https://www.comp.nus.edu.sg). ## Project team -### John Doe +### Abi Halim - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/AbiHalim)] -* Role: Project Advisor - -### Jane Doe - - +* Role: Team Lead +* In charge of Documentation -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +### Vee Hua Zhi -* Role: Team Lead -* Responsibilities: UI + -### Johnny Doe +* Role: Code Quality +* In charge of ensuring Code Quality - +### Shashwat Chandra -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] + -* Role: Developer -* Responsibilities: Data +[[github](http://github.com/shashwatchan)] +[[website](http://shashwatchandra.com)] -### Jean Doe +* Role: PM +* Responsibilities: Testing - -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +### Song Yuexi -* Role: Developer -* Responsibilities: Dev Ops + Threading + -### James Doe +[[github](https://github.com/YosieSYX)] +[[portfolio](https://yosiesyx.github.io/SongYuexi/)] - +* Role: Testing, Scheduling and tracking +* Responsibilities: In charge of defining, assigning, and tracking project tasks, Ensures the testing of the project is done properly and on time. -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] -* Role: Developer -* Responsibilities: UI diff --git a/docs/Configuration.md b/docs/Configuration.md index 13cf0faea16..9996ca1f02c 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -1,5 +1,6 @@ --- layout: page +nav_exclude: true title: Configuration guide --- diff --git a/docs/DevOps.md b/docs/DevOps.md index d2fd91a6001..09152dc59c8 100644 --- a/docs/DevOps.md +++ b/docs/DevOps.md @@ -1,5 +1,6 @@ --- layout: page +nav_exclude: true title: DevOps guide --- @@ -73,7 +74,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/{{site.repository}}/tree/master/src/main/java/seedu/address/MainApp.java). 1. Generate a fat JAR file using Gradle (i.e., `gradlew shadowJar`). 1. Tag the repo with the version number. e.g. `v0.1` 1. [Create a new release using GitHub](https://help.github.com/articles/creating-releases/). Upload the JAR file you created. diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 743c65a49d2..f8df0c94004 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -1,7 +1,15 @@ --- layout: page +nav_order: 3 title: Developer Guide --- + +# Developer Guide +{: .no_toc} + +## Table of Contents +{: .no_toc .text-delta } + * Table of Contents {:toc} @@ -9,7 +17,12 @@ title: Developer Guide ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +* It is based on the [AddressBook-Level3 (AB3) project](https://github.com/se-edu/addressbook-level3) created by the [SE-EDU initiative](https://se-education.org). +In addition to the functionalities provided in AB3, we introduce features that can help NUS students better manage their contacts' information. +* GitHub Copilot Inline Editor was used as an autocomplete to complete repititive and tedious code. +* GitHub Copilot Inline Editor was used to generate some testcases. +* GitHub Copilot was used to generate some commit messages. +* ChatGPT was used to format and correct the wordings and format of User Guide and Developer Guide, including the glossary. -------------------------------------------------------------------------------------------------------------------- @@ -21,10 +34,8 @@ Refer to the guide [_Setting up and getting started_](SettingUp.md). ## **Design** -
- -:bulb: **Tip:** The `.puml` files used to create diagrams in this document `docs/diagrams` folder. Refer to the [_PlantUML Tutorial_ at se-edu/guides](https://se-education.org/guides/tutorials/plantUml.html) to learn how to create and edit diagrams. -
+{: .tip } +> The `.puml` files used to create diagrams in this document `docs/diagrams` folder. Refer to the [_PlantUML Tutorial_ at se-edu/guides](https://se-education.org/guides/tutorials/plantUml.html) to learn how to create and edit diagrams. ### Architecture @@ -36,7 +47,7 @@ Given below is a quick overview of main components and how they interact with ea **Main components of the architecture** -**`Main`** (consisting of classes [`Main`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/MainApp.java)) is in charge of the app launch and shut down. +**`Main`** (consisting of classes [`Main`](https://github.com/{{site.repository}}/tree/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/{{site.repository}}/tree/master/src/main/java/seedu/address/MainApp.java)) is in charge of the app launch and shut down. * At app launch, it initializes the other components in the correct sequence, and connects them up with each other. * At shut down, it shuts down the other components and invokes cleanup methods where necessary. @@ -68,13 +79,13 @@ 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/{{site.repository}}/tree/master/src/main/java/seedu/address/ui/Ui.java) ![Structure of the UI Component](images/UiClassDiagram.png) The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. -The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/resources/view/MainWindow.fxml) +The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/{{site.repository}}/tree/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/{{site.repository}}/tree/master/src/main/resources/view/MainWindow.fxml) The `UI` component, @@ -85,24 +96,24 @@ The `UI` component, ### Logic component -**API** : [`Logic.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/logic/Logic.java) +**API** : [`Logic.java`](https://github.com/{{site.repository}}/tree/master/src/main/java/seedu/address/logic/Logic.java) Here's a (partial) class diagram of the `Logic` component: -The sequence diagram below illustrates the interactions within the `Logic` component, taking `execute("delete 1")` API call as an example. +The sequence diagram below illustrates the interactions within the `Logic` component, taking `execute("edit 2 y/2")` API call as an example. -![Interactions Inside the Logic Component for the `delete 1` Command](images/DeleteSequenceDiagram.png) +![Interactions Inside the Logic Component for the `edit 2 y/2` Command](images/EditSequenceDiagram.png) -
:information_source: **Note:** The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline continues till the end of diagram. -
+{: .note } +> The lifeline for `EditCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline continues till the end of diagram. How the `Logic` component works: -1. When `Logic` is called upon to execute a command, it is passed to an `AddressBookParser` object which in turn creates a parser that matches the command (e.g., `DeleteCommandParser`) and uses it to parse the command. -1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `DeleteCommand`) which is executed by the `LogicManager`. -1. The command can communicate with the `Model` when it is executed (e.g. to delete a person).
+1. When `Logic` is called upon to execute a command, it is passed to an `AddressBookParser` object which in turn creates a parser that matches the command (e.g., `EditCommandParser`) and uses it to parse the command. +1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `EditCommand`) which is executed by the `LogicManager`. +1. The command can communicate with the `Model` when it is executed (e.g. to edit a person).
Note that although this is shown as a single step in the diagram above (for simplicity), in the code it can take several interactions (between the command object and the `Model`) to achieve. 1. The result of the command execution is encapsulated as a `CommandResult` object which is returned back from `Logic`. @@ -115,7 +126,7 @@ How the parsing works: * All `XYZCommandParser` classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. ### Model component -**API** : [`Model.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/model/Model.java) +**API** : [`Model.java`](https://github.com/{{site.repository}}/tree/master/src/main/java/seedu/address/model/Model.java) @@ -127,16 +138,14 @@ The `Model` component, * stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects. * does not depend on any of the other three components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components) -
:information_source: **Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
- - - -
+{: .note } +> An alternative (arguably, a more OOP) model is given below. 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.
+> ### Storage component -**API** : [`Storage.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/storage/Storage.java) +**API** : [`Storage.java`](https://github.com/{{site.repository}}/tree/master/src/main/java/seedu/address/storage/Storage.java) @@ -177,30 +186,31 @@ Step 2. The user executes `delete 5` command to delete the 5th person in the add ![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 `add n/David …​` to add a new person. This activity diagram summarizes the process of adding a new person. -![UndoRedoState2](images/UndoRedoState2.png) +![AddPersonActivityDiagram](images%2FAddPersonActivityDiagram.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`. +The `add` command also calls `Model#commitAddressBook()`, causing another modified address book state to be saved into the `addressBookStateList`. -
+![UndoRedoState2](images/UndoRedoState2.png) + +{: .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`. Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoAddressBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous address book state, and restores the address book to that state. ![UndoRedoState3](images/UndoRedoState3.png) -
: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 +{: .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. -
- The following sequence diagram shows how an undo operation goes through the `Logic` component: ![UndoSequenceDiagram](images/UndoSequenceDiagram-Logic.png) -
:information_source: **Note:** The lifeline for `UndoCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. - -
+{: .note } +> The lifeline for `UndoCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. Similarly, how an undo operation goes through the `Model` component is shown below: @@ -208,9 +218,8 @@ Similarly, how an undo operation goes through the `Model` component is shown bel The `redo` command does the opposite — it calls `Model#redoAddressBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the address book to that state. -
:information_source: **Note:** If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone AddressBook states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. - -
+{: .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. 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. @@ -237,12 +246,6 @@ The following activity diagram summarizes what happens when a user executes a ne * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). * Cons: We must ensure that the implementation of each individual command are correct. -_{more aspects and alternatives to be added}_ - -### \[Proposed\] Data archiving - -_{Explain here how the data archiving feature will be implemented}_ - -------------------------------------------------------------------------------------------------------------------- @@ -262,71 +265,180 @@ _{Explain here how the data archiving feature will be implemented}_ **Target user profile**: -* 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 +NUS undergraduate students who +* frequently use NUSMods to plan, record, and share their course schedules +* are socially active in NUS with a need to manage the contacts of their fellow NUS undergraduate students +* are tech-savvy and familiar with installing jar files +* can type fast and prefer using CLI apps over mouse interactions -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app +**Value proposition**: +NUSMates allows NUS undergraduate students to record the contact details of their fellow NUS undergraduate students with NUS-specific contact information such as year, major, housing, and modules. +Tailored towards frequent NUSMods users, the app makes it seamless to record module information using NUSMods links, helping users easily find friends who are taking the same modules - so they can form project groups, share notes or know who to reach out to for help. ### User stories Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` -| Priority | As a …​ | I want to …​ | So that I can…​ | -| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | -| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App | -| `* * *` | user | add a new person | | -| `* * *` | user | delete a person | remove entries that I no longer need | -| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list | -| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident | -| `*` | user with many persons in the address book | sort persons by name | locate a person easily | +| 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 | record a person's year of study | | +| `* * *` | user | record a person's NUS major | | +| `* * *` | user | record a person's housing | | +| `* * *` | user | record a person's Singaporean phone number | | +| `* * *` | user | record a person's email | | +| `* * *` | user | record the link of a person's NUSMods schedule | record the modules and timetables of contacts | +| `* * *` | user | view a person's modules | | +| `* * *` | user | delete a person | remove contacts that I no longer need | +| `* * *` | user | edit a contact’s details | update outdated or incorrect information | +| `* * *` | user | find contacts by name | locate details of contacts without having to go through the entire list | +| `* * *` | user | find contacts by module | find friends to take modules with | +| `* * *` | user | view a list of all my contacts | quickly find and access their details | +| `* * *` | user | copy the link to my contact's NUSMods schedule | easily paste it into my browser to open their schedule on the NUSMods website | +| `* * *` | user | clear all contacts at once | reset my address book when needed | +| `* * *` | user | exit the application using a command | close it quickly when I am done using it | +| `* *` | user | have my contacts saved automatically | make sure my data is not lost when I close the application | +| `* *` | user | interact with a graphical interface while using command-line inputs | visually confirm my actions and navigate the application more easily | +| `* *` | user | back up my data | make sure my data won't get lost | +| `* *` | user | import contacts from a backup file | | +| `*` | advanced user | edit the data file directly | modify my contact list without using the application interface | + + -*{More to be added}* ### 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 `AddressBook` and the **Actor** is the `user`, unless specified otherwise. All use cases also require the precondition that the app already be open, along with any additional use cases specified. -**Use case: Delete a person** +**Use Case: UC01 -- Add Contacts** **MSS** -1. User requests to list persons -2. AddressBook shows a list of persons -3. User requests to delete a specific person in the list -4. AddressBook deletes the person +1. User provides contact details to add. +2. System adds new contact. +3. System shows new contact. + + Use Case Ends. + +**Extensions:** + +* 1a. System detects an error in the entered data. + + * 1a1. System requests for correct data. + * 1a2. User provides new data. + + Steps 1a1-1a2 are repeated until the data entered are correct. + + Use case resumes from step 2. + +**Use case: UC02 -- Delete a Contact** + +**Preconditions:** +* A non-empty list of persons is currently displayed to the user + +**MSS** + +1. User requests to delete a specific contact in the list. +2. AddressBook deletes the person. Use case ends. **Extensions** -* 2a. The list is empty. +* 1a. The given index is invalid. + + * 1a1. System shows an error message and requests for a correct index. + * 1a2. User provides a new index. + + Steps 1a1-1a2 are repeated until the index entered is correct. + + Use case resumes at step 2. + + +**Use Case: UC03 -- Link a contact to NUSMods schedule** + +**Preconditions** +* The contact to be linked exists, or the user is in the process of creating a new valid contact. + + +**MSS** +1. User provides a contact and a NUSMods timetable link during creation or editing of the contact. +2. System links the provided timetable to the contact, replacing any previously linked timetable if present. +3. System displays the updated contact with the updated modules from the timetable. + + Use Case Ends. + +**Extensions** +* 2a. Provided timetable link is invalid. + * 2a1. System informs the user that the link is invalid and requests a new link. + * 2a2. User provides a new link. + + Steps 2a1-2a2 are repeated until the link entered is correct. + + Use case resumes from step 2. + +**Use Case: UC04 -- Find Contacts Taking a Specific Module** + +**Actor:** NUS Undergraduate Student + +**MSS** + +1. User enters the `findMod` command along with a module code. +2. The system searches for contacts who are taking the specified module. +3. The system lists matching contacts. + + Use Case Ends. + +**Extensions** + +* 1a. User provides an invalid module code. + + * 1a1. System requests for correct data. + * 1a2. User provides new data. + + Steps 1a1-1a2 are repeated until the data entered are valid. + + Use case resumes from step 2. - Use case ends. +**Use Case: UC05 -- Find Contacts By Name** -* 3a. The given index is invalid. +**Actor:** NUS Undergraduate Student - * 3a1. AddressBook shows an error message. +**MSS** + +1. User enters the `find` command along with keywords. +2. The system searches for contacts whose names contain at least one of the given keywords. +3. The system lists matching contacts. + + Use Case Ends. + +**Extensions** + +* 1a. No matching contacts. + + Use Case Ends. - Use case resumes at step 2. -*{More to be added}* ### Non-Functional Requirements -1. Should work on any _mainstream OS_ as long as it has Java `17` or above installed. -2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. +1. Should work on any [_mainstream OS_](#glossary) as long as it has Java `17` or above installed. +2. Should be able to hold up to 1000 users 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. Searches should return results within 1 second for a dataset of 1000 contacts. ### Glossary * **Mainstream OS**: Windows, Linux, Unix, MacOS +* [**NUSMods**](https://nusmods.com/): A website used by NUS students to view and plan their module timetables +* **Contact**: A person’s details that are stored in the address book * **Private contact detail**: A contact detail that is not meant to be shared with others +* [**CLI (Command Line Interface)**](https://en.wikipedia.org/wiki/Command-line_interface): A means of interacting with a computer program by inputting lines of text called command lines +* [**GUI (Graphical User Interface)**](https://en.wikipedia.org/wiki/Graphical_user_interface): A means of interacting with a computer program using a graphical interface, such as windows, icons, and buttons +* **Tag**: A keyword or label associated with a contact +* [**Module**](https://www.nus.edu.sg/registrar/academic-information-policies/graduate/modular-system): A subject or course that NUS students take as part of their degree programme. Each module has a unique code (e.g., CS2103T) and typically includes lectures, tutorials, and/or labs. -------------------------------------------------------------------------------------------------------------------- @@ -334,18 +446,18 @@ 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; +{: .note } +> These instructions only provide a starting point for testers to work on; testers are expected to do more *exploratory* testing. -
- ### Launch and shutdown 1. Initial launch 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. Open a terminal and `cd` into the folder you put the jar file. + 3. Use the command `java -jar "nusmates.jar"` to run the application.
Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. 1. Saving window preferences @@ -354,8 +466,6 @@ 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 …​ }_ - ### Deleting a person 1. Deleting a person while all persons are being shown @@ -363,20 +473,61 @@ testers are expected to do more *exploratory* testing. 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. 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. + Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. + Expected: No person is deleted. Error details shown in the status message. 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
Expected: Similar to previous. -1. _{ more test cases …​ }_ +### Editing a Person + +1. Editing a person while all persons are being shown + + 1. Prerequisites: List all persons using the `list` command. Multiple persons listed. + No one is named "John Doe". + + 1. Test case: `edit 1 n/John Doe`
+ Expected: First contact is updated with the new name. Name of the updated contact shown in the status message. + + 1. Test case: `edit 1`
+ Expected: No person is edited. Error details shown in the status message. + + 1. Other incorrect delete commands to try: `edit 0`, `edit 0 n/Joe` `edit x`, `...` (where x is larger than the list size)
+ Expected: Similar to previous. + +1. Editing the link of a person while all persons are being shown + + 1. Prerequisites: List all persons using the `list` command. Multiple persons listed. + + 1. Test case: `edit 1 l/https://nusmods.com/timetable/sem-2/share?CS1010S=LEC:1,TUT:1&CS2030=LEC:1,LAB:1`
+ Expected: First contact is updated with the new link. Updated details of the contact shown in the status message. + Clicking on `NUSMods Schedule` of the first person will result in copying the link which can be pasted in the browser. + + 1. Test case: `edit 1 l/google.com`
+ Expected: No person is edited. Error details shown in the status message. ### Saving data 1. Dealing with missing/corrupted data files - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ + 1. Test case: Editing the JSON file so that it is still valid (e.g. changing `"year" : "2",` to `"year" : "3",`) + Expected: The app should not crash. It should load the address book with the new data. + + 1. Test case: Editing the JSON file to make it invalid (e.g. removing a comma) + Expected: The app should not crash. It should create a new empty address book and delete the corrupted file. It should show an error message in the console. + + 1. Test case: Deleting the JSON file + Expected: The app should not crash. It should create a new address book with sample data. It should create the JSON file again upon any valid command. + +## **Appendix: Planned Enhancements** + +Team size: 4 -1. _{ more test cases …​ }_ +1. **Optional fields should be able to be cleared**: Currently optional fields cannot be cleared once set. Optional fields should be able to be cleared using the `edit` command, just like tags. For example: Phone should be able to be cleared by doing `edit 1 p/` (with nothing after the space). +2. **Fix issue of tags overflowing out of the UI**: Currently, extremely long tags which overflow out of the UI get abruptly cut off, such as in the screenshot below. This off-screen overflow should be handles more elegantly, such as truncating the tag with a `...` before cutting it within the UI boundary. +![TagUiOverflow.png](images%2FTagUiOverflow.png) +3. **Allow names with `/`**: Currently, names cannot include the `/` character, even though there are valid names which include this character. In the future, this should be allowed to make NUSMates more inclusive. +4. **Long error messages**: Currently, the error messages returned when entering an invlaid command are quite long and hard to read. In the future, this should be fixed by having line breaks or making the messages shorter to improve readbility. +5. **More Robust Link Validation**: Currently, our link validation for the NUSMods timetable works in the following manner: verify that the link is indeed from ```nusmods.com```, then parse the module codes from it ensuring that they satisfy some basic constraints (2-4 alphabet prefix, then 4 digits, and finally 0-5 alphanumeric characters). In the future, we plan to make this validation even more robust by verifying that the parsed codes are actually modules that exist (for example) by maintaining an offline list of valid codes. diff --git a/docs/Documentation.md b/docs/Documentation.md index 3e68ea364e7..dcd04719f69 100644 --- a/docs/Documentation.md +++ b/docs/Documentation.md @@ -1,5 +1,6 @@ --- layout: page +nav_exclude: true title: Documentation guide --- diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 5cce9e31787..0424309e078 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -1,22 +1,38 @@ GEM remote: https://rubygems.org/ specs: - activesupport (7.0.7.2) - concurrent-ruby (~> 1.0, >= 1.0.2) + activesupport (8.0.2) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) + logger (>= 1.4.2) minitest (>= 5.1) - tzinfo (~> 2.0) - addressable (2.8.4) - public_suffix (>= 2.0.2, < 6.0) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + base64 (0.2.0) + benchmark (0.4.0) + bigdecimal (3.1.9) coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.11.1) + coffee-script-source (1.12.2) colorator (1.1.0) - commonmarker (0.23.10) - concurrent-ruby (1.2.2) - dnsruby (1.70.0) + commonmarker (0.23.11) + concurrent-ruby (1.3.5) + connection_pool (2.5.0) + csv (3.3.3) + dnsruby (1.72.4) + base64 (~> 0.2.0) + logger (~> 1.6.5) simpleidn (~> 0.2.1) + drb (2.2.1) em-websocket (0.5.3) eventmachine (>= 0.12.9) http_parser.rb (~> 0) @@ -24,25 +40,26 @@ GEM ffi (>= 1.15.0) eventmachine (1.2.7) eventmachine (1.2.7-x64-mingw32) - execjs (2.8.1) - faraday (2.7.5) - faraday-net_http (>= 2.0, < 3.1) - ruby2_keywords (>= 0.0.4) - faraday-net_http (3.0.2) - ffi (1.15.5) - ffi (1.15.5-x64-mingw32) + execjs (2.10.0) + faraday (2.12.2) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-net_http (3.4.0) + net-http (>= 0.5.0) + ffi (1.17.1) forwardable-extended (2.6.0) - gemoji (3.0.1) - github-pages (228) - github-pages-health-check (= 1.17.9) - jekyll (= 3.9.3) - jekyll-avatar (= 0.7.0) - jekyll-coffeescript (= 1.1.1) - jekyll-commonmark-ghpages (= 0.4.0) - jekyll-default-layout (= 0.1.4) - jekyll-feed (= 0.15.1) + gemoji (4.1.0) + github-pages (232) + github-pages-health-check (= 1.18.2) + jekyll (= 3.10.0) + jekyll-avatar (= 0.8.0) + jekyll-coffeescript (= 1.2.2) + jekyll-commonmark-ghpages (= 0.5.1) + jekyll-default-layout (= 0.1.5) + jekyll-feed (= 0.17.0) jekyll-gist (= 1.5.0) - jekyll-github-metadata (= 2.13.0) + jekyll-github-metadata (= 2.16.1) jekyll-include-cache (= 0.2.1) jekyll-mentions (= 1.6.0) jekyll-optional-front-matter (= 0.3.2) @@ -69,30 +86,32 @@ GEM jekyll-theme-tactile (= 0.2.0) jekyll-theme-time-machine (= 0.2.0) jekyll-titles-from-headings (= 0.5.3) - jemoji (= 0.12.0) - kramdown (= 2.3.2) + jemoji (= 0.13.0) + kramdown (= 2.4.0) kramdown-parser-gfm (= 1.1.0) liquid (= 4.0.4) mercenary (~> 0.3) minima (= 2.5.1) - nokogiri (>= 1.13.6, < 2.0) - rouge (= 3.26.0) + nokogiri (>= 1.16.2, < 2.0) + rouge (= 3.30.0) terminal-table (~> 1.4) - github-pages-health-check (1.17.9) + webrick (~> 1.8) + github-pages-health-check (1.18.2) addressable (~> 2.3) dnsruby (~> 1.60) - octokit (~> 4.0) - public_suffix (>= 3.0, < 5.0) + octokit (>= 4, < 8) + public_suffix (>= 3.0, < 6.0) typhoeus (~> 1.3) html-pipeline (2.14.3) activesupport (>= 2) nokogiri (>= 1.4) http_parser.rb (0.8.0) - i18n (1.14.1) + i18n (1.14.7) concurrent-ruby (~> 1.0) - jekyll (3.9.3) + jekyll (3.10.0) addressable (~> 2.4) colorator (~> 1.0) + csv (~> 3.0) em-websocket (~> 0.5) i18n (>= 0.7, < 2) jekyll-sass-converter (~> 1.0) @@ -103,27 +122,28 @@ GEM pathutil (~> 0.9) rouge (>= 1.7, < 4) safe_yaml (~> 1.0) - jekyll-avatar (0.7.0) + webrick (>= 1.0) + jekyll-avatar (0.8.0) jekyll (>= 3.0, < 5.0) - jekyll-coffeescript (1.1.1) + jekyll-coffeescript (1.2.2) coffee-script (~> 2.2) - coffee-script-source (~> 1.11.1) + coffee-script-source (~> 1.12) jekyll-commonmark (1.4.0) commonmarker (~> 0.22) - jekyll-commonmark-ghpages (0.4.0) - commonmarker (~> 0.23.7) - jekyll (~> 3.9.0) + jekyll-commonmark-ghpages (0.5.1) + commonmarker (>= 0.23.7, < 1.1.0) + jekyll (>= 3.9, < 4.0) jekyll-commonmark (~> 1.4.0) rouge (>= 2.0, < 5.0) - jekyll-default-layout (0.1.4) - jekyll (~> 3.0) - jekyll-feed (0.15.1) + jekyll-default-layout (0.1.5) + jekyll (>= 3.0, < 5.0) + jekyll-feed (0.17.0) jekyll (>= 3.7, < 5.0) jekyll-gist (1.5.0) octokit (~> 4.2) - jekyll-github-metadata (2.13.0) + jekyll-github-metadata (2.16.1) jekyll (>= 3.4, < 5.0) - octokit (~> 4.0, != 4.4.0) + octokit (>= 4, < 7, != 4.4.0) jekyll-include-cache (0.2.1) jekyll (>= 3.7, < 5.0) jekyll-mentions (1.6.0) @@ -194,26 +214,30 @@ GEM jekyll (>= 3.3, < 5.0) jekyll-watch (2.2.1) listen (~> 3.0) - jemoji (0.12.0) - gemoji (~> 3.0) + jemoji (0.13.0) + gemoji (>= 3, < 5) html-pipeline (~> 2.2) jekyll (>= 3.0, < 5.0) - kramdown (2.3.2) + json (2.10.2) + kramdown (2.4.0) rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) liquid (4.0.4) - listen (3.8.0) + listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) + logger (1.6.6) mercenary (0.3.6) - mini_portile2 (2.8.6) + mini_portile2 (2.8.8) minima (2.5.1) jekyll (>= 3.5, < 5.0) jekyll-feed (~> 0.9) jekyll-seo-tag (~> 2.1) - minitest (5.19.0) - nokogiri (1.16.5) + minitest (5.25.5) + net-http (0.6.0) + uri + nokogiri (1.18.6) mini_portile2 (~> 2.8.2) racc (~> 1.4) octokit (4.25.1) @@ -221,15 +245,14 @@ GEM sawyer (~> 0.9) pathutil (0.16.2) forwardable-extended (~> 2.6) - public_suffix (4.0.7) - racc (1.7.3) + public_suffix (5.1.1) + racc (1.8.1) rb-fsevent (0.11.2) - rb-inotify (0.10.1) + rb-inotify (0.11.1) ffi (~> 1.0) - rexml (3.3.9) - rouge (3.26.0) - ruby2_keywords (0.0.5) - rubyzip (2.3.2) + rexml (3.4.1) + rouge (3.30.0) + rubyzip (2.4.1) safe_yaml (1.0.5) sass (3.7.4) sass-listen (~> 4.0.0) @@ -239,19 +262,16 @@ GEM sawyer (0.9.2) addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) - simpleidn (0.2.1) - unf (~> 0.1.4) + securerandom (0.4.1) + simpleidn (0.2.3) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) - typhoeus (1.4.0) + typhoeus (1.4.1) ethon (>= 0.9.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) - unf_ext (0.0.8.2-x64-mingw32) unicode-display_width (1.8.0) + uri (1.0.3) webrick (1.8.2) PLATFORMS @@ -264,4 +284,4 @@ DEPENDENCIES webrick BUNDLED WITH - 2.1.4 + 2.6.2 diff --git a/docs/Logging.md b/docs/Logging.md index 5e4fb9bc217..3717d9cb401 100644 --- a/docs/Logging.md +++ b/docs/Logging.md @@ -1,5 +1,6 @@ --- layout: page +nav_exclude: true title: Logging guide --- diff --git a/docs/SettingUp.md b/docs/SettingUp.md index aef33ec72fd..a890cba57c2 100644 --- a/docs/SettingUp.md +++ b/docs/SettingUp.md @@ -1,5 +1,6 @@ --- layout: page +nav_exclude: true title: Setting up and getting started --- diff --git a/docs/Testing.md b/docs/Testing.md index 8a99e82438a..82da69d655c 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -1,5 +1,6 @@ --- layout: page +nav_exclude: true title: Testing guide --- diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 27c2d1cf16c..484a4e9883c 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,122 +1,182 @@ --- layout: page +nav_order: 2 title: User Guide --- -AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized for use via a Command Line Interface** (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, AB3 can get your contact management tasks done faster than traditional GUI apps. +# NUSMates User Guide +{: .no_toc} -* Table of Contents -{:toc} +NUSMates allows NUS undergraduate students to record the contact details of their fellow NUS undergraduate students. With NUSMates, you can record NUS-specific contact information such as [year](#year), [major](#major), [housing](#housing), and [modules](#module). +NUSMates makes it seamless for you to record [module](#module) information using an [NUSMods link](#nusmods-link), helping you easily find friends who are taking the same [modules](#module) - so you can form project groups, share notes, or know who to reach out to for help. --------------------------------------------------------------------------------------------------------------------- +#### You will love NUSMates if: +{: .no_toc} -## Quick start +* You're an NUS undergraduate student who frequently uses NUSMods +* You're socially active in NUS with a need to manage the contacts of your fellow NUS undergraduate students +* You often use NUSMods to plan, record, and share your course schedules +* You're tech-savvy and familiar with installing jar files +* You can type fast and prefer typing commands over using a mouse -1. Ensure you have Java `17` or above installed in your Computer.
- **Mac users:** Ensure you have the precise JDK version prescribed [here](https://se-education.org/guides/tutorials/javaInstallationMac.html). +{: .tip } +> You can quickly look up all commands in the [Command Summary](#command-summary), or check the [Glossary](#glossary) if you’re unsure about any technical terms used. + +-------------------------------------------------------------------------------------------------------------------- -1. Download the latest `.jar` file from [here](https://github.com/se-edu/addressbook-level3/releases). +## Table of Contents +{: .no_toc .text-delta } -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +1. Table of Contents +{:toc} + +-------------------------------------------------------------------------------------------------------------------- -1. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar addressbook.jar` command to run the application.
- A GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.
- ![Ui](images/Ui.png) +## Quick Start -1. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
- Some example commands you can try: +{: .tip } +> Having trouble installing the app or confused by any of the steps? Check out the [Detailed Installation Guide](#detailed-installation-guide) for step-by-step help. - * `list` : Lists all contacts. +1. Make sure you have **[Java](#java) 17 or later** installed in your computer.
+ > {: .note } + > **Mac users:** Ensure you have the precise JDK version prescribed [here](https://se-education.org/guides/tutorials/javaInstallationMac.html). - * `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. +2. Download the latest [.jar file](#jar-file) from [here](https://github.com/{{site.repository}}/releases). - * `delete 3` : Deletes the 3rd contact shown in the current list. +3. Copy the file to the [home folder](#home-folder) you want to use as the [home folder](#home-folder) for NUSMates. NUSMates will later generate files in this folder, including save data. - * `clear` : Deletes all contacts. +4. Open a [terminal](#terminal) and `cd` into the folder you put the [.jar file](#jar-file) in. - * `exit` : Exits the app. +5. Use the command `java -jar "nusmates.jar"` to run the application. + A [GUI](#gui) similar to the below should appear in a few seconds. Note how the app contains some sample data.
+ > | ![Ui](images/Ui.png) | + > |:--------------------------------------------------------:| + > | GUI which should appear after you launch the application | -1. Refer to the [Features](#features) below for details of each command. +6. Type the command in the command box and press Enter to execute it. +7. Refer to the [Features](#features) below for details of each command. -------------------------------------------------------------------------------------------------------------------- -## Features +
-
+## Command Summary -**:information_source: Notes about the command format:**
+{: .note } +> For more detailed information regarding the command format and each command, refer to the [Features](#features) section. -* 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`. +| Command | Format, Examples | +|-------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Add** | `add n/NAME [p/PHONE_NUMBER] [e/EMAIL] [y/YEAR] [m/MAJOR] [h/HOUSING] [l/NUSMODS_LINK] [t/TAG] [t/MORE_TAGS]...`
e.g., `add n/John Doe p/98765432 e/johnd@example.com y/2 m/Computer Science h/UTown Residence l/https://nusmods.com/timetable/sem-2/share?CS2103T=LEC:G12 t/kiasu` | +| **Edit** | `edit index [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [y/YEAR] [m/MAJOR] [h/HOUSING] [l/NUSMODS_LINK] [t/TAG]...`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` | +| **Delete** | `delete index`
e.g., `delete 3` | +| **Find** | `find KEYWORD [MORE_KEYWORDS]...`
e.g., `find James Jake` | +| **FindMod** | `findMod KEYWORD [MORE_KEYWORDS]...`
e.g., `findMod CS2103T CS2101` | +| **List** | `list` | +| **Clear** | `clear` | +| **Exit** | `exit` | +| **Help** | `help` | -* Items in square brackets are optional.
- e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`. +-------------------------------------------------------------------------------------------------------------------- -* 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. +## Storing a contact -* 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. +This section explains the meaning of each contact field you can include when storing a contact. These fields are also used as parameters in some commands. -* 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`. -* If you are using a PDF version of this document, be careful when copying and pasting commands that span multiple lines as space characters surrounding line-breaks may be omitted when copied over to the application. -
+| Field | Explanation | Constraints | +|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------| +| `n/NAME` | The name of the person. This is the only required field. | A non-empty string consisting of alphanumeric characters and spaces. | +| `p/PHONE_NUMBER` | The person’s phone number. | **8-digit number**, as all target users are expected to be Singapore residents. | +| `e/EMAIL` | The person’s email address. | Any valid email address. | +| `y/YEAR` | The person’s [year](#year) of study at NUS. E.g., `1` = Year 1.
| Any number between 1 to 6, which is the maximum candidature period. | +| `m/MAJOR` | The person’s [major](#major) at NUS. E.g., Computer Science. | Any string. | +| `h/HOUSING` | Where the person stays, such as UTown Residence or off-campus. | Any string. | +| `l/NUSMODS_LINK` | A link to the person’s [NUSMods](#nusmods-link) timetable containing the modules they are taking. You can click the link to copy it to your clipboard. | Any valid NUSMods timetable link. | +| `t/TAG` | [Tags](#tag) to categorise the person, e.g., `t/friend`, `t/project`. One person can have multiple tags.
💡 Tip: You can use tags to record CCAs, country of origin, etc. | Any alphanumeric string. | -### Viewing help : `help` +-------------------------------------------------------------------------------------------------------------------- -Shows a message explaning how to access the help page. +
-![help message](images/helpMessage.png) +## Features -Format: `help` +{: .note } +> * Words in `UPPER_CASE` are the [parameters](#parameter) you must supply when entering the command.
+ e.g. in `add n/NAME`, `NAME` is a [parameter](#parameter) which can be used as `add 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. +> * Extraneous parameters for commands that do not take in parameters (such as `list`, `clear`, `exit` and `help`) will be ignored. + e.g. if the command specifies `list 123`, it will be interpreted as `list`. +{: .tip } +> * [Parameters](#parameter) 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 you are using a PDF version of this document, be careful when copying and pasting commands that span multiple lines as space characters surrounding line-breaks may be omitted when copied over to the application. ### Adding a person: `add` -Adds a person to the address book. +You can add a person to NUSMates and record their contact information with the `add` command. -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +Format: `add n/NAME [p/PHONE_NUMBER] [e/EMAIL] [y/YEAR] [m/MAJOR] [h/HOUSING] [l/NUSMODS_LINK] [t/TAG] [t/MORE_TAGS]...` -
:bulb: **Tip:** -A person can have any number of tags (including 0) -
+* All parameters **except for `NAME`** are optional.
+e.g. You can add a contact with only name, [year](#year), and [major](#major) using `add n/John Doe y/2 m/Computer Science`
+* A person can have any number of [tags](#tag), including none.
+* `NUSMODS_LINK` refers to the student's [NUSMods link](#nusmods-link) course schedule original link. 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` - -### Listing all persons : `list` +* `add n/John Doe p/98765432 e/johnd@example.com y/2 m/Computer Science h/UTown Residence l/https://nusmods.com/timetable/sem-2/share?CS2103T=LEC:G12` +* `add n/Betsy Crowe t/friend e/betsycrowe@example.com y/1 m/Electrical Engineering h/PGPR p/1234567 l/https://nusmods.com/timetable/sem-2/share?CS2040=TUT:12,LAB:06,LEC:1` -Shows a list of all persons in the address book. +{: .note } +> The application does not allow for two contacts to have the same name! However, other fields may have duplicates. -Format: `list` +{: .tip } +> For details on the NUSMods link parameter and how to get it, read [here](#how-to-get-the-nusmods-link) ### Editing a person : `edit` -Edits an existing person in the address book. +You can edit the details of a person whose contact information you already saved with the `edit` command. -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +Format: `edit index [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [y/YEAR] [m/MAJOR] [h/HOUSING] [l/NUSMODS_LINK] [t/TAG]...` -* Edits the person at the specified `INDEX`. The index refers to the index number shown in the displayed person list. The index **must be a positive integer** 1, 2, 3, …​ -* At least one of the optional fields must be provided. +* `Index` refers to the [index](#index) number shown in the displayed person list. The [index](#index) **must be a positive integer** 1, 2, 3, …​ +* **At least one of the [parameters](#parameter) must be provided.** * Existing values will be updated to the input values. -* When editing tags, the existing tags of the person will be removed i.e adding of tags is not cumulative. -* You can remove all the person’s tags by typing `t/` without - specifying any tags after it. +* You can remove all the person’s [tags](#tag) by typing `t/` without specifying any [tags](#tag) after it. +* When editing [tags](#tag), the existing [tags](#tag) of the person will be removed i.e adding of [tags](#tag) is not cumulative. + +{: .warning } +> You cannot combine tag clearing `t/` and tag addition `t/TAG` in the same command. Doing so will result in an error. +> For example, the command `edit 1 t/ t/friends` is invalid — you must either clear all tags `t/` or replace them with new tags `t/friends t/groupmate`, but not both. Examples: * `edit 1 p/91234567 e/johndoe@example.com` Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively. -* `edit 2 n/Betsy Crower t/` Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. +* `edit 2 n/Betsy Crower t/` Edits the name of the 2nd person to be `Betsy Crower` and clears all existing [tags](#tag). + +### Deleting a person : `delete` + +You can delete a person from NUSMates with the `delete` command. + +Format: `delete index` + +* Deletes the person at the specified `index`. +* The [index](#index) refers to the [index](#index) number shown in the displayed person list. +* The [index](#index) **must be a positive integer** 1, 2, 3, …​ + +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. ### Locating persons by name: `find` -Finds persons whose names contain any of the given keywords. +You can find a person in your contacts whose name matches a keyword with the `find` command. -Format: `find KEYWORD [MORE_KEYWORDS]` +Format: `find KEYWORD [MORE_KEYWORDS]...` * The search is case-insensitive. e.g `hans` will match `Hans` +* You can use multiple keywords separated by spaces, e.g. `Hans Bo` * 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` @@ -124,77 +184,307 @@ Format: `find KEYWORD [MORE_KEYWORDS]` 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) +* `find Huazhi` returns `Huazhi` +* `find Abi Yuexi` returns `Abi`, `Yuexi`
-### Deleting a person : `delete` +| ![result for 'find Abi Yuexi'](images/findDemo.png) | +|:---------------------------------------------------:| +| Result for `find Abi Yuexi` | + +### Locating persons by [module](#module): `findMod` -Deletes the specified person from the address book. +You can find a person taking certain [modules](#module) with the `findMod` command. -Format: `delete INDEX` +Format: `findMod KEYWORD [MOREKEYWORDS]...` -* Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. -* The index **must be a positive integer** 1, 2, 3, …​ +* The search is case-insensitive. e.g `cs2030` will match `CS2030` +* The order of the keywords does not matter. e.g. `CS2030 CS2103T` will match `CS2103T CS2030` +* Only the [module](#module) is searched. +* Only full words will be matched e.g. `CS2103` will not match `CS2103T` +* Persons matching at least one of the [modules](#module) searched will be returned (i.e. `OR` search). +* Note that NUSMates can distinguish between modules added as student or as a TA on NUSMods. Modules with the TA option enabled are appended with a `(TA)`, so `CS2030` would become `CS2030(TA)` instead. 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. +* `findMod CS2109S` returns `Abi, Yuexi` + +| ![result for 'findMod CS2109S'](images/findModDemo.png) | +|:-------------------------------------------------------:| +| Result for `findMod CS2109S` | + +### Listing all persons : `list` + +After finding specific persons, you can list all your contacts again with the `list` command. + +Format: `list` ### Clearing all entries : `clear` -Clears all entries from the address book. +You can quickly delete all your contacts with the `clear` command. Format: `clear` + +{: .warning } +> There is no way to undo clearing all your contacts, so be careful! + ### Exiting the program : `exit` -Exits the program. +You can exit the NUSMates app by using the `exit` command. Format: `exit` -### Saving the data +### Viewing help : `help` + +You can return to this user guide for help on using NUSMates by using the `help` command. + +| ![help message](images/helpCommand.png) | +|:---------------------------------------------------------:| +| Pop-up window displayed when you enter the `help` command | + +Format: `help` + +### Saving and editing the data [file](#json-file) + +NUSMates data is saved in the hard disk automatically after any command that changes the data, so you don't need to save manually. + +NUSMates data is saved automatically as a [JSON file](#json-file) `[JAR file location]/data/addressbook.json`. If you are an advanced user, you are welcome to update data directly by editing that file. + +{: .warning } +> If your changes to the data file makes its format invalid, NUSMates will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the data file before editing it.
+> Furthermore, certain edits can cause NUSMates to behave in unexpected ways (e.g., if a value entered is outside of the acceptable range). Therefore, edit the data file only if you are confident that you can update it correctly. + +-------------------------------------------------------------------------------------------------------------------- + +## How to get the NUSMods Link + +{: .note-title } +> ℹ️ Note: What is NUSMods? +> +> NUSMods is a website used by NUS students to view and plan their module timetables. A typical NUSMods timetable might look something like this: +![nusmods_sample.png](images/nusmods_sample.png) +>You can share your NUSMods timetable with others using an NUSMods link. NUSMates uses this NUSMods link to record the module information of a contact. + +
+ +### Step 1: Share/Sync +{: .no_toc} +Click on the `Share/Sync` button on the top right. + +| ![nusmods_step1.png](images/nusmods_step1.png) | +|:----------------------------------------------:| +| `Share/Sync` button on the NUSMods website | -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 +### Step 2: Show original link +{: .no_toc} +Click on the `Show original link` button to convert the shortened NUSMods link to the original one. -AddressBook data are saved automatically as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. +| ![nusmods_step2.png](images/nusmods_step2.png) | +|:--------------------------------------------------:| +| `Show original link` button on the NUSMods website | -
:exclamation: **Caution:** -If your changes to the data file makes its format invalid, AddressBook will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it.
-Furthermore, certain edits can cause the AddressBook to behave in unexpected ways (e.g., if a value entered is outside of the acceptable range). Therefore, edit the data file only if you are confident that you can update it correctly. -
+
-### Archiving data files `[coming in v2.0]` +### Step 3. Copy +{: .no_toc} +Copy the link, and you're done! -_Details coming soon ..._ +| ![nusmods_step3.png](images/nusmods_step3.png) | +|:----------------------------------------------:| +| `Copy link` button on the NUSMods website | + +You can now use this link as the `l/NUSMODS_LINK` parameter in the [add](#adding-a-person-add) or [edit](#editing-a-person--edit) commands. + +-------------------------------------------------------------------------------------------------------------------- + +## Detailed Installation Guide + +This section helps you install and run NUSMates, even if you have never used a [terminal](#terminal) or heard of [Java](#java) before. Just follow the steps one by one! + + +### Step 1: Check if you already have Java +{: .no_toc} + +NUSMates runs using [Java](#java), which is a common tool installed on many computers. + +#### To check: +{: .no_toc} + +1. Open your [terminal](#terminal): + - On **Windows**: Press the `Windows` key, type `cmd`, and press Enter. + - On **macOS**: Press `Cmd + Space`, type `Terminal`, and hit Enter. + +2. Type this into the terminal and press Enter: + ``` + java -version + ``` + +3. If you see a version number that starts with `17` or higher, such as in the image below, you’re good to go. Move to step 2.
+ > | ![java-version.png](images%2Fjava-version.png) | + > |:----------------------------------------------------------------------------------------------:| + > | Result of entering the `java -version` command in terminal, and where to see your Java version | + +4. If you see an error, or a number less than 17, you’ll need to install Java. Follow the guide for your operating system below: + + - [Java Installation Guide (Windows & macOS)](https://se-education.org/guides/tutorials/javaInstallation.html) + + +### Step 2: Download the NUSMates `.jar` file +{: .no_toc} + +1. Go to the latest release of NUSMates on the [NUSMates releases page](https://github.com/AY2425S2-CS2103T-T11-1/tp/releases/) + +2. Find the most recent version, and download the file named `nusmates.jar` + + +### Step 3: Choose a [home folder](#home-folder) +{: .no_toc} + +1. Create a new folder anywhere you like, e.g., on your `Desktop` or in `Documents`. +2. Give it a name like `NUSMates`. +3. Move the downloaded `.jar` file into this folder. + + +### Step 4: Open the [terminal](#terminal) in that folder +{: .no_toc} + +You’ll now “go into” the folder using the terminal. + +#### On Windows: +{: .no_toc} + +1. Open **File Explorer** and navigate to your NUSMates folder. +2. In the address bar at the top, type `cmd` and press Enter. +3. A [terminal](#terminal) window will appear, already inside the folder. + +#### On macOS: +{: .no_toc} + +1. Open the **Terminal** app. +2. Type `cd ` (with a space), then drag and drop your NUSMates folder into the terminal. +3. Press Enter. + +### Step 5: Run NUSMates +{: .no_toc} + +Now that you’re in the correct folder, run this command: + +``` +java -jar "nusmates.jar" +``` + +If everything works, a window will pop up showing NUSMates with some sample contacts, such as below. + +| ![Ui](images/Ui.png) | +|:--------------------------------------------------------:| +| GUI which should appear after you launch the application | + +
+ +{: .warning } +> If you see an error: +> - Double-check that you're in the correct folder and that the file name is exactly `nusmates.jar`. +> - Make sure [Java](#java) was installed properly. +> - Visit the [Troubleshooting](#troubleshooting) section for more help. -------------------------------------------------------------------------------------------------------------------- ## 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 the app in the other computer and overwrite the empty data [file](#json-file) it creates with the [file](#json-file) that contains the data of your previous AddressBook [home folder](#home-folder). + +**Q**: Can I use NUSMates on my mobile device?
+**A**: NUSMates is a command-line application that may not be optimized for mobile devices. +It’s best used on desktop environments where a terminal or command prompt is available. + +**Q**: How do I get the NUSMods link?
+**A**: You may refer to the detailed step-by-step guide [here](#how-to-get-the-nusmods-link). + +**Q**: Can I add two contacts with the same name?
+**A**: No, NUSMates does not allow duplicate names when adding contacts. +Each contact must have a unique name in the system. +To add multiple people with the same name, distinguish them in some way such as adding a descriptor (e.g., John Doe (Work)) or a number (Amy1, Amy2). + +**Q**: Can I edit a contact's module information manually?
+**A**: No, a contact's module information is automatically retrieved from the NUSMods link they provided. +If their schedule changes, simply ask them to send you the updated link, and you can update their module information using the [edit](#editing-a-person--edit) command. + +**Q**: If my contact changes their schedule on NUSMods, will their modules be automatically updated in NUSMates?
+**A**: No, changes in NUSMods will not automatically be reflected in NUSMates. You can ask your contact to send you the updated link, and you can update their module information using the [edit](#editing-a-person--edit) command. + +**Q**: How are hidden modules in the timetable treated?
+**A**: Our link processing treats hidden modules as usual, and **does** add it to the module list of the contact. -------------------------------------------------------------------------------------------------------------------- -## Known issues +## Troubleshooting -1. **When using multiple screens**, if you move the application to a secondary screen, and later switch to using only the primary screen, the GUI will open off-screen. The remedy is to delete the `preferences.json` file created by the application before running the application again. +1. **When using multiple screens**, if you move the application to a secondary screen, and later switch to using only the primary screen, the [GUI](#gui) will open off-screen. The remedy is to delete the `preferences.json` [file](#json-file) created by the application before running the application again. 2. **If you minimize the Help Window** and then run the `help` command (or use the `Help` menu, or the keyboard shortcut `F1`) again, the original Help Window will remain minimized, and no new Help Window will appear. The remedy is to manually restore the minimized Help Window. -------------------------------------------------------------------------------------------------------------------- -## Command summary - -Action | Format, Examples ---------|------------------ -**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` -**Clear** | `clear` -**Delete** | `delete INDEX`
e.g., `delete 3` -**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` -**Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake` -**List** | `list` -**Help** | `help` +## Glossary +Here are the definitions of some uncommon or domain-specific terms used frequently in this user guide. + +### Java +{: .no_toc} +A programming language and platform used to run NUSMates. You need Java 17 or above installed on your computer to run the `.jar` file. + +### `.jar` file +{: .no_toc} +A Java Archive file that packages a Java application. You run it using the `java -jar` command. + +### Terminal +{: .no_toc} +A text-based interface that allows users to interact with the computer using commands. On Windows, this is called the Command Prompt; on macOS, it’s called Terminal. + +### `cd` command +{: .no_toc} +Short for “change directory.” This command is used in the terminal to navigate to the folder where your `.jar` file is located. + +### Home folder +{: .no_toc} +The folder where you place the `.jar` file. NUSMates stores its data in this folder. + +### GUI +{: .no_toc} +Short for Graphical User Interface. It’s the visual interface of the app with windows and buttons. + +### Parameter +{: .no_toc} +A specific piece of information the user provides when entering a command. Parameters are usually written in `UPPER_CASE` in the command format. + +### Index +{: .no_toc} +A number that represents the position of a contact in the list shown in the app. For example, the first contact has index 1. + +### Tag +{: .no_toc} +A label you can add to a contact to help categorise them, such as storing CCA information e.g. `t/NUS Amplified` or `t/NUS Computing Club`. + +### JSON file +{: .no_toc} +A type of file used to store data in a structured format. NUSMates uses a JSON file to save your contact data. + +### NUSMods Link +{: .no_toc} +NUSMods is a website used by NUS students to view and plan their module timetables. You can share your NUSMods timetable using an NUSMods link. For more information, refer to [how to get the NUSMods link](#how-to-get-the-nusmods-link). + +### Module +{: .no_toc} +A subject or course that NUS students take as part of their degree programme. Each module has a unique code (e.g., CS2103T) and typically includes lectures, tutorials, and/or labs. + +### Major +{: .no_toc} +The primary field of study that a student is specialising in at NUS, such as Computer Science or Electrical Engineering. + +### Year +{: .no_toc} +Refers to the student's current year of study at NUS, e.g., Year 1 (first-year student), Year 2, and so on. + +### Housing +{: .no_toc} +Refers to the student's housing option at NUS or where they live in general such as UTown Residence, Lighthouse, or their address off campus. diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..6b9a4072125 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,14 +1,29 @@ -title: "AB-3" -theme: minima - -header_pages: - - UserGuide.md - - DeveloperGuide.md - - AboutUs.md +title: "NUSMates" +remote_theme: just-the-docs/just-the-docs markdown: kramdown -repository: "se-edu/addressbook-level3" +aux_links: + NUSMates on GitHub: https://github.com/AY2425S2-CS2103T-T11-1/tp + +callouts: + tip: + title: "💡 Tip" + color: yellow + note: + title: "ℹ️ Note" + color: blue + warning: + title: "⚠️ Warning" + color: red + +nav_external_links: + - title: NUSMates on GitHub + url: https://github.com/AY2425S2-CS2103T-T11-1/tp + hide_icon: false # set to true to hide the external link icon - defaults to false + opens_in_new_tab: true # set to true to open this link in a new tab - defaults to false + +repository: "AY2425S2-CS2103T-T11-1/tp" github_icon: "images/github-icon.png" plugins: diff --git a/docs/_includes/backToTop.html b/docs/_includes/backToTop.html new file mode 100644 index 00000000000..3dd8ff52cea --- /dev/null +++ b/docs/_includes/backToTop.html @@ -0,0 +1,92 @@ + + + + + + + + diff --git a/docs/_includes/footer_custom.html b/docs/_includes/footer_custom.html new file mode 100644 index 00000000000..f6f25e6fca7 --- /dev/null +++ b/docs/_includes/footer_custom.html @@ -0,0 +1 @@ +{% include backToTop.html %} diff --git a/docs/_includes/head.html b/docs/_includes/head.html deleted file mode 100644 index 83ac5326933..00000000000 --- a/docs/_includes/head.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - {%- include custom-head.html -%} - - {{page.title}} - - diff --git a/docs/_includes/custom-head.html b/docs/_includes/head_custom.html similarity index 80% rename from docs/_includes/custom-head.html rename to docs/_includes/head_custom.html index 8559a67ffad..48436c3fa11 100644 --- a/docs/_includes/custom-head.html +++ b/docs/_includes/head_custom.html @@ -2,5 +2,5 @@ Placeholder to allow defining custom head, in principle, you can add anything here, e.g. favicons: 1. Head over to https://realfavicongenerator.net/ to add your own favicons. - 2. Customize default _includes/custom-head.html in your source directory and insert the given code snippet. + 2. Customize default _includes/head_custom.html in your source directory and insert the given code snippet. {% endcomment %} diff --git a/docs/_includes/header.html b/docs/_includes/header.html deleted file mode 100644 index 33badcd4f99..00000000000 --- a/docs/_includes/header.html +++ /dev/null @@ -1,36 +0,0 @@ - diff --git a/docs/_layouts/alt-page.html b/docs/_layouts/alt-page.html deleted file mode 100644 index 5dbc6ef245f..00000000000 --- a/docs/_layouts/alt-page.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -layout: default ---- -
- -
-

{{ page.alt_title | escape }}

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

{{ page.title | escape }}

-
- -
- {{ content }} -
- -
diff --git a/docs/diagrams/AddPersonActivityDiagram.puml b/docs/diagrams/AddPersonActivityDiagram.puml new file mode 100644 index 00000000000..e482c0fa4c2 --- /dev/null +++ b/docs/diagrams/AddPersonActivityDiagram.puml @@ -0,0 +1,16 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +start +:User inputs command to add person; +:System checks if input is valid; + +if () then ([input is valid]) + :New person is added; + :System indicates success; +else ([else]) +:System indicates error; +endif +stop +@enduml diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml index 598474a5c82..5a656e6b7b6 100644 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ b/docs/diagrams/BetterModelClassDiagram.puml @@ -13,9 +13,12 @@ UniqueTagList -right-> "*" Tag UniquePersonList -right-> Person Person -up-> "*" Tag +Person *--> "1" Name +Person *--> "0..1" Phone +Person *--> "0..1" Email +Person *--> "0..1" Housing +Person *--> "0..1" Link +Person *--> "0..1" Major +Person *--> "0..1" Year -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address @enduml diff --git a/docs/diagrams/EditSequenceDiagram.puml b/docs/diagrams/EditSequenceDiagram.puml new file mode 100644 index 00000000000..ae8eeab982a --- /dev/null +++ b/docs/diagrams/EditSequenceDiagram.puml @@ -0,0 +1,77 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":EditCommandParser" as EditCommandParser LOGIC_COLOR +participant "d:EditCommand" as EditCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "m:Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("edit 2 y/2") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("edit 2 y/2") +activate AddressBookParser + +create EditCommandParser +AddressBookParser -> EditCommandParser +activate EditCommandParser + +EditCommandParser --> AddressBookParser +deactivate EditCommandParser + +AddressBookParser -> EditCommandParser : parse("2 y/2") +activate EditCommandParser + +create EditCommand +EditCommandParser -> EditCommand : 2, y/2 +activate EditCommand + +EditCommand --> EditCommandParser : +deactivate EditCommand + +EditCommandParser --> AddressBookParser : d +deactivate EditCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +EditCommandParser -[hidden]-> AddressBookParser +destroy EditCommandParser + +AddressBookParser --> LogicManager : d +deactivate AddressBookParser + +LogicManager -> EditCommand : execute(m) +activate EditCommand + +EditCommand -> Model : setPerson(2, y/2) +activate Model + +Model --> EditCommand +deactivate Model + + +EditCommand -> Model : updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS) +activate Model + +Model --> EditCommand +deactivate Model + +create CommandResult +EditCommand -> CommandResult +activate CommandResult + +CommandResult --> EditCommand +deactivate CommandResult + +EditCommand --> LogicManager : r +deactivate EditCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 0de5673070d..ac535ed4bdf 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -14,11 +14,14 @@ Class UserPrefs Class UniquePersonList Class Person -Class Address +Class Housing Class Email Class Name Class Phone Class Tag +Class Link +Class Year +Class Major Class I #FFFFFF } @@ -37,18 +40,21 @@ UserPrefs .up.|> ReadOnlyUserPrefs AddressBook *--> "1" UniquePersonList UniquePersonList --> "~* all" Person -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address +Person *--> "1" Name +Person *--> "0..1" Phone +Person *--> "0..1" Email +Person *--> "0..1" Housing Person *--> "*" Tag +Person *--> "0..1" Link +Person *--> "0..1" Year +Person *--> "0..1" Major Person -[hidden]up--> I UniquePersonList -[hidden]right-> I Name -[hidden]right-> Phone -Phone -[hidden]right-> Address -Address -[hidden]right-> Email +Phone -[hidden]right-> Housing +Housing -[hidden]right-> Email ModelManager --> "~* filtered" Person @enduml diff --git a/docs/images/AddPersonActivityDiagram.png b/docs/images/AddPersonActivityDiagram.png new file mode 100644 index 00000000000..697c1be943b Binary files /dev/null and b/docs/images/AddPersonActivityDiagram.png differ diff --git a/docs/images/BetterModelClassDiagram.png b/docs/images/BetterModelClassDiagram.png index 02a42e35e76..d1e59e73c71 100644 Binary files a/docs/images/BetterModelClassDiagram.png and b/docs/images/BetterModelClassDiagram.png differ diff --git a/docs/images/EditSequenceDiagram.png b/docs/images/EditSequenceDiagram.png new file mode 100644 index 00000000000..231d6b664f9 Binary files /dev/null and b/docs/images/EditSequenceDiagram.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index a19fb1b4ac8..06f3c4216bd 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/TagUiOverflow.png b/docs/images/TagUiOverflow.png new file mode 100644 index 00000000000..7fd02dccffc Binary files /dev/null and b/docs/images/TagUiOverflow.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..d4859fa7550 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/abihalim.png b/docs/images/abihalim.png new file mode 100644 index 00000000000..e022f40bbca Binary files /dev/null and b/docs/images/abihalim.png differ diff --git a/docs/images/findAbiYuexi.png b/docs/images/findAbiYuexi.png new file mode 100644 index 00000000000..540f974386f Binary files /dev/null and b/docs/images/findAbiYuexi.png differ diff --git a/docs/images/findCS2109Sresult.png b/docs/images/findCS2109Sresult.png new file mode 100644 index 00000000000..ee10ea8c09d Binary files /dev/null and b/docs/images/findCS2109Sresult.png differ diff --git a/docs/images/findDemo.png b/docs/images/findDemo.png new file mode 100644 index 00000000000..6590b929a9f Binary files /dev/null and b/docs/images/findDemo.png differ diff --git a/docs/images/findModDemo.png b/docs/images/findModDemo.png new file mode 100644 index 00000000000..50d2db84fdf Binary files /dev/null and b/docs/images/findModDemo.png differ diff --git a/docs/images/helpCommand.png b/docs/images/helpCommand.png new file mode 100644 index 00000000000..6f0e43ac692 Binary files /dev/null and b/docs/images/helpCommand.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/java-version.png b/docs/images/java-version.png new file mode 100644 index 00000000000..5ab2dc45857 Binary files /dev/null and b/docs/images/java-version.png differ diff --git a/docs/images/nusmods_sample.png b/docs/images/nusmods_sample.png new file mode 100644 index 00000000000..99648797e47 Binary files /dev/null and b/docs/images/nusmods_sample.png differ diff --git a/docs/images/nusmods_step1.png b/docs/images/nusmods_step1.png new file mode 100644 index 00000000000..e60f116016d Binary files /dev/null and b/docs/images/nusmods_step1.png differ diff --git a/docs/images/nusmods_step2.png b/docs/images/nusmods_step2.png new file mode 100644 index 00000000000..ea65a444f97 Binary files /dev/null and b/docs/images/nusmods_step2.png differ diff --git a/docs/images/nusmods_step3.png b/docs/images/nusmods_step3.png new file mode 100644 index 00000000000..80e65ea8a93 Binary files /dev/null and b/docs/images/nusmods_step3.png differ diff --git a/docs/images/shashwatchan.png b/docs/images/shashwatchan.png new file mode 100644 index 00000000000..a7aea05792e Binary files /dev/null and b/docs/images/shashwatchan.png differ diff --git a/docs/images/veehz.png b/docs/images/veehz.png new file mode 100644 index 00000000000..9fcca753106 Binary files /dev/null and b/docs/images/veehz.png differ diff --git a/docs/images/yosiesyx.png b/docs/images/yosiesyx.png new file mode 100644 index 00000000000..176e02b4ffe Binary files /dev/null and b/docs/images/yosiesyx.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..c318e422e9d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,21 @@ --- layout: page -title: AddressBook Level-3 +nav_order: 1 +title: Home --- -[![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) +# NUSMates + +[![CI Status](https://github.com/{{site.repository}}/workflows/Java%20CI/badge.svg)](https://github.com/{{site.repository}}/actions) +[![codecov](https://codecov.io/gh/{{site.repository}}/branch/master/graph/badge.svg)](https://codecov.io/gh/{{site.repository}}) ![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). +**NUSMates allows NUS undergraduate students to record the contact details of their fellow NUS undergraduate students.** With NUSMates, you can record NUS-specific contact information such as year, major, housing, and modules. +NUSMates also makes it seamless to record module information using an NUSMods link, helping you easily find friends who are taking the same modules - so you can form project groups, share notes, or know who to reach out to for help. -* If you are interested in using AddressBook, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). -* If you are interested about developing AddressBook, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. +* If you are interested in using NUSMates, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +* If you are interested about developing NUSMates, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. **Acknowledgements** diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md index 773a07794e2..2785ed4cca9 100644 --- a/docs/team/johndoe.md +++ b/docs/team/johndoe.md @@ -1,5 +1,6 @@ --- layout: page +nav_exclude: true title: John Doe's Project Portfolio Page --- diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java index ecd32c31b53..ebd653ce299 100644 --- a/src/main/java/seedu/address/logic/Messages.java +++ b/src/main/java/seedu/address/logic/Messages.java @@ -14,10 +14,12 @@ 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_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_PERSON_LISTED_OVERVIEW = "1 person listed!"; public static final String MESSAGE_DUPLICATE_FIELDS = - "Multiple values specified for the following single-valued field(s): "; + "Multiple values specified for the following single-valued field(s): "; /** * Returns an error message indicating the duplicate prefixes. @@ -37,14 +39,47 @@ public static String getErrorMessageForDuplicatePrefixes(Prefix... duplicatePref public static String format(Person person) { final StringBuilder builder = new StringBuilder(); builder.append(person.getName()) - .append("; Phone: ") - .append(person.getPhone()) - .append("; Email: ") - .append(person.getEmail()) - .append("; Address: ") - .append(person.getAddress()) - .append("; Tags: "); - person.getTags().forEach(builder::append); + .append("; "); + if (person.getPhone() != null) { + builder.append("Phone: ") + .append(person.getPhone()) + .append("; "); + } + if (person.getEmail() != null) { + builder.append("Email: ") + .append(person.getEmail()) + .append("; "); + } + if (person.getYear() != null) { + builder.append("Year: ") + .append(person.getYear()) + .append("; "); + } + if (person.getMajor() != null) { + builder.append("Major: ") + .append(person.getMajor()) + .append("; "); + } + if (person.getHousing() != null) { + builder.append("Housing: ") + .append(person.getHousing()) + .append("; "); + } + if (person.getLink() != null) { + builder.append("Link: ") + .append(person.getLink()) + .append("; "); + } + if (!person.getTags().isEmpty()) { + builder.append("Tags: "); + person.getTags().forEach(builder::append); + } + + int length = builder.length(); + if (length > 0 && builder.charAt(length - 2) == ';') { + builder.delete(length - 2, length); + } + return builder.toString(); } diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index 5d7185a9680..845c6b4288e 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -1,11 +1,14 @@ 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_HOUSING; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LINK; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MAJOR; 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_YEAR; import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; @@ -25,15 +28,21 @@ public class AddCommand extends Command { + PREFIX_NAME + "NAME " + PREFIX_PHONE + "PHONE " + PREFIX_EMAIL + "EMAIL " - + PREFIX_ADDRESS + "ADDRESS " + + PREFIX_YEAR + "YEAR " + + PREFIX_MAJOR + "MAJOR " + + PREFIX_HOUSING + "HOUSING " + + PREFIX_LINK + "LINK " + "[" + 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_EMAIL + "johnd@u.nus.edu " + + PREFIX_YEAR + "2 " + + PREFIX_MAJOR + "Computer Science " + + PREFIX_HOUSING + "Lighthouse Block 29 " + + PREFIX_LINK + " https://nusmods.com/timetable/sem-2/share?CS2103T=LEC:1&CS2101=TUT:1 " + PREFIX_TAG + "friends " - + PREFIX_TAG + "owesMoney"; + + PREFIX_TAG + "teammate"; 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"; diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 4b581c7331e..1e54a735ff2 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -1,19 +1,24 @@ 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_HOUSING; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LINK; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MAJOR; 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_YEAR; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Stream; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.CollectionUtil; @@ -21,11 +26,14 @@ import seedu.address.logic.Messages; 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.Housing; +import seedu.address.model.person.Link; +import seedu.address.model.person.Major; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Year; import seedu.address.model.tag.Tag; /** @@ -42,7 +50,10 @@ public class EditCommand extends Command { + "[" + PREFIX_NAME + "NAME] " + "[" + PREFIX_PHONE + "PHONE] " + "[" + PREFIX_EMAIL + "EMAIL] " - + "[" + PREFIX_ADDRESS + "ADDRESS] " + + "[" + PREFIX_YEAR + "YEAR] " + + "[" + PREFIX_MAJOR + "MAJOR] " + + "[" + PREFIX_HOUSING + "HOUSING] " + + "[" + PREFIX_LINK + "LINK] " + "[" + PREFIX_TAG + "TAG]...\n" + "Example: " + COMMAND_WORD + " 1 " + PREFIX_PHONE + "91234567 " @@ -85,7 +96,8 @@ public CommandResult execute(Model model) throws CommandException { model.setPerson(personToEdit, editedPerson); model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson))); + return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, + editPersonDescriptor.getEditedFieldsMessage())); } /** @@ -98,10 +110,14 @@ private static Person createEditedPerson(Person personToEdit, EditPersonDescript 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()); + Year updatedYear = editPersonDescriptor.getYear().orElse(personToEdit.getYear()); + Major updatedMajor = editPersonDescriptor.getMajor().orElse(personToEdit.getMajor()); + Housing updatedHousing = editPersonDescriptor.getHousing().orElse(personToEdit.getHousing()); + Link updatedLink = editPersonDescriptor.getLink().orElse(personToEdit.getLink()); Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + return new Person(updatedName, updatedPhone, updatedEmail, updatedYear, + updatedMajor, updatedHousing, updatedLink, updatedTags); } @Override @@ -136,7 +152,10 @@ public static class EditPersonDescriptor { private Name name; private Phone phone; private Email email; - private Address address; + private Year year; + private Major major; + private Housing housing; + private Link link; private Set tags; public EditPersonDescriptor() {} @@ -149,7 +168,10 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { setName(toCopy.name); setPhone(toCopy.phone); setEmail(toCopy.email); - setAddress(toCopy.address); + setYear(toCopy.year); + setMajor(toCopy.major); + setHousing(toCopy.housing); + setLink(toCopy.link); setTags(toCopy.tags); } @@ -157,7 +179,54 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { * Returns true if at least one field is edited. */ public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); + return CollectionUtil.isAnyNonNull(name, phone, email, year, major, housing, link, tags); + } + + /** + * Gets all fields which have been edited + * @return An Optional of List containing any edited fields, otherwise an empty Optional if none were edited + */ + private Optional> getEditedFields() { + if (!isAnyFieldEdited()) { + return Optional.empty(); + } else { + List fieldsArray = Arrays.asList(name, phone, email, year, major, housing, link, tags); + Stream nonNullFieldsStream = fieldsArray.stream().filter(Objects::nonNull); + return Optional.of(nonNullFieldsStream.toList()); + } + } + + private String buildTagsMessage(Set tags) { + StringBuilder message = new StringBuilder("Tags: "); + for (Object tag : tags) { + message.append(tag.toString()).append(", "); + } + return message.toString(); + } + + private String buildFieldMessage(Object field) { + return field.getClass().getSimpleName() + ": " + field.toString() + ", "; + } + + /** + * Gets a String which tells which fields were edited, for the command success message. + * @return String detailing fields which have been edited, otherwise an empty String if none were edited + */ + public String getEditedFieldsMessage() { + Optional> editedFields = this.getEditedFields(); + StringBuilder message = new StringBuilder(); + if (!editedFields.isPresent()) { + return ""; + } + for (Object field : editedFields.get()) { + if (field instanceof Set) { + message.append(buildTagsMessage((Set) field)); + } else { + message.append(buildFieldMessage(field)); + } + } + message.setLength(message.length() - 2); + return message.toString(); } public void setName(Name name) { @@ -184,12 +253,36 @@ public Optional getEmail() { return Optional.ofNullable(email); } - public void setAddress(Address address) { - this.address = address; + public void setYear(Year year) { + this.year = year; + } + + public Optional getYear() { + return Optional.ofNullable(year); + } + + public void setMajor(Major major) { + this.major = major; + } + + public Optional getMajor() { + return Optional.ofNullable(major); + } + + public void setHousing(Housing housing) { + this.housing = housing; + } + + public Optional getHousing() { + return Optional.ofNullable(housing); + } + + public void setLink(Link link) { + this.link = link; } - public Optional
getAddress() { - return Optional.ofNullable(address); + public Optional getLink() { + return Optional.ofNullable(link); } /** @@ -224,7 +317,10 @@ public boolean equals(Object other) { return Objects.equals(name, otherEditPersonDescriptor.name) && Objects.equals(phone, otherEditPersonDescriptor.phone) && Objects.equals(email, otherEditPersonDescriptor.email) - && Objects.equals(address, otherEditPersonDescriptor.address) + && Objects.equals(year, otherEditPersonDescriptor.year) + && Objects.equals(major, otherEditPersonDescriptor.major) + && Objects.equals(housing, otherEditPersonDescriptor.housing) + && Objects.equals(link, otherEditPersonDescriptor.link) && Objects.equals(tags, otherEditPersonDescriptor.tags); } @@ -234,7 +330,10 @@ public String toString() { .add("name", name) .add("phone", phone) .add("email", email) - .add("address", address) + .add("year", year) + .add("major", major) + .add("housing", housing) + .add("link", link) .add("tags", tags) .toString(); } diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index 72b9eddd3a7..b34738997d6 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -30,8 +30,17 @@ public FindCommand(NameContainsKeywordsPredicate predicate) { public CommandResult execute(Model model) { requireNonNull(model); model.updateFilteredPersonList(predicate); - return new CommandResult( - String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); + + String resultMessage; + if (model.getFilteredPersonList().size() == 1) { + resultMessage = String.format(Messages.MESSAGE_PERSON_LISTED_OVERVIEW, + model.getFilteredPersonList().size()); + } else { + resultMessage = String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, + model.getFilteredPersonList().size()); + } + + return new CommandResult(resultMessage); } @Override diff --git a/src/main/java/seedu/address/logic/commands/FindModCommand.java b/src/main/java/seedu/address/logic/commands/FindModCommand.java new file mode 100644 index 00000000000..bc508aa7569 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FindModCommand.java @@ -0,0 +1,88 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.logging.Logger; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.model.Model; +import seedu.address.model.person.ModContainsKeywordsPredicate; + + + +/** + * Finds and lists all persons in address book whose name contains any of the argument keywords. + * Keyword matching is case insensitive. + */ +public class FindModCommand extends Command { + + public static final String COMMAND_WORD = "findMod"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all people who are enrolled in any of " + + "the specified module codes (case-insensitive) and displays them as a list with index numbers.\n" + + "Parameters: MODULE_CODE [MORE_MODULE_CODES]...\n" + + "Example: " + COMMAND_WORD + " CS2100 CS2103T"; + + private static final Logger logger = Logger.getLogger(FindModCommand.class.getName()); + + private final ModContainsKeywordsPredicate predicate; + /** + * Constructs a {@code FindModCommand} with the specified {@code ModContainsKeywordsPredicate}. + * This constructor initializes the command with the provided predicate and logs the creation of the command. + * + * @param predicate The predicate to filter persons by module enrollment. + */ + public FindModCommand(ModContainsKeywordsPredicate predicate) { + this.predicate = predicate; + logger.info("FindModCommand created with predicate: " + predicate); + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + logger.info("Executing FindModCommand with predicate: " + predicate); + + model.updateFilteredPersonList(predicate); + String resultMessage; + + if (model.getFilteredPersonList().size() == 1) { + resultMessage = String.format(Messages.MESSAGE_PERSON_LISTED_OVERVIEW, + model.getFilteredPersonList().size()); + } else { + resultMessage = String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, + model.getFilteredPersonList().size()); + } + + + logger.info("Command executed successfully, " + model.getFilteredPersonList().size() + " persons found."); + + return new CommandResult(resultMessage); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof FindModCommand)) { + return false; + } + + FindModCommand otherFindCommand = (FindModCommand) other; + boolean isEqual = predicate.equals(otherFindCommand.predicate); + logger.fine("Checking equality between FindModCommand objects: " + isEqual); + + return isEqual; + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("predicate", predicate) + .toString(); + } +} + diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 4ff1a97ed77..cf31577c1c2 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -1,51 +1,80 @@ package seedu.address.logic.parser; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_HOUSING; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LINK; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MAJOR; 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_YEAR; 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.Housing; +import seedu.address.model.person.Link; +import seedu.address.model.person.Major; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Year; 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. + * 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); + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, + PREFIX_EMAIL, PREFIX_YEAR, PREFIX_MAJOR, PREFIX_HOUSING, PREFIX_LINK, + PREFIX_TAG); - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) + // Checks whether mandatory fields are present + if (!arePrefixesPresent(argMultimap, PREFIX_NAME) || !argMultimap.getPreamble().isEmpty()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AddCommand.MESSAGE_USAGE)); } - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_HOUSING, PREFIX_YEAR, PREFIX_MAJOR, PREFIX_LINK); 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()); + Phone phone = argMultimap.getValue(PREFIX_PHONE).isPresent() + ? ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()) + : null; + Email email = argMultimap.getValue(PREFIX_EMAIL).isPresent() + ? ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()) + : null; + Year year = argMultimap.getValue(PREFIX_YEAR).isPresent() + ? ParserUtil.parseYear(argMultimap.getValue(PREFIX_YEAR).get()) + : null; + Major major = argMultimap.getValue(PREFIX_MAJOR).isPresent() + ? ParserUtil.parseMajor(argMultimap.getValue(PREFIX_MAJOR).get()) + : null; + Housing housing = argMultimap.getValue(PREFIX_HOUSING).isPresent() + ? ParserUtil.parseHousing(argMultimap.getValue(PREFIX_HOUSING).get()) + : null; + Link link = argMultimap.getValue(PREFIX_LINK).isPresent() + ? ParserUtil.parseLink(argMultimap.getValue(PREFIX_LINK).get()) + : null; Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - Person person = new Person(name, phone, email, address, tagList); + Person person = new Person(name, phone, email, year, major, housing, link, tagList); return new AddCommand(person); } @@ -54,8 +83,9 @@ public AddCommand parse(String args) throws ParseException { * 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()); + 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 3149ee07e0b..bcdd426b7e4 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -15,6 +15,7 @@ import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.ExitCommand; import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.FindModCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.ListCommand; import seedu.address.logic.parser.exceptions.ParseException; @@ -68,6 +69,9 @@ public Command parseCommand(String userInput) throws ParseException { case FindCommand.COMMAND_WORD: return new FindCommandParser().parse(arguments); + case FindModCommand.COMMAND_WORD: + return new FindModCommandParser().parse(arguments); + case ListCommand.COMMAND_WORD: return new ListCommand(); diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..8feb8705e1d 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -9,7 +9,10 @@ public class CliSyntax { public static final Prefix PREFIX_NAME = new Prefix("n/"); public static final Prefix PREFIX_PHONE = new Prefix("p/"); public static final Prefix PREFIX_EMAIL = new Prefix("e/"); - public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); + public static final Prefix PREFIX_YEAR = new Prefix("y/"); + public static final Prefix PREFIX_MAJOR = new Prefix("m/"); + public static final Prefix PREFIX_HOUSING = new Prefix("h/"); public static final Prefix PREFIX_TAG = new Prefix("t/"); + public static final Prefix PREFIX_LINK = new Prefix("l/"); } diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 46b3309a78b..0b08820e3dc 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -2,11 +2,14 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_HOUSING; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LINK; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MAJOR; 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_YEAR; import java.util.Collection; import java.util.Collections; @@ -32,7 +35,8 @@ public class EditCommandParser implements Parser { public EditCommand parse(String args) throws ParseException { requireNonNull(args); ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_YEAR, PREFIX_MAJOR, PREFIX_HOUSING, PREFIX_LINK, PREFIX_TAG); Index index; @@ -42,7 +46,8 @@ public EditCommand parse(String args) throws ParseException { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); } - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_HOUSING, PREFIX_YEAR, PREFIX_MAJOR, PREFIX_LINK); EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); @@ -55,8 +60,17 @@ public EditCommand parse(String args) throws ParseException { 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())); + if (argMultimap.getValue(PREFIX_HOUSING).isPresent()) { + editPersonDescriptor.setHousing(ParserUtil.parseHousing(argMultimap.getValue(PREFIX_HOUSING).get())); + } + if (argMultimap.getValue(PREFIX_YEAR).isPresent()) { + editPersonDescriptor.setYear(ParserUtil.parseYear(argMultimap.getValue(PREFIX_YEAR).get())); + } + if (argMultimap.getValue(PREFIX_MAJOR).isPresent()) { + editPersonDescriptor.setMajor(ParserUtil.parseMajor(argMultimap.getValue(PREFIX_MAJOR).get())); + } + if (argMultimap.getValue(PREFIX_LINK).isPresent()) { + editPersonDescriptor.setLink(ParserUtil.parseLink(argMultimap.getValue(PREFIX_LINK).get())); } parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); diff --git a/src/main/java/seedu/address/logic/parser/FindModCommandParser.java b/src/main/java/seedu/address/logic/parser/FindModCommandParser.java new file mode 100644 index 00000000000..753d1a52629 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FindModCommandParser.java @@ -0,0 +1,60 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.model.mod.ModuleCode.VALIDATION_REGEX; + +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; + +import seedu.address.logic.commands.FindModCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.ModContainsKeywordsPredicate; + +/** + * Parses input arguments and creates a new FindModCommand object. + */ +public class FindModCommandParser implements Parser { + + private static final Logger logger = Logger.getLogger(FindModCommandParser.class.getName()); + + /** + * Parses the given {@code String} of arguments in the context of the FindModCommand + * and returns a FindModCommand object for execution. + * + * @param args The input arguments provided by the user. + * @return A new FindModCommand object containing the parsed module keywords. + * @throws ParseException if the user input does not conform to the expected format. + */ + public FindModCommand parse(String args) throws ParseException { + logger.info("Received input arguments: " + args); + + String trimmedArgs = args.trim(); + logger.info("Trimmed input arguments: '" + trimmedArgs + "'"); + + if (trimmedArgs.isEmpty()) { + logger.warning("Empty arguments detected, throwing ParseException."); + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindModCommand.MESSAGE_USAGE)); + } + + String[] nameKeywords = trimmedArgs.split("\\s+"); + logger.info("Split keywords: " + Arrays.toString(nameKeywords)); + + for (String keyword : nameKeywords) { + if (!keyword.matches(VALIDATION_REGEX)) { + logger.log(Level.SEVERE, "Invalid module format detected: {0}", keyword); + throw new ParseException( + String.format("ModuleCode format invalid!\n" + MESSAGE_INVALID_COMMAND_FORMAT, + FindModCommand.MESSAGE_USAGE)); + } + } + + logger.info("All keywords are valid."); + + FindModCommand command = new FindModCommand(new ModContainsKeywordsPredicate(Arrays.asList(nameKeywords))); + logger.info("Successfully created FindModCommand with predicate: " + command); + + return command; + } +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..9d82420e20e 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -9,10 +9,13 @@ import seedu.address.commons.core.index.Index; import seedu.address.commons.util.StringUtil; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; import seedu.address.model.person.Email; +import seedu.address.model.person.Housing; +import seedu.address.model.person.Link; +import seedu.address.model.person.Major; import seedu.address.model.person.Name; import seedu.address.model.person.Phone; +import seedu.address.model.person.Year; import seedu.address.model.tag.Tag; /** @@ -66,18 +69,18 @@ public static Phone parsePhone(String phone) throws ParseException { } /** - * Parses a {@code String address} into an {@code Address}. + * Parses {@code String housing} information into a {@code Housing}. * Leading and trailing whitespaces will be trimmed. * - * @throws ParseException if the given {@code address} is invalid. + * @throws ParseException if the given {@code housing} is invalid. */ - public static Address parseAddress(String address) throws ParseException { - requireNonNull(address); - String trimmedAddress = address.trim(); - if (!Address.isValidAddress(trimmedAddress)) { - throw new ParseException(Address.MESSAGE_CONSTRAINTS); + public static Housing parseHousing(String housing) throws ParseException { + requireNonNull(housing); + String trimmedHousing = housing.trim(); + if (!Housing.isValidHousing(trimmedHousing)) { + throw new ParseException(Housing.MESSAGE_CONSTRAINTS); } - return new Address(trimmedAddress); + return new Housing(trimmedHousing); } /** @@ -95,6 +98,55 @@ public static Email parseEmail(String email) throws ParseException { return new Email(trimmedEmail); } + /** + * Parses a {@code String year} into a {@code Year}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code year} is invalid. + */ + public static Year parseYear(String year) throws ParseException { + requireNonNull(year); + String trimmedYear = year.trim(); + if (!Year.isValidYear(trimmedYear)) { + throw new ParseException(Year.MESSAGE_CONSTRAINTS); + } + try { + return Year.fromString(year); + } catch (IllegalArgumentException e) { + throw new ParseException(Year.MESSAGE_CONSTRAINTS); + } + } + + /** + * Parses a {@code String major} into a {@code Major}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code major} is invalid. + */ + public static Major parseMajor(String major) throws ParseException { + requireNonNull(major); + String trimmedMajor = major.trim(); + if (!Major.isValidMajor(trimmedMajor)) { + throw new ParseException(Major.MESSAGE_CONSTRAINTS); + } + return new Major(trimmedMajor); + } + + /** + * Parses a {@code String link} into a {@code Link}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code link} is invalid. + */ + public static Link parseLink(String link) throws ParseException { + requireNonNull(link); + String trimmedLink = link.trim(); + if (!Link.isValidLink(trimmedLink)) { + throw new ParseException(Link.MESSAGE_CONSTRAINTS); + } + return new Link(trimmedLink); + } + /** * Parses a {@code String tag} into a {@code Tag}. * Leading and trailing whitespaces will be trimmed. diff --git a/src/main/java/seedu/address/model/mod/ModuleCode.java b/src/main/java/seedu/address/model/mod/ModuleCode.java new file mode 100644 index 00000000000..1f081ff2a28 --- /dev/null +++ b/src/main/java/seedu/address/model/mod/ModuleCode.java @@ -0,0 +1,69 @@ +package seedu.address.model.mod; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents the Module Code of an NUS course + * Guarantees: immutable; is valid as declared in + * {@link #isValidModuleCode(String)} + */ +public class ModuleCode { + public static final String MESSAGE_CONSTRAINTS = "Module Code should be of the format prefix-code-suffix, where:\n" + + "1. The prefix has a length of 2-4 composed of capitalized letters.\n" + + "2. The code is a 4-digit number.\n" + + "3. The suffix has a length of 0-5, which can be composed of capitalized letters and/or numbers.\n"; + + /* + * The module code must start with a 2-4 letter prefix, + * followed by a 4-digit number, and may be followed with a 1-2 letter suffix. + * All letters must be capitalized + * + */ + public static final String VALIDATION_REGEX = "[A-Za-z]{2,4}\\d{4}[A-Za-z0-9]{0,5}(?:\\(TA\\))?"; + + public final String value; + + /** + * Constructs a {@code ModuleCode}. + * + * @param moduleCode A valid module code. + */ + public ModuleCode(String moduleCode) { + requireNonNull(moduleCode); + checkArgument(isValidModuleCode(moduleCode), MESSAGE_CONSTRAINTS); + this.value = moduleCode; + } + + /** + * Returns if a given string is a valid module code. + */ + public static boolean isValidModuleCode(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public String toString() { + return this.value; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ModuleCode)) { + return false; + } + + ModuleCode otherModuleCode = (ModuleCode) other; + return this.value.equals(otherModuleCode.value); + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Housing.java similarity index 55% rename from src/main/java/seedu/address/model/person/Address.java rename to src/main/java/seedu/address/model/person/Housing.java index 469a2cc9a1e..70dc7057558 100644 --- a/src/main/java/seedu/address/model/person/Address.java +++ b/src/main/java/seedu/address/model/person/Housing.java @@ -4,15 +4,16 @@ import static seedu.address.commons.util.AppUtil.checkArgument; /** - * Represents a Person's address in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} + * Represents a Person's housing in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidHousing(String)} */ -public class Address { +public class Housing { - public static final String MESSAGE_CONSTRAINTS = "Addresses can take any values, and it should not be blank"; + public static final String MESSAGE_CONSTRAINTS = + "Housing can take any values, and it should not be blank"; /* - * The first character of the address must not be a whitespace, + * The first character of the housing information must not be a whitespace, * otherwise " " (a blank string) becomes a valid input. */ public static final String VALIDATION_REGEX = "[^\\s].*"; @@ -20,20 +21,20 @@ public class Address { public final String value; /** - * Constructs an {@code Address}. + * Constructs an {@code Housing}. * - * @param address A valid address. + * @param housing Valid housing information. */ - public Address(String address) { - requireNonNull(address); - checkArgument(isValidAddress(address), MESSAGE_CONSTRAINTS); - value = address; + public Housing(String housing) { + requireNonNull(housing); + checkArgument(isValidHousing(housing), MESSAGE_CONSTRAINTS); + value = housing; } /** * Returns true if a given string is a valid email. */ - public static boolean isValidAddress(String test) { + public static boolean isValidHousing(String test) { return test.matches(VALIDATION_REGEX); } @@ -49,12 +50,12 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof Address)) { + if (!(other instanceof Housing)) { return false; } - Address otherAddress = (Address) other; - return value.equals(otherAddress.value); + Housing otherHousing = (Housing) other; + return value.equals(otherHousing.value); } @Override diff --git a/src/main/java/seedu/address/model/person/Link.java b/src/main/java/seedu/address/model/person/Link.java new file mode 100644 index 00000000000..b8de74063b2 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Link.java @@ -0,0 +1,144 @@ +package seedu.address.model.person; +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Pattern; + +import seedu.address.model.mod.ModuleCode; + +/** + * Represents a Person's NUSMods link in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidLink(String)} + */ +public class Link { + public static final String MESSAGE_CONSTRAINTS = "Link needs to be a valid NUSMods timetable link, e.g., " + + "https://nusmods.com/timetable/sem-1/share?CS1010=TUT:06,LAB:E07"; + public static final String TA_EXAMPLE = "https://nusmods.com/timetable/sem-2/share?CS2101=&CS2103T=" + + "LEC:G12&CS3230=LEC:1,TUT:10&MA3211=LEC:1,TUT:2&ta=CS3230(TUT:10)"; + private static final Pattern PATH_PATTERN = Pattern.compile("^/timetable/(sem-(?:1|2)|st-(?:i{1,2}))/share$"); + public final String value; + + /** + * Constructs an {@code Link}. + * + * @param link A valid NUSMods timetable link. + */ + public Link(String link) { + requireNonNull(link); + checkArgument(isValidLink(link), MESSAGE_CONSTRAINTS); + value = link; + } + + /** + * Extract Module Codes from a given link. + */ + public static Set extractCodes(String link) { + Set codes = new HashSet<>(); + int queryStart = link.indexOf('?'); + if (queryStart == -1 || queryStart == link.length() - 1) { + return codes; // no query parameters + } + + String queryString = link.substring(queryStart + 1); + String[] pairs = queryString.split("&"); + for (String pair : pairs) { + int equalPos = pair.indexOf('='); + if (equalPos != -1) { + String key = pair.substring(0, equalPos); + String value = pair.substring(equalPos + 1); + if ("ta".equals(key)) { + // Process the "ta" parameter: split by comma to get individual module codes + String[] taModules = value.split("\\),"); + for (String taModule : taModules) { + int parenIndex = taModule.indexOf('('); + String code = taModule.substring(0, parenIndex); + if (parenIndex != -1) { + // Extract module code before '(' + codes.remove(code); // remove the module code without the suffix + codes.add(code + "(TA)"); // add the module code with the suffix + } else { + codes.add(taModule); + } + } + } else if ("hidden".equals(key)) { + continue; // skip hidden modules + } else { + codes.add(key); + } + } else { + codes.add(pair); // handles case with no '=' + } + } + return codes; + } + + + /** + * Returns if a given string is a valid NUSMods timetable link. + */ + public static boolean isValidLink(String test) { + try { + URI uri = new URI(test); + // Validate scheme and host. + if (!"https".equals(uri.getScheme()) + || !"nusmods.com".equals(uri.getHost())) { + return false; + } + // Validate path against the expected pattern. + String path = uri.getPath(); + if (path == null || !PATH_PATTERN.matcher(path).matches()) { + return false; + } + String query = uri.getQuery(); + if (query != null && !query.trim().isEmpty()) { + // Validate query parameters: each should be a key=value pair (value may be + // empty). + String[] pairs = query.split("&"); + for (String pair : pairs) { + if (pair.isEmpty()) { + return false; + } + String[] keyValue = pair.split("=", 2); + if (keyValue[0].trim().isEmpty()) { + return false; + } + } + } + } catch (URISyntaxException e) { + return false; + } + Set codes = extractCodes(test); + for (String code : codes) { + if (!ModuleCode.isValidModuleCode(code)) { + return false; + } + } + return true; + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof Link)) { + return false; + } + Link otherLink = (Link) other; + return value.equals(otherLink.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/person/Major.java b/src/main/java/seedu/address/model/person/Major.java new file mode 100644 index 00000000000..8aaec9ada43 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Major.java @@ -0,0 +1,65 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Person's major in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidMajor(String)} + */ +public class Major { + + public static final String MESSAGE_CONSTRAINTS = "Majors can take any values, and it should not be blank"; + + /* + * The first character of the major 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 a {@code Major}. + * + * @param major A valid major. + */ + public Major(String major) { + requireNonNull(major); + checkArgument(isValidMajor(major), MESSAGE_CONSTRAINTS); + value = major; + } + + /** + * Returns true if a given string is a valid major. + */ + public static boolean isValidMajor(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Major)) { + return false; + } + + Major otherMajor = (Major) other; + return this.value.equals(otherMajor.value); + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/person/ModContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/ModContainsKeywordsPredicate.java new file mode 100644 index 00000000000..d09d9d8686b --- /dev/null +++ b/src/main/java/seedu/address/model/person/ModContainsKeywordsPredicate.java @@ -0,0 +1,52 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; +import seedu.address.commons.util.ToStringBuilder; + +/** + * Tests that a {@code Person}'s {@code Mod} matches any of the keywords given. + */ +public class ModContainsKeywordsPredicate implements Predicate { + private final List keywords; + + /** + * Constructs a {@code ModContainsKeywordsPredicate} with the given keywords. + * + * @param keywords The list of module keywords to filter persons by. + */ + public ModContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Person person) { + return person.getModules().stream() + .anyMatch(mod -> keywords.stream() + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase( + mod.value, keyword)) + ); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ModContainsKeywordsPredicate)) { + return false; + } + + ModContainsKeywordsPredicate otherNameContainsKeywordsPredicate = (ModContainsKeywordsPredicate) other; + return keywords.equals(otherNameContainsKeywordsPredicate.keywords); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("keywords", keywords).toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index abe8c46b535..0a470bbecfb 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -8,11 +8,12 @@ import java.util.Set; import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.mod.ModuleCode; 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. + * Represents a Person in the address book. Guarantees: details are present and not null, field + * values are validated, immutable. */ public class Person { @@ -22,19 +23,29 @@ public class Person { private final Email email; // Data fields - private final Address address; + private final Year year; + private final Major major; + private final Housing housing; private final Set tags = new HashSet<>(); + private final Link link; + /** - * Every field must be present and not null. + * Every field must be present and not null. Constructs a person based on link. */ - public Person(Name name, Phone phone, Email email, Address address, Set tags) { - requireAllNonNull(name, phone, email, address, tags); + public Person(Name name, Phone phone, Email email, Year year, Major major, Housing housing, + Link link, Set tags) { + requireAllNonNull(name); this.name = name; this.phone = phone; this.email = email; - this.address = address; - this.tags.addAll(tags); + this.year = year; + this.major = major; + this.housing = housing; + this.link = link; + if (tags != null) { + this.tags.addAll(tags); + } } public Name getName() { @@ -49,21 +60,49 @@ public Email getEmail() { return email; } - public Address getAddress() { - return address; + public Year getYear() { + return year; + } + + public Major getMajor() { + return major; + } + + public Housing getHousing() { + return housing; + } + + public Link getLink() { + return link; } /** - * Returns an immutable tag set, which throws {@code UnsupportedOperationException} - * if modification is attempted. + * 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. + * Returns an immutable module set, which throws {@code UnsupportedOperationException} if + * modification is attempted. + */ + public Set getModules() { + if (link == null) { + return new HashSet(); + } + Set stringModules = Link.extractCodes(link.value); + Set modules = new HashSet(); + for (String module : stringModules) { + modules.add(new ModuleCode(module)); + } + return modules; + } + + /** + * 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) { @@ -75,8 +114,8 @@ public boolean isSamePerson(Person otherPerson) { } /** - * Returns true if both persons have the same identity and data fields. - * This defines a stronger notion of equality between two persons. + * 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) { @@ -93,14 +132,17 @@ public boolean equals(Object other) { return name.equals(otherPerson.name) && phone.equals(otherPerson.phone) && email.equals(otherPerson.email) - && address.equals(otherPerson.address) + && year.equals(otherPerson.year) + && major.equals(otherPerson.major) + && housing.equals(otherPerson.housing) + && link.equals(otherPerson.link) && tags.equals(otherPerson.tags); } @Override public int hashCode() { // use this method for custom fields hashing instead of implementing your own - return Objects.hash(name, phone, email, address, tags); + return Objects.hash(name, phone, email, year, major, housing, link, tags); } @Override @@ -109,8 +151,12 @@ public String toString() { .add("name", name) .add("phone", phone) .add("email", email) - .add("address", address) + .add("year", year) + .add("major", major) + .add("housing", housing) .add("tags", tags) + .add("modules", this.getModules()) + .add("link", link) .toString(); } diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java index d733f63d739..d0fbfccda2d 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/seedu/address/model/person/Phone.java @@ -9,10 +9,9 @@ */ public class Phone { - public static final String MESSAGE_CONSTRAINTS = - "Phone numbers should only contain numbers, and it should be at least 3 digits long"; - public static final String VALIDATION_REGEX = "\\d{3,}"; + "Phone numbers should be Singaporean. It can only contain numbers, and should be exactly 8 digits long"; + public static final String VALIDATION_REGEX = "\\d{8}"; public final String value; /** diff --git a/src/main/java/seedu/address/model/person/Year.java b/src/main/java/seedu/address/model/person/Year.java new file mode 100644 index 00000000000..c7e61f72dfc --- /dev/null +++ b/src/main/java/seedu/address/model/person/Year.java @@ -0,0 +1,49 @@ +package seedu.address.model.person; + +/** + * Represents the year of study of an NUS student. + * Guarantees: immutable; is valid as declared in {@link #isValidYear(String)} + */ +public enum Year { + YEAR_1(1), YEAR_2(2), YEAR_3(3), YEAR_4(4), YEAR_5(5), YEAR_6(6); + + public static final String MESSAGE_CONSTRAINTS = "Year should be 1, 2, 3, 4, 5, or 6."; + public static final String VALIDATION_REGEX = "[1-6]"; + public final int value; + + Year(int year) { + this.value = year; + } + + /** + * Returns true if a given string is a valid year. + */ + public static boolean isValidYear(String test) { + return test.matches(VALIDATION_REGEX); + } + + /** + * Converts a user input string into a corresponding Year + * @return The corresponding Year from 1-6 + * @throws IllegalArgumentException if year is invalid + */ + public static Year fromString(String year) { + if (!isValidYear(year)) { + throw new IllegalArgumentException(MESSAGE_CONSTRAINTS); + } + switch (Integer.parseInt(year)) { + case 1: return YEAR_1; + case 2: return YEAR_2; + case 3: return YEAR_3; + case 4: return YEAR_4; + case 5: return YEAR_5; + case 6: return YEAR_6; + default: throw new IllegalArgumentException(MESSAGE_CONSTRAINTS); + } + } + + @Override + public String toString() { + return "Year " + value; + } +} diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facf..274f2d4e879 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -6,11 +6,14 @@ 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.Housing; +import seedu.address.model.person.Link; +import seedu.address.model.person.Major; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Year; import seedu.address.model.tag.Tag; /** @@ -19,24 +22,26 @@ public class SampleDataUtil { public static Person[] getSamplePersons() { return new Person[] { - new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), - new Address("Blk 30 Geylang Street 29, #06-40"), + new Person(new Name("Abi"), new Phone("87438807"), new Email("abihalim@example.com"), + Year.YEAR_2, new Major("Computer Science"), + new Housing("Blk 30 Geylang Street 29, #06-40"), + new Link("https://nusmods.com/timetable/sem-2/share?CS2101=&CS2103T=LEC:G12"), getTagSet("friends")), - new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), - new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), + new Person(new Name("Yuexi"), new Phone("99272758"), new Email("yuexi@example.com"), + Year.YEAR_2, new Major("Computer Science"), + new Housing("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), + new Link("https://nusmods.com/timetable/sem-2/share?CS2101=&CS2103T=LEC:G12"), 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"), + new Person(new Name("Shashwat"), new Phone("93210283"), new Email("shashwat@example.com"), + Year.YEAR_4, new Major("Computer Science"), + new Housing("Blk 11 Ang Mo Kio Street 74, #11-04"), + new Link("https://nusmods.com/timetable/sem-2/share?CS2101=&CS2103T=LEC:G12"), 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"), + new Person(new Name("Huazhi"), new Phone("91031282"), new Email("huazhi@example.com"), + Year.YEAR_2, new Major("Computer Science"), + new Housing("Blk 436 Serangoon Gardens Street 26, #16-43"), + new Link("https://nusmods.com/timetable/sem-2/share?CS2101=&CS2103T=LEC:G12"), getTagSet("family")), - new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), - new Address("Blk 47 Tampines Street 20, #17-35"), - getTagSet("classmates")), - new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), - new Address("Blk 45 Aljunied Street 85, #11-31"), - getTagSet("colleagues")) }; } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java index bd1ca0f56c8..7ab9092b657 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java @@ -10,11 +10,14 @@ 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.Housing; +import seedu.address.model.person.Link; +import seedu.address.model.person.Major; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Year; import seedu.address.model.tag.Tag; /** @@ -27,7 +30,10 @@ class JsonAdaptedPerson { private final String name; private final String phone; private final String email; - private final String address; + private final String year; + private final String major; + private final String housing; + private final String link; private final List tags = new ArrayList<>(); /** @@ -35,12 +41,16 @@ class JsonAdaptedPerson { */ @JsonCreator public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, - @JsonProperty("email") String email, @JsonProperty("address") String address, - @JsonProperty("tags") List tags) { + @JsonProperty("email") String email, @JsonProperty("year") String year, + @JsonProperty("major") String major, @JsonProperty("housing") String housing, + @JsonProperty("link") String link, @JsonProperty("tags") List tags) { this.name = name; this.phone = phone; this.email = email; - this.address = address; + this.year = year; + this.major = major; + this.housing = housing; + this.link = link; if (tags != null) { this.tags.addAll(tags); } @@ -51,9 +61,12 @@ public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone */ public JsonAdaptedPerson(Person source) { name = source.getName().fullName; - phone = source.getPhone().value; - email = source.getEmail().value; - address = source.getAddress().value; + phone = source.getPhone() != null ? source.getPhone().value : null; + email = source.getEmail() != null ? source.getEmail().value : null; + year = source.getYear() != null ? String.valueOf(source.getYear().value) : null; + major = source.getMajor() != null ? source.getMajor().value : null; + housing = source.getHousing() != null ? source.getHousing().value : null; + link = source.getLink() != null ? source.getLink().value : null; tags.addAll(source.getTags().stream() .map(JsonAdaptedTag::new) .collect(Collectors.toList())); @@ -62,7 +75,8 @@ public JsonAdaptedPerson(Person source) { /** * 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. + * @throws IllegalValueException if there were any data constraints violated in the adapted + * person. */ public Person toModelType() throws IllegalValueException { final List personTags = new ArrayList<>(); @@ -71,39 +85,71 @@ public Person toModelType() throws IllegalValueException { } if (name == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); + 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); + Phone modelPhone; if (phone == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName())); - } - if (!Phone.isValidPhone(phone)) { + modelPhone = null; + } else if (!Phone.isValidPhone(phone)) { throw new IllegalValueException(Phone.MESSAGE_CONSTRAINTS); + } else { + modelPhone = new Phone(phone); } - final Phone modelPhone = new Phone(phone); + Email modelEmail; if (email == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName())); - } - if (!Email.isValidEmail(email)) { + modelEmail = null; + } else if (!Email.isValidEmail(email)) { throw new IllegalValueException(Email.MESSAGE_CONSTRAINTS); + } else { + modelEmail = new Email(email); + } + + Year modelYear; + if (year == null) { + modelYear = null; + } else if (!Year.isValidYear(year)) { + throw new IllegalValueException(Year.MESSAGE_CONSTRAINTS); + } else { + modelYear = Year.fromString(year); } - final Email modelEmail = new Email(email); - if (address == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName())); + Link modelLink; + if (link == null) { + modelLink = null; + } else if (!Link.isValidLink(link)) { + throw new IllegalValueException(Link.MESSAGE_CONSTRAINTS); + } else { + modelLink = new Link(link); } - if (!Address.isValidAddress(address)) { - throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); + + Major modelMajor; + if (major == null) { + modelMajor = null; + } else if (!Major.isValidMajor(major)) { + throw new IllegalValueException(Major.MESSAGE_CONSTRAINTS); + } else { + modelMajor = new Major(major); + } + + Housing modelHousing; + if (housing == null) { + modelHousing = null; + } else if (!Housing.isValidHousing(housing)) { + throw new IllegalValueException(Housing.MESSAGE_CONSTRAINTS); + } else { + modelHousing = new Housing(housing); } - final Address modelAddress = new Address(address); final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); + return new Person(modelName, modelPhone, modelEmail, modelYear, modelMajor, modelHousing, + modelLink, modelTags); } } diff --git a/src/main/java/seedu/address/ui/Clipboard.java b/src/main/java/seedu/address/ui/Clipboard.java new file mode 100644 index 00000000000..64497219133 --- /dev/null +++ b/src/main/java/seedu/address/ui/Clipboard.java @@ -0,0 +1,22 @@ +package seedu.address.ui; + +import javafx.scene.input.ClipboardContent; + +/** + * Utility class for clipboard operations. + */ +public class Clipboard { + + /** + * Copies the given text to the system clipboard. + * + * @param text The text to copy. + */ + public static void copyToClipboard(String text) { + javafx.scene.input.Clipboard clipboard = javafx.scene.input.Clipboard.getSystemClipboard(); + ClipboardContent content = new ClipboardContent(); + content.putString(text); + clipboard.setContent(content); + } +} + diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 3f16b2fcf26..50d6fff468b 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/address/ui/HelpWindow.java @@ -15,7 +15,7 @@ */ public class HelpWindow extends UiPart { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; + public static final String USERGUIDE_URL = "https://ay2425s2-cs2103t-t11-1.github.io/tp/UserGuide.html"; public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); @@ -90,7 +90,7 @@ public void focus() { } /** - * Copies the URL to the user guide to the clipboard. + * Copies the URL of the user guide to the clipboard. */ @FXML private void copyUrl() { diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index 094c42cda82..5c6229bcd4a 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -3,6 +3,7 @@ import java.util.Comparator; import javafx.fxml.FXML; +import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; @@ -35,11 +36,19 @@ public class PersonCard extends UiPart { @FXML private Label phone; @FXML - private Label address; + private Label year; + @FXML + private Label major; + @FXML + private Label housing; @FXML private Label email; @FXML private FlowPane tags; + @FXML + private FlowPane modules; + @FXML + private Hyperlink link; /** * Creates a {@code PersonCode} with the given {@code Person} and index to display. @@ -49,11 +58,42 @@ public PersonCard(Person person, int displayedIndex) { 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); + + // For non-mandatory fields don't show if is null. + // Managed: https://stackoverflow.com/a/28559958 + phone.setText(person.getPhone() != null ? person.getPhone().value : ""); + phone.setVisible(person.getPhone() != null); + phone.setManaged(person.getPhone() != null); + + year.setText(person.getYear() != null ? String.valueOf(person.getYear()) : ""); + year.setVisible(person.getYear() != null); + year.setManaged(person.getYear() != null); + + major.setText(person.getMajor() != null ? person.getMajor().value : ""); + major.setVisible(person.getMajor() != null); + major.setManaged(person.getMajor() != null); + + housing.setText(person.getHousing() != null ? person.getHousing().value : ""); + housing.setVisible(person.getHousing() != null); + housing.setManaged(person.getHousing() != null); + + email.setText(person.getEmail() != null ? person.getEmail().value : ""); + email.setVisible(person.getEmail() != null); + email.setManaged(person.getEmail() != null); + person.getTags().stream() .sorted(Comparator.comparing(tag -> tag.tagName)) .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + person.getModules().stream() + .sorted(Comparator.comparing(module -> module.value)) + .forEach(module -> modules.getChildren().add(new Label(module.value))); + if (person.getLink() == null) { + link.setDisable(true); + } else { + link.setOnAction(event -> { + Clipboard.copyToClipboard(person.getLink().value); + PopupMessage.showMessage(cardPane, "Link copied!"); + }); + } } } diff --git a/src/main/java/seedu/address/ui/PopupMessage.java b/src/main/java/seedu/address/ui/PopupMessage.java new file mode 100644 index 00000000000..abef1bdb57d --- /dev/null +++ b/src/main/java/seedu/address/ui/PopupMessage.java @@ -0,0 +1,54 @@ +package seedu.address.ui; + +import javafx.animation.FadeTransition; +import javafx.application.Platform; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; +import javafx.scene.paint.Color; +import javafx.scene.text.Font; +import javafx.stage.Popup; +import javafx.util.Duration; + +/** + * Utility class for showing a small disappearing message message. + */ +public class PopupMessage { + + /** + * Displays a temporary message that disappears after a few seconds. + * + * @param parent The parent Region (HBox, VBox, StackPane) to show the message in. + * @param message The message to display. + */ + public static void showMessage(Region parent, String message) { + Platform.runLater(() -> { + Label messageLabel = new Label(message); + messageLabel.setFont(new Font(14)); + messageLabel.setTextFill(Color.WHITE); + messageLabel.setStyle( + "-fx-background-color: rgba(0, 0, 0, 0.75); " + "-fx-padding: 10px; -fx-background-radius: 5;"); + + Popup popup = new Popup(); + popup.getContent().add(messageLabel); + popup.setAutoHide(true); + + // Position at the bottom center of the window + double centerX = parent.getScene().getWindow().getX() + parent.getScene().getWidth() / 2 + - messageLabel.getWidth() / 2; + double centerY = parent.getScene().getWindow().getY() + parent.getScene().getHeight() - 50; + popup.setX(centerX); + popup.setY(centerY); + + // Fade out effect + FadeTransition fadeOut = new FadeTransition(Duration.seconds(2), messageLabel); + fadeOut.setFromValue(1.0); + fadeOut.setToValue(0.0); + fadeOut.setOnFinished(e -> popup.hide()); + + popup.show(parent.getScene().getWindow()); + fadeOut.play(); + }); + } +} + + diff --git a/src/main/resources/images/NUSMODS.png b/src/main/resources/images/NUSMODS.png new file mode 100644 index 00000000000..bf4e381e992 Binary files /dev/null and b/src/main/resources/images/NUSMODS.png differ diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8..a84beee86a6 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -1,6 +1,5 @@ .background { - -fx-background-color: derive(#1d1d1d, 20%); - background-color: #383838; /* Used in the default.html file */ + -fx-background-color: #222324; /* Used in the default.html file */ } .label { @@ -13,14 +12,14 @@ .label-bright { -fx-font-size: 11pt; -fx-font-family: "Segoe UI Semibold"; - -fx-text-fill: white; + -fx-text-fill: #aaaaaa; -fx-opacity: 1; } .label-header { -fx-font-size: 32pt; -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; + -fx-text-fill: #aaaaaa; -fx-opacity: 1; } @@ -40,9 +39,9 @@ } .table-view { - -fx-base: #1d1d1d; - -fx-control-inner-background: #1d1d1d; - -fx-background-color: #1d1d1d; + -fx-base: #222324; + -fx-control-inner-background: #222324; + -fx-background-color: #222324; -fx-table-cell-border-color: transparent; -fx-table-header-border-color: transparent; -fx-padding: 5; @@ -67,7 +66,7 @@ .table-view .column-header .label { -fx-font-size: 20pt; -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; + -fx-text-fill: #aaaaaa; -fx-alignment: center-left; -fx-opacity: 1; } @@ -77,34 +76,38 @@ } .split-pane:horizontal .split-pane-divider { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #222324; -fx-border-color: transparent transparent transparent #4d4d4d; } .split-pane { -fx-border-radius: 1; -fx-border-width: 1; - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #222324; } .list-view { -fx-background-insets: 0; -fx-padding: 0; - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #222324; } .list-cell { -fx-label-padding: 0 0 0 0; -fx-graphic-text-gap : 0; -fx-padding: 0 0 0 0; + -fx-border-color: #474747; + -fx-border-width: 1; + -fx-border-radius: 5; + -fx-background-radius: 5; } .list-cell:filled:even { - -fx-background-color: #3c3e3f; + -fx-background-color: #282929; } .list-cell:filled:odd { - -fx-background-color: #515658; + -fx-background-color: #222324; } .list-cell:filled:selected { @@ -114,10 +117,11 @@ .list-cell:filled:selected #cardPane { -fx-border-color: #3e7b91; -fx-border-width: 1; + -fx-border-radius: 5; } .list-cell .label { - -fx-text-fill: white; + -fx-text-fill: #aaaaaa; } .cell_big_label { @@ -132,25 +136,41 @@ -fx-text-fill: #010504; } +.cell_middle_label { + -fx-font-family: "Segoe UI"; + -fx-font-size: 13px; + -fx-text-fill: rgb(130, 180, 240); + -fx-padding: 0 0 0 0; /* No extra padding */ + -fx-alignment: center-left; /* Aligns text properly */ +} + +.hyperlink { + -fx-font-size: 13px; + -fx-font-family: "Segoe UI"; + -fx-text-fill: rgb(130, 180, 240); + -fx-background-color: transparent; + -fx-padding: 0; /* Make sure no extra padding is affecting alignment */ +} + .stack-pane { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #222324; } .pane-with-border { - -fx-background-color: derive(#1d1d1d, 20%); - -fx-border-color: derive(#1d1d1d, 10%); + -fx-background-color: #222324; + -fx-border-color: derive(#222324, 10%); -fx-border-top-width: 1px; } .status-bar { - -fx-background-color: derive(#1d1d1d, 30%); + -fx-background-color: derive(#222324, 30%); } .result-display { -fx-background-color: transparent; -fx-font-family: "Segoe UI Light"; -fx-font-size: 13pt; - -fx-text-fill: white; + -fx-text-fill: #aaaaaa; } .result-display .label { @@ -159,47 +179,47 @@ .status-bar .label { -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; + -fx-text-fill: #aaaaaa; -fx-padding: 4px; -fx-pref-height: 30px; } .status-bar-with-border { - -fx-background-color: derive(#1d1d1d, 30%); - -fx-border-color: derive(#1d1d1d, 25%); + -fx-background-color: derive(#222324, 30%); + -fx-border-color: derive(#222324, 25%); -fx-border-width: 1px; } .status-bar-with-border .label { - -fx-text-fill: white; + -fx-text-fill: #aaaaaa; } .grid-pane { - -fx-background-color: derive(#1d1d1d, 30%); - -fx-border-color: derive(#1d1d1d, 30%); + -fx-background-color: derive(#222324, 30%); + -fx-border-color: derive(#222324, 30%); -fx-border-width: 1px; } .grid-pane .stack-pane { - -fx-background-color: derive(#1d1d1d, 30%); + -fx-background-color: derive(#222324, 30%); } .context-menu { - -fx-background-color: derive(#1d1d1d, 50%); + -fx-background-color: #292929; } .context-menu .label { - -fx-text-fill: white; + -fx-text-fill: #aaaaaa; } .menu-bar { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #292929; } .menu-bar .label { -fx-font-size: 14pt; -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; + -fx-text-fill: #aaaaaa; -fx-opacity: 0.9; } @@ -217,7 +237,7 @@ -fx-border-color: #e2e2e2; -fx-border-width: 2; -fx-background-radius: 0; - -fx-background-color: #1d1d1d; + -fx-background-color: #222324; -fx-font-family: "Segoe UI", Helvetica, Arial, sans-serif; -fx-font-size: 11pt; -fx-text-fill: #d8d8d8; @@ -229,12 +249,12 @@ } .button:pressed, .button:default:hover:pressed { - -fx-background-color: white; - -fx-text-fill: #1d1d1d; + -fx-background-color: #aaaaaa; + -fx-text-fill: #222324; } .button:focused { - -fx-border-color: white, white; + -fx-border-color: #aaaaaa, #aaaaaa; -fx-border-width: 1, 1; -fx-border-style: solid, segments(1, 1); -fx-border-radius: 0, 0; @@ -243,8 +263,8 @@ .button:disabled, .button:default:disabled { -fx-opacity: 0.4; - -fx-background-color: #1d1d1d; - -fx-text-fill: white; + -fx-background-color: #222324; + -fx-text-fill: #aaaaaa; } .button:default { @@ -257,36 +277,36 @@ } .dialog-pane { - -fx-background-color: #1d1d1d; + -fx-background-color: #222324; } .dialog-pane > *.button-bar > *.container { - -fx-background-color: #1d1d1d; + -fx-background-color: #222324; } .dialog-pane > *.label.content { -fx-font-size: 14px; -fx-font-weight: bold; - -fx-text-fill: white; + -fx-text-fill: #aaaaaa; } .dialog-pane:header *.header-panel { - -fx-background-color: derive(#1d1d1d, 25%); + -fx-background-color: derive(#222324, 25%); } .dialog-pane:header *.header-panel *.label { -fx-font-size: 18px; -fx-font-style: italic; - -fx-fill: white; - -fx-text-fill: white; + -fx-fill: #aaaaaa; + -fx-text-fill: #aaaaaa; } .scroll-bar { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #222324; } .scroll-bar .thumb { - -fx-background-color: derive(#1d1d1d, 50%); + -fx-background-color: #29292a; -fx-background-insets: 3; } @@ -318,14 +338,14 @@ } #commandTextField { - -fx-background-color: transparent #383838 transparent #383838; + -fx-background-color: transparent #222324 transparent #222324; -fx-background-insets: 0; - -fx-border-color: #383838 #383838 #ffffff #383838; + -fx-border-color: #222324 #222324 #515658 #222324; -fx-border-insets: 0; -fx-border-width: 1; -fx-font-family: "Segoe UI Light"; -fx-font-size: 13pt; - -fx-text-fill: white; + -fx-text-fill: #aaaaaa; } #filterField, #personListPanel, #personWebpage { @@ -333,18 +353,27 @@ } #resultDisplay .content { - -fx-background-color: transparent, #383838, transparent, #383838; + -fx-background-color: transparent, #222324, transparent, #222324; -fx-background-radius: 0; } -#tags { +#tags, #modules { -fx-hgap: 7; -fx-vgap: 3; } #tags .label { - -fx-text-fill: white; - -fx-background-color: #3e7b91; + -fx-text-fill: #21354e; + -fx-background-color: #7198c8; + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 2; + -fx-font-size: 11; +} + +#modules .label { + -fx-text-fill: #831f1a; + -fx-background-color: #e27e7d; -fx-padding: 1 3 1 3; -fx-border-radius: 2; -fx-background-radius: 2; diff --git a/src/main/resources/view/Extensions.css b/src/main/resources/view/Extensions.css index bfe82a85964..498dba79e50 100644 --- a/src/main/resources/view/Extensions.css +++ b/src/main/resources/view/Extensions.css @@ -4,8 +4,12 @@ } .list-cell:empty { - /* Empty cells will not have alternating colours */ - -fx-background: #383838; + /* Empty cells will not have alternating colours and no border*/ + -fx-background: #222324; + -fx-border-color: transparent; + -fx-border-width: 0; + -fx-border-radius: 0; + -fx-background-radius: 0; } .tag-selector { diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 7778f666a0a..ed7c6182dd6 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -12,7 +12,7 @@ + title="NUSMates" minWidth="450" minHeight="600" onCloseRequest="#handleExit"> diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index 84e09833a87..ff19836d083 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -9,12 +9,13 @@ + - + @@ -27,10 +28,27 @@ + + + + +