diff --git a/.gitignore b/.gitignore index 284c4ca7cd9..85d189d2d56 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ src/main/resources/docs/ /.idea/ /out/ /*.iml +/.classpath +/.project +/.settings # Storage/log files /data/ @@ -21,3 +24,10 @@ src/test/data/sandbox/ # MacOS custom attributes files created by Finder .DS_Store docs/_site/ + +.classpath +.project +.settings/ + +# Build files for FXML +/bin/ diff --git a/README.md b/README.md index 16208adb9b6..fe74e0a674e 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,23 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) +# TAbby Dabby + +[![Java CI](https://github.com/AY2425S2-CS2103T-T12-1/tp/actions/workflows/gradle.yml/badge.svg)](https://github.com/AY2425S2-CS2103T-T12-1/tp/actions/workflows/gradle.yml) +[![Codecov](https://codecov.io/gh/AY2425S2-CS2103T-T12-1/tp/branch/master/graph/badge.svg)](https://codecov.io/gh/AY2425S2-CS2103T-T12-1/tp/branch/master/graph/badge.svg) ![Ui](docs/images/Ui.png) -* This is **a sample project for Software Engineering (SE) students**.
- Example usages: - * as a starting point of a course project (as opposed to writing everything from scratch) - * as a case study -* The project simulates an ongoing software project for a desktop application (called _AddressBook_) used for managing contact details. - * It is **written in OOP fashion**. It provides a **reasonably well-written** code base **bigger** (around 6 KLoC) than what students usually write in beginner-level SE modules, without being overwhelmingly big. - * It comes with a **reasonable level of user and developer documentation**. -* It is named `AddressBook Level 3` (`AB3` for short) because it was initially created as a part of a series of `AddressBook` projects (`Level 1`, `Level 2`, `Level 3` ...). -* For the detailed documentation of this project, see the **[Address Book Product Website](https://se-education.org/addressbook-level3)**. -* This project is a **part of the se-education.org** initiative. If you would like to contribute code to this project, see [se-education.org](https://se-education.org/#contributing-to-se-edu) for more info. +TAbby Dabby is a **desktop app designed for teaching assistants, 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, it can get your administrative tasks done faster than traditional GUI apps. + +It is named TAbby Dabby because it rolls off the tongue and is designed to assist TAs (i.e., Teaching Assistants) with their administrative work. + +For the detailed documentation of this project, see the [TAbby Dabby Product Website](https://ay2425s2-cs2103t-t12-1.github.io/). + +This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). + +## Features + +At its core, TAbby Dabby allows teaching assistants to: + +- Create and manage contacts of students, teaching assistants, and lecturers +- Create and manage tutorial groups +- Track students' attendance in each tutorial group +- Track students' assignment scores in each tutorial group diff --git a/build.gradle b/build.gradle index 0db3743584e..98f2c08fd67 100644 --- a/build.gradle +++ b/build.gradle @@ -4,6 +4,8 @@ plugins { id 'com.github.johnrengelman.shadow' version '7.1.2' id 'application' id 'jacoco' + + id 'org.openjfx.javafxplugin' version '0.0.13' } mainClassName = 'seedu.address.Main' @@ -20,6 +22,11 @@ checkstyle { toolVersion = '10.2' } +javafx { + version = '17.0.6' + modules = ['javafx.controls', 'javafx.fxml'] +} + test { useJUnitPlatform() finalizedBy jacocoTestReport @@ -47,15 +54,19 @@ dependencies { implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'win' implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'mac' implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'mac-aarch64' implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'win' implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'mac' implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'mac-aarch64' implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'win' implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'mac' implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'mac-aarch64' implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'win' implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'mac' implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'mac-aarch64' implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.7.0' implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.7.4' @@ -66,7 +77,11 @@ dependencies { } shadowJar { - archiveFileName = 'addressbook.jar' + archiveFileName = 'TAbbyDabby.jar' +} + +run { + enableAssertions = true } defaultTasks 'clean', 'test' diff --git a/docs/AboutUs.md b/docs/AboutUs.md index ff3f04abd02..615fc5b04db 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -9,51 +9,47 @@ You can reach us at the email `seer[at]comp.nus.edu.sg` ## Project team -### John Doe +### Dexter Kwan - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/dexterkwxn)] -* Role: Project Advisor +* Role: Executive sub-GenAI Code Monkey -### Jane Doe +### Thaddeus Lim - + -[[github](http://github.com/johndoe)] +[[github](http://github.com/lyhthaddeus)] [[portfolio](team/johndoe.md)] -* Role: Team Lead -* Responsibilities: UI +* Role: Underpaid Intern +* Responsibilities: Brings Coffee -### Johnny Doe +### Ng Wei En - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](http://github.com/weien02)] [[portfolio](team/johndoe.md)] -* Role: Developer -* Responsibilities: Data +* Role: Group Deadweight +* Responsibilities: Just vibing -### Jean Doe +### Liew Zhao Wei - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/zwliew)] -* Role: Developer -* Responsibilities: Dev Ops + Threading +* Role: Jr. Intern -### James Doe +### Avinash Parthiban - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/avinazz3)] +[[portfolio](team/avinash.md)] -* Role: Developer -* Responsibilities: UI +* Role: Developer/100 +* Responsibilities: Messing up git commands diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 743c65a49d2..c144e65cb7a 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -2,63 +2,66 @@ layout: page title: Developer Guide --- -* Table of Contents + +- Table of Contents {:toc} --------------------------------------------------------------------------------------------------------------------- +--- ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +- This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). --------------------------------------------------------------------------------------------------------------------- +--- ## **Setting up, getting started** 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. +
### Architecture -The ***Architecture Diagram*** given above explains the high-level design of the App. +The **_Architecture Diagram_** given above explains the high-level design of the 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. -* 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. + +- 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. The bulk of the app's work is done by the following four components: -* [**`UI`**](#ui-component): The UI of the App. -* [**`Logic`**](#logic-component): The command executor. -* [**`Model`**](#model-component): Holds the data of the App in memory. -* [**`Storage`**](#storage-component): Reads data from, and writes data to, the hard disk. +- [**`UI`**](#ui-component): The UI of the App. +- [**`Logic`**](#logic-component): The command executor. +- [**`Model`**](#model-component): Holds the data of the App in memory. +- [**`Storage`**](#storage-component): Reads data from, and writes data to, the hard disk. [**`Commons`**](#common-classes) represents a collection of classes used by multiple other components. **How the architecture components interact with each other** -The *Sequence Diagram* below shows how the components interact with each other for the scenario where the user issues the command `delete 1`. +The _Sequence Diagram_ below shows how the components interact with each other for the scenario where the user issues the command `delete 1`. Each of the four main components (also shown in the diagram above), -* defines its *API* in an `interface` with the same name as the Component. -* implements its functionality using a concrete `{Component Name}Manager` class (which follows the corresponding API `interface` mentioned in the previous point. +- defines its _API_ in an `interface` with the same name as the Component. +- implements its functionality using a concrete `{Component Name}Manager` class (which follows the corresponding API `interface` mentioned in the previous point. For example, the `Logic` component defines its API in the `Logic.java` interface and implements its functionality using the `LogicManager.java` class which follows the `Logic` interface. Other components interact with a given component through its interface rather than the concrete class (reason: to prevent outside component's being coupled to the implementation of a component), as illustrated in the (partial) class diagram below. @@ -78,10 +81,10 @@ The `UI` component uses the JavaFx UI framework. The layout of these UI parts ar The `UI` component, -* executes user commands using the `Logic` component. -* listens for changes to `Model` data so that the UI can be updated with the modified data. -* keeps a reference to the `Logic` component, because the `UI` relies on the `Logic` to execute commands. -* depends on some classes in the `Model` component, as it displays `Person` object residing in the `Model`. +- executes user commands using the `Logic` component. +- listens for changes to `Model` data so that the UI can be updated with the modified data. +- keeps a reference to the `Logic` component, because the `UI` relies on the `Logic` to execute commands. +- depends on some classes in the `Model` component, as it displays `Person` object residing in the `Model`. ### Logic component @@ -111,21 +114,22 @@ Here are the other classes in `Logic` (omitted from the class diagram above) tha How the parsing works: -* When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as a `Command` object. -* All `XYZCommandParser` classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. + +- When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as a `Command` object. +- All `XYZCommandParser` classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. ### Model component + **API** : [`Model.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/model/Model.java) - The `Model` component, -* stores the address book data i.e., all `Person` objects (which are contained in a `UniquePersonList` object). -* stores the currently 'selected' `Person` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. -* stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects. -* does not depend on any of the other three components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components) +- stores the address book data i.e., all `Person`, `Group` and `GroupMemberDetails` objects (which are contained in a `UniquePersonList` and `UniqueGroupList` object). +- stores the currently 'selected' `Person` or `Group` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. +- stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects. +- does not depend on any of the other three components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components)
:information_source: **Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
@@ -133,6 +137,11 @@ The `Model` component,
+- `Group` class contains an `ArrayListMap` object which acts similarly to a Map +- The `Person` objects are stored in the `Group` object as 'keys' of the `ArrayListMap` within `Group` object +- The values of the `ArrayListMap` would be the corresponding `GroupMemberDetail` object + + ### Storage component @@ -140,30 +149,106 @@ The `Model` component, -The `Storage` component, -* can save both address book data and user preference data in JSON format, and read them back into corresponding objects. -* inherits from both `AddressBookStorage` and `UserPrefStorage`, which means it can be treated as either one (if only the functionality of only one is needed). -* depends on some classes in the `Model` component (because the `Storage` component's job is to save/retrieve objects that belong to the `Model`) +The `Storage` component is responsible for persisting address book data and user preferences to the hard disk. It handles the conversion between in-memory objects and their file representations. + +#### Structure + +The Storage component is organized into two main areas: + +1. **AddressBook Storage**: Manages the reading and writing of address book data + + - `AddressBookStorage` interface defines the operations + - `JsonAddressBookStorage` provides JSON-based implementation + - Uses `JsonSerializableAddressBook` as an intermediate representation + - Contains a hierarchy of JSON adapter classes that mirror the model structure: + - `JsonAdaptedGroup` represents groups in the address book + - `JsonAdaptedGroupMemberDetails` contains details about group membership + - `JsonAdaptedPerson` and `JsonAdaptedAssignment` linked to group member details + - `JsonAdaptedTag` represents tags associated with persons + +2. **UserPrefs Storage**: Manages the reading and writing of user preferences + - `UserPrefsStorage` interface defines the operations + - `JsonUserPrefsStorage` provides JSON-based implementation + +The `Storage` interface extends both `AddressBookStorage` and `UserPrefsStorage`, providing a unified API. `StorageManager` implements this interface and coordinates between both storage types. + +#### Key Operations + +**Reading Data**: + +- Retrieves data via file paths specified in the storage implementations +- Converts JSON data to model objects through the adapter class hierarchy +- Returns `Optional` objects to handle cases where files don't exist +- Throws `DataLoadingException` when data cannot be loaded properly + +**Writing Data**: + +- Accepts model objects and converts them to JSON format using adapter classes +- Writes to specified file paths +- Throws `IOException` when writing fails +- Logs operations for debugging purposes + +#### Design Considerations + +- **Interface Segregation**: Separate interfaces allow for independent implementation and testing of different storage aspects +- **Adapter Pattern**: JSON adapter classes separate model concerns from persistence details +- **Complex Composition**: The JSON classes mirror the complex relationships in the model, including groups, group memberships, persons, assignments, and tags +- **Dependency Injection**: `StorageManager` takes concrete storage implementations as constructor parameters, facilitating testing and flexibility + +This design provides clean separation of concerns, allowing the model to remain focused on business logic while the storage component handles persistence details. The hierarchy of JSON adapter classes supports the address book's ability to manage not just persons and tags, but also groups, group memberships, and assignments. ### Common classes Classes used by multiple components are in the `seedu.address.commons` package. --------------------------------------------------------------------------------------------------------------------- +--- ## **Implementation** This section describes some noteworthy details on how certain features are implemented. +### Adding a person to a group + +The sequence diagram below illustrate the process of adding a Person to a Group by writing the command `add-to-group n/ p g/ g`. + +1. First the command will go through the standard logic sequence. Creating a Unique Command parser to parse input data to create + a `AddPersonToGroupCommand` object +2. The `LogicManger` then execute the command by calling `execute(m) +3. `AddPersonToGroupCommand` will get the `Person` object to be added and the `Group` object to be added to from `Model` +4. Lastly, the `addPersonToGroup` method will be called to add the `Person` object into the `Group` object. +5. A `CommandResult` is returned for `Ui` purpose + +![AddPersonToGroupSequenceDiagram-Logic](images/AddPersonToGroupSequenceDiagram-Logic.png) + +The `Group` object will create a new `GroupMemberDetail` object tied to the newly added `Person` object and stored into the Map as a key-value pair. 6. After `addPersonToGroup` is called, the `Model` will call on the `VersionedAddressBook` to which adds `p:Person` to `g:Group` 7. `Group` will create a new `GroupMemberDetails` object that corresponds to `p:Person` object 8. Both the `p:Person` and the newly created `GroupMemberDetails` objects will be stored in an `ArrayListMap` within the `Group` object + +![AddPersonToGroupSequenceDiagram-Model](images/AddPersonToGroupSequenceDiagram-Model.png) + +### Adding a new assignment + +The sequence diagram below illustrates the process of adding an Assignment to a Group using the command `add-assignment n/a g/g d/d`. + +1. As with all commands, the command will go through the standard logic sequence. A unique command parser is created to parse the input data and construct an `AddAssignmentCommand` object. +2. The `LogicManager` then executes the command by calling `execute(m)` +3. The `AddAssignmentCommand` retrieves the target `Group` object from the `Model` +4. The `Model`'s `addAssignmentToGroup` method is called with the necessary parameters. +5. A `CommandResult` is returned for `Ui` purposes. + +![AddAssignmentSequenceDiagram-Logic](images/AddAssignmentSequenceDiagram-Logic.png) + +Inside the `Group` object, the `Assignment` constructor is called to create a new `Assignment` object using the parsed name and deadline. The new `Assignment` object is then added to the assignments `ArrayList` field of the `Group` object. + +![AddAssignmentSequenceDiagram-Model](images/AddAssignmentSequenceDiagram-Model.png) + ### \[Proposed\] Undo/redo feature #### Proposed Implementation The proposed undo/redo mechanism is facilitated by `VersionedAddressBook`. It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. Additionally, it implements the following operations: -* `VersionedAddressBook#commit()` — Saves the current address book state in its history. -* `VersionedAddressBook#undo()` — Restores the previous address book state from its history. -* `VersionedAddressBook#redo()` — Restores a previously undone address book state from its history. +- `VersionedAddressBook#commit()` — Saves the current address book state in its history. +- `VersionedAddressBook#undo()` — Restores the previous address book state from its history. +- `VersionedAddressBook#redo()` — Restores a previously undone address book state from its history. These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. @@ -228,33 +313,27 @@ The following activity diagram summarizes what happens when a user executes a ne **Aspect: How undo & redo executes:** -* **Alternative 1 (current choice):** Saves the entire address book. - * Pros: Easy to implement. - * Cons: May have performance issues in terms of memory usage. - -* **Alternative 2:** Individual command knows how to undo/redo by - itself. - * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). - * Cons: We must ensure that the implementation of each individual command are correct. - -_{more aspects and alternatives to be added}_ - -### \[Proposed\] Data archiving +- **Alternative 1 (current choice):** Saves the entire address book. -_{Explain here how the data archiving feature will be implemented}_ + - Pros: Easy to implement. + - Cons: May have performance issues in terms of memory usage. +- **Alternative 2:** Individual command knows how to undo/redo by + itself. + - Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). + - Cons: We must ensure that the implementation of each individual command are correct. --------------------------------------------------------------------------------------------------------------------- +--- ## **Documentation, logging, testing, configuration, dev-ops** -* [Documentation guide](Documentation.md) -* [Testing guide](Testing.md) -* [Logging guide](Logging.md) -* [Configuration guide](Configuration.md) -* [DevOps guide](DevOps.md) +- [Documentation guide](Documentation.md) +- [Testing guide](Testing.md) +- [Logging guide](Logging.md) +- [Configuration guide](Configuration.md) +- [DevOps guide](DevOps.md) --------------------------------------------------------------------------------------------------------------------- +--- ## **Appendix: Requirements** @@ -262,73 +341,536 @@ _{Explain here how the data archiving feature will be implemented}_ **Target user profile**: -* has a need to manage a significant number of contacts -* prefer desktop apps over other types -* can type fast -* prefers typing to mouse interactions -* is reasonably comfortable using CLI apps - -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app +- is a Teaching Assistant +- has a need to manage a significant number of students/contacts +- prefer desktop apps over other types +- can type fast +- prefers typing to mouse interactions +- is reasonably comfortable using CLI apps +**Value proposition**: makes administrative work more convenient for a Teaching Assistant ### User stories Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` -| Priority | As a …​ | I want to …​ | So that I can…​ | -| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | -| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App | -| `* * *` | user | add a new person | | -| `* * *` | user | delete a person | remove entries that I no longer need | -| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list | -| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident | -| `*` | user with many persons in the address book | sort persons by name | locate a person easily | +| Priority | As a …​ | I want to …​ | So that I can…​ | +| -------- | ------------------ | ------------------------------------------------------------- | ------------------------------------------------------------------------ | +| `* * *` | Teaching Assistant | Add a new contact with details such as name, contact and role | Quickly access their information when required | +| `* * *` | Teaching Assistant | Create and manage groups | Organize my contact based on my different responsibilities | +| `* * *` | Teaching Assistant | See the list of contact in my class list | Easily access the information when required | +| `* * *` | Teaching Assistant | Delete a contact | Remove students who have dropped / transferred my class | +| `* * *` | Teaching Assistant | Update a contact's detail | Update my information of my contact to the latest up-to-date information | +| `* *` | Teaching Assistant | Search for a contact by an identifier like name or role | I can quickly find a contact I need | +| `* *` | Teaching Assistant | Fileter contacts by role such as student/ TA or Professor | Organize my address book more efficiently | +| `* *` | Teaching Assistant | Mark certain contact with 'priority' | Quickly access te most important contacts | +| `* *` | Teaching Assistant | Add notes to a contact | Easily remember important details about the contact | +| `* *` | Teaching Assistant | Mark the attendance count of students | Keep track of attendence for grading | +| `*` | Teaching Assistant | Import contact list from a CSV file | Avoid manually inputting each contact | +| `*` | Teaching Assistant | Log interaction with students | Track who I have assisted and follow up if needed | +| `*` | Teaching Assistant | See an image of my student | Remember their faces and names easier | +| `*` | Teaching Assistant | Export my contact to a CSV file | Back up or share my contact list | +| `*` | Teaching Assistant | Track my student's homework submission status | Mark their work for grading | +| `*` | Teaching Assistant | Set reminders for specific contacts | Don't forget to follow up on important tasks | +| `*` | Teaching Assistant | Track whether I marked my student's homework | Ensrure I do not miss out any work during marking | +| `*` | Teaching Assistant | Track my students' grades | Assess my own teacing ability | +| `*` | Teaching Assistant | See the dates of upcoming tutorials and exams | Make the necessary preparations before hand | +| `*` | Teaching Assistant | See deadline for submission and other important dates | Remind my students to prepare for them | +| `*` | Teaching Assistant | Track the progress for preparations of my teaching material | Ensure it is completed before tutorials | +| `*` | Teaching Assistant | Plot a whisker plot of the current grade of my students | Track their learning progress and identify stuggling students | + +## **Appendix: Use cases** -*{More to be added}* +(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise) -### Use cases +### UC01 - Delete a person -(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise) +**MSS** + +1. User requests to list persons +2. AddressBook displays persons +3. User requests to delete a specific person +4. AddressBook deletes the person + + Use case ends. + +**Extensions** -**Use case: Delete a person** +- 2a. The list is empty. + + Use case ends. + +- 3a. The given index is invalid. + + - 3a1. AddressBook indicates error. + + Use case resumes at step 2. + +### UC02 - Adding a person **MSS** -1. User requests to list persons -2. AddressBook shows a list of persons -3. User requests to delete a specific person in the list -4. AddressBook deletes the person +1. User provides contact details +2. AddressBook adds the contact +3. AddressBook indicates success - Use case ends. + Use case ends. **Extensions** -* 2a. The list is empty. +- 1a. The provided details are invalid. + + - 1a1. AddressBook indicates error. + - 1a2. User provides valid details. + + Use case resumes from step 2. + +### UC03 - Editing a Contact + +**MSS** + +1. User requests to list persons +2. AddressBook displays persons +3. User requests to edit a specific person with new details +4. AddressBook updates the contact information +5. AddressBook indicates success + + Use case ends. + +**Extensions** + +- 2a. The list is empty. Use case ends. -* 3a. The given index is invalid. +- 3a. The given index is invalid. + + - 3a1. AddressBook indicates error. + + Use case resumes at step 2. + +- 3b. The provided details are invalid. + + - 3b1. AddressBook indicates error. + - 3b2. User provides valid details. + + Use case resumes from step 4. + +### UC04 - Find a contact by name + +**Preconditions:** + +- The address book contains at least one person. + +**MSS:** + +1. User requests to find persons by keyword(s) +2. AddressBook displays persons whose names contain the keyword(s) + + Use case ends. + +**Extensions:** + +- 2a. No person matches the keyword(s). + + - 2a1. AddressBook indicates no matches found. + + Use case ends. + +- 1a. User provides invalid search parameters. + + - 1a1. AddressBook indicates error. + + Use case ends. + +### UC05 - List all contacts + +**MSS:** + +1. User requests to list all persons +2. AddressBook displays all persons + + Use case ends. + +**Extensions:** + +- 2a. The address book is empty. + + - 2a1. AddressBook indicates no contacts exist. + + Use case ends. + +### UC06 - Mark attendance for a student + +**Preconditions:** + +- The student exists +- The group exists +- The student is a member of the specified group + +**MSS:** + +1. User requests to mark attendance with student name, group name, and week number +2. AddressBook updates the attendance record +3. AddressBook indicates success + + Use case ends. + +**Extensions:** + +- 1a. Week number is invalid. + + - 1a1. AddressBook indicates error. + + Use case ends. + +- 1b. Group does not exist. + + - 1b1. AddressBook indicates error. + + Use case ends. + +- 1c. Student does not exist. + + - 1c1. AddressBook indicates error. + + Use case ends. + +- 1d. Student is not a member of the specified group. + + - 1d1. AddressBook indicates error. + + Use case ends. + +### UC07 - Unmark attendance for a student + +**Preconditions:** + +- The student exists +- The group exists +- The student is a member of the specified group + +**MSS:** + +1. User requests to unmark attendance with student name, group name, and week number +2. AddressBook updates the attendance record +3. AddressBook indicates success + + Use case ends. + +**Extensions:** + +- 1a. Week number is invalid. + + - 1a1. AddressBook indicates error. + + Use case ends. + +- 1b. Group does not exist. + + - 1b1. AddressBook indicates error. + + Use case ends. + +- 1c. Student does not exist. + + - 1c1. AddressBook indicates error. + + Use case ends. + +- 1d. Student is not a member of the specified group. + + - 1d1. AddressBook indicates error. + + Use case ends. + +### UC08 - Show a student's attendance in a group + +**Preconditions:** + +- The student exists +- The group exists +- The student is a member of the group + +**MSS:** + +1. User requests attendance records for a specific student in a specific group +2. AddressBook displays the attendance records + + Use case ends. + +**Extensions:** + +- 1a. Group does not exist. + + - 1a1. AddressBook indicates error. + + Use case ends. + +- 1b. Student does not exist. + + - 1b1. AddressBook indicates error. + + Use case ends. + +- 1c. Student is not a member of the specified group. + + - 1c1. AddressBook indicates error. + + Use case ends. - * 3a1. AddressBook shows an error message. +### UC09 - Add a group - Use case resumes at step 2. +**Preconditions:** -*{More to be added}* +- The group name is unique + +**MSS:** + +1. User requests to create a new group with a specified name +2. AddressBook creates the group +3. AddressBook indicates success + + Use case ends. + +**Extensions:** + +- 1a. A group with the same name already exists. + + - 1a1. AddressBook indicates error. + + Use case ends. + +### UC10 - Delete a group + +**MSS:** + +1. User requests to list groups +2. AddressBook displays groups +3. User requests to delete a specific group +4. AddressBook deletes the group +5. AddressBook indicates success + + Use case ends. + +**Extensions:** + +- 2a. The list is empty. + + Use case ends. + +- 3a. The given index is invalid. + + - 3a1. AddressBook indicates error. + + Use case resumes at step 2. + +### UC11 - Edit a group + +**MSS:** + +1. User requests to list groups +2. AddressBook displays groups +3. User requests to edit a specific group with a new name +4. AddressBook updates the group name +5. AddressBook indicates success + + Use case ends. + +**Extensions:** + +- 2a. The list is empty. + + Use case ends. + +- 3a. The given index is invalid. + + - 3a1. AddressBook indicates error. + + Use case resumes at step 2. + +- 3b. The new group name already exists. + + - 3b1. AddressBook indicates error. + + Use case resumes at step 3. + +### UC12 - Show group details + +**MSS:** + +1. User requests to list groups +2. AddressBook displays groups +3. User requests details of a specific group +4. AddressBook displays the group's name and member list + + Use case ends. + +**Extensions:** + +- 2a. The list is empty. + + Use case ends. + +- 3a. The given index is invalid. + + - 3a1. AddressBook indicates error. + + Use case resumes at step 2. + +### UC13 - Find a group by name + +**Preconditions:** + +- At least one group exists in the address book. + +**MSS:** + +1. User requests to find groups by keyword(s) +2. AddressBook displays groups whose names contain the keyword(s) + + Use case ends. + +**Extensions:** + +- 2a. No group matches the keyword(s). + + - 2a1. AddressBook indicates no matches found. + + Use case ends. + +- 1a. User provides invalid search parameters. + + - 1a1. AddressBook indicates error. + + Use case ends. + +### UC14 - List all groups + +**MSS:** + +1. User requests to list all groups +2. AddressBook displays all groups + + Use case ends. + +**Extensions:** + +- 2a. No groups exist. + + - 2a1. AddressBook indicates no groups exist. + + Use case ends. + +### UC15 - Add a person to a group + +**Preconditions:** + +- The person exists +- The group exists +- The person is not already a member of the group + +**MSS:** + +1. User requests to add a person to a group +2. AddressBook adds the person to the group +3. AddressBook indicates success + + Use case ends. + +**Extensions:** + +- 1a. Person does not exist. + + - 1a1. AddressBook indicates error. + + Use case ends. + +- 1b. Group does not exist. + + - 1b1. AddressBook indicates error. + + Use case ends. + +- 1c. Person is already in the group. + + - 1c1. AddressBook indicates error. + + Use case ends. + +### UC16 - Remove a person from a group + +**Preconditions:** + +- The person exists +- The group exists +- The person is a member of the group + +**MSS:** + +1. User requests to remove a person from a group +2. AddressBook removes the person from the group +3. AddressBook indicates success + + Use case ends. + +**Extensions:** + +- 1a. Person does not exist. + + - 1a1. AddressBook indicates error. + + Use case ends. + +- 1b. Group does not exist. + + - 1b1. AddressBook indicates error. + + Use case ends. + +- 1c. Person is not a member of the group. + + - 1c1. AddressBook indicates error. + + Use case ends. + +### UC17 - View help information + +**MSS:** + +1. User requests help information +2. AddressBook displays help information + + Use case ends. ### Non-Functional Requirements 1. Should work on any _mainstream OS_ as long as it has Java `17` or above installed. 2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. 3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. +4. Should not require an internet connection for all functionalities. +5. Should work well in multitasking scenarios, allowing TAs to switch between the app and other teaching tools (e.g., PDF readers, Excel) without performance degradation. +6. Should be lightweight, running smoothly on the same laptops TAs use for general university tasks, without any additional hardware. +7. Data should be backed up into a user-editable file format, making it easy to share across devices or with other TAs. +8. Should be easily upgradable, allowing new versions or features to be added without disrupting existing user data or workflows. -*{More to be added}* +_{More to be added}_ ### Glossary -* **Mainstream OS**: Windows, Linux, Unix, MacOS -* **Private contact detail**: A contact detail that is not meant to be shared with others +- **Assignment**: A deliverable that students in a tutorial group have to submit; typically comes with a grade +- **Attendance**: A record of whether a student attended the tutorial session of a tutorial group for a week. +- **Contact**: An entry with details of a person of interest, i.e., a student, a teacher, or a teaching assistant +- **Exam**: A formal test of a student's understanding of the content in a course; typically comes with a grade +- **Group**: A tutorial group that can contain multiple students, TAs, and lecturers. +- **Head TA**: The teaching assistant(s) in charge of all other teaching assistants for a particular course +- **Mainstream OS**: Windows, Linux, Unix, MacOS +- **Person**: Any one whose contact details you would want to store in the address book. They are typically students, teaching assistants, or lecturers. +- **Private contact detail**: A contact detail that is not meant to be shared with others +- **Teaching assistant (TA)**: A person who assists in teaching a class; they are typically also a full-time student +- **Tutorial**: Lessons that students attend which complement the content taught in lectures; typically taught by TAs --------------------------------------------------------------------------------------------------------------------- +--- ## **Appendix: Instructions for manual testing** @@ -343,40 +885,160 @@ testers are expected to do more *exploratory* testing. 1. Initial launch - 1. Download the jar file and copy into an empty folder + - Download the jar file and copy into an empty folder. + + - Double-click the jar file. + + - Expected result: Shows the GUI with a set of sample contacts. - 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. +2. Saving window preferences -1. Saving window preferences + - Resize the window to an optimum size. Move the window to a different location. Close the window. - 1. Resize the window to an optimum size. Move the window to a different location. Close the window. + - Re-launch the app by double-clicking the jar file.
+ + - Expected: The most recent window size and location is retained. + +### Adding a person - 1. Re-launch the app by double-clicking the jar file.
- Expected: The most recent window size and location is retained. +1. Adding person with valid fields -1. _{ more test cases …​ }_ + - `add n/John Doe p/12345678 e/john@email.com a/123 Street` + + - `add n/John S/O Jonathan p/2345 e/john2@email.com a/1234 Street` + + - `add n/Doe O'Neil p/999 e/doe3@email.com a/Deer Street` + + - `add n/Mary-Jane Williams p/9876543210 e/mjwilliams@email.com a/Spidey Street` + + - Expected result: The persons with the specified details are added. + +2. Adding person with invalid fields + + - `add n/Shawn p/12 e/john@email.com a/12 Street` (invalid phone number) + + - `add n/John Athan p/2345 e/john2.com a/34 Street` (invalid email) + + - `add n/Sean*Neil p/999 e/doe3@email.com a/Deer Street` (unsupported characters in name) + + - Expected result: The app displays error messages for the incorrect fields; the persons are not added. ### Deleting a person 1. Deleting a person while all persons are being shown - 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. + - Prerequisites: List all persons using the `list` command. Group has at least 1 person. + + - Test case: Delete first person on the list `delete 1`
+ + - Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. + + - The deleted person should also be removed from all groups they were previously part of. This can be verified using `list-group` to see all the groups. + + - Test case: Delete with invalid index `delete 0`
+ + - Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. + + +### Adding a Group + +1. Adding group with valid fields + + - `add-group n/G12` + + - `add-group n/G13 t/CS2103` + - Expected result: The two groups are added and can be seen when running the `list-group` command. + +### Manging Persons in Group + +1. Adding a Person to a Group + + - Prerequisites: Group G12 exists (see [Adding a Group](#adding-a-group)) + + - Test case: Add new person to existing group + + - Add new person (from [Adding a Person](#adding-a-person)) `add n/Johnny p/91234 e/joony@gmail.com a/3 Kent Ridge Rd` + + - Add the person to the group `add-to-group n/Johnny g/G12` + + - Expected result: Person Johnny should show up inside group G12 when using either `list-group` or `show-group-details` commands + +2. Adding a Person who already exist in the Group + + - Prerequisite: Johnny is already in Group G12 (see point 1) + + - Test case: Add existing person in group to existing group + + - Add person to Group `add-to-group n/Johnny g/G12` + + - Expected: Error message indicating Johnny is already in Group G12. + +3. Deleting a Person from a Group + + - Prerequisite: Johnny is already in Group G12 (see point 1) + + - Test case: Remove Johnny from Group G12 + + - `delete-from-group n/Johnny g/G12` + + - Expected: Johnny should no longer show up inside Group G12 when using either `list-group` or `find-group` commands + +4. Deleting a Person who is not a member of Group + + - Prerequisite: Person `p` is not a member of Group G12 + + - Test case: `delete-from-group n/p g/G12` + + - Expected: Error message indicating `p` does not exist in G12. + +## **Appendix: Effort** + +### Group and GroupMemberDetail + +The project introduced a new object: `Group`, alongside a relational object `GroupMemberDetail` that describes students' details inside a particular group. This requires us to ensure that data is synchronized through the user workflow by making sure that our Maps and Lists point to the same object by reference. In particular, since `Person` is hashed by name, we had to take caution when allowing editing of name. To deal with this, we introduced 2 new data structures: `ArrayListSet` and `ArrayListMap`. These classes have the interface of `Set` and `Map` respectively, but are internally stored in `ArrayList` objects so that editing the key objects does not change our key values. This is important since we use `Person` as a key for our Map of `GroupMemberDetail` and that `hashCode()` hashes `Person` based off its name. + +The introduction of a relation object `GroupMemberDetail` also made it important for us to agree on a source of truth for synchronizing data. We decided to keep the relation data inside the `Group` data, while keeping a name of the person referenced, whose reference will be looked up during the data loading stage. This design relies on the assumption made by AB3, which was that 2 persons are equal if they have the same name (mentioned under the implementation of `Person::isSamePerson()`). Hence, `Person` data is stored as one list, and `Group` data is stored as another list (which contains `GroupMemberDetail` for each group member). Data is loaded by first reading and creating `Person` objects, followed by `Groups`. Each `GroupMemberDetail` will then associate the referenced name with a `Person` object that should exist. This allows us to ensure that the references are created and referenced properly. + +### Reuse and Adaptation + +- While the base `Person` and command structure were derived from AB3, at least **60–70%** of our implementation around Groups was written from scratch. +- Reuse was mainly in utility classes (e.g., `ToStringBuilder`, `AppUtil`) and storage design patterns. +- Our work in adapting the storage layer was influenced by the original `JsonSerializableAddressBook` class but extended heavily to handle nested structures. + +### Summary + +Implementing `Group` and `GroupMemberDetail` added substantial architectural and technical complexity, transforming the app from a simple JSON list data structure to one that required careful synchronization and development of new data structures to support our needs, requiring a significant portion of our effort. + +## **Appendix: Planned Enhancements** + +Team size: 5 + +### Submission and grading of assignments +Currently, the app allows TAs to add, delete and edit assignments, but there is no support for submission and grading of assignments. We plan to allow TAs to add and grade student submissions for assignments. TAs will also be able to add submission deadlines and set penalties for late submissions. Ultimately, the goal is for TAs to be able to create assignment deadlines, record down their students' time of submission, and grade students' work while also penalizing late submissions. - 1. Test case: `delete 1`
- Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. +### Display of assignment data +Currently, assignments cannot be viewed as there is not much to be done with them without a grading feature. We plan to make them displayable together with the implementation of grading of assignments in the future so that the information is meaningful. This will be done via some sort of `list-assignments` command that will show what assignments are in the group, and for each student if they have submitted or not. - 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. +### Nicer UI and output of command feedback +Currently, shows JSON output which is informative and clear for a technically inclined person, but does not look as nice on the UI. Future plan is to enhance the output format to be more visually appealing alongside a UI visual upgrade to display both person and group lists side-by-side for better information tracking. We also intend to make the feedback window dynamically sized so that error messages fit within the box and users will not need to scroll through, thus reducing friction when inputting wrong commands. - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
- Expected: Similar to previous. +### Support for TA and Lecturer roles +Currently, all persons in the application are assumed to be students which is most important to track for a TA. We plan to add support for TA and Professor roles which would facilitate the feature on grading and submission of assignments. -1. _{ more test cases …​ }_ +### Listing students in sorted name order +Currently, students in groups are indexed arbitrarily. For a future enhancement, we intend to list them in sorted name order for easier lookup. -### Saving data +### Marking the attendance of multiple students for multiple weeks at once +Currently, `mark-attendance` only allows the attendance of a single student and for a single week to be marked. This can be time-consuming when marking attendance for larger classes. To make the experience more efficient, we plan to allow TAs to mark multiple students' attendance for multiple weeks at once. The command would look like `mark-all-attendance g/GROUP w/WEEK_1 [w/WEEK_2]... n/PERSON_NAME_1 [n/PERSON_NAME_2]...`. -1. Dealing with missing/corrupted data files +### Display groups that students are in +When listing students currently, the list does not show which groups they are in. We plan to add this information in for this command so that tutors can see which group each student belongs to without going through all the groups he is in charge of. - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ +### Precise validation for phone numbers +Currently, the app allows any string of digits with between 3 and 15 digits to be entered as a phone number. +We plan to add more precise validation to ensure that the phone number is in a valid format and length. +This will help prevent errors when trying to contact students. -1. _{ more test cases …​ }_ +### Support for more names +Currently, not all characters are supported in names, like Cyrillic characters and some combinations of slashes (i.e., `p/` and `g/`). +We plan to extend support for more names in the future. diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 27c2d1cf16c..d7182fea23e 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -3,198 +3,629 @@ 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. +TAbby Dabby is a **desktop app to help teaching assistants (TAs) with their administrative duties**. +It is optimised for TAs who are comfortable using a command line interface (CLI) but still want a graphical user interface (GUI). +If you are a TA who can type fast, TAbby Dabby can get your administrative tasks done faster than traditional GUI apps. -* Table of Contents +- Table of Contents {:toc} --------------------------------------------------------------------------------------------------------------------- +--- + +## Introduction + +TAbby Dabby helps TAs with four main tasks: +- Managing the contact details of students, co-TAs, and lecturers. TAbby Dabby calls these people **Persons**. +- Managing the tutorial groups that you are a part of. TAbby Dabby calls these tutorial groups **Groups**. +- Tracking the attendance of students in your tutorial groups. TAbby Dabby calls this **Attendance**. +- Tracking the assignments of your tutorial groups. TAbby Dabby calls these **Assignments**. + +This User Guide uses the terms **Persons**, **Groups**, **Attendance**, and **Assignment** frequently to refer to these entities. + +
+ +**:information_source: Relationship between each entity**
+ +- Each **Group** can have multiple **Persons** (members) in it, and each **Person** can be a member of multiple **Groups**. +- Each **Group** can have multiple **Assignments** in it, but each **Assignment** can only belong to one **Group**. +- Each **Person** has an **Attendance** record for each **Group** they belong to, and each **Group** has an **Attendance** record for each **Person** in it. + +
+ +There are two separate lists in this app: the person list and the group list. +The person list contains all the persons, while the group list contains all the groups. ## Quick start -1. Ensure you have Java `17` or above installed in your Computer.
- **Mac users:** Ensure you have the precise JDK version prescribed [here](https://se-education.org/guides/tutorials/javaInstallationMac.html). +1. Install the Java Development Kit version `17`.
+ **Windows users:** Install JDK 17 installed from [here](https://www.oracle.com/java/technologies/downloads/#java17)
+ **Mac users:** Ensure you have the precise JDK version specified [here](https://se-education.org/guides/tutorials/javaInstallationMac.html). -1. Download the latest `.jar` file from [here](https://github.com/se-edu/addressbook-level3/releases). +2. Ensure that your installation works.
+ * Open a command terminal (Windows: search for _cmd_ , Mac: use Spotlight to find _Terminal_ ) + * Verify installation by typing `java -version` in your terminal; it should successfully output JDK version 17. -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +3. Download the latest `TAbbyDabby.jar` file from [here](https://github.com/AY2425S2-CS2103T-T12-1/tp/releases). -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. Copy the file to the folder you want to use as the _home folder_ for your TAbby Dabby. + +5. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar TAbbyDabby.jar` command to run the application.
+ The following GUI should appear in a few seconds. Note how the app contains some sample data.
![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.
+6. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
Some example commands you can try: - * `list` : Lists all contacts. + - `add n/Jensen Huang p/98765432 e/jensenh@nvidia.com a/21 Lower Kent Ridge Rd, Singapore 119077` : Adds a person named `Jensen Huang` to the list of persons. - * `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 person shown in the current person list. - * `delete 3` : Deletes the 3rd contact shown in the current list. + - `list` : Lists all persons. - * `clear` : Deletes all contacts. + - `clear` : Deletes all persons. - * `exit` : Exits the app. + - `add-group n/CS2103T T12` : Adds a new group named `CS2103T T12` to the list of groups. -1. Refer to the [Features](#features) below for details of each command. + - `exit` : Exits the app. --------------------------------------------------------------------------------------------------------------------- +7. Refer to the next section for more details on how to use the app. -## Features +--- -
+## General notes about using the app -**:information_source: Notes about the command format:**
+### Command formats -* 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`. +
+ +**:information_source: Notes about command format**
-* 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`. +- Fields in square brackets are optional; every other field is compulsory.
+ e.g You can fill in `n/NAME [t/TAG]` as `n/Jensen Huang t/friend` or as `n/Jensen Huang`, but not as `t/friend`. -* Items with `…`​ after them can be used multiple times including zero times.
+- Fields with `…`​ after them can be used multiple times including zero times.
e.g. `[t/TAG]…​` can be used as ` ` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. -* Parameters can be in any order.
+- You can fill in fields in any order.
e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. -* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
+- Redundant parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
e.g. if the command specifies `help 123`, it will be interpreted as `help`. -* If you are using a PDF version of this document, be careful when copying and pasting commands that span multiple lines as space characters surrounding line-breaks may be omitted when copied over to the application. +- If you type a command with a wrong format, TAbby Dabby will display an example to guide you to re-input the command correctly. + +- If you are using a PDF version of this document, be careful when copying and pasting commands that span multiple lines as space characters surrounding line-breaks may be omitted when copied over to the application.
-### Viewing help : `help` +### Common fields -Shows a message explaning how to access the help page. +Many of our commands also share similar fields. +Here are some notes on them: -![help message](images/helpMessage.png) +
+ +**:information_source: Notes about common fields**
+ +- NAME-related and TAG fields are case-sensitive, and must contain only alphanumeric characters, spaces, apostrophes, slashes, and dashes. This includes the `n/`, `g/`, and `t/` fields.
+ e.g. `Jensen-Huang s/o Michael O'Neil` and `jensen-huang s/o michael o'neil` are valid and distinct names and tags. + +- NAME-related fields must be unique within the same category.
+ e.g. If you add a person with the name `Jensen Huang`, you cannot add another person with the same name. Same goes for groups. + +- Duplicate TAGs are merged.
+ e.g. If you add a person with the fields `t/friend t/friend`, the person will only have one tag `friend`. + +- INDEX fields refer to the index number of the person (resp. group) in the person (resp. group) list that was last displayed. It **must be a positive integer** 1, 2, 3, …, up till the number of persons (resp. groups) in the list. + +
+ +## Commands + +### General commands + +#### Viewing help: `help` + +Shows a message explaining how to access the help page. + +**Expected output** Format: `help` +**Expected output** + +The GUI displays a pop-up window with a link to this User Guide. + +Example result of `help`: + +![help message](images/helpMessage.png) + +#### Exiting the program: `exit` + +Exits the program. + +Format: `exit` + +**Expected output** -### Adding a person: `add` +The GUI closes and the app terminates. -Adds a person to the address book. +### Person commands + +#### Adding a person: `add` + +Adds a person to the person list. +Useful for adding details of your students. Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` -
:bulb: **Tip:** -A person can have any number of tags (including 0) -
+**Notes** -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` +- Phone numbers must have between 3 and 15 digits; they need not be unique. -### Listing all persons : `list` +**Examples** -Shows a list of all persons in the address book. +- `add n/Jensen Huang p/98765432 e/jensenh@nvidia.com a/21 Lower Kent Ridge Rd, Singapore 119077 t/friend` +- `add n/Jeff Bezos p/12345678 t/friend e/jeffb@amazon.com a/21 Lower Kent Ridge Rd, Singapore 119077` -Format: `list` +**Expected output** + +The GUI displays the person list with the new person added to the end of it. + +Example result of `add n/Jensen Huang p/98765432 e/jensenh@nvidia.com a/21 Lower Kent Ridge Rd, Singapore 119077 t/friends`: + +![Add command](images/AddCommandResult.png) + +**Known Limitations** + +Common names that contain slashes like S/O and D/O are supported, but not all slashes are supported, i.e., `p/ ``a/` `t/` `e/`, as they are not typically used in names. We plan to extend support for rarer names in the future. + +#### Deleting a person: `delete` + +Deletes the specified person from the person list. +Useful for removing the details of someone who is no longer a student. + +Format: `delete INDEX` -### Editing a person : `edit` +**Examples** -Edits an existing person in the address book. +- `list` followed by `delete 2` deletes the second person in the person list. +- `find Jensen` followed by `delete 1` deletes the first person in the results of the `find` command. Find out more about the `find` command [here](#finding-persons-by-name-find). + +**Expected output** + +The GUI displays the person list command, but with the specified person removed from it. +The persons that came after the deleted person will shift up to fill the gap and their indices will be updated accordingly. +See [add](#adding-a-person-add) for a similar example of the expected output. + +Before running `delete 1`: + +![Before delete](images/DeleteCommandResultBefore.png) + +After running `delete 1`: + +![After delete](images/DeleteCommandResultAfter.png) + +#### Editing a person: `edit` + +Edits the details of the specified person in the person list. Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` -* Edits the person at the specified `INDEX`. The index refers to the index number shown in the displayed person list. The index **must be a positive integer** 1, 2, 3, …​ -* 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. +**Notes** -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. +- You must fill in at least one of the optional fields. +- Existing values will be updated to the input values. +- When editing tags, the existing tags of the person will be removed, e.g., if the person at index `2` currently has the tag `frenemy`, and we run the command `edit 2 t/enemy`, the tag `frenemy` will be removed, and a new tag `enemy` will be added. +- You can remove all the person’s tags by typing `t/` without specifying any tags after it. -### Locating persons by name: `find` +**Examples** -Finds persons whose names contain any of the given keywords. +- `edit 1 p/91234567 e/jensenh@yahoo.com` Edits the phone number and email address of the 1st person to be `91234567` and `jensenh@yahoo.com` respectively. +- `edit 2 n/Jeff Bezos t/` Edits the name of the 2nd person to be `Jeff Bezos` and clears all existing tags. +- `edit 2 n/Jeff Bezos t/friend` Edits the name of the 2nd person to be `Jeff Bezos`, clears all existing tags, and adds the tag `friend`. -Format: `find KEYWORD [MORE_KEYWORDS]` +**Expected output** -* 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` +Example result of `edit 1 p/91234567 e/jensenh@yahoo.com t/buddy`: -Examples: -* `find John` returns `john` and `John Doe` -* `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) +![Result for `edit 1 p/91234567 e/jensenh@yahoo.com`](images/EditCommandResult.png) -### Deleting a person : `delete` +#### Listing all persons: `list` -Deletes the specified person from the address book. +Shows a list of all persons in the person list. -Format: `delete INDEX` +Format: `list` + +**Expected output** + +The GUI displays a list of all persons in the person list. + +#### Finding persons by name: `find` + +Finds persons whose names contain any of the specified keywords. -* 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, …​ +Format: `find KEYWORD [MORE_KEYWORDS]...` -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. +**Notes** -### Clearing all entries : `clear` +- The search is case-insensitive, e.g., `jensen` will match `Jensen`. +- The order of the keywords does not matter. e.g. `Huang Jensen` will match `Jensen Huang`. +- Only the name is searched. +- Only full words will be matched, e.g., `Jen` will not match `Jens`. +- Persons matching at least one keyword will be returned (i.e. `OR` search), e.g., `Jensen Bezos` will return `Jensen Huang` and `Jeff Bezos`. -Clears all entries from the address book. +**Examples** + +- `find huang jensen` may output `huang jensen`, `jensen huang`, and `Jensen Huang`. +- `find huang bezos` may output `Jensen Huang` and `Jeff Bezos`. + +**Expected output** + +The GUI displays a list of persons whose names contain any of the specified keywords. + +Example result of executing the `find huang bezos` command: + +![Result for 'find huang bezos'](images/findHuangBezosResult.png) + +#### Deleting all persons: `clear` + +Deletes all persons from the person list. Format: `clear` -### Exiting the program : `exit` +**Expected output** -Exits the program. +The GUI displays an empty person list. -Format: `exit` +### Group commands + +#### Adding a new group: `add-group` + +Adds a new group to the group list. +Useful for adding new tutorial groups. + +Format: `add-group n/GROUP_NAME [t/TAG]...` + +**Examples** + +- `add-group n/CS2103T T12 t/CS` creates a group with name `CS2103T T12` and the tag `CS`. + +**Expected output** + +The GUI displays the group list with the new group added to the end of it. + +Example result of `add-group n/CS2103T T12 t/CS`: + +![Result of add-group](images/AddGroupCommandResult.png) + +#### Deleting a group: `delete-group` + +Deletes the specified group from the group list. +Useful for removing a tutorial group that is no longer needed. + +Format: `delete-group INDEX` + +**Examples** + +- `delete-group 2` deletes the group with index `2` in the last shown group list. + +**Expected output** + +The GUI displays the group list, but with the specified group removed from it. +See [add-group](#adding-a-new-group-add-group) for a similar example of the expected output. + +#### Editing a group: `edit-group` + +Edits the specified group details in the group list. + +Format: `edit-group INDEX [n/GROUP_NAME] [t/TAG]…​` + +**Notes** + +- 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 group will be removed, e.g., if the group at index `2` currently has the tag `gaming`, and we run the command `edit-group 2 t/study`, the tag `gaming` will be removed, and a new tag `study` will be added. +- You can remove all the group’s tags by typing `t/` without specifying any tags after it. + +**Examples** + +- `edit-group 1 n/CS2103T T12` Edits the name of the first group to be `CS2103T T12`. +- `edit-group 2 n/CS2103T T12 t/` Edits the name of the second group to be `CS2103T T12` and clears all existing tags. +- `edit-group 2 n/CS2103T T12 t/study t/friends` Edits the name of the second group to be `CS2103T T12`, clears all existing tags, and adds the tags `study` and `friends`. + +**Expected output** + +The GUI displays the specified group's updated details. +See [show-group-details](#showing-group-details-show-group-details) for a similar example of the expected output. + +Note: this differs from the output of the analogous `edit` command for persons, which displays the person list instead. + +Example result of `edit-group 1 n/CS2103T T12 t/study t/friends`: + +![Result of edit-group](images/EditGroupCommandResult.png) + +#### Listing all groups: `list-group` + +Shows a list of all groups in the group list along with their information, e.g., indices and names. + +Format: `list-group` + +**Expected output** + +The GUI displays a list of all groups in the group list. + +Example result of `list-group`: + +![Result of list](images/ListGroupCommandResult.png) + +#### Finding a group by name: `find-group` + +Finds groups whose names contain any of the given keywords. + +Format: `find-group KEYWORD [MORE_KEYWORDS]` + +**Notes** + +- The search is case-insensitive, e.g., `cs2103t t12` will match `CS2103T T12`. +- The order of the keywords does not matter. e.g. `T12 CS2103T` will match `CS2103T T12`. +- Only the name is searched. +- Only full words will be matched, e.g., `CS210` will not match `CS2103T`. +- Groups matching at least one keyword will be returned (i.e. `OR` search), e.g., `CS2103T T13` will return `CS2103T T12` and `CS2101 T13`. + +**Examples** + +- `find-group T12` may output `T12`, `t12`, and `CS2103T T12`. +- `find-group t12 t13` may output `CS2103T T12` and `CS2103T T13`. + +**Expected output** + +The GUI displays a list of groups whose names contain any of the specified keywords. + +Example result of executing the `find-group t12 t13` command: + +![Result of find-group](images/FindGroupCommandResult.png) + +#### Adding a person to a group: `add-to-group` + +Adds the specified person to the specified group. + +Format: `add-to-group n/PERSON_NAME g/GROUP_NAME` + +**Examples** + +- `add-to-group n/Jensen Huang g/CS2103T T12` adds the person named `Jensen Huang` to the group named `CS2103T T12`. + +**Expected output** + +The GUI displays the specified group's updated details with the specified person added to it. + +Example result of `add-to-group n/Jensen Huang g/CS2103T T12`: + +![Result of add-to-group](images/AddToGroupCommandResult.png) + +**Known limitations** + +- The person is assumed to be a student for now. We plan to support the addition of co-TAs and lecturers in the future. + +#### Removing a person from a group: `delete-from-group` + +Removes the specified person from the specified group. + +Format: `delete-from-group n/PERSON_NAME g/GROUP_NAME` + +**Examples** + +- `delete-from-group n/Jensen Huang g/CS2103T T12` removes the person named `Jensen Huang` from the group named `CS2103T T12`. + +**Expected output** + +The GUI displays the specified group's updated details with the specified person removed from it. +See [add-to-group](#adding-a-person-to-a-group-add-to-group) for a similar example of the expected output. + +#### Showing group details: `show-group-details` + +Shows the key details regarding the specified group. + +Format: `show-group-details INDEX` + +**Examples** + +- `show-group-details 2` shows all the details of the group with index `2` in the last shown group list. + +**Expected output** + +The GUI shows the details of the specified group, including: + - Group name and tags + - Number of students, TAs, lecturers, and assignments + - Name, role, and attendance of every group member + +Example result of `show-group-details 1`: + +![Result of show-group-details](images/ShowGroupDetailsCommandResult.png) + +### Attendance commands + +#### Marking the attendance of a person: `mark-attendance` + +Marks the attendance of the specified person in the specified group for the specified week. + +Format: `mark-attendance n/PERSON_NAME g/GROUP_NAME w/WEEK_NUMBER` + +**Notes** + +- `WEEK_NUMBER` must be a positive integer between 1 and 13 (inclusive). + +**Examples** + +- `mark-attendance n/Jensen Huang g/CS2103T T12 w/10` marks the attendance for `Jensen Huang` in `CS2103T T12` for week `10`. + +**Expected output** + +The GUI displays the specified group's updated details with the specified person's attendance marked for the specified week. +See [show-group-details](#showing-group-details-show-group-details) for a similar example of the expected output. + +#### Unmarking the attendance of a person: `unmark-attendance` + +Removes the attendance record of the specified person in the specified group for the specified week. + +Format: `unmark-attendance n/PERSON_NAME g/GROUP_NAME w/WEEK_NUMBER` + +**Notes** + +- `WEEK_NUMBER` must be a positive integer between 1 and 13 (inclusive). + +**Examples** + +- `unmark-attendance n/Jensen Huang g/CS2103T T12 w/10` unmarks the attendance for `Jensen Huang` in `CS2103T T12` for week `10`. + +**Expected output** + +The GUI displays the specified group's updated details with the specified person's attendance unmarked for the specified week. +See [show-group-details](#showing-group-details-show-group-details) for a similar example of the expected output. + +#### Showing the attendance records for a person: `show-attendance` + +Displays the attendance record of the specified person in the specified group. + +Format: `show-attendance n/PERSON_NAME g/GROUP_NAME` + +**Examples** + +- `show-attendance n/Jensen Huang g/CS2103T T12` displays the attendance for `Jensen Huang` in `CS2103T T12`. + +**Expected output** + +The GUI displays the specified person's attendance record for the specified group. + +Example result of `show-attendance n/Jensen Huang g/CS2103T T12`: + +![Result of show-attendance](images/ShowAttendanceCommandResult.png) + +### Assignment commands + +#### Adding an assignment in a group: `add-assignment` + +Adds a new assignment in the specified group. + +Format: `add-assignment n/ASSIGNMENT_NAME g/GROUP_NAME d/DEADLINE` + +**Notes** + +- `ASSIGNMENT_NAME` is the name of the assignment. +- `GROUP_NAME` is the name of the group. +- `DEADLINE` is the deadline of the assignment in the format `DD-MM-YYYY`. + +**Examples** + +- `add-assignment n/HW 1 g/CS2103T T12 d/21-04-2025` adds an assignment named `HW 1` to the group `CS2103T T12` with a deadline of `21-04-2025`. + +**Expected output** + +The GUI displays the specified group's updated details with the new assignment added to it. + +Example result of `add-assignment n/HW 1 g/CS2103T T12 d/21-04-2025`: + +![Result of add-assignment](images/AddAssignmentCommandResult.png) + +#### Deleting an assignment in a group: `delete-assignment` + +Deletes an assignment in the specified group. + +Format: `delete-assignment n/ASSIGNMENT_NAME g/GROUP_NAME` + +**Notes** + +- `ASSIGNMENT_NAME` is the name of the assignment. +- `GROUP_NAME` is the name of the group. + +**Examples** + +- `delete-assignment n/HW 1 g/CS2103T T12` deletes the assignment named `HW 1` in the group `CS2103T T12`. + +**Expected output** + +The GUI displays the specified group's updated details with the specified assignment removed from it. +See [add-assignment](#adding-an-assignment-in-a-group-add-assignment) for a similar example of the expected output. + +#### Editing an assignment in a group: `edit-assignment` + +Edits details of the specified assignment in the specified group. + +Format: `edit-assignment n/ASSIGNMENT_NAME g/GROUP [N/NEW NAME] [d/DEADLINE]` + +**Notes** + +- `ASSIGNMENT_NAME` is the name of the assignment. +- `GROUP_NAME` is the name of the group. +- `NEW NAME` is the new name of the assignment. +- `DEADLINE` is the new deadline of the assignment in the format `DD-MM-YYYY`. + +**Examples** + +- `edit-assignment n/HW 1 g/CS2103T T12 N/Assignment 1 d/21-04-2025` renames the assignment named `HW 1` in the group `CS2103T T12` to `Assignment 1` with a deadline of `21-04-2025`. + +**Expected output** + +The GUI displays the specified group's updated details with the specified assignment updated. +See [add-assignment](#adding-an-assignment-in-a-group-add-assignment) for a similar example of the expected output. + +--- ### 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. +TAbby Dabby's data is saved in your computer's hard disk automatically after any 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. +You can find TAbby Dabby's data as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file.
: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. +If your changes to the data file makes its format invalid, TAbby Dabby will discard all data and start with a new set of preloaded data at the next run. +Hence, it is recommended to take a backup of the file before editing it.
+Furthermore, certain edits can cause the TAbby Dabby to behave in unexpected ways (e.g., if a value entered is outside of the acceptable range). +Therefore, edit the data file only if you are confident that you can update it correctly.
-### Archiving data files `[coming in v2.0]` - -_Details coming soon ..._ - --------------------------------------------------------------------------------------------------------------------- +--- ## FAQ **Q**: How do I transfer my data to another Computer?
-**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous AddressBook home folder. +**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous TAbby Dabby home folder.
+:bulb: **Refresher:** You can find the data file in TAbby Dabby's home folder at /data/addressbook.json. --------------------------------------------------------------------------------------------------------------------- +--- ## Known issues 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. -2. **If you minimize the Help Window** and then run the `help` command (or use the `Help` menu, or the keyboard shortcut `F1`) again, the original Help Window will remain minimized, and no new Help Window will appear. The remedy is to manually restore the minimized Help Window. +2. **If you minimise the Help Window** and then run the `help` command (or use the `Help` menu, or the keyboard shortcut `F1`) again, the original Help Window will remain minimised, and no new Help Window will appear. The remedy is to manually restore the minimised Help Window. --------------------------------------------------------------------------------------------------------------------- +--- ## Command summary -Action | Format, Examples ---------|------------------ -**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` -**Clear** | `clear` -**Delete** | `delete INDEX`
e.g., `delete 3` -**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` -**Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake` -**List** | `list` -**Help** | `help` +| Action | Format, Examples | +| ---------------------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Help** | `help` | +| **Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​`
e.g. `add n/Jensen Huang p/98765432 e/jensenh@nvidia.com a/21 Lower Kent Ridge Rd, Singapore 119077` | +| **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/Jensen Huang e/jensenh@yahoo.com` | +| **List** | `list` | +| **Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g. `find huang jensen` | +| **Clear** | `clear` | +| **Add Group** | `add-group n/GROUP_NAME`
e.g. `add-group n/CS2103T T12` | +| **Delete Group** | `delete-group INDEX`
e.g. `delete-group 1` | +| **Edit Group** | `edit-group INDEX [n/GROUP_NAME] [t/TAG]…​`
e.g. `edit-group 1 n/CS2103 T12 t/study` | +| **List Group** | `list-group` | +| **Find Group** | `find-group KEYWORD [MORE_KEYWORDS]`
e.g. `find-group CS2103T T12` | +| **Add to Group** | `add-to-group n/PERSON_NAME g/GROUP_NAME`
e.g. `add-to-group n/Jensen Huang g/CS2103T T12` | +| **Delete from Group** | `delete-from-group n/PERSON_NAME g/GROUP_NAME`
e.g. `delete-from-group n/Jensen Huang g/CS2103T T12` | +| **Show Group Details** | `show-group-details INDEX`
e.g. `show-group-details 1` | +| **Mark Attendance** | `mark-attendance n/PERSON_NAME g/GROUP_NAME w/WEEK_NUMBER`
e.g. `mark-attendance n/Jensen Huang g/CS2103T T12 w/10` | +| **Unmark Attendance** | `unmark-attendance n/PERSON_NAME g/GROUP_NAME w/WEEK_NUMBER`
e.g. `unmark-attendance n/Jensen Huang g/CS2103T T12 w/10` | +| **Show Attendance** | `show-attendance n/PERSON_NAME g/GROUP_NAME`
e.g. `show-attendance n/Jensen Huang g/CS2103T T12` | +| **Add Assignment** | `add-assignment n/ASSIGNMENT_NAME g/GROUP_NAME d/DEADLINE`
e.g. `add-assignment n/HW 1 g/CS2103T T12 d/21-04-2025` | +| **Delete Assignment** | `delete-assignment n/ASSIGNMENT_NAME g/GROUP_NAME`
e.g. `delete-assignment n/HW 1 g/CS2103T T12` | +| **Edit Assignment** | `edit-assignment n/ASSIGNMENT NAME g/GROUP [N/NEW NAME] [d/DEADLINE]`
e.g. `edit-assignment n/HW 1 g/CS2103T T12 N/Assignment 1 d/21-04-2025` | +| **Exit** | `exit` | diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..62213b059c3 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "TAbby Dabby" theme: minima header_pages: diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..ef407269df0 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: "TAbby Dabby"; font-size: 32px; } } diff --git a/docs/diagrams/AddAssignmentSequenceDiagram-Logic.puml b/docs/diagrams/AddAssignmentSequenceDiagram-Logic.puml new file mode 100644 index 00000000000..1b94d91328b --- /dev/null +++ b/docs/diagrams/AddAssignmentSequenceDiagram-Logic.puml @@ -0,0 +1,76 @@ +@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 ":AddAssignmentCommandParser" as AddAssignmentCommandParser LOGIC_COLOR +participant ":AddAssignmentCommand" as AddAssignmentCommand 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("add-assignment n/ a g/ g d/ d") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("add-assignment n/ a g/ g d/ d") +activate AddressBookParser + +create AddAssignmentCommandParser +AddressBookParser -> AddAssignmentCommandParser +activate AddAssignmentCommandParser + +AddAssignmentCommandParser --> AddressBookParser +deactivate AddAssignmentCommandParser + +AddressBookParser -> AddAssignmentCommandParser : parse("n/ a g/ g d/ d") +activate AddAssignmentCommandParser + +create AddAssignmentCommand +AddAssignmentCommandParser -> AddAssignmentCommand +activate AddAssignmentCommand + +AddAssignmentCommand --> AddAssignmentCommandParser : +deactivate AddAssignmentCommand + +AddAssignmentCommandParser --> AddressBookParser +deactivate AddAssignmentCommandParser +AddAssignmentCommandParser -[hidden]-> AddressBookParser +destroy AddAssignmentCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> AddAssignmentCommand : execute(c) +activate AddAssignmentCommand + +AddAssignmentCommand -> Model : getGroup("g") +activate Model + +Model --> AddAssignmentCommand : group +deactivate Model + +AddAssignmentCommand -> Model : addAssignmentToGroup("n", d, group) +activate Model + +Model --> AddAssignmentCommand : assignment +deactivate Model + +create CommandResult +AddAssignmentCommand -> CommandResult +activate CommandResult + +CommandResult --> AddAssignmentCommand +deactivate CommandResult + +AddAssignmentCommand --> LogicManager : r +deactivate AddAssignmentCommand + +[<--LogicManager +deactivate LogicManager + +@enduml diff --git a/docs/diagrams/AddAssignmentSequenceDiagram-Model.puml b/docs/diagrams/AddAssignmentSequenceDiagram-Model.puml new file mode 100644 index 00000000000..f18da54121c --- /dev/null +++ b/docs/diagrams/AddAssignmentSequenceDiagram-Model.puml @@ -0,0 +1,46 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Model MODEL_COLOR_T1 +participant "m:Model" as Model MODEL_COLOR +participant ":VersionedAddressBook" as VersionedAddressBook MODEL_COLOR +participant "g:Group" as Group MODEL_COLOR +participant ":Assignment" as Assignment MODEL_COLOR +participant ":ArrayList" as ArrayList MODEL_COLOR +end box + +[-> Model : addAssignmentToGroup(n, d, group) +activate Model + +Model -> VersionedAddressBook : addAssignmentToGroup(n, d, group) +activate VersionedAddressBook + +VersionedAddressBook -> Group : addAssignment(n, d) +activate Group + +create Assignment +Group -> Assignment : new Assignment(n, d) +activate Assignment + +Assignment --> Group : assignment +deactivate Assignment + +Group -> ArrayList: add(assignment) + +activate ArrayList + +ArrayList --> Group + +deactivate ArrayList +deactivate Assignment +Group --> VersionedAddressBook +deactivate Group + +VersionedAddressBook --> Model +deactivate VersionedAddressBook + +[<-- Model +deactivate Model +@enduml + diff --git a/docs/diagrams/AddPersonToGroupSequenceDiagram-Logic.puml b/docs/diagrams/AddPersonToGroupSequenceDiagram-Logic.puml new file mode 100644 index 00000000000..a9e621998c9 --- /dev/null +++ b/docs/diagrams/AddPersonToGroupSequenceDiagram-Logic.puml @@ -0,0 +1,82 @@ +@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 ":AddPersonToGroupCommandParser" as AddPersonToGroupCommandParser LOGIC_COLOR +participant ":AddPersonToGroupCommand" as AddPersonToGroupCommand 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("add-to-group n/ p g/ g") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("add-to-group n/ p g/ g") +activate AddressBookParser + +create AddPersonToGroupCommandParser +AddressBookParser -> AddPersonToGroupCommandParser +activate AddPersonToGroupCommandParser + +AddPersonToGroupCommandParser --> AddressBookParser +deactivate AddPersonToGroupCommandParser + +AddressBookParser -> AddPersonToGroupCommandParser : parse("n/ p g/ g") +activate AddPersonToGroupCommandParser + +create AddPersonToGroupCommand +AddPersonToGroupCommandParser -> AddPersonToGroupCommand +activate AddPersonToGroupCommand + +AddPersonToGroupCommand --> AddPersonToGroupCommandParser : +deactivate AddPersonToGroupCommand + +AddPersonToGroupCommandParser --> AddressBookParser : d +deactivate AddPersonToGroupCommandParser +AddPersonToGroupCommandParser -[hidden]-> AddressBookParser +destroy AddPersonToGroupCommandParser + +AddressBookParser --> LogicManager : d +deactivate AddressBookParser + +LogicManager -> AddPersonToGroupCommand : execute(m) +activate AddPersonToGroupCommand + +AddPersonToGroupCommand -> Model : getPerson("p") +activate Model + +Model --> AddPersonToGroupCommand : Person +deactivate Model + +AddPersonToGroupCommand -> Model : getGroup("g") +activate Model + +Model --> AddPersonToGroupCommand : Group +deactivate Model + +AddPersonToGroupCommand -> Model : addPersonToGroup(p, g) +activate Model + +Model -> AddPersonToGroupCommand +deactivate Model + +create CommandResult +AddPersonToGroupCommand -> CommandResult +activate CommandResult + +CommandResult --> AddPersonToGroupCommand +deactivate CommandResult + +AddPersonToGroupCommand --> LogicManager : r +deactivate AddPersonToGroupCommand + +[<--LogicManager +deactivate LogicManager + +@enduml diff --git a/docs/diagrams/AddPersonToGroupSequenceDiagram-Model.puml b/docs/diagrams/AddPersonToGroupSequenceDiagram-Model.puml new file mode 100644 index 00000000000..c8892035975 --- /dev/null +++ b/docs/diagrams/AddPersonToGroupSequenceDiagram-Model.puml @@ -0,0 +1,43 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Model MODEL_COLOR_T1 +participant "m:Model" as Model MODEL_COLOR +participant ":VersionedAddressBook" as VersionedAddressBook MODEL_COLOR +participant "g:Group" as Group MODEL_COLOR +participant ":GroupMemberDetails" as GroupMemberDetails MODEL_COLOR +participant ":ArrayListMap" as ArrayListMap MODEL_COLOR +end box + +[-> Model : addPersonToGroup(p, g) +activate Model + +Model -> VersionedAddressBook : addPersonToGroup(p, g) +activate VersionedAddressBook + +VersionedAddressBook -> Group : add(p) +activate Group + +create GroupMemberDetails +Group -> GroupMemberDetails : new GroupMemberDetails(p, this) +activate GroupMemberDetails + +GroupMemberDetails --> Group : GroupMemberDetails instance +deactivate GroupMemberDetails + +Group -> ArrayListMap : put(p), GroupMemberDetails) +activate ArrayListMap + +ArrayListMap --> Group +deactivate ArrayListMap + +Group --> VersionedAddressBook +deactivate Group + +VersionedAddressBook --> Model +deactivate VersionedAddressBook + +[<- Model +deactivate Model +@enduml diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml index 598474a5c82..c6d022bd19b 100644 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ b/docs/diagrams/BetterModelClassDiagram.puml @@ -5,12 +5,15 @@ skinparam arrowColor MODEL_COLOR skinparam classBackgroundColor MODEL_COLOR AddressBook *-right-> "1" UniquePersonList +AddressBook *-right-> "1" UniqueGroupList AddressBook *-right-> "1" UniqueTagList UniqueTagList -[hidden]down- UniquePersonList UniqueTagList -[hidden]down- UniquePersonList +UniqueTagList -[hidden]down- UniqueGroupList UniqueTagList -right-> "*" Tag UniquePersonList -right-> Person +UniqueGroupList -right-> Group Person -up-> "*" Tag @@ -18,4 +21,10 @@ Person *--> Name Person *--> Phone Person *--> Email Person *--> Address + +Group -up-> "*" Tag +Group -up-> "*" Assignment + +Group *--> Name +Group --> "*" Person @enduml diff --git a/docs/diagrams/DeleteSequenceDiagram.puml b/docs/diagrams/DeleteSequenceDiagram.puml index 5241e79d7da..4b52737e9e4 100644 --- a/docs/diagrams/DeleteSequenceDiagram.puml +++ b/docs/diagrams/DeleteSequenceDiagram.puml @@ -55,6 +55,12 @@ activate Model Model --> DeleteCommand deactivate Model +DeleteCommand -> Model : deletePersonFromAllGroups(p) +activate Model + +Model -> DeleteCommand +deactivate Model + create CommandResult DeleteCommand -> CommandResult activate CommandResult diff --git a/docs/diagrams/MarkAttendanceSequenceDiagram.puml b/docs/diagrams/MarkAttendanceSequenceDiagram.puml new file mode 100644 index 00000000000..6b0f485556b --- /dev/null +++ b/docs/diagrams/MarkAttendanceSequenceDiagram.puml @@ -0,0 +1,75 @@ +@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 ":MarkAttendanceCommandParser" as MarkAttendanceCommandParser LOGIC_COLOR +participant ":MarkAttendanceCommand" as MarkAttendanceCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "m:Model" as Model MODEL_COLOR +participant ":GroupMemberDetails" as GroupMemberDetails MODEL_COLOR +end box + +[-> LogicManager : execute("delete 1") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("delete 1") +activate AddressBookParser + +create MarkAttendanceCommandParser +AddressBookParser -> MarkAttendanceCommandParser +activate MarkAttendanceCommandParser + +MarkAttendanceCommandParser --> AddressBookParser +deactivate MarkAttendanceCommandParser + +AddressBookParser -> MarkAttendanceCommandParser : parse("1") +activate MarkAttendanceCommandParser + +create MarkAttendanceCommand +MarkAttendanceCommandParser -> MarkAttendanceCommand +activate MarkAttendanceCommand + +MarkAttendanceCommand --> MarkAttendanceCommandParser : +deactivate MarkAttendanceCommand + +MarkAttendanceCommandParser --> AddressBookParser : d +deactivate MarkAttendanceCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +MarkAttendanceCommandParser -[hidden]-> AddressBookParser +destroy MarkAttendanceCommandParser + +AddressBookParser --> LogicManager : d +deactivate AddressBookParser + +LogicManager -> MarkAttendanceCommand : execute(m) +activate MarkAttendanceCommand + +MarkAttendanceCommand -> Model : getGroupMemberDetails(1) +activate Model + +Model --> MarkAttendanceCommand : GroupMemberDetails +deactivate Model + +MarkAttendanceCommand -> GroupMemberDetails : markAttendance(w) +activate GroupMemberDetails + +GroupMemberDetails -> MarkAttendanceCommand +deactivate GroupMemberDetails + +create CommandResult +MarkAttendanceCommand -> CommandResult +activate CommandResult + +CommandResult -> MarkAttendanceCommand +deactivate CommandResult + +MarkAttendanceCommand -> LogicManager : r +deactivate MarkAttendanceCommand + +@enduml diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 0de5673070d..0e128007f99 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -11,14 +11,12 @@ Class "<>\nModel" as Model Class AddressBook Class ModelManager Class UserPrefs +Class ResultList Class UniquePersonList +Class UniqueGroupList Class Person -Class Address -Class Email -Class Name -Class Phone -Class Tag +Class Group Class I #FFFFFF } @@ -28,6 +26,7 @@ HiddenOutside ..> Model AddressBook .up.|> ReadOnlyAddressBook +ModelManager -[hidden]down- I ModelManager .up.|> Model Model .right.> ReadOnlyUserPrefs Model .left.> ReadOnlyAddressBook @@ -36,19 +35,16 @@ ModelManager -right-> "1" UserPrefs UserPrefs .up.|> ReadOnlyUserPrefs AddressBook *--> "1" UniquePersonList +AddressBook *--> "1" UniqueGroupList UniquePersonList --> "~* all" Person -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address -Person *--> "*" Tag +UniqueGroupList --> "~* all" Group -Person -[hidden]up--> I -UniquePersonList -[hidden]right-> I +UniquePersonList -[hidden]right- I +UniquePersonList -[hidden]left- UniqueGroupList -Name -[hidden]right-> Phone -Phone -[hidden]right-> Address -Address -[hidden]right-> Email - -ModelManager --> "~* filtered" Person +ModelManager ---> "1" ResultList +ResultList -up--> "~* filtered" Person +ResultList -up--> "~* filtered" Group +ResultList -[hidden]up- I +Person -[hidden]left-> Group @enduml diff --git a/docs/diagrams/PersonInGroupDiagram.puml b/docs/diagrams/PersonInGroupDiagram.puml new file mode 100644 index 00000000000..cfee7785058 --- /dev/null +++ b/docs/diagrams/PersonInGroupDiagram.puml @@ -0,0 +1,34 @@ +@startuml + +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + +AddressBook *-right-> "1" UniqueGroupList +AddressBook *-right-> "1" UniquePersonList +UniqueGroupList -[hidden]down- UniqueGroupList +UniqueGroupList -[hidden]down- UniquePersonList + +UniqueGroupList -right-> "*" Group +UniquePersonList -down-> Person + +Group *--> Person : "Key" +Group *--> GroupMemberDetails : "Value" +Group *--> Assignment + +Person *--> Name +Person *--> Phone +Person *--> Email +Person *--> Address +GroupMemberDetails *--> Person +GroupMemberDetails *--> Group + +enum Role { + TA + Student + Professor +} +GroupMemberDetails *--> Role + +@enduml diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index a821e06458c..3761b2f4259 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -19,6 +19,9 @@ Class "<>\nAddressBookStorage" as AddressBookStorage Class JsonAddressBookStorage Class JsonSerializableAddressBook Class JsonAdaptedPerson +Class JsonAdaptedGroup +Class JsonAdaptedGroupMemberDetails +Class JsonAdaptedAssignment Class JsonAdaptedTag } @@ -38,6 +41,11 @@ JsonUserPrefsStorage .up.|> UserPrefsStorage JsonAddressBookStorage .up.|> AddressBookStorage JsonAddressBookStorage ..> JsonSerializableAddressBook JsonSerializableAddressBook --> "*" JsonAdaptedPerson +JsonSerializableAddressBook --> "*" JsonAdaptedGroup JsonAdaptedPerson --> "*" JsonAdaptedTag +JsonAdaptedGroup --> "*" JsonAdaptedGroupMemberDetails +JsonAdaptedGroup --> "*" JsonAdaptedTag +JsonAdaptedGroupMemberDetails --> "*" JsonAdaptedAssignment +JsonAdaptedGroupMemberDetails --> "1" JsonAdaptedPerson @enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..585c0910ce2 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -11,8 +11,10 @@ Class UiManager Class MainWindow Class HelpWindow Class ResultDisplay -Class PersonListPanel +Class ResultListPanel Class PersonCard +Class GroupCard +Class GroupDetailCard Class StatusBarFooter Class CommandBox } @@ -32,29 +34,38 @@ UiManager .left.|> Ui UiManager -down-> "1" MainWindow MainWindow *-down-> "1" CommandBox MainWindow *-down-> "1" ResultDisplay -MainWindow *-down-> "1" PersonListPanel +MainWindow *-down-> "1" ResultListPanel MainWindow *-down-> "1" StatusBarFooter MainWindow --> "0..1" HelpWindow -PersonListPanel -down-> "*" PersonCard +ResultListPanel -down-> "*" PersonCard +ResultListPanel -down-> "*" GroupCard +ResultListPanel -down-> "*" GroupDetailCard MainWindow -left-|> UiPart +MainWindow -[hidden]-|> UiPart ResultDisplay --|> UiPart CommandBox --|> UiPart -PersonListPanel --|> UiPart +ResultListPanel --|> UiPart PersonCard --|> UiPart +GroupCard --|> UiPart +GroupDetailCard --|> UiPart StatusBarFooter --|> UiPart HelpWindow --|> UiPart PersonCard ..> Model +GroupCard ..> Model +GroupDetailCard ..> Model UiManager -right-> Logic MainWindow -left-> Logic -PersonListPanel -[hidden]left- HelpWindow +ResultListPanel -[hidden]left- HelpWindow HelpWindow -[hidden]left- CommandBox CommandBox -[hidden]left- ResultDisplay ResultDisplay -[hidden]left- StatusBarFooter -MainWindow -[hidden]-|> UiPart +PersonCard -[hidden]left- GroupCard +GroupCard -[hidden]left- GroupDetailCard +GroupDetailCard -[hidden]up- UiPart @enduml diff --git a/docs/images/AddAssignmentCommandResult.png b/docs/images/AddAssignmentCommandResult.png new file mode 100644 index 00000000000..86dd2e34e00 Binary files /dev/null and b/docs/images/AddAssignmentCommandResult.png differ diff --git a/docs/images/AddAssignmentSequenceDiagram-Logic.png b/docs/images/AddAssignmentSequenceDiagram-Logic.png new file mode 100644 index 00000000000..0b3cb8249cd Binary files /dev/null and b/docs/images/AddAssignmentSequenceDiagram-Logic.png differ diff --git a/docs/images/AddAssignmentSequenceDiagram-Model.png b/docs/images/AddAssignmentSequenceDiagram-Model.png new file mode 100644 index 00000000000..15f0783a174 Binary files /dev/null and b/docs/images/AddAssignmentSequenceDiagram-Model.png differ diff --git a/docs/images/AddCommandResult.png b/docs/images/AddCommandResult.png new file mode 100644 index 00000000000..627469c36e8 Binary files /dev/null and b/docs/images/AddCommandResult.png differ diff --git a/docs/images/AddGroupCommandResult.png b/docs/images/AddGroupCommandResult.png new file mode 100644 index 00000000000..5c18acc60fc Binary files /dev/null and b/docs/images/AddGroupCommandResult.png differ diff --git a/docs/images/AddPersonToGroupSequenceDiagram-Logic.png b/docs/images/AddPersonToGroupSequenceDiagram-Logic.png new file mode 100644 index 00000000000..e392722d514 Binary files /dev/null and b/docs/images/AddPersonToGroupSequenceDiagram-Logic.png differ diff --git a/docs/images/AddPersonToGroupSequenceDiagram-Model.png b/docs/images/AddPersonToGroupSequenceDiagram-Model.png new file mode 100644 index 00000000000..6aea717d5de Binary files /dev/null and b/docs/images/AddPersonToGroupSequenceDiagram-Model.png differ diff --git a/docs/images/AddToGroupCommandResult.png b/docs/images/AddToGroupCommandResult.png new file mode 100644 index 00000000000..9ca0c644039 Binary files /dev/null and b/docs/images/AddToGroupCommandResult.png differ diff --git a/docs/images/BetterModelClassDiagram.png b/docs/images/BetterModelClassDiagram.png index 02a42e35e76..212c81b6d4b 100644 Binary files a/docs/images/BetterModelClassDiagram.png and b/docs/images/BetterModelClassDiagram.png differ diff --git a/docs/images/DeleteCommandResultAfter.png b/docs/images/DeleteCommandResultAfter.png new file mode 100644 index 00000000000..95f8924265a Binary files /dev/null and b/docs/images/DeleteCommandResultAfter.png differ diff --git a/docs/images/DeleteCommandResultBefore.png b/docs/images/DeleteCommandResultBefore.png new file mode 100644 index 00000000000..024365f7cdc Binary files /dev/null and b/docs/images/DeleteCommandResultBefore.png differ diff --git a/docs/images/EditCommandResult.png b/docs/images/EditCommandResult.png new file mode 100644 index 00000000000..4cb59018cc3 Binary files /dev/null and b/docs/images/EditCommandResult.png differ diff --git a/docs/images/EditGroupCommandResult.png b/docs/images/EditGroupCommandResult.png new file mode 100644 index 00000000000..66753acf39d Binary files /dev/null and b/docs/images/EditGroupCommandResult.png differ diff --git a/docs/images/FindGroupCommandResult.png b/docs/images/FindGroupCommandResult.png new file mode 100644 index 00000000000..080fd4e67be Binary files /dev/null and b/docs/images/FindGroupCommandResult.png differ diff --git a/docs/images/ListGroupCommandResult.png b/docs/images/ListGroupCommandResult.png new file mode 100644 index 00000000000..ecf49bff23a Binary files /dev/null and b/docs/images/ListGroupCommandResult.png differ diff --git a/docs/images/MarkAttendanceSequenceDiagram1.png b/docs/images/MarkAttendanceSequenceDiagram1.png new file mode 100644 index 00000000000..71959bb667c Binary files /dev/null and b/docs/images/MarkAttendanceSequenceDiagram1.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index a19fb1b4ac8..977c2823a5c 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/PersonInGroupDiagram.png b/docs/images/PersonInGroupDiagram.png new file mode 100644 index 00000000000..4b1ed6fe383 Binary files /dev/null and b/docs/images/PersonInGroupDiagram.png differ diff --git a/docs/images/ShowAttendanceCommandResult.png b/docs/images/ShowAttendanceCommandResult.png new file mode 100644 index 00000000000..43de3d454e4 Binary files /dev/null and b/docs/images/ShowAttendanceCommandResult.png differ diff --git a/docs/images/ShowGroupDetailsCommandResult.png b/docs/images/ShowGroupDetailsCommandResult.png new file mode 100644 index 00000000000..c9bd2847399 Binary files /dev/null and b/docs/images/ShowGroupDetailsCommandResult.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 18fa4d0d51f..c51bf8cd6ab 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..bc102524c45 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/UiClassDiagram.png b/docs/images/UiClassDiagram.png index 11f06d68671..cbe24a8e2b3 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/avinazz3.png b/docs/images/avinazz3.png new file mode 100644 index 00000000000..91246a162bc Binary files /dev/null and b/docs/images/avinazz3.png differ diff --git a/docs/images/dexterkwxn.png b/docs/images/dexterkwxn.png new file mode 100644 index 00000000000..603ec0e773e Binary files /dev/null and b/docs/images/dexterkwxn.png differ diff --git a/docs/images/findAlexDavidResult.png b/docs/images/findAlexDavidResult.png deleted file mode 100644 index 235da1c273e..00000000000 Binary files a/docs/images/findAlexDavidResult.png and /dev/null differ diff --git a/docs/images/findHuangBezosResult.png b/docs/images/findHuangBezosResult.png new file mode 100644 index 00000000000..c1390b75ced Binary files /dev/null and b/docs/images/findHuangBezosResult.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png index b1f70470137..2366b240c8f 100644 Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ diff --git a/docs/images/lyhthaddeus.png b/docs/images/lyhthaddeus.png new file mode 100644 index 00000000000..7be283249a6 Binary files /dev/null and b/docs/images/lyhthaddeus.png differ diff --git a/docs/images/weien02.png b/docs/images/weien02.png new file mode 100644 index 00000000000..ef2b9b65eb2 Binary files /dev/null and b/docs/images/weien02.png differ diff --git a/docs/images/zwliew.png b/docs/images/zwliew.png new file mode 100644 index 00000000000..972fe84ee96 Binary files /dev/null and b/docs/images/zwliew.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..43e0ad6ebb3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,16 @@ --- layout: page -title: AddressBook Level-3 +title: TAbby Dabby --- -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) -[![codecov](https://codecov.io/gh/se-edu/addressbook-level3/branch/master/graph/badge.svg)](https://codecov.io/gh/se-edu/addressbook-level3) +[![Java CI](https://github.com/AY2425S2-CS2103T-T12-1/tp/actions/workflows/gradle.yml/badge.svg)](https://github.com/AY2425S2-CS2103T-T12-1/tp/actions/workflows/gradle.yml) ![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). +**TAbby Dabby 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 TAbby Dabby, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +* If you are interested about developing TAbby Dabby, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. **Acknowledgements** diff --git a/docs/team/avinash.md b/docs/team/avinash.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/main/java/seedu/address/commons/util/ArrayListMap.java b/src/main/java/seedu/address/commons/util/ArrayListMap.java new file mode 100644 index 00000000000..e64716b2ac7 --- /dev/null +++ b/src/main/java/seedu/address/commons/util/ArrayListMap.java @@ -0,0 +1,115 @@ +package seedu.address.commons.util; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Map; + +/** + * A Map implementation backed by ArrayLists and compares using Object#equals. + * + * @param The type of the keys in the map. + * @param The type of the values in the map. + */ +public class ArrayListMap implements Map { + private final ArrayListSet keys; + private final ArrayList vals; + + /** + * Creates a new empty ArrayListMap. + */ + public ArrayListMap() { + this.keys = new ArrayListSet<>(); + this.vals = new ArrayList<>(); + } + + @Override + public int size() { + return keys.size(); + } + + @Override + public boolean isEmpty() { + return keys.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return keys.contains(key); + } + + @Override + public boolean containsValue(Object value) { + return vals.contains(value); + } + + @Override + public V get(Object key) { + int index = keys.indexOf(key); + if (index == -1) { + return null; + } + return vals.get(index); + } + + public V get(int index) { + return vals.get(index); + } + + @Override + public V put(K key, V value) { + int index = keys.indexOf(key); + if (index == -1) { + keys.add(key); + vals.add(value); + return null; + } + return vals.set(index, value); + } + + @Override + public V remove(Object key) { + int index = keys.indexOf(key); + if (index == -1) { + return null; + } + V val = vals.remove(index); + keys.remove(index); + return val; + } + + @Override + public void putAll(Map m) { + for (var entry : m.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + public void replaceKey(K k, K l) { + keys.set(k, l); + } + + @Override + public void clear() { + keys.clear(); + vals.clear(); + } + + @Override + public ArrayListSet keySet() { + return keys; + } + + @Override + public ArrayList values() { + return vals; + } + + @Override + public HashSet> entrySet() { + HashSet> set = new HashSet<>(); + for (int i = 0; i < keys.size(); i++) { + set.add(Map.entry(keys.get(i), vals.get(i))); + } + return set; + } +} diff --git a/src/main/java/seedu/address/commons/util/ArrayListSet.java b/src/main/java/seedu/address/commons/util/ArrayListSet.java new file mode 100644 index 00000000000..6f3720fcc02 --- /dev/null +++ b/src/main/java/seedu/address/commons/util/ArrayListSet.java @@ -0,0 +1,118 @@ +package seedu.address.commons.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.Set; + +/** + * A Set implementation backed by ArrayLists and compares using Object#equals. + * @param The type of the keys in the set. + */ +public class ArrayListSet implements Set { + private final ArrayList keys; + + public ArrayListSet() { + this.keys = new ArrayList<>(); + } + + @Override + public int size() { + return keys.size(); + } + + @Override + public boolean isEmpty() { + return keys.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return keys.contains(o); + } + + @Override + public Iterator iterator() { + return keys.iterator(); + } + + @Override + public Object[] toArray() { + return keys.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return keys.toArray(a); + } + + @Override + public boolean add(K k) { + if (contains(k)) { + return false; + } + return keys.add(k); + } + + @Override + public boolean remove(Object o) { + return keys.remove(o); + } + + /** + * Removes the element at the specified position in this list. + * Shifts any subsequent elements to the left (subtracts one from their + * indices). + * + * @param index the index of the element to be removed + * @return true + * @throws IndexOutOfBoundsException {@inheritDoc} + */ + public boolean remove(int index) { + keys.remove(index); + return true; + } + + @Override + public boolean containsAll(Collection c) { + return keys.containsAll(c); + } + + @Override + public boolean addAll(Collection c) { + boolean changed = false; + for (K k : c) { + changed |= add(k); + } + return changed; + } + + @Override + public boolean retainAll(Collection c) { + return keys.retainAll(c); + } + + @Override + public boolean removeAll(Collection c) { + return keys.removeAll(c); + } + + @Override + public void clear() { + keys.clear(); + } + + public int indexOf(Object o) { + return keys.indexOf(o); + } + + public K get(int index) { + return keys.get(index); + } + + public void set(K k, K l) { + int index = indexOf(k); + keys.set(index, l); + } + +} diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 92cd8fa605a..d04beabde03 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -9,6 +9,7 @@ import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.person.Person; +import seedu.address.ui.Result; /** * API of the Logic component @@ -33,6 +34,9 @@ public interface Logic { /** Returns an unmodifiable view of the filtered list of persons */ ObservableList getFilteredPersonList(); + /** Returns an unmodifiable view of the filtered list of results */ + ObservableList getResultList(); + /** * Returns the user prefs' address book file path. */ diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 5aa3b91c7d0..7904d74833f 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -17,6 +17,7 @@ import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.person.Person; import seedu.address.storage.Storage; +import seedu.address.ui.Result; /** * The main LogicManager of the app. @@ -71,6 +72,11 @@ public ObservableList getFilteredPersonList() { return model.getFilteredPersonList(); } + @Override + public ObservableList getResultList() { + return model.getResultList(); + } + @Override public Path getAddressBookFilePath() { return model.getAddressBookFilePath(); diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java index ecd32c31b53..33bff865c97 100644 --- a/src/main/java/seedu/address/logic/Messages.java +++ b/src/main/java/seedu/address/logic/Messages.java @@ -1,10 +1,13 @@ package seedu.address.logic; +import java.util.Arrays; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import seedu.address.logic.parser.Prefix; +import seedu.address.model.assignment.Assignment; +import seedu.address.model.group.Group; import seedu.address.model.person.Person; /** @@ -12,12 +15,22 @@ */ public class Messages { - public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; + public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command. Type \"help\" to see the list of commands."; public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; - public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; + public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid!\n" + + "Ensure that it is not out of range."; public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; + public static final String MESSAGE_GROUPS_LISTED_OVERVIEW = "%1$d groups listed!"; public static final String MESSAGE_DUPLICATE_FIELDS = - "Multiple values specified for the following single-valued field(s): "; + "Multiple values specified for the following single-valued field(s): "; + + public static final String MESSAGE_INVALID_GROUP_DISPLAYED_INDEX = "The group index provided is invalid!\n" + + "Ensure that it is not out of range."; + + public static final String MESSAGE_PERSON_NOT_FOUND = "This person does not exist!"; + public static final String MESSAGE_GROUP_NOT_FOUND = "This group does not exist!"; + public static final String MESSAGE_PERSON_NOT_IN_GROUP = "This person does not exist in the group!"; + public static final String MESSAGE_INVALID_WEEK_NUM = "Week number must be between 1 and 13 (inclusive)!"; /** * Returns an error message indicating the duplicate prefixes. @@ -32,7 +45,7 @@ public static String getErrorMessageForDuplicatePrefixes(Prefix... duplicatePref } /** - * Formats the {@code person} for display to the user. + * Formats the {@code group} for display to the user. */ public static String format(Person person) { final StringBuilder builder = new StringBuilder(); @@ -43,9 +56,32 @@ public static String format(Person person) { .append(person.getEmail()) .append("; Address: ") .append(person.getAddress()) - .append("; Tags: "); + .append("; Tags: ["); person.getTags().forEach(builder::append); + builder.append("]"); return builder.toString(); } + /** + * Formats the {@code person} for display to the user. + */ + public static String format(Group group) { + final StringBuilder builder = new StringBuilder(); + builder.append(group.getGroupName()) + .append("\nTags: ["); + group.getTags().forEach(builder::append); + builder.append("]\nMembers: [\n"); + group.getGroupMembersMap().forEach((key, value) -> builder.append(Messages.format(key)) + .append("; Role: ") + .append(value.getRole()) + .append(", Attendance: ") + .append(Arrays.toString(value.getAttendance())) + .append("\n")); + builder.append("]"); + return builder.toString(); + } + + public static String format(Assignment assignment) { + return assignment.getName() + "; Deadline: " + assignment.getDeadline(); + } } diff --git a/src/main/java/seedu/address/logic/commands/AddAssignmentCommand.java b/src/main/java/seedu/address/logic/commands/AddAssignmentCommand.java new file mode 100644 index 00000000000..99765e67c98 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddAssignmentCommand.java @@ -0,0 +1,123 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.Messages.MESSAGE_GROUP_NOT_FOUND; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; + +import java.time.LocalDate; + +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.assignment.Assignment; +import seedu.address.model.assignment.exceptions.DuplicateAssignmentException; +import seedu.address.model.group.Group; +import seedu.address.model.group.exceptions.GroupNotFoundException; + +/** + * Adds a new assignment to the specified group. + */ +public class AddAssignmentCommand extends Command { + + /** + * The command word to trigger this command. + */ + public static final String COMMAND_WORD = "add-assignment"; + + public static final String MESSAGE_USAGE = String.format(""" + %s: Adds a new assignment in the specified group. + Parameters: %sASSIGNMENT_NAME %sGROUP_NAME %sDEADLINE + Example: %s %sHW 1 %sCS2103T T12 %s21-04-2025 + """, + COMMAND_WORD, PREFIX_NAME, PREFIX_GROUP, PREFIX_DATE, COMMAND_WORD, PREFIX_NAME, + PREFIX_GROUP, PREFIX_DATE); + + private static final String MESSAGE_SUCCESS = "Added new assignment to group!\nGroup: %s\nAssignment: %s"; + + /** + * Name of the new assignment to be added. + */ + private final String name; + + /** + * Name of the group to be added to. + */ + private final String groupName; + + /** + * Deadline of the assignment + */ + private final LocalDate deadline; + + /** + * Grade penalty for late submission, default value is 1.0. + */ + private final Float penalty; + + /** + * Creates an AddAssignmentCommand to add a new assignment with the specified name, group, and deadline. + * + * @param name The name of the assignment to be added. + * @param groupName The name of the group to be added. + * @param deadline The deadline of the assignment. + */ + public AddAssignmentCommand(String name, String groupName, LocalDate deadline, Float penalty) { + requireAllNonNull(name, groupName, deadline); + this.name = name; + this.groupName = groupName; + this.deadline = deadline; + this.penalty = penalty == null ? 1.0F : penalty; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + Group group; + try { + group = model.getGroup(groupName); + } catch (GroupNotFoundException e) { + throw new CommandException(MESSAGE_GROUP_NOT_FOUND); + } + + if (model.isAssignmentInGroup(name, group)) { + throw new CommandException("Another assignment with the same name already exists in group!"); + } + + try { + Assignment assignment = model.addAssignmentToGroup(name, deadline, group, penalty); + return new CommandResult(String.format(MESSAGE_SUCCESS, groupName, Messages.format(assignment)), + true, group); + } catch (DuplicateAssignmentException d) { + throw new CommandException(d.getMessage()); + } + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AddAssignmentCommand otherAddAssignmentCommand)) { + return false; + } + + return name.equals(otherAddAssignmentCommand.name) && groupName.equals(otherAddAssignmentCommand.groupName); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("name", name) + .add("groupName", groupName) + .add("deadline", deadline) + .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..606bd8433f8 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -20,23 +20,17 @@ public class AddCommand extends Command { public static final String COMMAND_WORD = "add"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " - + "Parameters: " - + PREFIX_NAME + "NAME " - + PREFIX_PHONE + "PHONE " - + PREFIX_EMAIL + "EMAIL " - + PREFIX_ADDRESS + "ADDRESS " - + "[" + PREFIX_TAG + "TAG]...\n" - + "Example: " + COMMAND_WORD + " " - + PREFIX_NAME + "John Doe " - + PREFIX_PHONE + "98765432 " - + PREFIX_EMAIL + "johnd@example.com " - + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " - + PREFIX_TAG + "friends " - + PREFIX_TAG + "owesMoney"; + public static final String MESSAGE_USAGE = String.format(""" + %s: Adds a person to the person list. Useful for adding new details of your students. + Parameters: %sNAME %sPHONE %sEMAIL %sADDRESS [%sTAG]... + Example: %s %sJensen Huang %s98765432 %sjensenh@nvidia.com %s21 Lower Kent Ridge Rd,\s""" + + "Singapore 119077 %sfriends", + COMMAND_WORD, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG, + COMMAND_WORD, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); - public static final String MESSAGE_SUCCESS = "New person added: %1$s"; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; + public static final String MESSAGE_SUCCESS = "Added new person:\n%1$s"; + public static final String MESSAGE_DUPLICATE_PERSON = "Another person with the same name already exists in the" + + " address book!"; private final Person toAdd; diff --git a/src/main/java/seedu/address/logic/commands/AddGroupCommand.java b/src/main/java/seedu/address/logic/commands/AddGroupCommand.java new file mode 100644 index 00000000000..592feccbbc4 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddGroupCommand.java @@ -0,0 +1,94 @@ +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_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.ArrayList; +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.group.Group; +import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; + +/** + * Adds a new group to the address book. + */ +public class AddGroupCommand extends Command { + + /** + * The command word to trigger this command. + */ + public static final String COMMAND_WORD = "add-group"; + + public static final String MESSAGE_USAGE = String.format(""" + %s: Adds a new group to the group list. Useful for adding new tutorial groups. + Parameters: %sGROUP_NAME [%sTAG]... + Example: %s %sCS2103T T12 %sCS""", + COMMAND_WORD, PREFIX_NAME, PREFIX_TAG, COMMAND_WORD, PREFIX_NAME, PREFIX_TAG); + + private static final String MESSAGE_SUCCESS = "New group added: %1$s"; + private static final String MESSAGE_DUPLICATE_GROUP = "Another group with the same name" + + " already exists in the address book!"; + + /** + * Name of the new group to be added. + */ + private final String groupName; + private final Set tags; + + /** + * Creates an AddGroupCommand to add a new group with the specified name. + * + * @param groupName The name of the group to be added. + */ + public AddGroupCommand(String groupName, Set tags) { + requireAllNonNull(groupName, tags); + this.groupName = groupName; + this.tags = tags; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + // Create a new group with the given name and an empty list of members + Group groupToAdd = new Group(groupName, new ArrayList(), tags); + + // Check if the group already exists + if (model.hasGroup(groupToAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_GROUP); + } + + // Add group to the model + model.addGroup(groupToAdd); + return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(groupToAdd))); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AddGroupCommand otherAddGroupCommand)) { + return false; + } + + return groupName.equals(otherAddGroupCommand.groupName) && tags.equals(otherAddGroupCommand.tags); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("groupName", groupName) + .add("tags", tags) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/AddPersonToGroupCommand.java b/src/main/java/seedu/address/logic/commands/AddPersonToGroupCommand.java new file mode 100644 index 00000000000..8286ff7c976 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddPersonToGroupCommand.java @@ -0,0 +1,80 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON; + +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.group.Group; +import seedu.address.model.group.exceptions.GroupNotFoundException; +import seedu.address.model.person.Person; +import seedu.address.model.person.exceptions.PersonNotFoundException; + +/** + * Adds a Person to a Group + */ +public class AddPersonToGroupCommand extends Command { + public static final String COMMAND_WORD = "add-to-group"; + + public static final String MESSAGE_USAGE = String.format(""" + %s: Adds the specified person to the specified group. + Parameters: %sPERSON_NAME %sGROUP_NAME + Example: add-to-group n/Jensen Huang g/CS2103T T12 + """, COMMAND_WORD, PREFIX_PERSON, PREFIX_GROUP); + + public static final String MESSAGE_SUCCESS = """ + Added person to group: + Person: %1$s + Group: %2$s"""; + public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the group!"; + + /** + * Index of Person to be added + */ + private final String personName; + /** + * Index of Group to be added to + */ + private final String groupName; + + /** + * Constructor for AddPersonToGroupCommand that takes two Index identifiers for + * Person and Group + * + * @param personName Index of Person to be added + * @param groupName Index of Group to be added to + */ + public AddPersonToGroupCommand(String personName, String groupName) { + requireNonNull(personName); + requireNonNull(groupName); + this.personName = personName; + this.groupName = groupName; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + Person person; + try { + person = model.getPerson(this.personName); + } catch (PersonNotFoundException e) { + throw new CommandException("Person not found!"); + } + Group group; + try { + group = model.getGroup(groupName); + } catch (GroupNotFoundException e) { + throw new CommandException("Group not found!"); + } + + if (model.isPersonInGroup(person, group)) { + throw new CommandException(MESSAGE_DUPLICATE_PERSON); + } + + model.addPersonToGroup(person, group); + return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(person), Messages.format(group)), + true, group); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 9c86b1fa6e4..6cb893eb134 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -11,7 +11,7 @@ public class ClearCommand extends Command { public static final String COMMAND_WORD = "clear"; - public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; + public static final String MESSAGE_SUCCESS = "Cleared the entire address book!"; @Override diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java index 249b6072d0d..bb4ac4a6ff9 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/seedu/address/logic/commands/CommandResult.java @@ -5,6 +5,7 @@ import java.util.Objects; import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.group.Group; /** * Represents the result of a command execution. @@ -18,6 +19,10 @@ public class CommandResult { /** The application should exit. */ private final boolean exit; + /** Whether the detailBox should be shown */ + private final boolean showGroupDetails; + /** The group to be shown */ + private final Group groupToShow; /** * Constructs a {@code CommandResult} with the specified fields. @@ -26,6 +31,8 @@ public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { this.feedbackToUser = requireNonNull(feedbackToUser); this.showHelp = showHelp; this.exit = exit; + this.showGroupDetails = false; + this.groupToShow = null; } /** @@ -36,6 +43,17 @@ public CommandResult(String feedbackToUser) { this(feedbackToUser, false, false); } + /** + * Construct a {@code CommandResult} with the specified {@code Group} to show. + */ + public CommandResult(String feedbackToUSer, boolean showGroupDetails, Group groupToShow) { + this.groupToShow = groupToShow; + this.showGroupDetails = showGroupDetails; + this.feedbackToUser = feedbackToUSer; + this.showHelp = false; + this.exit = false; + } + public String getFeedbackToUser() { return feedbackToUser; } @@ -48,6 +66,14 @@ public boolean isExit() { return exit; } + public boolean isShowGroupDetails() { + return showGroupDetails; + } + + public Group getGroupToShow() { + return groupToShow; + } + @Override public boolean equals(Object other) { if (other == this) { diff --git a/src/main/java/seedu/address/logic/commands/DeleteAssignmentCommand.java b/src/main/java/seedu/address/logic/commands/DeleteAssignmentCommand.java new file mode 100644 index 00000000000..1e53cc4719d --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteAssignmentCommand.java @@ -0,0 +1,101 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.Messages.MESSAGE_GROUP_NOT_FOUND; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.assignment.exceptions.AssignmentNotFoundException; +import seedu.address.model.group.Group; +import seedu.address.model.group.exceptions.GroupNotFoundException; + +/** + * Deletes the specified assignment. + */ +public class DeleteAssignmentCommand extends Command { + + /** + * The command word to trigger this command. + */ + public static final String COMMAND_WORD = "delete-assignment"; + + public static final String MESSAGE_USAGE = String.format(""" + %s: Deletes an assignment in the specified group. + Parameters: %sASSIGNMENT_NAME %sGROUP_NAME + Example: %s %sHW 1 %sCS2103T T12 + """, + COMMAND_WORD, PREFIX_NAME, PREFIX_GROUP, COMMAND_WORD, PREFIX_NAME, PREFIX_GROUP); + + private static final String MESSAGE_SUCCESS = "Assignment %s deleted from group %s."; + + /** + * Name of the assignment to be deleted. + */ + private final String name; + + /** + * Name of the group the assignment belongs in. + */ + private final String groupName; + + /** + * Creates a DeleteAssignmentCommand to add a new assignment with the specified name, group, and deadline. + * + * @param name The name of the assignment to be added. + * @param groupName The name of the group to be added. + */ + public DeleteAssignmentCommand(String name, String groupName) { + requireAllNonNull(name, groupName); + this.name = name; + this.groupName = groupName; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + Group group; + try { + group = model.getGroup(groupName); + } catch (GroupNotFoundException e) { + throw new CommandException(MESSAGE_GROUP_NOT_FOUND); + } + + try { + model.removeAssignmentFromGroup(name, group); + } catch (AssignmentNotFoundException e) { + throw new CommandException("Assignment not found in the group!"); + } + + return new CommandResult(String.format(MESSAGE_SUCCESS, name, groupName), true, group); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof DeleteAssignmentCommand otherDeleteAssignmentCommand)) { + return false; + } + + return name.equals(otherDeleteAssignmentCommand.name) + && groupName.equals(otherDeleteAssignmentCommand.groupName); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("name", name) + .add("groupName", groupName) + .toString(); + } +} + + diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index 1135ac19b74..ec241bd3750 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -18,12 +18,19 @@ public class DeleteCommand extends Command { public static final String COMMAND_WORD = "delete"; - public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Deletes the person identified by the index number used in the displayed person list.\n" - + "Parameters: INDEX (must be a positive integer)\n" - + "Example: " + COMMAND_WORD + " 1"; - - public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; + public static final String MESSAGE_USAGE = String.format(""" + %s: Deletes the specified person from the person list. Useful for removing the details of""" + + """ + someone who is no longer a student. + Parameters: INDEX (must be a positive integer) + Note: INDEX refers to the index number of the person in the last displayed person list. It must""" + + """ + be a positive integer, i.e., 1, 2, 3, ... + Example: %s 2 + """, + COMMAND_WORD, COMMAND_WORD); + + public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted person:\n%1$s"; private final Index targetIndex; @@ -41,7 +48,9 @@ public CommandResult execute(Model model) throws CommandException { } Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); + model.deletePersonFromAllGroups(personToDelete); model.deletePerson(personToDelete); + return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(personToDelete))); } diff --git a/src/main/java/seedu/address/logic/commands/DeleteGroupCommand.java b/src/main/java/seedu/address/logic/commands/DeleteGroupCommand.java new file mode 100644 index 00000000000..ca1b370c131 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteGroupCommand.java @@ -0,0 +1,86 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.group.Group; + +/** + * Deletes an existing group identified using its displayed index. + */ +public class DeleteGroupCommand extends Command { + /** + * The command word to trigger this command. + */ + public static final String COMMAND_WORD = "delete-group"; + + public static final String MESSAGE_USAGE = String.format(""" + %s: Deletes the specified group from the group list. Useful for removing a tutorial group""" + + """ + that is no longer needed. + Parameters: INDEX (must be a positive integer) + Note: INDEX refers to the index number of the group in the last displayed group list.""" + + """ + It must be a positive integer, i.e., 1, 2, 3, ... + Example: %s 2 + """, + COMMAND_WORD, COMMAND_WORD); + + private static final String MESSAGE_DELETE_GROUP_SUCCESS = "Deleted group:\n%1$s"; + + /** + * Index of the group to be edited. + */ + private final Index targetIndex; + + /** + * Creates an DeleteGroupCommand to delete an existing group. + * + * @param targetIndex The index of the group to be deleted. + */ + public DeleteGroupCommand(Index targetIndex) { + requireNonNull(targetIndex); + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredGroupList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_GROUP_DISPLAYED_INDEX); + } + + Group groupToDelete = lastShownList.get(targetIndex.getZeroBased()); + model.deleteGroup(groupToDelete); + return new CommandResult(String.format(MESSAGE_DELETE_GROUP_SUCCESS, Messages.format(groupToDelete))); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof DeleteGroupCommand otherDeleteGroupCommand)) { + return false; + } + + return targetIndex.equals(otherDeleteGroupCommand.targetIndex); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("targetIndex", targetIndex) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeletePersonFromGroupCommand.java b/src/main/java/seedu/address/logic/commands/DeletePersonFromGroupCommand.java new file mode 100644 index 00000000000..893b7226149 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeletePersonFromGroupCommand.java @@ -0,0 +1,91 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_PERSON_NOT_IN_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON; + +import java.util.List; + +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.group.Group; +import seedu.address.model.person.Person; + +/** + * Removes a specific Person from a specified Group + */ +public class DeletePersonFromGroupCommand extends Command { + public static final String COMMAND_WORD = "delete-from-group"; + + public static final String MESSAGE_USAGE = String.format(""" + %s: Removes the specified person from the specified group. Useful for""" + + """ + removing a student who is no longer in the tutorial group. + Parameters: %s %sPERSON_NAME %sGROUP_NAME + Example: %s %sJensen Huang %sCS2103T T12 + """, + COMMAND_WORD, COMMAND_WORD, PREFIX_PERSON, PREFIX_GROUP, COMMAND_WORD, PREFIX_PERSON, PREFIX_GROUP); + + public static final String MESSAGE_SUCCESS = "Removed person from group:\n%1$s"; + /** + * Index of Person to be added + */ + private final String toDelete; + /** + * Index of Group to be added to + */ + private final String toBeDeletedFrom; + + /** + * Constructor for AddPersonToGroupCommand that takes two Index identifiers for + * Person and Group + * + * @param toDelete Index of Person to be added + * @param toBeDeletedFrom Index of Group to be added to + */ + public DeletePersonFromGroupCommand(String toDelete, String toBeDeletedFrom) { + requireNonNull(toDelete); + requireNonNull(toBeDeletedFrom); + this.toDelete = toDelete; + this.toBeDeletedFrom = toBeDeletedFrom; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List personList = model.getFilteredPersonList(); + List groupList = model.getFilteredGroupList(); + + Person personToDelete = null; + Group groupToBeDeletedFrom = null; + + for (Person person : personList) { + if (person.getName().fullName.equals(toDelete)) { + personToDelete = person; + } + } + for (Group group : groupList) { + if (group.getGroupName().equals(toBeDeletedFrom)) { + groupToBeDeletedFrom = group; + } + } + + if (personToDelete == null) { + throw new CommandException(Messages.MESSAGE_PERSON_NOT_FOUND); + } + + if (groupToBeDeletedFrom == null) { + throw new CommandException(Messages.MESSAGE_GROUP_NOT_FOUND); + } + + if (groupToBeDeletedFrom.contains(personToDelete)) { + model.deletePersonFromGroup(personToDelete, groupToBeDeletedFrom); + return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(personToDelete)), + true, groupToBeDeletedFrom); + } else { + throw new CommandException(MESSAGE_PERSON_NOT_IN_GROUP); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/EditAssignmentCommand.java b/src/main/java/seedu/address/logic/commands/EditAssignmentCommand.java new file mode 100644 index 00000000000..bf6afe4d13d --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/EditAssignmentCommand.java @@ -0,0 +1,132 @@ +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_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NEW_NAME; + +import java.time.LocalDate; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.assignment.exceptions.AssignmentNotFoundException; +import seedu.address.model.assignment.exceptions.DuplicateAssignmentException; +import seedu.address.model.group.Group; +import seedu.address.model.group.exceptions.GroupNotFoundException; + +/** + * Edits the specified assignment. + */ +public class EditAssignmentCommand extends Command { + + /** + * The command word to trigger this command. + */ + public static final String COMMAND_WORD = "edit-assignment"; + + public static final String MESSAGE_USAGE = String.format(""" + %s: Edits the assignment in the specified group. + Parameters: %sASSIGNMENT_NAME %sGROUP_NAME [%sNEW_NAME] [%sDEADLINE] + Example: %s %sHW 1 %sCS2103T T12 %sHW 1 %s21-04-2025 + """, + COMMAND_WORD, PREFIX_NAME, PREFIX_GROUP, PREFIX_NEW_NAME, PREFIX_DATE, + COMMAND_WORD, PREFIX_NAME, PREFIX_GROUP, PREFIX_NEW_NAME, PREFIX_DATE); + + public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; + private static final String MESSAGE_SUCCESS = "Assignment in group %s has been edited: %s"; + + /** + * Name of the assignment. + */ + private final String name; + + /** + * Name of the group the assignment belongs in. + */ + private final String groupName; + + /** + * New name of the assignment. + */ + private final String newName; + + /** + * New deadline of the assignment. + */ + private final LocalDate deadline; + + /** + * Late penalty for assignment. + */ + private final Float penalty; + + + /** + * Creates an EditAssignmentCommand to edit an assignment. + * + * @param name The name of the assignment to be added. + * @param groupName The name of the group to be added. + */ + public EditAssignmentCommand(String name, String groupName, String newName, LocalDate deadline, Float penalty) { + requireAllNonNull(name, groupName); + this.name = name; + this.groupName = groupName; + if (name.equals(newName)) { + this.newName = null; + } else { + this.newName = newName; + } + this.deadline = deadline; + this.penalty = penalty; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + Group group; + try { + group = model.getGroup(groupName); + } catch (GroupNotFoundException e) { + throw new CommandException("Group not found!"); + } + + try { + model.editAssignment(name, newName, deadline, group, penalty); + } catch (AssignmentNotFoundException e) { + throw new CommandException("Assignment not found!"); + } catch (DuplicateAssignmentException d) { + throw new CommandException(d.getMessage()); + } + + return new CommandResult(String.format(MESSAGE_SUCCESS, groupName, newName == null ? name : newName), + true, group); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditAssignmentCommand otherEditAssignmentCommand)) { + return false; + } + + return name.equals(otherEditAssignmentCommand.name) + && groupName.equals(otherEditAssignmentCommand.groupName); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("name", name) + .add("groupName", groupName) + .add("newName", newName) + .add("deadline", deadline) + .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..00fce92bf23 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -35,28 +35,29 @@ public class EditCommand extends Command { public static final String COMMAND_WORD = "edit"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the person identified " - + "by the index number used in the displayed person list. " - + "Existing values will be overwritten by the input values.\n" - + "Parameters: INDEX (must be a positive integer) " - + "[" + PREFIX_NAME + "NAME] " - + "[" + PREFIX_PHONE + "PHONE] " - + "[" + PREFIX_EMAIL + "EMAIL] " - + "[" + PREFIX_ADDRESS + "ADDRESS] " - + "[" + PREFIX_TAG + "TAG]...\n" - + "Example: " + COMMAND_WORD + " 1 " - + PREFIX_PHONE + "91234567 " - + PREFIX_EMAIL + "johndoe@example.com"; - - public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; + public static final String MESSAGE_USAGE = String.format(""" + %s: Edits the details of the specified person in the person list. + Parameters: INDEX [%sNAME] [%sPHONE] [%sEMAIL] [%sADDRESS] [%sTAG]... + Notes: + * INDEX refers to the index number of the person in the last displayed person list.""" + + """ + It must be a positive integer 1, 2, 3, + * Existing values will be updated to the input values. + Example: %s 1 %s91234567 %sjensenh@yahoo.com + """, + COMMAND_WORD, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG, COMMAND_WORD, + PREFIX_PHONE, PREFIX_EMAIL); + + public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited person:\n%1$s"; public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; + public static final String MESSAGE_DUPLICATE_PERSON = "Another person with the same name " + + "already exists in the address book!"; private final Index index; private final EditPersonDescriptor editPersonDescriptor; /** - * @param index of the person in the filtered person list to edit + * @param index of the person in the filtered person list to edit * @param editPersonDescriptor details to edit the person with */ public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { @@ -139,7 +140,8 @@ public static class EditPersonDescriptor { private Address address; private Set tags; - public EditPersonDescriptor() {} + public EditPersonDescriptor() { + } /** * Copy constructor. diff --git a/src/main/java/seedu/address/logic/commands/EditGroupCommand.java b/src/main/java/seedu/address/logic/commands/EditGroupCommand.java new file mode 100644 index 00000000000..95927f890ec --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/EditGroupCommand.java @@ -0,0 +1,145 @@ +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_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.group.Group; +import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; + +/** + * Represents a command that edits the details of an existing group in the address book. + */ +public class EditGroupCommand extends Command { + + /** + * The command word to trigger this command. + */ + public static final String COMMAND_WORD = "edit-group"; + + /** + * Usage message for the command. + */ + public static final String MESSAGE_USAGE = String.format(""" + %s: Edits the specified group details in the group list. + Parameters: INDEX [%sGROUP_NAME] [%sTAG]... + Notes: + * INDEX refers to the index number of the group in the last displayed group list.""" + + """ + It must be a positive integer 1, 2, 3, + * Existing values will be updated to the input values. + Example: %s 1 %sCS2103T T12 t/study + """, COMMAND_WORD, PREFIX_NAME, PREFIX_TAG, COMMAND_WORD, PREFIX_NAME); + + /** + * Success message for editing a group. + */ + public static final String MESSAGE_EDIT_GROUP_SUCCESS = "Edited Group: %1$s"; + + /** + * Error message if a duplicate group exists in the address book. + */ + public static final String MESSAGE_DUPLICATE_GROUP = "Another group with the same name " + + "already exists in the address book!"; + + /** + * Index of the group to be edited. + */ + private final Index index; + + /** + * New name for the group. + */ + private final String newGroupName; + + private final Set tags; + + /** + * Creates an EditGroupCommand to update a group's name. + * + * @param index The index of the group to be edited. + * @param newGroupName The new name for the group. + */ + public EditGroupCommand(Index index, String newGroupName, Collection tags) { + requireAllNonNull(index); + + this.index = index; + this.newGroupName = newGroupName; + this.tags = tags == null ? new HashSet<>() : new HashSet<>(tags); + } + + /** + * Executes the command to edit a group. + * + * @param model The model in which the command should be executed. + * @return A CommandResult indicating the outcome of the command execution. + * @throws CommandException If the index is invalid or if the new group name already exists. + */ + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredGroupList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException("Invalid Group"); + } + + Group groupToEdit = lastShownList.get(index.getZeroBased()); + String groupName = newGroupName == null ? groupToEdit.getGroupName() : newGroupName; + Group editedGroup = createEditedGroup(groupToEdit, groupName, tags); + + if (!groupToEdit.isSameGroup(editedGroup) && model.hasGroup(editedGroup)) { + throw new CommandException(MESSAGE_DUPLICATE_GROUP); + } + + model.setGroup(groupToEdit, editedGroup); + return new CommandResult(String.format(MESSAGE_EDIT_GROUP_SUCCESS, groupName), true, editedGroup); + } + + /** + * Creates a new Group object with the updated name while retaining the group members. + * + * @param groupToEdit The existing group to be edited. + * @param newGroupName The new name for the group. + * @return A new Group object with the updated name and existing members. + */ + private static Group createEditedGroup(Group groupToEdit, String newGroupName, Collection tags) { + assert groupToEdit != null; + + ArrayList list = groupToEdit.getGroupMembers(); + return groupToEdit.createEditedGroup(newGroupName, tags); + } + + /** + * Checks if this command is equal to another object. + * + * @param other The other object to compare to. + * @return True if both objects are EditGroupCommand instances with the same index and new group name. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles null cases + if (!(other instanceof EditGroupCommand otherCmd)) { + return false; + } + + return index.equals(otherCmd.index) + && newGroupName.equals(otherCmd.newGroupName) + && tags.equals(otherCmd.tags); + } +} diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index 72b9eddd3a7..ab8087074f6 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -15,10 +15,12 @@ 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 = String.format(""" + %s: Finds persons whose names contain any of the specified keywords. + Parameters: KEYWORD [MORE_KEYWORDS]... + Note: The keywords are case-insensitive. + Example: %s huang bezos + """, COMMAND_WORD, COMMAND_WORD); private final NameContainsKeywordsPredicate predicate; diff --git a/src/main/java/seedu/address/logic/commands/FindGroupCommand.java b/src/main/java/seedu/address/logic/commands/FindGroupCommand.java new file mode 100644 index 00000000000..b0e6f0c1683 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FindGroupCommand.java @@ -0,0 +1,60 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.model.Model; +import seedu.address.model.group.GroupNameContainsKeywordsPredicate; + +/** + * Finds and lists all groups in address book whose name contains any of the argument keywords. + * Keyword matching is case insensitive. + */ +public class FindGroupCommand extends Command { + + public static final String COMMAND_WORD = "find-group"; + + public static final String MESSAGE_USAGE = String.format(""" + %s: Finds groups whose names contain any of the specified keywords. + Parameters: KEYWORD [MORE_KEYWORDS]... + Note: The keywords are case-insensitive. + Example: %s t12 t13 + """, COMMAND_WORD, COMMAND_WORD); + + private final GroupNameContainsKeywordsPredicate predicate; + + public FindGroupCommand(GroupNameContainsKeywordsPredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredGroupList(predicate); + return new CommandResult( + String.format(Messages.MESSAGE_GROUPS_LISTED_OVERVIEW, model.getFilteredGroupList().size())); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof FindGroupCommand)) { + return false; + } + + FindGroupCommand otherFindCommand = (FindGroupCommand) other; + return predicate.equals(otherFindCommand.predicate); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("predicate", predicate) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/GradeAssignmentCommand.java b/src/main/java/seedu/address/logic/commands/GradeAssignmentCommand.java new file mode 100644 index 00000000000..db953cc78f4 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/GradeAssignmentCommand.java @@ -0,0 +1,93 @@ +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_ASSIGNMENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCORE; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.assignment.exceptions.AssignmentNotFoundException; +import seedu.address.model.group.Group; +import seedu.address.model.group.exceptions.GroupNotFoundException; +import seedu.address.model.person.Person; +import seedu.address.model.person.exceptions.PersonNotFoundException; + +/** + * Represent a command that will grade a student's assignment for that group + */ +public class GradeAssignmentCommand extends Command { + + public static final String COMMAND_WORD = "grade-assignment"; + + public static final String MESSAGE_USAGE = String.format(""" + %s: Grades the assignment submission by a person in a group. + Parameters: %sPERSON_NAME %sGROUP_NAME %sASSIGNMENT_NAME %sFLOAT_SCORE + Example: %s %sJensen Huang %sCS2103T T12 %sHW 1 %s70.3 + """, + COMMAND_WORD, PREFIX_PERSON, PREFIX_GROUP, PREFIX_ASSIGNMENT, PREFIX_SCORE, + COMMAND_WORD, PREFIX_PERSON, PREFIX_GROUP, PREFIX_ASSIGNMENT, PREFIX_SCORE); + + public static final String MESSAGE_GRADE_ASSIGNMENT_SUCCESS = "Graded Assignment %s for %s, %s with %f score"; + private final String personName; + private final String groupName; + private final String assignmentName; + private final Float score; + + /** + * Creates a {@code GradeAssignmentCommand} to grade student assignment. + * + * @param personName Name of the person. + * @param groupName Name of the group. + * @param assignmentName Name of the assignment. + * @param score Score that person obtained. + */ + public GradeAssignmentCommand(String personName, String groupName, String assignmentName, Float score) { + requireAllNonNull(personName, groupName, assignmentName, score); + this.personName = personName; + this.groupName = groupName; + this.assignmentName = assignmentName; + this.score = score; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + Person person; + try { + person = model.getPerson(personName); + } catch (PersonNotFoundException e) { + throw new CommandException("Person not found!"); + } + Group group; + try { + group = model.getGroup(groupName); + } catch (GroupNotFoundException e) { + throw new CommandException("Group not found!"); + } + try { + model.gradeAssignment(person, group, this.assignmentName, this.score); + } catch (AssignmentNotFoundException e) { + throw new CommandException("Assignment not in group!"); + } + Float finalScore = model.getGrade(person, group, this.assignmentName); + return new CommandResult(String.format(MESSAGE_GRADE_ASSIGNMENT_SUCCESS, assignmentName, personName, + groupName, finalScore)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof GradeAssignmentCommand otherCmd)) { + return false; + } + return personName.equals(otherCmd.personName) + && groupName.equals(otherCmd.groupName) + && assignmentName.equals(otherCmd.assignmentName) + && score.equals(otherCmd.score); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java index 84be6ad2596..465ff1cc058 100644 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListCommand.java @@ -12,7 +12,7 @@ public class ListCommand extends Command { public static final String COMMAND_WORD = "list"; - public static final String MESSAGE_SUCCESS = "Listed all persons"; + public static final String MESSAGE_SUCCESS = "Listed all persons."; @Override diff --git a/src/main/java/seedu/address/logic/commands/ListGroupCommand.java b/src/main/java/seedu/address/logic/commands/ListGroupCommand.java new file mode 100644 index 00000000000..465d95f6c0f --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ListGroupCommand.java @@ -0,0 +1,20 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_GROUPS; + +import seedu.address.model.Model; +/** + * Lists all groups in the address book to the user + */ +public class ListGroupCommand extends Command { + + public static final String COMMAND_WORD = "list-group"; + public static final String MESSAGE_SUCCESS = "Listed all groups."; + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredGroupList(PREDICATE_SHOW_ALL_GROUPS); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/MarkAttendanceCommand.java b/src/main/java/seedu/address/logic/commands/MarkAttendanceCommand.java new file mode 100644 index 00000000000..ad73e9facb3 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/MarkAttendanceCommand.java @@ -0,0 +1,131 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.Messages.MESSAGE_GROUP_NOT_FOUND; +import static seedu.address.logic.Messages.MESSAGE_INVALID_WEEK_NUM; +import static seedu.address.logic.Messages.MESSAGE_PERSON_NOT_FOUND; +import static seedu.address.logic.Messages.MESSAGE_PERSON_NOT_IN_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON; +import static seedu.address.logic.parser.CliSyntax.PREFIX_WEEK; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.group.Group; +import seedu.address.model.group.GroupMemberDetail; +import seedu.address.model.group.exceptions.GroupNotFoundException; +import seedu.address.model.person.Person; +import seedu.address.model.person.exceptions.PersonNotFoundException; + +/** + * Represents a command that marks a student's attendance for a particular group and week. + */ +public class MarkAttendanceCommand extends Command { + + /** + * The command word to trigger this command. + */ + public static final String COMMAND_WORD = "mark-attendance"; + + /** + * Usage message for the command. + */ + public static final String MESSAGE_USAGE = String.format(""" + %s: Marks the attendance of the specified person in the specified group for the specified week. + Parameters: %sPERSON_NAME %sGROUP_NAME %sWEEK_NUMBER + Example: %s %sJensen Huang %sCS2103T T12 %s10 + """, + COMMAND_WORD, PREFIX_PERSON, PREFIX_GROUP, PREFIX_WEEK, COMMAND_WORD, PREFIX_PERSON, + PREFIX_GROUP, PREFIX_WEEK); + + /** + * Success message for command. + */ + public static final String MESSAGE_MARK_ATTENDANCE_SUCCESS = "Marked attendance for %s!\nGroup: %s\nWeek %d"; + + + /** + * Group Member Detail object to be updated. + */ + private final String personName; + private final String groupName; + private final int week; + + + /** + * Creates a {@code MarkAttendanceCommand} to mark a student's attendance. + * + * @param personName The name of the student. + * @param groupName The name of the group the student belongs to. + * @param week The week number for which attendance is being marked. + */ + public MarkAttendanceCommand(String personName, String groupName, int week) { + requireAllNonNull(personName, groupName, week); + this.personName = personName; + this.groupName = groupName; + this.week = week; + } + + /** + * Executes the command to mark attendance. + * + * @param model The model in which the command should be executed. + * @return A CommandResult indicating the outcome of the command execution. + * @throws CommandException if week number is invalid + */ + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + Group group; + try { + group = model.getGroup(groupName); + } catch (GroupNotFoundException e) { + throw new CommandException(MESSAGE_GROUP_NOT_FOUND); + } + + Person person; + try { + person = model.getPerson(personName); + } catch (PersonNotFoundException e) { + throw new CommandException(MESSAGE_PERSON_NOT_FOUND); + } + + if (!GroupMemberDetail.isValidWeek(week)) { + throw new CommandException(MESSAGE_INVALID_WEEK_NUM); + } + + try { + model.markAttendance(person, group, week); + } catch (PersonNotFoundException e) { + throw new CommandException(MESSAGE_PERSON_NOT_IN_GROUP); + } + + return new CommandResult(String.format(MESSAGE_MARK_ATTENDANCE_SUCCESS, personName, groupName, week), + true, group); + } + + + /** + * Checks if this command is equal to another object. + * + * @param other The other object to compare to. + * @return True if both objects are MarkAttendanceCommand instances with the same GroupMemberDetail and week number. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles null cases + if (!(other instanceof MarkAttendanceCommand otherCmd)) { + return false; + } + + return personName.equals(otherCmd.personName) + && groupName.equals(otherCmd.groupName) + && week == otherCmd.week; + } +} diff --git a/src/main/java/seedu/address/logic/commands/ShowAttendanceCommand.java b/src/main/java/seedu/address/logic/commands/ShowAttendanceCommand.java new file mode 100644 index 00000000000..e262f5233de --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ShowAttendanceCommand.java @@ -0,0 +1,128 @@ +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_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.group.Group; +import seedu.address.model.group.GroupMemberDetail; +import seedu.address.model.group.exceptions.GroupNotFoundException; +import seedu.address.model.person.Person; +import seedu.address.model.person.exceptions.PersonNotFoundException; + +/** + * Represents a command that shows a student's attendance for a particular group. + */ +public class ShowAttendanceCommand extends Command { + + /** + * The command word to trigger this command. + */ + public static final String COMMAND_WORD = "show-attendance"; + + /** + * Usage message for the command. + */ + public static final String MESSAGE_USAGE = String.format(""" + %s: Displays the attendance record of the specified person in the specified group. + Parameters: %sPERSON_NAME %sGROUP_NAME + Example: %s %sJensen Huang %sCS2103T T12 + """, + COMMAND_WORD, PREFIX_PERSON, PREFIX_GROUP, COMMAND_WORD, PREFIX_PERSON, PREFIX_GROUP); + + /** + * Success message for command. + */ + public static final String MESSAGE_SHOW_ATTENDANCE_SUCCESS = "Showing attendance for %s in %s"; + + private final String personName; + private final String groupName; + + /** + * Creates a {@code ShowAttendanceCommand} to show a student's attendance. + * + * @param personName The name of the student. + * @param groupName The name of the group the student belongs to. + */ + public ShowAttendanceCommand(String personName, String groupName) { + requireAllNonNull(personName, groupName); + this.personName = personName; + this.groupName = groupName; + } + + /** + * Executes the command to show attendance. + * + * @param model The model in which the command should be executed. + * @return A CommandResult indicating the outcome of the command execution. + * @throws CommandException if group or person is not found, or person is not in the group + */ + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + Group group; + try { + group = model.getGroup(groupName); + } catch (GroupNotFoundException e) { + throw new CommandException("Group not found!"); + } + + Person person; + try { + person = model.getPerson(personName); + } catch (PersonNotFoundException e) { + throw new CommandException("Person not found!"); + } + + if (!group.contains(person)) { + throw new CommandException("Person does not exist in group!"); + } + + GroupMemberDetail groupMemberDetail = group.getGroupMemberDetail(person); + boolean[] attendance = groupMemberDetail.getAttendance(); + + // Count present weeks for the feedback message + int presentCount = 0; + for (boolean present : attendance) { + if (present) { + presentCount++; + } + } + + // Create a detailed text representation for display + StringBuilder sb = new StringBuilder(); + sb.append(String.format("Attendance for %s in %s:\n", personName, groupName)); + sb.append(String.format("Total attendance: %d/%d weeks\n\n", presentCount, attendance.length)); + + for (int i = 0; i < attendance.length; i++) { + sb.append(String.format("Week %d: %s\n", i + 1, attendance[i] ? "Present" : "Absent")); + } + + return new CommandResult(sb.toString()); + } + + /** + * Checks if this command is equal to another object. + * + * @param other The other object to compare to. + * @return True if both objects are ShowAttendanceCommand instances with the same person and group names. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles null cases + if (!(other instanceof ShowAttendanceCommand otherCmd)) { + return false; + } + + return personName.equals(otherCmd.personName) + && groupName.equals(otherCmd.groupName); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ShowGroupDetailsCommand.java b/src/main/java/seedu/address/logic/commands/ShowGroupDetailsCommand.java new file mode 100644 index 00000000000..e43a2829b79 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ShowGroupDetailsCommand.java @@ -0,0 +1,62 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.group.Group; + +/** + * Shows the main details regarding a {@code Group}. + */ +public class ShowGroupDetailsCommand extends Command { + + public static final String COMMAND_WORD = "show-group-details"; + + public static final String MESSAGE_USAGE = String.format(""" + %s: Shows all the core details regarding the specified group. + Parameters: INDEX + Notes: + * Shows details including: + * Group name and tags + * Number of group members + * Name, role, and attendance of every group member + * INDEX refers to the index number of the group in the last displayed group list.""" + + """ + It must be a positive integer, i.e., 1, 2, 3, ... + Example: %s 2 + """, COMMAND_WORD, COMMAND_WORD); + + private static final String MESSAGE_SHOW_GROUP_DETAILS_SUCCESS = "Showing details for group:\n%1$s"; + + private final Index targetIndex; + + /** + * Creates a new ShowGroupDetailsCommand. + * + * @param targetIndex The index of the group to show. This refers to the index in the last displayed group list. + */ + public ShowGroupDetailsCommand(Index targetIndex) { + requireNonNull(targetIndex); + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredGroupList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_GROUP_DISPLAYED_INDEX); + } + + Group groupToShow = lastShownList.get(targetIndex.getZeroBased()); + model.showGroupDetails(groupToShow); + return new CommandResult(String.format(MESSAGE_SHOW_GROUP_DETAILS_SUCCESS, Messages.format(groupToShow)), + true, groupToShow); + } +} diff --git a/src/main/java/seedu/address/logic/commands/UnmarkAttendanceCommand.java b/src/main/java/seedu/address/logic/commands/UnmarkAttendanceCommand.java new file mode 100644 index 00000000000..312cbc50c01 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/UnmarkAttendanceCommand.java @@ -0,0 +1,134 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.Messages.MESSAGE_GROUP_NOT_FOUND; +import static seedu.address.logic.Messages.MESSAGE_INVALID_WEEK_NUM; +import static seedu.address.logic.Messages.MESSAGE_PERSON_NOT_FOUND; +import static seedu.address.logic.Messages.MESSAGE_PERSON_NOT_IN_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON; +import static seedu.address.logic.parser.CliSyntax.PREFIX_WEEK; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.group.Group; +import seedu.address.model.group.GroupMemberDetail; +import seedu.address.model.group.exceptions.GroupNotFoundException; +import seedu.address.model.person.Person; +import seedu.address.model.person.exceptions.PersonNotFoundException; + +/** + * Represents a command that unmarks a student's attendance for a particular group and week. + */ +public class UnmarkAttendanceCommand extends Command { + + /** + * The command word to trigger this command. + */ + public static final String COMMAND_WORD = "unmark-attendance"; + + /** + * Usage message for the command. + */ + public static final String MESSAGE_USAGE = String.format(""" + %s: Removes the attendance record of the specified person in the""" + + """ + specified group for the specified week. + Parameters: %sPERSON_NAME %sGROUP_NAME %sWEEK_NUMBER + Example: %s %sJensen Huang %sCS2103T T12 %s10 + """, + COMMAND_WORD, PREFIX_PERSON, PREFIX_GROUP, PREFIX_WEEK, COMMAND_WORD, PREFIX_PERSON, + PREFIX_GROUP, PREFIX_WEEK); + + + /** + * Success message for command. + */ + public static final String MESSAGE_UNMARK_ATTENDANCE_SUCCESS = "Unmarked attendance for %s, %s, week %d"; + + + /** + * Group Member Detail object to be updated. + */ + private final String personName; + private final String groupName; + private final int week; + + + /** + * Creates a {@code M=UnmarkAttendanceCommand} to unmark a student's attendance. + * + * @param personName The name of the student. + * @param groupName The name of the group the student belongs to. + * @param week The week number for which attendance is being unmarked. + */ + public UnmarkAttendanceCommand(String personName, String groupName, int week) { + requireAllNonNull(personName, groupName, week); + this.personName = personName; + this.groupName = groupName; + this.week = week; + } + + /** + * Executes the command to unmark attendance. + * + * @param model The model in which the command should be executed. + * @return A CommandResult indicating the outcome of the command execution. + * @throws CommandException if week number is invalid + */ + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + Group group; + try { + group = model.getGroup(groupName); + } catch (GroupNotFoundException e) { + throw new CommandException(MESSAGE_GROUP_NOT_FOUND); + } + + Person person; + try { + person = model.getPerson(personName); + } catch (PersonNotFoundException e) { + throw new CommandException(MESSAGE_PERSON_NOT_FOUND); + } + + if (!GroupMemberDetail.isValidWeek(week)) { + throw new CommandException(MESSAGE_INVALID_WEEK_NUM); + } + + try { + model.unmarkAttendance(person, group, week); + } catch (PersonNotFoundException e) { + throw new CommandException(MESSAGE_PERSON_NOT_IN_GROUP); + } + + return new CommandResult(String.format(MESSAGE_UNMARK_ATTENDANCE_SUCCESS, personName, groupName, week), + true, group); + } + + + /** + * Checks if this command is equal to another object. + * + * @param other The other object to compare to. + * @return True if both objects are equal. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles null cases + if (!(other instanceof UnmarkAttendanceCommand otherCmd)) { + return false; + } + + return personName.equals(otherCmd.personName) + && groupName.equals(otherCmd.groupName) + && week == otherCmd.week; + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddAssignmentCommandParser.java b/src/main/java/seedu/address/logic/parser/AddAssignmentCommandParser.java new file mode 100644 index 00000000000..f1a4b923ea2 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddAssignmentCommandParser.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_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LATE_PENALTY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; + +import java.time.LocalDate; +import java.util.stream.Stream; + +import seedu.address.logic.commands.AddAssignmentCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new AddAssignmentCommand object. + */ +public class AddAssignmentCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddAssignmentCommand + * and returns an AddAssignmentCommand object for execution. + * @throws ParseException if the user input does not conform to the expected format + */ + public AddAssignmentCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_GROUP, + PREFIX_DATE); + + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_GROUP, PREFIX_DATE) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AddAssignmentCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_GROUP, PREFIX_DATE, PREFIX_LATE_PENALTY); + String assignmentName = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()).toString(); + String groupName = ParserUtil.parseGroupName(argMultimap.getValue(PREFIX_GROUP).get()); + LocalDate deadline = ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).get()); + Float penalty = null; + if (argMultimap.getValue(PREFIX_LATE_PENALTY).isPresent()) { + penalty = Float.parseFloat(argMultimap.getValue(PREFIX_LATE_PENALTY).get()); + } + + return new AddAssignmentCommand(assignmentName, groupName, deadline, penalty); + } + + /** + * 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/AddGroupCommandParser.java b/src/main/java/seedu/address/logic/parser/AddGroupCommandParser.java new file mode 100644 index 00000000000..3b9d1d86fd8 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddGroupCommandParser.java @@ -0,0 +1,46 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Set; +import java.util.stream.Stream; + +import seedu.address.logic.commands.AddGroupCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new AddGroupCommand object. + */ +public class AddGroupCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddGroupCommand + * and returns an AddGroupCommand object for execution. + * @throws ParseException if the user input does not conform to the expected format + */ + public AddGroupCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_TAG); + + if (!arePrefixesPresent(argMultimap, PREFIX_NAME) || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AddGroupCommand.MESSAGE_USAGE)); + } + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME); + + String groupName = ParserUtil.parseGroupName(argMultimap.getValue(PREFIX_NAME).get()); + Set taglist = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); + + return new AddGroupCommand(groupName, taglist); + } + + /** + * 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/AddPersonToGroupCommandParser.java b/src/main/java/seedu/address/logic/parser/AddPersonToGroupCommandParser.java new file mode 100644 index 00000000000..a07701a748d --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddPersonToGroupCommandParser.java @@ -0,0 +1,43 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON; + +import java.util.stream.Stream; + +import seedu.address.logic.commands.AddPersonToGroupCommand; +import seedu.address.logic.parser.exceptions.ParseException; +/** + * Parses input arguments and create a new AddPersonToGroupCommand + */ +public class AddPersonToGroupCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of AddPersonToGroupCommand + * and returns a AddPersonToGroupCommand object for execution + */ + @Override + public AddPersonToGroupCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_PERSON, PREFIX_GROUP); + + if (!arePrefixesPresent(argMultimap, PREFIX_PERSON, PREFIX_GROUP) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AddPersonToGroupCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_PERSON, PREFIX_GROUP); + String personName = ParserUtil.parseName(argMultimap.getValue(PREFIX_PERSON).get()).toString(); + String groupName = ParserUtil.parseGroupName(argMultimap.getValue(PREFIX_GROUP).get()); + + return new AddPersonToGroupCommand(personName, groupName); + } + /** + * 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..9118c412b5c 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -8,15 +8,29 @@ import java.util.regex.Pattern; import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.commands.AddAssignmentCommand; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddGroupCommand; +import seedu.address.logic.commands.AddPersonToGroupCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.DeleteAssignmentCommand; import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.DeleteGroupCommand; +import seedu.address.logic.commands.DeletePersonFromGroupCommand; +import seedu.address.logic.commands.EditAssignmentCommand; import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.EditGroupCommand; import seedu.address.logic.commands.ExitCommand; import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.FindGroupCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.ListGroupCommand; +import seedu.address.logic.commands.MarkAttendanceCommand; +import seedu.address.logic.commands.ShowAttendanceCommand; +import seedu.address.logic.commands.ShowGroupDetailsCommand; +import seedu.address.logic.commands.UnmarkAttendanceCommand; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -77,6 +91,48 @@ public Command parseCommand(String userInput) throws ParseException { case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case EditGroupCommand.COMMAND_WORD: + return new EditGroupCommandParser().parse(arguments); + + case ListGroupCommand.COMMAND_WORD: + return new ListGroupCommand(); + + case DeleteGroupCommand.COMMAND_WORD: + return new DeleteGroupCommandParser().parse(arguments); + + case ShowGroupDetailsCommand.COMMAND_WORD: + return new ShowGroupDetailsCommandParser().parse(arguments); + + case AddGroupCommand.COMMAND_WORD: + return new AddGroupCommandParser().parse(arguments); + + case AddPersonToGroupCommand.COMMAND_WORD: + return new AddPersonToGroupCommandParser().parse(arguments); + + case DeletePersonFromGroupCommand.COMMAND_WORD: + return new DeletePersonFromGroupCommandParser().parse(arguments); + + case FindGroupCommand.COMMAND_WORD: + return new FindGroupCommandParser().parse(arguments); + + case MarkAttendanceCommand.COMMAND_WORD: + return new MarkAttendanceCommandParser().parse(arguments); + + case UnmarkAttendanceCommand.COMMAND_WORD: + return new UnmarkAttendanceCommandParser().parse(arguments); + + case ShowAttendanceCommand.COMMAND_WORD: + return new ShowAttendanceCommandParser().parse(arguments); + + case AddAssignmentCommand.COMMAND_WORD: + return new AddAssignmentCommandParser().parse(arguments); + + case DeleteAssignmentCommand.COMMAND_WORD: + return new DeleteAssignmentCommandParser().parse(arguments); + + case EditAssignmentCommand.COMMAND_WORD: + return new EditAssignmentCommandParser().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/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..28cfbe5423d 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -11,5 +11,12 @@ public class CliSyntax { public static final Prefix PREFIX_EMAIL = new Prefix("e/"); public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); public static final Prefix PREFIX_TAG = new Prefix("t/"); - + public static final Prefix PREFIX_GROUP = new Prefix("g/"); + public static final Prefix PREFIX_PERSON = PREFIX_NAME; // Alias for person name + public static final Prefix PREFIX_WEEK = new Prefix("w/"); + public static final Prefix PREFIX_ASSIGNMENT = new Prefix("A/"); + public static final Prefix PREFIX_SCORE = new Prefix("s/"); + public static final Prefix PREFIX_DATE = new Prefix("d/"); + public static final Prefix PREFIX_NEW_NAME = new Prefix("N/"); + public static final Prefix PREFIX_LATE_PENALTY = new Prefix("l/"); } diff --git a/src/main/java/seedu/address/logic/parser/DeleteAssignmentCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteAssignmentCommandParser.java new file mode 100644 index 00000000000..76f8591fc51 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteAssignmentCommandParser.java @@ -0,0 +1,47 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; + +import java.util.stream.Stream; + +import seedu.address.logic.commands.DeleteAssignmentCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new DeleteAssignmentCommand object. + */ +public class DeleteAssignmentCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddAssignmentCommand + * and returns an AddAssignmentCommand object for execution. + * @throws ParseException if the user input does not conform to the expected format + */ + public DeleteAssignmentCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_GROUP); + + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_GROUP) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + DeleteAssignmentCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_GROUP); + String assignmentName = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()).toString(); + String groupName = ParserUtil.parseName(argMultimap.getValue(PREFIX_GROUP).get()).toString(); + + return new DeleteAssignmentCommand(assignmentName, groupName); + } + + /** + * 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/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java index 3527fe76a3e..e72ac652e54 100644 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java @@ -1,7 +1,5 @@ package seedu.address.logic.parser; -import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; - import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.parser.exceptions.ParseException; @@ -22,7 +20,7 @@ public DeleteCommand parse(String args) throws ParseException { return new DeleteCommand(index); } catch (ParseException pe) { throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); + pe.getMessage() + "\n" + DeleteCommand.MESSAGE_USAGE, pe); } } diff --git a/src/main/java/seedu/address/logic/parser/DeleteGroupCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteGroupCommandParser.java new file mode 100644 index 00000000000..687d09678be --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteGroupCommandParser.java @@ -0,0 +1,29 @@ +package seedu.address.logic.parser; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.DeleteGroupCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new DeleteGroupCommand object + */ +public class DeleteGroupCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the {@code DeleteGroupCommand} + * and returns an {@code DeleteGroupCommand} object for execution. + * + * @param args The user input arguments as a {@code String}. + * @return An {@code DeleteGroupCommand} object containing the parsed index. + * @throws ParseException If the user input does not conform to the expected format. + */ + @Override + public DeleteGroupCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteGroupCommand(index); + } catch (ParseException pe) { + throw new ParseException( + pe.getMessage() + "\n" + DeleteGroupCommand.MESSAGE_USAGE, pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/DeletePersonFromGroupCommandParser.java b/src/main/java/seedu/address/logic/parser/DeletePersonFromGroupCommandParser.java new file mode 100644 index 00000000000..8812bffca7c --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeletePersonFromGroupCommandParser.java @@ -0,0 +1,44 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON; + +import java.util.stream.Stream; + +import seedu.address.logic.commands.DeletePersonFromGroupCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new DeletePersonFromGroupCommand object + */ +public class DeletePersonFromGroupCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeletePersonFromGroupCommand + * and returns a DeletePersonFromGroupCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + @Override + public DeletePersonFromGroupCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_PERSON, PREFIX_GROUP); + + if (!arePrefixesPresent(argMultimap, PREFIX_PERSON, PREFIX_GROUP) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + DeletePersonFromGroupCommand.MESSAGE_USAGE)); + } + + String personName = argMultimap.getValue(PREFIX_PERSON).get(); + String groupName = argMultimap.getValue(PREFIX_GROUP).get(); + + return new DeletePersonFromGroupCommand(personName, groupName); + } + /** + * 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/EditAssignmentCommandParser.java b/src/main/java/seedu/address/logic/parser/EditAssignmentCommandParser.java new file mode 100644 index 00000000000..bec670f42b3 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/EditAssignmentCommandParser.java @@ -0,0 +1,66 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LATE_PENALTY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NEW_NAME; + +import java.time.LocalDate; +import java.util.stream.Stream; + +import seedu.address.commons.util.CollectionUtil; +import seedu.address.logic.commands.EditAssignmentCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new EditAssignmentCommand object. + */ +public class EditAssignmentCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the EditAssignmentCommand + * and returns an EditAssignmentCommand object for execution. + * @throws ParseException if the user input does not conform to the expected format + */ + public EditAssignmentCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_GROUP, PREFIX_NEW_NAME, + PREFIX_DATE); + + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_GROUP) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + EditAssignmentCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_GROUP, PREFIX_NEW_NAME, PREFIX_DATE); + String assignmentName = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()).toString(); + String groupName = ParserUtil.parseName(argMultimap.getValue(PREFIX_GROUP).get()).toString(); + String newName = null; + if (argMultimap.getValue(PREFIX_NEW_NAME).isPresent()) { + newName = ParserUtil.parseName(argMultimap.getValue(PREFIX_NEW_NAME).get()).toString(); + } + LocalDate deadline = null; + if (argMultimap.getValue(PREFIX_DATE).isPresent()) { + deadline = ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).get()); + } + Float penalty = null; + if (argMultimap.getValue(PREFIX_LATE_PENALTY).isPresent()) { + penalty = Float.parseFloat(argMultimap.getValue(PREFIX_LATE_PENALTY).get()); + } + if (!CollectionUtil.isAnyNonNull(newName, deadline, penalty)) { + throw new ParseException(EditAssignmentCommand.MESSAGE_NOT_EDITED); + } + + return new EditAssignmentCommand(assignmentName, groupName, newName, deadline, penalty); + } + + /** + * 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/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 46b3309a78b..91b8ef7842a 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -1,23 +1,16 @@ 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_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import java.util.Collection; -import java.util.Collections; -import java.util.Optional; -import java.util.Set; - import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.tag.Tag; /** * Parses input arguments and creates a new EditCommand object @@ -27,6 +20,7 @@ public class EditCommandParser implements Parser { /** * Parses the given {@code String} of arguments in the context of the EditCommand * and returns an EditCommand object for execution. + * * @throws ParseException if the user input does not conform the expected format */ public EditCommand parse(String args) throws ParseException { @@ -39,7 +33,7 @@ public EditCommand parse(String args) throws ParseException { try { index = ParserUtil.parseIndex(argMultimap.getPreamble()); } catch (ParseException pe) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); + throw new ParseException(pe.getMessage() + "\n" + EditCommand.MESSAGE_USAGE, pe); } argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); @@ -58,7 +52,7 @@ public EditCommand parse(String args) throws ParseException { if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); } - parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); + ParserUtil.parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); if (!editPersonDescriptor.isAnyFieldEdited()) { throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); @@ -66,20 +60,4 @@ public EditCommand parse(String args) throws ParseException { return new EditCommand(index, editPersonDescriptor); } - - /** - * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. - * If {@code tags} contain only one element which is an empty string, it will be parsed into a - * {@code Set} containing zero tags. - */ - private Optional> parseTagsForEdit(Collection tags) throws ParseException { - assert tags != null; - - if (tags.isEmpty()) { - return Optional.empty(); - } - Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; - return Optional.of(ParserUtil.parseTags(tagSet)); - } - } diff --git a/src/main/java/seedu/address/logic/parser/EditGroupCommandParser.java b/src/main/java/seedu/address/logic/parser/EditGroupCommandParser.java new file mode 100644 index 00000000000..286fb7f58b8 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/EditGroupCommandParser.java @@ -0,0 +1,59 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Collection; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.EditGroupCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new {@code EditGroupCommand} object. + */ +public class EditGroupCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the + * {@code EditGroupCommand} + * and returns an {@code EditGroupCommand} object for execution. + * + * @param args The user input arguments as a {@code String}. + * @return An {@code EditGroupCommand} object containing the parsed index and + * new group name. + * @throws ParseException If the user input does not conform to the expected + * format. + */ + public EditGroupCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_TAG); + + Index index; + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (IllegalValueException ive) { + throw new ParseException(String.format("%s\n%s", ive.getMessage(), + EditGroupCommand.MESSAGE_USAGE), ive); + } + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME); + + String newGroupName = null; + if (argMultimap.getValue(PREFIX_NAME).isPresent()) { + newGroupName = ParserUtil.parseGroupName(argMultimap.getValue(PREFIX_NAME).get()); + } + + Collection tags = null; + if (argMultimap.getValue(PREFIX_TAG).isPresent()) { + tags = ParserUtil.parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).get(); + } + if (newGroupName == null && tags == null) { + throw new ParseException("At least one field to edit must be provided."); // EditCommand.MESSAGE_NOT_EDITED + } + + return new EditGroupCommand(index, newGroupName, tags); + } +} diff --git a/src/main/java/seedu/address/logic/parser/FindGroupCommandParser.java b/src/main/java/seedu/address/logic/parser/FindGroupCommandParser.java new file mode 100644 index 00000000000..5ebf7401ded --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FindGroupCommandParser.java @@ -0,0 +1,33 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.Arrays; + +import seedu.address.logic.commands.FindGroupCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.group.GroupNameContainsKeywordsPredicate; + +/** + * Parses input arguments and creates a new FindGroupCommand object + */ +public class FindGroupCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the FindGroupCommand + * and returns a FindGroupCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public FindGroupCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindGroupCommand.MESSAGE_USAGE)); + } + + String[] groupNameKeywords = trimmedArgs.split("\\s+"); + + return new FindGroupCommand(new GroupNameContainsKeywordsPredicate(Arrays.asList(groupNameKeywords))); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/GradeAssignmentCommandParser.java b/src/main/java/seedu/address/logic/parser/GradeAssignmentCommandParser.java new file mode 100644 index 00000000000..1313ac63666 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/GradeAssignmentCommandParser.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_ASSIGNMENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCORE; + +import java.util.stream.Stream; + +import seedu.address.logic.commands.GradeAssignmentCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parse input and create a new {@code GradeAssignmentCommand} class + */ +public class GradeAssignmentCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the {@code GradeAssignmentCommand} + * and returns an {@code GradeAssignmentCommand} object for execution. + * + * @param args The user input arguments as a {@code String}. + * @return An {@code GradeAssignmentCommand} object containing the parsed arguments. + * @throws ParseException If the user input does not conform to the expected format. + */ + public GradeAssignmentCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_PERSON, PREFIX_GROUP, + PREFIX_ASSIGNMENT, PREFIX_SCORE); + if (!arePrefixesPresent(argMultimap, PREFIX_PERSON, PREFIX_GROUP, PREFIX_ASSIGNMENT, PREFIX_SCORE) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + GradeAssignmentCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_PERSON, PREFIX_GROUP, PREFIX_ASSIGNMENT, PREFIX_SCORE); + String personName = ParserUtil.parseName(argMultimap.getValue(PREFIX_PERSON).get()).toString(); + String groupName = ParserUtil.parseGroupName(argMultimap.getValue(PREFIX_GROUP).get()); + String assignmentName = ParserUtil.parseName(argMultimap.getValue(PREFIX_ASSIGNMENT).get()).toString(); + Float score = Float.parseFloat(argMultimap.getValue(PREFIX_SCORE).get()); + return new GradeAssignmentCommand(personName, groupName, assignmentName, score); + + } + /** + * 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/MarkAttendanceCommandParser.java b/src/main/java/seedu/address/logic/parser/MarkAttendanceCommandParser.java new file mode 100644 index 00000000000..b9b3173f746 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/MarkAttendanceCommandParser.java @@ -0,0 +1,52 @@ +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_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON; +import static seedu.address.logic.parser.CliSyntax.PREFIX_WEEK; + +import java.util.stream.Stream; + +import seedu.address.logic.commands.MarkAttendanceCommand; +import seedu.address.logic.parser.exceptions.ParseException; + + +/** + * Parses input arguments and creates a new {@code MarkAttendanceCommand} object. + */ +public class MarkAttendanceCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the {@code MarkAttendance} + * and returns an {@code MarkAttendance} object for execution. + * + * @param args The user input arguments as a {@code String}. + * @return An {@code MarkAttendanceCommand} object containing the parsed arguments. + * @throws ParseException If the user input does not conform to the expected format. + */ + public MarkAttendanceCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_PERSON, PREFIX_GROUP, PREFIX_WEEK); + + if (!arePrefixesPresent(argMultimap, PREFIX_PERSON, PREFIX_GROUP, PREFIX_WEEK) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + MarkAttendanceCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_PERSON, PREFIX_GROUP, PREFIX_WEEK); + String personName = ParserUtil.parseName(argMultimap.getValue(PREFIX_PERSON).get()).toString(); + String groupName = ParserUtil.parseGroupName(argMultimap.getValue(PREFIX_GROUP).get()); + int week = Integer.parseInt(argMultimap.getValue(PREFIX_WEEK).get()); + return new MarkAttendanceCommand(personName, groupName, week); + } + + /** + * 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/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..84c8978d5be 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -2,13 +2,19 @@ import static java.util.Objects.requireNonNull; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; +import java.util.Optional; 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.group.Group; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; @@ -20,7 +26,8 @@ */ public class ParserUtil { - public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; + public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer!"; + public static final String MESSAGE_INVALID_DATE = "Date is not in dd-MM-yyyy format."; /** * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be @@ -50,6 +57,21 @@ public static Name parseName(String name) throws ParseException { return new Name(trimmedName); } + /** + * Parses the String group name and validated it. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given String violated Message Constraints for Group Name + */ + public static String parseGroupName(String name) throws ParseException { + requireNonNull(name); + String trimmedName = name.trim(); + if (!Group.isValidGroupName(trimmedName)) { + throw new ParseException(Group.MESSAGE_CONSTRAINTS); + } + return trimmedName; + } + /** * Parses a {@code String phone} into a {@code Phone}. * Leading and trailing whitespaces will be trimmed. @@ -121,4 +143,36 @@ public static Set parseTags(Collection tags) throws ParseException } return tagSet; } + + /** + * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. + * If {@code tags} contain only one element which is an empty string, it will be parsed into a + * {@code Set} containing zero tags. + */ + public static Optional> parseTagsForEdit(Collection tags) throws ParseException { + assert tags != null; + + if (tags.isEmpty()) { + return Optional.empty(); + } + Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; + return Optional.of(ParserUtil.parseTags(tagSet)); + } + + /** + * Parses {@code String} into a {@code Date}. + * The format used is dd-MM-yyyy. + * + * @param dateString + */ + public static LocalDate parseDate(String dateString) throws ParseException { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + LocalDate date; + try { + date = LocalDate.parse(dateString, formatter); + } catch (DateTimeParseException e) { + throw new ParseException(MESSAGE_INVALID_DATE); + } + return date; + } } diff --git a/src/main/java/seedu/address/logic/parser/ShowAttendanceCommandParser.java b/src/main/java/seedu/address/logic/parser/ShowAttendanceCommandParser.java new file mode 100644 index 00000000000..2b42f1486ee --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ShowAttendanceCommandParser.java @@ -0,0 +1,45 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON; + +import java.util.stream.Stream; + +import seedu.address.logic.commands.ShowAttendanceCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ShowAttendanceCommand object + */ +public class ShowAttendanceCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the ShowAttendanceCommand + * and returns a ShowAttendanceCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ShowAttendanceCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_PERSON, PREFIX_GROUP); + + if (!arePrefixesPresent(argMultimap, PREFIX_PERSON, PREFIX_GROUP) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + ShowAttendanceCommand.MESSAGE_USAGE)); + } + + String personName = ParserUtil.parseName(argMultimap.getValue(PREFIX_PERSON).get()).toString(); + String groupName = ParserUtil.parseGroupName(argMultimap.getValue(PREFIX_GROUP).get()); + + return new ShowAttendanceCommand(personName, groupName); + } + + /** + * 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/ShowGroupDetailsCommandParser.java b/src/main/java/seedu/address/logic/parser/ShowGroupDetailsCommandParser.java new file mode 100644 index 00000000000..441f0e70b71 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ShowGroupDetailsCommandParser.java @@ -0,0 +1,29 @@ +package seedu.address.logic.parser; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.ShowGroupDetailsCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new {@code ShowGroupDetailsCommand} object + */ +public class ShowGroupDetailsCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the {@code ShowGroupDetailsCommand} + * and returns an {@code ShowGroupDetailsCommand} object for execution. + * + * @param args The user input arguments as a {@code String}. + * @return An {@code ShowGroupDetailsCommand} object containing the parsed index. + * @throws ParseException If the user input does not conform to the expected format. + */ + @Override + public ShowGroupDetailsCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new ShowGroupDetailsCommand(index); + } catch (ParseException pe) { + throw new ParseException( + pe.getMessage() + "\n" + ShowGroupDetailsCommand.MESSAGE_USAGE, pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/UnmarkAttendanceCommandParser.java b/src/main/java/seedu/address/logic/parser/UnmarkAttendanceCommandParser.java new file mode 100644 index 00000000000..fd6e38de2ee --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/UnmarkAttendanceCommandParser.java @@ -0,0 +1,52 @@ +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_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON; +import static seedu.address.logic.parser.CliSyntax.PREFIX_WEEK; + +import java.util.stream.Stream; + +import seedu.address.logic.commands.UnmarkAttendanceCommand; +import seedu.address.logic.parser.exceptions.ParseException; + + +/** + * Parses input arguments and creates a new {@code UnmarkAttendanceCommand} object. + */ +public class UnmarkAttendanceCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the {@code UnmarkAttendance} + * and returns an {@code UnmarkAttendance} object for execution. + * + * @param args The user input arguments as a {@code String}. + * @return An {@code UnmarkAttendanceCommand} object containing the parsed arguments. + * @throws ParseException If the user input does not conform to the expected format. + */ + public UnmarkAttendanceCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_PERSON, PREFIX_GROUP, PREFIX_WEEK); + + if (!arePrefixesPresent(argMultimap, PREFIX_PERSON, PREFIX_GROUP, PREFIX_WEEK) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + UnmarkAttendanceCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_PERSON, PREFIX_GROUP, PREFIX_WEEK); + String personName = ParserUtil.parseName(argMultimap.getValue(PREFIX_PERSON).get()).toString(); + String groupName = ParserUtil.parseGroupName(argMultimap.getValue(PREFIX_GROUP).get()); + int week = Integer.parseInt(argMultimap.getValue(PREFIX_WEEK).get()); + return new UnmarkAttendanceCommand(personName, groupName, week); + } + + /** + * 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/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 73397161e84..890e8ce0bee 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -2,65 +2,104 @@ import static java.util.Objects.requireNonNull; +import java.time.LocalDate; import java.util.List; import javafx.collections.ObservableList; import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.assignment.Assignment; +import seedu.address.model.group.Group; +import seedu.address.model.group.GroupMemberDetail; +import seedu.address.model.group.UniqueGroupList; +import seedu.address.model.group.exceptions.GroupNotFoundException; import seedu.address.model.person.Person; import seedu.address.model.person.UniquePersonList; +import seedu.address.model.person.exceptions.PersonNotFoundException; /** - * Wraps all data at the address-book level - * Duplicates are not allowed (by .isSamePerson comparison) + * Represents an address book that stores a list of persons and groups. + * Ensures that no duplicate persons or groups exist based on identity comparisons. */ public class AddressBook implements ReadOnlyAddressBook { + /** + * The list of unique persons in the address book. + */ private final UniquePersonList persons; + /** + * The list of unique groups in the address book. + */ + private final UniqueGroupList groups; + /* * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication * between constructors. See https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html * * Note that non-static init blocks are not recommended to use. There are other ways to avoid duplication - * among constructors. + * among constructors. */ { persons = new UniquePersonList(); + groups = new UniqueGroupList(); } - public AddressBook() {} + /** + * Constructs an empty AddressBook. + */ + public AddressBook() { + } /** - * Creates an AddressBook using the Persons in the {@code toBeCopied} + * Constructs an AddressBook using the data from an existing {@code ReadOnlyAddressBook}. + * + * @param toBeCopied The address book whose data should be copied. */ public AddressBook(ReadOnlyAddressBook toBeCopied) { this(); resetData(toBeCopied); } - //// list overwrite operations + //// List overwrite operations /** - * Replaces the contents of the person list with {@code persons}. - * {@code persons} must not contain duplicate persons. + * Replaces the contents of the person list with a new list of persons. + * Ensures that the new list does not contain duplicate persons. + * + * @param persons The new list of persons. */ public void setPersons(List persons) { this.persons.setPersons(persons); } /** - * Resets the existing data of this {@code AddressBook} with {@code newData}. + * Replaces the contents of the group list with a new list of groups. + * Ensures that the new list does not contain duplicate groups. + * + * @param groups The new list of groups. + */ + public void setGroups(List groups) { + this.groups.setGroups(groups); + } + + /** + * Resets the existing data of this {@code AddressBook} with new data. + * + * @param newData The new data to replace the current address book data. */ public void resetData(ReadOnlyAddressBook newData) { requireNonNull(newData); - setPersons(newData.getPersonList()); + setGroups(newData.getGroupList()); } - //// person-level operations + //// Person-level operations /** - * Returns true if a person with the same identity as {@code person} exists in the address book. + * Checks if a person with the same identity as {@code person} exists in the address book. + * + * @param person The person to check. + * @return True if the person exists, false otherwise. */ public boolean hasPerson(Person person) { requireNonNull(person); @@ -68,53 +107,233 @@ public boolean hasPerson(Person person) { } /** - * Adds a person to the address book. - * The person must not already exist in the address book. + * Adds a new person to the address book. + * Ensures that the person does not already exist in the address book. + * + * @param p The person to add. */ public void addPerson(Person p) { persons.add(p); } /** - * Replaces the given person {@code target} in the list with {@code editedPerson}. - * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. + * Replaces a target person with an edited person in the address book. + * Ensures that the target exists and that the edited person does not duplicate another existing person. + * + * @param target The person to be replaced. + * @param editedPerson The new person data. */ public void setPerson(Person target, Person editedPerson) { requireNonNull(editedPerson); - persons.setPerson(target, editedPerson); + for (Group group : groups) { + if (group.contains(target)) { + group.setGroupMember(target, editedPerson); + } + } } /** - * Removes {@code key} from this {@code AddressBook}. - * {@code key} must exist in the address book. + * Removes a person from the address book. + * Ensures that the person exists before removal. + * + * @param key The person to remove. */ public void removePerson(Person key) { persons.remove(key); } - //// util methods + public Person getPerson(String personName) { + for (Person person : persons) { + if (person.getName().toString().equals(personName)) { + return person; + } + } + throw new PersonNotFoundException(); + } - @Override - public String toString() { - return new ToStringBuilder(this) - .add("persons", persons) - .toString(); + //// Group-level operations + + /** + * Checks if a group with the same identity as {@code group} exists in the address book. + * + * @param group The group to check. + * @return True if the group exists, false otherwise. + */ + public boolean hasGroup(Group group) { + requireNonNull(group); + return groups.contains(group); } + /** + * Removes a group from the address book. + * Ensures that the group exists before removal. + * + * @param key The group to remove. + */ + public void removeGroup(Group key) { + groups.remove(key); + } + + /** + * Replaces a target group with an edited group in the address book. + * Ensures that the target exists and the edited group does not duplicate another existing group. + * + * @param target The group to be replaced. + * @param editedGroup The new group data. + */ + public void setGroup(Group target, Group editedGroup) { + requireNonNull(editedGroup); + groups.setGroup(target, editedGroup); + } + + /** + * Adds a group to the address book. + * The group must not already exist in the address book. + */ + public void addGroup(Group g) { + groups.add(g); + } + + //// Utility methods + + /** + * Returns an unmodifiable view of the list of persons. + * + * @return An observable list of persons. + */ @Override public ObservableList getPersonList() { return persons.asUnmodifiableObservableList(); } + /** + * Returns an unmodifiable view of the list of groups. + * + * @return An observable list of groups. + */ + public ObservableList getGroupList() { + return groups.asUnmodifiableObservableList(); + } + + public Group getGroup(String groupName) { + for (Group group : groups) { + if (group.getGroupName().equals(groupName)) { + return group; + } + } + throw new GroupNotFoundException(); + } + + public void addPersonToGroup(Person personToAdd, Group groupToBeAddedTo) { + groupToBeAddedTo.add(personToAdd); + } + + public void deletePersonFromGroup(Person personToRemove, Group groupToBeRemovedFrom) { + groupToBeRemovedFrom.remove(personToRemove); + } + + /** + * Removes person from all groups they are in. + */ + public void deletePersonFromAllGroups(Person personToRemove) { + for (Group group : groups) { + if (group.contains(personToRemove)) { + deletePersonFromGroup(personToRemove, group); + } + } + } + + /** + * Adds assignment to the group specified. + */ + public Assignment addAssignmentToGroup(String assignmentName, LocalDate deadline, Group group, Float penalty) { + return group.addAssignment(assignmentName, deadline, penalty); + } + + /** + * Deletes assignment from the group specified. + */ + public void removeAssignmentFromGroup(String assignmentName, Group group) { + group.removeAssignment(assignmentName); + } + + /** + * Edits specified assignment from the specified group. + * @param assignmentName The assignment name. + * @param newName The new assignment name. + * @param deadline A {@code LocalDate} object specifying the assignment deadline. + * @param group A {@code Group} object specifying the group which the assignment is under. + */ + public void editAssignment(String assignmentName, String newName, LocalDate deadline, Group group, Float penalty) { + group.editAssignment(assignmentName, newName, deadline, penalty); + } + + /** + * Checks if the assignment is in the group specified. + */ + public boolean isAssignmentInGroup(String assignmentName, Group group) { + return group.containsAssignment(assignmentName); + } + + /** + * Grades an assignment specified with the relevant score + */ + public void gradeAssignment(Person person, Group group, String assignmentName, Float score) { + GroupMemberDetail personDetail = group.getGroupMemberDetail(person); + Assignment assignment = group.getAssignment(assignmentName); + personDetail.gradeAssignment(assignment, score); + } + + /** + * Retrives a grade for a specified assignment. + */ + public Float getGrade(Person person, Group group, String assignmentName) { + GroupMemberDetail personDetail = group.getGroupMemberDetail(person); + Assignment assignment = group.getAssignment(assignmentName); + return personDetail.getAssignmentGrade(assignment); + } + + /** + * Mark attendance of person in group. + */ + public void markAttendance(Person person, Group group, int week) { + group.markAttendance(person, week); + } + + /** + * Unmark attendance of person in group. + */ + public void unmarkAttendance(Person person, Group group, int week) { + group.unmarkAttendance(person, week); + } + + /** + * Returns a string representation of the AddressBook object. + * + * @return A string describing the address book. + */ + @Override + public String toString() { + return new ToStringBuilder(this) + .add("persons", persons) + .add("groups", groups) + .toString(); + } + + /** + * Checks if this AddressBook is equal to another object. + * + * @param other The other object to compare to. + * @return True if both objects are AddressBooks with the same persons list. + */ @Override public boolean equals(Object other) { if (other == this) { return true; } - // instanceof handles nulls + // instanceof handles null cases if (!(other instanceof AddressBook)) { return false; } @@ -123,6 +342,11 @@ public boolean equals(Object other) { return persons.equals(otherAddressBook.persons); } + /** + * Returns the hash code of the AddressBook. + * + * @return The hash code based on the persons list. + */ @Override public int hashCode() { return persons.hashCode(); diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..99582ca4489 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -1,18 +1,28 @@ package seedu.address.model; import java.nio.file.Path; +import java.time.LocalDate; import java.util.function.Predicate; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; +import seedu.address.model.assignment.Assignment; +import seedu.address.model.group.Group; import seedu.address.model.person.Person; +import seedu.address.ui.Result; /** * The API of the Model component. */ public interface Model { - /** {@code Predicate} that always evaluate to true */ + /** + * {@code Predicate} that always evaluate to true + */ Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; + /** + * {@code Predicate} that always evaluate to true + */ + Predicate PREDICATE_SHOW_ALL_GROUPS = unused -> true; /** * Replaces user prefs data with the data in {@code userPrefs}. @@ -49,7 +59,9 @@ public interface Model { */ void setAddressBook(ReadOnlyAddressBook addressBook); - /** Returns the AddressBook */ + /** + * Returns the AddressBook + */ ReadOnlyAddressBook getAddressBook(); /** @@ -76,12 +88,136 @@ public interface Model { */ void setPerson(Person target, Person editedPerson); - /** Returns an unmodifiable view of the filtered person list */ + /** + * Returns an unmodifiable view of the filtered person list + */ ObservableList getFilteredPersonList(); + /** + * Returns an unmodifiable view of the result list + */ + ObservableList getResultList(); + + boolean hasGroup(Group group); + + /** + * Deletes the given group. + * The group must exist in the address book. + */ + void deleteGroup(Group target); + + void setGroup(Group target, Group editedGroup); + + /** + * Returns a unmodifiable view of the filtered group list + */ + ObservableList getFilteredGroupList(); + + /** * 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); + + /** + * Updates the filter of filtered group list to filter by the given {@code predicate}. + * + * @throws NullPointerException if {@code predicate} is null. + */ + void updateFilteredGroupList(Predicate predicate); + + /** + * Adds the given group. + * The group must not already exist in the address book. + */ + void addGroup(Group group); + + /** + * Shows all the details of the specified group. + * + * @param groupToShow The group whose details should be shown. + */ + void showGroupDetails(Group groupToShow); + + /** + * Adds the given person to the given group. + * The person must not already exist in the group. + */ + void addPersonToGroup(Person personToAdd, Group groupToBeAddedTo); + + /** + * Removes the given person from the given group. + * The person must exist in the group. + */ + void deletePersonFromGroup(Person personToRemove, Group groupToRemoveFrom); + + /** + * Removes the given person from the all groups they were in. + */ + void deletePersonFromAllGroups(Person personToRemove); + + /** + * Add an assignment to the group specified. + * + * @return The created assignment + */ + Assignment addAssignmentToGroup(String assignmentName, LocalDate deadline, Group group, Float penalty); + + /** + * Removes the specified assignment. + */ + void removeAssignmentFromGroup(String assignmentName, Group group); + + /** + * Edits the specified assignment. + */ + void editAssignment(String assignmentName, String newName, LocalDate deadline, Group group, Float penalty); + + /** + * Returns true if the assignment is in the group. + * @param assignmentName Assignment to check + * @param group Group to check + * @return true if the assignment is in the group, false otherwise + */ + boolean isAssignmentInGroup(String assignmentName, Group group); + + /** + * Grades a specified assignment by the given grade. + */ + void gradeAssignment(Person person, Group group, String assignmentName, Float score); + + /** + * Mark attendance of a person in a group. + */ + void markAttendance(Person person, Group group, int week); + + /** + * Unmark attendance of a person in a group. + */ + void unmarkAttendance(Person person, Group group, int week); + + /** + * Retrieves a group matching the provided group name. + */ + Group getGroup(String groupName); + + /** + * Retrieves a person matching the provided person name. + */ + Person getPerson(String personName); + + /** + * Retrieves the grade of a specified assignment. + */ + Float getGrade(Person person, Group group, String assignmentName); + + /** + * Returns true if the person is in the group. + * @param person Person to check + * @param group Group to check + * @return true if the person is in the group, false otherwise + */ + boolean isPersonInGroup(Person person, Group group); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 57bc563fde6..c7c36126d9e 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.time.LocalDate; import java.util.function.Predicate; import java.util.logging.Logger; @@ -11,7 +12,10 @@ import javafx.collections.transformation.FilteredList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.model.assignment.Assignment; +import seedu.address.model.group.Group; import seedu.address.model.person.Person; +import seedu.address.ui.Result; /** * Represents the in-memory model of the address book data. @@ -22,6 +26,8 @@ public class ModelManager implements Model { private final AddressBook addressBook; private final UserPrefs userPrefs; private final FilteredList filteredPersons; + private final FilteredList filteredGroups; + private final ResultList results; /** * Initializes a ModelManager with the given addressBook and userPrefs. @@ -34,8 +40,11 @@ public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs this.addressBook = new AddressBook(addressBook); this.userPrefs = new UserPrefs(userPrefs); filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + filteredGroups = new FilteredList<>(this.addressBook.getGroupList()); + results = new ResultList(filteredPersons, filteredGroups); } + public ModelManager() { this(new AddressBook(), new UserPrefs()); } @@ -122,10 +131,159 @@ public ObservableList getFilteredPersonList() { return filteredPersons; } + /** + * Returns an unmodifiable view of the list of {@code Result} backed by the internal list of + * {@code versionedAddressBook} + */ + @Override + public ObservableList getResultList() { + return results.getObservableResults(); + } + @Override public void updateFilteredPersonList(Predicate predicate) { requireNonNull(predicate); filteredPersons.setPredicate(predicate); + results.setSource(ResultList.Source.Persons); + } + + //=========== Filtered Group List Accessors ============================================================= + + @Override + public ObservableList getFilteredGroupList() { + return filteredGroups; + } + + @Override + public void updateFilteredGroupList(Predicate predicate) { + requireNonNull(predicate); + filteredGroups.setPredicate(predicate); + results.setSource(ResultList.Source.Groups); + } + + @Override + public boolean hasGroup(Group group) { + requireNonNull(group); + return addressBook.hasGroup(group); + } + + @Override + public void deleteGroup(Group target) { + requireNonNull(target); + addressBook.removeGroup(target); + } + + @Override + public void setGroup(Group target, Group editedGroup) { + requireAllNonNull(target, editedGroup); + + addressBook.setGroup(target, editedGroup); + showGroupDetails(editedGroup); + } + + @Override + public void addGroup(Group group) { + requireNonNull(group); + addressBook.addGroup(group); + updateFilteredGroupList(PREDICATE_SHOW_ALL_GROUPS); + } + + @Override + public void showGroupDetails(Group groupToShow) { + requireNonNull(groupToShow); + var details = groupToShow.getGroupDetails(); + results.setSource(ResultList.Source.GroupDetails, details); + } + + @Override + public Group getGroup(String groupName) { + return addressBook.getGroup(groupName); + } + + @Override + public Person getPerson(String personName) { + return addressBook.getPerson(personName); + } + + @Override + public Float getGrade(Person person, Group group, String assignmentName) { + requireAllNonNull(person, group, assignmentName); + return addressBook.getGrade(person, group, assignmentName); + } + + @Override + public boolean isPersonInGroup(Person person, Group group) { + requireAllNonNull(person, group); + return group.contains(person); + } + + @Override + public void addPersonToGroup(Person personToAdd, Group groupToBeAddedTo) { + requireAllNonNull(personToAdd, groupToBeAddedTo); + addressBook.addPersonToGroup(personToAdd, groupToBeAddedTo); + showGroupDetails(groupToBeAddedTo); + } + + @Override + public void deletePersonFromGroup(Person personToRemove, Group groupToRemoveFrom) { + requireAllNonNull(personToRemove, groupToRemoveFrom); + addressBook.deletePersonFromGroup(personToRemove, groupToRemoveFrom); + showGroupDetails(groupToRemoveFrom); + } + + @Override + public void deletePersonFromAllGroups(Person personToRemove) { + requireNonNull(personToRemove); + addressBook.deletePersonFromAllGroups(personToRemove); + } + + @Override + public Assignment addAssignmentToGroup(String assignmentName, LocalDate deadline, Group group, Float penalty) { + requireAllNonNull(assignmentName, deadline, group, penalty); + Assignment assignment = addressBook.addAssignmentToGroup(assignmentName, deadline, group, penalty); + showGroupDetails(group); + return assignment; + } + + @Override + public void removeAssignmentFromGroup(String assignmentName, Group group) { + requireAllNonNull(assignmentName, group); + addressBook.removeAssignmentFromGroup(assignmentName, group); + showGroupDetails(group); + } + + @Override + public void editAssignment(String assignmentName, String newName, LocalDate deadline, Group group, Float penalty) { + requireAllNonNull(assignmentName, group); + addressBook.editAssignment(assignmentName, newName, deadline, group, penalty); + showGroupDetails(group); + } + + @Override + public boolean isAssignmentInGroup(String assignmentName, Group group) { + requireAllNonNull(assignmentName, group); + return addressBook.isAssignmentInGroup(assignmentName, group); + } + + @Override + public void gradeAssignment(Person person, Group group, String assignmentName, Float score) { + requireAllNonNull(person, group, assignmentName, score); + addressBook.gradeAssignment(person, group, assignmentName, score); + updateFilteredGroupList(PREDICATE_SHOW_ALL_GROUPS); + } + + @Override + public void markAttendance(Person person, Group group, int week) { + requireAllNonNull(person, group, week); + addressBook.markAttendance(person, group, week); + showGroupDetails(group); + } + + @Override + public void unmarkAttendance(Person person, Group group, int week) { + requireAllNonNull(person, group, week); + addressBook.unmarkAttendance(person, group, week); + showGroupDetails(group); } @Override @@ -144,5 +302,4 @@ public boolean equals(Object other) { && userPrefs.equals(otherModelManager.userPrefs) && filteredPersons.equals(otherModelManager.filteredPersons); } - } diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java index 6ddc2cd9a29..c12d06e7aec 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java @@ -1,6 +1,7 @@ package seedu.address.model; import javafx.collections.ObservableList; +import seedu.address.model.group.Group; import seedu.address.model.person.Person; /** @@ -13,5 +14,10 @@ public interface ReadOnlyAddressBook { * This list will not contain any duplicate persons. */ ObservableList getPersonList(); + /** + * Returns an unmodifiable view of the groups list. + * This list will not contain any duplicate groups. + */ + ObservableList getGroupList(); } diff --git a/src/main/java/seedu/address/model/ResultList.java b/src/main/java/seedu/address/model/ResultList.java new file mode 100644 index 00000000000..ed3de04dd7f --- /dev/null +++ b/src/main/java/seedu/address/model/ResultList.java @@ -0,0 +1,103 @@ +package seedu.address.model; + +import java.util.Collection; + +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import seedu.address.model.group.Group; +import seedu.address.model.person.Person; +import seedu.address.ui.Result; + +/** + * Model containing the data backing the result list in the UI. + */ +public class ResultList { + /** + * Possible sources for the results in the list. + */ + public enum Source { + Persons, + Groups, + GroupDetails, + } + + private final ObservableList persons; + private final ObservableList groups; + private final ObservableList results; + private Source source; + + /** + * Creates a new ResultList backed by the given ObservableLists. + * + * @param persons The backing list of persons. + * @param groups The backing list of groups. + */ + public ResultList(ObservableList persons, ObservableList groups) { + this.persons = persons; + this.groups = groups; + + // Start by showing persons by default. + this.source = Source.Persons; + this.results = FXCollections.observableArrayList(persons); + + persons.addListener((ListChangeListener) c -> { + processListChange(c, Source.Persons); + }); + groups.addListener((ListChangeListener) c -> { + processListChange(c, Source.Groups); + }); + } + + public ObservableList getObservableResults() { + return results; + } + + public void setSource(Source source) { + this.source = source; + switch (source) { + case Persons: + results.setAll(persons); + break; + case Groups: + results.setAll(groups); + break; + case GroupDetails: + // Use ResultList#setSource(Source, Collection) instead. + default: + throw new IllegalArgumentException("Invalid source"); + } + } + + public void setSource(Source source, Collection results) { + switch (source) { + case GroupDetails: + this.results.setAll(results); + break; + case Persons: + case Groups: + // Use ResultList#setSource(Source) instead. + default: + throw new IllegalArgumentException("Invalid source"); + } + this.source = source; + } + + private void processListChange(ListChangeListener.Change c, Source expectedSource) { + if (this.source != expectedSource) { + return; + } + while (c.next()) { + if (c.wasRemoved()) { + results.remove(c.getFrom(), c.getFrom() + c.getRemovedSize()); + } + if (c.wasAdded()) { + results.addAll(c.getFrom(), c.getAddedSubList()); + } + if (c.wasUpdated()) { + results.removeAll(c.getRemoved()); + results.addAll(c.getAddedSubList()); + } + } + } +} diff --git a/src/main/java/seedu/address/model/assignment/Assignment.java b/src/main/java/seedu/address/model/assignment/Assignment.java new file mode 100644 index 00000000000..130797ef2f0 --- /dev/null +++ b/src/main/java/seedu/address/model/assignment/Assignment.java @@ -0,0 +1,91 @@ +package seedu.address.model.assignment; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.text.MessageFormat; +import java.time.LocalDate; +import java.util.Objects; + +/** + * Repesents an assignment. + */ +public class Assignment { + public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + public static final String MESSAGE_CONSTRAINTS = "Assignment names must be non-empty and alphanumeric"; + /** + * The assignment name. + */ + private String name; + + /** + * The assignment deadline. + */ + private LocalDate deadline; + /** + * Penalty for grading should there be late submission. + */ + private Float penalty; + + /** + * Constructs a {@code Assignment}. + * + * @param name The assignment name. + * @param deadline deadline of the assignment. + */ + public Assignment(String name, LocalDate deadline, Float penalty) { + requireAllNonNull(name, deadline, penalty); + this.name = name; + this.deadline = deadline; + this.penalty = penalty; + } + + /** + * Edits the assignment details. + * + * @param name The assignment name. + * @param deadline deadline of the assignment. + */ + public void editAssignment(String name, LocalDate deadline, Float penalty) { + if (name != null) { + this.name = name; + } + if (deadline != null) { + this.deadline = deadline; + } + + if (penalty != null) { + this.penalty = penalty; + } + } + + public static boolean isValidName(String test) { + return test.matches(VALIDATION_REGEX); + } + + public String getName() { + return this.name; + } + + public LocalDate getDeadline() { + return this.deadline; + } + + public Float getPenalty() { + return this.penalty; + } + + /** + * Computes the hash code for this assignment based on its name and group. + * + * @return The hash code of the group. + */ + @Override + public int hashCode() { + return Objects.hash(name); + } + + @Override + public String toString() { + return MessageFormat.format("Assignment'{'name=''{0}'', deadline={1}'}'", name, deadline); + } +} diff --git a/src/main/java/seedu/address/model/assignment/exceptions/AssignmentNotFoundException.java b/src/main/java/seedu/address/model/assignment/exceptions/AssignmentNotFoundException.java new file mode 100644 index 00000000000..a1912d9b6db --- /dev/null +++ b/src/main/java/seedu/address/model/assignment/exceptions/AssignmentNotFoundException.java @@ -0,0 +1,8 @@ +package seedu.address.model.assignment.exceptions; + +/** + * Signals that the operation was unable to find the Assignment + */ +public class AssignmentNotFoundException extends RuntimeException{ + +} diff --git a/src/main/java/seedu/address/model/assignment/exceptions/DuplicateAssignmentException.java b/src/main/java/seedu/address/model/assignment/exceptions/DuplicateAssignmentException.java new file mode 100644 index 00000000000..74de17d31fa --- /dev/null +++ b/src/main/java/seedu/address/model/assignment/exceptions/DuplicateAssignmentException.java @@ -0,0 +1,11 @@ +package seedu.address.model.assignment.exceptions; + +/** + * Signals that the operation will result in duplicate Assignments (Assignments are + * considered duplicates if they have the same name). + */ +public class DuplicateAssignmentException extends RuntimeException { + public DuplicateAssignmentException() { + super("Operation would result in duplicate assignments"); + } +} diff --git a/src/main/java/seedu/address/model/group/Group.java b/src/main/java/seedu/address/model/group/Group.java new file mode 100644 index 00000000000..608461e6690 --- /dev/null +++ b/src/main/java/seedu/address/model/group/Group.java @@ -0,0 +1,450 @@ +package seedu.address.model.group; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import javafx.scene.layout.Region; +import seedu.address.commons.util.ArrayListMap; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.assignment.Assignment; +import seedu.address.model.assignment.exceptions.AssignmentNotFoundException; +import seedu.address.model.assignment.exceptions.DuplicateAssignmentException; +import seedu.address.model.person.Person; +import seedu.address.model.person.exceptions.DuplicatePersonException; +import seedu.address.model.person.exceptions.PersonNotFoundException; +import seedu.address.model.tag.Tag; +import seedu.address.ui.GroupCard; +import seedu.address.ui.Result; +import seedu.address.ui.UiPart; + +/** + * Represents a Group in the address book. + * A {@code Group} consists of a unique name and an ordered list of unique members. + */ +public class Group implements Result { + + /** + * Message to indicate the constraints for group names. + */ + public static final String MESSAGE_CONSTRAINTS = + "Group name must not be blank, and it must contain only alphanumeric characters, spaces," + + " apostrophes, slashes, and dashes."; + + /** + * Regular expression to validate group names. + */ + public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} /\\-']*"; + + /** + * The name of the group. + */ + private String groupName; + + /** + * The map of all members in the group. + */ + private final ArrayListMap groupMembers; + + /** + * The list of all assignments in the group. + */ + private final ArrayList assignments; + + private final Set tags; + + /** + * Constructs a {@code Group} with a specified name. + * Initializes an empty list of group members. + * + * @param groupName A valid group name. + */ + public Group(String groupName) { + this(groupName, (Collection) null, null); + } + + /** + * Constructs a {@code Group} with a specified name and an existing list of members. + * Assumes groupMembers are students. + * + * @param groupName A valid group name. + * @param groupMembers The list of members in the group. + */ + public Group(String groupName, Collection groupMembers) { + this(groupName, groupMembers, null); + } + + /** + * Constructs a {@code Group} with a specified name, existing list of members, and a set of tags. + * + * @param groupName A valid group name. + * @param groupMembers The collection of members in the group. + * @param tags The collection of tags for the group. + */ + public Group(String groupName, Collection groupMembers, Collection tags) { + requireNonNull(groupName); + checkArgument(isValidGroupName(groupName), MESSAGE_CONSTRAINTS); + this.groupName = groupName; + this.groupMembers = new ArrayListMap<>(); + if (groupMembers != null) { + for (Person p : groupMembers) { + this.groupMembers.put(p, new GroupMemberDetail(p, this)); + } + } + this.tags = tags == null ? new HashSet<>() : new HashSet<>(tags); + this.assignments = new ArrayList<>(); + } + + /** + * Constructs a {@code Group} with a specified name, existing Map and set of tags. + * + * @param groupName A valid group name. + * @param groupMembers A map of Person as key to GroupMemberDetail as value. + * @param tags The collection of tags for the group. + */ + public Group(String groupName, ArrayListMap groupMembers, Collection tags) { + this(groupName, groupMembers, tags, null); + } + + /** + * Constructs a {@code Group} with a specified name, existing Map and set of tags. + * + * @param groupName A valid group name. + * @param groupMembers A map of Person as key to GroupMemberDetail as value. + * @param tags The collection of tags for the group. + * @param assignments The collection of tags for the group. + */ + public Group( + String groupName, + ArrayListMap groupMembers, + Collection tags, + Collection assignments) { + requireNonNull(groupName); + checkArgument(isValidGroupName(groupName), MESSAGE_CONSTRAINTS); + this.groupName = groupName; + this.groupMembers = groupMembers; + this.tags = tags == null ? new HashSet<>() : new HashSet<>(tags); + this.assignments = assignments == null ? new ArrayList<>() : new ArrayList<>(assignments); + } + + public Group createEditedGroup(String newGroupName, Collection tags) { + return new Group(newGroupName, groupMembers, tags, assignments); + } + + /** + * Checks if the given string is a valid group name. + * + * @param test The string to test. + * @return True if the name is valid, false otherwise. + */ + public static boolean isValidGroupName(String test) { + return test.matches(VALIDATION_REGEX); + } + + /** + * Gets the name of the group. + * + * @return The group's name. + */ + public String getGroupName() { + return this.groupName; + } + + /** + * Sets a new name for the group. + * + * @param groupName The new name to assign to the group. + */ + public void setGroupName(String groupName) { + requireNonNull(groupName); + checkArgument(isValidGroupName(groupName), MESSAGE_CONSTRAINTS); + this.groupName = groupName; + } + + /** + * Retrieves the list of group members. + * + * @return An ArrayList of group members. + */ + public ArrayList getGroupMembers() { + return new ArrayList<>(groupMembers.keySet()); + } + + public ArrayListMap getGroupMembersMap() { + ArrayListMap copied = new ArrayListMap<>(); + copied.putAll(this.groupMembers); + return copied; + } + + /** + * Replaces the old person with new edited person while keeping the GroupMemberDetails. + * + * @param target + * @param editedPerson + */ + public void setGroupMember(Person target, Person editedPerson) throws PersonNotFoundException { + if (!contains(target)) { + throw new PersonNotFoundException(); + } + groupMembers.replaceKey(target, editedPerson); + groupMembers.computeIfPresent(editedPerson, (k, v) -> v.copy(editedPerson)); + } + + /** + * Returns an immutable tag set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public Set getTags() { + return Collections.unmodifiableSet(tags); + } + + /** + * Returns true if both groups have the same name. + * This defines a weaker notion of equality between two gorups. + */ + public boolean isSameGroup(Group otherGroup) { + if (otherGroup == this) { + return true; + } + return otherGroup != null && otherGroup.getGroupName().equals(groupName); + } + + /** + * Checks whether this group is equal to another object. + * Two groups are considered equal if they have the same name. + * + * @param other The object to compare with. + * @return True if both groups have the same name, false otherwise. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Group otherGroup)) { + return false; + } + + return groupName.equals(otherGroup.groupName) && tags.equals(otherGroup.tags); + } + + /** + * Computes the hash code for this group based on its name. + * + * @return The hash code of the group. + */ + @Override + public int hashCode() { + return Objects.hash(groupName, tags); + } + + /** + * Checks if the group contains a specific person. + * + * @param toCheck The person to check for membership in the group. + * @return True if the person exists in the group, false otherwise. + */ + public boolean contains(Person toCheck) { + requireNonNull(toCheck); + return groupMembers.containsKey(toCheck); + } + + /** + * Adds a person to the group. + * Ensures that the person does not already exist in the group. + * + * @param p The person to be added. + * @throws DuplicatePersonException If the person already exists in the group. + */ + public void add(Person p) { + requireNonNull(p); + if (contains(p)) { + throw new DuplicatePersonException(); + } + this.groupMembers.put(p, new GroupMemberDetail(p, this)); + } + + /** + * Removes a person from the group. + * Ensures that the person exists before attempting removal. + * + * @param p The person to be removed. + * @throws PersonNotFoundException If the person is not found in the group. + */ + public void remove(Person p) { + if (groupMembers.remove(p) == null) { + throw new PersonNotFoundException(); + } + } + + /** + * Retrieves a person at a specified index in the group. + * + * @param i The index of the person to retrieve. + * @return The person at the given index. + */ + public Person get(int i) { + return groupMembers.get(i).getPerson(); + } + + /** + * Gets the number of members in the group. + * + * @return The size of the group. + */ + public int size() { + return this.groupMembers.size(); + } + + public GroupMemberDetail getGroupMemberDetail(Person person) { + return groupMembers.get(person); + } + + /** + * Gets the {@code Assignment} specified by name. If no such assignment is found, returns null. + * + * @param assignmentName The name of the assignment + * @return The desired assignment if found + */ + public Assignment getAssignment(String assignmentName) throws AssignmentNotFoundException { + for (Assignment a : assignments) { + if (a.getName().equals(assignmentName)) { + return a; + } + } + throw new AssignmentNotFoundException(); + } + + /** + * Gets all {@code Assignment} in the group + * + * @return All assignments in the group. + */ + public ArrayList getAssignments() { + return assignments; + } + + /** + * Checks if the group contains the {@code Assignment} specified by name. + * + * @param assignmentName The name of the assignment + * @return True if the Assignment exists and false otherwise. + */ + public boolean containsAssignment(String assignmentName) { + return assignments.stream() + .anyMatch(a -> a.getName().equals(assignmentName)); + } + + /** + * Adds the assignment to the group. + * + * @param assignmentName The assignment name. + * @param deadline A {@code LocalDate} object specifying the assignment deadline. + */ + public Assignment addAssignment(String assignmentName, LocalDate deadline, Float penalty) { + if (containsAssignment(assignmentName)) { + throw new DuplicateAssignmentException(); + } + Assignment assignment = new Assignment(assignmentName, deadline, penalty); + assignments.add(assignment); + return assignment; + } + + /** + * Removes an assignment from the group. + * + * @param assignmentName The assignment name to be removed. + */ + public void removeAssignment(String assignmentName) throws AssignmentNotFoundException { + for (Assignment a : assignments) { + if (a.getName().equals(assignmentName)) { + assignments.remove(a); + return; + } + } + throw new AssignmentNotFoundException(); + } + + /** + * Edits the specified assignment. + * + * @param assignmentName The assignment name of the assignment to be edited. + * @param newName The new name of the assignment. + * @param deadline A {@code LocalDate} object specifying the assignment deadline. + */ + public void editAssignment(String assignmentName, String newName, LocalDate deadline, Float penalty) { + for (Assignment a : assignments) { + if (a.getName().equals(newName)) { + throw new DuplicateAssignmentException(); + } + if (a.getName().equals(assignmentName)) { + a.editAssignment(newName, deadline, penalty); + return; + } + } + throw new AssignmentNotFoundException(); + } + + /** + * Marks attendance of a person for a specified week. + * + * @param person The person to mark the attendance + * @param week A valid week. + * @throws PersonNotFoundException + */ + public void markAttendance(Person person, int week) throws PersonNotFoundException { + if (!groupMembers.containsKey(person)) { + throw new PersonNotFoundException(); + } + GroupMemberDetail groupMemberDetail = groupMembers.get(person); + groupMemberDetail.markAttendance(week); + } + + /** + * Unmarks attendance of a person for a specified week. + * + * @param person The person to mark the attendance + * @param week A valid week. + * @throws PersonNotFoundException + */ + public void unmarkAttendance(Person person, int week) throws PersonNotFoundException { + if (!groupMembers.containsKey(person)) { + throw new PersonNotFoundException(); + } + GroupMemberDetail groupMemberDetail = groupMembers.get(person); + groupMemberDetail.unmarkAttendance(week); + } + + + /** + * Returns a string representation of the group in the format "[GroupName]". + * + * @return A formatted string representing the group. + */ + @Override + public String toString() { + return new ToStringBuilder(this) + .add("name", groupName) + .add("tags", tags) + .toString(); + } + + @Override + public UiPart createCard(int displayedIndex) { + return new GroupCard(this, displayedIndex); + } + + public ArrayList getGroupDetails() { + return groupMembers.values(); + } +} diff --git a/src/main/java/seedu/address/model/group/GroupMemberDetail.java b/src/main/java/seedu/address/model/group/GroupMemberDetail.java new file mode 100644 index 00000000000..5527d9ce3e9 --- /dev/null +++ b/src/main/java/seedu/address/model/group/GroupMemberDetail.java @@ -0,0 +1,283 @@ +package seedu.address.model.group; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.time.LocalDate; +import java.util.Objects; + +import javafx.scene.layout.Region; +import seedu.address.commons.util.ArrayListMap; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.assignment.Assignment; +import seedu.address.model.person.Person; +import seedu.address.ui.GroupDetailCard; +import seedu.address.ui.Result; +import seedu.address.ui.UiPart; + +/** + * Represents a GroupMemberDetail in the address book to associate a Person with a Group. + * A {@code GroupMemberDetail} consists of a person, group, attendance, and assignment grades. + */ +public class GroupMemberDetail implements Result { + /** + * Different roles of memebers in a group + */ + public enum Role { + Student, + TeachingAssistant, + Lecturer + } + + public static final int WEEKS_PER_SEMESTER = 13; + + /** + * Message to indicate the constraints for week number + */ + public static final String MESSAGE_CONSTRAINTS = String.format( + "Weeks should be between 1 and %d", WEEKS_PER_SEMESTER); + + /** + * The {@code Person} whose detail is describing. + */ + private Person person; + /** + * The {@code Group} whose person belong to. + */ + private Group group; + + /** + * The {@code Role} that the person has. + */ + private Role role; + + /** + * The list of members in the group, stored in order of insertion. + */ + private boolean[] attendance; + + /** + * The grades of the assignments of this person in this group. + */ + private ArrayListMap grades; + + /** + * Constructs a {@code GroupMemberDetail} with a specified group member {@code Person}. + * Initializes an empty list of attendance. + * Assumes the person is a student + * + * @param Person A valid person. + * @param Group A valid group. + */ + public GroupMemberDetail(Person person, Group group) { + this(person, group, Role.Student, new boolean[WEEKS_PER_SEMESTER]); + } + + /** + * Constructs a {@code GroupMemberDetail} with a specified group member {@code Person}. + * Initializes an empty list of attendance. + * Assumes the person is a student + * + * @param Person A valid person. + * @param Group A valid group. + * @param Role A valid role. + */ + public GroupMemberDetail(Person person, Group group, Role role, boolean[] attendance) { + requireAllNonNull(person, role); + this.person = person; + this.group = group; + this.role = role; + this.attendance = attendance; + this.grades = new ArrayListMap<>(); + } + + /** + * Constructs a {@code GroupMemberDetail} with a specified group member {@code Person}. + * Initializes an empty list of attendance. + * Assumes the person is a student. + * Used by storage + * + * @param person A valid person. + * @param role A valid role. + * @param attendance A valid attendance array. + * @param grades A valid grade map. + */ + public GroupMemberDetail(Person person, Role role, boolean[] attendance, + ArrayListMap grades) { + requireAllNonNull(person, role, attendance, grades); + this.person = person; + this.role = role; + this.attendance = attendance; + this.grades = grades; + } + + public GroupMemberDetail copy(Person newPerson) { + return new GroupMemberDetail(newPerson, this.group, this.role, this.attendance); + } + + /** + * Checks if the week is between 1 and the number of weeks in a semester. + * + * @param test The week number to test. + * @return True if the week is valid, false otherwise. + */ + public static boolean isValidWeek(int test) { + return test >= 1 && test <= WEEKS_PER_SEMESTER; + } + + /** + * Gets the person. + * + * @return The person associated with this object. + */ + public Person getPerson() { + return this.person; + } + + /** + * Gets the group. + * + * @return The group associated with this object. + */ + public Group getGroup() { + return this.group; + } + + /** + * Sets the Group. + * + * @param group A valid group. + */ + public void setGroup(Group group) { + this.group = group; + } + + /** + * Gets the role. + * + * @return The role of the person. + */ + public Role getRole() { + return this.role; + } + + /** + * Sets the role. + * + * @param role The role of the person. + */ + public void setRole(Role role) { + requireNonNull(role); + this.role = role; + } + + /** + * Gets the attendance. + * + * @return The attendance. + */ + public boolean[] getAttendance() { + return this.attendance; + } + + /** + * Gets the grades. + * + * @return The grades. + */ + public ArrayListMap getGrades() { + return this.grades; + } + + /** + * Marks attendance for a particular week; + * + * @param week The week to mark the attendance + */ + public void markAttendance(int week) { + checkArgument(isValidWeek(week), MESSAGE_CONSTRAINTS); + this.attendance[week - 1] = true; + } + + /** + * Unmarks attendance for a particular week; + * + * @param week The week to unmark the attendance + */ + public void unmarkAttendance(int week) { + checkArgument(isValidWeek(week), MESSAGE_CONSTRAINTS); + this.attendance[week - 1] = false; + } + + /** + * Assigns a specified grade to an Assignment. + * Calculates a simple penalty for late submission + */ + public void gradeAssignment(Assignment assignment, Float score) { + Float penalty = assignment.getPenalty(); + if (LocalDate.now().isAfter(assignment.getDeadline())) { + score = score * penalty; + } + grades.put(assignment, score); + } + + /** + * Gets the grade for the specified assignment. + */ + public Float getAssignmentGrade(Assignment assignment) { + return this.grades.get(assignment); + } + + /** + * Checks whether this group is equal to another object. + * Two groups are considered equal if they have the same name. + * + * @param other The object to compare with. + * @return True if both groups have the same name, false otherwise. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof GroupMemberDetail otherGroupMemberDetail)) { + return false; + } + + return group.equals(otherGroupMemberDetail.getGroup()) && person.equals(otherGroupMemberDetail.getPerson()); + } + + /** + * Computes the hash code for this group based on its name. + * + * @return The hash code of the group. + */ + @Override + public int hashCode() { + return Objects.hash(person, group); + } + + /** + * Returns a string representation of the group in the format "[GroupName]". + * + * @return A formatted string representing the group. + */ + @Override + public String toString() { + return new ToStringBuilder(this) + .add("person", person) + .add("group", group) + .add("attendance", attendance) + .add("role", role) + .toString(); + } + + @Override + public UiPart createCard(int displayedIndex) { + return new GroupDetailCard(this, displayedIndex); + } +} + diff --git a/src/main/java/seedu/address/model/group/GroupNameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/group/GroupNameContainsKeywordsPredicate.java new file mode 100644 index 00000000000..a64fea4f570 --- /dev/null +++ b/src/main/java/seedu/address/model/group/GroupNameContainsKeywordsPredicate.java @@ -0,0 +1,44 @@ +package seedu.address.model.group; + +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 Group}'s {@code Name} matches any of the keywords given. + */ +public class GroupNameContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public GroupNameContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Group group) { + return keywords.stream() + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(group.getGroupName(), keyword)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof GroupNameContainsKeywordsPredicate)) { + return false; + } + + GroupNameContainsKeywordsPredicate otherPredicate = (GroupNameContainsKeywordsPredicate) other; + return keywords.equals(otherPredicate.keywords); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("keywords", keywords).toString(); + } +} diff --git a/src/main/java/seedu/address/model/group/UniqueGroupList.java b/src/main/java/seedu/address/model/group/UniqueGroupList.java new file mode 100644 index 00000000000..e00ce26ade0 --- /dev/null +++ b/src/main/java/seedu/address/model/group/UniqueGroupList.java @@ -0,0 +1,142 @@ +package seedu.address.model.group; + +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.group.exceptions.DuplicateGroupsException; +import seedu.address.model.group.exceptions.GroupNotFoundException; + +/** + * A list of groups that enforces uniqueness between its elements and does not allow nulls. + * A group is considered unique by comparing using {@code Group#isSameGroup(Group)}. + * + * Supports a minimal set of list operations. + * + * @see Group#isSameGroup(Group) + */ +public class UniqueGroupList implements Iterable { + + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Returns true if the list contains an equivalent group as the given argument. + */ + public boolean contains(Group toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSameGroup); + } + + /** + * Adds a group to the list. + * The group must not already exist in the list. + */ + public void add(Group toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateGroupsException(); + } + internalList.add(toAdd); + } + + /** + * Removes the equivalent group from the list. + * The group must exist in the list. + */ + public void remove(Group toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new GroupNotFoundException(); + } + } + + public void setGroups(UniqueGroupList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code group}. + * {@code groups} must not contain duplicate groups. + */ + public void setGroups(List groups) { + requireAllNonNull(groups); + if (!groupsAreUnique(groups)) { + throw new DuplicateGroupsException(); + } + + internalList.setAll(groups); + } + + public void setGroup(Group target, Group editedGroup) { + requireAllNonNull(target, editedGroup); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new GroupNotFoundException(); + } + + if (!target.isSameGroup(editedGroup) && contains(editedGroup)) { + throw new DuplicateGroupsException(); + } + + internalList.set(index, editedGroup); + } + + /** + * 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 UniqueGroupList otherUniqueGroupList)) { + return false; + } + + return internalList.equals(otherUniqueGroupList.internalList); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + @Override + public String toString() { + return internalList.toString(); + } + + /** + * Returns true if {@code groups} contains only unique groups. + */ + private boolean groupsAreUnique(List groups) { + for (int i = 0; i < groups.size() - 1; i++) { + for (int j = i + 1; j < groups.size(); j++) { + if (groups.get(i).isSameGroup(groups.get(j))) { + return false; + } + } + } + return true; + } +} + diff --git a/src/main/java/seedu/address/model/group/exceptions/DuplicateGroupsException.java b/src/main/java/seedu/address/model/group/exceptions/DuplicateGroupsException.java new file mode 100644 index 00000000000..7bab3a843bb --- /dev/null +++ b/src/main/java/seedu/address/model/group/exceptions/DuplicateGroupsException.java @@ -0,0 +1,10 @@ +package seedu.address.model.group.exceptions; +/** + * Signals that the operation will result in duplicate Groups (Groups are considered duplicates if they have the same + * identity). + */ +public class DuplicateGroupsException extends RuntimeException { + public DuplicateGroupsException() { + super("Operation would result in duplicate groups"); + } +} diff --git a/src/main/java/seedu/address/model/group/exceptions/GroupNotFoundException.java b/src/main/java/seedu/address/model/group/exceptions/GroupNotFoundException.java new file mode 100644 index 00000000000..f0ccd1839c3 --- /dev/null +++ b/src/main/java/seedu/address/model/group/exceptions/GroupNotFoundException.java @@ -0,0 +1,6 @@ +package seedu.address.model.group.exceptions; + +/** + * Signals that the operation is unable to find the specified group. + */ +public class GroupNotFoundException extends RuntimeException{} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java index 173f15b9b00..e6a9dfc6b62 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/person/Name.java @@ -10,13 +10,14 @@ public class Name { public static final String MESSAGE_CONSTRAINTS = - "Names should only contain alphanumeric characters and spaces, and it should not be blank"; + "Name must not be blank, and it must contain only alphanumeric characters, spaces," + + " apostrophes, slashes, and dashes."; /* * The first character of the address must not be a whitespace, * otherwise " " (a blank string) becomes a valid input. */ - public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} /\\-']*"; public final String fullName; diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index abe8c46b535..b1d4b0bb5a5 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -7,14 +7,18 @@ import java.util.Objects; import java.util.Set; +import javafx.scene.layout.Region; import seedu.address.commons.util.ToStringBuilder; import seedu.address.model.tag.Tag; +import seedu.address.ui.PersonCard; +import seedu.address.ui.Result; +import seedu.address.ui.UiPart; /** * Represents a Person in the address book. * Guarantees: details are present and not null, field values are validated, immutable. */ -public class Person { +public class Person implements Result { // Identity fields private final Name name; @@ -114,4 +118,8 @@ public String toString() { .toString(); } + @Override + public UiPart createCard(int displayedIndex) { + return new PersonCard(this, displayedIndex); + } } diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java index d733f63d739..fc8817a448a 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/seedu/address/model/person/Phone.java @@ -11,8 +11,8 @@ public class Phone { public static final String MESSAGE_CONSTRAINTS = - "Phone numbers should only contain numbers, and it should be at least 3 digits long"; - public static final String VALIDATION_REGEX = "\\d{3,}"; + "Phone numbers should only contain numbers, and it should contain between 3 and 15 digits"; + public static final String VALIDATION_REGEX = "\\d{3,15}"; public final String value; /** diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java index f1a0d4e233b..5fd852c8cdb 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/seedu/address/model/tag/Tag.java @@ -9,8 +9,8 @@ */ public class Tag { - public static final String MESSAGE_CONSTRAINTS = "Tags names should be alphanumeric"; - public static final String VALIDATION_REGEX = "\\p{Alnum}+"; + public static final String MESSAGE_CONSTRAINTS = "Tag names should be alphanumeric!"; + public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} /\\-']*"; public final String tagName; diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facf..6e00e826bab 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -1,11 +1,13 @@ package seedu.address.model.util; +import java.util.ArrayList; import java.util.Arrays; import java.util.Set; import java.util.stream.Collectors; import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.group.Group; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; @@ -39,12 +41,20 @@ public static Person[] getSamplePersons() { getTagSet("colleagues")) }; } + public static Group[] getSampleGroups() { + return new Group[] { + new Group(new String("Group 1"), new ArrayList(Arrays.asList(getSamplePersons()))) + }; + } public static ReadOnlyAddressBook getSampleAddressBook() { AddressBook sampleAb = new AddressBook(); for (Person samplePerson : getSamplePersons()) { sampleAb.addPerson(samplePerson); } + for (Group sampleGroup : getSampleGroups()) { + sampleAb.addGroup(sampleGroup); + } return sampleAb; } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedAssignment.java b/src/main/java/seedu/address/storage/JsonAdaptedAssignment.java new file mode 100644 index 00000000000..f15d15f8b6b --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedAssignment.java @@ -0,0 +1,57 @@ +package seedu.address.storage; + +import java.time.LocalDate; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.assignment.Assignment; +import seedu.address.model.person.Name; + + +/** + * Jackson-friendly version of {@link Assignment}. + */ +public class JsonAdaptedAssignment { + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Person's %s field is missing!"; + private String name; + private LocalDate deadline; + private Float penalty; + + /** + * Creates a {@code JsonAdaptedAssignment} from the given details. + */ + @JsonCreator + public JsonAdaptedAssignment(@JsonProperty("name") String name, @JsonProperty("date") LocalDate deadline, + @JsonProperty("penalty") Float penalty) { + this.name = name; + this.deadline = deadline; + this.penalty = penalty; + } + /** + * Converts a given {@code Assignment} into this class for Jackson use. + */ + public JsonAdaptedAssignment(Assignment source) { + this.name = source.getName(); + this.deadline = source.getDeadline(); + this.penalty = source.getPenalty(); + } + /** + * Converts this Jackson-friendly adapted group object into the model's {@code Assignment} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted person. + */ + public Assignment toModelType() throws IllegalValueException { + if (name == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); + } + if (!Assignment.isValidName(name)) { + throw new IllegalValueException(Assignment.MESSAGE_CONSTRAINTS); + } + final String modelName = name; + final LocalDate modelDate = deadline; + final Float modelPenalty = penalty; + return new Assignment(modelName, modelDate, modelPenalty); + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedGroup.java b/src/main/java/seedu/address/storage/JsonAdaptedGroup.java new file mode 100644 index 00000000000..e27c2520d7f --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedGroup.java @@ -0,0 +1,127 @@ +package seedu.address.storage; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.ArrayListMap; +import seedu.address.model.AddressBook; +import seedu.address.model.assignment.Assignment; +import seedu.address.model.group.Group; +import seedu.address.model.group.GroupMemberDetail; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.exceptions.PersonNotFoundException; +import seedu.address.model.tag.Tag; + +/** + * Jackson-friendly version of {@link Group}. + */ +class JsonAdaptedGroup { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Group's %s field is missing!"; + + private static final Logger logger = LogsCenter.getLogger(JsonAdaptedGroup.class); + + private final String name; + + private final ArrayListMap groupMembers = new ArrayListMap<>(); + private final List tags = new ArrayList<>(); + private final ArrayList assignments = new ArrayList<>(); + + /** + * Constructs a {@code JsonAdaptedGroup} with the given person details. + */ + @JsonCreator + public JsonAdaptedGroup(@JsonProperty("name") String name, + @JsonProperty("persons") ArrayListMap + persons, + @JsonProperty("tags") List tags, + @JsonProperty("assignments") List assignments) { + this.name = name; + if (persons != null) { + this.groupMembers.putAll(persons); + } + if (tags != null) { + this.tags.addAll(tags); + } + if (assignments != null) { + this.assignments.addAll(assignments); + } + } + + /** + * Converts a given {@code Group} into this class for Jackson use. + */ + public JsonAdaptedGroup(Group source) { + name = source.getGroupName(); + for (Map.Entry entry : source.getGroupMembersMap().entrySet()) { + String key = entry.getKey().getName().fullName; + JsonAdaptedGroupMemberDetails value = new JsonAdaptedGroupMemberDetails(entry.getValue()); + this.groupMembers.put(key, value); + } + tags.addAll(source.getTags().stream() + .map(JsonAdaptedTag::new) + .collect(Collectors.toList())); + + assignments.addAll(source.getAssignments().stream() + .map(JsonAdaptedAssignment::new) + .collect(Collectors.toList())); + } + + /** + * Converts this Jackson-friendly adapted group object into the model's {@code Group} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted person. + */ + public Group toModelType(AddressBook addressBook) throws IllegalValueException { + final ArrayListMap modelGroupMembers = new ArrayListMap<>(); + for (Map.Entry entry : groupMembers.entrySet()) { + String personName = entry.getKey(); + GroupMemberDetail groupMemberDetail; + Person person; + try { + person = addressBook.getPerson(personName); + groupMemberDetail = entry.getValue().toModelType(person); + modelGroupMembers.put(person, groupMemberDetail); + } catch (PersonNotFoundException e) { + // Person not found in addressbook, remove from group as well. + logger.info("Person in Group datafile not found in Address Book:" + personName + + ". Removing from Group data."); + continue; + } + } + + final List modelTags = new ArrayList<>(); + for (JsonAdaptedTag tag : tags) { + modelTags.add(tag.toModelType()); + } + + if (name == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); + } + if (!Group.isValidGroupName(name)) { + throw new IllegalValueException(Group.MESSAGE_CONSTRAINTS); + } + final String modelName = name; + + final List modelAssignments = new ArrayList<>(); + for (JsonAdaptedAssignment assignment : assignments) { + modelAssignments.add(assignment.toModelType()); + } + + Group modelGroup = new Group(modelName, modelGroupMembers, modelTags, modelAssignments); + // Set all GroupMemberDetail.group to this + for (GroupMemberDetail value : modelGroupMembers.values()) { + value.setGroup(modelGroup); + } + return modelGroup; + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedGroupMemberDetails.java b/src/main/java/seedu/address/storage/JsonAdaptedGroupMemberDetails.java new file mode 100644 index 00000000000..66ca8b3504a --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedGroupMemberDetails.java @@ -0,0 +1,90 @@ +package seedu.address.storage; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.ArrayListMap; +import seedu.address.model.assignment.Assignment; +import seedu.address.model.group.GroupMemberDetail; +import seedu.address.model.group.GroupMemberDetail.Role; +import seedu.address.model.person.Person; +/** + * Jackson-friendly version of {@link GroupMemberDetail}. + */ +public class JsonAdaptedGroupMemberDetails { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "GroupMember's %s field is missing!"; + + private Role role; + private List attendance = new ArrayList<>(); + private ArrayListMap grades = new ArrayListMap<>(); + private ArrayListMap assignments = new ArrayListMap<>(); + + /** + * Constructs a {@code JsonAdaptedGroupMemberDetails} from the given details. + */ + @JsonCreator + public JsonAdaptedGroupMemberDetails(@JsonProperty("Role") Role role, + @JsonProperty("attendance") List attendance, + @JsonProperty("grades") + ArrayListMap grades, + @JsonProperty("assignments") + ArrayListMap assignments) { + this.role = role; + if (attendance != null) { + this.attendance.addAll(attendance); + } + if (grades != null) { + this.grades.putAll(grades); + } + if (assignments != null) { + this.assignments.putAll(assignments); + } + } + + /** + * Converts a given {@code GroupMemberDetail} into this class for Jackson use. + */ + public JsonAdaptedGroupMemberDetails(GroupMemberDetail source) { + this.role = source.getRole(); + for (boolean attendance : source.getAttendance()) { + this.attendance.add(attendance); + }; + for (Map.Entry entry : source.getGrades().entrySet()) { + Float valueFloat = entry.getValue(); + JsonAdaptedAssignment valueAssignment = new JsonAdaptedAssignment(entry.getKey()); + String key = entry.getKey().getName(); + this.grades.put(key, valueFloat); + this.assignments.put(key, valueAssignment); + } + } + /** + * Converts this Jackson-friendly adapted group object into the model's {@code GroupMemberDetail} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted person. + */ + public GroupMemberDetail toModelType(Person person) throws IllegalValueException { + requireNonNull(person); + Person modelPerson = person; + Role modelRole = this.role; + boolean[] modelAttendance = new boolean[GroupMemberDetail.WEEKS_PER_SEMESTER]; + for (int i = 0; i < GroupMemberDetail.WEEKS_PER_SEMESTER; i++) { + modelAttendance[i] = this.attendance.get(i); + } + ArrayListMap modelGrade = new ArrayListMap<>(); + + for (Map.Entry entry : this.assignments.entrySet()) { + Assignment key = entry.getValue().toModelType(); + Float value = this.grades.get(entry.getKey()); + modelGrade.put(key, value); + } + return new GroupMemberDetail(modelPerson, modelRole, modelAttendance, modelGrade); + } +} diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java index 5efd834091d..4d7c36e239e 100644 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java @@ -11,6 +11,7 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.group.Group; import seedu.address.model.person.Person; /** @@ -20,15 +21,20 @@ class JsonSerializableAddressBook { public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; + public static final String MESSAGE_DUPLICATE_GROUP = "Groups list contains duplicate group(s)."; private final List persons = new ArrayList<>(); + private final List groups = new ArrayList<>(); /** - * Constructs a {@code JsonSerializableAddressBook} with the given persons. + * Constructs a {@code JsonSerializableAddressBook} with the given persons and groups. */ @JsonCreator - public JsonSerializableAddressBook(@JsonProperty("persons") List persons) { + public JsonSerializableAddressBook( + @JsonProperty("persons") List persons, + @JsonProperty("groups") List groups) { this.persons.addAll(persons); + this.groups.addAll(groups); } /** @@ -38,6 +44,7 @@ public JsonSerializableAddressBook(@JsonProperty("persons") List { + private static final String FXML = "DetailBox.fxml"; + + @FXML + private FlowPane tags; + @FXML + private Label name; + @FXML + private Label numStudent; + @FXML + private Label numTa; + @FXML + private Label numProf; + @FXML + private Label numAssignment; + + /** + * Construct a DetailBox for UI with the given Group + */ + public DetailBox(Group group) { + super(FXML); + update(group); + } + + /** + * Update group to be shown in the UI + */ + public void update(Group group) { + name.setText("Group Name: " + group.getGroupName()); + ArrayListMap groupMembers = group.getGroupMembersMap(); + int numStudents = 0; + int numTAs = 0; + int numLecturer = 0; + for (GroupMemberDetail detail : groupMembers.values()) { + switch (detail.getRole()) { + case Student: + numStudents++; + break; + case TeachingAssistant: + numTAs++; + break; + case Lecturer: + numLecturer++; + break; + default: + break; + } + } + numStudent.setText("No. Students: " + numStudents); + numTa.setText("No. TAs: " + numTAs); + numProf.setText("No. Professors: " + numLecturer); + final var assignments = group.getAssignments(); + numAssignment.setText("Assignments (" + assignments.size() + "): [\n" + group.getAssignments().stream() + .map(Messages::format).reduce("", (a, b) -> a + " " + b + "\n") + "]"); + tags.getChildren().clear(); + group.getTags().stream() + .sorted(Comparator.comparing(tag -> tag.tagName)) + .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + } +} diff --git a/src/main/java/seedu/address/ui/GroupCard.java b/src/main/java/seedu/address/ui/GroupCard.java new file mode 100644 index 00000000000..dfa3494d132 --- /dev/null +++ b/src/main/java/seedu/address/ui/GroupCard.java @@ -0,0 +1,107 @@ +package seedu.address.ui; + +import java.util.ArrayList; +import java.util.Comparator; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.address.model.group.Group; +import seedu.address.model.person.Person; + +/** + * A UI component that displays information of a {@code Group}. + */ +public class GroupCard extends UiPart { + + private static final String FXML = "GroupListCard.fxml"; + private static final int MAX_MEMBERS_TO_DISPLAY = 999; + private static final String MORE_MEMBERS_LABEL = "...and %d more"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + public final Group group; + + @FXML + private HBox cardPane; + @FXML + private Label name; + @FXML + private Label id; + @FXML + private FlowPane tags; + @FXML + private FlowPane members; + @FXML + private Label memberCount; + + /** + * Creates a {@code GroupCode} with the given {@code Group} and index to display. + */ + public GroupCard(Group group, int displayedIndex) { + super(FXML); + this.group = group; + id.setText(displayedIndex + ". "); + name.setText(group.getGroupName()); + + // Display tags + group.getTags().stream() + .sorted(Comparator.comparing(tag -> tag.tagName)) + .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + + // Display first 3 members + displayTruncatedMembers(); + } + + + /** + * Displays the first MAX_MEMBERS_TO_DISPLAY members of the group, + * and indicates how many more members there are if the group has more members. + */ + private void displayTruncatedMembers() { + ArrayList groupMembers = group.getGroupMembers(); + int totalMembers = groupMembers.size(); + + // Display count of members + memberCount.setText(totalMembers + " members"); + + if (totalMembers == 0) { + return; + } + + // Create a single label with comma-separated names + StringBuilder memberListText = new StringBuilder(); + + // Add the first few members with commas + int membersToShow = Math.min(totalMembers, MAX_MEMBERS_TO_DISPLAY); + for (int i = 0; i < membersToShow; i++) { + Person person = groupMembers.get(i); + memberListText.append(person.getName().fullName); + + // Add comma and space if not the last member + if (i < totalMembers - 1) { + memberListText.append(", "); + } + } + + // Add the members label + Label memberLabel = new Label(memberListText.toString()); + memberLabel.getStyleClass().add("member-label"); + members.getChildren().add(memberLabel); + + // Add an indicator if there are more members than shown + if (totalMembers > MAX_MEMBERS_TO_DISPLAY) { + Label moreLabel = new Label(String.format(MORE_MEMBERS_LABEL, totalMembers - MAX_MEMBERS_TO_DISPLAY)); + moreLabel.getStyleClass().add("more-members-label"); + members.getChildren().add(moreLabel); + } + } +} diff --git a/src/main/java/seedu/address/ui/GroupDetailCard.java b/src/main/java/seedu/address/ui/GroupDetailCard.java new file mode 100644 index 00000000000..5843eea2f4d --- /dev/null +++ b/src/main/java/seedu/address/ui/GroupDetailCard.java @@ -0,0 +1,66 @@ +package seedu.address.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.address.model.group.GroupMemberDetail; + +/** + * A UI component that displays information of a {@code GroupMemberDetail}. + */ +public class GroupDetailCard extends UiPart { + private static final String FXML = "GroupDetailListCard.fxml"; + private static final int MAX_MEMBERS_TO_DISPLAY = 3; + private static final String MORE_MEMBERS_LABEL = "...and %d more"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + public final GroupMemberDetail detail; + + @FXML + private HBox cardPane; + @FXML + private Label id; + @FXML + private Label name; + @FXML + private Label role; + @FXML + private Label attendance; + + /** + * Creates a {@code GroupCode} with the given {@code Group} and index to display. + */ + public GroupDetailCard(GroupMemberDetail detail, int displayedIndex) { + super(FXML); + + this.detail = detail; + id.setText("Member #" + displayedIndex + " of " + detail.getGroup().size()); + name.setText("Name: " + detail.getPerson().getName().fullName); + role.setText("Role: " + detail.getRole().toString()); + + StringBuilder attendanceSb = new StringBuilder(); + attendanceSb.append("Attendance: "); + var detailAttendance = detail.getAttendance(); + boolean hasAttended = false; + for (int i = 0; i < detailAttendance.length; ++i) { + if (detailAttendance[i]) { + attendanceSb.append("W").append(i + 1).append(" "); + hasAttended = true; + } + } + if (!hasAttended) { + attendanceSb.append("None"); + } + attendance.setText(attendanceSb.toString()); + + // TODO: show assignment scores + } +} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 3f16b2fcf26..409f8b00519 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/address/ui/HelpWindow.java @@ -15,7 +15,7 @@ */ public class HelpWindow extends UiPart { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; + public static final String USERGUIDE_URL = "https://ay2425s2-cs2103t-t12-1.github.io/tp/UserGuide.html"; public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 79e74ef37c0..4a5b3f3ccf4 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -16,6 +16,7 @@ import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.group.Group; /** * The Main Window. Provides the basic application layout containing @@ -31,9 +32,10 @@ public class MainWindow extends UiPart { private Logic logic; // Independent Ui parts residing in this Ui container - private PersonListPanel personListPanel; + private ResultListPanel resultListPanel; private ResultDisplay resultDisplay; private HelpWindow helpWindow; + private DetailBox detailBox; @FXML private StackPane commandBoxPlaceholder; @@ -42,7 +44,7 @@ public class MainWindow extends UiPart { private MenuItem helpMenuItem; @FXML - private StackPane personListPanelPlaceholder; + private StackPane resultListPanelPlaceholder; @FXML private StackPane resultDisplayPlaceholder; @@ -50,6 +52,9 @@ public class MainWindow extends UiPart { @FXML private StackPane statusbarPlaceholder; + @FXML + private StackPane detailBoxPlaceholder; + /** * Creates a {@code MainWindow} with the given {@code Stage} and {@code Logic}. */ @@ -78,6 +83,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) { @@ -110,8 +116,8 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { * Fills up all the placeholders of this window. */ void fillInnerParts() { - personListPanel = new PersonListPanel(logic.getFilteredPersonList()); - personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + resultListPanel = new ResultListPanel(logic.getResultList()); + resultListPanelPlaceholder.getChildren().add(resultListPanel.getRoot()); resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); @@ -121,6 +127,30 @@ void fillInnerParts() { CommandBox commandBox = new CommandBox(this::executeCommand); commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); + + hideGroupDetails(); + } + + /** + * Toggle show on the detail box to show it. + */ + public void showGroupDetails(Group group) { + if (detailBox == null) { + detailBox = new DetailBox(group); + detailBoxPlaceholder.getChildren().add(detailBox.getRoot()); + } else { + detailBox.update(group); // Add an update method in DetailBox to refresh its content + } + detailBoxPlaceholder.setVisible(true); + detailBoxPlaceholder.setManaged(true); + } + + /** + * Toggle hide on the details box so that it does not show. + */ + public void hideGroupDetails() { + detailBoxPlaceholder.setVisible(false); + detailBoxPlaceholder.setManaged(false); } /** @@ -163,10 +193,6 @@ private void handleExit() { primaryStage.hide(); } - public PersonListPanel getPersonListPanel() { - return personListPanel; - } - /** * Executes the command and returns the result. * @@ -186,6 +212,13 @@ private CommandResult executeCommand(String commandText) throws CommandException handleExit(); } + if (commandResult.isShowGroupDetails()) { + Group groupToShow = commandResult.getGroupToShow(); + showGroupDetails(groupToShow); + } else { + hideGroupDetails(); + } + return commandResult; } catch (CommandException | ParseException e) { logger.info("An error occurred while executing command: " + commandText); diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/seedu/address/ui/PersonListPanel.java deleted file mode 100644 index f4c501a897b..00000000000 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ /dev/null @@ -1,49 +0,0 @@ -package seedu.address.ui; - -import java.util.logging.Logger; - -import javafx.collections.ObservableList; -import javafx.fxml.FXML; -import javafx.scene.control.ListCell; -import javafx.scene.control.ListView; -import javafx.scene.layout.Region; -import seedu.address.commons.core.LogsCenter; -import seedu.address.model.person.Person; - -/** - * Panel containing the list of persons. - */ -public class PersonListPanel extends UiPart { - private static final String FXML = "PersonListPanel.fxml"; - private final Logger logger = LogsCenter.getLogger(PersonListPanel.class); - - @FXML - private ListView personListView; - - /** - * Creates a {@code PersonListPanel} with the given {@code ObservableList}. - */ - public PersonListPanel(ObservableList personList) { - super(FXML); - personListView.setItems(personList); - personListView.setCellFactory(listView -> new PersonListViewCell()); - } - - /** - * Custom {@code ListCell} that displays the graphics of a {@code Person} using a {@code PersonCard}. - */ - class PersonListViewCell extends ListCell { - @Override - protected void updateItem(Person person, boolean empty) { - super.updateItem(person, empty); - - if (empty || person == null) { - setGraphic(null); - setText(null); - } else { - setGraphic(new PersonCard(person, getIndex() + 1).getRoot()); - } - } - } - -} diff --git a/src/main/java/seedu/address/ui/Result.java b/src/main/java/seedu/address/ui/Result.java new file mode 100644 index 00000000000..4c20bc5534a --- /dev/null +++ b/src/main/java/seedu/address/ui/Result.java @@ -0,0 +1,16 @@ +package seedu.address.ui; + +import javafx.scene.layout.Region; + +/** + * Represents an item in the result list in the UI. + */ +public interface Result { + /** + * Returns a custom graphic that displays the contents of this result in a ListView. + * + * @param displayedIndex the index of the item in the list. + * @return the custom graphic. + */ + public UiPart createCard(int displayedIndex); +} diff --git a/src/main/java/seedu/address/ui/ResultListPanel.java b/src/main/java/seedu/address/ui/ResultListPanel.java new file mode 100644 index 00000000000..f975dedcdae --- /dev/null +++ b/src/main/java/seedu/address/ui/ResultListPanel.java @@ -0,0 +1,48 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; + +/** + * Panel containing the list of results. + */ +public class ResultListPanel extends UiPart { + private static final String FXML = "ResultListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(ResultListPanel.class); + + @FXML + private ListView resultListView; + + /** + * Creates a {@code ResultListPanel} with the given {@code ObservableList}. + */ + public ResultListPanel(ObservableList resultList) { + super(FXML); + resultListView.setItems(resultList); + resultListView.setCellFactory(listView -> new ResultListViewCell()); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Result}. + */ + class ResultListViewCell extends ListCell { + @Override + protected void updateItem(Result result, boolean empty) { + super.updateItem(result, empty); + + if (empty || result == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(result.createCard(getIndex() + 1).getRoot()); + } + } + } + +} diff --git a/src/main/resources/view/AttendanceCard.fxml b/src/main/resources/view/AttendanceCard.fxml new file mode 100644 index 00000000000..f864782eea5 --- /dev/null +++ b/src/main/resources/view/AttendanceCard.fxml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/AttendanceStyles.css b/src/main/resources/view/AttendanceStyles.css new file mode 100644 index 00000000000..816ce049c40 --- /dev/null +++ b/src/main/resources/view/AttendanceStyles.css @@ -0,0 +1,38 @@ +/* Attendance panel styling */ +.attendance-pane { + -fx-padding: 5 0 5 0; + -fx-hgap: 7; + -fx-vgap: 5; +} + +.attendance-present { + -fx-padding: 2 5 2 5; + -fx-border-radius: 3; + -fx-background-radius: 3; + -fx-font-size: 11; + -fx-text-fill: white; + -fx-background-color: #2E8B57; + -fx-opacity: 0.9; +} + +.attendance-absent { + -fx-padding: 2 5 2 5; + -fx-border-radius: 3; + -fx-background-radius: 3; + -fx-font-size: 11; + -fx-text-fill: white; + -fx-background-color: #CD5C5C; + -fx-opacity: 0.9; +} + +.attendance-summary { + -fx-padding: 2 5 2 5; + -fx-border-radius: 3; + -fx-background-radius: 3; + -fx-font-size: 11; + -fx-font-weight: bold; + -fx-text-fill: white; + -fx-background-color: #4682B4; + -fx-opacity: 0.9; + -fx-margin-top: 5; +} diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8..d95f646f6be 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -132,6 +132,13 @@ -fx-text-fill: #010504; } +.detail_card { + -fx-font-family: "Segoe UI"; + -fx-font-size: 15px; + -fx-text-fill: white; + -fx-padding : 2px 2px; +} + .stack-pane { -fx-background-color: derive(#1d1d1d, 20%); } @@ -350,3 +357,25 @@ -fx-background-radius: 2; -fx-font-size: 11; } + +/* Additional styles for member display in groups */ + +.members-pane { + -fx-hgap: 4; + -fx-vgap: 4; +} + +.member-label { + -fx-padding: 1 3 1 3; + -fx-background-radius: 3; + -fx-border-radius: 3; + -fx-font-size: 10pt; +} + +.more-members-label { + -fx-padding: 1 3 1 3; + -fx-text-fill: #555555; + -fx-font-style: italic; + -fx-font-size: 9pt; +} + diff --git a/src/main/resources/view/DetailBox.fxml b/src/main/resources/view/DetailBox.fxml new file mode 100644 index 00000000000..910b72da05a --- /dev/null +++ b/src/main/resources/view/DetailBox.fxml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/src/main/resources/view/GroupDetailListCard.fxml b/src/main/resources/view/GroupDetailListCard.fxml new file mode 100644 index 00000000000..d5a44afdca5 --- /dev/null +++ b/src/main/resources/view/GroupDetailListCard.fxml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/GroupListCard.fxml b/src/main/resources/view/GroupListCard.fxml new file mode 100644 index 00000000000..ba9a5ec610e --- /dev/null +++ b/src/main/resources/view/GroupListCard.fxml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 7778f666a0a..533ae292a4b 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -12,7 +12,7 @@ + title="TAbby Dabby" minWidth="450" minHeight="600" onCloseRequest="#handleExit"> @@ -40,17 +40,24 @@ + minHeight="100" prefHeight="300" maxHeight="300"> - + + + + + + + - + diff --git a/src/main/resources/view/PersonListPanel.fxml b/src/main/resources/view/ResultListPanel.fxml similarity index 77% rename from src/main/resources/view/PersonListPanel.fxml rename to src/main/resources/view/ResultListPanel.fxml index a1bb6bbace8..34dbd0ecb9d 100644 --- a/src/main/resources/view/PersonListPanel.fxml +++ b/src/main/resources/view/ResultListPanel.fxml @@ -4,5 +4,5 @@ - + diff --git a/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json b/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json index 6a4d2b7181c..9198f848c64 100644 --- a/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json +++ b/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json @@ -9,5 +9,10 @@ "phone": "948asdf2424", "email": "hans@example.com", "address": "4th street" + } ], + "groups" : [ { + "name" : "Group1", + "persons" : [ ], + "tags" : [ ] } ] } diff --git a/src/test/data/JsonAddressBookStorageTest/invalidPersonAddressBook.json b/src/test/data/JsonAddressBookStorageTest/invalidPersonAddressBook.json index ccd21f7d1a9..3e08ab33c58 100644 --- a/src/test/data/JsonAddressBookStorageTest/invalidPersonAddressBook.json +++ b/src/test/data/JsonAddressBookStorageTest/invalidPersonAddressBook.json @@ -4,5 +4,10 @@ "phone": "9482424", "email": "hans@example.com", "address": "4th street" + } ], + "groups" : [ { + "name" : "Group1", + "persons" : [ ], + "tags" : [ ] } ] } diff --git a/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json index a7427fe7aa2..94c481d3a06 100644 --- a/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json @@ -10,5 +10,11 @@ "phone": "94351253", "email": "pauline@example.com", "address": "4th street" + } ], + "groups" : [ { + "name" : "Group1", + "tags" : [ ], + "groupMembers" : { + } } ] } diff --git a/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json index ad3f135ae42..eaa53c48736 100644 --- a/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json @@ -4,5 +4,12 @@ "phone": "9482424", "email": "invalid@email!3e", "address": "4th street" + } ], + + "groups" : [ { + "name" : "Group1", + "tags" : [ ], + "groupMembers" : { + } } ] } diff --git a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json index 72262099d35..ec296e62adf 100644 --- a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json @@ -42,5 +42,11 @@ "email" : "anna@example.com", "address" : "4th street", "tags" : [ ] + } ], + "groups" : [ { + "name" : "Group1", + "tags" : [ ], + "groupMembers" : { + } } ] } diff --git a/src/test/java/seedu/address/commons/util/ArrayListMapTest.java b/src/test/java/seedu/address/commons/util/ArrayListMapTest.java new file mode 100644 index 00000000000..de0c39c9e9e --- /dev/null +++ b/src/test/java/seedu/address/commons/util/ArrayListMapTest.java @@ -0,0 +1,83 @@ +package seedu.address.commons.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.address.model.group.Group; +import seedu.address.model.group.GroupMemberDetail; +import seedu.address.model.person.Person; +import seedu.address.testutil.GroupBuilder; +import seedu.address.testutil.PersonBuilder; + +public class ArrayListMapTest { + private ArrayListMap members; + private Person p; + private Group g; + private GroupMemberDetail detail; + + @BeforeEach + public void setUp() { + members = new ArrayListMap<>(); + p = new PersonBuilder().build(); + g = new GroupBuilder().build(); + detail = new GroupMemberDetail(p, g); + } + + @Test + public void init_success() { + assertEquals(0, members.size()); + assertTrue(members.isEmpty()); + } + + @Test + public void put_success() { + GroupMemberDetail prev = members.put(p, detail); + + assertNull(prev); + assertFalse(members.isEmpty()); + assertEquals(1, members.size()); + assertTrue(members.containsKey(p)); + assertEquals(detail, members.get(p)); + } + + @Test + public void put_existingKey_success() { + members.put(p, detail); + + Group g2 = new GroupBuilder().withName("New Name").build(); + GroupMemberDetail detail2 = new GroupMemberDetail(p, g2); + GroupMemberDetail prev = members.put(p, detail2); + + assertEquals(detail, prev); + assertTrue(members.containsKey(p)); + assertEquals(detail2, members.get(p)); + assertEquals(1, members.size()); + } + + @Test + public void keySet_success() { + members.put(p, detail); + + var keySet = members.keySet(); + + assertTrue(keySet.contains(p)); + assertEquals(1, members.size()); + } + + @Test + public void containsKey_exists_success() { + members.put(p, detail); + + assertTrue(members.containsKey(p)); + } + + @Test + public void containsKey_doesNotExist_success() { + assertFalse(members.containsKey(p)); + } +} diff --git a/src/test/java/seedu/address/commons/util/ArrayListSetTest.java b/src/test/java/seedu/address/commons/util/ArrayListSetTest.java new file mode 100644 index 00000000000..233e6435690 --- /dev/null +++ b/src/test/java/seedu/address/commons/util/ArrayListSetTest.java @@ -0,0 +1,127 @@ +package seedu.address.commons.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.address.model.person.Person; +import seedu.address.testutil.PersonBuilder; + +public class ArrayListSetTest { + private ArrayListSet persons; + + @BeforeEach + public void setUp() { + persons = new ArrayListSet<>(); + } + + @Test + public void init_success() { + assertEquals(0, persons.size()); + assertTrue(persons.isEmpty()); + } + + @Test + public void add_success() { + Person p = new PersonBuilder().build(); + boolean added = persons.add(p); + + assertTrue(added); + assertEquals(1, persons.size()); + assertTrue(persons.contains(p)); + } + + @Test + public void add_duplicatePerson_doesNotChangeTheSet() { + Person p = new PersonBuilder().build(); + persons.add(p); + boolean readded = persons.add(p); + + assertFalse(readded); + assertEquals(1, persons.size()); + } + + @Test + public void remove_validPerson_success() { + Person p = new PersonBuilder().build(); + persons.add(p); + + int oldSize = persons.size(); + boolean removed = persons.remove(p); + int newSize = persons.size(); + + assertTrue(removed); + assertFalse(persons.contains(p)); + assertEquals(oldSize - 1, newSize); + } + + @Test + public void remove_invalidPerson_doesNotChangeTheSet() { + Person p = new PersonBuilder().build(); + persons.add(p); + + Person p2 = new PersonBuilder().withName("New name 1").build(); + int oldSize = persons.size(); + boolean removed = persons.remove(p2); + int newSize = persons.size(); + + assertFalse(removed); + assertTrue(persons.contains(p)); + assertFalse(persons.contains(p2)); + assertEquals(oldSize, newSize); + } + + @Test + public void remove_validIndex_success() { + Person p = new PersonBuilder().build(); + persons.add(p); + + int oldSize = persons.size(); + boolean removed = persons.remove(0); + int newSize = persons.size(); + + assertTrue(removed); + assertFalse(persons.contains(p)); + assertEquals(oldSize - 1, newSize); + } + + @Test + public void remove_invalidIndex_throwsIndexOutOfBoundsException() { + assertThrows(IndexOutOfBoundsException.class, () -> persons.remove(persons.size())); + assertThrows(IndexOutOfBoundsException.class, () -> persons.remove(-1)); + } + + @Test + public void indexOf_success() { + Person p = new PersonBuilder().build(); + persons.add(p); + Person p2 = new PersonBuilder().withName("New name 1").build(); + persons.add(p2); + + int pIndex = persons.indexOf(p); + int p2Index = persons.indexOf(p2); + + assertEquals(0, pIndex); + assertEquals(1, p2Index); + + Person p3 = new PersonBuilder().withName("New name 2").build(); + int p3Index = persons.indexOf(p3); + + assertEquals(-1, p3Index); + } + + @Test + public void clear_success() { + Person p = new PersonBuilder().build(); + persons.add(p); + + persons.clear(); + + assertEquals(0, persons.size()); + assertTrue(persons.isEmpty()); + } +} diff --git a/src/test/java/seedu/address/logic/commands/AddCommandTest.java b/src/test/java/seedu/address/logic/commands/AddCommandTest.java index 90e8253f48e..ecc5849e829 100644 --- a/src/test/java/seedu/address/logic/commands/AddCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/AddCommandTest.java @@ -8,6 +8,7 @@ import static seedu.address.testutil.TypicalPersons.ALICE; import java.nio.file.Path; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Arrays; import java.util.function.Predicate; @@ -22,8 +23,11 @@ import seedu.address.model.Model; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.ReadOnlyUserPrefs; +import seedu.address.model.assignment.Assignment; +import seedu.address.model.group.Group; import seedu.address.model.person.Person; import seedu.address.testutil.PersonBuilder; +import seedu.address.ui.Result; public class AddCommandTest { @@ -153,10 +157,121 @@ public ObservableList getFilteredPersonList() { throw new AssertionError("This method should not be called."); } + @Override + public ObservableList getResultList() { + throw new AssertionError("This method should not be called."); + } + @Override public void updateFilteredPersonList(Predicate predicate) { throw new AssertionError("This method should not be called."); } + + @Override + public void updateFilteredGroupList(Predicate predicate) { + throw new AssertionError("This method should not be called."); + } + + @Override + public boolean hasGroup(Group group) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deleteGroup(Group target) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setGroup(Group target, Group editedGroup) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList getFilteredGroupList() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addGroup(Group group) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void showGroupDetails(Group groupToShow) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addPersonToGroup(Person personToAdd, Group groupToBeAddedTo) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deletePersonFromGroup(Person personToRemove, Group groupToBeRemovedFrom) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deletePersonFromAllGroups(Person personToRemove) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Assignment addAssignmentToGroup(String assignmentName, LocalDate deadline, Group group, Float penalty) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void removeAssignmentFromGroup(String assignmentName, Group group) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void editAssignment(String assignmentName, String newName, LocalDate deadline, Group group, + Float penalty) { + throw new AssertionError("This method should not be called."); + } + + @Override + public boolean isAssignmentInGroup(String assignmentName, Group group) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void gradeAssignment(Person person, Group group, String assignmentName, Float score) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void markAttendance(Person person, Group group, int week) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void unmarkAttendance(Person person, Group group, int week) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Group getGroup(String groupName) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Person getPerson(String personName) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Float getGrade(Person person, Group group, String assignmentName) { + throw new AssertionError("This method should not be called"); + } + + @Override + public boolean isPersonInGroup(Person person, Group group) { + return false; + } } /** diff --git a/src/test/java/seedu/address/logic/parser/DeleteCommandParserTest.java b/src/test/java/seedu/address/logic/parser/DeleteCommandParserTest.java index 6a40e14a649..23a254aa5a4 100644 --- a/src/test/java/seedu/address/logic/parser/DeleteCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/DeleteCommandParserTest.java @@ -1,8 +1,8 @@ package seedu.address.logic.parser; -import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.logic.parser.ParserUtil.MESSAGE_INVALID_INDEX; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; import org.junit.jupiter.api.Test; @@ -27,6 +27,6 @@ public void parse_validArgs_returnsDeleteCommand() { @Test public void parse_invalidArgs_throwsParseException() { - assertParseFailure(parser, "a", String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); + assertParseFailure(parser, "a", MESSAGE_INVALID_INDEX + "\n" + DeleteCommand.MESSAGE_USAGE); } } diff --git a/src/test/java/seedu/address/logic/parser/DeleteGroupCommandParserTest.java b/src/test/java/seedu/address/logic/parser/DeleteGroupCommandParserTest.java new file mode 100644 index 00000000000..236fd435cf3 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/DeleteGroupCommandParserTest.java @@ -0,0 +1,32 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.logic.parser.ParserUtil.MESSAGE_INVALID_INDEX; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.DeleteGroupCommand; + +/** + * As we are only doing white-box testing, our test cases do not cover path variations + * outside of the DeleteGroupCommand code. For example, inputs "1" and "1 abc" take the + * same path through the DeleteGroupCommand, and therefore we test only one of them. + * The path variation for those two cases occur inside the ParserUtil, and + * therefore should be covered by the ParserUtilTest. + */ +public class DeleteGroupCommandParserTest { + + private final DeleteGroupCommandParser parser = new DeleteGroupCommandParser(); + + @Test + public void parse_validArgs_returnsDeleteCommand() { + assertParseSuccess(parser, "1", new DeleteGroupCommand(INDEX_FIRST_PERSON)); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + assertParseFailure(parser, "a", MESSAGE_INVALID_INDEX + "\n" + DeleteGroupCommand.MESSAGE_USAGE); + } +} diff --git a/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java b/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java index cc7175172d4..6e12539f547 100644 --- a/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java @@ -1,6 +1,5 @@ package seedu.address.logic.parser; -import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_BOB; import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_AMY; @@ -28,6 +27,7 @@ import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.logic.parser.ParserUtil.MESSAGE_INVALID_INDEX; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; import static seedu.address.testutil.TypicalIndexes.INDEX_THIRD_PERSON; @@ -50,7 +50,7 @@ public class EditCommandParserTest { private static final String TAG_EMPTY = " " + PREFIX_TAG; private static final String MESSAGE_INVALID_FORMAT = - String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE); + MESSAGE_INVALID_INDEX + "\n" + EditCommand.MESSAGE_USAGE; private EditCommandParser parser = new EditCommandParser(); diff --git a/src/test/java/seedu/address/logic/parser/EditGroupCommandParserTest.java b/src/test/java/seedu/address/logic/parser/EditGroupCommandParserTest.java new file mode 100644 index 00000000000..c1d42284737 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/EditGroupCommandParserTest.java @@ -0,0 +1,74 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.commands.CommandTestUtil.INVALID_EMAIL_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_NAME_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_TAG_DESC; +import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_FRIEND; +import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_HUSBAND; +import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_AMY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_AMY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_AMY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.ParserUtil.MESSAGE_INVALID_INDEX; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.EditGroupCommand; +import seedu.address.model.group.Group; +import seedu.address.model.tag.Tag; + +public class EditGroupCommandParserTest { + + private static final String TAG_EMPTY = " " + PREFIX_TAG; + + private static final String MESSAGE_INVALID_FORMAT = + MESSAGE_INVALID_INDEX + "\n" + EditGroupCommand.MESSAGE_USAGE; + + private final EditGroupCommandParser parser = new EditGroupCommandParser(); + + @Test + public void parse_missingParts_failure() { + // no index specified + assertParseFailure(parser, VALID_NAME_AMY, MESSAGE_INVALID_FORMAT); + + // no field specified + assertParseFailure(parser, "1", EditCommand.MESSAGE_NOT_EDITED); + + // no index and no field specified + assertParseFailure(parser, "", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidPreamble_failure() { + // negative index + assertParseFailure(parser, "-5" + NAME_DESC_AMY, MESSAGE_INVALID_FORMAT); + + // zero index + assertParseFailure(parser, "0" + NAME_DESC_AMY, MESSAGE_INVALID_FORMAT); + + // invalid arguments being parsed as preamble + assertParseFailure(parser, "1 some random string", MESSAGE_INVALID_FORMAT); + + // invalid prefix being parsed as preamble + assertParseFailure(parser, "1 i/ string", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidValue_failure() { + assertParseFailure(parser, "1" + INVALID_NAME_DESC, Group.MESSAGE_CONSTRAINTS); // invalid name + assertParseFailure(parser, "1" + INVALID_TAG_DESC, Tag.MESSAGE_CONSTRAINTS); // invalid tag + + // while parsing {@code PREFIX_TAG} alone will reset the tags of the {@code Person} being edited, + // parsing it together with a valid tag results in error + assertParseFailure(parser, "1" + TAG_DESC_FRIEND + TAG_DESC_HUSBAND + TAG_EMPTY, Tag.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, "1" + TAG_DESC_FRIEND + TAG_EMPTY + TAG_DESC_HUSBAND, Tag.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, "1" + TAG_EMPTY + TAG_DESC_FRIEND + TAG_DESC_HUSBAND, Tag.MESSAGE_CONSTRAINTS); + + // multiple invalid values, but only the first invalid value is captured + assertParseFailure(parser, "1" + INVALID_NAME_DESC + INVALID_EMAIL_DESC + VALID_ADDRESS_AMY + VALID_PHONE_AMY, + Group.MESSAGE_CONSTRAINTS); + } +} diff --git a/src/test/java/seedu/address/logic/parser/ParserUtilTest.java b/src/test/java/seedu/address/logic/parser/ParserUtilTest.java index 4256788b1a7..0ae9ba6674c 100644 --- a/src/test/java/seedu/address/logic/parser/ParserUtilTest.java +++ b/src/test/java/seedu/address/logic/parser/ParserUtilTest.java @@ -27,11 +27,11 @@ public class ParserUtilTest { private static final String INVALID_EMAIL = "example.com"; private static final String INVALID_TAG = "#friend"; - private static final String VALID_NAME = "Rachel Walker"; + private static final String VALID_NAME = "Rachel-Walker d/o Nvidia"; private static final String VALID_PHONE = "123456"; private static final String VALID_ADDRESS = "123 Main Street #0505"; private static final String VALID_EMAIL = "rachel@example.com"; - private static final String VALID_TAG_1 = "friend"; + private static final String VALID_TAG_1 = "friend of Jensen-Huang s/o Nvidia"; private static final String VALID_TAG_2 = "neighbour"; private static final String WHITESPACE = " \t\r\n"; @@ -79,6 +79,29 @@ public void parseName_validValueWithWhitespace_returnsTrimmedName() throws Excep assertEquals(expectedName, ParserUtil.parseName(nameWithWhitespace)); } + @Test + public void parseGroupName_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> ParserUtil.parseGroupName((String) null)); + } + + @Test + public void parseGroupName_invalidValue_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.parseGroupName(INVALID_NAME)); + } + + @Test + public void parseGroupName_validValueWithoutWhitespace_returnsName() throws Exception { + String expectedName = VALID_NAME; + assertEquals(expectedName, ParserUtil.parseGroupName(VALID_NAME)); + } + + @Test + public void parseGroupName_validValueWithWhitespace_returnsTrimmedName() throws Exception { + String nameWithWhitespace = WHITESPACE + VALID_NAME + WHITESPACE; + String expectedName = VALID_NAME; + assertEquals(expectedName, ParserUtil.parseGroupName(nameWithWhitespace)); + } + @Test public void parsePhone_null_throwsNullPointerException() { assertThrows(NullPointerException.class, () -> ParserUtil.parsePhone((String) null)); diff --git a/src/test/java/seedu/address/model/AddressBookTest.java b/src/test/java/seedu/address/model/AddressBookTest.java index 68c8c5ba4d5..0fe0cf8652b 100644 --- a/src/test/java/seedu/address/model/AddressBookTest.java +++ b/src/test/java/seedu/address/model/AddressBookTest.java @@ -18,6 +18,7 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import seedu.address.model.group.Group; import seedu.address.model.person.Person; import seedu.address.model.person.exceptions.DuplicatePersonException; import seedu.address.testutil.PersonBuilder; @@ -85,7 +86,9 @@ public void getPersonList_modifyList_throwsUnsupportedOperationException() { @Test public void toStringMethod() { - String expected = AddressBook.class.getCanonicalName() + "{persons=" + addressBook.getPersonList() + "}"; + String expected = AddressBook.class.getCanonicalName() + + "{persons=" + addressBook.getPersonList() + + ", groups=" + addressBook.getGroupList() + "}"; assertEquals(expected, addressBook.toString()); } @@ -94,6 +97,7 @@ public void toStringMethod() { */ private static class AddressBookStub implements ReadOnlyAddressBook { private final ObservableList persons = FXCollections.observableArrayList(); + private final ObservableList groups = FXCollections.observableArrayList(); AddressBookStub(Collection persons) { this.persons.setAll(persons); @@ -103,6 +107,11 @@ private static class AddressBookStub implements ReadOnlyAddressBook { public ObservableList getPersonList() { return persons; } + + @Override + public ObservableList getGroupList() { + return groups; + } } } diff --git a/src/test/java/seedu/address/testutil/GroupBuilder.java b/src/test/java/seedu/address/testutil/GroupBuilder.java new file mode 100644 index 00000000000..82c09d91326 --- /dev/null +++ b/src/test/java/seedu/address/testutil/GroupBuilder.java @@ -0,0 +1,34 @@ +package seedu.address.testutil; + +import static java.util.Objects.requireNonNull; + +import seedu.address.model.group.Group; + +/** + * A utility class to help with building Group objects. + */ +public class GroupBuilder { + public static final String DEFAULT_NAME = "CS2101 T12"; + + private String name; + + /** + * Creates a {@code GroupBuilder} with the default details. + */ + public GroupBuilder() { + name = DEFAULT_NAME; + } + + /** + * Sets the {@code Name} of the {@code Group} that we are building. + */ + public GroupBuilder withName(String name) { + requireNonNull(name); + this.name = name; + return this; + } + + public Group build() { + return new Group(name); + } +}