diff --git a/README.md b/README.md index 13f5c77403f..2e330a5788a 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,12 @@ -[![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/AY2324S2-CS2103T-T11-4/tp/graph/badge.svg?token=YS8FHD1IQM)](https://codecov.io/gh/AY2324S2-CS2103T-T11-4/tp) ![Ui](docs/images/Ui.png) -* This is **a sample project for Software Engineering (SE) students**.
+* **TrAcker** is a student software engineering project that aims to create an application to help Head Teaching + Assistants (Head TAs) track the records of other Teaching Assistants (TAs) and students. Example usages: - * as a starting point of a course project (as opposed to writing everything from scratch) - * as a case study -* The project simulates an ongoing software project for a desktop application (called _AddressBook_) used for managing contact details. - * It is **written in OOP fashion**. It provides a **reasonably well-written** code base **bigger** (around 6 KLoC) than what students usually write in beginner-level SE modules, without being overwhelmingly big. - * It comes with a **reasonable level of user and developer documentation**. -* It is named `AddressBook Level 3` (`AB3` for short) because it was initially created as a part of a series of `AddressBook` projects (`Level 1`, `Level 2`, `Level 3` ...). -* For the detailed documentation of this project, see the **[Address Book Product Website](https://se-education.org/addressbook-level3)**. -* This project is a **part of the se-education.org** initiative. If you would like to contribute code to this project, see [se-education.org](https://se-education.org#https://se-education.org/#contributing) for more info. + * Add/delete contact details of TAs and students + * Update contact details of TAs and students + * Edit tags which reflect the attendance of students + +* This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). diff --git a/build.gradle b/build.gradle index a2951cc709e..934e7dbc478 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,10 @@ repositories { maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } } +run { + enableAssertions = true +} + checkstyle { toolVersion = '10.2' } @@ -66,7 +70,8 @@ dependencies { } shadowJar { - archiveFileName = 'addressbook.jar' + archiveBaseName = 'TrAcker' + archiveClassifier = 'v1.4test' } defaultTasks 'clean', 'test' diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 1c9514e966a..5e0d626c710 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -9,51 +9,50 @@ You can reach us at the email `seer[at]comp.nus.edu.sg` ## Project team -### John Doe +### Ho Kai Ting - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/kaitinghh)] +[[portfolio](team/kaitinghh.md)] -* Role: Project Advisor +* Role: Developer +* Responsibilities: -### Jane Doe +### Wang Xinrong - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/wang-xinrong)] +[[portfolio](team/wangxinrong.md)] -* Role: Team Lead +* Role: Developer * Responsibilities: UI -### Johnny Doe +### Wong Kai Jie - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](http://github.com/wongkj12)] [[portfolio](team/wongkj12.md)] * Role: Developer * Responsibilities: Data -### Jean Doe +### Yong Kotaro - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/yongkotaro)] * Role: Developer -* Responsibilities: Dev Ops + Threading +* Responsibilities: UI -### James Doe +### Yu Chenbo - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/yyccbb)] +[[portfolio](team/yyccbb)] * Role: Developer -* Responsibilities: UI +* Responsibilities: Project Structure diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 1b56bb5d31b..693df72a764 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -7,12 +7,6 @@ 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} - --------------------------------------------------------------------------------------------------------------------- - ## **Setting up, getting started** Refer to the guide [_Setting up and getting started_](SettingUp.md). @@ -20,9 +14,7 @@ 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.
@@ -31,12 +23,11 @@ Refer to the guide [_Setting up and getting started_](SettingUp.md). The ***Architecture Diagram*** given above explains the high-level design of the App. - Given below is a quick overview of main components and how they interact with each other. **Main components of the architecture** -**`Main`** (consisting of classes [`Main`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/MainApp.java)) is in charge of the app launch and shut down. +**`Main`** (consisting of classes [`Main`](https://github.com/AY2324S2-CS2103T-T11-4/tp/blob/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/AY2324S2-CS2103T-T11-4/tp/blob/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 +59,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/AY2324S2-CS2103T-T11-4/tp/blob/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/AY2324S2-CS2103T-T11-4/tp/blob/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, @@ -85,23 +76,23 @@ 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/AY2324S2-CS2103T-T11-4/tp/blob/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("mark 1 /t Assignment1 /ts cg")` 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 `mark 1 /t Assignment1 /ts cg` Command](images/MarkSequenceDiagram.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. +
:information_source: **Note:** The lifeline for `MarkCommandParser` 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. 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., `MarkCommandParser`) 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., `MarkCommand`) which is executed by the `LogicManager`. 1. The command can communicate with the `Model` when it is executed (e.g. to delete a person).
Note that although this is shown as a single step in the diagram above (for simplicity), in the code it can take several interactions (between the command object and the `Model`) to achieve. 1. The result of the command execution is encapsulated as a `CommandResult` object which is returned back from `Logic`. @@ -115,7 +106,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/AY2324S2-CS2103T-T11-4/tp/blob/master/src/main/java/seedu/address/model/Model.java) @@ -136,7 +127,7 @@ The `Model` component, ### 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/AY2324S2-CS2103T-T11-4/tp/blob/master/src/main/java/seedu/address/storage/Storage.java) @@ -153,7 +144,67 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa ## **Implementation** -This section describes some noteworthy details on how certain features are implemented. +This section describes some noteworthy details on how certain features unique to TrAcker are implemented. + +### Tagging system + +TrAcker helps users track assignment status, tutorial attendance and tutorial groups via the Tagging system. +The class diagram below depicts how different types of Tags are implemented: + + + +We use a `Set` to store the set of tags for each Student / TA, such that Tags are uniquely identified +by their `tagName`. Note that TAs may only have `TutorialTag`. + +### Tutorial Tags + +Users may only mark persons with valid TutorialTags they have previously defined using ``tuttag add``. +The following activity diagram depicts a typical flow of how a user would add a new tutorial group. + + + +### Available Command + +TrAcker allows users to find available TAs for a specific tutorial group. The `AvailableCommandParser` parses the user's input and creates an `AvailableCommand` containing a `TutorialTagContainsGroupPredicate`. The `AvailableCommand` is then executed by `LogicManager` to update the `FilterPersonList` in the `Model`. This retrieves the list of available TAs for the specified tutorial group. +The workflow is shown below. + +![AvailableActivityDiagram](images/AvailableActivityDiagram.png) + +### Find Command + +The `find` command filters persons in the current displayed list based on the user specified flags. +Here we provide a brief summary of the specific behaviour of the `find` command: +* The search is case-insensitive +* When searching by name, we perform subword matching e.g. `Han` will match `Hans` +* Between optional fields supplied, an AND search is performed (e.g. `find stu /n John /i 6Z` will find all +Students who have both a name containing `John` and an ID containing `6Z`.) +* For multiple values supplied to the same field, an OR search is performed. +* **Searching by Tags:** + * For TutorialTags, subword matching is performed (e.g. `find /t wed assignment1` will find all + persons with a TutorialTag where `wed` is a subword or an `assignment1` tag.) + * This is to make it easier for TAs to search by the day of the week for Tutorial tags. + * As follows, for other types of tags, full word matching is performed. + +The below sequence diagram depicts the process of a user executing the `find` command: + +![FindSequenceDiagram](images/FindSequenceDiagram.png) + +### Stateful Utility Classes + +The `ParserUtil` and `StringUtil` classes were originally classes that provide utility +APIs for parsing and string operations. Upon noticing the need that some parsing and string +operations require state information of the current model, both classes are updated to record +the state information and follow the singleton pattern. They are renamed `StatefulParserUtil` and +`StatefulStringUtil`. +
+ +For both classes, +* An `initialize()` method is provided to initialize with the current model. This method should only +be called once. +* An `getInstance()` method is provided to access the private field `model` that +captures the state information +* Some utility methods can now use the state information (e.g. the current filtered list of persons) +to achieve their functionalities. ### \[Proposed\] Undo/redo feature @@ -239,10 +290,6 @@ The following activity diagram summarizes what happens when a user executes a ne _{more aspects and alternatives to be added}_ -### \[Proposed\] Data archiving - -_{Explain here how the data archiving feature will be implemented}_ - -------------------------------------------------------------------------------------------------------------------- @@ -262,40 +309,65 @@ _{Explain here how the data archiving feature will be implemented}_ **Target user profile**: -* has a need to manage a significant number of contacts +* Head TA of CS2103T +* has a need to manage a significant number of contacts of students and other TAs * prefer desktop apps over other types * can type fast * prefers typing to mouse interactions * is reasonably comfortable using CLI apps -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app +**Value proposition**: manage students and TAs faster than a typical mouse/GUI driven app ### User stories Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` -| Priority | As a …​ | I want to …​ | So that I can…​ | -| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | -| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App | -| `* * *` | user | add a new person | | -| `* * *` | user | delete a person | remove entries that I no longer need | -| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list | -| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident | -| `*` | user with many persons in the address book | sort persons by name | locate a person easily | - -*{More to be added}* +| Priority | As a …​ | I want to …​ | So that I can…​ | +|----------|----------------------------------------------|--------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------| +| `* * *` | user | mark students' attendance | view attendance records easily in the app. | +| `* * *` | user | add a contact entry | | +| `* * *` | user | delete a contact entry | | +| `* * *` | user | add tags | | +| `* * *` | user | delete tags | | +| `* * *` | user | edit a contact entry | | +| `* * *` | user | search for specific contacts | easily retrieve the contact info of a student/tutor if I need to contact them. | +| `* * *` | user | have a basic GUI to interact with | use the app conveniently instead of in shell/terminal. | +| `* *` | user with many students and tutors to manage | send group emails to students/tutors | save time on gathering email addresses. | +| `* *` | user | organize students into classes | separate records for students from different classes. | +| `* *` | beginner user | access a user guide | understand the functions of the app. | +| `* *` | potential user exploring the app | see the app populated with sample data | get an idea of how the app is used. | +| `* *` | user with many students and tutors to manage | filter contacts with relevant keywords or tags | access records relevant to a specific keyword quickly. | +| `* *` | novice user | expect the commands to be common-sensical | pick up the app at speed. | +| `* *` | novice user | expect warnings to be given to irreversible actions such as batch delete contacts | will not lose my data out of unfamiliarity with the commands. | +| `* *` | frequent user | batch edit contacts | make similar changes to a large number of contacts quickly. | +| `* *` | user | see which tutors are available | quickly allocate them for replacement tutoring. | +| `* *` | novice user | have a sleek and simple UI | use the app easily. | +| `* *` | careless user | backup the last few edits | revert changes if necessary. | +| `* *` | user | track students' assignment progress | follow up if necessary. | +| `* *` | user | edit tags | easily customise existing tags for my use case. | +| `* *` | careless user | backup the last few edits | revert changes if necessary. | +| `*` | expert user | export data in .xlsx | share records with other tutors. | +| `*` | expert user | create shortcuts for specific commands | perform the usual tasks quickly. | +| `*` | user with lots of data stored | batch imports to have a timestamp | easily locate certain information based on import time, or batch delete them when obsolete. | +| `*` | novice user | accessible help page to remind me of command keywords | carry out tasks quickly even without remembering the command keywords. | +| `*` | user | generate attendance reports of the students | see who has been skipping classes. | +| `*` | busy user | receive reminders for upcoming classes | keep track of my upcoming lessons. | +| `*` | impatient user | use the app offline | use the app even when the connection is poor. | +| `*` | user who is familiar with cli | be able to access already executed commands | execute them again when needed without having to type everything again. | +| `*` | user who is familiar with cli | see a list of suggested commands after having typed the initial letters of a keyword | quickly select a command to complete and it is less likely that I misspell a command. | +| `*` | beginner user | GUI to be simple and self-explanatory | get familiar with the app easily. | ### 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 `TrAcker`, the **Actor** is the `user` and **persons** can be both a student and a TA unless specified otherwise) **Use case: Delete a person** **MSS** 1. User requests to list persons -2. AddressBook shows a list of persons +2. TrAcker shows a list of persons 3. User requests to delete a specific person in the list 4. AddressBook deletes the person @@ -309,26 +381,125 @@ Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unli * 3a. The given index is invalid. - * 3a1. AddressBook shows an error message. + * 3a1. TrAcker shows an error message. Use case resumes at step 2. -*{More to be added}* +**Use case: Add a person** -### Non-Functional Requirements +**MSS** + +1. User requests to add a person with relevant entries such as name, phone number and email +2. TrAcker adds the entry to its contact list -1. Should work on any _mainstream OS_ as long as it has Java `11` or above installed. -2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. -3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. + Use case ends. + +**Extensions** -*{More to be added}* +* 1a. User inputs information in incorrect format + + * 1a1. TrAcker shows an error message. + + Use case resumes at step 1 + +**Use case: Search a contact** + +**MSS** + +1. User requests to search a contact by keywords (for names) +2. TrAcker shows a list of contacts whose names match the keywords + + Use case ends. + +**Extensions** + +* 1a. No contacts have names including the specified keywords. + + *1a1. TrAcker shows an empty list + + Use case ends. + +**Use case: Edit a contact** + +**MSS** + +1. User requests to edit a contact with new information +2. TrAcker updates the contact with specified new information + + Use case ends. + +**Extensions** + +* 1a. User inputs new information in incorrect format + + * 1a1. TrAcker shows an error message. + + Use case resumes at step 1 + +### Non-Functional Requirements + +1. **(Technical)** Should work on any _mainstream OS_ as long as it has Java `11` or above installed. +2. **(Quality)** 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. +3. **(Quality)** The user interface and commands should be intuitive and user-friendly, requiring minimal time to learn. +4. **(Quality)** The app should provide clear and informative error messages in case of invalid inputs or command failures. +5. **(Performance)** The app should respond to user actions within 0.5 seconds, ensuring smooth navigation and interaction. +6. **(Performance)** The app should be able to support at minimum a contacts list of 200 without affecting performance. +7. **(Performance)** The app should be able to run smoothly even on low-end hardware configurations. +8. **(Process)** The project is expected to adhere to a schedule that adds updates incrementally at least once every two weeks. +9. **(Project scope)** The product should be focused on the needs of our target user, CS Head Teaching Assistants. +10. **(Documentation)** Comprehensive user guide should be provided, with detailed instructions on how to use each command. +11. **(Documentation)** Comprehensive developer guide should be created to facilitate ongoing maintenance of the app. ### Glossary * **Mainstream OS**: Windows, Linux, Unix, MacOS -* **Private contact detail**: A contact detail that is not meant to be shared with others +* **Tutor**: A teaching assistant for CS2103T +* **Student**: A student taking CS2103T +* **Tag**: A label that can be attached to the student/TA. It can represent a Tutorial Group, Attendance or Assignment. +* **Tag Status**: The current status of a student's tag. More details can be found in the User Guide. +* **Contact Entry**: Contact information of either a student or a TA, containing the name, ID, phone number, email. -------------------------------------------------------------------------------------------------------------------- +## **Appendix: Planned Enhancements** + +Team size: 5 + +1. **Restrict actions on TutorialTags for clarity:** As of now, there is nothing restricting users from doing +`tuttag add /t TUE08` followed by `mark 1 /t TUE08 /ts cg`, i.e. the TutorialTag `TUE08` is being treated as an AssignmentTag +by being assigned the TagStatus `cg`, and is moved to the first row. This can be confusing if users forget their Tutorial / Assignment tags. +For future versions of TrAcker we intend to make the distinction between different types of tags clearer by restricting the possible TagStatus +of TutorialTags. + + +2. **Restrict TutorialTag status for Students:** Currently Students get have a TutorialTag marked as "AVAILABLE" +by entering for instance `mark 1 /t TUE08 /ts av`. However, this is likely not very useful for TAs and becomes problematic if the same Student +is to be assigned the tutorial group `TUE08`. Thus we plan to also restrict Students to only be able to have TutorialTags with an +ASSIGNED TagStatus. + + +3. **Enhance information prompts:** Some of the information / error messages in TrAcker can be improved for clarity. +For example running the `clear` command leaves the previous information still in the information prompt. For future versions of TrAcker +we plan to clear out the information panel in order to show messages relevant the current command being run. + + +4. **Improve GUI to handle longer inputs:** One of the limitations of TrAcker's current GUI is that long input text +gets truncated if the window size is small (or, on extreme inputs, text can be truncated in a full-sized window). We +intend to work on enhancing the flexibility of our GUI in order to handle longer input lengths by adding a scroll pane if lengths +are too long. + + +5. **Making Phone Number an optional field:** Currently, TrAcker functions similarly to an address book where phone number is a +compulsory field when adding a person. However, students' phone numbers may not be available nor relevant data which TAs require. Thus in order +to make the process of adding students smoother, we plan to make the phone number field optional in the future. + +6. **Making parsing of phone number and email stricter:** Currently, TrAcker does not limit the number of characters in the phone number field, +and accepts any string with '@' as a valid email input. We plan to accept only 3-15 characters in the phone number field, and +accept only valid email addresses in the future. + +7. **Import / Export from .csv:** Currently TrAcker relies on users manually adding student / TA data using the `add` command. +This can be quite time-consuming, especially if users are trying to migrate student / TA data from existing systems in .csv format. +Thus we plan to include a feature to import data from .csv format (where all required fields are filled in). Similarly, we also intend to +develop an Export feature for users to export their list of Student / TA data into .csv format for added convenience. ## **Appendix: Instructions for manual testing** diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 7abd1984218..1d3331038a8 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -3,143 +3,221 @@ layout: page 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. +Welcome to **TrAcker**! This is a *handy contact management app* built for *Computer Science Head Teaching Assistants* +(CS Head TAs) in NUS. Optimised for use via a command line interface, you can manage student assignments, attendance, +tutor availability and much more with *just a few keystrokes*! * Table of Contents {:toc} -------------------------------------------------------------------------------------------------------------------- +## About this User Guide + +We designed this User Guide to bring you through the functionalities of TrAcker step-by-step! + +If you are a :star: **first-time user**, we're excited to have you onboard! We recommend you to follow our User Guide +sequentially to get yourself up to speed. + +Throughout the user guide, we have added some extra information for additional clarity! + +
:information_source: **Info:** +I am here to provide you with additional information about the commands! +
+ +
:bulb: **Tips:** +I am here to help you with extra tips! +
+ +
:exclamation: **Caution:** +I am here to warn you of potential risks or issues! +
+ +If you are an :star::star::star: **experienced user**, skip ahead to [Command summary](#command-summary) +for a quick refresher. + +-------------------------------------------------------------------------------------------------------------------- + ## Quick start 1. Ensure you have Java `11` or above installed in your Computer. -1. Download the latest `addressbook.jar` from [here](https://github.com/se-edu/addressbook-level3/releases). +2. Download the latest `TrAcker.jar` from [here](https://github.com/AY2324S2-CS2103T-T11-4/tp/releases). -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +3. Copy the file to the folder you want to use as the _home folder_ for your **TrAcker** app. -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.
+4. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar TrAcker.jar` + command to run the application.
+ The GUI with some sample data should appear in a few seconds:
![Ui](images/Ui.png) -1. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
+5. Type the command in the command box and press Enter to execute it.
Some example commands you can try: * `list` : Lists all contacts. - - * `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. - - * `delete 3` : Deletes the 3rd contact shown in the current list. + + * `add stu /n John Doe /i A0123456Y /p 91234567 /e johndoe@ex.com` : Adds the Student `John Doe` to your contact list. + + * `add ta /n Jane Smith /i A0654321Y /p 97654321 /e janesmith@ex.com` : Adds the TA `Jane Smith` to your contact list. + + * `delete 3` : Deletes the 3rd contact shown in the displayed list. * `clear` : Deletes all contacts. * `exit` : Exits the app. -1. Refer to the [Features](#features) below for details of each command. +6. If a command is not recognized, a message containing the correct usage of the command will be shown. + +7. Head to [Features](#basic-features) below for details of each command. -------------------------------------------------------------------------------------------------------------------- -## Features +## Basic Features + +While using TrAcker for the first time might be overwhelming, fret not as we are here to guide you through. Let's +go through the basic commands to get you started! + +
+ +:bulb: **Tip:**
+ +* You can press `Enter` or `/` to activate input field and start typing when the TrAcker window is in focus. This is provided that there is no popup window demanding attention. +
**:information_source: Notes about the command format:**
-* Words in `UPPER_CASE` are the parameters to be supplied by the user.
- e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. +* Words in `UPPER_CASE` are the parameters to be supplied.
+ e.g. in `add stu /n NAME`, `NAME` is a parameter that can be used as `add stu /n John Doe`. + +* Parameters with `...` after them can be supplied zero, one or more times.
+ e.g. `[/t TAG...]` can be used as `/t Assignment1` or `/t Assignment1 Assignment2`(`Assignment1` and `Assignment2` +would be treated as two different tags. Refer to the [Tagging](#tagging) section for more information). * 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`. + e.g. `/n NAME [/p PHONE]` can be used as `/n John Doe /p 91234567` 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. +* The vertical bar or pipe `|` is used to denote alternatives.
-* 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. +* Pipe symbol and square brackets together `[|]` denote alternative items that are optional.
+ e.g. in `add [stu | ta] /n NAME`, `stu` and `ta` are alternatives, either exactly one or none of them should be used. + Refer to the [Add](#adding-a-student-or-ta-add-stu-add-ta) command section for more information). -* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
+* Parameters can be supplied in any order.
+ e.g. if the command specifies `/n NAME /i ID`, `/i ID /n NAME` is also acceptable and has the same effect. + +* Extraneous parameters for single-word commands that do not take in parameters +(specifically `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. +* If you are using a PDF version of this document, be careful when copying and pasting commands that span across +multiple lines as space characters surrounding line-breaks may be omitted when copied over to the application.
-### Viewing help : `help` -Shows a message explaning how to access the help page. +### Adding a Student or TA: `add stu`, `add ta` -![help message](images/helpMessage.png) +Adds a Student/TA to the address book. -Format: `help` +Format: +* To add a Student,
+ `add [stu] /n NAME /i ID /p PHONE /e EMAIL` + +* To add a TA,
+ `add ta /n NAME /i ID /p PHONE /e EMAIL` +* Every person is saved either as a Student or TA. If the type of the person is not specified, the person will be + saved as a Student by default.
+* Each person's ID is unique, so you cannot add 2 people with the same ID.
-### Adding a person: `add` +Examples: +* `add stu /n Alex Yeoh /i A0777777L /p 87438807 /e alexyeoh@ex.com` +* `add ta /n Charlotte Oliveiro /i A2222222P /p 93210283 /e charlotte@ex.com` -Adds a person to the address book. +### Editing a person : `edit` -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +Edits an existing person in the contact book. -
:bulb: **Tip:** -A person can have any number of tags (including 0) -
+Format: `edit INDEX [/n NAME] [/p PHONE] [/e EMAIL]` + +* 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** within the size of the displayed list. +* At least one of the optional fields must be provided. +* Existing values will be updated to the input values. +* A person's `type` (`stu` or `ta`) and `ID` cannot be edited. 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` +* `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` Edits the name of the 2nd person to be `Betsy Crower`. ### Listing all persons : `list` -Shows a list of all persons in the address book. +Shows a list of all persons in TrAcker. With this command, you can undo any filtering by previous +[find](#locating-persons-find) commands. Format: `list` -### Editing a person : `edit` +### Locating persons: `find` -Edits an existing person in the address book. +Filters all persons whose contact details contain the specified keywords under the specified flag and +displays them as a list with index numbers. -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +Format: `find [stu | ta] [/n NAME] [/i ID] [/p PHONE] [/e EMAIL] [/t TAGS...]` -* 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. -* 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. +* At least one of the optional fields must be supplied. +* The search is case-insensitive. e.g. `hans` will match `Hans` +* The order of the keywords under each flag does not matter. e.g. `Hans Bo` will match `Bo Hans` +* Subwords will be matched e.g. `Han` will match `Hans` + +* Between optional fields supplied, the search filters for persons meeting criteria specified for ALL fields at the +same time, (i.e. `AND` search). e.g. `find stu /n John /i 6Z` will find all Students who have both a name containing +`John` and an ID containing `6Z`.
+* For multiple values (split by space) supplied to the same optional field, the search filters for persons meeting ANY criteria, (i.e. `OR` search). + e.g. `find stu /n John Smith` will find all Students who have either a name containing `John` or a name containing `Smith`. + +* When performing search for the `TAGS` field (more information on tags [here](#tagging)): + * For tutorial tags, subword matching is performed + * For other types of tags, it performs full word matching + + e.g. `find /t wed assignment1` will find all persons with a tutorial tag where `wed` is a subword +or an `assignment1` tag
+ e.g. `find stu /n John /t wed assignment1` will find all persons with a name containing `John` AND +either a tutorial tag where `wed` is a subword or an `assignment1` tag 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. +* `find ta` returns all TAs + ![result for 'find ta'](images/findTaResult.png) -### Locating persons by name: `find` +* `find /n John` returns `john` and `John Doe` -Finds persons whose names contain any of the given keywords. +### Deleting persons : `delete` -Format: `find KEYWORD [MORE_KEYWORDS]` +Deletes the person(s) specified by their indices from the displayed person list. A popup will appear to confirm the +deletion. -* The search is case-insensitive. e.g `hans` will match `Hans` -* The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans` -* Only the name is searched. -* Only full words will be matched e.g. `Han` will not match `Hans` -* Persons matching at least one keyword will be returned (i.e. `OR` search). - e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` +Format: `delete (all | INDEX [OTHER_INDICES...])` -Examples: -* `find John` returns `john` and `John Doe` -* `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) +* Deletes the person(s) at the specified `INDEX`. +* If `all` is used, all persons in the displayed list are deleted. The displayed list might not be the same as the full +list. For example, if the `delete all` command is used after a [`find NAME`](#locating-persons-find) command, +all contacts found by the `find NAME` command would be deleted but not those excluded from the displayed list. +* The index **must be a positive integer** within the size of the displayed list. -### Deleting a person : `delete` +Examples: +* `list` followed by `delete 2` deletes the 2nd person in the full contact list. +* `find /n Betsy` followed by `delete 1` deletes the 1st person in the results of the `find /n Betsy` command. -Deletes the specified person from the address book. +
-Format: `delete INDEX` +:bulb: **Warning Popup:**
-* 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, …​ +* Oops! It looks like you're about to perform an action that could lead to unintended data loss. No worries though - +we've got your back! To make sure everything stays safe and sound, we just need a quick confirmation from you.
+* We have introduced toggling using your left and right arrow keys, as well as the enter key for increased efficiency. -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. +
### Clearing all entries : `clear` @@ -155,44 +233,219 @@ Format: `exit` ### Saving the data -AddressBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. +Worried about data loss? That is not an issue with TrAcker.
+ +TrAcker data is saved in the hard disk automatically after each command that changes the data. There is no need to save manually. ### Editing the data file -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. +TrAcker saves its data as a JSON file `[JAR file location]/data/addressbook.json` automatically. +Advanced users are welcome to update data directly by editing that data file. +**While the app is running**, edits to the `addressbook.json` file will not be reflected in the UI. +To view the changes, rerun the application. -
: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. +
:exclamation: **Caution:**
+* If your changes to the data file renders its format invalid, TrAcker will discard all existing data and start with an +empty data file in the next run. Hence, it is recommended to have a backup of the data file before editing it.
+* Furthermore, certain edits can cause TrAcker to behave in unexpected ways (e.g., if a value entered is outside the +acceptable range). Therefore, please edit the data file only if you are confident that you can update it correctly. +
+ +-------------------------------------------------------------------------------------------------------------------- + +## Tagging + +Forget the hassle :hammer: of managing administrative tasks as the Head TA! With TrAcker tags, you can conveniently track +students' assignment completion status, their assigned tutorial groups as well as their tutorial attendance records. +You are also able to track TA's assigned tutorial slots and their availability for other tutorial slots in case +substitutions are needed. + +TrAcker allows the use of three different types of tags - **Assignment, Attendance,** and **Tutorial** - +which can be attached to Students and TAs. +The different tag types along with their corresponding tag statuses are described below. + +### Tag Status + +| Tag type | Status | +|------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Assignment | `cg` : COMPLETE_GOOD (completed before deadline)
`cb` : COMPLETE_BAD (completed after deadline)
`ig` : INCOMPLETE_GOOD (incomplete before deadline)
`ib` : INCOMPLETE_BAD (incomplete after deadline) | +| Attendance | `p` : PRESENT
`a` : ABSENT
`awr` : ABSENT_WITH_REASON | +| Tutorial | `as` : ASSIGNED
`av` : AVAILABLE | + +### Tag Name +Regardless of tag type, tag names must abide by the following constraints: +- must be alphanumeric +- no whitespace between words in the tag +- tags attached to each person must have unique names (each person can only have one tag of a specific name, regardless of tag types. But tags of the same name can be attached to different contacts) + +Here are some recommended tag names for the various tag types. + +| Tag type | Examples of recommended tag names | +|------------|-----------------------------------| +| Assignment | `Assignment1` `MockPE` | +| Attendance | `Week1` `Week2` | +| Tutorial | `TUE08` `WED10` `THU09` | + + +### Marking a tag : `mark` + +Updates the status of the specified tag with the specified status for the specified person(s). If the +tag specified does not yet exist for the person(s), a new tag with the tag name and tag status will be +created and attached to the person(s). + +
+ +:bulb: **Notes:**
+ +* The type of the tag(s) to be updated/created are specified through their tag status.
+* If a tag is to be marked with the status `cg`, `cb`, `ig` or `ib`, it would be identified as an assignment tag and displayed together with other assignment tags in the UI. Similarly for attendance and tutorial tags.
+* If a specific person(s) already has a tag with the same tag name as the tag that is to be marked, but his existing tag has a different tag type as the type identified by new status from the mark command, his original tag would then be replaced by the tag with new type and status but the same tag name. + +
+ +Format: `mark (all | INDEX [OTHER_INDICES...]) /t TAG [OTHER_TAGS...] /ts TAG_STATUS` + +* The index refers to the index number shown in the displayed person list. +* The index **must be a positive integer** within the size of the displayed list. +* When `all` is used, the command will apply to all persons in the displayed list. +* When multiple `TAG`s are specified, the same `TAG_STATUS` will be applied to all the tags. +* `TAG_STATUS` must be one of the [above specified values](#tag-status) + +Examples: +* `mark 1 /t Assignment1 /ts cg` updates the `Assignment1` tag +to COMPLETE_GOOD for the 1st person in the displayed list if they already +have the tag. The `Assignment1` tag of COMPLETE_GOOD status would be added +to the contact if they previously did not have the tag. +* `mark 2 3 /t week1 week2 /ts awr` updates the `week1` and `week2` tags to +ABSENT_WITH_REASON for the 2nd and 3rd persons in the displayed list +if they already have the tag. Both tags with specified status would be added to the two contacts if any of them +previously did not have the tags. +* `mark all /t TUE08 /ts as` updates the `TUE08` tag to ASSIGNED to +assign every person in the displayed list to the tutorial group TUE08 if they already have the tag. The `TUE08` tag +with specified status would be added to any listed contact that previously did not have the tag. + +
+ +:information_source: **Note:**
+ +For **Tutorial** tags, the tutorial name must be that of a valid Tutorial tag in the +list of available tutorial sessions defined with the [tuttag](#adding-a-tutorial-tuttag-add) command. +For example, in the third example above, `TUE08` should be added as a tutorial tag first using [`tuttag add /t TUE08`](#adding-a-tutorial-tuttag-add). + +
+ +### Adding a Tutorial: `tuttag add` + +Adds the specified Tutorial tag to the list of valid Tutorial tags. Tutorial tags (identified by Tutorial tag names) not from the valid Tutorial tag list cannot be used. + +Format: `tuttag add /t TAG` + +Examples: + +* `tuttag add /t TUE08` adds TUE08 as a valid Tutorial tag. + +### Deleting a Tutorial: `tuttag del` + +Deletes the specified Tutorial tag from the valid Tutorial tag list. If the specified tag does not exist in the list, no change would happen. + +Warning: All persons with the Tutorial tag (identified by tag name, regardless of tag status) will also have the Tutorial tag removed should this command execute successfully. +However, if the tag the person has is of the same specified tag name but of a tag type other than Tutorial (it is an Assignment or Attendance tag), this tag would not be removed. + +Format: `tuttag del /t TAG` + +Examples: + +* `tuttag del /t WED09` deletes WED09 from the valid Tutorial tag list, and removes the WED09 Tutorial tag from all persons. + +### Listing All Tutorials: `tuttag list` + +Lists all valid Tutorial tags in TrAcker. + +Format: `tuttag list` + +### Removing a tag: `removetag` + +Removes an individual tag of the specified tag name (regardless of tag type) from a person. If the specified tag does not exist, the person's tags would remain unchanged. + +Format: `removetag (all | INDEX [OTHER_INDICES...]) /t TAG` + +* The index refers to the index number shown in the displayed person list. +* The index **must be a positive integer** within the size of the displayed list. +* If the person does not have the specified tag, the command will leave the person unchanged. + +Examples: +* `removetag 1 /t Assignment1` removes the `Assignment1` tag from the 1st person in the displayed list. +* `removetag 2 3 /t Assignment2` removes the `Assignment2` tag from the 2nd and 3rd person in the displayed list. +* `removetag all /t Assignment3` removes the `Assignment3` tag from every person in the displayed list. + + +### Locating available TAs for a tutorial group: `available` + +Filters and lists all TAs who are available for (and not assigned to) a specified tutorial session to serve as replacement TAs. + +Format: `available /g TUTORIAL` + +
+ +:bulb: **Notes:**
+ +* Only one Tutorial tag name can be specified per `available` command.
+* Thus, after each `/g` flag, there can only be one Tutorial tag name specified, i.e. `available /g WED10 THU10` is an invalid input.
+* There can also only be one '/g' flag supplied per `available` command. If more than one `/g` flag is supplied within the same `available` command, +only the tag name after the last `/g` will be processed, i.e. in `available /g WED10 /g THU10`, only `THU10 +will be taken in as a parameter while `WED10` will be ignored.
-### Archiving data files `[coming in v2.0]` +* The search is case-sensitive and must match the specified tutorial group exactly. + +Examples: +* `available /g TUE08` returns all TAs who are available for tutorial group `TUE08` + +### Viewing help : `help` + +Prompts a popup containing the link to the user guide. -_Details coming soon ..._ +![help message](images/helpMessage.png) + +Format: `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. +Welcome to our FAQ section! Here, we've gathered answers to the questions we hear most often from our users. +If you can't find what you're looking for, feel free to reach out to us directly. + +**Q**: How do I transfer my data to another computer?
+**A**: Install the app in the other computer and overwrite the data file it creates with the file that +contains the data of your previous TrAcker home folder. -------------------------------------------------------------------------------------------------------------------- -## Known issues +## Known issues + +:hammer: Heads-up, we're aware of a few hiccups that some users might be experiencing. Don't worry, our team is +working hard to squash those bugs! Meanwhile, here are some workarounds for you: -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. +* **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.
-------------------------------------------------------------------------------------------------------------------- ## 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` +| Action | Format, Examples | +|-------------------------------|------------------------------------------------------------------------------------------------------------------------------------------| +| **Add** | add [stu | ta] /n NAME /i ID /p PHONE /e EMAIL ​
e.g., `add stu /n Alex Yeoh /i A0777777L /p 87438807 /e alexyeoh@example.com` | +| **List** | list | +| **Edit** | edit INDEX [/n NAME] [/p PHONE] [/e EMAIL] ​
e.g.,`edit 1 /p 91234567 /e johndoe@example.com` | +| **Find** | find [stu | ta] [/n NAME] [/i ID] [/p PHONE] [/e EMAIL] [/t TAGS...]
e.g., `find /t wed assignment1` | +| **Delete** | delete (all | INDEX [OTHER_INDICES...])
e.g., `delete 3` | +| **Clear** | clear | +| **Exit** | exit | +| **Mark** | mark (all | INDEX [OTHER_INDICES...]) /t TAG [OTHER_TAGS...] /ts TAG_STATUS
e.g., `mark 1 /t Assignment1 /ts cg` | +| **Create Valid Tutorial Tag** | tuttag add /t TAG
e.g., `tuttag add /t TUE08` | +| **Delete Valid Tutorial Tag** | tuttag del /t TAG
e.g., `tuttag del /t WED09` | +| **List Valid Tutorial Tags** | tuttag list | +| **Remove Tag** | removetag INDEX /t TAG
e.g., `removetag 1 /t Assignment1` | +| **Available** | available /g TUTORIAL
e.g., `available /g TUES08` | +| **Help** | help | diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..0d64a21fd8d 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "TrAcker" theme: minima header_pages: @@ -8,7 +8,7 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2324S2-CS2103T-T11-4/tp" github_icon: "images/github-icon.png" plugins: diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..d6da95db4de 100644 --- a/docs/_sass/minima/_base.scss +++ b/docs/_sass/minima/_base.scss @@ -288,7 +288,7 @@ table { text-align: center; } .site-header:before { - content: "AB-3"; + content: "TrAcker"; font-size: 32px; } } diff --git a/docs/diagrams/AvailableActivityDiagram.puml b/docs/diagrams/AvailableActivityDiagram.puml new file mode 100644 index 00000000000..792d9e80f8a --- /dev/null +++ b/docs/diagrams/AvailableActivityDiagram.puml @@ -0,0 +1,21 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +start +:User enters Available Command; + +:AvailableCommandParser parses input; + +if () then ([command is valid]) + :Creates an AvailableCommand with a TutorialTagContainsGroupPredicate; + if () then ([tutorial group exists]) + :Update FilterPersonList with the predicate; + else ([else]) + :Display error message; + endif +else ([command is invalid]) + :Display error message; +endif +stop +@enduml diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml index 598474a5c82..9357580c7b3 100644 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ b/docs/diagrams/BetterModelClassDiagram.puml @@ -5,17 +5,18 @@ skinparam arrowColor MODEL_COLOR skinparam classBackgroundColor MODEL_COLOR AddressBook *-right-> "1" UniquePersonList -AddressBook *-right-> "1" UniqueTagList -UniqueTagList -[hidden]down- UniquePersonList -UniqueTagList -[hidden]down- UniquePersonList +AddressBook *-right-> "1" UniqueTutorialTagList +UniqueTutorialTagList -[hidden]down- UniquePersonList +UniqueTutorialTagList -[hidden]down- UniquePersonList -UniqueTagList -right-> "*" Tag +UniqueTutorialTagList -right-> "*" Tag UniquePersonList -right-> Person Person -up-> "*" Tag Person *--> Name +Person *--> PersonType +Person *--> ID Person *--> Phone Person *--> Email -Person *--> Address @enduml diff --git a/docs/diagrams/FindSequenceDiagram.puml b/docs/diagrams/FindSequenceDiagram.puml new file mode 100644 index 00000000000..ceb3abb0ab8 --- /dev/null +++ b/docs/diagrams/FindSequenceDiagram.puml @@ -0,0 +1,70 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":FindCommandParser" as FindCommandParser LOGIC_COLOR +participant "d:FindCommand" as FindCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "m:Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("find /n John /i 6Z") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("find /n John /i 6Z") +activate AddressBookParser + +create FindCommandParser +AddressBookParser -> FindCommandParser +activate FindCommandParser + +FindCommandParser --> AddressBookParser +deactivate FindCommandParser + +AddressBookParser -> FindCommandParser : parse("/n John /i 6Z") +activate FindCommandParser + +create FindCommand +FindCommandParser -> FindCommand +activate FindCommand + +FindCommand --> FindCommandParser : +deactivate FindCommand + +FindCommandParser --> AddressBookParser : d +deactivate FindCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +FindCommandParser -[hidden]-> AddressBookParser +destroy FindCommandParser + +AddressBookParser --> LogicManager : d +deactivate AddressBookParser + +LogicManager -> FindCommand : execute(m) +activate FindCommand + +FindCommand -> Model : persistentUpdateFilteredList(Predicate) +activate Model + +Model --> FindCommand +deactivate Model + +create CommandResult +FindCommand -> CommandResult +activate CommandResult + +CommandResult --> FindCommand +deactivate CommandResult + +FindCommand --> LogicManager : r +deactivate FindCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/MarkSequenceDiagram.puml b/docs/diagrams/MarkSequenceDiagram.puml new file mode 100644 index 00000000000..b4ff25d0c95 --- /dev/null +++ b/docs/diagrams/MarkSequenceDiagram.puml @@ -0,0 +1,70 @@ +@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 ":MarkCommandParser" as MarkCommandParser LOGIC_COLOR +participant "p:MarkCommand" as MarkCommand 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("mark 1 /t Assignment1 /ts cg") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("mark 1 /t Assignment1 /ts cg") +activate AddressBookParser + +create MarkCommandParser +AddressBookParser -> MarkCommandParser +activate MarkCommandParser + +MarkCommandParser --> AddressBookParser +deactivate MarkCommandParser + +AddressBookParser -> MarkCommandParser : parse("1 /t Assignment1 /ts cg") +activate MarkCommandParser + +create MarkCommand +MarkCommandParser -> MarkCommand +activate MarkCommand + +MarkCommand --> MarkCommandParser : +deactivate MarkCommand + +MarkCommandParser --> AddressBookParser : p +deactivate MarkCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +MarkCommandParser -[hidden]-> AddressBookParser +destroy MarkCommandParser + +AddressBookParser --> LogicManager : p +deactivate AddressBookParser + +LogicManager -> MarkCommand : execute(m) +activate MarkCommand + +MarkCommand -> Model : setPerson(personToEdit, editedPerson) +activate Model + +Model --> MarkCommand +deactivate Model + +create CommandResult +MarkCommand -> CommandResult +activate CommandResult + +CommandResult --> MarkCommand +deactivate CommandResult + +MarkCommand --> LogicManager : r +deactivate MarkCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 0de5673070d..86fde05cffb 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -14,9 +14,10 @@ Class UserPrefs Class UniquePersonList Class Person -Class Address +Class PersonType Class Email Class Name +Class ID Class Phone Class Tag @@ -37,18 +38,22 @@ UserPrefs .up.|> ReadOnlyUserPrefs AddressBook *--> "1" UniquePersonList UniquePersonList --> "~* all" Person + +Person *--> PersonType Person *--> Name +Person *--> ID Person *--> Phone Person *--> Email -Person *--> Address Person *--> "*" Tag Person -[hidden]up--> I UniquePersonList -[hidden]right-> I -Name -[hidden]right-> Phone -Phone -[hidden]right-> Address -Address -[hidden]right-> Email +PersonType -[hidden]right-> Name +Name -[hidden]right-> ID +ID -[hidden]right-> Phone + + ModelManager --> "~* filtered" Person @enduml diff --git a/docs/diagrams/TaggingClassDiagram.puml b/docs/diagrams/TaggingClassDiagram.puml new file mode 100644 index 00000000000..ccd03714e5d --- /dev/null +++ b/docs/diagrams/TaggingClassDiagram.puml @@ -0,0 +1,27 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor LOGIC_COLOR_T4 +skinparam classBackgroundColor LOGIC_COLOR + +package "Tagging"{ +Class "{abstract}\nPerson" as Person +Class Student +Class Ta +Class "{abstract}\nTag" as Tag +Class AssignmentTag +Class AttendanceTag +Class TutorialTag +} + +Student -up-|> Person +Ta -up-|> Person +Student --> "*" AssignmentTag +Student --> "*" AttendanceTag +Student --> "*" TutorialTag +Ta --> "*" TutorialTag + +AssignmentTag -up--|> Tag +AttendanceTag -up--|> Tag +TutorialTag -up---|> Tag +@enduml diff --git a/docs/diagrams/TuttagActivityDiagram.puml b/docs/diagrams/TuttagActivityDiagram.puml new file mode 100644 index 00000000000..89f105f0535 --- /dev/null +++ b/docs/diagrams/TuttagActivityDiagram.puml @@ -0,0 +1,23 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +start +:User enters "tuttag add /t TUE08"; + +:EditTutTagListCommandParser parses input; + +if () then ([command is valid]) + :Updates UniqueTutorialTagList with new TutorialTag;; + :User enters "mark all /t TUE08 /ts as"; + :MarkCommandParser parses input; + if () then ([command is valid]) + :Every person in displayed list gets "TUE08" TutorialTag with TagStatus "ASSIGNED"; + else ([command is invalid]) + :Display error message; + endif +else ([command is invalid]) + :Display error message; +endif +stop +@enduml diff --git a/docs/images/AvailableActivityDiagram.png b/docs/images/AvailableActivityDiagram.png new file mode 100644 index 00000000000..5fa59f13c97 Binary files /dev/null and b/docs/images/AvailableActivityDiagram.png differ diff --git a/docs/images/BetterModelClassDiagram.png b/docs/images/BetterModelClassDiagram.png index 02a42e35e76..c9154416e95 100644 Binary files a/docs/images/BetterModelClassDiagram.png and b/docs/images/BetterModelClassDiagram.png differ diff --git a/docs/images/FindSequenceDiagram.png b/docs/images/FindSequenceDiagram.png new file mode 100644 index 00000000000..d4cd021baa7 Binary files /dev/null and b/docs/images/FindSequenceDiagram.png differ diff --git a/docs/images/MarkSequenceDiagram.png b/docs/images/MarkSequenceDiagram.png new file mode 100644 index 00000000000..705440e78d9 Binary files /dev/null and b/docs/images/MarkSequenceDiagram.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index a19fb1b4ac8..958c84eb7f2 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/TaggingClassDiagram.png b/docs/images/TaggingClassDiagram.png new file mode 100644 index 00000000000..fa7c2e0bd6d Binary files /dev/null and b/docs/images/TaggingClassDiagram.png differ diff --git a/docs/images/TuttagActivityDiagram.png b/docs/images/TuttagActivityDiagram.png new file mode 100644 index 00000000000..c349c0602e3 Binary files /dev/null and b/docs/images/TuttagActivityDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..36af73f68e9 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/findTaResult.png b/docs/images/findTaResult.png new file mode 100644 index 00000000000..ec969ba17d1 Binary files /dev/null and b/docs/images/findTaResult.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png index b1f70470137..691fd12d2fe 100644 Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ diff --git a/docs/images/kaitinghh.png b/docs/images/kaitinghh.png new file mode 100644 index 00000000000..e278caa301a Binary files /dev/null and b/docs/images/kaitinghh.png differ diff --git a/docs/images/wang-xinrong.png b/docs/images/wang-xinrong.png new file mode 100644 index 00000000000..4f8e7806739 Binary files /dev/null and b/docs/images/wang-xinrong.png differ diff --git a/docs/images/wongkj12.png b/docs/images/wongkj12.png new file mode 100644 index 00000000000..6b9544e634a Binary files /dev/null and b/docs/images/wongkj12.png differ diff --git a/docs/images/yongkotaro.png b/docs/images/yongkotaro.png new file mode 100644 index 00000000000..3d299becb4a Binary files /dev/null and b/docs/images/yongkotaro.png differ diff --git a/docs/images/yyccbb.png b/docs/images/yyccbb.png new file mode 100644 index 00000000000..ee832261d44 Binary files /dev/null and b/docs/images/yyccbb.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..a9e038ab5cf 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ --- layout: page -title: AddressBook Level-3 +title: TrAcker --- [![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) @@ -8,10 +8,12 @@ title: AddressBook Level-3 ![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). +**TrAcker is a desktop application for managing your contact details.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). -* If you are interested in using AddressBook, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). -* If you are interested about developing AddressBook, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. +* If you are interested in using **TrAcker**, head over to the [_Quick Start_ section of the **User Guide**](UserGuide. + html#quick-start). +* If you are interested about developing **TrAcker**, the [**Developer Guide**](DeveloperGuide.html) is a good place to + start. **Acknowledgements** diff --git a/docs/team/kaitinghh.md b/docs/team/kaitinghh.md new file mode 100644 index 00000000000..a113a026ee6 --- /dev/null +++ b/docs/team/kaitinghh.md @@ -0,0 +1,46 @@ +--- +layout: page +title: Ho Kai Ting's Project Portfolio Page +--- + +### Project: AddressBook Level 3 + +AddressBook - Level 3 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to undo/redo previous commands. + * What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. + * Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. + * Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. + * Credits: *{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}* + +* **New Feature**: Added a history command that allows the user to navigate to previous commands using up/down keys. + +* **Code contributed**: [RepoSense link]() + +* **Project management**: + * Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub + +* **Enhancements to existing features**: + * Updated the GUI color scheme (Pull requests [\#33](), [\#34]()) + * Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests [\#36](), [\#38]()) + +* **Documentation**: + * User Guide: + * Added documentation for the features `delete` and `find` [\#72]() + * Did cosmetic tweaks to existing documentation of features `clear`, `exit`: [\#74]() + * Developer Guide: + * Added implementation details of the `delete` feature. + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#12](), [\#32](), [\#19](), [\#42]() + * Contributed to forum discussions (examples: [1](), [2](), [3](), [4]()) + * Reported bugs and suggestions for other teams in the class (examples: [1](), [2](), [3]()) + * Some parts of the history feature I added was adopted by several other class mates ([1](), [2]()) + +* **Tools**: + * Integrated a third party library (Natty) to the project ([\#42]()) + * Integrated a new Github plugin (CircleCI) to the team repo + +* _{you can add/remove categories in the list above}_ diff --git a/docs/team/wangxinrong.md b/docs/team/wangxinrong.md new file mode 100644 index 00000000000..6794ac53041 --- /dev/null +++ b/docs/team/wangxinrong.md @@ -0,0 +1,44 @@ +--- +layout: page +title: Wang Xinrong's Project Portfolio Page +--- + +### Project: AddressBook Level 3 + +AddressBook - Level 3 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to undo/redo previous commands. + * What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. + * Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. + * Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. + * Credits: *{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}* + +* **New Feature**: Added a history command that allows the user to navigate to previous commands using up/down keys. + +* **Code contributed**: [RepoSense link]() + +* **Project management**: + * Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub + +* **Enhancements to existing features**: + * Updated the GUI color scheme (Pull requests [\#33](), [\#34]()) + * Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests [\#36](), [\#38]()) + +* **Documentation**: + * User Guide: + * Added documentation for the features `delete` and `find` [\#72]() + * Did cosmetic tweaks to existing documentation of features `clear`, `exit`: [\#74]() + * Developer Guide: + * Added implementation details of the `delete` feature. + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#12](), [\#32](), [\#19](), [\#42]() + * Contributed to forum discussions (examples: [1](), [2](), [3](), [4]()) + * Reported bugs and suggestions for other teams in the class (examples: [1](), [2](), [3]()) + * Some parts of the history feature I added was adopted by several other class mates ([1](), [2]()) + +* **Tools**: + * Integrated a third party library (Natty) to the project ([\#42]()) + * Integrated a new Github plugin (CircleCI) to the team repo diff --git a/docs/team/wongkj12.md b/docs/team/wongkj12.md new file mode 100644 index 00000000000..4c242ce1785 --- /dev/null +++ b/docs/team/wongkj12.md @@ -0,0 +1,46 @@ +--- +layout: page +title: Kai Jie's Project Portfolio Page +--- + +### Project: AddressBook Level 3 + +AddressBook - Level 3 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to undo/redo previous commands. + * What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. + * Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. + * Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. + * Credits: *{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}* + +* **New Feature**: Added a history command that allows the user to navigate to previous commands using up/down keys. + +* **Code contributed**: [RepoSense link]() + +* **Project management**: + * Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub + +* **Enhancements to existing features**: + * Updated the GUI color scheme (Pull requests [\#33](), [\#34]()) + * Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests [\#36](), [\#38]()) + +* **Documentation**: + * User Guide: + * Added documentation for the features `delete` and `find` [\#72]() + * Did cosmetic tweaks to existing documentation of features `clear`, `exit`: [\#74]() + * Developer Guide: + * Added implementation details of the `delete` feature. + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#12](), [\#32](), [\#19](), [\#42]() + * Contributed to forum discussions (examples: [1](), [2](), [3](), [4]()) + * Reported bugs and suggestions for other teams in the class (examples: [1](), [2](), [3]()) + * Some parts of the history feature I added was adopted by several other class mates ([1](), [2]()) + +* **Tools**: + * Integrated a third party library (Natty) to the project ([\#42]()) + * Integrated a new Github plugin (CircleCI) to the team repo + +* _{you can add/remove categories in the list above}_ diff --git a/docs/team/johndoe.md b/docs/team/yyccbb.md similarity index 100% rename from docs/team/johndoe.md rename to docs/team/yyccbb.md diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 3d6bd06d5af..c62badc0a91 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -12,9 +12,10 @@ import seedu.address.commons.core.Version; import seedu.address.commons.exceptions.DataLoadingException; import seedu.address.commons.util.ConfigUtil; -import seedu.address.commons.util.StringUtil; +import seedu.address.commons.util.StatefulStringUtil; import seedu.address.logic.Logic; import seedu.address.logic.LogicManager; +import seedu.address.logic.parser.StatefulParserUtil; import seedu.address.model.AddressBook; import seedu.address.model.Model; import seedu.address.model.ModelManager; @@ -44,6 +45,7 @@ public class MainApp extends Application { protected Logic logic; protected Storage storage; protected Model model; + protected StatefulStringUtil statefulStringUtil; protected Config config; @Override @@ -65,6 +67,10 @@ public void init() throws Exception { logic = new LogicManager(model, storage); ui = new UiManager(logic); + + StatefulStringUtil.initialize(model); + + StatefulParserUtil.initialize(model); } /** @@ -83,6 +89,8 @@ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { logger.info("Creating a new data file " + storage.getAddressBookFilePath() + " populated with a sample AddressBook."); } + // need to check the initial data against the tutorial tag list to remove the invalid TutoiralTags + // (TutorialTags that are not in the UniqueTutorialTagList yet have been attached to contact entries) initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); } catch (DataLoadingException e) { logger.warning("Data file at " + storage.getAddressBookFilePath() + " could not be loaded." @@ -131,7 +139,7 @@ protected Config initConfig(Path configFilePath) { try { ConfigUtil.saveConfig(initializedConfig, configFilePathUsed); } catch (IOException e) { - logger.warning("Failed to save config file : " + StringUtil.getDetails(e)); + logger.warning("Failed to save config file : " + StatefulStringUtil.getDetails(e)); } return initializedConfig; } @@ -162,7 +170,7 @@ protected UserPrefs initPrefs(UserPrefsStorage storage) { try { storage.saveUserPrefs(initializedPrefs); } catch (IOException e) { - logger.warning("Failed to save config file : " + StringUtil.getDetails(e)); + logger.warning("Failed to save config file : " + StatefulStringUtil.getDetails(e)); } return initializedPrefs; @@ -180,7 +188,7 @@ public void stop() { try { storage.saveUserPrefs(model.getUserPrefs()); } catch (IOException e) { - logger.severe("Failed to save preferences " + StringUtil.getDetails(e)); + logger.severe("Failed to save preferences " + StatefulStringUtil.getDetails(e)); } } } diff --git a/src/main/java/seedu/address/commons/core/index/Index.java b/src/main/java/seedu/address/commons/core/index/Index.java index dd170d8b68d..17cedf18e5c 100644 --- a/src/main/java/seedu/address/commons/core/index/Index.java +++ b/src/main/java/seedu/address/commons/core/index/Index.java @@ -47,6 +47,11 @@ public static Index fromOneBased(int oneBasedIndex) { return new Index(oneBasedIndex - 1); } + @Override + public int hashCode() { + return zeroBasedIndex; + } + @Override public boolean equals(Object other) { if (other == this) { @@ -57,9 +62,8 @@ public boolean equals(Object other) { if (!(other instanceof Index)) { return false; } - Index otherIndex = (Index) other; - return zeroBasedIndex == otherIndex.zeroBasedIndex; + return this.zeroBasedIndex == otherIndex.zeroBasedIndex; } @Override diff --git a/src/main/java/seedu/address/commons/util/StatefulStringUtil.java b/src/main/java/seedu/address/commons/util/StatefulStringUtil.java new file mode 100644 index 00000000000..97d3898c32b --- /dev/null +++ b/src/main/java/seedu/address/commons/util/StatefulStringUtil.java @@ -0,0 +1,149 @@ +package seedu.address.commons.util; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Arrays; + +import javafx.collections.ObservableList; +import seedu.address.model.Model; +import seedu.address.model.tag.Tag; +import seedu.address.model.tag.TagStatus; +import seedu.address.model.tag.TutorialTag; + +/** + * Helper functions for handling strings. + */ +public class StatefulStringUtil { + private static StatefulStringUtil instance = null; + private Model model; + + private StatefulStringUtil(Model model) { + this.model = model; + } + + /** + * Initializes the StatefulStringUtil instance. + */ + public static void initialize(Model model) { + if (instance == null) { + instance = new StatefulStringUtil(model); + } + } + + public static StatefulStringUtil getInstance() { + if (instance == null) { + throw new IllegalStateException("StatefulStringUtil has not been initialized"); + } + return instance; + } + + /** + * Returns true if the {@code sentence} contains the {@code word}. + * Ignores case, and performs subword matching. + *
examples:
+     *       containsWordIgnoreCase("ABc def", "abc") == true
+     *       containsWordIgnoreCase("ABc def", "DEF") == true
+     *       containsWordIgnoreCase("ABc def", "AB") == true
+     *       containsWordIgnoreCase("ABc def", "Ac") == false //not a match
+     *       
+ * @param sentence cannot be null + * @param word cannot be null, cannot be empty + */ + public static boolean containsSubwordIgnoreCase(String sentence, String word) { + requireNonNull(sentence); + requireNonNull(word); + + String preppedWord = word.trim().toLowerCase(); + checkArgument(!preppedWord.isEmpty(), "Word parameter cannot be empty"); + checkArgument(preppedWord.split("\\s+").length == 1, "Word parameter should be a single word"); + + String preppedSentence = sentence; + String[] wordsInPreppedSentence = preppedSentence.split("\\s+"); + + return Arrays.stream(wordsInPreppedSentence) + .anyMatch(wordInPreppedSentence -> wordInPreppedSentence.toLowerCase().contains(preppedWord)); + } + + /** + * Returns true if the {@code tag} contains the {@code word}. + * Ignores case, and performs subword matching if {@code word} is a subword of a valid tutorialtag, + * else it performs a full word match. + * @param tag cannot be null + * @param word cannot be null, cannot be empty + */ + public static boolean tagContainsWordIgnoreCase(Tag tag, String word) { + requireNonNull(tag); + requireNonNull(word); + + String tagName = tag.getTagName().toLowerCase(); + String preppedWord = word.trim(); + checkArgument(!preppedWord.isEmpty(), "Word parameter cannot be empty"); + ObservableList validTutorials = + StatefulStringUtil.getInstance().model.getTutorialTagList(); + + for (TutorialTag tutorial : validTutorials) { + String tutorialGroup = tutorial.getTagName().toLowerCase(); + if (tutorialGroup.contains(preppedWord)) { + return tagName.contains(preppedWord) && tag.isTutorial(); + } + } + return tagName.equalsIgnoreCase(preppedWord); + } + + /** + * Returns true if the {@code tag} contains the {@code tutorialGroup}. + * Ignores case, and performs a full word match. + * @param tag cannot be null + * @param tutorialGroup cannot be null, cannot be empty + */ + public static boolean containsTutorialGroup(Tag tag, String tutorialGroup) { + requireNonNull(tag); + requireNonNull(tutorialGroup); + + String tagName = tag.getTagName(); + checkArgument(!tutorialGroup.isEmpty(), "Tutorial group parameter cannot be empty"); + checkArgument(!tutorialGroup.contains(" "), "Only use one word for tutorial group parameter"); + + ObservableList validTutorials = + StatefulStringUtil.getInstance().model.getTutorialTagList(); + + for (TutorialTag tutorial : validTutorials) { + String validTutorialGroupTag = tutorial.getTagName(); + if (validTutorialGroupTag.equalsIgnoreCase(tutorialGroup)) { + return tagName.equalsIgnoreCase(tutorialGroup) && tag.getTagStatus() == TagStatus.AVAILABLE; + } + } + return false; + } + + /** + * Returns a detailed message of the t, including the stack trace. + */ + public static String getDetails(Throwable t) { + requireNonNull(t); + StringWriter sw = new StringWriter(); + t.printStackTrace(new PrintWriter(sw)); + return t.getMessage() + "\n" + sw.toString(); + } + + /** + * Returns true if {@code s} represents a non-zero unsigned integer + * e.g. 1, 2, 3, ..., {@code Integer.MAX_VALUE}
+ * Will return false for any other non-null string input + * e.g. empty string, "-1", "0", "+1", and " 2 " (untrimmed), "3 0" (contains whitespace), "1 a" (contains letters) + * @throws NullPointerException if {@code s} is null. + */ + public static boolean isNonZeroUnsignedInteger(String s) { + requireNonNull(s); + + try { + int value = Integer.parseInt(s); + return value > 0 && !s.startsWith("+"); // "+1" is successfully parsed by Integer#parseInt(String) + } catch (NumberFormatException nfe) { + return false; + } + } +} diff --git a/src/main/java/seedu/address/commons/util/StringListUtil.java b/src/main/java/seedu/address/commons/util/StringListUtil.java new file mode 100644 index 00000000000..6d41c12f912 --- /dev/null +++ b/src/main/java/seedu/address/commons/util/StringListUtil.java @@ -0,0 +1,30 @@ +package seedu.address.commons.util; + +import java.util.ArrayList; +import java.util.List; + +/** + * This ListUtil class contains additional methods for list manipulation. + */ +public class StringListUtil { + + /** + * Given a list of strings, returns a new list of strings separated by white spaces. + * @param originalList list of strings + * @return separatedList a new list of strings separated by white spaces + */ + public static List separateWithSpaces(List originalList) { + List separatedList = new ArrayList<>(); + for (String str : originalList) { + if (str.contains(" ")) { + String[] parts = str.split("\\s+"); + for (String part : parts) { + separatedList.add(part); + } + } else { + separatedList.add(str); + } + } + return separatedList; + } +} diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/address/commons/util/StringUtil.java deleted file mode 100644 index 61cc8c9a1cb..00000000000 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ /dev/null @@ -1,68 +0,0 @@ -package seedu.address.commons.util; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.Arrays; - -/** - * Helper functions for handling strings. - */ -public class StringUtil { - - /** - * Returns true if the {@code sentence} contains the {@code word}. - * Ignores case, but a full word match is required. - *
examples:
-     *       containsWordIgnoreCase("ABc def", "abc") == true
-     *       containsWordIgnoreCase("ABc def", "DEF") == true
-     *       containsWordIgnoreCase("ABc def", "AB") == false //not a full word match
-     *       
- * @param sentence cannot be null - * @param word cannot be null, cannot be empty, must be a single word - */ - public static boolean containsWordIgnoreCase(String sentence, String word) { - requireNonNull(sentence); - requireNonNull(word); - - String preppedWord = word.trim(); - checkArgument(!preppedWord.isEmpty(), "Word parameter cannot be empty"); - checkArgument(preppedWord.split("\\s+").length == 1, "Word parameter should be a single word"); - - String preppedSentence = sentence; - String[] wordsInPreppedSentence = preppedSentence.split("\\s+"); - - return Arrays.stream(wordsInPreppedSentence) - .anyMatch(preppedWord::equalsIgnoreCase); - } - - /** - * Returns a detailed message of the t, including the stack trace. - */ - public static String getDetails(Throwable t) { - requireNonNull(t); - StringWriter sw = new StringWriter(); - t.printStackTrace(new PrintWriter(sw)); - return t.getMessage() + "\n" + sw.toString(); - } - - /** - * Returns true if {@code s} represents a non-zero unsigned integer - * e.g. 1, 2, 3, ..., {@code Integer.MAX_VALUE}
- * Will return false for any other non-null string input - * e.g. empty string, "-1", "0", "+1", and " 2 " (untrimmed), "3 0" (contains whitespace), "1 a" (contains letters) - * @throws NullPointerException if {@code s} is null. - */ - public static boolean isNonZeroUnsignedInteger(String s) { - requireNonNull(s); - - try { - int value = Integer.parseInt(s); - return value > 0 && !s.startsWith("+"); // "+1" is successfully parsed by Integer#parseInt(String) - } catch (NumberFormatException nfe) { - return false; - } - } -} diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 92cd8fa605a..95cbb436453 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -4,6 +4,7 @@ import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; +import seedu.address.logic.commands.Command; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; @@ -16,12 +17,14 @@ public interface Logic { /** * Executes the command and returns the result. - * @param commandText The command as entered by the user. + * @param command The requested command. * @return the result of the command execution. * @throws CommandException If an error occurs during command execution. * @throws ParseException If an error occurs during parsing. */ - CommandResult execute(String commandText) throws CommandException, ParseException; + CommandResult execute(Command command) throws CommandException, ParseException; + + Command parseCommand(String commandText) throws CommandException, ParseException; /** * Returns the AddressBook. diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 5aa3b91c7d0..7a2b5d45249 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -43,11 +43,15 @@ public LogicManager(Model model, Storage storage) { } @Override - public CommandResult execute(String commandText) throws CommandException, ParseException { + public Command parseCommand(String commandText) throws CommandException, ParseException { logger.info("----------------[USER COMMAND][" + commandText + "]"); + Command command = addressBookParser.parseCommand(commandText); + return command; + } + @Override + public CommandResult execute(Command command) throws CommandException, ParseException { CommandResult commandResult; - Command command = addressBookParser.parseCommand(commandText); commandResult = command.execute(model); try { diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java index ecd32c31b53..8251d08d1fb 100644 --- a/src/main/java/seedu/address/logic/Messages.java +++ b/src/main/java/seedu/address/logic/Messages.java @@ -6,6 +6,8 @@ import seedu.address.logic.parser.Prefix; import seedu.address.model.person.Person; +import seedu.address.model.person.PersonType; +import seedu.address.model.tag.Tag; /** * Container for user visible messages. @@ -14,10 +16,18 @@ public class Messages { public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; - public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; + public static final String MESSAGE_INVALID_PERSON_TYPE = PersonType.MESSAGE_CONSTRAINTS; + public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided %1$s is invalid"; public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; + public static final String MESSAGE_PERSON_LISTED_OVERVIEW = "%1$d person listed!"; public static final String MESSAGE_DUPLICATE_FIELDS = "Multiple values specified for the following single-valued field(s): "; + public static final String MESSAGE_INVALID_TUTORIAL_TAG_VALUE = + "Specified tutorial tag name is not allowed: "; + + public static final String MESSAGE_AVAILABLE_TAS_OVERVIEW = "%1$d TAs available for the tutorial group."; + + public static final String MESSAGE_INVALID_TAG_NAME = Tag.MESSAGE_CONSTRAINTS; /** * Returns an error message indicating the duplicate prefixes. @@ -37,13 +47,16 @@ public static String getErrorMessageForDuplicatePrefixes(Prefix... duplicatePref public static String format(Person person) { final StringBuilder builder = new StringBuilder(); builder.append(person.getName()) + .append("; ID: ") + .append(person.getId()) .append("; Phone: ") .append(person.getPhone()) .append("; Email: ") - .append(person.getEmail()) - .append("; Address: ") - .append(person.getAddress()) - .append("; Tags: "); + .append(person.getEmail()); + if (person.getTags().isEmpty()) { + return builder.toString(); + } + builder.append("; Tags: "); person.getTags().forEach(builder::append); 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..c88fde106ac 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -1,11 +1,12 @@ 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_ID; 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.TYPE_STU; +import static seedu.address.logic.parser.CliSyntax.TYPE_TA; import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; @@ -20,20 +21,19 @@ public class AddCommand extends Command { public static final String COMMAND_WORD = "add"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a Student / TA to TrAcker.\n" + "Parameters: " - + PREFIX_NAME + "NAME " - + PREFIX_PHONE + "PHONE " - + PREFIX_EMAIL + "EMAIL " - + PREFIX_ADDRESS + "ADDRESS " - + "[" + PREFIX_TAG + "TAG]...\n" + + "[" + TYPE_STU + " | " + TYPE_TA + "] " + + PREFIX_NAME + " NAME " + + PREFIX_ID + " ID " + + PREFIX_PHONE + " PHONE " + + PREFIX_EMAIL + " EMAIL \n" + "Example: " + COMMAND_WORD + " " - + PREFIX_NAME + "John Doe " - + PREFIX_PHONE + "98765432 " - + PREFIX_EMAIL + "johnd@example.com " - + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " - + PREFIX_TAG + "friends " - + PREFIX_TAG + "owesMoney"; + + TYPE_STU + " " + + PREFIX_NAME + " John Doe " + + PREFIX_ID + " A0123456Z " + + PREFIX_PHONE + " 98765432 " + + PREFIX_EMAIL + " johnd@example.com "; 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/AvailableCommand.java b/src/main/java/seedu/address/logic/commands/AvailableCommand.java new file mode 100644 index 00000000000..4ccd0eb4af2 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AvailableCommand.java @@ -0,0 +1,63 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GROUP; + +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.TutorialTagContainsGroupPredicate; +import seedu.address.model.tag.TutorialTag; + +/** + * Finds and lists all available TAs for the tutorial group. + */ +public class AvailableCommand extends Command { + public static final String COMMAND_WORD = "available"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all available TAs for the tutorial slot.\n" + + "Parameters: /g TUTORIAL \n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_GROUP + " TUES08 "; + + public static final String MESSAGE_NON_EMPTY_GROUP_NAME = "Tutorial group name parameter cannot be empty"; + public static final String SAMPLE_COMMAND = COMMAND_WORD + " " + PREFIX_GROUP + " WED10 "; + + private final TutorialTagContainsGroupPredicate predicate; + private final TutorialTag tutorialGroup; + + /** + * Creates an AvailableCommand to find all available TAs for the tutorial group. + */ + public AvailableCommand(TutorialTagContainsGroupPredicate predicate, TutorialTag tutorialGroup) { + this.predicate = predicate; + this.tutorialGroup = tutorialGroup; + } + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + if (!model.hasTutorialTag(tutorialGroup)) { + throw new CommandException(Messages.MESSAGE_INVALID_TUTORIAL_TAG_VALUE + tutorialGroup.getTagName()); + } + + model.updateFilteredPersonList(predicate); + return new CommandResult( + String.format(Messages.MESSAGE_AVAILABLE_TAS_OVERVIEW, model.getFilteredPersonList().size())); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AvailableCommand)) { + return false; + } + + AvailableCommand otherAvailableCommand = (AvailableCommand) other; + return predicate.equals(otherAvailableCommand.predicate); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 9c86b1fa6e4..2564621acfd 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -13,7 +13,9 @@ public class ClearCommand extends Command { public static final String COMMAND_WORD = "clear"; public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; - + public ClearCommand() { + this.needsWarningPopup = true; + } @Override public CommandResult execute(Model model) { requireNonNull(model); diff --git a/src/main/java/seedu/address/logic/commands/Command.java b/src/main/java/seedu/address/logic/commands/Command.java index 64f18992160..1fdaec0c931 100644 --- a/src/main/java/seedu/address/logic/commands/Command.java +++ b/src/main/java/seedu/address/logic/commands/Command.java @@ -7,6 +7,7 @@ * Represents a command with hidden internal logic and the ability to be executed. */ public abstract class Command { + protected boolean needsWarningPopup = false; /** * Executes the command and returns the result message. @@ -16,5 +17,8 @@ public abstract class Command { * @throws CommandException If an error occurs during command execution. */ public abstract CommandResult execute(Model model) throws CommandException; + public boolean getNeedsWarningPopup() { + return needsWarningPopup; + } } diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index 1135ac19b74..0e07e7cc493 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -1,8 +1,11 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import java.util.ArrayList; import java.util.List; +import java.util.Set; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.ToStringBuilder; @@ -19,30 +22,81 @@ public class DeleteCommand extends Command { public static final String COMMAND_WORD = "delete"; public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Deletes the person identified by the index number used in the displayed person list.\n" - + "Parameters: INDEX (must be a positive integer)\n" - + "Example: " + COMMAND_WORD + " 1"; + + ": Deletes the person(s) identified by the index number used in the displayed person list.\n" + + "Parameters: (all | INDEX (must be a positive integer within the size of the displayed list) " + + "[OTHER_INDICES]) \n" + + "Example 1: " + COMMAND_WORD + " 1\n" + + "Example 2: " + COMMAND_WORD + " 1 5 3"; public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; - private final Index targetIndex; + private final Set indices; - public DeleteCommand(Index targetIndex) { - this.targetIndex = targetIndex; + /** + * Constructor for the delete command. + * @param indices set of indices whose contact entry are to be deleted. + */ + public DeleteCommand(Set indices) { + requireNonNull(indices); + this.needsWarningPopup = true; + this.indices = indices; } @Override public CommandResult execute(Model model) throws CommandException { requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); + // need not check for duplicate indices because a Set is used + List lastShownList = new ArrayList<>(model.getFilteredPersonList()); + + StringBuilder resultStringBuilder; + + checksInvalidIndices(lastShownList, indices); + try { + resultStringBuilder = indices.stream().map(x -> { + try { + return executeHelper(model, lastShownList, x); + } catch (CommandException e) { + throw new RuntimeException(e); + } + }).reduce(StringBuilder::append).orElse(new StringBuilder()); + } catch (RuntimeException e) { + Throwable cause = e.getCause(); + if (cause instanceof CommandException) { + throw (CommandException) cause; + } else { + throw e; + } + } - if (targetIndex.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + resultStringBuilder.deleteCharAt(resultStringBuilder.length() - 1); // remove the last '\n' + return new CommandResult(resultStringBuilder.toString()); + } + private void checksInvalidIndices(List lastShownList, Set indices) throws CommandException { + // check whether index specified is within valid range + List invalidIndices = new ArrayList<>(); + for (Index index: indices) { + if (index.getZeroBased() >= lastShownList.size()) { + invalidIndices.add(index.getOneBased()); + } + } + if (invalidIndices.size() > 0) { + throw new CommandException( + String.format(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX, invalidIndices)); } + } + + /** + * Deletes a single {@code Person}. Index is guaranteed to be a valid index. + */ + private StringBuilder executeHelper(Model model, List lastShownList, Index index) throws CommandException { + requireAllNonNull(model, lastShownList, index); - Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); + Person personToDelete = lastShownList.get(index.getZeroBased()); model.deletePerson(personToDelete); - return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(personToDelete))); + + return new StringBuilder() + .append(String.format(MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(personToDelete))) + .append("\n"); } @Override @@ -57,13 +111,13 @@ public boolean equals(Object other) { } DeleteCommand otherDeleteCommand = (DeleteCommand) other; - return targetIndex.equals(otherDeleteCommand.targetIndex); + return indices.equals(otherDeleteCommand.indices); } @Override public String toString() { return new ToStringBuilder(this) - .add("targetIndex", targetIndex) + .add("index", indices.toString()) .toString(); } } diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 4b581c7331e..a8abc075c89 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -1,15 +1,11 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; -import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -21,10 +17,11 @@ 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.Id; import seedu.address.model.person.Name; import seedu.address.model.person.Person; +import seedu.address.model.person.PersonType; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; @@ -35,18 +32,16 @@ public class EditCommand extends Command { public static final String COMMAND_WORD = "edit"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the person identified " + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the particulars of the person identified " + "by the index number used in the displayed person list. " + "Existing values will be overwritten by the input values.\n" - + "Parameters: INDEX (must be a positive integer) " - + "[" + PREFIX_NAME + "NAME] " - + "[" + PREFIX_PHONE + "PHONE] " - + "[" + PREFIX_EMAIL + "EMAIL] " - + "[" + PREFIX_ADDRESS + "ADDRESS] " - + "[" + PREFIX_TAG + "TAG]...\n" + + "Parameters: INDEX (must be a positive integer within the size of the displayed list) " + + "[" + PREFIX_NAME + " NAME] " + + "[" + PREFIX_PHONE + " PHONE] " + + "[" + PREFIX_EMAIL + " EMAIL]\n" + "Example: " + COMMAND_WORD + " 1 " - + PREFIX_PHONE + "91234567 " - + PREFIX_EMAIL + "johndoe@example.com"; + + PREFIX_PHONE + " 91234567 " + + PREFIX_EMAIL + " johndoe@example.com"; public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; @@ -95,13 +90,15 @@ public CommandResult execute(Model model) throws CommandException { private static Person createEditedPerson(Person personToEdit, EditPersonDescriptor editPersonDescriptor) { assert personToEdit != null; + PersonType currentType = personToEdit.getType(); Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName()); + Id currentId = personToEdit.getId(); Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); - Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); - Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); + Set currentTags = personToEdit.getTags(); - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + return Person.of(currentType, updatedName, currentId, updatedPhone, updatedEmail, + currentTags); } @Override @@ -136,8 +133,6 @@ public static class EditPersonDescriptor { private Name name; private Phone phone; private Email email; - private Address address; - private Set tags; public EditPersonDescriptor() {} @@ -149,15 +144,13 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { setName(toCopy.name); setPhone(toCopy.phone); setEmail(toCopy.email); - setAddress(toCopy.address); - setTags(toCopy.tags); } /** * Returns true if at least one field is edited. */ public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); + return CollectionUtil.isAnyNonNull(name, phone, email); } public void setName(Name name) { @@ -184,31 +177,6 @@ public Optional getEmail() { return Optional.ofNullable(email); } - public void setAddress(Address address) { - this.address = address; - } - - public Optional
getAddress() { - return Optional.ofNullable(address); - } - - /** - * Sets {@code tags} to this object's {@code tags}. - * A defensive copy of {@code tags} is used internally. - */ - public void setTags(Set tags) { - this.tags = (tags != null) ? new HashSet<>(tags) : null; - } - - /** - * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} - * if modification is attempted. - * Returns {@code Optional#empty()} if {@code tags} is null. - */ - public Optional> getTags() { - return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); - } - @Override public boolean equals(Object other) { if (other == this) { @@ -223,9 +191,7 @@ public boolean equals(Object other) { EditPersonDescriptor otherEditPersonDescriptor = (EditPersonDescriptor) other; return Objects.equals(name, otherEditPersonDescriptor.name) && Objects.equals(phone, otherEditPersonDescriptor.phone) - && Objects.equals(email, otherEditPersonDescriptor.email) - && Objects.equals(address, otherEditPersonDescriptor.address) - && Objects.equals(tags, otherEditPersonDescriptor.tags); + && Objects.equals(email, otherEditPersonDescriptor.email); } @Override @@ -234,8 +200,6 @@ public String toString() { .add("name", name) .add("phone", phone) .add("email", email) - .add("address", address) - .add("tags", tags) .toString(); } } diff --git a/src/main/java/seedu/address/logic/commands/EditTutTagListCommand.java b/src/main/java/seedu/address/logic/commands/EditTutTagListCommand.java new file mode 100644 index 00000000000..745479a4211 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/EditTutTagListCommand.java @@ -0,0 +1,146 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; +import seedu.address.model.tag.TagStatus; +import seedu.address.model.tag.TutorialTag; + +/** + * Finds and lists all persons in address book whose name contains any of the argument keywords. + * Keyword matching is case insensitive. + */ +public class EditTutTagListCommand extends Command { + + /** + * Represents the subtype of the EditTutTagListCommand. ADD represents command to add an available + * tutorial tag, DELETE represents command to delete an available tutorial tag and LIST represents + * command to list all available tutorial tags. + */ + public enum CommandSubtype { ADD, DELETE, LIST }; + + public static final String COMMAND_WORD = "tuttag"; + public static final String ADD_FLAG = "add"; + public static final String DELETE_FLAG = "del"; + public static final String LIST_FLAG = "list"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Creates a TutorialTag to be used with the specified " + + "TagName.\n" + + "Parameters: MODE (must be 'add' for adding tags, 'del' for deleting tags or 'list' for listing " + + "existing tags) " + + PREFIX_TAG + " TAGNAME\n" + + "Example: " + COMMAND_WORD + " " + ADD_FLAG + " " + PREFIX_TAG + " THU10\n"; + + public static final String SAMPLE_COMMAND = COMMAND_WORD + " " + ADD_FLAG + " " + PREFIX_TAG + " WED10"; + public static final String MESSAGE_SUCCESS = "New tutorial tag added: %1$s"; + public static final String MESSAGE_DUPLICATE_TUTORIALTAG = "This tutorial tag already exists in the address book"; + public static final String EMPTY_TUTORIALTAGLIST_OUTPUT = "Available Tutorial Tag(s): [ ]"; + private final String tagName; + private final CommandSubtype commandType; + + /** + * Creates a new EditTutTagListCommand. + * + * @param tagName TagName of the tutorial tag to be added or deleted. + * @param commandType indicates the command subtype. + */ + public EditTutTagListCommand(String tagName, CommandSubtype commandType) { + this.tagName = tagName; + this.commandType = commandType; + } + + /** + * Creates a new EditTutTagListCommand. + * + * @param commandType indicates the command subtype. + */ + public EditTutTagListCommand(CommandSubtype commandType) { + this.commandType = commandType; + this.tagName = null; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + if (commandType == CommandSubtype.ADD) { + TutorialTag tag = new TutorialTag(tagName, TagStatus.AVAILABLE); + if (model.hasTutorialTag(tag)) { + throw new CommandException(MESSAGE_DUPLICATE_TUTORIALTAG); + } + model.addTutorialTag(tag); + } + + if (commandType == CommandSubtype.DELETE) { + // Check if specified tutorial tag exists + TutorialTag tag = new TutorialTag(tagName, TagStatus.AVAILABLE); + if (!model.hasTutorialTag(tag)) { + throw new CommandException(Messages.MESSAGE_INVALID_TUTORIAL_TAG_VALUE + tagName); + } + + model.deleteTutorialTag(new TutorialTag(tagName, TagStatus.AVAILABLE)); + + // Remove the specified tag from all persons + List entireList = model.getPersonList(); + for (Person person : entireList) { + Set currTags = new HashSet<>(person.getTags()); + assert person != null; + Person editedPerson = createEditedPerson(person, Tag.removeTutTagFromTagSet(currTags, this.tagName)); + + // Update the person list + model.setPerson(person, editedPerson); + } + + } + + if (commandType == CommandSubtype.LIST) { + // nothing needs to be done + } + + return new CommandResult(model.getTutorialTagListString()); + } + + /** + * Creates and returns a {@code Person} with the details of {@code personToEdit} + */ + private static Person createEditedPerson(Person personToEdit, Set newTags) { + assert personToEdit != null; + return Person.of(personToEdit.getType(), personToEdit.getName(), personToEdit.getId(), + personToEdit.getPhone(), personToEdit.getEmail(), newTags); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditTutTagListCommand)) { + return false; + } + + EditTutTagListCommand otherFindCommand = (EditTutTagListCommand) other; + return tagName.equals(otherFindCommand.tagName); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("tagName", tagName) + .toString(); + } + + public static boolean isListCommand(CommandSubtype type) { + return type == CommandSubtype.LIST; + } +} diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index 72b9eddd3a7..7f8e73ac738 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -2,34 +2,43 @@ import static java.util.Objects.requireNonNull; +import java.util.List; + import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.FieldContainsKeywordsPredicate; /** * Finds and lists all persons in address book whose name contains any of the argument keywords. - * Keyword matching is case insensitive. + * Keyword matching is case-insensitive. */ public class FindCommand extends Command { public static final String COMMAND_WORD = "find"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " - + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" - + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" - + "Example: " + COMMAND_WORD + " alice bob charlie"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Filters all persons whose contact details contain " + + "the specified keywords (case-insensitive) under the specified flag and displays them as a list with " + + "index numbers.\n" + + "Parameters: [stu | ta] [/n NAME] [/i ID] [/p PHONE] [/e EMAIL]\n" + + "Example: " + COMMAND_WORD + " stu /n grace /p 900"; - private final NameContainsKeywordsPredicate predicate; + private final List predicates; - public FindCommand(NameContainsKeywordsPredicate predicate) { - this.predicate = predicate; + public FindCommand(List predicates) { + this.predicates = predicates; } @Override public CommandResult execute(Model model) { requireNonNull(model); - model.updateFilteredPersonList(predicate); + model.persistentUpdateFilteredList(this.predicates); + + int filteredListSize = model.getFilteredPersonList().size(); + if (filteredListSize <= 1) { + return new CommandResult( + String.format(Messages.MESSAGE_PERSON_LISTED_OVERVIEW, model.getFilteredPersonList().size())); + } return new CommandResult( String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); } @@ -46,13 +55,15 @@ public boolean equals(Object other) { } FindCommand otherFindCommand = (FindCommand) other; - return predicate.equals(otherFindCommand.predicate); + return predicates.equals(otherFindCommand.predicates); } - + // UPDATE THIS @Override public String toString() { - return new ToStringBuilder(this) - .add("predicate", predicate) - .toString(); + ToStringBuilder sb = new ToStringBuilder(this); + for (FieldContainsKeywordsPredicate predicate : predicates) { + sb.add("predicate", predicate); + } + return sb.toString(); } } diff --git a/src/main/java/seedu/address/logic/commands/MarkCommand.java b/src/main/java/seedu/address/logic/commands/MarkCommand.java new file mode 100644 index 00000000000..c4bbcf34604 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/MarkCommand.java @@ -0,0 +1,163 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAGSTATUS; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; +import seedu.address.model.tag.TagStatus; +import seedu.address.model.tag.TagType; +import seedu.address.model.tag.TutorialTag; + +/** + * Changes the remark of an existing person in the address book. + */ +public class MarkCommand extends Command { + + public static final String COMMAND_WORD = "mark"; + + public static final String SAMPLE_COMMAND = COMMAND_WORD + " 1 " + PREFIX_TAG + + " assignment1 " + PREFIX_TAGSTATUS + " cg"; + public static final String SAMPLE_COMMAND_2 = COMMAND_WORD + " 1 2 " + PREFIX_TAG + + " assignment2 assignment3 " + PREFIX_TAGSTATUS + " ig"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Updates the status of the specified tag(s) " + + "of specified contact entry(s) with the specified status.\n" + + "If the tag(s) specified does not exist, a new tag with the tag name" + + " and tag status would be created.\n" + + "Parameters: (all | INDEX (must be a positive integer within the size of the displayed list) " + + "[OTHER_INDICES]) " + + PREFIX_TAG + " TAG_NAME [OTHER_TAG_NAMES] " + PREFIX_TAGSTATUS + " TAG_STATUS\n" + + "Example 1: " + SAMPLE_COMMAND + "\n" + + "Example 2: " + SAMPLE_COMMAND_2; + + public static final String MESSAGE_MARK_PERSON_SUCCESS = "Updated Person: %1$s"; + private final Set indices; + private final Set tagNames; + private final TagStatus tagStatus; + + /** + * @param indices of the person in the filtered person list to update tag status + * @param tagNames name of the tag whose status is to be updated + * @param tagStatus the status to update the specified tag with + */ + public MarkCommand(Set indices, Set tagNames, TagStatus tagStatus) { + requireAllNonNull(indices, tagNames, tagStatus); + this.indices = indices; + this.tagNames = tagNames; + this.tagStatus = tagStatus; + } + + /** + * Edit the tags of a single {@code Person}. + */ + private StringBuilder executeHelper(Model model, Index index) throws CommandException { + requireAllNonNull(model, index); + List lastShownList = model.getFilteredPersonList(); + + // check whether index specified is within valid range + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(String.format(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX, + index.getOneBased())); + } + + Person personToEdit = lastShownList.get(index.getZeroBased()); + Set currTags = new HashSet<>(personToEdit.getTags()); + + // create a new person with the new tag(s), necessary as the person fields are final + Person editedPerson = createEditedPerson(personToEdit, Tag.updateTagsWithNewTags(currTags, + this.tagNames, this.tagStatus)); + + model.setPerson(personToEdit, editedPerson); + + return new StringBuilder() + .append(String.format(MESSAGE_MARK_PERSON_SUCCESS, Messages.format(editedPerson))) + .append("\n"); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + // need not check for duplicate indices because a Set is used + + // + + // only tutorial tags with predefined tag names are allowed + if (Tag.getTagTypeWithTagStatus(tagStatus) == TagType.TUTORIAL) { + for (String tagName: tagNames) { + TutorialTag tag = new TutorialTag(tagName, TagStatus.AVAILABLE); + if (!model.hasTutorialTag(tag)) { + throw new CommandException(Messages.MESSAGE_INVALID_TUTORIAL_TAG_VALUE + tagName); + } + } + } + + StringBuilder resultStringBuilder; + try { + resultStringBuilder = indices.stream().map(x -> { + try { + return executeHelper(model, x); + } catch (CommandException e) { + throw new RuntimeException(e); + } + }).reduce(StringBuilder::append).orElse(new StringBuilder()); + } catch (RuntimeException e) { + Throwable cause = e.getCause(); + if (cause instanceof CommandException) { + throw (CommandException) cause; + } else { + throw e; + } + } + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + resultStringBuilder.deleteCharAt(resultStringBuilder.length() - 1); // remove the last '\n' + return new CommandResult(resultStringBuilder.toString()); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof MarkCommand)) { + return false; + } + + // state check + MarkCommand e = (MarkCommand) other; + return indices.equals(e.indices) + && tagNames.equals(e.tagNames) + && tagStatus.equals(e.tagStatus); + } + + /** + * Creates and returns a {@code Person} with the details of {@code personToEdit} + */ + private static Person createEditedPerson(Person personToEdit, Set newTags) { + assert personToEdit != null; + return Person.of(personToEdit.getType(), personToEdit.getName(), personToEdit.getId(), + personToEdit.getPhone(), personToEdit.getEmail(), newTags); + } + @Override + public String toString() { + return new ToStringBuilder(this) + .add("index", indices.toString()) + .add("tagName(s)", tagNames) + .add("tagStatus", tagStatus) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/RemovetagCommand.java b/src/main/java/seedu/address/logic/commands/RemovetagCommand.java new file mode 100644 index 00000000000..0d4407c14d1 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/RemovetagCommand.java @@ -0,0 +1,133 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; + +/** + * Changes the remark of an existing person in the address book. + */ +public class RemovetagCommand extends Command { + + public static final String COMMAND_WORD = "removetag"; + public static final String SAMPLE_COMMAND = COMMAND_WORD + " 1 " + PREFIX_TAG + " assignment1 "; + // to be updated + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Removes the specified tag from the specified" + + " contact entry.\n" + + "Parameters: (all | INDEX (must be a positive integer within the size of the displayed list) " + + "[OTHER_INDICES]) " + + PREFIX_TAG + " TAG\n" + + "Example: " + SAMPLE_COMMAND; + + public static final String MESSAG_REMOVETAG_SUCCESS = "Updated Person: %1$s"; + private final Set indices; + private final Set tagNames; + + /** + * @param indices of the person in the filtered person list to have the tag deleted. + * @param tagNames name of the tag to be deleted. + */ + public RemovetagCommand(Set indices, Set tagNames) { + requireAllNonNull(indices, tagNames); + this.indices = indices; + this.tagNames = tagNames; + } + + /** + * Removes the specified tags of a single {@code Person}. + */ + private StringBuilder executeHelper(Model model, Index index) throws CommandException { + requireAllNonNull(model, index); + List lastShownList = model.getFilteredPersonList(); + + // check whether index specified is within valid range + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToEdit = lastShownList.get(index.getZeroBased()); + Set currTags = new HashSet<>(personToEdit.getTags()); + + // create a new person with the new tag(s), necessary as the person fields are final + Person editedPerson = createEditedPerson(personToEdit, Tag.removeTagsFromTagSet(currTags, + this.tagNames)); + + model.setPerson(personToEdit, editedPerson); + + return new StringBuilder() + .append(String.format(MESSAG_REMOVETAG_SUCCESS, Messages.format(editedPerson))) + .append("\n"); + } + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + // need not check for duplicate indices because a Set is used + + StringBuilder resultStringBuilder; + try { + resultStringBuilder = indices.stream().map(x -> { + try { + return executeHelper(model, x); + } catch (CommandException e) { + throw new RuntimeException(e); + } + }).reduce(StringBuilder::append).orElse(new StringBuilder()); + } catch (RuntimeException e) { + Throwable cause = e.getCause(); + if (cause instanceof CommandException) { + throw (CommandException) cause; + } else { + throw e; + } + } + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + resultStringBuilder.deleteCharAt(resultStringBuilder.length() - 1); // remove the last '\n' + return new CommandResult(resultStringBuilder.toString()); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof RemovetagCommand)) { + return false; + } + + // state check + RemovetagCommand e = (RemovetagCommand) other; + return indices.equals(e.indices) + && tagNames.equals(e.tagNames); + } + + /** + * Creates and returns a {@code Person} with the details of {@code personToEdit} + */ + private static Person createEditedPerson(Person personToEdit, Set newTags) { + assert personToEdit != null; + return Person.of(personToEdit.getType(), personToEdit.getName(), personToEdit.getId(), + personToEdit.getPhone(), personToEdit.getEmail(), newTags); + } + @Override + public String toString() { + return new ToStringBuilder(this) + .add("index", indices.toString()) + .add("tagName(s)", tagNames).toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java b/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java index a16bd14f2cd..f44c0c19186 100644 --- a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java +++ b/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java @@ -1,5 +1,7 @@ package seedu.address.logic.commands.exceptions; +import seedu.address.logic.commands.Command; + /** * Represents an error which occurs during execution of a {@link Command}. */ diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 4ff1a97ed77..bdcc9ef0c5f 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -1,21 +1,20 @@ 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_ID; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import java.util.Set; -import java.util.stream.Stream; +import java.util.HashSet; 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.Id; import seedu.address.model.person.Name; import seedu.address.model.person.Person; +import seedu.address.model.person.PersonType; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; @@ -31,31 +30,24 @@ public class AddCommandParser implements Parser { */ public AddCommand parse(String args) throws ParseException { ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_ID, PREFIX_PHONE, + PREFIX_EMAIL); - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) - || !argMultimap.getPreamble().isEmpty()) { + if (!StatefulParserUtil.arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ID, PREFIX_PHONE, + PREFIX_EMAIL)) { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); } - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); - Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); - Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); - Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); - Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); - Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_ID, PREFIX_PHONE, PREFIX_EMAIL); + PersonType type = StatefulParserUtil.parsePersonType(argMultimap.getPreamble()); + Name name = StatefulParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); + Id id = StatefulParserUtil.parseId(argMultimap.getValue(PREFIX_ID).get()); + Phone phone = StatefulParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); + Email email = StatefulParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); - Person person = new Person(name, phone, email, address, tagList); + Person person = Person.of(type, name, id, phone, email, new HashSet()); return new AddCommand(person); } - /** - * Returns true if none of the prefixes contains empty {@code Optional} values in the given - * {@code ArgumentMultimap}. - */ - private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { - return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); - } - } diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 3149ee07e0b..c94126b574e 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -9,14 +9,18 @@ import seedu.address.commons.core.LogsCenter; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AvailableCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.Command; import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.EditTutTagListCommand; import seedu.address.logic.commands.ExitCommand; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.MarkCommand; +import seedu.address.logic.commands.RemovetagCommand; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -44,7 +48,7 @@ public Command parseCommand(String userInput) throws ParseException { } final String commandWord = matcher.group("commandWord"); - final String arguments = matcher.group("arguments"); + final String arguments = matcher.group("arguments") + " "; // Note to developers: Change the log level in config.json to enable lower level (i.e., FINE, FINER and lower) // log messages such as the one below. @@ -65,6 +69,12 @@ public Command parseCommand(String userInput) throws ParseException { case ClearCommand.COMMAND_WORD: return new ClearCommand(); + case MarkCommand.COMMAND_WORD: + return new MarkCommandParser().parse(arguments); + + case RemovetagCommand.COMMAND_WORD: + return new RemovetagCommandParser().parse(arguments); + case FindCommand.COMMAND_WORD: return new FindCommandParser().parse(arguments); @@ -77,6 +87,12 @@ public Command parseCommand(String userInput) throws ParseException { case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case EditTutTagListCommand.COMMAND_WORD: + return new EditTutTagListCommandParser().parse(arguments); + + case AvailableCommand.COMMAND_WORD: + return new AvailableCommandParser().parse(arguments); + default: logger.finer("This user input caused a ParseException: " + userInput); throw new ParseException(MESSAGE_UNKNOWN_COMMAND); diff --git a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java index 5c9aebfa488..b20e55857bc 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java +++ b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java @@ -70,7 +70,7 @@ private static List findPrefixPositions(String argsString, Prefi * {@code fromIndex} = 0, this method returns 5. */ private static int findPrefixPosition(String argsString, String prefix, int fromIndex) { - int prefixIndex = argsString.indexOf(" " + prefix, fromIndex); + int prefixIndex = argsString.indexOf(" " + prefix + " ", fromIndex); return prefixIndex == -1 ? -1 : prefixIndex + 1; // +1 as offset for whitespace } diff --git a/src/main/java/seedu/address/logic/parser/AvailableCommandParser.java b/src/main/java/seedu/address/logic/parser/AvailableCommandParser.java new file mode 100644 index 00000000000..12a6d7eabf5 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AvailableCommandParser.java @@ -0,0 +1,61 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.util.AppUtil.checkArgument; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.Messages.MESSAGE_INVALID_TAG_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GROUP; + +import java.util.Optional; + +import seedu.address.logic.commands.AvailableCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.TutorialTagContainsGroupPredicate; +import seedu.address.model.tag.Tag; +import seedu.address.model.tag.TagStatus; +import seedu.address.model.tag.TutorialTag; + +/** + * Parses input arguments and creates a new AvailableCommand object + */ +public class AvailableCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AvailableCommand + * and returns an AvailableCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AvailableCommand parse(String args) throws ParseException { + if (args.trim().isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AvailableCommand.MESSAGE_USAGE)); + } + + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_GROUP); + + String preamble = argMultimap.getPreamble(); + if (!preamble.equals("")) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AvailableCommand.MESSAGE_USAGE)); + } + + Optional group = argMultimap.getValue(PREFIX_GROUP); + + try { + checkArgument(!group.isEmpty(), String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AvailableCommand.MESSAGE_USAGE)); + checkArgument(!group.get().equals(""), AvailableCommand.MESSAGE_NON_EMPTY_GROUP_NAME); + } catch (IllegalArgumentException e) { + throw new ParseException(e.getMessage()); + } + + String tutorialGroup = argMultimap.getValue(PREFIX_GROUP).get(); + + if (!Tag.isValidTagName(tutorialGroup)) { + throw new ParseException(MESSAGE_INVALID_TAG_NAME); + } + + TutorialTag tutorialTag = new TutorialTag(tutorialGroup, TagStatus.AVAILABLE); + + return new AvailableCommand(new TutorialTagContainsGroupPredicate(tutorialGroup), tutorialTag); + } +} diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..89bf56529d1 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -1,15 +1,21 @@ package seedu.address.logic.parser; +import seedu.address.model.person.PersonType; + /** * Contains Command Line Interface (CLI) syntax definitions common to multiple commands */ public class CliSyntax { /* Prefix definitions */ - public static final Prefix PREFIX_NAME = new Prefix("n/"); - public static final Prefix PREFIX_PHONE = new Prefix("p/"); - public static final Prefix PREFIX_EMAIL = new Prefix("e/"); - public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); - public static final Prefix PREFIX_TAG = new Prefix("t/"); - + public static final PersonType TYPE_STU = PersonType.STU; + public static final PersonType TYPE_TA = PersonType.TA; + public static final PersonType TYPE_DEFAULT = PersonType.STU; + public static final Prefix PREFIX_NAME = new Prefix("/n"); + public static final Prefix PREFIX_ID = new Prefix("/i"); + public static final Prefix PREFIX_PHONE = new Prefix("/p"); + public static final Prefix PREFIX_EMAIL = new Prefix("/e"); + public static final Prefix PREFIX_TAG = new Prefix("/t"); + public static final Prefix PREFIX_GROUP = new Prefix("/g"); + public static final Prefix PREFIX_TAGSTATUS = new Prefix("/ts"); } diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java index 3527fe76a3e..344f11045b5 100644 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java @@ -2,6 +2,8 @@ import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import java.util.Set; + import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.parser.exceptions.ParseException; @@ -17,9 +19,11 @@ public class DeleteCommandParser implements Parser { * @throws ParseException if the user input does not conform the expected format */ public DeleteCommand parse(String args) throws ParseException { + Set indices; + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args); try { - Index index = ParserUtil.parseIndex(args); - return new DeleteCommand(index); + indices = StatefulParserUtil.parseIndices(argMultimap.getPreamble()); + return new DeleteCommand(indices); } catch (ParseException pe) { throw new ParseException( String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 46b3309a78b..a6e84e99b9d 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -2,11 +2,9 @@ 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_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import java.util.Collection; import java.util.Collections; @@ -31,34 +29,29 @@ 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); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL); Index index; try { - index = ParserUtil.parseIndex(argMultimap.getPreamble()); + index = StatefulParserUtil.parseIndex(argMultimap.getPreamble()); } catch (ParseException pe) { 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); EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); if (argMultimap.getValue(PREFIX_NAME).isPresent()) { - editPersonDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); + editPersonDescriptor.setName(StatefulParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); } if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { - editPersonDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get())); + editPersonDescriptor.setPhone(StatefulParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get())); } if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { - editPersonDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get())); + editPersonDescriptor.setEmail(StatefulParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get())); } - if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { - editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); - } - parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); if (!editPersonDescriptor.isAnyFieldEdited()) { throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); @@ -79,7 +72,7 @@ private Optional> parseTagsForEdit(Collection tags) throws Pars return Optional.empty(); } Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; - return Optional.of(ParserUtil.parseTags(tagSet)); + return Optional.of(StatefulParserUtil.parseTags(tagSet)); } } diff --git a/src/main/java/seedu/address/logic/parser/EditTutTagListCommandParser.java b/src/main/java/seedu/address/logic/parser/EditTutTagListCommandParser.java new file mode 100644 index 00000000000..cb4f7a243bc --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/EditTutTagListCommandParser.java @@ -0,0 +1,55 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import seedu.address.logic.commands.EditTutTagListCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new EditTutTagListCommand object + */ +public class EditTutTagListCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the EditTutTagListCommand + * and returns a EditTutTagListCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public EditTutTagListCommand parse(String args) throws ParseException { + if (args.trim().isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditTutTagListCommand.MESSAGE_USAGE)); + } + + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_TAG); + + String commandFlag = argMultimap.getPreamble(); + try { + EditTutTagListCommand.CommandSubtype commandSubtype = StatefulParserUtil.isCreatingNewTag(commandFlag); + + // if the EditTutTagListCommand is to list all available tutorial tags + if (EditTutTagListCommand.isListCommand(commandSubtype)) { + return new EditTutTagListCommand(commandSubtype); + } + + // if the EditTutTagListCommand is not to list all available tutorial tags, PREFIX_TAG must be present + if (!StatefulParserUtil.arePrefixesPresent(argMultimap, PREFIX_TAG)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + EditTutTagListCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_TAG); + String tagName = argMultimap.getValue(PREFIX_TAG).get(); + Tag.isTagNameValid(tagName); + return new EditTutTagListCommand(tagName, commandSubtype); + + } catch (ParseException e) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditTutTagListCommand.MESSAGE_USAGE)); + } catch (IllegalArgumentException e) { + throw new ParseException(e.getMessage()); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java index 2867bde857b..ec9577f81b3 100644 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/FindCommandParser.java @@ -1,12 +1,22 @@ package seedu.address.logic.parser; +import static seedu.address.commons.util.AppUtil.checkArgument; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ID; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import seedu.address.commons.util.StringListUtil; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.FieldContainsKeywordsPredicate; +import seedu.address.model.person.PersonType; /** * Parses input arguments and creates a new FindCommand object @@ -19,15 +29,40 @@ public class FindCommandParser implements Parser { * @throws ParseException if the user input does not conform the expected format */ public FindCommand parse(String args) throws ParseException { - String trimmedArgs = args.trim(); - if (trimmedArgs.isEmpty()) { + if (args.trim().isEmpty()) { throw new ParseException( String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); } - String[] nameKeywords = trimmedArgs.split("\\s+"); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_ID, PREFIX_PHONE, + PREFIX_EMAIL, PREFIX_TAG); - return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); - } + List predicates = new ArrayList<>(); + + PersonType type = StatefulParserUtil.parseFindPersonType(argMultimap.getPreamble()); + if (type != null) { + List keyword = new ArrayList<>(); + keyword.add(type.toString()); + predicates.add(new FieldContainsKeywordsPredicate(keyword)); + } + List allPrefixes = Arrays.asList(PREFIX_NAME, PREFIX_ID, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_TAG); + for (Prefix prefix: allPrefixes) { + if (StatefulParserUtil.arePrefixesPresent(argMultimap, prefix)) { + List keywords = argMultimap.getAllValues(prefix); + List separated = StringListUtil.separateWithSpaces(keywords); + try { + for (String keyword : separated) { + checkArgument(!keyword.isEmpty(), "Word parameter cannot be empty"); + } + } catch (IllegalArgumentException e) { + throw new ParseException(e.getMessage()); + } + predicates.add(new FieldContainsKeywordsPredicate(prefix, separated)); + } + } + + return new FindCommand(predicates); + } } diff --git a/src/main/java/seedu/address/logic/parser/MarkCommandParser.java b/src/main/java/seedu/address/logic/parser/MarkCommandParser.java new file mode 100644 index 00000000000..503e87a17e6 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/MarkCommandParser.java @@ -0,0 +1,63 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAGSTATUS; + +import java.util.Set; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.MarkCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tag.Tag; +import seedu.address.model.tag.TagStatus; + + +/** + * Parses input arguments and creates a new {@code RemarkCommand} object + */ +public class MarkCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the {@code MarkCommand} + * and returns a {@code MarkCommand} object for execution. + * @throws ParseException if the user input does not conform to the expected format + */ + public MarkCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_TAG, PREFIX_TAGSTATUS); + + Set indices; + try { + indices = StatefulParserUtil.parseIndices(argMultimap.getPreamble()); + } catch (IllegalValueException ive) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, MarkCommand.MESSAGE_USAGE), ive); + } + + if (!StatefulParserUtil.arePrefixesPresent(argMultimap, PREFIX_TAG, PREFIX_TAGSTATUS)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, MarkCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_TAG, PREFIX_TAGSTATUS); + String tagNamesString = argMultimap.getValue(PREFIX_TAG).get(); + Set tagNames = StatefulParserUtil.parseTagNamesString(tagNamesString); + + String statusIdentifier = argMultimap.getValue(PREFIX_TAGSTATUS).get(); + + + + // an alternative approach is to instantiate the Tag object and try to + // catch the Illegal Exception Error. The tag can then be fed into the + // MarkCommand. The author decided to pass in the tagName instead as the + // TagName might be used to search for tags in future implementations + try { + TagStatus tagStatus = TagStatus.getTagStatus(statusIdentifier); + tagNames.forEach(Tag::isTagNameValid); + return new MarkCommand(indices, tagNames, tagStatus); + } catch (IllegalArgumentException e) { + throw new ParseException(e.getMessage()); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java deleted file mode 100644 index b117acb9c55..00000000000 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ /dev/null @@ -1,124 +0,0 @@ -package seedu.address.logic.parser; - -import static java.util.Objects.requireNonNull; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; - -import seedu.address.commons.core.index.Index; -import seedu.address.commons.util.StringUtil; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Contains utility methods used for parsing strings in the various *Parser classes. - */ -public class ParserUtil { - - public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; - - /** - * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be - * trimmed. - * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). - */ - public static Index parseIndex(String oneBasedIndex) throws ParseException { - String trimmedIndex = oneBasedIndex.trim(); - if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) { - throw new ParseException(MESSAGE_INVALID_INDEX); - } - return Index.fromOneBased(Integer.parseInt(trimmedIndex)); - } - - /** - * Parses a {@code String name} into a {@code Name}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code name} is invalid. - */ - public static Name parseName(String name) throws ParseException { - requireNonNull(name); - String trimmedName = name.trim(); - if (!Name.isValidName(trimmedName)) { - throw new ParseException(Name.MESSAGE_CONSTRAINTS); - } - return new Name(trimmedName); - } - - /** - * Parses a {@code String phone} into a {@code Phone}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code phone} is invalid. - */ - public static Phone parsePhone(String phone) throws ParseException { - requireNonNull(phone); - String trimmedPhone = phone.trim(); - if (!Phone.isValidPhone(trimmedPhone)) { - throw new ParseException(Phone.MESSAGE_CONSTRAINTS); - } - return new Phone(trimmedPhone); - } - - /** - * Parses a {@code String address} into an {@code Address}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code address} is invalid. - */ - public static Address parseAddress(String address) throws ParseException { - requireNonNull(address); - String trimmedAddress = address.trim(); - if (!Address.isValidAddress(trimmedAddress)) { - throw new ParseException(Address.MESSAGE_CONSTRAINTS); - } - return new Address(trimmedAddress); - } - - /** - * Parses a {@code String email} into an {@code Email}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code email} is invalid. - */ - public static Email parseEmail(String email) throws ParseException { - requireNonNull(email); - String trimmedEmail = email.trim(); - if (!Email.isValidEmail(trimmedEmail)) { - throw new ParseException(Email.MESSAGE_CONSTRAINTS); - } - return new Email(trimmedEmail); - } - - /** - * Parses a {@code String tag} into a {@code Tag}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code tag} is invalid. - */ - public static Tag parseTag(String tag) throws ParseException { - requireNonNull(tag); - String trimmedTag = tag.trim(); - if (!Tag.isValidTagName(trimmedTag)) { - throw new ParseException(Tag.MESSAGE_CONSTRAINTS); - } - return new Tag(trimmedTag); - } - - /** - * Parses {@code Collection tags} into a {@code Set}. - */ - public static Set parseTags(Collection tags) throws ParseException { - requireNonNull(tags); - final Set tagSet = new HashSet<>(); - for (String tagName : tags) { - tagSet.add(parseTag(tagName)); - } - return tagSet; - } -} diff --git a/src/main/java/seedu/address/logic/parser/RemovetagCommandParser.java b/src/main/java/seedu/address/logic/parser/RemovetagCommandParser.java new file mode 100644 index 00000000000..f9dc8ddabec --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/RemovetagCommandParser.java @@ -0,0 +1,53 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Set; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.RemovetagCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new {@code RemarkCommand} object + */ +public class RemovetagCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the {@code RemovetagCommand} + * and returns a {@code RemovetagCommand} object for execution. + * @throws ParseException if the user input does not conform to the expected format + */ + public RemovetagCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_TAG); + + Set indices; + try { + indices = StatefulParserUtil.parseIndices(argMultimap.getPreamble()); + } catch (IllegalValueException ive) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + RemovetagCommand.MESSAGE_USAGE), ive); + } + + if (!StatefulParserUtil.arePrefixesPresent(argMultimap, PREFIX_TAG)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + RemovetagCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_TAG); + String tagNamesString = argMultimap.getValue(PREFIX_TAG).get(); + Set tagNames = StatefulParserUtil.parseTagNamesString(tagNamesString); + + try { + tagNames.forEach(Tag::isTagNameValid); + return new RemovetagCommand(indices, tagNames); + } catch (IllegalArgumentException e) { + throw new ParseException(e.getMessage()); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/StatefulParserUtil.java b/src/main/java/seedu/address/logic/parser/StatefulParserUtil.java new file mode 100644 index 00000000000..474360f92bb --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/StatefulParserUtil.java @@ -0,0 +1,285 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.StatefulStringUtil; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.EditTutTagListCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.Model; +import seedu.address.model.person.Email; +import seedu.address.model.person.Id; +import seedu.address.model.person.Name; +import seedu.address.model.person.PersonType; +import seedu.address.model.person.Phone; +import seedu.address.model.tag.Tag; +import seedu.address.model.tag.TagStatus; + +/** + * Contains utility methods used for parsing strings in the various *Parser classes. + */ +public class StatefulParserUtil { + public static final String MESSAGE_INVALID_INDEX = "Index is out of bounds or not an unsigned positive integer."; + private static StatefulParserUtil instance = null; + private Model model; + private StatefulParserUtil(Model model) { + this.model = model; + } + + /** + * Initializes the {@code StatefulParserUtil} singleton with a model. + */ + public static void initialize(Model model) { + if (instance == null) { + instance = new StatefulParserUtil(model); + } + } + + /** + * Get the instance of the {@code StatefulParserUtil} singleton. + */ + public static StatefulParserUtil getInstance() { + if (instance == null) { + throw new IllegalStateException("Instance not initialized yet."); + } + return instance; + } + + /** + * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be + * trimmed. + * + * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). + */ + public static Index parseIndex(String oneBasedIndex) throws ParseException { + String trimmedIndex = oneBasedIndex.trim(); + if (!StatefulStringUtil.isNonZeroUnsignedInteger(trimmedIndex)) { + throw new ParseException(MESSAGE_INVALID_INDEX); + } + + int intIndex = Integer.parseInt(trimmedIndex); + Model model = StatefulParserUtil.getInstance().model; + if (intIndex > model.getFilteredPersonList().size()) { + throw new ParseException(MESSAGE_INVALID_INDEX); + } + return Index.fromOneBased(intIndex); + } + + /** + * Parses a {@code String} consisting of indices into a {@code Set}. + * + * @throws ParseException if any substring is not a valid index. + */ + public static Set parseIndices(String oneBasedIndices) throws ParseException { + requireNonNull(oneBasedIndices); + String trimmedOneBasedIndices = oneBasedIndices.trim(); + + Set parsedIndices = new HashSet<>(); + List indicesList; + if (trimmedOneBasedIndices.equals("all")) { + Model model = StatefulParserUtil.getInstance().model; + int size = model.getFilteredPersonList().size(); + indicesList = Stream.iterate(1, x -> x + 1) + .limit(size) + .map(Objects::toString) + .collect(Collectors.toList()); + } else { + indicesList = Arrays.asList(trimmedOneBasedIndices.split("\\s+")); + } + + // Java Lambda expressions do not allow propagating of checked exceptions. + // To circumvent this, wrap the checked exception in an unchecked exception. + try { + parsedIndices = indicesList.stream().map(x -> { + try { + return parseIndex(x); + } catch (ParseException e) { + throw new RuntimeException(e); + } + }).collect(Collectors.toSet()); + } catch (RuntimeException e) { + Throwable cause = e.getCause(); + if (cause instanceof ParseException) { + throw (ParseException) cause; + } else { + throw e; + } + } + + return parsedIndices; + } + + /** + * Parses a {@code String type} into a {@code PersonType}. + * Leading and trailing whitespaces will be trimmed. + * An unspecified person type will default to student. + * + * @throws ParseException if the given {@code type} is invalid. + */ + public static PersonType parsePersonType(String type) throws ParseException { + requireNonNull(type); + String trimmedType = type.trim(); + if (!PersonType.isValidPersonType(trimmedType) && !trimmedType.isEmpty()) { + throw new ParseException(PersonType.MESSAGE_CONSTRAINTS); + } + return PersonType.getPersonType(trimmedType); + } + + /** + * Parses a {@code String type} into a {@code PersonType} for the find command. + * Leading and trailing whitespaces will be trimmed. + * An unspecified person type will return null. + * + * @throws ParseException if the given {@code type} is invalid. + */ + public static PersonType parseFindPersonType(String type) throws ParseException { + requireNonNull(type); + String trimmedType = type.trim(); + if (!PersonType.isValidPersonType(trimmedType) && !trimmedType.isEmpty()) { + throw new ParseException(PersonType.MESSAGE_CONSTRAINTS); + } else if (trimmedType.isEmpty()) { + return null; + } else { + return PersonType.getPersonType(trimmedType); + } + } + + + /** + * Parses a {@code String name} into a {@code Name}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code name} is invalid. + */ + public static Name parseName(String name) throws ParseException { + requireNonNull(name); + String trimmedName = name.trim(); + if (!Name.isValidName(trimmedName)) { + throw new ParseException(Name.MESSAGE_CONSTRAINTS); + } + return new Name(trimmedName); + } + + /** + * Parses a {@code String id} into a {@code Id}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code id} is invalid. + */ + public static Id parseId(String id) throws ParseException { + requireNonNull(id); + String trimmedId = id.trim(); + if (!Id.isValidId(trimmedId)) { + throw new ParseException(Id.MESSAGE_CONSTRAINTS); + } + return new Id(trimmedId); + } + + /** + * Parses a {@code String phone} into a {@code Phone}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code phone} is invalid. + */ + public static Phone parsePhone(String phone) throws ParseException { + requireNonNull(phone); + String trimmedPhone = phone.trim(); + if (!Phone.isValidPhone(trimmedPhone)) { + throw new ParseException(Phone.MESSAGE_CONSTRAINTS); + } + return new Phone(trimmedPhone); + } + + /** + * Parses a {@code String email} into an {@code Email}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code email} is invalid. + */ + public static Email parseEmail(String email) throws ParseException { + requireNonNull(email); + String trimmedEmail = email.trim(); + if (!Email.isValidEmail(trimmedEmail)) { + throw new ParseException(Email.MESSAGE_CONSTRAINTS); + } + return new Email(trimmedEmail); + } + + /** + * Parses a {@code String tag} into a {@code Tag}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code tag} is invalid. + */ + public static Tag parseTag(String tag) throws ParseException { + requireNonNull(tag); + String trimmedTag = tag.trim(); + if (!Tag.isValidTagName(trimmedTag)) { + throw new ParseException(Tag.MESSAGE_CONSTRAINTS); + } + return Tag.createTag(trimmedTag, TagStatus.DEFAULT_STATUS); + } + + /** + * Parses {@code Collection tags} into a {@code Set}. + */ + public static Set parseTags(Collection tags) throws ParseException { + requireNonNull(tags); + final Set tagSet = new HashSet<>(); + for (String tagName : tags) { + tagSet.add(parseTag(tagName)); + } + return tagSet; + } + + /** + * Parses a {@code String} of tag names separated by whitespaces + * into a {@code Set}, where each element corresponds to a + * trimmed tag name. + */ + public static Set parseTagNamesString(String tagNames) { + requireNonNull(tagNames); + final Set tagNamesSet = new HashSet<>(); + tagNamesSet.addAll(Arrays.asList(tagNames.split("\\s+"))); + return tagNamesSet; + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + public static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + + /** + * @param flag Command flag on whether the EditTutTagListCommand + * @throws ParseException + */ + public static EditTutTagListCommand.CommandSubtype isCreatingNewTag(String flag) throws ParseException { + if (flag.equals(EditTutTagListCommand.ADD_FLAG)) { + return EditTutTagListCommand.CommandSubtype.ADD; + } + + if (flag.equals(EditTutTagListCommand.DELETE_FLAG)) { + return EditTutTagListCommand.CommandSubtype.DELETE; + } + + if (flag.equals(EditTutTagListCommand.LIST_FLAG)) { + return EditTutTagListCommand.CommandSubtype.LIST; + } + + throw new ParseException(Messages.MESSAGE_INVALID_COMMAND_FORMAT); + } + +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 73397161e84..7bcc1b83894 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -8,6 +8,8 @@ import seedu.address.commons.util.ToStringBuilder; import seedu.address.model.person.Person; import seedu.address.model.person.UniquePersonList; +import seedu.address.model.person.UniqueTutorialTagList; +import seedu.address.model.tag.TutorialTag; /** * Wraps all data at the address-book level @@ -16,6 +18,7 @@ public class AddressBook implements ReadOnlyAddressBook { private final UniquePersonList persons; + private final UniqueTutorialTagList tutorialTags; /* * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication @@ -26,6 +29,7 @@ public class AddressBook implements ReadOnlyAddressBook { */ { persons = new UniquePersonList(); + tutorialTags = new UniqueTutorialTagList(); } public AddressBook() {} @@ -48,6 +52,14 @@ public void setPersons(List persons) { this.persons.setPersons(persons); } + /** + * Replaces the contents of the tutorial tags list with {@code tutorialTags}. + * {@code tutorialTags} must not contain duplicate tutorial tags. + */ + public void setTutorialTags(List tutorialTags) { + this.tutorialTags.setTutorialTags(tutorialTags); + } + /** * Resets the existing data of this {@code AddressBook} with {@code newData}. */ @@ -55,6 +67,7 @@ public void resetData(ReadOnlyAddressBook newData) { requireNonNull(newData); setPersons(newData.getPersonList()); + setTutorialTags(newData.getTutorialTagList()); } //// person-level operations @@ -94,6 +107,31 @@ public void removePerson(Person key) { persons.remove(key); } + //// tutorialTag-level operations + + /** + */ + public boolean hasTutorialTag(TutorialTag tutorialTag) { + requireNonNull(tutorialTag); + return tutorialTags.contains(tutorialTag); + } + + /** + * Adds a tutorialTag to the address book. + * The tutorialTag must not already exist in the address book. + */ + public void addTutorialTag(TutorialTag t) { + tutorialTags.add(t); + } + + /** + * Removes {@code key} from this {@code AddressBook}. + * {@code key} must exist in the address book. + */ + public void removeTutorialTag(TutorialTag key) { + tutorialTags.remove(key); + } + //// util methods @Override @@ -108,6 +146,11 @@ public ObservableList getPersonList() { return persons.asUnmodifiableObservableList(); } + @Override + public ObservableList getTutorialTagList() { + return tutorialTags.asUnmodifiableObservableList(); + } + @Override public boolean equals(Object other) { if (other == this) { @@ -120,11 +163,27 @@ public boolean equals(Object other) { } AddressBook otherAddressBook = (AddressBook) other; - return persons.equals(otherAddressBook.persons); + return persons.equals(otherAddressBook.persons) && tutorialTags.equals(otherAddressBook.tutorialTags); } @Override public int hashCode() { return persons.hashCode(); } + + public String getTutorialTagListString() { + StringBuilder sb = new StringBuilder("Available Tutorial Tag(s): ["); + + if (!tutorialTags.isEmpty()) { + for (TutorialTag tag : tutorialTags) { + sb.append(tag.getTagName()).append(", "); + } + sb.delete(sb.length() - 2, sb.length()); // Remove the last comma and space + } else { + return sb.append(" ]").toString(); + } + + sb.append("]"); + return sb.toString(); + } } diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..bafa3a05277 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -1,11 +1,13 @@ package seedu.address.model; import java.nio.file.Path; +import java.util.List; import java.util.function.Predicate; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; import seedu.address.model.person.Person; +import seedu.address.model.tag.TutorialTag; /** * The API of the Model component. @@ -76,12 +78,43 @@ public interface Model { */ void setPerson(Person target, Person editedPerson); + /** Returns an unmodifiable view of the entire person list */ + ObservableList getPersonList(); + /** Returns an unmodifiable view of the filtered person list */ ObservableList getFilteredPersonList(); + /** + * Applies multiple predicates to filter the filtered person list. + * @param predicates List of predicates. + */ + void persistentUpdateFilteredList(List> predicates); /** * Updates the filter of the filtered person list to filter by the given {@code predicate}. * @throws NullPointerException if {@code predicate} is null. */ void updateFilteredPersonList(Predicate predicate); + + /** + * Deletes the given tutorial tag. + * The tutorial tag must exist in the address book. + */ + void deleteTutorialTag(TutorialTag target); + + /** + * Adds the given tutorial tag. + * {@code tutorialTag} must not already exist in the address book. + */ + void addTutorialTag(TutorialTag tutorialTag); + + /** + * Returns true if a tutorial tag with the same identity as {@code tutorialTag} exists in the address book. + */ + boolean hasTutorialTag(TutorialTag tutorialTag); + + /** Returns an unmodifiable view of the filtered tutorial tag list */ + ObservableList getTutorialTagList(); + + /** Returns a String representing the filtered tutorial tag list */ + String getTutorialTagListString(); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 57bc563fde6..b1c23c62a2e 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -4,6 +4,7 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; import java.nio.file.Path; +import java.util.List; import java.util.function.Predicate; import java.util.logging.Logger; @@ -12,6 +13,7 @@ import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; import seedu.address.model.person.Person; +import seedu.address.model.tag.TutorialTag; /** * Represents the in-memory model of the address book data. @@ -21,7 +23,7 @@ public class ModelManager implements Model { private final AddressBook addressBook; private final UserPrefs userPrefs; - private final FilteredList filteredPersons; + private FilteredList filteredPersons; /** * Initializes a ModelManager with the given addressBook and userPrefs. @@ -30,7 +32,6 @@ public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs requireAllNonNull(addressBook, userPrefs); logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); - this.addressBook = new AddressBook(addressBook); this.userPrefs = new UserPrefs(userPrefs); filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); @@ -113,6 +114,11 @@ public void setPerson(Person target, Person editedPerson) { //=========== Filtered Person List Accessors ============================================================= + @Override + public ObservableList getPersonList() { + return addressBook.getPersonList(); + } + /** * Returns an unmodifiable view of the list of {@code Person} backed by the internal list of * {@code versionedAddressBook} @@ -128,6 +134,14 @@ public void updateFilteredPersonList(Predicate predicate) { filteredPersons.setPredicate(predicate); } + @Override + public void persistentUpdateFilteredList(List> predicates) { + Predicate combinedPredicate = predicates.stream() + .>map(p -> (Predicate) p) + .reduce(Predicate::and) + .orElse(person -> true); + filteredPersons.setPredicate(combinedPredicate); + } @Override public boolean equals(Object other) { if (other == this) { @@ -145,4 +159,28 @@ public boolean equals(Object other) { && filteredPersons.equals(otherModelManager.filteredPersons); } + @Override + public void deleteTutorialTag(TutorialTag tutorialTag) { + addressBook.removeTutorialTag(tutorialTag); + } + + @Override + public void addTutorialTag(TutorialTag tutorialTag) { + addressBook.addTutorialTag(tutorialTag); + } + + @Override + public boolean hasTutorialTag(TutorialTag tutorialTag) { + requireNonNull(tutorialTag); + return addressBook.hasTutorialTag(tutorialTag); + } + + @Override + public ObservableList getTutorialTagList() { + return addressBook.getTutorialTagList(); + } + + public String getTutorialTagListString() { + return addressBook.getTutorialTagListString(); + } } diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java index 6ddc2cd9a29..f728c7c8eec 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java @@ -2,6 +2,7 @@ import javafx.collections.ObservableList; import seedu.address.model.person.Person; +import seedu.address.model.tag.TutorialTag; /** * Unmodifiable view of an address book @@ -14,4 +15,10 @@ public interface ReadOnlyAddressBook { */ ObservableList getPersonList(); + /** + * Returns an unmodifiable view of the tutorial tag list. + * This list will not contain any duplicate tutorial tags. + */ + ObservableList getTutorialTagList(); + } diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java deleted file mode 100644 index 469a2cc9a1e..00000000000 --- a/src/main/java/seedu/address/model/person/Address.java +++ /dev/null @@ -1,65 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's address in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} - */ -public class Address { - - public static final String MESSAGE_CONSTRAINTS = "Addresses can take any values, and it should not be blank"; - - /* - * The first character of the address must not be a whitespace, - * otherwise " " (a blank string) becomes a valid input. - */ - public static final String VALIDATION_REGEX = "[^\\s].*"; - - public final String value; - - /** - * Constructs an {@code Address}. - * - * @param address A valid address. - */ - public Address(String address) { - requireNonNull(address); - checkArgument(isValidAddress(address), MESSAGE_CONSTRAINTS); - value = address; - } - - /** - * Returns true if a given string is a valid email. - */ - public static boolean isValidAddress(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof Address)) { - return false; - } - - Address otherAddress = (Address) other; - return value.equals(otherAddress.value); - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java index c62e512bc29..2806032f1f1 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/seedu/address/model/person/Email.java @@ -14,7 +14,7 @@ public class Email { + "and adhere to the following constraints:\n" + "1. The local-part should only contain alphanumeric characters and these special characters, excluding " + "the parentheses, (" + SPECIAL_CHARACTERS + "). The local-part may not start or end with any special " - + "characters.\n" + + "characters and no consecutive special characters are allowed.\n" + "2. This is followed by a '@' and then a domain name. The domain name is made up of domain labels " + "separated by periods.\n" + "The domain name must:\n" diff --git a/src/main/java/seedu/address/model/person/FieldContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/FieldContainsKeywordsPredicate.java new file mode 100644 index 00000000000..437f4154aaa --- /dev/null +++ b/src/main/java/seedu/address/model/person/FieldContainsKeywordsPredicate.java @@ -0,0 +1,91 @@ +package seedu.address.model.person; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ID; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StatefulStringUtil; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.parser.Prefix; + +/** + * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. + */ +public class FieldContainsKeywordsPredicate implements Predicate { + private Prefix prefix; + private final List keywords; + + /** + * Constructor for predicate with a prefix + * @param prefix prefix category to search within + * @param keywords keywords to search for + */ + public FieldContainsKeywordsPredicate(Prefix prefix, List keywords) { + this.prefix = prefix; + this.keywords = keywords; + } + + /** + * Constructor for predicate without a prefix. Used for type finding. + * @param keywords keywords to search for + */ + public FieldContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Person person) { + if (this.prefix == null) { // type + return keywords.stream() + .anyMatch(keyword -> StatefulStringUtil + .containsSubwordIgnoreCase(person.getType().name(), keyword)); + } else if (this.prefix.equals(PREFIX_NAME)) { + return keywords.stream() + .anyMatch(keyword -> StatefulStringUtil + .containsSubwordIgnoreCase(person.getName().fullName, keyword)); + } else if (this.prefix.equals(PREFIX_ID)) { + return keywords.stream() + .anyMatch(keyword -> StatefulStringUtil + .containsSubwordIgnoreCase(person.getId().value, keyword)); + } else if (this.prefix.equals(PREFIX_PHONE)) { + return keywords.stream() + .anyMatch(keyword -> StatefulStringUtil + .containsSubwordIgnoreCase(person.getPhone().value, keyword)); + } else if (this.prefix.equals(PREFIX_EMAIL)) { + return keywords.stream() + .anyMatch(keyword -> StatefulStringUtil + .containsSubwordIgnoreCase(person.getEmail().value, keyword)); + } else if (this.prefix.equals(PREFIX_TAG)) { + return keywords.stream() + .anyMatch(keyword -> person.getTags().stream() + .anyMatch(tag -> StatefulStringUtil.tagContainsWordIgnoreCase(tag, keyword))); + } else { + return false; + } + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof FieldContainsKeywordsPredicate)) { + return false; + } + + FieldContainsKeywordsPredicate otherFieldContainsKeywordsPredicate = (FieldContainsKeywordsPredicate) other; + return keywords.equals(otherFieldContainsKeywordsPredicate.keywords); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add(this.prefix + " keywords", keywords).toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/Id.java b/src/main/java/seedu/address/model/person/Id.java new file mode 100644 index 00000000000..8c1f580a58e --- /dev/null +++ b/src/main/java/seedu/address/model/person/Id.java @@ -0,0 +1,60 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Person's student/staff ID in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidId(String)} + */ +public class Id { + + public static final String MESSAGE_CONSTRAINTS = + "ID should start and end with a letter, with 7 digits in between"; + public static final String VALIDATION_REGEX = "[a-zA-Z][0-9]{7}[a-zA-Z]"; + public final String value; + + /** + * Constructs a {@code Id}. + * + * @param id A valid id. + */ + public Id(String id) { + requireNonNull(id); + checkArgument(isValidId(id), MESSAGE_CONSTRAINTS); + value = id; + } + + /** + * Returns true if a given string is a valid id. + */ + public static boolean isValidId(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 Id)) { + return false; + } + + Id otherId = (Id) other; + return value.equals(otherId.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java deleted file mode 100644 index 62d19be2977..00000000000 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ /dev/null @@ -1,44 +0,0 @@ -package seedu.address.model.person; - -import java.util.List; -import java.util.function.Predicate; - -import seedu.address.commons.util.StringUtil; -import seedu.address.commons.util.ToStringBuilder; - -/** - * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. - */ -public class NameContainsKeywordsPredicate implements Predicate { - private final List keywords; - - public NameContainsKeywordsPredicate(List keywords) { - this.keywords = keywords; - } - - @Override - public boolean test(Person person) { - return keywords.stream() - .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof NameContainsKeywordsPredicate)) { - return false; - } - - NameContainsKeywordsPredicate otherNameContainsKeywordsPredicate = (NameContainsKeywordsPredicate) other; - return keywords.equals(otherNameContainsKeywordsPredicate.keywords); - } - - @Override - public String toString() { - return new ToStringBuilder(this).add("keywords", keywords).toString(); - } -} diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index abe8c46b535..b893c481d84 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -2,67 +2,79 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; -import java.util.Collections; -import java.util.HashSet; -import java.util.Objects; import java.util.Set; -import seedu.address.commons.util.ToStringBuilder; import seedu.address.model.tag.Tag; /** * Represents a Person in the address book. * Guarantees: details are present and not null, field values are validated, immutable. */ -public class Person { +public abstract class Person { // Identity fields - private final Name name; - private final Phone phone; - private final Email email; - - // Data fields - private final Address address; - private final Set tags = new HashSet<>(); - + final PersonType type; + final Name name; + final Id id; + final Phone phone; + final Email email; /** * Every field must be present and not null. */ - public Person(Name name, Phone phone, Email email, Address address, Set tags) { - requireAllNonNull(name, phone, email, address, tags); + Person(PersonType type, Name name, Id id, Phone phone, Email email) { + requireAllNonNull(type, name, id, phone, email); + this.type = type; this.name = name; + this.id = id; this.phone = phone; this.email = email; - this.address = address; - this.tags.addAll(tags); } + /** + * Creates either a Student or a TA. + * All fields are compulsory. + * + * @param type + * @param name + * @param id + * @param phone + * @param email + * @param tags + */ + public static Person of(PersonType type, Name name, Id id, Phone phone, Email email, + Set tags) { + requireAllNonNull(type, name, id, phone, email, tags); + if (type == PersonType.STU) { + return new Student(name, id, phone, email, tags); + } else { + return new Ta(name, id, phone, email, tags); + } + } + + public PersonType getType() { + return type; + } public Name getName() { return name; } - + public Id getId() { + return id; + } public Phone getPhone() { return phone; } - public Email getEmail() { return email; } - public Address getAddress() { - return address; - } - /** * Returns an immutable tag set, which throws {@code UnsupportedOperationException} * if modification is attempted. */ - public Set getTags() { - return Collections.unmodifiableSet(tags); - } + public abstract Set getTags(); /** - * Returns true if both persons have the same name. + * Returns true if both persons have the same id. * This defines a weaker notion of equality between two persons. */ public boolean isSamePerson(Person otherPerson) { @@ -71,47 +83,6 @@ public boolean isSamePerson(Person otherPerson) { } return otherPerson != null - && otherPerson.getName().equals(getName()); + && otherPerson.getId().equals(getId()); } - - /** - * Returns true if both persons have the same identity and data fields. - * This defines a stronger notion of equality between two persons. - */ - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof Person)) { - return false; - } - - Person otherPerson = (Person) other; - return name.equals(otherPerson.name) - && phone.equals(otherPerson.phone) - && email.equals(otherPerson.email) - && address.equals(otherPerson.address) - && 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); - } - - @Override - public String toString() { - return new ToStringBuilder(this) - .add("name", name) - .add("phone", phone) - .add("email", email) - .add("address", address) - .add("tags", tags) - .toString(); - } - } diff --git a/src/main/java/seedu/address/model/person/PersonType.java b/src/main/java/seedu/address/model/person/PersonType.java new file mode 100644 index 00000000000..006307c8ad8 --- /dev/null +++ b/src/main/java/seedu/address/model/person/PersonType.java @@ -0,0 +1,38 @@ +package seedu.address.model.person; + +/** + * Represents the possible types of a contact entry in the tracker. + * Guarantees: is valid as declared in {@link #isValidPersonType(String)} + */ +public enum PersonType { + STU, + TA; + + public static final String MESSAGE_CONSTRAINTS = "Person type should only be 'stu' or 'ta'"; + + /** + * Returns the PersonType enum based on the given string. + */ + public static PersonType getPersonType(String type) { + switch (type) { + case "stu": + return PersonType.STU; + case "ta": + return PersonType.TA; + default: + return PersonType.STU; + } + } + + /** + * Returns true if a given string is a valid person type. + */ + public static boolean isValidPersonType(String type) { + return type.equals("stu") || type.equals("ta"); + } + + @Override + public String toString() { + return name().toLowerCase(); + } +} diff --git a/src/main/java/seedu/address/model/person/Student.java b/src/main/java/seedu/address/model/person/Student.java new file mode 100644 index 00000000000..4a2a3b91752 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Student.java @@ -0,0 +1,77 @@ +package seedu.address.model.person; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.tag.Tag; + +/** + * Represents a Student in TrAcker. + * A Student is a type of Person. + */ +public class Student extends Person { + private final Set tags = new HashSet<>(); + /** + * Every field must be present and not null. + * + * @param name + * @param id + * @param phone + * @param email + * @param tags + */ + public Student(Name name, Id id, Phone phone, Email email, Set tags) { + super(PersonType.STU, name, id, phone, email); + this.tags.addAll(tags); + } + + @Override + public Set getTags() { + return Collections.unmodifiableSet(tags); + } + + /** + * Returns true if both persons have the same identity and data fields. + * This defines a stronger notion of equality between two persons. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Student)) { + return false; + } + + Student otherStudent = (Student) other; + return name.equals(otherStudent.name) + && type.equals(otherStudent.type) + && id.equals(otherStudent.id) + && phone.equals(otherStudent.phone) + && email.equals(otherStudent.email) + && tags.equals(otherStudent.tags); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(type, name, phone, email, tags); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("type", type) + .add("name", name) + .add("id", id) + .add("phone", phone) + .add("email", email) + .add("tags", tags) + .toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/Ta.java b/src/main/java/seedu/address/model/person/Ta.java new file mode 100644 index 00000000000..fc50343a00f --- /dev/null +++ b/src/main/java/seedu/address/model/person/Ta.java @@ -0,0 +1,77 @@ +package seedu.address.model.person; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.tag.Tag; + +/** + * Represents a TA in TrAcker. + * A TA is a type of Person. + */ +public class Ta extends Person { + private final Set tags = new HashSet<>(); + /** + * Every field must be present and not null. + * + * @param name + * @param id + * @param phone + * @param email + * @param tags + */ + public Ta(Name name, Id id, Phone phone, Email email, Set tags) { + super(PersonType.TA, name, id, phone, email); + this.tags.addAll(tags); + } + + @Override + public Set getTags() { + return Collections.unmodifiableSet(tags); + } + + /** + * Returns true if both persons have the same identity and data fields. + * This defines a stronger notion of equality between two persons. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Ta)) { + return false; + } + + Ta otherTA = (Ta) other; + return name.equals(otherTA.name) + && type.equals(otherTA.type) + && id.equals(otherTA.id) + && phone.equals(otherTA.phone) + && email.equals(otherTA.email) + && tags.equals(otherTA.tags); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(type, name, phone, email, tags); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("type", type) + .add("name", name) + .add("id", id) + .add("phone", phone) + .add("email", email) + .add("tags", tags) + .toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/TutorialTagContainsGroupPredicate.java b/src/main/java/seedu/address/model/person/TutorialTagContainsGroupPredicate.java new file mode 100644 index 00000000000..040030e9039 --- /dev/null +++ b/src/main/java/seedu/address/model/person/TutorialTagContainsGroupPredicate.java @@ -0,0 +1,48 @@ +package seedu.address.model.person; + +import java.util.function.Predicate; + +import seedu.address.commons.util.StatefulStringUtil; +import seedu.address.commons.util.ToStringBuilder; + + +/** + * Tests that a {@code TA}'s {@code Tag} matches any of the tutorial groups given. + */ +public class TutorialTagContainsGroupPredicate implements Predicate { + private final String tutorialGroup; + + public TutorialTagContainsGroupPredicate(String tutorialGroup) { + this.tutorialGroup = tutorialGroup; + } + + @Override + public boolean test(Person person) { + if (person.getType() != PersonType.TA) { + return false; + } + return person.getTags().stream() + .anyMatch(tag -> StatefulStringUtil.containsTutorialGroup(tag, tutorialGroup)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof TutorialTagContainsGroupPredicate)) { + return false; + } + + TutorialTagContainsGroupPredicate otherNameContainsKeywordsPredicate = + (TutorialTagContainsGroupPredicate) other; + return tutorialGroup.equals(otherNameContainsKeywordsPredicate.tutorialGroup); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("tutorialGroup", tutorialGroup).toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/UniqueTutorialTagList.java b/src/main/java/seedu/address/model/person/UniqueTutorialTagList.java new file mode 100644 index 00000000000..997cdaaeedd --- /dev/null +++ b/src/main/java/seedu/address/model/person/UniqueTutorialTagList.java @@ -0,0 +1,133 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Iterator; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.person.exceptions.DuplicateTutorialTagException; +import seedu.address.model.person.exceptions.TutorialTagNotFoundException; +import seedu.address.model.tag.TutorialTag; + + +/** + * A list of tutorial tags that enforces uniqueness between its elements and does not allow nulls. + * A tutorial tag is considered unique by comparing using {@code TutorialTag#isSameTutorialTag(TutorialTag)}. + * As such, adding and updating of tutorial tags uses TutorialTag#isSameTutorialTag(TutorialTag) for equality so as to + * ensure that the tutorial tag being added or updated is unique in terms of identity in the UniqueTutorialTagList. + * However, the removal of a tutorial tag uses TutorialTag#equals(Object) so as to ensure that the tutorial tag + * with exactly the same TagName will be removed. + * + * Supports a minimal set of list operations. + */ +public class UniqueTutorialTagList implements Iterable { + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Returns true if the list contains an equivalent tutorial tag as the given argument. + */ + public boolean contains(TutorialTag toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSameTutorialTag); + } + + /** + * Adds a tutorial tag to the list. + * The tutorial tag must not already exist in the list. + */ + public void add(TutorialTag toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateTutorialTagException(); + } + internalList.add(toAdd); + } + + /** + * Removes the equivalent tutorial tag from the list. + * The tutorial tag must exist in the list. + */ + public void remove(TutorialTag toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new TutorialTagNotFoundException(); + } + } + + public void setTutorialTags(UniqueTutorialTagList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code tutorialTags}. + * {@code tutorialTags} must not contain duplicate tutorial tags. + */ + public void setTutorialTags(List tutorialTags) { + requireAllNonNull(tutorialTags); + if (!tutorialTagsAreUnique(tutorialTags)) { + throw new DuplicateTutorialTagException(); + } + + internalList.setAll(tutorialTags); + } + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableObservableList() { + return internalUnmodifiableList; + } + + @Override + public Iterator iterator() { + return internalList.iterator(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof UniqueTutorialTagList)) { + return false; + } + + UniqueTutorialTagList otherUniqueTutorialTagsList = (UniqueTutorialTagList) other; + return internalList.equals(otherUniqueTutorialTagsList.internalList); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + @Override + public String toString() { + return internalList.toString(); + } + + public boolean isEmpty() { + return internalList.isEmpty(); + } + + /** + * Returns true if {@code tutorialTags} contains only unique tutorial tags. + */ + private boolean tutorialTagsAreUnique(List tutorialTags) { + for (int i = 0; i < tutorialTags.size() - 1; i++) { + for (int j = i + 1; j < tutorialTags.size(); j++) { + if (tutorialTags.get(i).isSameTutorialTag(tutorialTags.get(j))) { + return false; + } + } + } + return true; + } +} diff --git a/src/main/java/seedu/address/model/person/exceptions/DuplicateTutorialTagException.java b/src/main/java/seedu/address/model/person/exceptions/DuplicateTutorialTagException.java new file mode 100644 index 00000000000..5ef7b8145da --- /dev/null +++ b/src/main/java/seedu/address/model/person/exceptions/DuplicateTutorialTagException.java @@ -0,0 +1,11 @@ +package seedu.address.model.person.exceptions; + +/** + * Signals that the operation will result in duplicate TutoiralTags (TutorialTags are considered duplicates + * if they have the same TagName). + */ +public class DuplicateTutorialTagException extends RuntimeException { + public DuplicateTutorialTagException() { + super("Operation would result in duplicate TutorialTags"); + } +} diff --git a/src/main/java/seedu/address/model/person/exceptions/TutorialTagNotFoundException.java b/src/main/java/seedu/address/model/person/exceptions/TutorialTagNotFoundException.java new file mode 100644 index 00000000000..d45d7afcb7c --- /dev/null +++ b/src/main/java/seedu/address/model/person/exceptions/TutorialTagNotFoundException.java @@ -0,0 +1,6 @@ +package seedu.address.model.person.exceptions; + +/** + * Signals that the operation is unable to find the specified TutorialTag. + */ +public class TutorialTagNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/tag/AssignmentTag.java b/src/main/java/seedu/address/model/tag/AssignmentTag.java new file mode 100644 index 00000000000..bb341100bd7 --- /dev/null +++ b/src/main/java/seedu/address/model/tag/AssignmentTag.java @@ -0,0 +1,16 @@ +package seedu.address.model.tag; + +/** + * Represents an assignment tag. + */ +public class AssignmentTag extends Tag { + /** + * Constructs a {@code Tag}. + * + * @param tagName A valid tag name. + * @param tagStatus A valid tag status. + */ + public AssignmentTag(String tagName, TagStatus tagStatus) { + super(tagName, tagStatus); + } +} diff --git a/src/main/java/seedu/address/model/tag/AttendanceTag.java b/src/main/java/seedu/address/model/tag/AttendanceTag.java new file mode 100644 index 00000000000..7135747a2cd --- /dev/null +++ b/src/main/java/seedu/address/model/tag/AttendanceTag.java @@ -0,0 +1,16 @@ +package seedu.address.model.tag; + +/** + * Represents an attendance tag. + */ +public class AttendanceTag extends Tag { + /** + * Constructs a {@code Tag}. + * + * @param tagName A valid tag name. + * @param tagStatus A valid tag status. + */ + public AttendanceTag(String tagName, TagStatus tagStatus) { + super(tagName, tagStatus); + } +} diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java index f1a0d4e233b..6fbb996d368 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/seedu/address/model/tag/Tag.java @@ -3,26 +3,96 @@ import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; +import java.util.Set; +import java.util.stream.Collectors; + /** * Represents a Tag in the address book. * Guarantees: immutable; name is valid as declared in {@link #isValidTagName(String)} */ -public class Tag { +public abstract class Tag { public static final String MESSAGE_CONSTRAINTS = "Tags names should be alphanumeric"; public static final String VALIDATION_REGEX = "\\p{Alnum}+"; - public final String tagName; + private final String tagName; + private TagStatus tagStatus; + private TagType tagType; /** * Constructs a {@code Tag}. * * @param tagName A valid tag name. */ - public Tag(String tagName) { + protected Tag(String tagName, TagStatus tagStatus) { requireNonNull(tagName); + // require the tagStatus not to be null for now + // in the future, a null tagStatus input should be set to INCOMPLETE_GOOD + // by default + requireNonNull(tagStatus); checkArgument(isValidTagName(tagName), MESSAGE_CONSTRAINTS); this.tagName = tagName; + this.tagStatus = tagStatus; + this.tagType = getTagTypeWithTagStatus(tagStatus); + } + + /** + * Creates a new Tag with specified tagName and tagStatus. + * + * @param tagName Name of the tag to be created. + * @param tagStatus Status of the tag. + * @return A new tag of specific type corresponding to the TagStatus input. + */ + public static Tag createTag(String tagName, TagStatus tagStatus) { + requireNonNull(tagName); + // require the tagStatus not to be null for now + // in the future, a null tagStatus input should be set to INCOMPLETE_GOOD + // by default + requireNonNull(tagStatus); + checkArgument(isValidTagName(tagName), MESSAGE_CONSTRAINTS); + TagType tagType = getTagTypeWithTagStatus(tagStatus); + + switch (tagType) { + case ASSIGNMENT: + return new AssignmentTag(tagName, tagStatus); + case TUTORIAL: + return new TutorialTag(tagName, tagStatus); + case ATTENDANCE: + return new AttendanceTag(tagName, tagStatus); + default: + return null; + } + } + + public String getTagName() { + return tagName; + } + + public TagStatus getTagStatus() { + return tagStatus; + } + + public TagType getTagType() { + return tagType; + } + + public static TagType getTagTypeWithTagStatus(TagStatus ts) { + switch (ts) { + case COMPLETE_GOOD: + case COMPLETE_BAD: + case INCOMPLETE_GOOD: + case INCOMPLETE_BAD: + return TagType.ASSIGNMENT; + case PRESENT: + case ABSENT: + case ABSENT_WITH_REASON: + return TagType.ATTENDANCE; + case ASSIGNED: + case AVAILABLE: + return TagType.TUTORIAL; + default: + return TagType.DEFAULT_TYPE; + } } /** @@ -56,7 +126,93 @@ public int hashCode() { * Format state as text for viewing. */ public String toString() { - return '[' + tagName + ']'; + return "[ " + tagName + " : " + tagStatus + " ]"; } + public static void isTagNameValid(String tagName) throws IllegalArgumentException { + checkArgument(isValidTagName(tagName), MESSAGE_CONSTRAINTS); + } + + /** + * @param currTags current tag set to be updated. + * @param tagName name of the new tag. + * @param tagStatus tagStatus of the new tag. + * @return + */ + public static Set updateTagsWithNewTag(Set currTags, String tagName, TagStatus tagStatus) { + // Instead of retrieving the Tag sharing the same name and update it, + // remove the potentially existing Tag of the same name from the hashset + // and then add in a new Tag with the same tagName but updated tagStatus. + // This is because Java Set does not provide a get() method. + Tag newTag = Tag.createTag(tagName, tagStatus); + currTags.remove(newTag); + currTags.add(Tag.createTag(tagName, tagStatus)); + return currTags; + } + + /** + * Merges the current set of tags with a new set of tags sharing the same status, + * identified by their tag names, updating the tag status of existing tags in the + * process. + * {@code updateTagsWithNewTag} method is called on each new tag. + */ + public static Set updateTagsWithNewTags(Set currTags, Set tagNames, TagStatus tagStatus) { + tagNames.forEach(x -> updateTagsWithNewTag(currTags, x, tagStatus)); + return currTags; + } + + /** + * @param currTags current tag set to be updated. + * @param tagName name of the new tag. + * @return + */ + public static Set removeTagFromTagSet(Set currTags, String tagName) { + // remove the potentially existing Tag of the same name from the hashset. + Tag newTag = Tag.createTag(tagName, TagStatus.DEFAULT_STATUS); + currTags.remove(newTag); + return currTags; + } + + /** + * @param currTags current tag set to be updated. + * @param tagName name of the new tutorial tag. + * @return + */ + public static Set removeTutTagFromTagSet(Set currTags, String tagName) { + // remove the potentially existing Tag of the same name from the hashset. + Tag newTag = Tag.createTag(tagName, TagStatus.DEFAULT_STATUS); + currTags.removeIf(tag -> tag.isTutorial() && tag.equals(newTag)); + return currTags; + } + + /** + * Removes the tags with the specified tag names from the current set of tags. + * @param currTags + * @param tagNames + * @return + */ + public static Set removeTagsFromTagSet(Set currTags, Set tagNames) { + currTags.removeAll(tagNames.stream() + .map(x -> Tag.createTag(x, TagStatus.DEFAULT_STATUS)) + .collect(Collectors.toSet())); + return currTags; + } + + public boolean isAttendance() { + return tagType == TagType.ATTENDANCE; + } + + public boolean isAssignment() { + return tagType == TagType.ASSIGNMENT; + } + + public boolean isTutorial() { + return tagType == TagType.TUTORIAL; + } + public boolean isAssigned() { + return isTutorial() && (tagStatus == TagStatus.ASSIGNED); + } + public boolean isAvailable() { + return isTutorial() && (tagStatus == TagStatus.AVAILABLE); + } } diff --git a/src/main/java/seedu/address/model/tag/TagStatus.java b/src/main/java/seedu/address/model/tag/TagStatus.java new file mode 100644 index 00000000000..8e36e973d8d --- /dev/null +++ b/src/main/java/seedu/address/model/tag/TagStatus.java @@ -0,0 +1,61 @@ +package seedu.address.model.tag; + +/** + * Represents submission / attendance status of a tag. + */ +public enum TagStatus { + COMPLETE_GOOD, // complete before deadline + COMPLETE_BAD, // complete after deadline + INCOMPLETE_GOOD, // incomplete before deadline + INCOMPLETE_BAD, // incomplete after deadline + + PRESENT, + ABSENT, + ABSENT_WITH_REASON, + + ASSIGNED, AVAILABLE; + + public static final TagStatus DEFAULT_STATUS = INCOMPLETE_GOOD; + public static final String COMPLETE_GOOD_KEYWORD = "cg"; + public static final String COMPLETE_BAD_KEYWORD = "cb"; + public static final String INCOMPLETE_GOOD_KEYWORD = "ig"; + public static final String INCOMPLETE_BAD_KEYWORD = "ib"; + public static final String PRESENT_KEYWORD = "p"; + public static final String ABSENT_KEYWORD = "a"; + public static final String ABSENT_WITH_REASON_KEYWORD = "awr"; + public static final String ASSIGNED_KEYWORD = "as"; + public static final String AVAILABLE_KEYWORD = "av"; + + public static final String INVALID_TAGSTATUS_ERROR_MSG = "Invalid TagStatus Flag, check out the help page for " + + "valid TagStatus tags."; + + /** + * @param status Keyword corresponding each of the TagStatus. + * @return TagStatus matching the keyword. + */ + public static TagStatus getTagStatus(String status) throws IllegalArgumentException { + switch (status) { + case COMPLETE_GOOD_KEYWORD: + return COMPLETE_GOOD; + case COMPLETE_BAD_KEYWORD: + return COMPLETE_BAD; + case INCOMPLETE_GOOD_KEYWORD: + return INCOMPLETE_GOOD; + case INCOMPLETE_BAD_KEYWORD: + return INCOMPLETE_BAD; + case PRESENT_KEYWORD: + return PRESENT; + case ABSENT_KEYWORD: + return ABSENT; + case ABSENT_WITH_REASON_KEYWORD: + return ABSENT_WITH_REASON; + case ASSIGNED_KEYWORD: + return ASSIGNED; + case AVAILABLE_KEYWORD: + return AVAILABLE; + default: + throw new IllegalArgumentException(INVALID_TAGSTATUS_ERROR_MSG); + } + } + +} diff --git a/src/main/java/seedu/address/model/tag/TagType.java b/src/main/java/seedu/address/model/tag/TagType.java new file mode 100644 index 00000000000..a5394dfb6b1 --- /dev/null +++ b/src/main/java/seedu/address/model/tag/TagType.java @@ -0,0 +1,11 @@ +package seedu.address.model.tag; + +/** + * Represents the different types of tags. + */ +public enum TagType { + ASSIGNMENT, + ATTENDANCE, + TUTORIAL; + public static final TagType DEFAULT_TYPE = ASSIGNMENT; +} diff --git a/src/main/java/seedu/address/model/tag/TutorialTag.java b/src/main/java/seedu/address/model/tag/TutorialTag.java new file mode 100644 index 00000000000..36400296545 --- /dev/null +++ b/src/main/java/seedu/address/model/tag/TutorialTag.java @@ -0,0 +1,24 @@ +package seedu.address.model.tag; + +/** + * Represents a tutorial tag. + */ +public class TutorialTag extends Tag { + /** + * Constructs a {@code Tag}. + * + * @param tagName A valid tag name. + * @param tagStatus A valid tag status. + */ + public TutorialTag(String tagName, TagStatus tagStatus) { + super(tagName, tagStatus); + } + + public boolean isSameTutorialTag(TutorialTag tutorialTag) { + return this.getTagName().equals(tutorialTag.getTagName()); + } + @Override + public String toString() { + return getTagName(); + } +} diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facf..f0664457e05 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -6,12 +6,17 @@ 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.Id; import seedu.address.model.person.Name; import seedu.address.model.person.Person; +import seedu.address.model.person.PersonType; import seedu.address.model.person.Phone; +import seedu.address.model.tag.AssignmentTag; +import seedu.address.model.tag.AttendanceTag; import seedu.address.model.tag.Tag; +import seedu.address.model.tag.TagStatus; +import seedu.address.model.tag.TutorialTag; /** * Contains utility methods for populating {@code AddressBook} with sample data. @@ -19,29 +24,54 @@ public class SampleDataUtil { public static Person[] getSamplePersons() { return new Person[] { - new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), - new Address("Blk 30 Geylang Street 29, #06-40"), - getTagSet("friends")), - new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), - new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), - getTagSet("colleagues", "friends")), - new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), - new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), - getTagSet("neighbours")), - new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), - new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), - getTagSet("family")), - new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), - new Address("Blk 47 Tampines Street 20, #17-35"), - getTagSet("classmates")), - new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), - new Address("Blk 45 Aljunied Street 85, #11-31"), - getTagSet("colleagues")) + Person.of(PersonType.STU, new Name("Bernice Yu"), new Id("A9128392K"), new Phone("99272758"), + new Email("berniceyu@example.com"), getTagSet( + new AssignmentTag("Assignment1", TagStatus.COMPLETE_GOOD), + new AssignmentTag("Assignment2", TagStatus.INCOMPLETE_GOOD), + new AttendanceTag("Week1", TagStatus.PRESENT), + new AttendanceTag("Week2", TagStatus.ABSENT), + new TutorialTag("WED09", TagStatus.ASSIGNED))), + Person.of(PersonType.TA, new Name("Charlotte Oliveiro"), new Id("A2222222P"), new Phone("93210283"), + new Email("charlotte@example.com"), getTagSet( + new TutorialTag("THU10", TagStatus.AVAILABLE), + new TutorialTag("WED09", TagStatus.ASSIGNED))), + Person.of(PersonType.STU, new Name("David Li"), new Id("A9128392Z"), new Phone("91031282"), + new Email("lidavid@example.com"), getTagSet( + new AssignmentTag("Assignment1", TagStatus.INCOMPLETE_GOOD), + new AssignmentTag("Assignment2", TagStatus.INCOMPLETE_BAD), + new AttendanceTag("Week1", TagStatus.ABSENT_WITH_REASON), + new AttendanceTag("Week2", TagStatus.PRESENT), + new TutorialTag("TUES08", TagStatus.ASSIGNED))), + Person.of(PersonType.STU, new Name("Irfan Ibrahim"), new Id("B0198266Z"), new Phone("92492021"), + new Email("irfan@example.com"), getTagSet( + new AssignmentTag("Assignment1", TagStatus.INCOMPLETE_GOOD), + new AssignmentTag("Assignment2", TagStatus.INCOMPLETE_GOOD), + new AttendanceTag("Week1", TagStatus.ABSENT_WITH_REASON), + new AttendanceTag("Week2", TagStatus.PRESENT), + new TutorialTag("THU10", TagStatus.ASSIGNED))), + Person.of(PersonType.STU, new Name("Roy Balakrishnan"), new Id("B0000666C"), new Phone("92624417"), + new Email("royb@example.com"), getTagSet( + new AssignmentTag("Assignment1", TagStatus.INCOMPLETE_GOOD), + new AssignmentTag("Assignment2", TagStatus.INCOMPLETE_BAD), + new AttendanceTag("Week1", TagStatus.PRESENT), + new AttendanceTag("Week2", TagStatus.PRESENT), + new TutorialTag("THU10", TagStatus.ASSIGNED))), + }; + } + + public static TutorialTag[] getSampleTutorialTags() { + return new TutorialTag[] { + new TutorialTag("TUE08", TagStatus.AVAILABLE), + new TutorialTag("WED09", TagStatus.AVAILABLE), + new TutorialTag("THU10", TagStatus.AVAILABLE), }; } public static ReadOnlyAddressBook getSampleAddressBook() { AddressBook sampleAb = new AddressBook(); + for (TutorialTag sampleTutorialTag : getSampleTutorialTags()) { + sampleAb.addTutorialTag(sampleTutorialTag); + } for (Person samplePerson : getSamplePersons()) { sampleAb.addPerson(samplePerson); } @@ -53,8 +83,16 @@ public static ReadOnlyAddressBook getSampleAddressBook() { */ public static Set getTagSet(String... strings) { return Arrays.stream(strings) - .map(Tag::new) + .map(tagName -> Tag.createTag(tagName, TagStatus.DEFAULT_STATUS)) .collect(Collectors.toSet()); } + /** + * Returns a tag set containing a list of tags. + */ + public static Set getTagSet(Tag... tags) { + return Arrays.stream(tags) + .map(tag -> Tag.createTag(tag.getTagName(), tag.getTagStatus())) + .collect(Collectors.toSet()); + } } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java index bd1ca0f56c8..e39960d0a71 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java @@ -10,10 +10,11 @@ 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.Id; import seedu.address.model.person.Name; import seedu.address.model.person.Person; +import seedu.address.model.person.PersonType; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; @@ -24,23 +25,27 @@ class JsonAdaptedPerson { public static final String MISSING_FIELD_MESSAGE_FORMAT = "Person's %s field is missing!"; + private final String type; private final String name; + private final String id; private final String phone; private final String email; - private final String address; private final List tags = new ArrayList<>(); /** * Constructs a {@code JsonAdaptedPerson} with the given person details. */ @JsonCreator - public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, - @JsonProperty("email") String email, @JsonProperty("address") String address, + public JsonAdaptedPerson(@JsonProperty("type") String type, @JsonProperty("name") String name, + @JsonProperty("id") String id, + @JsonProperty("phone") String phone, + @JsonProperty("email") String email, @JsonProperty("tags") List tags) { + this.type = type; this.name = name; + this.id = id; this.phone = phone; this.email = email; - this.address = address; if (tags != null) { this.tags.addAll(tags); } @@ -50,10 +55,11 @@ public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone * Converts a given {@code Person} into this class for Jackson use. */ public JsonAdaptedPerson(Person source) { + type = source.getType().toString(); name = source.getName().fullName; + id = source.getId().value; phone = source.getPhone().value; email = source.getEmail().value; - address = source.getAddress().value; tags.addAll(source.getTags().stream() .map(JsonAdaptedTag::new) .collect(Collectors.toList())); @@ -70,6 +76,15 @@ public Person toModelType() throws IllegalValueException { personTags.add(tag.toModelType()); } + if (type == null) { + throw new IllegalValueException( + String.format(MISSING_FIELD_MESSAGE_FORMAT, PersonType.class.getSimpleName())); + } + if (!PersonType.isValidPersonType(type)) { + throw new IllegalValueException(PersonType.MESSAGE_CONSTRAINTS); + } + final PersonType personType = PersonType.getPersonType(type); + if (name == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); } @@ -77,6 +92,13 @@ public Person toModelType() throws IllegalValueException { throw new IllegalValueException(Name.MESSAGE_CONSTRAINTS); } final Name modelName = new Name(name); + if (id == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Id.class.getSimpleName())); + } + if (!Id.isValidId(id)) { + throw new IllegalValueException(Id.MESSAGE_CONSTRAINTS); + } + final Id modelId = new Id(id); if (phone == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName())); @@ -94,16 +116,9 @@ public Person toModelType() throws IllegalValueException { } final Email modelEmail = new Email(email); - if (address == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName())); - } - if (!Address.isValidAddress(address)) { - throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); - } - final Address modelAddress = new Address(address); - final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); + + return Person.of(personType, modelName, modelId, modelPhone, modelEmail, modelTags); } } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedTag.java b/src/main/java/seedu/address/storage/JsonAdaptedTag.java index 0df22bdb754..61e31859e08 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedTag.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedTag.java @@ -1,10 +1,12 @@ package seedu.address.storage; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonProperty; import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.tag.Tag; +import seedu.address.model.tag.TagStatus; +import seedu.address.model.tag.TagType; /** * Jackson-friendly version of {@link Tag}. @@ -12,27 +14,46 @@ class JsonAdaptedTag { private final String tagName; + private final TagStatus tagStatus; + private final TagType tagType; /** * Constructs a {@code JsonAdaptedTag} with the given {@code tagName}. */ @JsonCreator - public JsonAdaptedTag(String tagName) { + public JsonAdaptedTag(@JsonProperty("tagName") String tagName, + @JsonProperty("tagStatus") TagStatus tagStatus, + @JsonProperty("tagType") TagType tagType) { this.tagName = tagName; + this.tagStatus = tagStatus; + this.tagType = tagType; } /** * Converts a given {@code Tag} into this class for Jackson use. */ public JsonAdaptedTag(Tag source) { - tagName = source.tagName; + tagName = source.getTagName(); + tagStatus = source.getTagStatus(); + tagType = source.getTagType(); } - @JsonValue + @JsonProperty("tagName") public String getTagName() { return tagName; } + @JsonProperty("tagStatus") + public TagStatus getTagStatus() { + return tagStatus; + } + + @JsonProperty("tagType") + public TagType getTagType() { + return tagType; + } + + /** * Converts this Jackson-friendly adapted tag object into the model's {@code Tag} object. * @@ -42,7 +63,7 @@ public Tag toModelType() throws IllegalValueException { if (!Tag.isValidTagName(tagName)) { throw new IllegalValueException(Tag.MESSAGE_CONSTRAINTS); } - return new Tag(tagName); + return Tag.createTag(tagName, tagStatus); } } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedTutorialTag.java b/src/main/java/seedu/address/storage/JsonAdaptedTutorialTag.java new file mode 100644 index 00000000000..62312e6dad5 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedTutorialTag.java @@ -0,0 +1,49 @@ +package seedu.address.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.tag.TagStatus; +import seedu.address.model.tag.TutorialTag; + +/** + * Jackson-friendly version of {@link TutorialTag}. + */ +class JsonAdaptedTutorialTag { + + private final String tagName; + + /** + * Constructs a {@code JsonAdaptedTag} with the given {@code tagName}. + */ + @JsonCreator + public JsonAdaptedTutorialTag(@JsonProperty("tagName") String tagName) { + this.tagName = tagName; + } + + /** + * Converts a given {@code TutorialTag} into this class for Jackson use. + */ + public JsonAdaptedTutorialTag(TutorialTag source) { + tagName = source.getTagName(); + } + + @JsonProperty("tagName") + public String getTagName() { + return tagName; + } + + /** + * Converts this Jackson-friendly adapted tutorialTag object into the model's {@code TutorialTag} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted tutorialTag. + */ + public TutorialTag toModelType() throws IllegalValueException { + if (!TutorialTag.isValidTagName(tagName)) { + throw new IllegalValueException(TutorialTag.MESSAGE_CONSTRAINTS); + } + return new TutorialTag(tagName, TagStatus.AVAILABLE); + } + +} diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java index 5efd834091d..0f48b3a1bbb 100644 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java @@ -12,6 +12,7 @@ import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.person.Person; +import seedu.address.model.tag.TutorialTag; /** * An Immutable AddressBook that is serializable to JSON format. @@ -20,15 +21,19 @@ class JsonSerializableAddressBook { public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; + public static final String MESSAGE_DUPLICATE_TUTORIALTAG = "Persons list contains duplicate tutorial tag(s)."; private final List persons = new ArrayList<>(); + private final List tutorialTags = new ArrayList<>(); /** - * Constructs a {@code JsonSerializableAddressBook} with the given persons. + * Constructs a {@code JsonSerializableAddressBook} with the given persons and tutorialTags. */ @JsonCreator - public JsonSerializableAddressBook(@JsonProperty("persons") List persons) { + public JsonSerializableAddressBook(@JsonProperty("persons") List persons, + @JsonProperty("tutorialTags") List tutorialTags) { this.persons.addAll(persons); + this.tutorialTags.addAll(tutorialTags); } /** @@ -38,6 +43,8 @@ public JsonSerializableAddressBook(@JsonProperty("persons") List { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; + public static final String USERGUIDE_URL = "https://ay2324s2-cs2103t-t11-4.github.io/tp/UserGuide.html"; public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 79e74ef37c0..7fae7c3b44d 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -5,14 +5,18 @@ import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.MenuItem; +import javafx.scene.control.TextField; import javafx.scene.control.TextInputControl; +import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; import javafx.scene.layout.StackPane; +import javafx.stage.Modality; import javafx.stage.Stage; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; import seedu.address.logic.Logic; +import seedu.address.logic.commands.Command; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; @@ -78,6 +82,7 @@ private void setAccelerators() { /** * Sets the accelerator of a MenuItem. + * * @param keyCombination the KeyCombination value of the accelerator */ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { @@ -147,6 +152,21 @@ public void handleHelp() { } } + /** + * Opens the warning window or focuses on it if it's already opened. + */ + @FXML + public void handleWarning(WarningWindow warningWindow) { + Stage popupStage = warningWindow.getRoot(); + + // ensure that the warningWindow is always focused and must + // be interacted before the user can return to the main window + popupStage.initModality(Modality.APPLICATION_MODAL); + popupStage.initOwner(primaryStage); + + warningWindow.showAndWait(); + } + void show() { primaryStage.show(); } @@ -163,34 +183,67 @@ private void handleExit() { primaryStage.hide(); } + /** + * Gives the command text field focus and makes it editable + * whenever enter or slash keys are pressed. + */ + @FXML + private void handleEnterReleased(KeyEvent event) { + if (event.getCode() == KeyCode.ENTER || event.getCode() == KeyCode.SLASH) { + TextField commandInputField = + (TextField) ((StackPane) this.commandBoxPlaceholder.getChildren().get(0)).getChildren().get(0); + commandInputField.requestFocus(); + commandInputField.setEditable(true); + } + } + public PersonListPanel getPersonListPanel() { return personListPanel; } /** * Executes the command and returns the result. - * - * @see seedu.address.logic.Logic#execute(String) */ private CommandResult executeCommand(String commandText) throws CommandException, ParseException { try { - CommandResult commandResult = logic.execute(commandText); - logger.info("Result: " + commandResult.getFeedbackToUser()); - resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); - - if (commandResult.isShowHelp()) { - handleHelp(); + Command command = logic.parseCommand(commandText); + + if (command.getNeedsWarningPopup()) { + WarningWindow warningWindow = new WarningWindow(); + warningWindow.setMessage(commandText + " - Click OK to confirm."); + handleWarning(warningWindow); + if (warningWindow.isOkClicked()) { + return unsafeExecuteCommand(command); + } else { + CommandResult commandResult = new CommandResult("Execution of " + commandText + + " aborted", false, false); + logger.info("Result: " + commandResult.getFeedbackToUser()); + resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); + return commandResult; + } + } else { + return unsafeExecuteCommand(command); } - - if (commandResult.isExit()) { - handleExit(); - } - - return commandResult; } catch (CommandException | ParseException e) { logger.info("An error occurred while executing command: " + commandText); resultDisplay.setFeedbackToUser(e.getMessage()); throw e; } } + + private CommandResult unsafeExecuteCommand(Command command) throws CommandException, ParseException { + CommandResult commandResult = logic.execute(command); + logger.info("Result: " + commandResult.getFeedbackToUser()); + resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); + + if (commandResult.isShowHelp()) { + handleHelp(); + } + + if (commandResult.isExit()) { + handleExit(); + } + + return commandResult; + } } diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index 094c42cda82..71b3ed4b588 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -1,6 +1,7 @@ package seedu.address.ui; import java.util.Comparator; +import java.util.function.Predicate; import javafx.fxml.FXML; import javafx.scene.control.Label; @@ -8,6 +9,8 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Region; import seedu.address.model.person.Person; +import seedu.address.model.person.PersonType; +import seedu.address.model.tag.Tag; /** * An UI component that displays information of a {@code Person}. @@ -32,14 +35,24 @@ public class PersonCard extends UiPart { private Label name; @FXML private Label id; + + @FXML + private Label index; @FXML private Label phone; @FXML private Label address; @FXML private Label email; + + @FXML + private Label type; @FXML - private FlowPane tags; + private FlowPane assignmentTags; + @FXML + private FlowPane attendanceTags; + @FXML + private FlowPane tutorialTags; /** * Creates a {@code PersonCode} with the given {@code Person} and index to display. @@ -47,13 +60,27 @@ public class PersonCard extends UiPart { public PersonCard(Person person, int displayedIndex) { super(FXML); this.person = person; - id.setText(displayedIndex + ". "); + index.setText(Integer.toString(displayedIndex)); name.setText(person.getName().fullName); + id.setText(person.getId().value); phone.setText(person.getPhone().value); - address.setText(person.getAddress().value); email.setText(person.getEmail().value); + type.setText(person.getType().toString()); + type.getStyleClass().setAll(person.getType() == PersonType.TA ? "type-ta" : "type-stu"); + addTagsToContainer(person, Tag::isAssignment, assignmentTags); + addTagsToContainer(person, Tag::isAttendance, attendanceTags); + addTagsToContainer(person, Tag::isAssigned, tutorialTags); + addTagsToContainer(person, Tag::isAvailable, tutorialTags); + } + + private void addTagsToContainer(Person person, Predicate filterPredicate, FlowPane container) { person.getTags().stream() - .sorted(Comparator.comparing(tag -> tag.tagName)) - .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + .sorted(Comparator.comparing(tag -> tag.getTagName())) + .filter(filterPredicate) + .forEach(tag -> { + Label tagLabel = new Label(tag.getTagName()); + tagLabel.getStyleClass().addAll("label", tag.getTagStatus().toString().toLowerCase()); + container.getChildren().add(tagLabel); + }); } } diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/seedu/address/ui/PersonListPanel.java index f4c501a897b..cd52e96c8e4 100644 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ b/src/main/java/seedu/address/ui/PersonListPanel.java @@ -16,7 +16,6 @@ public class PersonListPanel extends UiPart { private static final String FXML = "PersonListPanel.fxml"; private final Logger logger = LogsCenter.getLogger(PersonListPanel.class); - @FXML private ListView personListView; diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java index fdf024138bc..ee7cd3c0338 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/address/ui/UiManager.java @@ -9,7 +9,7 @@ import javafx.stage.Stage; import seedu.address.MainApp; import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.util.StringUtil; +import seedu.address.commons.util.StatefulStringUtil; import seedu.address.logic.Logic; /** @@ -20,7 +20,7 @@ public class UiManager implements Ui { public static final String ALERT_DIALOG_PANE_FIELD_ID = "alertDialogPane"; private static final Logger logger = LogsCenter.getLogger(UiManager.class); - private static final String ICON_APPLICATION = "/images/address_book_32.png"; + private static final String ICON_APPLICATION = "/images/icon.png"; private Logic logic; private MainWindow mainWindow; @@ -45,7 +45,7 @@ public void start(Stage primaryStage) { mainWindow.fillInnerParts(); } catch (Throwable e) { - logger.severe(StringUtil.getDetails(e)); + logger.severe(StatefulStringUtil.getDetails(e)); showFatalErrorDialogAndShutdown("Fatal error during initializing", e); } } @@ -79,7 +79,7 @@ private static void showAlertDialogAndWait(Stage owner, AlertType type, String t * and exits the application after the user has closed the alert dialog. */ private void showFatalErrorDialogAndShutdown(String title, Throwable e) { - logger.severe(title + " " + e.getMessage() + StringUtil.getDetails(e)); + logger.severe(title + " " + e.getMessage() + StatefulStringUtil.getDetails(e)); showAlertDialogAndWait(Alert.AlertType.ERROR, title, e.getMessage(), e.toString()); Platform.exit(); System.exit(1); diff --git a/src/main/java/seedu/address/ui/WarningWindow.java b/src/main/java/seedu/address/ui/WarningWindow.java new file mode 100644 index 00000000000..897fb9a9075 --- /dev/null +++ b/src/main/java/seedu/address/ui/WarningWindow.java @@ -0,0 +1,133 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.fxml.FXML; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.stage.Stage; +import seedu.address.commons.core.LogsCenter; + +/** + * This class is the controller for the warning window pop-up. + */ +public class WarningWindow extends UiPart { + + private static final Logger logger = LogsCenter.getLogger(WarningWindow.class); + private static final String FXML = "WarningWindow.fxml"; + + @FXML + private Label warningMessage; + + @FXML + private Button okButton; + + @FXML + private Button cancelButton; + + private boolean isOkClicked = false; + + /** + * Constructor for Warning Window controller + * @param root + */ + public WarningWindow(Stage root) { + super(FXML, root); + Scene scene = root.getScene(); + if (scene != null) { + cancelButton.setCancelButton(true); + cancelButton.requestFocus(); + okButton.disarm(); + cancelButton.arm(); + } else { + logger.warning("Scene is null, unable to set default button."); + } + + scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> { + if (event.getCode() == KeyCode.LEFT) { + okButton.requestFocus(); + okButton.arm(); + cancelButton.disarm(); + event.consume(); + } + }); + + scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> { + if (event.getCode() == KeyCode.RIGHT) { + cancelButton.requestFocus(); + cancelButton.arm(); + okButton.disarm(); + event.consume(); + } + }); + + scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> { + if (event.getCode() == KeyCode.ENTER) { + if (scene != null && scene.getFocusOwner() instanceof Button) { + Button focusedButton = (Button) scene.getFocusOwner(); + focusedButton.fire(); + event.consume(); + } + } + }); + } + + /** + * Creates a new WarningWindow. + */ + public WarningWindow() { + this(new Stage()); + } + + public void setMessage(String message) { + warningMessage.setText(message); + } + + public boolean isOkClicked() { + return isOkClicked; + } + + @FXML + private void okClicked() { + isOkClicked = true; + hide(); + } + + @FXML + private void cancelClicked() { + hide(); + } + + /** + * Shows warning page and waits for user action. + */ + public void showAndWait() { + logger.fine("Showing warning page about the application and waiting."); + getRoot().showAndWait(); + getRoot().centerOnScreen(); + } + + /** + * Returns true if the warning window is currently being shown. + */ + public boolean isShowing() { + return getRoot().isShowing(); + } + + /** + * Hides the warning window. + */ + public void hide() { + getRoot().hide(); + } + + /** + * Focuses on the warning window. + */ + public void focus() { + getRoot().requestFocus(); + } +} diff --git a/src/main/resources/images/address_book_32.png b/src/main/resources/images/address_book_32.png deleted file mode 100644 index 29810cf1fd9..00000000000 Binary files a/src/main/resources/images/address_book_32.png and /dev/null differ diff --git a/src/main/resources/images/icon.png b/src/main/resources/images/icon.png new file mode 100644 index 00000000000..bf404254708 Binary files /dev/null and b/src/main/resources/images/icon.png differ diff --git a/src/main/resources/images/warning_icon.png b/src/main/resources/images/warning_icon.png new file mode 100644 index 00000000000..440c334f016 Binary files /dev/null and b/src/main/resources/images/warning_icon.png differ diff --git a/src/main/resources/view/CommandBox.fxml b/src/main/resources/view/CommandBox.fxml index 124283a392e..f3b1384fe62 100644 --- a/src/main/resources/view/CommandBox.fxml +++ b/src/main/resources/view/CommandBox.fxml @@ -1,9 +1,10 @@ - - + + - - + + - diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8..7aafbde0d3c 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -90,7 +90,7 @@ .list-view { -fx-background-insets: 0; -fx-padding: 0; - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #e0e0e0; } .list-cell { @@ -100,21 +100,26 @@ } .list-cell:filled:even { - -fx-background-color: #3c3e3f; + -fx-background-color: white; + -fx-background-radius: 15; } .list-cell:filled:odd { - -fx-background-color: #515658; + -fx-background-color: white; + -fx-background-radius: 15; } .list-cell:filled:selected { - -fx-background-color: #424d5f; + -fx-background-color: #b6c2d6; + -fx-background-radius: 15; } +/* .list-cell:filled:selected #cardPane { - -fx-border-color: #3e7b91; - -fx-border-width: 1; -} + -fx-border-color: black; + -fx-border-width: 2; + -fx-border-radius: 15; +}*/ .list-cell .label { -fx-text-fill: white; @@ -147,19 +152,23 @@ } .result-display { - -fx-background-color: transparent; + -fx-padding: 1; + -fx-background-color: #e0e0e0; + -fx-border-color: #383838; + -fx-border-width: 1; + -fx-border-radius: 5; -fx-font-family: "Segoe UI Light"; -fx-font-size: 13pt; - -fx-text-fill: white; + -fx-text-fill: black; } .result-display .label { - -fx-text-fill: black !important; + -fx-text-fill: black; } .status-bar .label { -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; + -fx-text-fill: black; -fx-padding: 4px; -fx-pref-height: 30px; } @@ -171,7 +180,7 @@ } .status-bar-with-border .label { - -fx-text-fill: white; + -fx-text-fill: black; } .grid-pane { @@ -189,17 +198,17 @@ } .context-menu .label { - -fx-text-fill: white; + -fx-text-fill: black; } .menu-bar { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #e0e0e0; } .menu-bar .label { -fx-font-size: 14pt; -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; + -fx-text-fill: black; -fx-opacity: 0.9; } @@ -282,11 +291,11 @@ } .scroll-bar { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #e0e0e0; } .scroll-bar .thumb { - -fx-background-color: derive(#1d1d1d, 50%); + -fx-background-color: grey; -fx-background-insets: 3; } @@ -307,9 +316,28 @@ -fx-padding: 8 1 8 1; } +.type-stu { + -fx-text-fill: white; + -fx-background-color: #e38314; + -fx-padding: 1 3 1 3; + -fx-border-radius: 5; + -fx-background-radius: 2; + -fx-font-size: 11; +} + +.type-ta { + -fx-text-fill: white; + -fx-background-color: #1d913c; + -fx-padding: 1 3 1 3; + -fx-border-radius: 5; + -fx-background-radius: 2; + -fx-font-size: 11; +} + #cardPane { - -fx-background-color: transparent; - -fx-border-width: 0; + -fx-border-width: 5; + -fx-border-color: #e0e0e0; + -fx-border-radius: 10; } #commandTypeLabel { @@ -318,14 +346,14 @@ } #commandTextField { - -fx-background-color: transparent #383838 transparent #383838; + -fx-background-color: transparent; -fx-background-insets: 0; - -fx-border-color: #383838 #383838 #ffffff #383838; + -fx-border-color: #383838; -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: black; } #filterField, #personListPanel, #personWebpage { @@ -333,16 +361,16 @@ } #resultDisplay .content { - -fx-background-color: transparent, #383838, transparent, #383838; + -fx-background-color: #e0e0e0; -fx-background-radius: 0; } -#tags { +#assignmentTags { -fx-hgap: 7; -fx-vgap: 3; } -#tags .label { +#assignmentTags .label { -fx-text-fill: white; -fx-background-color: #3e7b91; -fx-padding: 1 3 1 3; @@ -350,3 +378,71 @@ -fx-background-radius: 2; -fx-font-size: 11; } + +#assignmentTags .label.complete_good { + -fx-background-color: green; /* Green color for tags with complete_good status */ +} + +#assignmentTags .label.complete_bad { + -fx-background-color: orange; /* Red color for tags with complete_bad status */ +} + +#assignmentTags .label.incomplete_good { + -fx-background-color: grey; /* Blue color for tags with incomplete_good status */ +} + +#assignmentTags .label.incomplete_bad { + -fx-background-color: red; /* Orange color for tags with incomplete_bad status */ +} + +#attendanceTags { + -fx-hgap: 7; + -fx-vgap: 3; +} + +#attendanceTags .label { + -fx-text-fill: white; + -fx-background-color: #3e7b91; + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 2; + -fx-font-size: 11; +} + +#attendanceTags .label.present { + -fx-background-color: green; /* Green color for tags with complete_good status */ +} + +#attendanceTags .label.absent { + -fx-background-color: red; /* Red color for tags with complete_bad status */ +} + +#attendanceTags .label.absent_with_reason { + -fx-background-color: orange; /* Blue color for tags with incomplete_good status */ +} + +#tutorialTags { + -fx-hgap: 7; + -fx-vgap: 3; +} + +#tutorialTags .label { + -fx-border-color: #3e7b91; + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 2; + -fx-font-size: 11; +} + +#tutorialTags .label.assigned { + -fx-background-color: #3e7b91; /* Dark blue color for tags with assigned status */ + -fx-text-fill: white /* White tex fill for tags with assigned status */ +} + +#tutorialTags .label.available { + -fx-background-color: white; /* White color for tags with assigned status */ + -fx-text-fill: black /* Black text fill for tags with assigned status */ +} + + + diff --git a/src/main/resources/view/Extensions.css b/src/main/resources/view/Extensions.css index bfe82a85964..8e90441204b 100644 --- a/src/main/resources/view/Extensions.css +++ b/src/main/resources/view/Extensions.css @@ -1,16 +1,16 @@ .error { - -fx-text-fill: #d06651 !important; /* The error class should always override the default text-fill style */ + -fx-text-fill: #c92808 !important; /* The error class should always override the default text-fill style */ } .list-cell:empty { /* Empty cells will not have alternating colours */ - -fx-background: #383838; + -fx-background: #e0e0e0; } .tag-selector { -fx-border-width: 1; - -fx-border-color: white; + -fx-border-color: grey; -fx-border-radius: 3; -fx-background-radius: 3; } diff --git a/src/main/resources/view/HelpWindow.css b/src/main/resources/view/HelpWindow.css index 17e8a8722cd..39898033dbf 100644 --- a/src/main/resources/view/HelpWindow.css +++ b/src/main/resources/view/HelpWindow.css @@ -1,19 +1,24 @@ #copyButton, #helpMessage { - -fx-text-fill: white; + -fx-text-fill: black; } #copyButton { - -fx-background-color: dimgray; + -fx-background-color: #e0e0e0; } #copyButton:hover { - -fx-background-color: gray; + -fx-background-color: grey; +} + +#copyButton:focused{ + -fx-border-color: #3e7b91; + -fx-border-radius: 2; } #copyButton:armed { - -fx-background-color: darkgray; + -fx-background-color: grey; } #helpMessageContainer { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: white; } diff --git a/src/main/resources/view/HelpWindow.fxml b/src/main/resources/view/HelpWindow.fxml index e01f330de33..19e333882e0 100644 --- a/src/main/resources/view/HelpWindow.fxml +++ b/src/main/resources/view/HelpWindow.fxml @@ -26,7 +26,7 @@ -