diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 00000000000..0cd51c5fb97
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,25 @@
+name: MarkBind Action
+
+on:
+ push:
+ branches:
+ - master
+
+jobs:
+ build_and_deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Install Graphviz
+ run: sudo apt-get install graphviz
+ - name: Install Java
+ uses: actions/setup-java@v3
+ with:
+ java-version: '11'
+ distribution: 'temurin'
+ - name: Build & Deploy MarkBind site
+ uses: MarkBind/markbind-action@v2
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ rootDirectory: './docs'
+ baseUrl: '/tp' # assuming your repo name is tp
+ version: '^5.2.0'
diff --git a/.gitignore b/.gitignore
index 284c4ca7cd9..eab4c7db6a5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,3 +21,4 @@ src/test/data/sandbox/
# MacOS custom attributes files created by Finder
.DS_Store
docs/_site/
+docs/_markbind/logs/
diff --git a/README.md b/README.md
index 13f5c77403f..240b76a7bb1 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,7 @@
-[](https://github.com/se-edu/addressbook-level3/actions)
+[](https://github.com/AY2324S2-CS2103T-F12-2/tp/actions/workflows/gradle.yml)

-* This is **a sample project for Software Engineering (SE) students**.
- Example usages:
- * as a starting point of a course project (as opposed to writing everything from scratch)
- * as a case study
-* The project simulates an ongoing software project for a desktop application (called _AddressBook_) used for managing contact details.
- * It is **written in OOP fashion**. It provides a **reasonably well-written** code base **bigger** (around 6 KLoC) than what students usually write in beginner-level SE modules, without being overwhelmingly big.
- * It comes with a **reasonable level of user and developer documentation**.
-* It is named `AddressBook Level 3` (`AB3` for short) because it was initially created as a part of a series of `AddressBook` projects (`Level 1`, `Level 2`, `Level 3` ...).
-* For the detailed documentation of this project, see the **[Address Book Product Website](https://se-education.org/addressbook-level3)**.
-* This project is a **part of the se-education.org** initiative. If you would like to contribute code to this project, see [se-education.org](https://se-education.org#https://se-education.org/#contributing) for more info.
+An app named Press Planner for freelance journalists that can streamline their workflow by organizing sources, tracking outlets interested in their stories, and managing collaborations with peers/editors. With features like tagging and grouping contacts, it facilitates efficient research, ensuring reporters can quickly reach out and report on breaking stories. It is optimized for use via a Command Line Interface (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, the CLI can be faster than the GUI.
+
+This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org).
diff --git a/build.gradle b/build.gradle
index a2951cc709e..17e88c66631 100644
--- a/build.gradle
+++ b/build.gradle
@@ -25,6 +25,10 @@ test {
finalizedBy jacocoTestReport
}
+run {
+ enableAssertions = true
+}
+
task coverage(type: JacocoReport) {
sourceDirectories.from files(sourceSets.main.allSource.srcDirs)
classDirectories.from files(sourceSets.main.output)
@@ -66,7 +70,7 @@ dependencies {
}
shadowJar {
- archiveFileName = 'addressbook.jar'
+ archiveFileName = 'pressplanner.jar'
}
defaultTasks 'clean', 'test'
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 00000000000..1748e487fbd
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,23 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+_markbind/logs/
+
+# Dependency directories
+node_modules/
+
+# Production build files (change if you output the build to a different directory)
+_site/
+
+# Env
+.env
+.env.local
+
+# IDE configs
+.vscode/
+.idea/*
+*.iml
diff --git a/docs/AboutUs.md b/docs/AboutUs.md
index 1c9514e966a..ad78aa0ab95 100644
--- a/docs/AboutUs.md
+++ b/docs/AboutUs.md
@@ -1,59 +1,54 @@
---
-layout: page
-title: About Us
+ layout: default.md
+ title: "About Us"
---
-We are a team based in the [School of Computing, National University of Singapore](http://www.comp.nus.edu.sg).
+# About Us
-You can reach us at the email `seer[at]comp.nus.edu.sg`
+We are a team based in the [School of Computing, National University of Singapore](http://www.comp.nus.edu.sg).
## Project team
-### John Doe
+### Aung Ko Khant
-
+
-[[homepage](http://www.comp.nus.edu.sg/~damithch)]
-[[github](https://github.com/johndoe)]
-[[portfolio](team/johndoe.md)]
+[[github](https://github.com/Ko-Khan)]
-* Role: Project Advisor
+* Role: Developer
+* Responsibilities: Testing
-### Jane Doe
+### Benny Loh Choon Kiong
-
+
-[[github](http://github.com/johndoe)]
-[[portfolio](team/johndoe.md)]
+[[github](https://github.com/bennyLCK)]
* Role: Team Lead
-* Responsibilities: UI
+* Responsibilities: Integration
-### Johnny Doe
+### Hamish Stewart Dawe
-
+
-[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)]
+[[github](https://github.com/H4mes)]
* Role: Developer
-* Responsibilities: Data
-
-### Jean Doe
+* Responsibilities: Deliverables + Deadlines
+### Hyun Eunkyu
-
+
-[[github](http://github.com/johndoe)]
-[[portfolio](team/johndoe.md)]
+[[github](https://github.com/Howlong11)]
* Role: Developer
-* Responsibilities: Dev Ops + Threading
+* Responsibilities: Scheduling + Tracking
-### James Doe
+### Murugan Maniish
-
+
-[[github](http://github.com/johndoe)]
-[[portfolio](team/johndoe.md)]
+[[github](https://github.com/Murugan-Maniish)]
* Role: Developer
-* Responsibilities: UI
+* Responsibilities: Code Quality
diff --git a/docs/Configuration.md b/docs/Configuration.md
index 13cf0faea16..32f6255f3b9 100644
--- a/docs/Configuration.md
+++ b/docs/Configuration.md
@@ -1,6 +1,8 @@
---
-layout: page
-title: Configuration guide
+ layout: default.md
+ title: "Configuration guide"
---
+# Configuration guide
+
Certain properties of the application can be controlled (e.g user preferences file location, logging level) through the configuration file (default: `config.json`).
diff --git a/docs/DevOps.md b/docs/DevOps.md
index d2fd91a6001..8228c845e86 100644
--- a/docs/DevOps.md
+++ b/docs/DevOps.md
@@ -1,12 +1,15 @@
---
-layout: page
-title: DevOps guide
+ layout: default.md
+ title: "DevOps guide"
+ pageNav: 3
---
-* Table of Contents
-{:toc}
+# DevOps guide
---------------------------------------------------------------------------------------------------------------------
+
+
+
+
## Build automation
diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md
index 1b56bb5d31b..95b73924a6a 100644
--- a/docs/DeveloperGuide.md
+++ b/docs/DeveloperGuide.md
@@ -1,15 +1,24 @@
+
+
---
-layout: page
-title: Developer Guide
+ layout: default.md
+ title: "Developer Guide"
+ pageNav: 3
---
-* Table of Contents
-{:toc}
+
+# PressPlanner Developer Guide
+
+
+
--------------------------------------------------------------------------------------------------------------------
## **Acknowledgements**
-* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well}
+* GitHub CoPilot was used to help speed up tasks like writing documentation, writing JavaDocs, creating TestUtil Classes and autocompleting repetitive method calls. It was also used at times to troubleshoot bugs and help to generate difficult parts of the code (e.g. regex expressions for parsing commands with differing requirements)
+
+
+* Some less commonly used packages of the Java's standard library were also used which includes the java.awt library and the java.net package which are used to implement the `link article` feature.
--------------------------------------------------------------------------------------------------------------------
@@ -21,14 +30,9 @@ 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.
@@ -53,16 +57,16 @@ The bulk of the app's work is done by the following four components:
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.
+* implements its functionality using a concrete `{Component Name}Manager` class which follows the corresponding API `interface` mentioned in the previous point.
For example, the `Logic` component defines its API in the `Logic.java` interface and implements its functionality using the `LogicManager.java` class which follows the `Logic` interface. Other components interact with a given component through its interface rather than the concrete class (reason: to prevent outside component's being coupled to the implementation of a component), as illustrated in the (partial) class diagram below.
-
+
The sections below give more details of each component.
@@ -70,7 +74,7 @@ The sections below give more details of each component.
The **API** of this component is specified in [`Ui.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/Ui.java)
-
+
The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI.
@@ -89,14 +93,16 @@ The `UI` component,
Here's a (partial) class diagram of the `Logic` component:
-
+
The sequence diagram below illustrates the interactions within the `Logic` component, taking `execute("delete 1")` API call as an example.
-
+
+
+
-
:information_source: **Note:** The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline continues till the end of diagram.
-
+**Note:** The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline continues till the end of diagram.
+
How the `Logic` component works:
@@ -108,16 +114,16 @@ How the `Logic` component works:
Here are the other classes in `Logic` (omitted from the class diagram above) that are used for parsing a user command:
-
+
How the parsing works:
-* When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as a `Command` object.
+* When called upon to parse a user command, the `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. If the command deals with Articles then the `AddressBookParser` will pass it to the `ArticleBookParser` which will then handle to command similarly to the `AddressBookParser`.
* 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,
@@ -127,22 +133,24 @@ The `Model` component,
* stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects.
* does not depend on any of the other three components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components)
-
:information_source: **Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
+
-
+**Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
-
+
+
+
### Storage component
**API** : [`Storage.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/storage/Storage.java)
-
+
The `Storage` component,
-* can save both address book data and user preference data in JSON format, and read them back into corresponding objects.
-* inherits from both `AddressBookStorage` and `UserPrefStorage`, which means it can be treated as either one (if only the functionality of only one is needed).
+* can save address book data, article book data and user preference data in JSON format, and read them back into corresponding objects.
+* inherits from `AddressBookStorage`, `ArticleBookStorage` and `UserPrefStorage`, which means it can be treated as any one of these (if only the functionality of only one is needed).
* depends on some classes in the `Model` component (because the `Storage` component's job is to save/retrieve objects that belong to the `Model`)
### Common classes
@@ -155,6 +163,188 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa
This section describes some noteworthy details on how certain features are implemented.
+
+### \[Implemented\] Sort persons feature
+
+#### Implementation
+
+The sort persons command is implemented in the `SortCommand` class. The `SortCommand` class is a subclass of the `PersonCommand` class, which is in turn a subclass of the `Command` class. The `SortCommand` class is responsible for sorting the persons in the address book according to a field present in the `Person` class by overriding the following method in the `Command` class:
+
+* `execute(Model model)` — Sorts the persons in the address book according to a field present in the `Person` class which is mapped to the `prefix` field in the `SortCommand` object.
+
+Given below is an example usage scenario and how the sort persons feature behaves at each step.
+
+Step 1. The user launches the application for the first time. The `AddressBook` will be initialized with the initial address book state.
+
+Step 2. The user executes `sort n/` command. The `sort` command calls `Model#sortAddressBook("n/")` which sorts the persons in the address book by name in increasing lexicographical order.
+
+The following sequence diagram shows how a sort persons operation goes through the `Logic` component:
+
+
+
+Similarly, how a sort persons operation goes through the `Model` component is shown below:
+
+
+
+#### Design considerations:
+
+**Aspect: How sort persons executes:**
+
+* **Alternative 1 (current choice):** Directly sort the `internalList` field present in the `UniquePersonList` object.
+ * Pros: Easy to implement.
+ * Cons: Permanently orders all persons in the `AddressBook` by the `Person` field specified by the related `prefix`.
+
+
+* **Alternative 2:** Clone the current `internalList` field
+ present in the `UniquePersonList` object, sort the clone, then replace the `internalUnmodifiableList` field in the `UniquePersonList` object with the sorted clone.
+ * Pros: Will not permanently order all persons in the `AddressBook` by the `Person` field specified by the related `prefix`, but only the current view displayed to the user which is refreshed for every opening of the application or commands that changes the view (e.g. `List`, `Find` commands).
+ * Cons: Takes up much more memory space directly proportional to the size of the `AddressBook` since a clone of all `Persons` has to be made.
+
+### \[Implemented\] Filter feature
+
+#### Implementation
+
+
+
+The filter mechanism is facilitated by `filter` interface. The ArticleFilter and PersonFilter classes will inherit from it.
+The filters will store `Predicate<>` objects that will determine which Persons or articles will be shown to the user.
+`ModelManager` will contain a filter, which it will use to generate `FilteredLists`
+
+Given below is an example usage scenario:
+
+Step 1. The user launches the application. The `ModelManager` will be initialized, along with the Filter objects it contains. `finalPredicate` will be set to display all articles for now.
+
+Step 2. The user executes `filter -a s/ st/ en/ t/DRAFT` to look for articles he is currently working on. The filter article command gets the `ArticleFilter` object using `getFilter()`. Then it updates the filter object by calling the `updateFilter()` method, changing the `finalPredicate`.
+
+
+
+
+Step 3. Now that the filter has been updated. The user now looks through PressPlanner to search for the article. He decides to search by title to make it faster. He executes `find -a AI`. Beyond matches with the name, PressPlanner is still filtering to show only DRAFT articles, allowing the user to search a smaller set.
+
+Step 4. The user has found his article and wishes to remove the filter. He does this by executing `set -a S/ ST/ EN/`. With no instructions, the predicate allows all articles to pass through the filter.
+
+Note: If start date is later than the end date, PressPlanner will refuse to execute the command, double-check the dates to avoid this scenario.
+
+Note: Filters are **NOT** stored by the program. If you close the app, your filters will be reset.
+
+### \[Implemented\] Lookup Commands
+
+#### Implementation
+
+The proposed lookup feature is enabled by altering `Person` and `Article` classes to store a list of `Article` and `Person` objects respectively. The `Person` class will have a `List` attribute that stores the articles that the person is involved in. The `Article` class will have a `List` attribute that stores the persons involved in the article.
+
+When a `Person` object is added/edited, the `Model` component will check if the name of the person `Person` matches the name of the contributors/interviewees in any `Article` in the `ArticleBook`. If it does, the `Person` object will be updated with new `Article` objects in the list of articles it contains. Editing the name will also change the name of the corresponding contributor/interviewee in the `Article` objects. Adding a `Article` object work similarly, but in reverse. However, editing the name of contributors/interviewees in the `Article` objects will not update the corresponding names `Person` objects.
+
+The following diagram shows how the LookupCommand is executed:
+
+
+
+
+
+The ArticlesInPersonPredicate tests the articles by checking whether the list of articles within the `Person` object contains the article being tested. The PersonsInArticlePredicate tests the persons by checking whether the list of persons within the `Article` object contains the article being tested.
+
+#### Design considerations:
+
+Aspect: How to store associations between `Person` and `Article` objects:
+
+* **Alternative 1 (current choice):** Do not store any associations between `Person` and `Article` objects. Instead, since everytime the app is opened it reads all the persons and articles and adds them to the `Model`, the `Model` will always recreate the associations between `Person` and `Article` objects.
+ * Pros: Easier to implement. Uses less storage.
+ * Cons: Could slow down lookup time.
+* **Alternative 2:** Store associations between `Person` and `Article` objects in a separate `AssociationStorage` object in the `Storage` component.
+ * Pros: Faster lookup time.
+ * Cons: More complex to implement and maintain. Uses more storage.
+
+Aspect: What criteria to use to create associations between `Person` and `Article` objects:
+
+* **Alternative 1 (current choice):** Use the name of the `Person` object to match with the name of the contributors/interviewees in the `Article` objects.
+ * Pros: Easier to implement.
+ * Cons: Does not allow for multiple persons with the same name to be associated with different articles, making the user work around this limitation by altering the names slightly.
+* **Alternative 2:** Use a unique identifier like an id for each `Person` object to match with the contributors/interviewees in the `Article` objects.
+ * Pros: Allows for multiple persons with the same name to be associated with different articles.
+ * Cons: More complex to implement.
+
+Aspect: How editing the names affects the associated objects:
+
+* **Alternative 1a (current choice):** Editing the name of a `Person` object will also change the name of the corresponding contributor/interviewee in the `Article` objects.
+ * Pros: Changes in name is automatically reflected in the `Article` objects.
+ * Cons: Could lead to unintended changes in the `Article` objects.
+* **Alternative 1b:** Editing the name of a `Person` object will not change the name of the corresponding contributor/interviewee in the `Article` objects.
+ * Pros: Prevents unintended changes in the `Article` objects.
+ * Cons: Could lead to inconsistencies between the `Person` and `Article` objects.
+
+### \[Implemented\] Sort articles feature
+
+#### Implementation
+
+This feature closely follows the logic and model design of the sort persons feature mentioned above, by referencing the article classes that are implemented similar to their person counterparts as listed below.
+
+The differences include:
+
+1. `SortArticleCommand` class inherits from the `ArticleCommand` class which in turn inherits from the `Command` class instead of the `SortCommand` class.
+1. `SortArticleCommandParser` instead of `SortCommandParser` class.
+1. `AddressBookParser` passes command flow to the `ArticleBookParser` class.
+1. Sorting will be done on the `ArticleBook` object instead of the `AddressBook` object.
+1. The execution of the `sort -a d/` command will invoke the `Model#sortArticleBook("d/")` method instead of the `Model#sortAddressBook("d/")` method.
+
+The following sequence diagram shows how a sort articles operation goes through the Logic component:
+
+
+
+Similarly, how a sort articles operation goes through the `Model` component is shown below:
+
+
+
+#### Design considerations:
+
+**Aspect: How sort articles executes:**
+
+* **Alternative 1 (current choice):** Directly sort the `internalList` field present in the `UniqueArticleList` object.
+ * Pros: Easy to implement.
+ * Cons: Permanently orders all articles in the `ArticleBook` by the `Article` field specified by the related `prefix`.
+
+
+* **Alternative 2:** Clone the current `internalList` field
+ present in the `UniqueArticleList` object, sort the clone, then replace the `internalUnmodifiableList` field in the `UniqueArticleList` object with the sorted clone.
+ * Pros: Will not permanently order all articles in the `ArticleBook` by the `Article` field specified by the related `prefix`, but only the current view displayed to the user which is refreshed for every opening of the application or commands that changes the view (e.g. `List`, `Find` commands).
+ * Cons: Takes up much more memory space directly proportional to the size of the `ArticleBook` since a clone of all `Articles` has to be made.
+
+### \[Implemented\] Link Webpage to Articles
+
+#### Implementation
+
+The link feature is implemented in the `ArticleCard` class, so that when the user clicks on the link button, the link of the article on the article card will be opened.
+The link feature is enabled by filling up `link` attribute of `Article` class when adding an article. This feature creates a link button on the UI of each `Article` that opens up a web browser and directs the user to the webpage of where the actual article is uploaded.
+Since the `Articlebook` does not store the whole content of the articles, users will be able to read the articles using this feature.
+
+Given below is an example usage scenario:
+
+Step 1. The user launches the application for the first time. The `ArticleBook` will be initialized with the initial article book state.
+
+Step 2. The user executes `add -a h/Article1 d/20-03-2024 s/draft l/https://www.article1.com` command to add a new article. The `add` command calls `Logic#addArticleCommand("Article1", 20-03-2024, draft "https://www.article1.com")` which adds the article to the `ArticleBook`.
+
+Step 3. Notice that the `link` attribute of the `Article` object is filled with the link provided by the user.
+
+Step 4. The user clicks on the link button on the UI of the `Article` object. The link button will open up a web browser and direct the user to the webpage of where the actual article is uploaded.
+
+
+
+
+#### Design Considerations
+
+**Aspect: How the link feature is implemented:**
+
+* **Alternative 1 (current choice):** The link feature is implemented in the `ArticleCard` class.
+ * Pros: Easy to implement.
+ * Cons: The link feature is not reusable for other classes.
+
+* **Alternative 2:** The link feature is implemented in a separate class.
+ * Pros: The link feature is reusable for other classes.
+ * Cons: More complex to implement.
+
+The class diagram below shows how the `Article` will look and interact after implementation of the link feature.
+
+
+
### \[Proposed\] Undo/redo feature
#### Proposed Implementation
@@ -171,58 +361,67 @@ Given below is an example usage scenario and how the undo/redo mechanism behaves
Step 1. The user launches the application for the first time. The `VersionedAddressBook` will be initialized with the initial address book state, and the `currentStatePointer` pointing to that single address book state.
-
+
Step 2. The user executes `delete 5` command to delete the 5th person in the address book. The `delete` command calls `Model#commitAddressBook()`, causing the modified state of the address book after the `delete 5` command executes to be saved in the `addressBookStateList`, and the `currentStatePointer` is shifted to the newly inserted address book state.
-
+
Step 3. The user executes `add n/David …` to add a new person. The `add` command also calls `Model#commitAddressBook()`, causing another modified address book state to be saved into the `addressBookStateList`.
-
+
+
+
-
:information_source: **Note:** If a command fails its execution, it will not call `Model#commitAddressBook()`, so the address book state will not be saved into the `addressBookStateList`.
+**Note:** If a command fails its execution, it will not call `Model#commitAddressBook()`, so the address book state will not be saved into the `addressBookStateList`.
-
+
Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoAddressBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous address book state, and restores the address book to that state.
-
+
+
-
:information_source: **Note:** If the `currentStatePointer` is at index 0, pointing to the initial AddressBook state, then there are no previous AddressBook states to restore. The `undo` command uses `Model#canUndoAddressBook()` to check if this is the case. If so, it will return an error to the user rather
+
+
+**Note:** If the `currentStatePointer` is at index 0, pointing to the initial AddressBook state, then there are no previous AddressBook states to restore. The `undo` command uses `Model#canUndoAddressBook()` to check if this is the case. If so, it will return an error to the user rather
than attempting to perform the undo.
-
+
The following sequence diagram shows how an undo operation goes through the `Logic` component:
-
+
+
+
-
:information_source: **Note:** The lifeline for `UndoCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram.
+**Note:** The lifeline for `UndoCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram.
-
+
Similarly, how an undo operation goes through the `Model` component is shown below:
-
+
The `redo` command does the opposite — it calls `Model#redoAddressBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the address book to that state.
-
:information_source: **Note:** If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone AddressBook states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo.
+
+
+**Note:** If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone AddressBook states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo.
-
+
Step 5. The user then decides to execute the command `list`. Commands that do not modify the address book, such as `list`, will usually not call `Model#commitAddressBook()`, `Model#undoAddressBook()` or `Model#redoAddressBook()`. Thus, the `addressBookStateList` remains unchanged.
-
+
Step 6. The user executes `clear`, which calls `Model#commitAddressBook()`. Since the `currentStatePointer` is not pointing at the end of the `addressBookStateList`, all address book states after the `currentStatePointer` will be purged. Reason: It no longer makes sense to redo the `add n/David …` command. This is the behavior that most modern desktop applications follow.
-
+
The following activity diagram summarizes what happens when a user executes a new command:
-
+
#### Design considerations:
@@ -239,6 +438,47 @@ The following activity diagram summarizes what happens when a user executes a ne
_{more aspects and alternatives to be added}_
+### \[Proposed\] Templating of Articles
+
+#### Proposed Implementation
+
+The proposed templating feature is enabled by creating a `Template` superclass that `Article` inherits from.
+
+The `Template` class will have the following attributes:
+- `Name` - The name of the template.
+- `Authors` - A list of `Person` objects that are the authors of the article.
+- `Sources` - A list of `Person` objects that are the sources of the article.
+- `Tags` - A list of tags that are associated with the article.
+
+The `Template` class will also have getter methods for accessing each of the attributes.
+
+The `Article` class will be augmented to have the `applyTemplate` method, which will take a `Template` object as an argument and apply the template to the article. This will involve setting the `Authors`, `Sources`, `Tags`, and `Status` attributes of the article to the corresponding attributes of the template if the values are not `null`.
+
+The `Model` component will be augmented with a `UniqueTemplateList` to store the templates. The `Model` will also have methods to add, delete, and list templates.
+
+#### Proposed Implementation
+
+Step 1. The user creates a new `Template` object by entering the correct CLI input and providing the index of an article and the attributes to be used in the template.
+
+Step 2. `MakeTemplateCommandParser` parses the attribute prefixes and corresponding values from the user input.
+
+Step 3. `MakeTemplateCommand` is created with the parsed attributes.
+
+Step 4. The newly made `Template` object is added to the `UniqueTemplateList` in the `Model` component and throws an error if there is a duplicate.
+
+Step 5. The user can apply the template to an article by entering the correct CLI input and providing the index of the article and the index of the template.
+
+Step 6. `ApplyTemplateCommandParser` parses the indexes from the user input.
+
+Step 7. `ApplyTemplateCommand` is created with the parsed indexes.
+
+Step 8. The `ApplyTemplateCommand` is executed, and the template is applied to the article.
+
+The following sequence diagram shows how the `MakeTemplateCommand` is executed:
+
+
+
+
### \[Proposed\] Data archiving
_{Explain here how the data archiving feature will be implemented}_
@@ -262,42 +502,81 @@ _{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
+* freelance journalists
+* has a need to manage a significant number of contacts for different facets of business
+* prefer using text-based commands than multistep GUI
* can type fast
-* prefers typing to mouse interactions
-* is reasonably comfortable using CLI apps
-
-**Value proposition**: manage contacts faster than a typical mouse/GUI driven app
+* value speed and efficiency
+**Value proposition**: An app for freelance journalists that can streamline their workflow by organizing sources, tracking outlets interested in their stories, and managing collaborations with peers/editors. With features like tagging and grouping contacts, it facilitates efficient research, ensuring reporters can quickly reach out and report on breaking stories.
### User stories
Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*`
-| Priority | As a … | I want to … | So that I can… |
-| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- |
-| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App |
-| `* * *` | user | add a new person | |
-| `* * *` | user | delete a person | remove entries that I no longer need |
-| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list |
-| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident |
-| `*` | user with many persons in the address book | sort persons by name | locate a person easily |
+| Priority | As a … | I want to … | So that I can… |
+|----------|---------------------------------------------|----------------------------------------|------------------------------------------------------------------------------------------------|
+| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App |
+| `* * *` | user | list all people | see all people I have added |
+| `* * *` | user | add a new person | |
+| `* * *` | user | delete a person | remove person entries that I no longer need |
+| `* * *` | user | edit a person | modify the details of a person anytime |
+| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list |
+| `* * *` | user | lookup associated articles to a person | find all articles related to that person |
+| `* * *` | user with many persons in the address book | sort persons by name | locate a person easily |
+| `* * *` | user | clear all person entries | remove all template or unwanted person data |
+| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident |
+| `* * *` | user | list all articles | see all articles I have added |
+| `* * *` | user | add a new article | |
+| `* * *` | user | delete an article | remove article entries that I no longer need |
+| `* * *` | user | edit an article | modify the details of an article anytime |
+| `* * *` | user | find an article by headline | locate details of articles without having to go through the entire list |
+| `* * *` | user | filter articles by common attributes | locate details of articles with common attributes without having to go through the entire list |
+| `* * *` | user | remove filter for articles | display the complete list of articles again after the filter is no longer needed |
+| `* * *` | user | lookup associated people to an article | find all people related to that article |
+| `* * *` | user with many articles in the address book | sort articles by date | locate the most recent articles easily |
*{More to be added}*
### Use cases
-(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise)
+(For all use cases below, the **System** is the `PressPlanner` and the **Actor** is the `user`, unless specified otherwise)
+
+**Use case: UC01 - List all people**
+
+**MSS**
+1. User requests to list all people.
+1. PressPlanner lists out all people.
+
+ Use case ends.
-**Use case: Delete a person**
+**Use case: UC02 - Add 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 requests to add a person.
+1. PressPlanner adds the person.
+1. PressPlanner shows the added person to user.
+
+ Use case ends.
+
+**Extensions**
+
+* 1a. Command was invalid.
+
+ * 1a1. PressPlanner shows an error message.
+
+ Use case resumes at step 1.
+
+
+**Use case: UC03 - Delete a person**
+
+**MSS**
+
+1. User requests to list persons.
+1. AddressBook shows a list of persons.
+1. User requests to delete a specific person in the list.
+1. AddressBook deletes the person.
Use case ends.
@@ -313,20 +592,307 @@ Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unli
Use case resumes at step 2.
-*{More to be added}*
+**Use case: UC04 - Edit a person**
-### Non-Functional Requirements
+**MSS**
-1. Should work on any _mainstream OS_ as long as it has Java `11` or above installed.
-2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage.
-3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse.
+1. User requests to ***list all persons (UC01)***.
+1. User requests to edit a specific person in the list
+ by providing at least one change to an attribute of the article.
+1. PressPlanner updates the article with the changes requested.
+1. PressPlanner shows the updated article to user.
-*{More to be added}*
+ Use case ends.
+
+**Extensions**
+
+* 2a. The given index is invalid.
+
+ * 2a1. PressPlanner shows an error message.
+
+ Use case resumes at step 2.
+
+**Use case: UC05 - Find people**
+
+**MSS**
+
+1. User requests to find people with names containing given keywords.
+1. PressPlanner displays a filtered list of people found,
+ each having a name containing at least one of the given keywords.
+
+ Use case ends.
+
+**Extensions**
+
+* 1a. No keywords are specified.
+
+ * 1a1. PressPlanner shows an error message.
+
+ Use case resumes at step 1.
+
+
+
+**Use case: UC06 - Lookup associated articles for a person**
+
+**MSS**
+
+1. User requests to ***list all people (UC01)***.
+1. User requests to lookup associated articles for a specific person in the list.
+1. PressPlanner displays a filtered list of articles found,
+ each having the person as a contributor or interviewee.
+
+ Use case ends.
+
+**Extensions**
+
+* 2a. The given index is invalid.
+
+ * 2a1. PressPlanner shows an error message.
+
+ Use case resumes at step 2.
+* 3a. The list is empty as there are no articles associated with the person.
+
+ Use case ends.
+
+**Use case: UC07 - Sort people by their names**
+
+**MSS**
+
+1. User requests to ***list all people (UC01)***.
+1. User requests to sort people by their names.
+1. PressPlanner sorts the people by their names in ascending alphabetical order and displays the sorted list of people.
+
+ Use case ends.
+
+**Extensions**
+
+* 1a. The list is empty.
+
+ Use case ends.
+
+
+* 1b. The list is already sorted before and no ***edits to a person (UC04)*** modifies a person's name and changes that person's relative alphabetical ordering in the list or ***adding of a person (UC02)*** which results in that person not being ordered with respect to the rest of the list were performed afterwards.
+
+ Use case ends.
+
+
+* 2a. Invalid sorting attribute is given.
+
+ * 2a1. PressPlanner shows an error message.
+
+ Use case resumes at step 2.
+
+
+**Use case: UC08 - Filter people**
+
+**MSS**
+1. User requests to filter people by tag.
+1. PressPlanner returns a filtered list of people,
+ all of whom have the matching tag.
+
+ Use case ends.
+
+**Extensions**
+
+* 1a. User enters a non-alphanumeric tag.
+
+ * 1a1. PressPlanner shows an error message.
+
+ Use case resumes at step 1.
+
+**Use case: UC09 - List all articles**
+
+**MSS**
+1. User requests to list articles.
+1. PressPlanner lists out all articles.
+
+ Use case ends.
+
+**Use case: UC10 - Add an article**
+
+**MSS**
+1. User requests to add article.
+1. PressPlanner adds article.
+1. PressPlanner displays success message to User.
+
+ Use case ends.
+
+**Extensions**
+
+* 1a. Command was invalid.
+
+ * 1a1. PressPlanner shows an error message.
+
+ Use case resumes at step 1.
+
+**Use case: UC11 - Delete an article**
+
+**MSS**
+
+1. User requests to ***list all articles (UC09)***.
+1. User requests to delete a specific article in the list.
+1. PressPlanner deletes the article.
+1. PressPlanner shows delete success message to user.
+
+ Use case ends.
+
+**Extensions**
+
+* 2a. The given index is invalid.
+
+ * 2a1. PressPlanner shows an error message.
+
+ Use case resumes at step 2.
+
+**Use case: UC12 - Edit an article**
+
+**MSS**
+
+1. User requests to ***list all articles (UC09)***.
+1. User requests to edit a specific article in the list
+ by providing at least one change to an attribute of the article.
+1. PressPlanner updates the article with the changes requested.
+1. PressPlanner shows the updated article to user.
+
+ Use case ends.
+
+**Extensions**
+
+* 2a. The given index is invalid.
+
+ * 2a1. PressPlanner shows an error message.
+
+ Use case resumes at step 2.
+
+
+* 2b. No changes to an attribute of the article is specified.
+
+ * 2b1. PressPlanner shows an error message.
+
+ Use case resumes at step 2.
+
+**Use case: UC13 - Find articles**
+
+**MSS**
+
+1. User requests to find articles with headlines containing given keywords.
+1. PressPlanner displays a filtered list of articles found,
+ each having a headline containing at least one of the given keywords.
+
+ Use case ends.
+
+**Extensions**
+
+* 1a. No keywords are specified.
+
+ * 1a1. PressPlanner shows an error message.
+
+ Use case resumes at step 1.
+
+
+
+**Use case: UC14 - Filter articles**
+
+**MSS**
+
+1. User requests to filter articles by status, date of publication or tag.
+1. PressPlanner displays a filtered list of articles,
+ all of which fits the user's criteria.
+
+ Use case ends
+
+**Extensions**
+
+* 1a. User gives an invalid status, tag or date.
+
+ * 1a1. PressPlanner shows an error message
+
+ Use case resumes at step 1.
+
+
+* 1b. User omits any prefix.
+
+ * 1b1.PressPlanner shows an error message
+
+ Use case resumes at step 1.
+
+
+
+**Use case: UC15 - Lookup associated people for an article**
+
+**MSS**
+
+1. User requests to ***list all articles (UC09)***.
+1. User requests to lookup associated persons for a specific article in the list.
+1. PressPlanner displays a filtered list of persons found,
+ each featuring in the article as a contributor or interviewee.
+
+ Use case ends.
+
+**Extensions**
+
+* 2a. The given index is invalid.
+
+ * 2a1. PressPlanner shows an error message.
+
+ Use case resumes at step 2.
+
+* 3a. The list is empty as there are no persons associated with the article.
+
+ Use case ends.
+
+**Use case: UC16 - Sort articles by their date**
+
+**MSS**
+
+1. User requests to ***list all articles (UC09)***.
+1. User requests to sort articles by their dates.
+1. PressPlanner sorts the articles by their dates in descending chronological order and displays the sorted list of articles.
+
+ Use case ends.
+
+**Extensions**
+
+* 1a. The list is empty.
+
+ Use case ends.
+
+* 1b. The list is already sorted before and no ***edits to an article (UC12)*** modifies an article's date and changes that article's relative chronological ordering in the list or ***adding of an article (UC10)*** which results in that article not being ordered with respect to the rest of the list were performed afterwards.
+
+ Use case ends.
+
+
+* 2a. Invalid sorting attribute is given.
+
+ * 2a1. PressPlanner shows an error message.
+
+ Use case resumes at step 2.
+
+
+
+**Use case: UC17 - Open Webpage of Articles**
+
+**MSS**
+
+1. User requests to ***list all articles (UC09)***.
+1. User requests to open webpage of a certain article.
+1. PressPlanner opens a browser with the URL of the article.
+
+### Non-Functional Requirements
+
+1. Should work on any _mainstream OS_ as long as it has Java `11` or above installed.
+1. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage.
+1. Should be able to hold up to 1000 articles without a noticeable sluggishness in performance for typical usage.
+1. 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.
+1. A user should see either a success message which represents a successful execution of a command, or an error message indicating that the command did not execute successfully for any user command supplied by the user.
+1. If a command should fail, the underlying person or article data stored in PressPlanner should not be modified in any way not clearly visible to the user, to prevent the user from being oblivious to such changes if needed at all.
### Glossary
* **Mainstream OS**: Windows, Linux, Unix, MacOS
* **Private contact detail**: A contact detail that is not meant to be shared with others
+* **Story**: A story written by interviewing the person
+* **Tag**: Additional information about a person or an article that the user can use to come up with his or her own classification system.
--------------------------------------------------------------------------------------------------------------------
@@ -334,10 +900,12 @@ Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unli
Given below are instructions to test the app manually.
-
:information_source: **Note:** These instructions only provide a starting point for testers to work on;
+
+
+**Note:** These instructions only provide a starting point for testers to work on;
testers are expected to do more *exploratory* testing.
-
+
### Launch and shutdown
@@ -356,27 +924,223 @@ testers are expected to do more *exploratory* testing.
1. _{ more test cases … }_
-### Deleting a person
+### Sorting people by their names
+
+1. Sorting people after inserting a person
+
+ 1. Prerequisites: There are 6 person entries in PressPlanner on first time launch, already in ascending alphabetical order, perform the following testcases in order.
+
+ 1. Test case: `add n/Aaron Tan p/82927320 e/a@gmail.com a/ Blk 123 Jurong Ring Road, #01-123` followed by `sort n/`
+ Expected: The person entries are sorted by their names in ascending alphabetical order. The person named `"Aaron Tan"` should be the first entry. Timestamp in the status bar is updated.
+
+ 1. Test case: `add n/Annie Lee p/82927320 e/a@gmail.com a/ Blk 123 Jurong Ring Road, #01-123` followed by `sort N/`
+ Expected: The person entries are sorted by their names in ascending alphabetical order. The person named `"Annie Lee"` should now be the third entry, after the person named `"Alex Yeoh"`. Timestamp in the status bar is updated.
+
+ 1. Test case: `add n/Zachery Tan p/82927320 e/a@gmail.com a/ Blk 123 Jurong Ring Road, #01-123` followed by `sort z/`
+ Expected: No reordering of people is done. Error details shown in the status message. Status bar remains the same.
+
+ 1. Other incorrect sort person commands to try: `sort`, `sort x`, `...` (where x is anything that is not `n/` or `N/`)
+ Expected: Similar to previous.
+
+
+### Adding an article
+
+1. Adding an article
+
+ 1. Test case: `add -a h/Article1 c/Author1 i/Interviewee1 t/Science d/01-01-2019 s/PUBLISHED`
+ Expected: Article1 is added to the list. Details of the added article shown in the status message.
+
+ 1. Test case: `add -a h/Article2 d/01-01-2021 s/PUBLISHED`
+ Expected: Article2 is added to the list. Details of the added article shown in the status message.
+
+ 1. Test case: `add -a h/Article3 c/Author3 s/DRAFT`
+ Expected: Error message is shown. Article3 is not added to the list.
-1. Deleting a person while all persons are being shown
+### Deleting an article
- 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list.
+1. Deleting an article while all article are being shown
- 1. Test case: `delete 1`
- Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated.
+ 1. Prerequisites: List all articles using the `list -a` command. Multiple articles in the list.
- 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same.
+ 1. Test case: `delete -a 1`
+ Expected: First article is deleted from the list. Details of the deleted article shown in the status message.
+
+ 1. Test case: `delete -a 0`
+ Expected: No article is deleted. Error details shown in the status message. Status bar remains the same.
- 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
+ 1. Other incorrect delete commands to try: `delete -a`, `delete -a x`, `...` (where x is larger than the list size)
Expected: Similar to previous.
-1. _{ more test cases … }_
+### Editing an article
+
+1. Edit an article while all articles are being shown
+
+ 1. Prerequisites: List all articles using the `list -a` command. Multiple articles in the list.
+
+ 1. Test case: `edit -a 1 h/Article1`
+ Expected: First article is edited to Article1. Details of the edited article shown in the status message.
+
+ 1. Test case: `edit -a 0 h/Article1`
+ Expected: No article is edited. Error details shown in the status message.
+
+ 1. Other incorrect edit commands to try: `edit -a`, `edit -a x`, `...` (where x is larger than the list size)
+ Expected: Similar to previous.
+
+### Finding articles
+
+1. Finding articles by their headlines using keywords
+
+ 1. Prerequisites: There is 1 article entry in PressPlanner on first time launch, perform the following testcases in order after adding the following articles provided as add article commands.
+ 1. `add -a h/one d/01-01-2001 s/draft`
+ 1. `add -a h/one two d/01-01-2001 s/draft`
+ 1. `add -a h/one two three d/01-01-2001 s/draft`
+
+ 1. Test case: `find -a one`
+ Expected: The only article with the headline `one` is shown in the list of articles. The status message shows the number of articles found. Timestamp in the status bar is updated.
+
+ 1. Test case: `find -a TWO`
+ Expected: Two articles are shown with headlines `one two` and `one two three`. The status message shows the number of articles found. Timestamp in the status bar is updated.
+
+ 1. Test case: `find -a thre`
+ Expected: No articles are found. The status message shows the number of articles found is `0`. Timestamp in the status bar is updated.
+
+
+### Filtering through articles
+1. Filtering through articles.
+ 1. Prerequisites: Populate PressPlanner with sufficient articles. You may use the following add commands:
+
+ 1. Use these commands to populate PressPlanner.
+ `add -a h/Test-1 c/Author1 i/Interviewee1 t/Science d/01-01-2019 s/PUBLISHED`
+ `add -a h/Test-2 c/Author2 i/Interviewee2 d/01-01-2021 s/PUBLISHED`
+ `add -a h/Test-3 c/Author3 d/01-01-2019 s/DRAFT`
+
+ 1. Test case: `filter -a s/ st/ en/ t/`
+ Expected:There will be no change in displayed articles.
+
+ 1. Test case: `filter -a s/DRAFT st/ en/ t/`
+ Expected: Only articles with draft status will be displayed.
+
+ 1. Test case: `filter -a s/ st/01-01-2020 en/12-12-2022 t/`
+ Expected: Only articles published between 01-01-2020 and 12-12-2022 will be displayed.
+
+ 1. Test case: `filter -a s/ st/ en/ t/Science`
+ Expected: Only articles with the tag `Science` will be displayed.
+
+ 1. Test case: `filter -a s/ st/`
+ Expected: An error informing the user that the command format is incorrect will be shown.
+
+ 1. Test case: `filter -a s/ st/ en/ t/non-alphanumeric`
+ Expected: An error informing the user that tags only consisting of alphanumeric characters will be shown.
+
+ 1. Test case: `filter -a s/ st/01-01-2020 en/01-01-2001 t/`
+ Expected: An error informing the user that start dates must come before end dates will be shown.
+
+### Lookup a person & article
+
+1. Lookup person/article after adding a person/article
+
+ 1. Prerequisites: Assume non-empty list of persons and articles. Change index numbers as needed.
+ 1. Test case: `add n/Alice1 p/12345678 e/alice@email.com a/Blk 424 #11-0536 Yishun Ring Road` `lookup 1`
+ Expected: Alice1 is added to the list. An empty list of articles associated with Alice1 is shown.
+
+ 1. Testcase: `add -a h/Article1 c/Alice1 d/11-09-2021 s/DRAFT` `lookup -a 1`
+ Expected: Article1 is added to the list. Alice1 is shown as a list of persons associated with Article1.
+
+ 1. Lookup person: `lookup 1`
+ Expected: Article1 is shown as a list of articles associated with Alice1.
+
+ 1. Test case: `lookup 0`
+ Expected: Error message is shown.
+
+ 1. Test case: `lookup -a 0`
+ Expected: Error message is shown.
+
+ 1. Delete the person and article added in the prerequisites. Then repeat the above testcases by altering the orders such that the article is added first and then person. The commands should differ accordingly.
+
+1. Lookup after editing person and article
+
+ 1. Test case: `edit 1 n/Alice2`
+ Expected: Alice is edited to Alice2. This is reflected in the article Article0 contributor tag as well.
+
+ 1. Test case: `edit -a 1 h/Article1`
+ Expected: Article0 is edited to Article1.
+
+ 1. Test case: `lookup 1`
+ Expected: Article1 is shown as a list of articles associated with Alice2.
+
+ 1. Test case: `lookup -a 1`
+ Expected: Alice2 is shown as a list of persons associated with Article1.
+
+### Sorting articles by their dates
+
+1. Sorting articles after inserting an article with a date of `"0X-01-2100"` replacing `"X"` with a number starting from 1 up to 9
+
+ 1. Prerequisites: There is 1 article entry in PressPlanner on first time launch, perform the following testcases in order.
+
+ 1. Test case: `add h/Article1 d/01-01-2100 s/draft` followed by `sort -a d/`
+ Expected: The article entries are sorted by their dates in descending chronological order. The article with the headline `Article1` and date `"01-01-2100"` should be the first entry. Timestamp in the status bar is updated.
+
+ 1. Test case: `add h/Article2 d/02-01-2100 s/draft` followed by `sort -a D/`
+ Expected: The article entries are sorted by their dates in descending chronological order. The article the headline `Article2` and date `"02-01-2100"` should be the first entry, before `Article1` with the date `"01-01-2100"`. Timestamp in the status bar is updated.
+
+ 1. Test case: `add h/Article3 d/03-01-2100 s/draft` followed by `sort -a z/`
+ Expected: No reordering of articles is done. Error details shown in the status message. Status bar remains the same.
+
+ 1. Other incorrect sort article commands to try: `sort -a`, `sort -a x`, `...` (where x is anything that is not `d/` or `D/`)
+ Expected: Similar to previous.
+
+### Opening Links
+
+1. Opening a link to an article
+
+ 1. Create articles using `add -a h/Article1 d/20-03-2024 s/draft l/https://www.google.com`, `add -a h/Article2 d/20-03-2024 s/draft l/https://www.facebook.com/` and `add -a h/Article3 d/20-03-2024 s/draft l/` commands.
+
+ 1. Test case: `add -a h/Article1 d/20-03-2024 s/draft l/https://www.google.com`, followed by click on the link button of the first article.
+ Expected: The link to google is opened in the default web browser.
+
+ 1. Test case: `add -a h/Article2 d/20-03-2024 s/draft l/https://www.facebook.com/`, followed by click on the link button of the last article.
+ Expected: The link to facebook is opened in the default web browser.
+
+ 1. Test case: `add -a h/Article3 d/20-03-2024 s/draft l/`, followed by click on the link button of an article that does not have a link.
+ Expected: Nothing happens.
+
+
+1. _{ more test cases … }_
### Saving data
1. Dealing with missing/corrupted data files
1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_
-1. _{ more test cases … }_
+--------------------------------------------------------------------------------------------------------------------
+## **Planned Enhancements**
+
+1. **Make URL failure more explicit**: Currently when a URL cannot be opened the app shows that by not opening anything. This can be improved by showing an error message.
+
+
+1. **The filter command can work for individual prefixes**: Currently the filter command only works for all prefixes. It can be improved by allowing the user to filter by individual prefixes.
+
+
+1. **Automatically sort persons by their names in ascending alphabetical ordering**: Currently whenever the user makes an edit to a person's name or adds new person entries which may result in a violation of the previous ordering, the user would have to execute the `sort n/` command to re-sort the person entries. It can be improved by automatically sorting people whenever new entries, or certain edits to them are made to reduce such inconveniences to the user.
+
+
+1. **Automatically sort articles by their publication dates in descending order**: Currently whenever the user makes an edit to an article's publication date or adds new article entries which may result in a violation of the previous ordering, the user would have to execute the `sort -a d/` command to re-sort the articles. It can be improved by automatically sorting articles whenever new entries, or certain edits to them are made to reduce such inconveniences to the user.
+
+
+1. **Provide alternative methods to create associations between persons and articles**: Currently the user can only create associations between persons and articles when adding/editing the persons or articles. It might not always be desirable to create associations when adding/editing persons or articles. This can be improved by providing an alternative method to create associations between persons and articles by using IDs unique to each person and article. Instead of using names to create associations, the user can use the IDs to create associations between persons and articles.
+
+
+1. **Allow the user to filter people**: Currently the user can only filter articles. It can be improved by allowing the user to filter people as well.
+
+
+1. **Allow editing or deleting at any index for duplicate articles**: Currently, the user can only edit or delete the first article when there are multiple draft articles with the identical attributes. It can be improved by allowing the user to edit or delete any article with the same headline.
+
+
+1. **Enhance prefix validation**: Currently, the user can use duplicate prefixes for the same attribute when adding or editing persons or articles, or even add incorrect prefixes in their commands. It can be improved by enhancing the prefix validation to prevent the user from using duplicate prefixes for the same attribute and to guide the user to use the correct prefixes in their commands.
+
+
+1. **Enable greater ease in adding/editing/deleting cumulative attributes**: Currently these tags for both person and article, and contributor, interviewee and outlets for articles are not cumulative. If user wants to add one more, they will have to add the existing ones at the same time using edit. They cannot also delete any single one, but have to clear all existing ones and add the ones that they want to remain again using edit. The enhancement can let there be a index for those attributes enabling the user to add/edit/delete them with greater ease.
+
+
+1. **Allow non-alphanumeric inputs for tags**: Currently, the user can only use alphanumeric characters for tags. It can be improved by allowing the user to use non-alphanumeric characters for tags.
diff --git a/docs/Documentation.md b/docs/Documentation.md
index 3e68ea364e7..082e652d947 100644
--- a/docs/Documentation.md
+++ b/docs/Documentation.md
@@ -1,29 +1,21 @@
---
-layout: page
-title: Documentation guide
+ layout: default.md
+ title: "Documentation guide"
+ pageNav: 3
---
-**Setting up and maintaining the project website:**
-
-* We use [**Jekyll**](https://jekyllrb.com/) to manage documentation.
-* The `docs/` folder is used for documentation.
-* To learn how set it up and maintain the project website, follow the guide [_[se-edu/guides] **Using Jekyll for project documentation**_](https://se-education.org/guides/tutorials/jekyll.html).
-* Note these points when adapting the documentation to a different project/product:
- * The 'Site-wide settings' section of the page linked above has information on how to update site-wide elements such as the top navigation bar.
- * :bulb: In addition to updating content files, you might have to update the config files `docs\_config.yml` and `docs\_sass\minima\_base.scss` (which contains a reference to `AB-3` that comes into play when converting documentation pages to PDF format).
-* If you are using Intellij for editing documentation files, you can consider enabling 'soft wrapping' for `*.md` files, as explained in [_[se-edu/guides] **Intellij IDEA: Useful settings**_](https://se-education.org/guides/tutorials/intellijUsefulSettings.html#enabling-soft-wrapping)
+# Documentation Guide
+* We use [**MarkBind**](https://markbind.org/) to manage documentation.
+* The `docs/` folder contains the source files for the documentation website.
+* To learn how set it up and maintain the project website, follow the guide [[se-edu/guides] Working with Forked MarkBind sites](https://se-education.org/guides/tutorials/markbind-forked-sites.html) for project documentation.
**Style guidance:**
* Follow the [**_Google developer documentation style guide_**](https://developers.google.com/style).
+* Also relevant is the [_se-edu/guides **Markdown coding standard**_](https://se-education.org/guides/conventions/markdown.html).
-* Also relevant is the [_[se-edu/guides] **Markdown coding standard**_](https://se-education.org/guides/conventions/markdown.html)
-
-**Diagrams:**
-
-* See the [_[se-edu/guides] **Using PlantUML**_](https://se-education.org/guides/tutorials/plantUml.html)
-**Converting a document to the PDF format:**
+**Converting to PDF**
-* See the guide [_[se-edu/guides] **Saving web documents as PDF files**_](https://se-education.org/guides/tutorials/savingPdf.html)
+* See the guide [_se-edu/guides **Saving web documents as PDF files**_](https://se-education.org/guides/tutorials/savingPdf.html).
diff --git a/docs/Gemfile b/docs/Gemfile
deleted file mode 100644
index c8385d85874..00000000000
--- a/docs/Gemfile
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-source "https://rubygems.org"
-
-git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
-
-gem 'jekyll'
-gem 'github-pages', group: :jekyll_plugins
-gem 'wdm', '~> 0.1.0' if Gem.win_platform?
-gem 'webrick'
diff --git a/docs/Logging.md b/docs/Logging.md
index 5e4fb9bc217..589644ad5c6 100644
--- a/docs/Logging.md
+++ b/docs/Logging.md
@@ -1,8 +1,10 @@
---
-layout: page
-title: Logging guide
+ layout: default.md
+ title: "Logging guide"
---
+# Logging guide
+
* We are using `java.util.logging` package for logging.
* The `LogsCenter` class is used to manage the logging levels and logging destinations.
* The `Logger` for a class can be obtained using `LogsCenter.getLogger(Class)` which will log messages according to the specified logging level.
diff --git a/docs/SettingUp.md b/docs/SettingUp.md
index 275445bd551..03df0295bd2 100644
--- a/docs/SettingUp.md
+++ b/docs/SettingUp.md
@@ -1,27 +1,32 @@
---
-layout: page
-title: Setting up and getting started
+ layout: default.md
+ title: "Setting up and getting started"
+ pageNav: 3
---
-* Table of Contents
-{:toc}
+# Setting up and getting started
+
+
--------------------------------------------------------------------------------------------------------------------
## Setting up the project in your computer
-
:exclamation: **Caution:**
+
+**Caution:**
Follow the steps in the following guide precisely. Things will not work out if you deviate in some steps.
-
+
First, **fork** this repo, and **clone** the fork into your computer.
If you plan to use Intellij IDEA (highly recommended):
1. **Configure the JDK**: Follow the guide [_[se-edu/guides] IDEA: Configuring the JDK_](https://se-education.org/guides/tutorials/intellijJdk.html) to to ensure Intellij is configured to use **JDK 11**.
-1. **Import the project as a Gradle project**: Follow the guide [_[se-edu/guides] IDEA: Importing a Gradle project_](https://se-education.org/guides/tutorials/intellijImportGradleProject.html) to import the project into IDEA.
- :exclamation: Note: Importing a Gradle project is slightly different from importing a normal Java project.
+1. **Import the project as a Gradle project**: Follow the guide [_[se-edu/guides] IDEA: Importing a Gradle project_](https://se-education.org/guides/tutorials/intellijImportGradleProject.html) to import the project into IDEA.
+
+ Note: Importing a Gradle project is slightly different from importing a normal Java project.
+
1. **Verify the setup**:
1. Run the `seedu.address.Main` and try a few commands.
1. [Run the tests](Testing.md) to ensure they all pass.
@@ -34,10 +39,11 @@ If you plan to use Intellij IDEA (highly recommended):
If using IDEA, follow the guide [_[se-edu/guides] IDEA: Configuring the code style_](https://se-education.org/guides/tutorials/intellijCodeStyle.html) to set up IDEA's coding style to match ours.
-
:bulb: **Tip:**
+
+ **Tip:**
Optionally, you can follow the guide [_[se-edu/guides] Using Checkstyle_](https://se-education.org/guides/tutorials/checkstyle.html) to find how to use the CheckStyle within IDEA e.g., to report problems _as_ you write code.
-
+
1. **Set up CI**
diff --git a/docs/Testing.md b/docs/Testing.md
index 8a99e82438a..78ddc57e670 100644
--- a/docs/Testing.md
+++ b/docs/Testing.md
@@ -1,12 +1,15 @@
---
-layout: page
-title: Testing guide
+ layout: default.md
+ title: "Testing guide"
+ pageNav: 3
---
-* Table of Contents
-{:toc}
+# Testing guide
---------------------------------------------------------------------------------------------------------------------
+
+
+
+
## Running tests
@@ -19,8 +22,10 @@ There are two ways to run tests.
* **Method 2: Using Gradle**
* Open a console and run the command `gradlew clean test` (Mac/Linux: `./gradlew clean test`)
-
:link: **Link**: Read [this Gradle Tutorial from the se-edu/guides](https://se-education.org/guides/tutorials/gradle.html) to learn more about using Gradle.
-
+
+
+**Link**: Read [this Gradle Tutorial from the se-edu/guides](https://se-education.org/guides/tutorials/gradle.html) to learn more about using Gradle.
+
--------------------------------------------------------------------------------------------------------------------
diff --git a/docs/UserGuide.md b/docs/UserGuide.md
index 7abd1984218..19663571ffd 100644
--- a/docs/UserGuide.md
+++ b/docs/UserGuide.md
@@ -1,198 +1,793 @@
----
-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.
-
-* Table of Contents
-{:toc}
-
---------------------------------------------------------------------------------------------------------------------
-
-## Quick start
+# PressPlanner User Guide
+
+## Table Of Contents
+
+* [1. Introduction](#1-introduction)
+ * [1.1. Using this Guide](#1-1-using-this-guide)
+ * [1.2. Why Use PressPlanner](#1-2-why-use-pressplanner)
+* [2. Getting Started](#2-getting-started)
+ * [2.1. Installation](#2-1-installation)
+ * [2.2. Launching the App](#2-2-launching-the-app)
+ * [2.3. The Beginner's Guide to PressPlanner](#2-3-the-beginner-s-guide-to-pressplanner)
+* [3. Features](#3-features)
+ * [3.1. Managing Contacts](#3-1-managing-contacts)
+ * [3.1.1. Adding a Person](#3-1-1-adding-a-person-add)
+ * [3.1.2. Deleting a Person](#3-1-2-deleting-a-person-delete)
+ * [3.1.3. Listing All Persons](#3-1-3-listing-all-persons-list)
+ * [3.1.4. Editing a Person](#3-1-4-editing-a-person-edit)
+ * [3.1.5. Searching for a Person by Name](#3-1-5-searching-for-a-person-by-name-find)
+ * [3.1.6. Lookup Associated Articles](#3-1-6-lookup-associated-articles-lookup)
+ * [3.1.7. Sorting Persons by Name](#3-1-7-sorting-persons-by-name-sort-n)
+ * [3.1.8. Clearing All Persons](#3-1-8-clearing-all-persons-clear)
+ * [3.2. Managing Articles](#3-2-managing-articles)
+ * [3.2.1. Adding an Article](#3-2-1-adding-an-article-add-a)
+ * [3.2.2. Deleting an Article](#3-2-2-deleting-an-article-delete-a)
+ * [3.2.3. Listing All Articles](#3-2-3-listing-all-articles-list-a)
+ * [3.2.4. Editing an Article](#3-2-4-editing-an-article-edit-a)
+ * [3.2.5. Searching for an Article by Headline](#3-2-5-searching-for-an-article-by-headline-find-a)
+ * [3.2.6. Filtering Articles](#3-2-6-filtering-articles-filter-a)
+ * [3.2.7. Removing a Filter](#3-2-7-removing-a-filter-rmfilter-a)
+ * [3.2.8. Lookup Associated Persons](#3-2-8-lookup-associated-persons-lookup-a)
+ * [3.2.9. Sorting Articles by Date](#3-2-9-sorting-articles-by-date-sort-a-d)
+ * [3.2.10. Opening a Webpage for an Article](#3-2-10-opening-a-webpage-for-an-article)
+ * [3.3. Other Commands](#3-3-other-commands)
+ * [3.3.1. Viewing Help ](#3-3-1-viewing-help-help)
+ * [3.3.2. Exiting PressPlanner](#3-3-2-exiting-pressplanner-exit)
+* [4. Commands Quick Reference](#4-commands-quick-reference)
+* [5. Upcoming Features](#5-upcoming-features)
+ * [5.1. Clearing all Articles](#5-1-clearing-all-articles)
+ * [5.2. Filtering People](#5-2-filtering-people)
+* [6. FAQs](#6-faqs)
+
+
+
+## [1. Introduction](#table-of-contents)
+### [1.1. Using this Guide](#1-introduction)
+This guide is intended to help you get started with PressPlanner. It will guide you through the installation process, provide a brief overview of the app's features, and give you a quick reference to the commands you can use. All sections headers will link you back to the start of their parent section, so you can easily navigate the guide.
+
+### [1.2. Why Use PressPlanner?](#1-introduction)
+PressPlanner was built with **freelance journalists in mind**. It acts as your contact list linked together with a collection of articles, helping you keep track of your contacts and articles.
+
+Unlike major firms, freelancers often lack the same wealth of contacts and resources. PressPlanner helps you maximise the value you can get from your contacts, by providing a platform to store and manage them and keeping track of which contacts you've worked with for different articles.
+
+PressPlanner's main features are its ability to help you:
+1. Develop deeper story angles and reconnect with past interviewees or collaborators.
+ - [Filter](#3-2-6-filtering-articles-filter-a) by tags to find past articles on a specific topic.
+ - [Lookup](#3-2-8-lookup-associated-persons-lookup-a) persons of interest related to those past articles.
+ - Contact these persons for interviews or collaboration.
+
+1. Follow up on breaking stories
+ - [Filter](#3-2-6-filtering-articles-filter-a) by status and tags to find published articles related to breaking news.
+ - Make changes to your article as the story develops.
+
+PressPlanner's tagging system for [persons](#3-1-managing-contacts) and [articles](#3-2-managing-articles) is flexible and powerful:
+- Customise your use of tags and still leverage the app's search and filter functions.
+
+## [2. Getting Started](#table-of-contents)
+
+### [2.1. Installation](#2-getting-started)
+1. Ensure that you have Java `11` or above installed on your computer.
+ - Download Java 11 from [the official Oracle website](https://www.oracle.com/java/technologies/downloads/#java11).
+ - If you are unsure what version of java you have, use [this guide](https://www.java.com/en/download/help/version_manual.html) to check.
+1. Download the jar file from [our latest release](https://github.com/AY2324S2-CS2103T-F12-2/tp/releases).
+1. Move it to an **Empty** folder.
+
+
+
+**:warning: Warning**
+
+App data will be stored in sub-folders from where it is launched. While you could run the app from any location, we recommend making a dedicated folder for our app to avoid confusion.
+
-1. Ensure you have Java `11` or above installed in your Computer.
+### [2.2. Launching the App](#2-getting-started)
+1. Open a command terminal, change directories into the folder you put the jar file in, and use the `java -jar pressplanner.jar` command to run the application.
+ * For Windows users, you can use the [`cd` command](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/cd) to change directories and the `dir`command to list files in the current directory.
+ * For macOS and Linux users, you can use the [`cd` command](https://help.ubuntu.com/community/UsingTheTerminal) to change directories and the `ls` command to list files in the current directory.
+1. A window similar to the one below should appear in a few seconds. Note how the app contains some sample data. The information on what each data field represents is shown in the picture below.
-1. Download the latest `addressbook.jar` from [here](https://github.com/se-edu/addressbook-level3/releases).
+
-1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook.
+### [2.3. The Beginner's Guide to PressPlanner](#2-getting-started)
-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.
- 
+
-1. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
- Some example commands you can try:
+**:bulb: Tip**
- * `list` : Lists all contacts.
+This section covers commands first-time users might need. For the full commands list, refer to the [Features](#3-features) section.
+
- * `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.
+Let's go over the basic PressPlanner workflow. Say you've just finished interviewing a certain Gill Bates. You want to save his contact for later and keep track of your article.
+
+1. Selecting the command box at the top of the page, let's first add Gill Bates to PressPlanner's address book list.
+ - To `add` a contact we need to include the following information separated by their prefixes:
+ - Name (`n/`)
+ - Phone number (`p/`)
+ - Email (`e/`)
+ - Address (`a/`)
+ - For example: `add n/Gill Bates p/12345678 e/gillbates@sicromoft.com a/Sicromoft HQ`
+
+
+
+ **:information_source: Important**
+
+ Adding an article uses the [`add -a` command](#3-2-1-adding-an-article-add-a), the `-a` standing for article.
+ * Note that the `-a` suffix is used for all commands pertaining to articles.
+
+
+1. Next let's add that article you just wrote.
+ - To `add -a` an article we need the following information:
+ - Headline (`h/`)
+ - Date (`d/`)
+ - We use a single field for the date.
+ - Status (`s/`)
+ - An article can be a `draft`, `published`, or `archived`.
+ - For example: `add -a h/Example Article d/20-10-2023 s/draft`
+
+
+
+
+
+ **:warning: Warning**
+ You should only use headline (`h/`), date (`d/`) and status (`s/`) prefixes once each. If you use multiple prefixes only the last prefix's value will be used:
+ * eg. `add -a h/My Article d/20-10-2023 s/draft h/My Second Article` will add an article titled `My Second Article`.
+ * eg. `add -a h/My Article d/01-01-2024 s/draft d/02-02-2024` will add an article with the date `02-02-2024`.
+
+
+1. Now that that's done, let's say you need to find Gill Bate's number to arrange another interview.
+ - Typing the command `find Gill Bates` will pull up his contact.
+
+1. If you made a mistake or want to see all your contacts again:
+ - Typing the command `list` will bring up all your contacts.
+
+1. If you want to look up your article:
+ - Typing the command `find -a Example Article` will pull up the article.
+
+1. If you want to see all your articles again:
+ - Typing the command `list -a` will bring them all up.
+
+Now that you know the basic workflow, go ahead and try it out for yourself. If you want to learn more commands, use the `help` command in-app or refer to the [features](#3-features) section of this guide.
+
+As you become more familiar with the app, use tags as you see fit to customise your workflow!
+- Here are some ideas to get you started:
+ - Using tags to rate interviewees' compliance and reliability.
+ - Noting down how many clicks articles got in the first 24 hours.
+ - Using tags to mark articles with potential for follow-up development.
+
+
+
+## [3. Features](#table-of-contents)
- * `delete 3` : Deletes the 3rd contact shown in the current list.
+
- * `clear` : Deletes all contacts.
+**:information_source: Important**
+
+Here are some important terms that will be used in this section:
+
+1. Commands are composed of a **command word** potentially followed by a few **prefixes** and their corresponding **parameters**.
+ * For the example command `example p/PARAMETER`:
+ * `example` is the command word.
+ * `p/` is the prefix.
+ * `PARAMETER` is the parameter to be supplied by you.
+
+1. `INDEX` is a parameter you may come across frequently. It refers to the index number shown in the current list view.
+ * `INDEX` must be a positive integer.
+ * An `INDEX` not present in the current list view is invalid.
+ * For example using the sample data shown below, indexes 1 - 6 are valid for persons, and index 1 is valid for articles:
+
+ 
+
+
+
+1. Words in `UPPER_CASE` are the parameters to be supplied by you.
+ * Refer to point 1 for the breakdown of the command structure.
+ * For the example command `example p/PARAMETER`:
+ * `PARAMETER` is the parameter to be supplied by you.
+ * The correct use of this command would thus be: `example p/my input`, replacing `PARAMETER` with your own input.
+ * For the real command [`delete INDEX`](#3-1-2-deleting-a-person-delete):
+ * `INDEX` is the parameter to be supplied by you.
+ * The correct use of this command would thus be: `delete 1`, replacing `INDEX` with a valid index.
+
+1. Items in square brackets are optional.
+ * For the example command `example p/PARAMETER [t/TAG]`:
+ * `example p/my input` is a valid use of the command.
+ * `example p/my input t/my tag` is also a valid use of the command.
+
+1. Items with `...` after them can be used multiple times. If the item is also in square brackets, it can even be used zero times.
+ * For the example command `example p/PARAMETER [t/TAG]...`:
+ * `example p/my input` is a valid use of the command.
+ * `example p/my input t/my tag` is also a valid use of the command.
+ * `example p/my input t/my tag t/my other tag` is also a valid use of the command.
+
+1. Parameters can be in any order.
+ * For the example command `example p/PARAMETER [t/TAG]...`:
+ * `example p/my input t/my tag` is a valid use of the command.
+ * `example t/my tag p/my input` is also a valid use of the command.
+ * `example t/my tag p/my input t/my other tag` is also a valid use of the command.
+
+1. Extraneous inputs for commands that do not take in parameters will be ignored.
+ * This specifically refers to the [`help`](#3-3-1-viewing-help-help), [`list`](#3-1-3-listing-all-persons-list), [`list -a`](#3-2-3-listing-all-articles-list-a), [`exit`](#3-3-2-exiting-pressplanner-exit) and [`clear`](#3-1-8-clearing-all-persons-clear) commands.
+ * e.g. `help 123` will be interpreted as `help`.
+
+1. Prefixes are case-insensitive.
+ * Only **prefixes** are always case-insensitive, **command words** are case-sensitive.
+ * Refer to point 1 if you are unsure of the terminology and command structure.
+ * For the example command `example p/PARAMETER`:
+ * `example p/my input` is a valid use of the command.
+ * `example P/my input` is also a valid use of the command.
+ * `EXAMPLE p/my input` is not a valid use of the command.
+
+1. Only correct prefixes will be recognised and accepted.
+ * Taking the [`add -a` command](#3-2-1-adding-an-article-add-a) for example:
+ * The command: `add -a h/My Headline invalid/ignore d/01-01-2024 s/draft` will not recognise `invalid/ignore` as a valid prefix and parameter pair.
+ * As a result, the command will interpret `My Headline invalid/ignore` as the headline and add a new article with the headline `My Headline invalid/ignore`.
+ * The command: `add -a h/My Headline d/01-01-2024 s/draft t/my tag invalid/ignore` will not recognise `invalid/ignore` as a valid prefix and parameter pair.
+ * As a result, the command will interpret `my tag invalid/ignore` as the attempted tag.
+ * This will display an error message prompting you to only use alphanumeric characters for tags.
+
- * `exit` : Exits the app.
+
-1. Refer to the [Features](#features) below for details of each command.
+**:warning: Warning**
---------------------------------------------------------------------------------------------------------------------
+If you are using a PDF version of this document, be careful when copying and pasting commands with line breaks as they may not paste correctly.
+
-## Features
+
+## [3.1. Managing Contacts](#3-features)
-**:information_source: Notes about the command format:**
+**:information_source: Important**
+
+Here are some important terms that will be used in this section:
+1. `NAME`
+ * Names must be alphanumeric.
+ * Special characters are not allowed.
+ * Whitespaces are allowed.
+
+2. `PHONE_NUMBER`
+ * Phone numbers must be numeric only.
+ * Spaces and special characters are not allowed.
+ * Phone numbers must be at least 3 digits long.
+
+3. `EMAIL`
+ * Emails must be in the format `local-part@domain`.
+ * The local part can contain alphanumeric characters and the `+_.-` special characters.
+ * The domain part must be:
+ * Made up of domain labels separated by periods.
+ * End with a domain label of at least 2 characters.
+ * Have each domain label start and end with an alphanumeric character.
+ * Have each domain label contain only alphanumeric characters and hyphens.
+
+4. `ADDRESS`
+ * Addresses can contain any characters.
+ * Addresses cannot be left blank.
+
+5. `TAG`
+ * Tags are alphanumeric.
+ * Tags are any keyword or phrase that helps categorise the person.
+ * Customise your workflow with tags that make sense to you.
+
-* 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`.
+### [3.1.1. Adding a Person](#3-1-managing-contacts) : `add`
-* 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`.
+Adds a person to PressPlanner's address book.
-* Items with `…` after them can be used multiple times including zero times.
- e.g. `[t/TAG]…` can be used as ` ` (i.e. 0 times), `t/friend`, `t/friend t/family` etc.
+Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...`
-* Parameters can be in any order.
- e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable.
+
-* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
- e.g. if the command specifies `help 123`, it will be interpreted as `help`.
+**:bulb: Tip**
-* 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.
+A person can have any number of tags (including 0).
-### Viewing help : `help`
+Examples:
+* `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01`
+* `add n/Betsy Crowe e/betsycrowe@example.com a/Apple HQ p/1234567 t/Marketing Department t/Apple`
-Shows a message explaning how to access the help page.
+
-
+### [3.1.2. Deleting a Person](#3-1-managing-contacts) : `delete`
-Format: `help`
+Deletes the specified person from PressPlanner's address book.
+Format: `delete INDEX`
-### Adding a person: `add`
+* Deletes the person at the specified `INDEX`.
-Adds a person to the address book.
+Examples:
+* `delete 2` deletes the 2nd person in the current address book view.
-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)
-
+**:information_source: Important**
-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`
+Commands such as [`find`](#3-1-5-searching-for-a-person-by-name-find) can alter the current view of the address book. The `INDEX` refers to the index number shown in the current view.
+
-### Listing all persons : `list`
+### [3.1.3. Listing All Persons](#3-1-managing-contacts) : `list`
-Shows a list of all persons in the address book.
+Shows a list of all persons in PressPlanner's address book.
+* Use this command to restore the full list of persons after using other commands.
Format: `list`
-### Editing a person : `edit`
+### [3.1.4. Editing a person](#3-1-managing-contacts) : `edit`
-Edits an existing person in the address book.
+Edits an existing person in PressPlanner's address book.
-Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…`
+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, …
+* Edits the person at the specified `INDEX`.
* 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.
+
+
+
+**:warning: Warning**
+
+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.
+
+
+
+
+**:information_source: Important**
+
+Editing the name of a person will automatically update their names in articles referencing them as contributors or interviewees.
+
Examples:
* `edit 1 p/91234567 e/johndoe@example.com` Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively.
-* `edit 2 n/Betsy Crower t/` Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags.
+* `edit 2 n/Betsy Crowe t/` Edits the name of the 2nd person to be `Betsy Crowe` and clears all existing tags.
+
+
-### Locating persons by name: `find`
+### [3.1.5. Searching for a Person by Name](#3-1-managing-contacts) : `find`
Finds persons whose names contain any of the given keywords.
Format: `find KEYWORD [MORE_KEYWORDS]`
-* 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`
+* 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`
+* Only full words will be matched e.g. `Han` will not match `Hans`.
* Persons matching at least one keyword will be returned (i.e. `OR` search).
e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang`
Examples:
* `find John` returns `john` and `John Doe`
-* `find alex david` returns `Alex Yeoh`, `David Li`
- 
+* `find alex david` returns `Alex Yeoh`, `David Li`
-### Deleting a person : `delete`
+* Before finding:
-Deletes the specified person from the address book.
+ 
-Format: `delete INDEX`
+* After finding using `find chester`:
+
+ 
-* 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, …
+
+
+### [3.1.6. Lookup Associated Articles](#3-1-managing-contacts) : `lookup`
+
+Display articles associated with the person where they are contributors or interviewees.
+
+Format: `lookup INDEX`
+
+* Display articles related to the person at the specified `INDEX`
+* The matching of persons to articles is based on the person's name.
+ * It is case-sensitive (e.g. Looking up `John` in the address book will not match articles with `john` as an interviewee or contributor).
+ * It is an exact match of the person's full name (e.g. Looking up `John` in the address book will not match articles with `Johnny` or `John Doe` as interviewees or contributors).
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.
+* `lookup 1` returns all articles associated with the first person in the list of contacts.
+
+* Before lookup:
+
+ 
+
+* After lookup using `lookup 2`:
+ * (The articles associated with the second person in the list of contacts are shown)
+
+ 
+
+### [3.1.7. Sorting Persons by Name](#3-1-managing-contacts) : `sort n/`
+
+Sorts persons in ascending order by the lexicographical (alphabetical) ordering of their names.
+
+Format: `sort n/`
-### Clearing all entries : `clear`
+* Executing the `sort n/` command sorts all persons in PressPlanner permanently.
+ * This works differently from commands which change the current view temporarily (e.g. [find](#3-1-5-searching-for-a-person-by-name-find)).
+ * Closing and reopening the app in future will result in contacts remaining sorted by name.
+* Sorting is only done when the command is executed and not automatically maintained afterwards:
+ * A person added using `add` after a `sort n/` command will be added to the end of the address book, regardless of their name.
+ * A person edited using `edit` to change their name after a `sort n/` command will not change its position in the address book.
+
+Example:
+* `sort n/` sorts all persons in PressPlanner in ascending order by the lexicographical (alphabetical) ordering of their names.
+
+* Before sorting:
+ * (Aaron Tan and Barry Allen are recent additions to the template data and are currently at the end of the list)
+
+ 
+
+* After sorting:
+ * (Both Aaron Tan and Barry Allen moved up the list and are now in ascending order with respect to alphabetical ordering of their names)
+
+ 
+
+* Success message shown:
+ * `sorted all persons by name`
+
+
+### [3.1.8. Clearing All Persons : `clear`](#3-1-managing-contacts)
Clears all entries from the address book.
Format: `clear`
-### Exiting the program : `exit`
+
-Exits the program.
+**:warning: Warning**
-Format: `exit`
+This action is irreversible. All persons will be deleted from the address book. The app will not prompt you to confirm this action.
+
+
+## [3.2. Managing Articles](#3-features)
+PressPlanner's article management system is designed to help you keep track of your articles and the people involved in them. As a freelancer, you lack the same resources a major firm has. PressPlanner helps you maximise the value you can get from your contacts by helping you keep track of which contacts you've worked with for different articles.
+
+
+
+**:bulb: Tip**
+
+Refer to [Why Use PressPlanner?](#1-2-why-use-pressplanner) for some of our recommended workflows and how PressPlanner can help you.
+
+
+
+
+**:information_source: Important**
+
+Here are some important terms that will be used in this section:
+1. `DATE`
+ * All articles have a mandatory `DATE` field. This field is also used in commands like [`filter`](#3-2-6-filtering-articles-filter-a)
+ * `DATE` must be in the format `dd-mm-yyyy [HH:mm]`.
+ * `HH:mm` is optional and defaults to `00:00` if not provided.
+ * `HH:mm` must be in 24-hour format.
+ * Examples of valid dates: `01-01-2023`, `01-01-2023 22:30`
+2. `STATUS`
+ * All articles have a mandatory `STATUS` field.
+ * `STATUS` can be:
+ 1. `draft`
+ 2. `published`
+ 3. `archived`.
+ * The `STATUS` parameter is not case-sensitive (e.g. `draft` and `DRAFT` are both valid inputs).
+
+
+### [3.2.1. Adding an Article](#3-2-managing-articles) : `add -a`
+Adds a new article to PressPlanner's database.
+
+Format: `add -a h/HEADLINE d/DATE s/STATUS [c/CONTRIBUTOR]... [i/INTERVIEWEE]... [t/TAG]...[o/OUTLET]... [l/LINK]`
+
+
+
+**:warning: Warning**
+
+1. Only `HEADLINE`, `DATE`, and `STATUS` are mandatory fields.
+ * Refer to [Managing Articles](#3-2-managing-articles) for the valid formats of `DATE` and `STATUS`.
+ * An article's `HEADLINE` must be unique **unless it is a `draft`**
+ * `HEADLINE` accepts any characters, but spaces at the start will be automatically removed.
+ * `HEADLINE` can also be left blank. This is not recommended, but allowed for flexibility.
+ * e.g. `add -a h/ d/20-10-2023 s/draft` is a valid command and will add an article with a blank headline.
+ * Some users may find this useful for adding drafts quickly and filling in the headline later.
+ * An article's `DATE` can represent what you choose to be relevant to your workflow, we recommend using it to represent:
+ * Time of creation for drafts.
+ * Time of publication for published articles.
+2. Entering multiple prefixes for any of these fields will overwrite the previous values.
+ * e.g. `add -a h/My Article d/20-10-2023 s/draft h/My Second Article` will add an article titled `My Second Article`.
+ * e.g. `add -a h/My Article d/01-01-2024 s/draft d/02-02-2024` will add an article with the date `02-02-2024`.
+
-### Saving the data
+* A `CONTRIBUTOR` is a co-author or information source that was not directly interviewed for an article.
+* An `INTERVIEWEE` is a person directly interviewed for an article.
+ * If a `CONTRIBUTOR` or `INTERVIEWEE` has the same name as a contact in PressPlanner's address book, [`lookup`](#3-1-6-lookup-associated-articles-lookup) and [`lookup -a`](#3-2-8-lookup-associated-persons-lookup-a) commands can be used to find articles and persons associated with each other.
+* An `OUTLET` is the publication or platform the article was published on.
+* A `TAG` is any keyword or phrase that helps categorise the article.
+* A `LINK` is a URL to the article.
-AddressBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually.
+
+
+**:information_source: Important**
+
+Adding an article will return to displaying all articles if a [find](#3-2-5-searching-for-an-article-by-headline-find-a) command was executed before.
+ * This does not apply to [filters](#3-2-6-filtering-articles-filter-a).
+
+
+Examples:
+* `add -a h/iPhone 13 Review d/20-03-2024 s/draft c/John Doe i/Michael Lee t/New Releases`
+* `add -a h/AI Inc. Acquired by Google d/30-08-2024 08:45 s/published c/Alex Johnson i/Emily Brown t/AI o/CNA l/www.example.com`
+
+### [3.2.2. Deleting an Article](#3-2-managing-articles) : `delete -a`
+
+Deletes an existing article from PressPlanner's database.
+
+Format : `delete -a INDEX`
+
+* Deletes the article at the specified `INDEX`.
+ * The `INDEX` refers to the index number shown in the current article list view.
+ * If a `filter`, `sort` or `find` command was executed before, the index refers to the index number shown in the filtered/sorted list of articles.
+ * e.g. `delete 1` after the `find` command deletes the first article found by the `find` command.
+
+Example : `delete -a 1` deletes the article at the first index.
+
+### [3.2.3. Listing All Articles](#3-2-managing-articles) : `list -a`
+
+List out all articles in PressPlanner's database.
+
+Format: `list -a`
+
+* No parameters necessary.
+ * Extra characters in the command (e.g. `list -ab`, `list -a1`) will be ignored and treated as `list` for persons instead.
+ * Extra whitespace characters in the command (e.g. `list -a `, `list -a `) are acceptable.
+ * Extra characters after a whitespace will be ignored (e.g. `list -a 123` will be interpreted as `list -a`).
+
+### [3.2.4. Editing an Article](#3-2-managing-articles) : `edit -a`
+
+Edits an existing article in PressPlanner's database.
+
+Format: `edit -a INDEX [h/HEADLINE] [d/DATE] [s/STATUS] [c/CONTRIBUTOR]... [i/INTERVIEWEE]... [t/TAG]... [o/OUTLET]... [l/LINK]`
-### Editing the data file
+* Edits the article at the specified `INDEX`.
+* **At least one** of the optional fields must be provided.
+* Refer to the [`add -a` command](#3-2-1-adding-an-article-add-a) for the format of each field.
+ * Note that `edit -a` will also behave the same as `add -a` in terms of returning to the full list of articles if used after a `find` command.
-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.
+
-
: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.
+
+
+**:warning: Warning**
+
+* Editing an article will be updated any included field to the new input.
+ * i.e. The original values will be overwritten by the new values.
+ * `c/`, `i/`, `t/`, `o/` and `l/` without any value after it will clear all existing values.
+ * e.g. `c/new contributor` will replace all existing contributors with `new contributor`.
+* Editing is irreversible, so make sure you have the correct information before executing the command.
-### Archiving data files `[coming in v2.0]`
+Examples:
+* `edit -a 1 h/iPhone Review` Edits the headline of the 1st article to be `iPhone Review`.
+* `edit -a 2 h/iPhone Review i/` Edits the headline of the 2nd article to be `iPhone Review` and clears all existing interviewees.
+
+### [3.2.5. Searching for an Article by Headline](#3-2-managing-articles) : `find -a`
+
+Find articles with headlines containing any of the given keywords.
+
+Format: `find -a KEYWORD [MORE_KEYWORDS]`
+
+* Only the headline is searched for matches.
+* The search is case-insensitive.
+ * e.g `iphone` will match `iPhone`
+* The order of the keywords does not matter.
+ * e.g. `Pro Vision` will match `Vision Pro`
+* Only full words will be matched.
+ * e.g. `iPhone` will not match `iPhones`
+* Articles matching at least one keyword will be returned.
+ * e.g. `find -a Vision Pro` will return an article with the headline: `Pro tips for Windows 10 Users`
+
+Examples:
+* `find -a Vision Pro` returns articles with headlines containing `Vision` or `Pro`.
+
+* Before finding:
+
+ 
+
+* After finding using `find -a iphone`:
+
+ 
+
+### [3.2.6. Filtering Articles](#3-2-managing-articles) : `filter -a`
+
+Filter PressPlanner's database by a combination of attributes to find articles you are looking for quickly.
+
+Format: `filter -a s/STATUS t/TAG ST/START_DATE EN/END_DATE`
+* All the prefixes need to be included, but can be left blank.
+ * e.g. `filter -a s/ t/ st/ en/` is a valid command
+ * e.g. `filter -a s/ t/ st/` is not a valid command
+ * Leaving `st/` or `en/` blank defaults to the earliest or latest date possible respectively.
+* Use the `filter` command **prior to a `find` command**.
+ * `filter` will list all matching articles within the database when first applied.
+ * `find` can be used to then search the filtered list.
+ * Using `filter` after a `find` command will overwrite the previous `find` command.
+* Filters are not stored between sessions, so make sure to finish your search before closing the app!
+* Filters will apply until you [remove](#3-2-7-removing-a-filter-rmfilter-a) it or apply a new filter, so make sure you [remove](#3-2-7-removing-a-filter-rmfilter-a) it after you are done!
+* Refer to the [add article](#3-2-1-adding-an-article-add-a) command for the format of each field.
+ * Note that `START_DATE` and `END_DATE` must be in the same format as `DATE` in the [add article](#3-2-1-adding-an-article-add-a) command.
+ * The `START_DATE` should come **before** the `END_DATE`. If not, you will receive an error!
+ * The date of the article you are looking for should not be equal to the `START_DATE` or `END_DATE`.
+* Only one filter command can be active at once, using another filter will override the last one.
+
+Examples:
+* `filter -a s/DRAFT t/ st/ en/` will restrict the display to showing only articles with draft status.
+* Using the command:
+
+ 
+
+* After the command:
+
+ 
+
+### [3.2.7. Removing a Filter](#3-2-managing-articles) : `rmfilter -a`
+Remove all filters so that all articles in PressPlanner's database are displayed.
+
+Format: `rmfilter -a`
+
+* No additional parameters.
+* The `-a` is necessary, additional letters will cause the command to fail.
+ * Additional parameters after a whitespace will be ignored (e.g. `rmfilter -a 123` will be interpreted as `rmfilter -a`).
+* Using the command:
+
+ 
+
+* After the command:
+
+ 
+
+### [3.2.8. Lookup Associated Persons](#3-2-managing-articles) : `lookup -a`
+
+Finds persons associated with an article as interviewees or contributors.
+
+Format: `lookup -a INDEX`
+
+* Display list of persons related to the article at the specified `INDEX`.
+* The matching of articles to persons is based on the person's name.
+ * It is case-sensitive (e.g. Looking up an article with `john` as an interviewee or contributor will not match `John` in the address book).
+ * It is an exact match of the person's full name (e.g. Looking up an article with `John` as an interviewee or contributor will not match `Johnny` or `John Doe` in the address book).
+
+Examples:
+* `lookup -a 1` returns all persons associated with the first article in the list of articles.
+
+* Before lookup:
+
+ 
+
+* After lookup using `lookup -a 2`:
+ * (Bob who was the contributor for the second article is shown in the list of persons associated with the article
+
+ 
+
+### [3.2.9. Sorting Articles by Date](#3-2-managing-articles) : `sort -a d/`
-_Details coming soon ..._
+Sort articles in PressPlanner's database in descending order by their date and time.
---------------------------------------------------------------------------------------------------------------------
+Format: `sort -a d/`
-## FAQ
+* Executing the `sort -a d/` command sorts all articles in PressPlanner permanently.
+ * This works differently from commands which change the current view (e.g. [find](#3-2-5-searching-for-an-article-by-headline-find-a), [lookup](#3-2-8-lookup-associated-persons-lookup-a) or [filter](#3-2-6-filtering-articles-filter-a)).
+ * Closing and reopening the app in future will result in articles remaining sorted by date.
+* Sorting is only done when the command is executed and not automatically maintained afterwards:
+ * An article added using `add -a` after a `sort -a d/` command will be added to the end of the list, regardless of its date.
+ * An article edited using `edit -a` to change the date after a `sort -a d/` command will not change its position in the list.
-**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.
+Example:
+* `sort -a d/` sorts all articles in PressPlanner in descending order by their date and time.
---------------------------------------------------------------------------------------------------------------------
+* Before sorting:
+ * (The first and second articles are in ascending order by date)
+
+ 
-## Known issues
+* After sorting:
+ * (The articles are now in descending order by date)
-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.
+ 
---------------------------------------------------------------------------------------------------------------------
+* Success message shown:
+ * `sorted all articles by date`
-## Command summary
+### [3.2.10. Opening a Webpage for an Article](#3-2-managing-articles)
+
+* By clicking the `Link` button of your article that is highlighted in yellow box in the picture below, you can open up the webpage for your article that was included in the article.
+
+ 
+
+
+
+**:warning: Warning**
+
+PressPlanner does not check the validity of links. If the webpage does not open when clicked, it likely means that the `link` of the article is invalid.
+
+
+## [3.3. Other Commands](#3-features)
+
+### [3.3.1. Viewing Help](#3-3-other-commands) : `help`
+
+Shows a message with the URL to access this User Guide.
+
+* Can also be accessed via the button at the top of the app
+* Can also be accessed via pressing the `F1` key on your keyboard
+
+ 
+
+Format: `help`
+
+### [3.3.2. Exiting PressPlanner](#3-3-other-commands) : `exit`
+
+Exits the program.
+
+Format: `exit`
-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`
+
+
+## [4. Commands Quick Reference](#table-of-contents)
+| Action | Command Format | Example |
+|----------------------------|--------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|
+| List Persons | `list` | `list` |
+| Add Person | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...` | `add n/Betsy Crowe p/1234567 e/betsycrowe@example.com a/Apple HQ t/Marketing Department t/Apple` |
+| Delete Person | `delete INDEX` | `delete 3` |
+| Edit Person | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]...` | `edit 2 n/Betsy Crowe e/betsycrowe@example.com` |
+| Find Person | `find KEYWORD [MORE_KEYWORDS]` | `find Crowe Betsy` |
+| Lookup Associated Articles | `lookup INDEX` | `lookup 1` |
+| Sort Persons by Name | `sort n/` | `sort n/` |
+| Clear Persons | `clear` | `clear` |
+| List Article | `list -a` | `list -a` |
+| Add Article | `add -a h/HEADLINE d/DATE s/STATUS [c/CONTRIBUTOR]... [i/INTERVIEWEE]... [t/TAG]... [o/OUTLET]... [l/LINK]` | `add -a h/AI Inc. Acquired by Google d/30-08-2024 08:45 s/published c/Alex Johnson i/Emily Brown t/AI o/CNA l/www.example.com` |
+| Delete Article | `delete -a INDEX` | `delete -a 1` |
+| Edit Article | `edit -a INDEX [h/HEADLINE] [d/DATE] [s/STATUS] [c/CONTRIBUTOR]... [i/INTERVIEWEE]... [t/TAG]... [o/OUTLET]... [l/LINK]` | `edit -a 2 h/iPhone Review` |
+| Find Article | `find -a KEYWORD [MORE_KEYWORDS]` | `find -a Vision Pro` |
+| Filter Articles | `filter -a s/STATUS t/TAG ST/START_DATE EN/END_DATE ` | `filter -a s/draft t/Apple st/01-01-2024 en/` |
+| Remove Filter | `rmfilter -a` | `rmfilter -a` |
+| Lookup Associated People | `lookup -a INDEX` | `lookup -a 1` |
+| Sort Articles | `sort -a d/` | `sort -a d/` |
+| Help | `help` | `help` |
+| Exit | `exit` | `exit` |
+
+
+
+## [5. Upcoming Features](#table-of-contents)
+### [5.1. Clearing all Articles](#5-upcoming-features)
+In PressPlanner's current version, we have provided a command to clear all persons from the address book using the `clear` command. This is intended to help you clear all contacts in the case of personal data privacy and security concerns.
+
+Users have requested a similar command to clear all articles from the article book to improve their personal workflows. We are working on implementing this feature in the near future.
+
+* In the meantime, you can manually delete the `articlebook.json` file in the `data` folder to clear all articles.
+ * For example, your articlebook may look something like this:
+
+ 
+ * Simply go into the folder PressPlanner is in:
+
+ 
+ * Go inside the data folder, which will look something like this:
+
+ 
+ * Delete the file named articlebook:
+
+ 
+ * Now, if you open PressPlanner again, you will see that your articles will have changed back to the sample data:
+
+ 
+ * Now all you have to do is to delete the sample article, and you are set!
+
+### [5.2. Filtering People](#5-upcoming-features)
+In PressPlanner's current version, filtering is available for articles and intended to help you find articles that you may have forgotten the name of by other attributes such as date, status, tags, etc.
+
+Users have requested a similar feature for persons. We are working on implementing this feature in the near future.
+
+## [6. FAQs](#table-of-contents)
+### [6.1. Why am I unable to run PressPlanner on my desktop?](#6-faqs)
+* Please check that you have downloaded Version 11 Java SDK that suits your computer’s operating system (Windows, MacOS, Linux).
+* If you are unsure of whether you have done so, please navigate back to the “Getting Started” Segment of the User Guide to access the link which will bring you to the Java Oracle Website where you can re-download the Version 11 Java SDK for your operating system.
+
+### [6.2. How do I ensure that I have saved all the contacts and articles I have added in this session?](#6-faqs)
+* Do not worry about issues regarding the saving of data you have entered into the application, they are saved into files automatically upon the execution of every command which modifies or adds new data.
+
+### [6.3. Why were all my previous data for contacts (and / or) articles from previous sessions deleted and replaced by the default template data?](#6-faqs)
+* This means that your save file was either corrupted or lost. To avoid this, refrain from editing files in the data folder (specifically **AddressBook.json** and **ArticleBook.json** files which contain the saved contacts and articles respectively, from previous sessions) unless you are sure about what you are doing.
+* To mitigate possible accidental data corruption which may result in the deletion of the save files, please make a copy of the data files after every session where major changes were made, so that in the event the most recent data is lost, you would still have a recent data file which can then be added back into the data folder located at the working directory of the PressPlanner.jar file and be loaded up into the application.
+
+### [6.4. Why does my link not open the browser?](#6-faqs)
+* This means that your URL added to the PressPlanner is an invalid link.
+* To let you know when the URL is invalid, we will soon be implementing the app to show you an error message when your link that is typed is invalid. Please wait for our future updates!
diff --git a/docs/_config.yml b/docs/_config.yml
deleted file mode 100644
index 6bd245d8f4e..00000000000
--- a/docs/_config.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-title: "AB-3"
-theme: minima
-
-header_pages:
- - UserGuide.md
- - DeveloperGuide.md
- - AboutUs.md
-
-markdown: kramdown
-
-repository: "se-edu/addressbook-level3"
-github_icon: "images/github-icon.png"
-
-plugins:
- - jemoji
diff --git a/docs/_data/projects.yml b/docs/_data/projects.yml
deleted file mode 100644
index 8f3e50cb601..00000000000
--- a/docs/_data/projects.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-- name: "AB-1"
- url: https://se-edu.github.io/addressbook-level1
-
-- name: "AB-2"
- url: https://se-edu.github.io/addressbook-level2
-
-- name: "AB-3"
- url: https://se-edu.github.io/addressbook-level3
-
-- name: "AB-4"
- url: https://se-edu.github.io/addressbook-level4
-
-- name: "Duke"
- url: https://se-edu.github.io/duke
-
-- name: "Collate"
- url: https://se-edu.github.io/collate
-
-- name: "Book"
- url: https://se-edu.github.io/se-book
-
-- name: "Resources"
- url: https://se-edu.github.io/resources
diff --git a/docs/_includes/custom-head.html b/docs/_includes/custom-head.html
deleted file mode 100644
index 8559a67ffad..00000000000
--- a/docs/_includes/custom-head.html
+++ /dev/null
@@ -1,6 +0,0 @@
-{% comment %}
- Placeholder to allow defining custom head, in principle, you can add anything here, e.g. favicons:
-
- 1. Head over to https://realfavicongenerator.net/ to add your own favicons.
- 2. Customize default _includes/custom-head.html in your source directory and insert the given code snippet.
-{% endcomment %}
diff --git a/docs/_includes/head.html b/docs/_includes/head.html
deleted file mode 100644
index 83ac5326933..00000000000
--- a/docs/_includes/head.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
- {%- include custom-head.html -%}
-
- {{page.title}}
-
-
diff --git a/docs/_includes/header.html b/docs/_includes/header.html
deleted file mode 100644
index 33badcd4f99..00000000000
--- a/docs/_includes/header.html
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
+
-:information_source: Don’t forget to update `AddressBookParser` to use our new `RemarkCommandParser`!
+Don’t forget to update `AddressBookParser` to use our new `RemarkCommandParser`!
-
+
If you are stuck, check out the sample
[here](https://github.com/se-edu/addressbook-level3/commit/dc6d5139d08f6403da0ec624ea32bd79a2ae0cbf#diff-8bf239e8e9529369b577701303ddd96af93178b4ed6735f91c2d8488b20c6b4a).
@@ -244,7 +247,7 @@ Simply add the following to [`seedu.address.ui.PersonCard`](https://github.com/s
**`PersonCard.java`:**
-``` java
+```java
@FXML
private Label remark;
```
@@ -276,11 +279,11 @@ We change the constructor of `Person` to take a `Remark`. We will also need to d
Unfortunately, a change to `Person` will cause other commands to break, you will have to modify these commands to use the updated `Person`!
-
+
-:bulb: Use the `Find Usages` feature in IntelliJ IDEA on the `Person` class to find these commands.
+Use the `Find Usages` feature in IntelliJ IDEA on the `Person` class to find these commands.
-
+
Refer to [this commit](https://github.com/se-edu/addressbook-level3/commit/ce998c37e65b92d35c91d28c7822cd139c2c0a5c) and check that you have got everything in order!
@@ -291,11 +294,11 @@ AddressBook stores data by serializing `JsonAdaptedPerson` into `json` with the
While the changes to code may be minimal, the test data will have to be updated as well.
-
+
-:exclamation: You must delete AddressBook’s storage file located at `/data/addressbook.json` before running it! Not doing so will cause AddressBook to default to an empty address book!
+You must delete AddressBook’s storage file located at `/data/addressbook.json` before running it! Not doing so will cause AddressBook to default to an empty address book!
-
+
Check out [this commit](https://github.com/se-edu/addressbook-level3/commit/556cbd0e03ff224d7a68afba171ad2eb0ce56bbf)
to see what the changes entail.
@@ -308,7 +311,7 @@ Just add [this one line of code!](https://github.com/se-edu/addressbook-level3/c
**`PersonCard.java`:**
-``` java
+```java
public PersonCard(Person person, int displayedIndex) {
//...
remark.setText(person.getRemark().value);
@@ -328,7 +331,7 @@ save it with `Model#setPerson()`.
**`RemarkCommand.java`:**
-``` java
+```java
//...
public static final String MESSAGE_ADD_REMARK_SUCCESS = "Added remark to Person: %1$s";
public static final String MESSAGE_DELETE_REMARK_SUCCESS = "Removed remark from Person: %1$s";
diff --git a/docs/tutorials/RemovingFields.md b/docs/tutorials/RemovingFields.md
index f29169bc924..c73bd379e5e 100644
--- a/docs/tutorials/RemovingFields.md
+++ b/docs/tutorials/RemovingFields.md
@@ -1,8 +1,11 @@
---
-layout: page
-title: "Tutorial: Removing Fields"
+ layout: default.md
+ title: "Tutorial: Removing Fields"
+ pageNav: 3
---
+# Tutorial: Removing Fields
+
> Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.
>
> — Antoine de Saint-Exupery
@@ -10,17 +13,17 @@ title: "Tutorial: Removing Fields"
When working on an existing code base, you will most likely find that some features that are no longer necessary.
This tutorial aims to give you some practice on such a code 'removal' activity by removing the `address` field from `Person` class.
-
+
**If you have done the [Add `remark` command tutorial](AddRemark.html) already**, you should know where the code had to be updated to add the field `remark`. From that experience, you can deduce where the code needs to be changed to _remove_ that field too. The removing of the `address` field can be done similarly.
However, if you have no such prior knowledge, removing a field can take a quite a bit of detective work. This tutorial takes you through that process. **At least have a read even if you don't actually do the steps yourself.**
-
+
-* Table of Contents
-{:toc}
+
+
## Safely deleting `Address`
@@ -50,10 +53,10 @@ Let’s try removing references to `Address` in `EditPersonDescriptor`.
1. Remove the usages of `address` and select `Do refactor` when you are done.
-
+
- :bulb: **Tip:** Removing usages may result in errors. Exercise discretion and fix them. For example, removing the `address` field from the `Person` class will require you to modify its constructor.
-
+ **Tip:** Removing usages may result in errors. Exercise discretion and fix them. For example, removing the `address` field from the `Person` class will require you to modify its constructor.
+
1. Repeat the steps for the remaining usages of `Address`
@@ -71,7 +74,7 @@ A quick look at the `PersonCard` class and its `fxml` file quickly reveals why i
**`PersonCard.java`**
-``` java
+```java
...
@FXML
private Label address;
diff --git a/docs/tutorials/TracingCode.md b/docs/tutorials/TracingCode.md
index 4fb62a83ef6..2b1b0f2d6b7 100644
--- a/docs/tutorials/TracingCode.md
+++ b/docs/tutorials/TracingCode.md
@@ -1,26 +1,30 @@
---
-layout: page
-title: "Tutorial: Tracing code"
+ layout: default.md
+ title: "Tutorial: Tracing code"
+ pageNav: 3
---
+# Tutorial: Tracing code
+
+
> Indeed, the ratio of time spent reading versus writing is well over 10 to 1. We are constantly reading old code as part of the effort to write new code. …\[Therefore,\] making it easy to read makes it easier to write.
>
> — Robert C. Martin Clean Code: A Handbook of Agile Software Craftsmanship
When trying to understand an unfamiliar code base, one common strategy used is to trace some representative execution path through the code base. One easy way to trace an execution path is to use a debugger to step through the code. In this tutorial, you will be using the IntelliJ IDEA’s debugger to trace the execution path of a specific user command.
-* Table of Contents
-{:toc}
+
+
## Before we start
Before we jump into the code, it is useful to get an idea of the overall structure and the high-level behavior of the application. This is provided in the 'Architecture' section of the developer guide. In particular, the architecture diagram (reproduced below), tells us that the App consists of several components.
-
+
It also has a sequence diagram (reproduced below) that tells us how a command propagates through the App.
-
+
Note how the diagram shows only the execution flows _between_ the main components. That is, it does not show details of the execution path *inside* each component. By hiding those details, the diagram aims to inform the reader about the overall execution path of a command without overwhelming the reader with too much details. In this tutorial, you aim to find those omitted details so that you get a more in-depth understanding of how the code works.
@@ -37,16 +41,16 @@ As you know, the first step of debugging is to put in a breakpoint where you wan
In our case, we would want to begin the tracing at the very point where the App start processing user input (i.e., somewhere in the UI component), and then trace through how the execution proceeds through the UI component. However, the execution path through a GUI is often somewhat obscure due to various *event-driven mechanisms* used by GUI frameworks, which happens to be the case here too. Therefore, let us put the breakpoint where the `UI` transfers control to the `Logic` component.
-
+
According to the sequence diagram you saw earlier (and repeated above for reference), the `UI` component yields control to the `Logic` component through a method named `execute`. Searching through the code base for an `execute()` method that belongs to the `Logic` component yields a promising candidate in `seedu.address.logic.Logic`.
-
+
-:bulb: **Intellij Tip:** The ['**Search Everywhere**' feature](https://www.jetbrains.com/help/idea/searching-everywhere.html) can be used here. In particular, the '**Find Symbol**' ('Symbol' here refers to methods, variables, classes etc.) variant of that feature is quite useful here as we are looking for a _method_ named `execute`, not simply the text `execute`.
-
+**Intellij Tip:** The ['**Search Everywhere**' feature](https://www.jetbrains.com/help/idea/searching-everywhere.html) can be used here. In particular, the '**Find Symbol**' ('Symbol' here refers to methods, variables, classes etc.) variant of that feature is quite useful here as we are looking for a _method_ named `execute`, not simply the text `execute`.
+
A quick look at the `seedu.address.logic.Logic` (an extract given below) confirms that this indeed might be what we’re looking for.
@@ -67,14 +71,14 @@ public interface Logic {
But apparently, this is an interface, not a concrete implementation.
That should be fine because the [Architecture section of the Developer Guide](../DeveloperGuide.html#architecture) tells us that components interact through interfaces. Here's the relevant diagram:
-
+
Next, let's find out which statement(s) in the `UI` code is calling this method, thus transferring control from the `UI` to the `Logic`.
-
+
-:bulb: **Intellij Tip:** The ['**Find Usages**' feature](https://www.jetbrains.com/help/idea/find-highlight-usages.html#find-usages) can find from which parts of the code a class/method/variable is being used.
-
+**Intellij Tip:** The ['**Find Usages**' feature](https://www.jetbrains.com/help/idea/find-highlight-usages.html#find-usages) can find from which parts of the code a class/method/variable is being used.
+

@@ -87,10 +91,10 @@ Now let’s set the breakpoint. First, double-click the item to reach the corres
Recall from the User Guide that the `edit` command has the format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…` For this tutorial we will be issuing the command `edit 1 n/Alice Yeoh`.
-
+
-:bulb: **Tip:** Over the course of the debugging session, you will encounter every major component in the application. Try to keep track of what happens inside the component and where the execution transfers to another component.
-
+**Tip:** Over the course of the debugging session, you will encounter every major component in the application. Try to keep track of what happens inside the component and where the execution transfers to another component.
+
1. To start the debugging session, simply `Run` \> `Debug Main`
@@ -110,7 +114,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [
**LogicManager\#execute().**
- ``` java
+ ```java
@Override
public CommandResult execute(String commandText)
throws CommandException, ParseException {
@@ -142,7 +146,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [

1. _Step into_ the line where user input in parsed from a String to a Command, which should bring you to the `AddressBookParser#parseCommand()` method (partial code given below):
- ``` java
+ ```java
public Command parseCommand(String userInput) throws ParseException {
...
final String commandWord = matcher.group("commandWord");
@@ -157,7 +161,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [
1. Stepping through the `switch` block, we end up at a call to `EditCommandParser().parse()` as expected (because the command we typed is an edit command).
- ``` java
+ ```java
...
case EditCommand.COMMAND_WORD:
return new EditCommandParser().parse(arguments);
@@ -166,8 +170,10 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [
1. Let’s see what `EditCommandParser#parse()` does by stepping into it. You might have to click the 'step into' button multiple times here because there are two method calls in that statement: `EditCommandParser()` and `parse()`.
-
:bulb: **Intellij Tip:** Sometimes, you might end up stepping into functions that are not of interest. Simply use the `step out` button to get out of them!
-
+
+
+ **Intellij Tip:** Sometimes, you might end up stepping into functions that are not of interest. Simply use the `step out` button to get out of them!
+
1. Stepping through the method shows that it calls `ArgumentTokenizer#tokenize()` and `ParserUtil#parseIndex()` to obtain the arguments and index required.
@@ -175,17 +181,17 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [

1. As you just traced through some code involved in parsing a command, you can take a look at this class diagram to see where the various parsing-related classes you encountered fit into the design of the `Logic` component.
-
+
1. Let’s continue stepping through until we return to `LogicManager#execute()`.
The sequence diagram below shows the details of the execution path through the Logic component. Does the execution path you traced in the code so far match the diagram?
- 
+
1. Now, step over until you read the statement that calls the `execute()` method of the `EditCommand` object received, and step into that `execute()` method (partial code given below):
**`EditCommand#execute()`:**
- ``` java
+ ```java
@Override
public CommandResult execute(Model model) throws CommandException {
...
@@ -205,25 +211,28 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [
* it uses the `updateFilteredPersonList` method to ask the `Model` to populate the 'filtered list' with _all_ persons.
FYI, The 'filtered list' is the list of persons resulting from the most recent operation that will be shown to the user immediately after. For the `edit` command, we populate it with all the persons so that the user can see the edited person along with all other persons. If this was a `find` command, we would be setting that list to contain the search results instead.
To provide some context, given below is the class diagram of the `Model` component. See if you can figure out where the 'filtered list' of persons is being tracked.
-
+
* :bulb: This may be a good time to read through the [`Model` component section of the DG](../DeveloperGuide.html#model-component)
1. As you step through the rest of the statements in the `EditCommand#execute()` method, you'll see that it creates a `CommandResult` object (containing information about the result of the execution) and returns it.
Advancing the debugger by one more step should take you back to the middle of the `LogicManager#execute()` method.
1. Given that you have already seen quite a few classes in the `Logic` component in action, see if you can identify in this partial class diagram some of the classes you've encountered so far, and see how they fit into the class structure of the `Logic` component:
-
+
+
* :bulb: This may be a good time to read through the [`Logic` component section of the DG](../DeveloperGuide.html#logic-component)
1. Similar to before, you can step over/into statements in the `LogicManager#execute()` method to examine how the control is transferred to the `Storage` component and what happens inside that component.
-
:bulb: **Intellij Tip:** When trying to step into a statement such as `storage.saveAddressBook(model.getAddressBook())` which contains multiple method calls, Intellij will let you choose (by clicking) which one you want to step into.
-
+
+
+ **Intellij Tip:** When trying to step into a statement such as `storage.saveAddressBook(model.getAddressBook())` which contains multiple method calls, Intellij will let you choose (by clicking) which one you want to step into.
+
-1. As you step through the code inside the `Storage` component, you will eventually arrive at the `JsonAddressBook#saveAddressBook()` method which calls the `JsonSerializableAddressBook` constructor, to create an object that can be _serialized_ (i.e., stored in storage medium) in JSON format. That constructor is given below (with added line breaks for easier readability):
+1. As you step through the code inside the `Storage` component, you will eventually arrive at the `JsonAddressBook#saveAddressBook()` method which calls the `JsonSerializableAddressBook` constructor, to create an object that can be _serialized_ (i.e., stored in storage medium) in JSON format. That constructor is given below (with added line breaks for easier readability):
**`JsonSerializableAddressBook` constructor:**
- ``` java
+ ```java
/**
* Converts a given {@code ReadOnlyAddressBook} into this class for Jackson use.
*
@@ -243,7 +252,8 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [
This is because regular Java objects need to go through an _adaptation_ for them to be suitable to be saved in JSON format.
1. While you are stepping through the classes in the `Storage` component, here is the component's class diagram to help you understand how those classes fit into the structure of the component.
-
+
+
* :bulb: This may be a good time to read through the [`Storage` component section of the DG](../DeveloperGuide.html#storage-component)
1. We can continue to step through until you reach the end of the `LogicManager#execute()` method and return to the `MainWindow#executeCommand()` method (the place where we put the original breakpoint).
@@ -251,7 +261,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [
1. Stepping into `resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser());`, we end up in:
**`ResultDisplay#setFeedbackToUser()`**
- ``` java
+ ```java
public void setFeedbackToUser(String feedbackToUser) {
requireNonNull(feedbackToUser);
resultDisplay.setText(feedbackToUser);
diff --git a/src/main/java/seedu/address/Main.java b/src/main/java/seedu/address/Main.java
index ec1b7958746..571add8943b 100644
--- a/src/main/java/seedu/address/Main.java
+++ b/src/main/java/seedu/address/Main.java
@@ -37,5 +37,6 @@ public static void main(String[] args) {
logger.warning("The warning about Unsupported JavaFX configuration below can be ignored.");
Application.launch(MainApp.class, args);
+
}
}
diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java
index 3d6bd06d5af..e35d811e0e8 100644
--- a/src/main/java/seedu/address/MainApp.java
+++ b/src/main/java/seedu/address/MainApp.java
@@ -16,14 +16,19 @@
import seedu.address.logic.Logic;
import seedu.address.logic.LogicManager;
import seedu.address.model.AddressBook;
+import seedu.address.model.ArticleBook;
import seedu.address.model.Model;
import seedu.address.model.ModelManager;
import seedu.address.model.ReadOnlyAddressBook;
+import seedu.address.model.ReadOnlyArticleBook;
import seedu.address.model.ReadOnlyUserPrefs;
import seedu.address.model.UserPrefs;
+import seedu.address.model.util.SampleArticleDataUtil;
import seedu.address.model.util.SampleDataUtil;
import seedu.address.storage.AddressBookStorage;
+import seedu.address.storage.ArticleBookStorage;
import seedu.address.storage.JsonAddressBookStorage;
+import seedu.address.storage.JsonArticleBookStorage;
import seedu.address.storage.JsonUserPrefsStorage;
import seedu.address.storage.Storage;
import seedu.address.storage.StorageManager;
@@ -36,7 +41,7 @@
*/
public class MainApp extends Application {
- public static final Version VERSION = new Version(0, 2, 2, true);
+ public static final Version VERSION = new Version(1, 3, 1, true);
private static final Logger logger = LogsCenter.getLogger(MainApp.class);
@@ -57,11 +62,13 @@ public void init() throws Exception {
UserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(config.getUserPrefsFilePath());
UserPrefs userPrefs = initPrefs(userPrefsStorage);
+ ArticleBookStorage articleBookStorage = new JsonArticleBookStorage(userPrefs.getArticleBookFilePath());
AddressBookStorage addressBookStorage = new JsonAddressBookStorage(userPrefs.getAddressBookFilePath());
- storage = new StorageManager(addressBookStorage, userPrefsStorage);
+ storage = new StorageManager(addressBookStorage, userPrefsStorage, articleBookStorage);
model = initModelManager(storage, userPrefs);
-
+ storage.saveAddressBook(model.getAddressBook());
+ storage.saveArticleBook(model.getArticleBook());
logic = new LogicManager(model, storage);
ui = new UiManager(logic);
@@ -77,6 +84,10 @@ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) {
Optional addressBookOptional;
ReadOnlyAddressBook initialData;
+
+ Optional articleBookOptional;
+ ReadOnlyArticleBook initialArticleData;
+
try {
addressBookOptional = storage.readAddressBook();
if (!addressBookOptional.isPresent()) {
@@ -90,7 +101,20 @@ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) {
initialData = new AddressBook();
}
- return new ModelManager(initialData, userPrefs);
+ try {
+ articleBookOptional = storage.readArticleBook();
+ if (!articleBookOptional.isPresent()) {
+ logger.info("Creating a new data file " + storage.getArticleBookFilePath()
+ + " populated with a sample ArticleBook.");
+ }
+ initialArticleData = articleBookOptional.orElseGet(SampleArticleDataUtil::getSampleArticleBook);
+ } catch (DataLoadingException e) {
+ logger.warning("Data file at " + storage.getArticleBookFilePath() + " could not be loaded."
+ + " Will be starting with an empty ArticleBook.");
+ initialArticleData = new ArticleBook();
+ }
+
+ return new ModelManager(initialData, initialArticleData, userPrefs);
}
private void initLogging(Config config) {
diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java
index 92cd8fa605a..a9bc67648fd 100644
--- a/src/main/java/seedu/address/logic/Logic.java
+++ b/src/main/java/seedu/address/logic/Logic.java
@@ -8,6 +8,7 @@
import seedu.address.logic.commands.exceptions.CommandException;
import seedu.address.logic.parser.exceptions.ParseException;
import seedu.address.model.ReadOnlyAddressBook;
+import seedu.address.model.article.Article;
import seedu.address.model.person.Person;
/**
@@ -23,6 +24,7 @@ public interface Logic {
*/
CommandResult execute(String commandText) throws CommandException, ParseException;
+ String getCommandType(String commandText) throws ParseException;
/**
* Returns the AddressBook.
*
@@ -33,6 +35,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 articles */
+ ObservableList getFilteredArticleList();
+
/**
* 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..940b8618c85 100644
--- a/src/main/java/seedu/address/logic/LogicManager.java
+++ b/src/main/java/seedu/address/logic/LogicManager.java
@@ -15,6 +15,7 @@
import seedu.address.logic.parser.exceptions.ParseException;
import seedu.address.model.Model;
import seedu.address.model.ReadOnlyAddressBook;
+import seedu.address.model.article.Article;
import seedu.address.model.person.Person;
import seedu.address.storage.Storage;
@@ -51,7 +52,12 @@ public CommandResult execute(String commandText) throws CommandException, ParseE
commandResult = command.execute(model);
try {
- storage.saveAddressBook(model.getAddressBook());
+ if (command.getCommandType().equals("personCommand")) {
+ storage.saveAddressBook(model.getAddressBook());
+ } else if (command.getCommandType().equals("articleCommand")) {
+ storage.saveArticleBook(model.getArticleBook());
+ }
+
} catch (AccessDeniedException e) {
throw new CommandException(String.format(FILE_OPS_PERMISSION_ERROR_FORMAT, e.getMessage()), e);
} catch (IOException ioe) {
@@ -61,6 +67,13 @@ public CommandResult execute(String commandText) throws CommandException, ParseE
return commandResult;
}
+ @Override
+ public String getCommandType(String commandText) throws ParseException {
+ logger.info("----------------[USER COMMAND][" + commandText + "]");
+ Command command = addressBookParser.parseCommand(commandText);
+ return command.getCommandType();
+ }
+
@Override
public ReadOnlyAddressBook getAddressBook() {
return model.getAddressBook();
@@ -71,6 +84,11 @@ public ObservableList getFilteredPersonList() {
return model.getFilteredPersonList();
}
+ @Override
+ public ObservableList getFilteredArticleList() {
+ return model.getFilteredArticleList();
+ }
+
@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..3554e46c476 100644
--- a/src/main/java/seedu/address/logic/Messages.java
+++ b/src/main/java/seedu/address/logic/Messages.java
@@ -5,6 +5,7 @@
import java.util.stream.Stream;
import seedu.address.logic.parser.Prefix;
+import seedu.address.model.article.Article;
import seedu.address.model.person.Person;
/**
@@ -16,8 +17,11 @@ public class Messages {
public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s";
public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid";
public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!";
+ public static final String MESSAGE_INVALID_ARTICLE_DISPLAYED_INDEX = "The article index provided is invalid";
+ public static final String MESSAGE_ARTICLES_LISTED_OVERVIEW = "%1$d articles listed!";
public static final String MESSAGE_DUPLICATE_FIELDS =
"Multiple values specified for the following single-valued field(s): ";
+ public static final String MESSAGE_INVALID_SORTING_PREFIX = "Invalid prefix given for sorting";
/**
* Returns an error message indicating the duplicate prefixes.
@@ -48,4 +52,27 @@ public static String format(Person person) {
return builder.toString();
}
+ /**
+ * Formats the {@code article} for display to the user.
+ */
+ public static String format(Article article) {
+ final StringBuilder builder = new StringBuilder();
+ builder.append(article.getTitle())
+ .append("; Contributors: ");
+ article.getAuthors().forEach(builder::append);
+ builder.append("; Interviewees: ");
+ article.getSources().forEach(builder::append);
+ builder.append("; Tags: ");
+ article.getTags().forEach(builder::append);
+ builder.append("; Outlets: ");
+ article.getOutlets().forEach(builder::append);
+ builder.append("; Date: ")
+ .append(article.getPublicationDateAsString())
+ .append("; Status: ")
+ .append(article.getStatus())
+ .append("; Link: ")
+ .append(article.getLink());
+ return builder.toString();
+ }
+
}
diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java
index 5d7185a9680..3634c988851 100644
--- a/src/main/java/seedu/address/logic/commands/AddCommand.java
+++ b/src/main/java/seedu/address/logic/commands/AddCommand.java
@@ -16,11 +16,11 @@
/**
* Adds a person to the address book.
*/
-public class AddCommand extends Command {
+public class AddCommand extends PersonCommand {
public static final String COMMAND_WORD = "add";
- public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. "
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book.\n"
+ "Parameters: "
+ PREFIX_NAME + "NAME "
+ PREFIX_PHONE + "PHONE "
diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java
index 9c86b1fa6e4..0131db1f65f 100644
--- a/src/main/java/seedu/address/logic/commands/ClearCommand.java
+++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java
@@ -8,7 +8,7 @@
/**
* Clears the address book.
*/
-public class ClearCommand extends Command {
+public class ClearCommand extends PersonCommand {
public static final String COMMAND_WORD = "clear";
public static final String MESSAGE_SUCCESS = "Address book has been cleared!";
diff --git a/src/main/java/seedu/address/logic/commands/Command.java b/src/main/java/seedu/address/logic/commands/Command.java
index 64f18992160..4ed802736d0 100644
--- a/src/main/java/seedu/address/logic/commands/Command.java
+++ b/src/main/java/seedu/address/logic/commands/Command.java
@@ -17,4 +17,10 @@ public abstract class Command {
*/
public abstract CommandResult execute(Model model) throws CommandException;
+ /**
+ * Returns the type of command.
+ * @return the type of command as a string.
+ */
+ public abstract String getCommandType();
+
}
diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java
index 1135ac19b74..f2f3bbd0493 100644
--- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java
+++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java
@@ -14,7 +14,7 @@
/**
* Deletes a person identified using it's displayed index from the address book.
*/
-public class DeleteCommand extends Command {
+public class DeleteCommand extends PersonCommand {
public static final String COMMAND_WORD = "delete";
diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java
index 4b581c7331e..f9424e07dd4 100644
--- a/src/main/java/seedu/address/logic/commands/EditCommand.java
+++ b/src/main/java/seedu/address/logic/commands/EditCommand.java
@@ -6,6 +6,7 @@
import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME;
import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE;
import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG;
+import static seedu.address.model.Model.PREDICATE_SHOW_ALL_ARTICLES;
import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS;
import java.util.Collections;
@@ -31,7 +32,7 @@
/**
* Edits the details of an existing person in the address book.
*/
-public class EditCommand extends Command {
+public class EditCommand extends PersonCommand {
public static final String COMMAND_WORD = "edit";
@@ -85,6 +86,7 @@ public CommandResult execute(Model model) throws CommandException {
model.setPerson(personToEdit, editedPerson);
model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS);
+ model.updateFilteredArticleList(PREDICATE_SHOW_ALL_ARTICLES);
return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson)));
}
@@ -101,7 +103,9 @@ private static Person createEditedPerson(Person personToEdit, EditPersonDescript
Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress());
Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags());
- return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags);
+ Person editedPerson = new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags);
+ editedPerson.setArticles(personToEdit);
+ return editedPerson;
}
@Override
diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java
index 3dd85a8ba90..f133c0c2bd7 100644
--- a/src/main/java/seedu/address/logic/commands/ExitCommand.java
+++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java
@@ -5,7 +5,7 @@
/**
* Terminates the program.
*/
-public class ExitCommand extends Command {
+public class ExitCommand extends PersonCommand {
public static final String COMMAND_WORD = "exit";
diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java
index 72b9eddd3a7..d9eb43ead84 100644
--- a/src/main/java/seedu/address/logic/commands/FindCommand.java
+++ b/src/main/java/seedu/address/logic/commands/FindCommand.java
@@ -9,9 +9,9 @@
/**
* Finds and lists all persons in address book whose name contains any of the argument keywords.
- * Keyword matching is case insensitive.
+ * Keyword matching is case-insensitive.
*/
-public class FindCommand extends Command {
+public class FindCommand extends PersonCommand {
public static final String COMMAND_WORD = "find";
diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java
index bf824f91bd0..d0a32b8d59f 100644
--- a/src/main/java/seedu/address/logic/commands/HelpCommand.java
+++ b/src/main/java/seedu/address/logic/commands/HelpCommand.java
@@ -5,7 +5,7 @@
/**
* Format full help instructions for every command for display.
*/
-public class HelpCommand extends Command {
+public class HelpCommand extends PersonCommand {
public static final String COMMAND_WORD = "help";
diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java
index 84be6ad2596..b81c56c7d02 100644
--- a/src/main/java/seedu/address/logic/commands/ListCommand.java
+++ b/src/main/java/seedu/address/logic/commands/ListCommand.java
@@ -8,7 +8,7 @@
/**
* Lists all persons in the address book to the user.
*/
-public class ListCommand extends Command {
+public class ListCommand extends PersonCommand {
public static final String COMMAND_WORD = "list";
diff --git a/src/main/java/seedu/address/logic/commands/LookupCommand.java b/src/main/java/seedu/address/logic/commands/LookupCommand.java
new file mode 100644
index 00000000000..9ef9bf51fea
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/LookupCommand.java
@@ -0,0 +1,69 @@
+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.person.Person;
+
+/**
+ * Lookup a person identified using it's displayed index from the address book.
+ */
+public class LookupCommand extends PersonCommand {
+
+ public static final String COMMAND_WORD = "lookup";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD
+ + ": Lookup 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_LOOKUP_PERSON_SUCCESS = "Lookup Person: %1$s";
+
+ private final Index targetIndex;
+
+ public LookupCommand(Index targetIndex) {
+ this.targetIndex = targetIndex;
+ }
+
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+ List lastShownList = model.getFilteredPersonList();
+
+ if (targetIndex.getZeroBased() >= lastShownList.size()) {
+ throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
+ }
+
+ Person personToLookup = lastShownList.get(targetIndex.getZeroBased());
+ model.lookupPerson(personToLookup);
+ return new CommandResult(String.format(MESSAGE_LOOKUP_PERSON_SUCCESS, Messages.format(personToLookup)));
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof LookupCommand)) {
+ return false;
+ }
+
+ LookupCommand otherLookupCommand = (LookupCommand) other;
+ return targetIndex.equals(otherLookupCommand.targetIndex);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("targetIndex", targetIndex)
+ .toString();
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/PersonCommand.java b/src/main/java/seedu/address/logic/commands/PersonCommand.java
new file mode 100644
index 00000000000..5eaac74bdac
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/PersonCommand.java
@@ -0,0 +1,13 @@
+package seedu.address.logic.commands;
+
+/**
+ * Represents a command with functionality pertaining to persons.
+ */
+public abstract class PersonCommand extends Command {
+ @Override
+ public String getCommandType() {
+ return "personCommand";
+ };
+
+}
+
diff --git a/src/main/java/seedu/address/logic/commands/SortCommand.java b/src/main/java/seedu/address/logic/commands/SortCommand.java
new file mode 100644
index 00000000000..bc633c7b8e6
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/SortCommand.java
@@ -0,0 +1,70 @@
+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.logic.commands.exceptions.CommandException;
+import seedu.address.logic.parser.SortCommandParser;
+import seedu.address.model.Model;
+
+/**
+ * Sorts all persons in the address book by an attribute of persons.
+ */
+public class SortCommand extends PersonCommand {
+
+ public static final String COMMAND_WORD = "sort";
+
+ public static final String MESSAGE_SUCCESS = "sorted all persons by name";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD
+ + ": sorts people according to an attribute and displays the sorted person list.\n"
+ + "Parameters: PREFIX (corresponds to each person's attribute e.g. n/ for Name)\n"
+ + "Example: " + COMMAND_WORD + " n/";
+
+ private final String prefix;
+
+ /**
+ * @param prefix referring to an attribute of persons to sort by
+ */
+ public SortCommand(String prefix) {
+ requireNonNull(prefix);
+ this.prefix = prefix;
+ }
+
+
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+
+ if (!SortCommandParser.isAllowedPrefix(prefix)) {
+ throw new CommandException(Messages.MESSAGE_INVALID_SORTING_PREFIX);
+ }
+
+ model.sortAddressBook(prefix);
+
+ return new CommandResult(MESSAGE_SUCCESS);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof SortCommand)) {
+ return false;
+ }
+
+ SortCommand otherSortCommand = (SortCommand) other;
+ return prefix.equals(otherSortCommand.prefix);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("prefix", prefix)
+ .toString();
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/articlecommands/AddArticleCommand.java b/src/main/java/seedu/address/logic/commands/articlecommands/AddArticleCommand.java
new file mode 100644
index 00000000000..bafbc1ce2b5
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/articlecommands/AddArticleCommand.java
@@ -0,0 +1,97 @@
+package seedu.address.logic.commands.articlecommands;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_CONTRIBUTOR;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_HEADLINE;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_INTERVIEWEE;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_LINK;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_OUTLET;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_STATUS;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG;
+
+import seedu.address.commons.util.ToStringBuilder;
+import seedu.address.logic.Messages;
+import seedu.address.logic.commands.CommandResult;
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.model.Model;
+import seedu.address.model.article.Article;
+
+/**
+ * Adds an article to the article book.
+ */
+public class AddArticleCommand extends ArticleCommand {
+
+ public static final String COMMAND_WORD = "add";
+
+ public static final String COMMAND_PREFIX = "-a";
+
+ // To be edited for use in test cases later on.
+ public static final String MESSAGE_USAGE = COMMAND_WORD + " " + COMMAND_PREFIX
+ + ": Adds an article to the article book.\n"
+ + "Parameters: "
+ + PREFIX_HEADLINE + "HEADLINE "
+ + "[" + PREFIX_CONTRIBUTOR + "CONTRIBUTOR]... "
+ + "[" + PREFIX_INTERVIEWEE + "INTERVIEWEE]... "
+ + "[" + PREFIX_TAG + "TAG]... "
+ + "[" + PREFIX_OUTLET + "OUTLET]... "
+ + PREFIX_DATE + "DATE "
+ + PREFIX_STATUS + "STATUS "
+ + "[" + PREFIX_LINK + "LINK]\n"
+ + "Example: " + COMMAND_WORD + " " + COMMAND_PREFIX + " "
+ + PREFIX_HEADLINE + "The Great Article "
+ + PREFIX_CONTRIBUTOR + "John Doe "
+ + PREFIX_INTERVIEWEE + "Jane Doe "
+ + PREFIX_TAG + "friends "
+ + PREFIX_OUTLET + "The Great Outlet "
+ + PREFIX_DATE + "10-10-2024 "
+ + PREFIX_STATUS + "DRAFT "
+ + PREFIX_LINK + "https://www.example.com";
+
+ public static final String MESSAGE_SUCCESS = "New article added: %1$s";
+ public static final String MESSAGE_DUPLICATE_ARTICLE = "This article already exists in the article book";
+
+ private final Article toAdd;
+
+ /**
+ * Creates an AddCommand to add the specified {@code Article}
+ */
+ public AddArticleCommand(Article article) {
+ requireNonNull(article);
+ toAdd = article;
+ }
+
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+
+ if (model.hasArticle(toAdd)) {
+ throw new CommandException(MESSAGE_DUPLICATE_ARTICLE);
+ }
+
+ model.addArticle(toAdd);
+ return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toAdd)));
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof AddArticleCommand)) {
+ return false;
+ }
+
+ AddArticleCommand otherAddArticleCommand = (AddArticleCommand) other;
+ return toAdd.equals(otherAddArticleCommand.toAdd);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("toAdd", toAdd)
+ .toString();
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/articlecommands/ArticleCommand.java b/src/main/java/seedu/address/logic/commands/articlecommands/ArticleCommand.java
new file mode 100644
index 00000000000..5474b82cc82
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/articlecommands/ArticleCommand.java
@@ -0,0 +1,16 @@
+package seedu.address.logic.commands.articlecommands;
+
+import seedu.address.logic.commands.Command;
+
+/**
+ * Represents a command with functionality pertaining to articles.
+ */
+public abstract class ArticleCommand extends Command {
+
+ @Override
+ public String getCommandType() {
+ return "articleCommand";
+ };
+
+}
+
diff --git a/src/main/java/seedu/address/logic/commands/articlecommands/DeleteArticleCommand.java b/src/main/java/seedu/address/logic/commands/articlecommands/DeleteArticleCommand.java
new file mode 100644
index 00000000000..2ddb5cbe618
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/articlecommands/DeleteArticleCommand.java
@@ -0,0 +1,73 @@
+package seedu.address.logic.commands.articlecommands;
+
+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.CommandResult;
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.model.Model;
+import seedu.address.model.article.Article;
+
+/**
+ * Deletes an article identified using it's displayed index from the article book.
+ */
+public class DeleteArticleCommand extends ArticleCommand {
+
+ public static final String COMMAND_WORD = "delete";
+
+ public static final String COMMAND_PREFIX = "-a";
+
+ // To be edited for use in test cases later on.
+ public static final String MESSAGE_USAGE = COMMAND_WORD + " " + COMMAND_PREFIX
+ + ": Deletes the article identified by the index number used in the displayed article list.\n"
+ + "Parameters: INDEX (must be a positive integer)\n"
+ + "Example: " + COMMAND_WORD + " " + COMMAND_PREFIX + " 1";
+
+ public static final String MESSAGE_DELETE_ARTICLE_SUCCESS = "Deleted Article: %1$s";
+
+ private final Index targetIndex;
+
+ public DeleteArticleCommand(Index targetIndex) {
+ this.targetIndex = targetIndex;
+ }
+
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+ List lastShownList = model.getFilteredArticleList();
+
+ if (targetIndex.getZeroBased() >= lastShownList.size()) {
+ throw new CommandException(Messages.MESSAGE_INVALID_ARTICLE_DISPLAYED_INDEX);
+ }
+
+ Article articleToDelete = lastShownList.get(targetIndex.getZeroBased());
+ model.deleteArticle(articleToDelete);
+ return new CommandResult(String.format(MESSAGE_DELETE_ARTICLE_SUCCESS, Messages.format(articleToDelete)));
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof DeleteArticleCommand)) {
+ return false;
+ }
+
+ DeleteArticleCommand otherDeleteArticleCommand = (DeleteArticleCommand) other;
+ return targetIndex.equals(otherDeleteArticleCommand.targetIndex);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("targetIndex", targetIndex)
+ .toString();
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/articlecommands/EditArticleCommand.java b/src/main/java/seedu/address/logic/commands/articlecommands/EditArticleCommand.java
new file mode 100644
index 00000000000..9e22f90e47e
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/articlecommands/EditArticleCommand.java
@@ -0,0 +1,296 @@
+package seedu.address.logic.commands.articlecommands;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_CONTRIBUTOR;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_HEADLINE;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_INTERVIEWEE;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_LINK;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_OUTLET;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_STATUS;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG;
+import static seedu.address.model.Model.PREDICATE_SHOW_ALL_ARTICLES;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.commons.util.CollectionUtil;
+import seedu.address.commons.util.ToStringBuilder;
+import seedu.address.logic.Messages;
+import seedu.address.logic.commands.CommandResult;
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.model.Model;
+import seedu.address.model.article.Article;
+import seedu.address.model.article.Article.Status;
+import seedu.address.model.article.Author;
+import seedu.address.model.article.Link;
+import seedu.address.model.article.Outlet;
+import seedu.address.model.article.PublicationDate;
+import seedu.address.model.article.Source;
+import seedu.address.model.article.Title;
+import seedu.address.model.tag.Tag;
+
+/**
+ * Edits the details of an existing article in the article book.
+ */
+public class EditArticleCommand extends ArticleCommand {
+
+ public static final String COMMAND_WORD = "edit";
+
+ public static final String COMMAND_PREFIX = "-a";
+
+ // To be edited for use in test cases later on.
+ public static final String MESSAGE_USAGE = COMMAND_WORD + " " + COMMAND_PREFIX
+ + ": Edits the details of the article identified "
+ + "by the index number used in the displayed article list. "
+ + "Existing values will be overwritten by the input values.\n"
+ + "Parameters: INDEX (must be a positive integer) "
+ + "[" + PREFIX_HEADLINE + "HEADLINE] "
+ + "[" + PREFIX_CONTRIBUTOR + "CONTRIBUTOR]... "
+ + "[" + PREFIX_INTERVIEWEE + "INTERVIEWEE]... "
+ + "[" + PREFIX_TAG + "TAG]... "
+ + "[" + PREFIX_OUTLET + "OUTLET]... "
+ + "[" + PREFIX_DATE + "DATE] "
+ + "[" + PREFIX_STATUS + "STATUS] "
+ + "[" + PREFIX_LINK + "LINK]\n"
+ + "Example: " + COMMAND_WORD + " " + COMMAND_PREFIX + " 1 "
+ + PREFIX_HEADLINE + "Headline "
+ + PREFIX_CONTRIBUTOR + "Contributor(s) "
+ + PREFIX_INTERVIEWEE + "Interviewee(s) "
+ + PREFIX_TAG + "New Tag(s) "
+ + PREFIX_OUTLET + "New Outlet(s) "
+ + PREFIX_DATE + "10-10-2024 "
+ + PREFIX_STATUS + "PUBLISHED "
+ + PREFIX_LINK + "https://www.example.com";
+
+ public static final String MESSAGE_EDIT_ARTICLE_SUCCESS = "Edited Article: %1$s";
+ public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided.";
+ public static final String MESSAGE_DUPLICATE_ARTICLE = "This article already exists in the article book.";
+
+ private final Index index;
+ private final EditArticleDescriptor editArticleDescriptor;
+
+ /**
+ * @param index of the article in the filtered article list to edit
+ * @param editArticleDescriptor details to edit the article with
+ */
+ public EditArticleCommand(Index index, EditArticleDescriptor editArticleDescriptor) {
+ requireNonNull(index);
+ requireNonNull(editArticleDescriptor);
+
+ this.index = index;
+ this.editArticleDescriptor = new EditArticleDescriptor(editArticleDescriptor);
+ }
+
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+ List lastShownList = model.getFilteredArticleList();
+
+ if (index.getZeroBased() >= lastShownList.size()) {
+ throw new CommandException(Messages.MESSAGE_INVALID_ARTICLE_DISPLAYED_INDEX);
+ }
+
+ Article articleToEdit = lastShownList.get(index.getZeroBased());
+ Article editedArticle = createEditedArticle(articleToEdit, editArticleDescriptor);
+
+ if (!articleToEdit.isSameArticle(editedArticle) && model.hasArticle(editedArticle)) {
+ throw new CommandException(MESSAGE_DUPLICATE_ARTICLE);
+ }
+
+ model.setArticle(articleToEdit, editedArticle);
+ model.updateFilteredArticleList(PREDICATE_SHOW_ALL_ARTICLES);
+ return new CommandResult(String.format(MESSAGE_EDIT_ARTICLE_SUCCESS, Messages.format(editedArticle)));
+ }
+
+ /**
+ * Creates and returns a {@code Article} with the details of {@code articleToEdit}
+ * edited with {@code editArticleDescriptor}.
+ */
+ private static Article createEditedArticle(Article articleToEdit, EditArticleDescriptor editArticleDescriptor) {
+ assert articleToEdit != null;
+
+ Title title = editArticleDescriptor.getTitle().orElse(articleToEdit.getTitle());
+ Set authors = editArticleDescriptor.getAuthors().orElse(articleToEdit.getAuthors());
+ Set sources = editArticleDescriptor.getSources().orElse(articleToEdit.getSources());
+ Set tags = editArticleDescriptor.getTags().orElse(articleToEdit.getTags());
+ Set outlets = editArticleDescriptor.getOutlets().orElse(articleToEdit.getOutlets());
+ PublicationDate publicationDate = editArticleDescriptor.getPublicationDate()
+ .orElse(articleToEdit.getPublicationDate());
+ Status status = editArticleDescriptor.getStatus().orElse(articleToEdit.getStatus());
+ Link link = editArticleDescriptor.getLink().orElse(articleToEdit.getLink());
+
+ Article editedArticle = new Article(title, authors, sources, tags,
+ outlets, publicationDate, status, link); // Include all article attributes here.
+ return editedArticle;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof EditArticleCommand)) {
+ return false;
+ }
+
+ EditArticleCommand otherEditArticleCommand = (EditArticleCommand) other;
+ return index.equals(otherEditArticleCommand.index)
+ && editArticleDescriptor.equals(otherEditArticleCommand.editArticleDescriptor);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("index", index)
+ .add("editArticleDescriptor", editArticleDescriptor)
+ .toString();
+ }
+
+ /**
+ * Stores the details to edit the article with. Each non-empty field value will replace the
+ * corresponding field value of the article.
+ */
+ public static class EditArticleDescriptor {
+
+ private Title title;
+ private Set authors;
+ private Set sources;
+ private Set tags;
+ private Set outlets;
+ private PublicationDate publicationDate;
+ private Status status;
+ private Link link;
+
+ public EditArticleDescriptor() {}
+
+ /**
+ * Copy constructor.
+ */
+ public EditArticleDescriptor(EditArticleDescriptor toCopy) {
+ setTitle(toCopy.title);
+ setAuthors(toCopy.authors);
+ setSources(toCopy.sources);
+ setTags(toCopy.tags);
+ setOutlets(toCopy.outlets);
+ setPublicationDate(toCopy.publicationDate);
+ setStatus(toCopy.status);
+ setLink(toCopy.link);
+ }
+
+ /**
+ * Returns true if at least one field is edited.
+ */
+ public boolean isAnyFieldEdited() {
+ return CollectionUtil.isAnyNonNull(title, authors, sources, outlets, publicationDate, tags, status, link);
+ }
+
+ public void setTitle(Title title) {
+ this.title = title;
+ }
+
+ public Optional getTitle() {
+ return Optional.ofNullable(title);
+ }
+
+ public void setAuthors(Set authors) {
+ this.authors = (authors != null) ? new HashSet<>(authors) : null;
+ }
+
+ public Optional> getAuthors() {
+ return (authors != null) ? Optional.of(Collections.unmodifiableSet(authors)) : Optional.empty();
+ }
+
+ public void setPublicationDate(PublicationDate publicationDate) {
+ this.publicationDate = publicationDate;
+ }
+
+ public Optional getPublicationDate() {
+ return Optional.ofNullable(publicationDate);
+ }
+
+ public void setSources(Set sources) {
+ this.sources = (sources != null) ? new HashSet<>(sources) : null;
+ }
+
+ public Optional> getSources() {
+ return (sources != null) ? Optional.of(Collections.unmodifiableSet(sources)) : Optional.empty();
+ }
+
+ public void setTags(Set tags) {
+ this.tags = (tags != null) ? new HashSet<>(tags) : null;
+ }
+
+ public Optional> getTags() {
+ return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty();
+ }
+
+ public void setOutlets(Set outlets) {
+ this.outlets = (outlets != null) ? new HashSet<>(outlets) : null;
+ }
+
+ public Optional> getOutlets() {
+ return (outlets != null) ? Optional.of(Collections.unmodifiableSet(outlets)) : Optional.empty();
+ }
+ public void setStatus(Status status) {
+ this.status = status;
+ }
+
+ public Optional getStatus() {
+ return Optional.ofNullable(status);
+ }
+
+ public void setLink(Link link) {
+ this.link = link;
+ }
+
+ public Optional getLink() {
+ return Optional.ofNullable(link);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof EditArticleDescriptor)) {
+ return false;
+ }
+
+ EditArticleDescriptor otherEditArticleDescriptor = (EditArticleDescriptor) other;
+
+ // Add more equality checks for article attributes below here.
+ return Objects.equals(title, otherEditArticleDescriptor.title)
+ && Objects.equals(authors, otherEditArticleDescriptor.authors)
+ && Objects.equals(sources, otherEditArticleDescriptor.sources)
+ && Objects.equals(outlets, otherEditArticleDescriptor.outlets)
+ && Objects.equals(publicationDate, otherEditArticleDescriptor.publicationDate)
+ && Objects.equals(tags, otherEditArticleDescriptor.tags)
+ && Objects.equals(status, otherEditArticleDescriptor.status)
+ && Objects.equals(link, otherEditArticleDescriptor.link);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("title", title)
+ .add("authors", authors)
+ .add("sources", sources)
+ .add("tags", tags)
+ .add("outlets", outlets)
+ .add("publicationDate", publicationDate)
+ .add("status", status)
+ .add("link", link)
+ .toString();
+ }
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/articlecommands/FilterArticleCommand.java b/src/main/java/seedu/address/logic/commands/articlecommands/FilterArticleCommand.java
new file mode 100644
index 00000000000..65b77323e8f
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/articlecommands/FilterArticleCommand.java
@@ -0,0 +1,62 @@
+package seedu.address.logic.commands.articlecommands;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.model.Model.PREDICATE_SHOW_ALL_ARTICLES;
+
+import java.util.function.Predicate;
+
+import seedu.address.logic.commands.CommandResult;
+import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.Model;
+import seedu.address.model.article.Article;
+import seedu.address.model.article.ArticleMatchesStatusPredicate;
+import seedu.address.model.article.ArticleMatchesTagPredicate;
+import seedu.address.model.article.ArticleMatchesTimePeriodPredicate;
+import seedu.address.model.article.PublicationDate;
+import seedu.address.model.article.exceptions.InvalidDatesException;
+import seedu.address.model.article.exceptions.InvalidStatusException;
+import seedu.address.model.tag.Tag;
+
+/**
+ * Use to set filter for Articles
+ */
+public class FilterArticleCommand extends ArticleCommand {
+ public static final String COMMAND_WORD = "filter";
+
+ public static final String COMMAND_PREFIX = "-a";
+
+ public static final String MESSAGE_SUCCESS = "Filter online\nUse " + RemoveArticleFilterCommand.COMMAND_WORD
+ + " -a to display all articles again";
+ private Predicate finalPredicate;
+
+ /**
+ * Constructs a FilterArticleCommand object.
+ * @param status The status to be filtered by.
+ */
+ public FilterArticleCommand(String status, Tag tag, PublicationDate start,
+ PublicationDate end) throws ParseException {
+ try {
+ finalPredicate = new ArticleMatchesStatusPredicate(status);
+ } catch (InvalidStatusException e) {
+ finalPredicate = PREDICATE_SHOW_ALL_ARTICLES;
+ }
+ try {
+ Predicate timePredicate = new ArticleMatchesTimePeriodPredicate(start, end);
+ finalPredicate = finalPredicate.and(timePredicate);
+ } catch (InvalidDatesException e) {
+ throw new ParseException(e.getMessage());
+ }
+ if (tag instanceof Tag) {
+ Predicate tagPredicate = new ArticleMatchesTagPredicate(tag);
+ finalPredicate = finalPredicate.and(tagPredicate);
+ }
+ }
+
+ @Override
+ public CommandResult execute(Model model) {
+ requireNonNull(model);
+ model.getFilter().updateFilter(finalPredicate);
+ model.updateFilteredArticleList(finalPredicate);
+ return new CommandResult(MESSAGE_SUCCESS);
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/articlecommands/FindArticleCommand.java b/src/main/java/seedu/address/logic/commands/articlecommands/FindArticleCommand.java
new file mode 100644
index 00000000000..b7b3b8c6332
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/articlecommands/FindArticleCommand.java
@@ -0,0 +1,66 @@
+package seedu.address.logic.commands.articlecommands;
+
+import static java.util.Objects.requireNonNull;
+
+import seedu.address.commons.util.ToStringBuilder;
+import seedu.address.logic.Messages;
+import seedu.address.logic.commands.CommandResult;
+import seedu.address.model.Model;
+import seedu.address.model.article.TitleContainsKeywordsPredicate;
+
+/**
+ * Finds and lists all articles in article book whose title contains any of the argument keywords.
+ * Keyword matching is case-insensitive.
+ */
+public class FindArticleCommand extends ArticleCommand {
+
+ public static final String COMMAND_WORD = "find";
+
+ public static final String COMMAND_PREFIX = "-a";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD + " " + COMMAND_PREFIX
+ + ": Finds all articles whose titles 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 + " " + COMMAND_PREFIX + " HDB UDP TCP";
+
+ private final TitleContainsKeywordsPredicate predicate;
+
+ /**
+ * @param predicate predicate to filter the list of articles
+ * with titles containing the keyword(s)
+ */
+ public FindArticleCommand(TitleContainsKeywordsPredicate predicate) {
+ this.predicate = predicate;
+ }
+
+ @Override
+ public CommandResult execute(Model model) {
+ requireNonNull(model);
+ model.updateFilteredArticleList(predicate);
+ return new CommandResult(
+ String.format(Messages.MESSAGE_ARTICLES_LISTED_OVERVIEW, model.getFilteredArticleList().size()));
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof FindArticleCommand)) {
+ return false;
+ }
+
+ FindArticleCommand otherFindArticleCommand = (FindArticleCommand) other;
+ return predicate.equals(otherFindArticleCommand.predicate);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("predicate", predicate)
+ .toString();
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/articlecommands/ListArticleCommand.java b/src/main/java/seedu/address/logic/commands/articlecommands/ListArticleCommand.java
new file mode 100644
index 00000000000..d1874048a93
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/articlecommands/ListArticleCommand.java
@@ -0,0 +1,26 @@
+package seedu.address.logic.commands.articlecommands;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.model.Model.PREDICATE_SHOW_ALL_ARTICLES;
+
+import seedu.address.logic.commands.CommandResult;
+import seedu.address.model.Model;
+
+/**
+ * Lists all articles in the article book to the user.
+ */
+public class ListArticleCommand extends ArticleCommand {
+
+ public static final String COMMAND_WORD = "list";
+
+ public static final String COMMAND_PREFIX = "-a";
+
+ public static final String MESSAGE_SUCCESS = "Listed all articles";
+
+ @Override
+ public CommandResult execute(Model model) {
+ requireNonNull(model);
+ model.updateFilteredArticleList(PREDICATE_SHOW_ALL_ARTICLES);
+ return new CommandResult(MESSAGE_SUCCESS);
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/articlecommands/LookupArticleCommand.java b/src/main/java/seedu/address/logic/commands/articlecommands/LookupArticleCommand.java
new file mode 100644
index 00000000000..51c53a40cef
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/articlecommands/LookupArticleCommand.java
@@ -0,0 +1,77 @@
+package seedu.address.logic.commands.articlecommands;
+
+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.CommandResult;
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.model.Model;
+import seedu.address.model.article.Article;
+
+/**
+ * Lookups an article identified using it's displayed index from the article book. //change?
+ */
+public class LookupArticleCommand extends ArticleCommand {
+
+ public static final String COMMAND_WORD = "lookup";
+
+ public static final String COMMAND_PREFIX = "-a";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD + " " + COMMAND_PREFIX
+ + ": Lookups the article identified by the index number used in the displayed article list.\n"
+ + "Parameters: INDEX (must be a positive integer)\n"
+ + "Example: " + COMMAND_WORD + " " + COMMAND_PREFIX + " 1";
+
+ public static final String MESSAGE_LOOKUP_ARTICLE_SUCCESS = "Lookup Article: %1$s"; //change?v
+
+ private final Index targetIndex;
+
+ /**
+ * Creates a LookupArticleCommand to lookup the article at the specified {@code Index}.
+ *
+ * @param targetIndex The index of the article to lookup.
+ */
+ public LookupArticleCommand(Index targetIndex) {
+ this.targetIndex = targetIndex;
+ }
+
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+ List lastShownList = model.getFilteredArticleList();
+
+ if (targetIndex.getZeroBased() >= lastShownList.size()) {
+ throw new CommandException(Messages.MESSAGE_INVALID_ARTICLE_DISPLAYED_INDEX);
+ }
+
+ Article articleToLookup = lastShownList.get(targetIndex.getZeroBased());
+ model.lookupArticle(articleToLookup);
+ return new CommandResult(String.format(MESSAGE_LOOKUP_ARTICLE_SUCCESS, Messages.format(articleToLookup)));
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof LookupArticleCommand)) {
+ return false;
+ }
+
+ LookupArticleCommand otherLookupArticleCommand = (LookupArticleCommand) other;
+ return targetIndex.equals(otherLookupArticleCommand.targetIndex);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("targetIndex", targetIndex)
+ .toString();
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/articlecommands/RemoveArticleFilterCommand.java b/src/main/java/seedu/address/logic/commands/articlecommands/RemoveArticleFilterCommand.java
new file mode 100644
index 00000000000..4278bee2036
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/articlecommands/RemoveArticleFilterCommand.java
@@ -0,0 +1,24 @@
+package seedu.address.logic.commands.articlecommands;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.model.Model.PREDICATE_SHOW_ALL_ARTICLES;
+
+import seedu.address.logic.commands.CommandResult;
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.model.Model;
+
+/**
+ * Removes filter so all articles are displayed
+ */
+public class RemoveArticleFilterCommand extends ArticleCommand {
+ public static final String COMMAND_WORD = "rmfilter";
+ public static final String COMMAND_PREFIX = "-a";
+ private static final String MESSAGE_SUCCESS = "Filters have been removed. All articles will be displayed again.";
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+ model.getFilter().updateFilter(PREDICATE_SHOW_ALL_ARTICLES);
+ model.updateFilteredArticleList(PREDICATE_SHOW_ALL_ARTICLES);
+ return new CommandResult(MESSAGE_SUCCESS);
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/articlecommands/SortArticleCommand.java b/src/main/java/seedu/address/logic/commands/articlecommands/SortArticleCommand.java
new file mode 100644
index 00000000000..b5d1b45569a
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/articlecommands/SortArticleCommand.java
@@ -0,0 +1,49 @@
+package seedu.address.logic.commands.articlecommands;
+
+import static java.util.Objects.requireNonNull;
+
+import seedu.address.logic.Messages;
+import seedu.address.logic.commands.CommandResult;
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.logic.parser.SortArticleCommandParser;
+import seedu.address.model.Model;
+
+/**
+ * Sorts all articles in the article book by an attribute of articles.
+ */
+public class SortArticleCommand extends ArticleCommand {
+
+ public static final String COMMAND_WORD = "sort";
+
+ public static final String COMMAND_PREFIX = "-a";
+
+ public static final String MESSAGE_SUCCESS = "sorted all articles by date";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD + " " + COMMAND_PREFIX
+ + ": sorts articles according to date and displays the sorted article list.\n"
+ + "Parameters: D/ (corresponds to prefix for article's date attribute)\n"
+ + "Example: " + COMMAND_WORD + " " + COMMAND_PREFIX + " D/";
+
+ private final String prefix;
+
+ /**
+ * @param prefix referring to an attribute of articles to sort by
+ */
+ public SortArticleCommand(String prefix) {
+ requireNonNull(prefix);
+ this.prefix = prefix;
+ }
+
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+
+ if (!SortArticleCommandParser.isAllowedPrefix(prefix)) {
+ throw new CommandException(Messages.MESSAGE_INVALID_SORTING_PREFIX);
+ }
+
+ model.sortArticleBook(prefix);
+
+ return new CommandResult(MESSAGE_SUCCESS);
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/AddArticleCommandParser.java b/src/main/java/seedu/address/logic/parser/AddArticleCommandParser.java
new file mode 100644
index 00000000000..5c5a88f1934
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/AddArticleCommandParser.java
@@ -0,0 +1,74 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_CONTRIBUTOR;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_HEADLINE;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_INTERVIEWEE;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_LINK;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_OUTLET;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_STATUS;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG;
+
+import java.util.Set;
+import java.util.stream.Stream;
+
+import seedu.address.logic.commands.articlecommands.AddArticleCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.article.Article;
+import seedu.address.model.article.Article.Status;
+import seedu.address.model.article.Author;
+import seedu.address.model.article.Link;
+import seedu.address.model.article.Outlet;
+import seedu.address.model.article.PublicationDate;
+import seedu.address.model.article.Source;
+import seedu.address.model.article.Title;
+import seedu.address.model.tag.Tag;
+
+/**
+ * Parses input arguments and creates a new AddArticleCommand object
+ */
+public class AddArticleCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the AddArticleCommand
+ * and returns an AddArticleCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public AddArticleCommand parse(String args) throws ParseException {
+ ArgumentMultimap argMultimap =
+ ArgumentTokenizer.tokenize(args, PREFIX_HEADLINE, PREFIX_CONTRIBUTOR, PREFIX_INTERVIEWEE, PREFIX_TAG,
+ PREFIX_OUTLET, PREFIX_DATE, PREFIX_STATUS, PREFIX_LINK);
+ if (!arePrefixesPresent(argMultimap, PREFIX_HEADLINE, PREFIX_DATE, PREFIX_STATUS)
+ || !argMultimap.getPreamble().isEmpty()) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddArticleCommand.MESSAGE_USAGE));
+ }
+ Title title = ParserUtil.parseTitle(argMultimap.getValue(PREFIX_HEADLINE).get());
+ Set authorList = ParserUtil.parseAuthors(argMultimap.getAllValues(PREFIX_CONTRIBUTOR));
+ Set sourceList = ParserUtil.parseSources(argMultimap.getAllValues(PREFIX_INTERVIEWEE));
+ Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG));
+ Set outletList = ParserUtil.parseOutlets(argMultimap.getAllValues(PREFIX_OUTLET));
+ PublicationDate publicationDate = ParserUtil.parsePublicationDate(argMultimap.getValue(PREFIX_DATE)
+ .get());
+
+ Status status = ParserUtil.parseStatus(argMultimap.getValue(PREFIX_STATUS).get());
+ Link link;
+ if (argMultimap.getValue(PREFIX_LINK).isEmpty()) {
+ link = new Link("");
+ } else {
+ link = ParserUtil.parseLink(argMultimap.getValue(PREFIX_LINK).get());
+ }
+ Article article = new Article(title, authorList, sourceList, tagList,
+ outletList, publicationDate, status, link);
+
+ return new AddArticleCommand(article);
+ }
+
+ /**
+ * 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..62d54da5ae5 100644
--- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java
+++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java
@@ -17,6 +17,8 @@
import seedu.address.logic.commands.FindCommand;
import seedu.address.logic.commands.HelpCommand;
import seedu.address.logic.commands.ListCommand;
+import seedu.address.logic.commands.LookupCommand;
+import seedu.address.logic.commands.SortCommand;
import seedu.address.logic.parser.exceptions.ParseException;
/**
@@ -46,6 +48,10 @@ public Command parseCommand(String userInput) throws ParseException {
final String commandWord = matcher.group("commandWord");
final String arguments = matcher.group("arguments");
+ if (arguments.trim().endsWith("-a") || arguments.trim().startsWith("-a ")) {
+ return ArticleBookParser.parseCommand(commandWord + arguments.trim().substring(2));
+ }
+
// Note to developers: Change the log level in config.json to enable lower level (i.e., FINE, FINER and lower)
// log messages such as the one below.
// Lower level log messages are used sparingly to minimize noise in the code.
@@ -71,12 +77,18 @@ public Command parseCommand(String userInput) throws ParseException {
case ListCommand.COMMAND_WORD:
return new ListCommand();
+ case SortCommand.COMMAND_WORD:
+ return new SortCommandParser().parse(arguments);
+
case ExitCommand.COMMAND_WORD:
return new ExitCommand();
case HelpCommand.COMMAND_WORD:
return new HelpCommand();
+ case LookupCommand.COMMAND_WORD:
+ return new LookupCommandParser().parse(arguments);
+
default:
logger.finer("This user input caused a ParseException: " + userInput);
throw new ParseException(MESSAGE_UNKNOWN_COMMAND);
diff --git a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java
index 5c9aebfa488..ff08b83e308 100644
--- a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java
+++ b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java
@@ -63,6 +63,9 @@ private static List findPrefixPositions(String argsString, Prefi
* is valid if there is a whitespace before {@code prefix}. Returns -1 if no
* such occurrence can be found.
*
+ * The prefix can be in upper case, as this function will make a local all-lowercase copy
+ * of the argsString to search.
+ *
* E.g if {@code argsString} = "e/hip/900", {@code prefix} = "p/" and
* {@code fromIndex} = 0, this method returns -1 as there are no valid
* occurrences of "p/" with whitespace before it. However, if
@@ -70,7 +73,8 @@ private static List findPrefixPositions(String argsString, Prefi
* {@code fromIndex} = 0, this method returns 5.
*/
private static int findPrefixPosition(String argsString, String prefix, int fromIndex) {
- int prefixIndex = argsString.indexOf(" " + prefix, fromIndex);
+ String argsStringLower = argsString.toLowerCase();
+ int prefixIndex = argsStringLower.indexOf(" " + prefix.toLowerCase(), fromIndex);
return prefixIndex == -1 ? -1
: prefixIndex + 1; // +1 as offset for whitespace
}
diff --git a/src/main/java/seedu/address/logic/parser/ArticleBookParser.java b/src/main/java/seedu/address/logic/parser/ArticleBookParser.java
new file mode 100644
index 00000000000..9ba62498ee1
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/ArticleBookParser.java
@@ -0,0 +1,91 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static seedu.address.logic.Messages.MESSAGE_UNKNOWN_COMMAND;
+
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import seedu.address.commons.core.LogsCenter;
+import seedu.address.logic.commands.Command;
+import seedu.address.logic.commands.HelpCommand;
+import seedu.address.logic.commands.articlecommands.AddArticleCommand;
+import seedu.address.logic.commands.articlecommands.DeleteArticleCommand;
+import seedu.address.logic.commands.articlecommands.EditArticleCommand;
+import seedu.address.logic.commands.articlecommands.FilterArticleCommand;
+import seedu.address.logic.commands.articlecommands.FindArticleCommand;
+import seedu.address.logic.commands.articlecommands.ListArticleCommand;
+import seedu.address.logic.commands.articlecommands.LookupArticleCommand;
+import seedu.address.logic.commands.articlecommands.RemoveArticleFilterCommand;
+import seedu.address.logic.commands.articlecommands.SortArticleCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses user input for Articles.
+ */
+public class ArticleBookParser {
+
+ /**
+ * Used for initial separation of command word and args.
+ */
+ private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)");
+ private static final Logger logger = LogsCenter.getLogger(AddressBookParser.class);
+
+ /**
+ * Parses user input into command for execution.
+ *
+ * @param userInput full user input string
+ * @return the command based on the user input
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ @SuppressWarnings("checkstyle:Regexp")
+ public static Command parseCommand(String userInput) throws ParseException {
+ final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim());
+ if (!matcher.matches()) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE));
+ }
+
+ final String commandWord = matcher.group("commandWord");
+ final String arguments = matcher.group("arguments");
+
+ // Note to developers: Change the log level in config.json to enable lower level (i.e., FINE, FINER and lower)
+ // log messages such as the one below.
+ // Lower level log messages are used sparingly to minimize noise in the code.
+ logger.fine("Command word: " + commandWord + "; Arguments: " + arguments);
+
+ switch (commandWord) {
+
+ case AddArticleCommand.COMMAND_WORD:
+ return new AddArticleCommandParser().parse(arguments);
+
+ case EditArticleCommand.COMMAND_WORD:
+ return new EditArticleCommandParser().parse(arguments);
+
+ case DeleteArticleCommand.COMMAND_WORD:
+ return new DeleteArticleCommandParser().parse(arguments);
+
+ case FindArticleCommand.COMMAND_WORD:
+ return new FindArticleCommandParser().parse(arguments);
+
+ case ListArticleCommand.COMMAND_WORD:
+ return new ListArticleCommand();
+
+ case SortArticleCommand.COMMAND_WORD:
+ return new SortArticleCommandParser().parse(arguments);
+
+ case FilterArticleCommand.COMMAND_WORD:
+ return new FilterArticleCommandParser().parse(arguments);
+
+ case RemoveArticleFilterCommand.COMMAND_WORD:
+ return new RemoveArticleFilterCommand();
+
+ case LookupArticleCommand.COMMAND_WORD:
+ return new LookupArticleCommandParser().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..9b3d0bc1095 100644
--- a/src/main/java/seedu/address/logic/parser/CliSyntax.java
+++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java
@@ -12,4 +12,14 @@ public class CliSyntax {
public static final Prefix PREFIX_ADDRESS = new Prefix("a/");
public static final Prefix PREFIX_TAG = new Prefix("t/");
+ public static final Prefix PREFIX_HEADLINE = new Prefix("h/");
+ public static final Prefix PREFIX_CONTRIBUTOR = new Prefix("c/");
+ public static final Prefix PREFIX_INTERVIEWEE = new Prefix("i/");
+ public static final Prefix PREFIX_OUTLET = new Prefix("o/");
+ public static final Prefix PREFIX_DATE = new Prefix("d/");
+ public static final Prefix PREFIX_STATUS = new Prefix("s/");
+ public static final Prefix PREFIX_START = new Prefix("st/");
+ public static final Prefix PREFIX_END = new Prefix("en/");
+ public static final Prefix PREFIX_LINK = new Prefix("l/");
+
}
diff --git a/src/main/java/seedu/address/logic/parser/DeleteArticleCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteArticleCommandParser.java
new file mode 100644
index 00000000000..ee9392a3bbf
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/DeleteArticleCommandParser.java
@@ -0,0 +1,27 @@
+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.articlecommands.DeleteArticleCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses input arguments and creates a new DeleteArticleCommand object
+ */
+public class DeleteArticleCommandParser implements Parser {
+ /**
+ * Parses the given {@code String} of arguments in the context of the DeleteArticleCommand
+ * and returns a DeleteArticleCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public DeleteArticleCommand parse(String args) throws ParseException {
+ try {
+ Index index = ParserUtil.parseIndex(args);
+ return new DeleteArticleCommand(index);
+ } catch (ParseException pe) {
+ throw new ParseException(
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteArticleCommand.MESSAGE_USAGE), pe);
+ }
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/EditArticleCommandParser.java b/src/main/java/seedu/address/logic/parser/EditArticleCommandParser.java
new file mode 100644
index 00000000000..9b2e8a4db46
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/EditArticleCommandParser.java
@@ -0,0 +1,123 @@
+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_CONTRIBUTOR;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_HEADLINE;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_INTERVIEWEE;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_LINK;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_OUTLET;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_STATUS;
+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.articlecommands.EditArticleCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.article.Author;
+import seedu.address.model.article.Outlet;
+import seedu.address.model.article.Source;
+import seedu.address.model.tag.Tag;
+
+/**
+ * Parses input arguments and creates a new EditArticleCommand object
+ */
+public class EditArticleCommandParser 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 EditArticleCommand parse(String args) throws ParseException {
+ requireNonNull(args);
+ ArgumentMultimap argMultimap =
+ ArgumentTokenizer.tokenize(args, PREFIX_HEADLINE, PREFIX_CONTRIBUTOR, PREFIX_INTERVIEWEE,
+ PREFIX_TAG, PREFIX_OUTLET, PREFIX_DATE, PREFIX_STATUS, PREFIX_LINK);
+
+ Index index;
+
+ try {
+ index = ParserUtil.parseIndex(argMultimap.getPreamble());
+ } catch (ParseException pe) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditArticleCommand.MESSAGE_USAGE),
+ pe);
+ }
+
+ argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_HEADLINE, PREFIX_DATE, PREFIX_STATUS);
+
+ EditArticleCommand.EditArticleDescriptor editArticleDescriptor = new EditArticleCommand.EditArticleDescriptor();
+
+ if (argMultimap.getValue(PREFIX_HEADLINE).isPresent()) {
+ editArticleDescriptor.setTitle(ParserUtil.parseTitle(argMultimap.getValue(PREFIX_HEADLINE).get()));
+ }
+ if (argMultimap.getValue(PREFIX_DATE).isPresent()) {
+ editArticleDescriptor.setPublicationDate(ParserUtil.parsePublicationDate(argMultimap
+ .getValue(PREFIX_DATE).get()));
+ }
+ if (argMultimap.getValue(PREFIX_STATUS).isPresent()) {
+ editArticleDescriptor.setStatus(ParserUtil.parseStatus(argMultimap.getValue(PREFIX_STATUS)
+ .get()));
+ }
+ if (argMultimap.getValue(PREFIX_LINK).isPresent()) {
+ editArticleDescriptor.setLink(ParserUtil.parseLink(argMultimap.getValue(PREFIX_LINK).get()));
+ }
+
+ parseAuthorsForEdit(argMultimap.getAllValues(PREFIX_CONTRIBUTOR)).ifPresent(editArticleDescriptor::setAuthors);
+ parseSourcesForEdit(argMultimap.getAllValues(PREFIX_INTERVIEWEE)).ifPresent(editArticleDescriptor::setSources);
+ parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editArticleDescriptor::setTags);
+ parseOutletsForEdit(argMultimap.getAllValues(PREFIX_OUTLET)).ifPresent(editArticleDescriptor::setOutlets);
+
+ if (!editArticleDescriptor.isAnyFieldEdited()) {
+ throw new ParseException(EditCommand.MESSAGE_NOT_EDITED);
+ }
+
+ return new EditArticleCommand(index, editArticleDescriptor);
+ }
+
+ private Optional> parseAuthorsForEdit(Collection authors) throws ParseException {
+ assert authors != null;
+
+ if (authors.isEmpty()) {
+ return Optional.empty();
+ }
+ Collection authorSet = authors.size() == 1 && authors.contains("") ? Collections.emptySet() : authors;
+ return Optional.of(ParserUtil.parseAuthors(authorSet));
+ }
+
+ private Optional> parseSourcesForEdit(Collection sources) throws ParseException {
+ assert sources != null;
+
+ if (sources.isEmpty()) {
+ return Optional.empty();
+ }
+ Collection sourceSet = sources.size() == 1 && sources.contains("") ? Collections.emptySet() : sources;
+ return Optional.of(ParserUtil.parseSources(sourceSet));
+ }
+
+ 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));
+ }
+ private Optional> parseOutletsForEdit(Collection outlets) throws ParseException {
+ assert outlets != null;
+
+ if (outlets.isEmpty()) {
+ return Optional.empty();
+ }
+ Collection outletSet = outlets.size() == 1 && outlets.contains("") ? Collections.emptySet() : outlets;
+ return Optional.of(ParserUtil.parseOutlets(outletSet));
+ }
+
+}
diff --git a/src/main/java/seedu/address/logic/parser/FilterArticleCommandParser.java b/src/main/java/seedu/address/logic/parser/FilterArticleCommandParser.java
new file mode 100644
index 00000000000..9b3f78c509e
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/FilterArticleCommandParser.java
@@ -0,0 +1,60 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.logic.parser.CliSyntax.PREFIX_END;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_START;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_STATUS;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG;
+
+import java.time.LocalDateTime;
+import java.util.stream.Stream;
+
+import seedu.address.logic.commands.articlecommands.FilterArticleCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.article.PublicationDate;
+import seedu.address.model.tag.Tag;
+
+/**
+ * Parses input arguments and creates a FilterArticleCommand object
+ */
+public class FilterArticleCommandParser implements Parser {
+
+ @Override
+ public FilterArticleCommand parse(String userInput) throws ParseException {
+ ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(userInput, PREFIX_STATUS,
+ PREFIX_TAG, PREFIX_START, PREFIX_END);
+ if (!arePrefixesPresent(argMultimap, PREFIX_STATUS, PREFIX_TAG, PREFIX_START, PREFIX_END)) {
+ throw new ParseException("Invalid command format!\nfilter: Applies a filter. "
+ + "Parameters: " + PREFIX_STATUS + "STATUS " + PREFIX_TAG + "TAG "
+ + PREFIX_START + "START DATE " + PREFIX_END + "END DATE"
+ + "\nExample: filter -a " + PREFIX_STATUS
+ + "DRAFT " + PREFIX_TAG + "Product Releases "
+ + PREFIX_START + "01-01-2001 " + PREFIX_END + "03-03-2023"
+ );
+ }
+ String status = argMultimap.getValue(PREFIX_STATUS).get();
+ String tagName = argMultimap.getValue(PREFIX_TAG).get();
+ String start = argMultimap.getValue(PREFIX_START).get();
+ String end = argMultimap.getValue(PREFIX_END).get();
+ PublicationDate startDate;
+ PublicationDate endDate;
+ Tag tag = null;
+ if (start.trim().equals("")) {
+ startDate = new PublicationDate(LocalDateTime.MIN);
+ } else {
+ startDate = ParserUtil.parsePublicationDate(start.trim());
+ }
+ if (end.trim().equals("")) {
+ endDate = new PublicationDate(LocalDateTime.MAX);
+ } else {
+ endDate = ParserUtil.parsePublicationDate(end.trim());
+ }
+ if (!tagName.trim().equals("")) {
+ tag = ParserUtil.parseTag(tagName);
+ }
+ return new FilterArticleCommand(status, tag, startDate, endDate);
+ }
+
+ 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/FindArticleCommandParser.java b/src/main/java/seedu/address/logic/parser/FindArticleCommandParser.java
new file mode 100644
index 00000000000..0e33f80b9e7
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/FindArticleCommandParser.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.articlecommands.FindArticleCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.article.TitleContainsKeywordsPredicate;
+
+/**
+ * Parses input arguments and creates a new FindArticleCommand object
+ */
+public class FindArticleCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the FindArticleCommand
+ * and returns a FindArticleCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public FindArticleCommand parse(String args) throws ParseException {
+ String trimmedArgs = args.trim();
+ if (trimmedArgs.isEmpty()) {
+ throw new ParseException(
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindArticleCommand.MESSAGE_USAGE));
+ }
+
+ String[] nameKeywords = trimmedArgs.split("\\s+");
+
+ return new FindArticleCommand(new TitleContainsKeywordsPredicate(Arrays.asList(nameKeywords)));
+ }
+
+}
diff --git a/src/main/java/seedu/address/logic/parser/LookupArticleCommandParser.java b/src/main/java/seedu/address/logic/parser/LookupArticleCommandParser.java
new file mode 100644
index 00000000000..eed009d7482
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/LookupArticleCommandParser.java
@@ -0,0 +1,28 @@
+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.articlecommands.LookupArticleCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses input arguments and creates a new LookupArticleCommand object
+ */
+public class LookupArticleCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the LookupArticleCommand
+ * and returns a LookupArticleCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public LookupArticleCommand parse(String args) throws ParseException {
+ try {
+ Index index = ParserUtil.parseIndex(args);
+ return new LookupArticleCommand(index);
+ } catch (ParseException pe) {
+ throw new ParseException(
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, LookupArticleCommand.MESSAGE_USAGE), pe);
+ }
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/LookupCommandParser.java b/src/main/java/seedu/address/logic/parser/LookupCommandParser.java
new file mode 100644
index 00000000000..00214362bf9
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/LookupCommandParser.java
@@ -0,0 +1,29 @@
+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.LookupCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses input arguments and creates a new LookupCommand object
+ */
+public class LookupCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the LookupCommand
+ * and returns a LookupCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public LookupCommand parse(String args) throws ParseException {
+ try {
+ Index index = ParserUtil.parseIndex(args);
+ return new LookupCommand(index);
+ } catch (ParseException pe) {
+ throw new ParseException(
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, LookupCommand.MESSAGE_USAGE), pe);
+ }
+ }
+
+}
diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java
index b117acb9c55..301bf69c959 100644
--- a/src/main/java/seedu/address/logic/parser/ParserUtil.java
+++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java
@@ -1,7 +1,16 @@
package seedu.address.logic.parser;
import static java.util.Objects.requireNonNull;
+import static seedu.address.model.article.Article.Status.ARCHIVED;
+import static seedu.address.model.article.Article.Status.DRAFT;
+import static seedu.address.model.article.Article.Status.PUBLISHED;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+import java.time.format.DateTimeParseException;
+import java.time.format.ResolverStyle;
+import java.time.temporal.ChronoField;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@@ -9,6 +18,13 @@
import seedu.address.commons.core.index.Index;
import seedu.address.commons.util.StringUtil;
import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.article.Article.Status;
+import seedu.address.model.article.Author;
+import seedu.address.model.article.Link;
+import seedu.address.model.article.Outlet;
+import seedu.address.model.article.PublicationDate;
+import seedu.address.model.article.Source;
+import seedu.address.model.article.Title;
import seedu.address.model.person.Address;
import seedu.address.model.person.Email;
import seedu.address.model.person.Name;
@@ -121,4 +137,150 @@ public static Set parseTags(Collection tags) throws ParseException
}
return tagSet;
}
+
+ /**
+ * Parses a {@code String title} into a {@code Title}.
+ */
+ public static Title parseTitle(String title) throws ParseException {
+ requireNonNull(title);
+ String trimmedTitle = title.trim();
+ if (!Title.isValidTitle(trimmedTitle)) {
+ throw new ParseException(Title.MESSAGE_CONSTRAINTS);
+ }
+ return new Title(trimmedTitle);
+ }
+
+ /**
+ * Parses a {@code String author} into a {@code Author}.
+ */
+ public static Author parseAuthor(String author) throws ParseException {
+ requireNonNull(author);
+ String trimmedAuthor = author.trim();
+ if (!Author.isValidAuthorName(trimmedAuthor)) {
+ throw new ParseException(Author.MESSAGE_CONSTRAINTS);
+ }
+ return new Author(trimmedAuthor);
+ }
+
+ /**
+ * Parses a {@code List authors} into a {@code Set}.
+ */
+ public static Set parseAuthors(Collection authors) throws ParseException {
+ requireNonNull(authors);
+ final Set authorSet = new HashSet<>();
+ for (String authorName : authors) {
+ authorSet.add(parseAuthor(authorName));
+ }
+ return authorSet;
+ }
+
+ /**
+ * Parses a {@code String publicationDate} into a {@code PublicationDate}.
+ */
+ public static PublicationDate parsePublicationDate(String publicationDate) throws ParseException {
+ requireNonNull(publicationDate);
+
+ DateTimeFormatter formatter = new DateTimeFormatterBuilder()
+ .appendPattern("dd-MM-uuuu[ HH:mm]")
+ .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
+ .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
+ .toFormatter();
+ DateTimeFormatter formatterStrict = formatter.withResolverStyle(ResolverStyle.STRICT);
+
+ String trimmedPublicationDate = publicationDate.trim();
+ try {
+ LocalDateTime tempDate = LocalDateTime.parse(trimmedPublicationDate, formatterStrict);
+ return new PublicationDate(tempDate);
+ } catch (DateTimeParseException e) {
+ throw new ParseException("Invalid date");
+ }
+ }
+
+ /**
+ * Parses a {@code LocalDateTime date} into a {@code String}.
+ * @param date The date to be parsed.
+ * @return The date in the format of [dd-MM-uuuu HH:mm].
+ */
+ public static String parseDateToString(LocalDateTime date) {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-uuuu HH:mm");
+ return date.format(formatter);
+ }
+
+ /**
+ * Parses a {@code String source} into a {@code Source}.
+ */
+ public static Source parseSource(String source) throws ParseException {
+ requireNonNull(source);
+ String trimmedSource = source.trim();
+ if (!Source.isValidSourceName(trimmedSource)) {
+ throw new ParseException(Source.MESSAGE_CONSTRAINTS);
+ }
+ return new Source(trimmedSource);
+ }
+
+ /**
+ * Parses a {@code List sources} into a {@code Set}.
+ */
+ public static Set parseSources(Collection sources) throws ParseException {
+ requireNonNull(sources);
+ final Set sourceSet = new HashSet<>();
+ for (String sourceName : sources) {
+ sourceSet.add(parseSource(sourceName));
+ }
+ return sourceSet;
+ }
+
+ /**
+ * Parses a {@code String outlet} into a {@code Outlet}.
+ */
+ public static Outlet parseOutlet(String outlet) throws ParseException {
+ requireNonNull(outlet);
+ String trimmedOutlet = outlet.trim();
+ if (!Outlet.isValidOutletName(trimmedOutlet)) {
+ throw new ParseException(Outlet.MESSAGE_CONSTRAINTS);
+ }
+ return new Outlet(trimmedOutlet);
+ }
+
+ /**
+ * Parses a {@code List outlets} into a {@code Set}.
+ */
+ public static Set parseOutlets(Collection outlets) throws ParseException {
+ requireNonNull(outlets);
+ final Set outletSet = new HashSet<>();
+ for (String outletName : outlets) {
+ outletSet.add(parseOutlet(outletName));
+ }
+ return outletSet;
+ }
+
+ /**
+ * Parses a {@code String status} into a {@code Status}.
+ */
+ public static Status parseStatus(String status) throws ParseException {
+ requireNonNull(status);
+ String trimmedStatus = status.trim();
+ switch (trimmedStatus.toUpperCase()) {
+ case "DRAFT":
+ return DRAFT;
+ case "PUBLISHED":
+ return PUBLISHED;
+ case "ARCHIVED":
+ return ARCHIVED;
+ default:
+ throw new ParseException("Invalid status provided. Please provide either draft, published or archived.");
+ }
+ }
+
+ /**
+ * Parses a {@code String link} into a {@code Link}.
+ */
+ public static Link parseLink(String link) throws ParseException {
+ requireNonNull(link);
+ String trimmedLink = link.trim();
+ if (!Link.isValidLink(trimmedLink)) {
+ throw new ParseException(Link.MESSAGE_CONSTRAINTS);
+ }
+ return new Link(trimmedLink);
+ }
}
diff --git a/src/main/java/seedu/address/logic/parser/SortArticleCommandParser.java b/src/main/java/seedu/address/logic/parser/SortArticleCommandParser.java
new file mode 100644
index 00000000000..27a0a8d9c9f
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/SortArticleCommandParser.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_DATE;
+
+import java.util.ArrayList;
+
+import seedu.address.logic.commands.articlecommands.SortArticleCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses input arguments and creates a new SortArticleCommand object
+ */
+public class SortArticleCommandParser implements Parser {
+
+ private static final ArrayList AllowedPrefixes = new ArrayList<>() {
+ {
+ add(PREFIX_DATE.getPrefix());
+ }
+ };
+
+ /**
+ * Checks if the given prefix is allowed in choosing an attribute for sorting that is case-insensitive.
+ */
+ public static boolean isAllowedPrefix(String prefix) {
+ return AllowedPrefixes.contains(prefix.toLowerCase()) || AllowedPrefixes.contains(prefix.toUpperCase());
+ }
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the SortArticleCommand
+ * and returns an SortArticleCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public SortArticleCommand parse(String args) throws ParseException {
+ String prefix = args.trim();
+ if (prefix.isEmpty() || !PREFIX_DATE.getPrefix().equalsIgnoreCase(prefix)) {
+ throw new ParseException(
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortArticleCommand.MESSAGE_USAGE));
+ }
+
+ return new SortArticleCommand(prefix);
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/SortCommandParser.java b/src/main/java/seedu/address/logic/parser/SortCommandParser.java
new file mode 100644
index 00000000000..2eae41b4871
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/SortCommandParser.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_NAME;
+
+import java.util.ArrayList;
+
+import seedu.address.logic.commands.SortCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses input arguments and creates a new SortCommand object
+ */
+public class SortCommandParser implements Parser {
+
+ private static final ArrayList AllowedPrefixes = new ArrayList<>() {
+ {
+ add(PREFIX_NAME.getPrefix());
+ }
+ };
+
+ /**
+ * Checks if the given prefix is allowed in choosing an attribute for sorting that is case-insensitive.
+ */
+ public static boolean isAllowedPrefix(String prefix) {
+ return AllowedPrefixes.contains(prefix.toLowerCase()) || AllowedPrefixes.contains(prefix.toUpperCase());
+ }
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the SortCommand
+ * and returns an SortCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public SortCommand parse(String args) throws ParseException {
+
+ String prefix = args.trim();
+ if (prefix.isEmpty() || !PREFIX_NAME.getPrefix().equalsIgnoreCase(prefix)) {
+ throw new ParseException(
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE));
+ }
+
+ return new SortCommand(prefix);
+ }
+
+}
diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java
index 73397161e84..fa5deeec946 100644
--- a/src/main/java/seedu/address/model/AddressBook.java
+++ b/src/main/java/seedu/address/model/AddressBook.java
@@ -94,6 +94,13 @@ public void removePerson(Person key) {
persons.remove(key);
}
+ /**
+ * Sorts the persons in the address book by the attribute derived from a given prefix.
+ */
+ public void sortAddressBook(String prefix) {
+ persons.sortPersons(prefix);
+ }
+
//// util methods
@Override
diff --git a/src/main/java/seedu/address/model/ArticleBook.java b/src/main/java/seedu/address/model/ArticleBook.java
new file mode 100644
index 00000000000..209e5bcf0fa
--- /dev/null
+++ b/src/main/java/seedu/address/model/ArticleBook.java
@@ -0,0 +1,153 @@
+package seedu.address.model;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.List;
+
+import javafx.collections.ObservableList;
+import seedu.address.commons.util.ToStringBuilder;
+import seedu.address.model.article.Article;
+import seedu.address.model.article.UniqueArticleList;
+import seedu.address.model.person.Person;
+
+/**
+ * Wraps all data at the article-book level
+ * Duplicates are not allowed (by .isSameArticle comparison)
+ */
+public class ArticleBook implements ReadOnlyArticleBook {
+
+ private final UniqueArticleList articles;
+
+ {
+ articles = new UniqueArticleList();
+ }
+
+ public ArticleBook() {}
+
+ /**
+ * Creates an ArticleBook using the Articles in the {@code toBeCopied}
+ */
+ public ArticleBook(ReadOnlyArticleBook toBeCopied) {
+ this();
+ resetData(toBeCopied);
+ }
+
+ //// list overwrite operations
+
+ /**
+ * Replaces the contents of the article list with {@code articless}.
+ * {@code articles} must not contain duplicate article.
+ */
+ public void setArticles(List articles) {
+ this.articles.setArticles(articles);
+ }
+
+ /**
+ * Resets the existing data of this {@code ArticleBook} with {@code newData}.
+ */
+ public void resetData(ReadOnlyArticleBook newData) {
+ requireNonNull(newData);
+
+ setArticles(newData.getArticleList());
+ }
+
+ //// article-level operations
+
+ /**
+ * Returns true if an article with the same identity as {@code article} exists in the article book.
+ */
+ public boolean hasArticle(Article article) {
+ requireNonNull(article);
+ return articles.contains(article);
+ }
+
+ /**
+ * Adds an article to the article book.
+ * The article must not already exist in the article book.
+ */
+ public void addArticle(Article article) {
+ articles.add(article);
+ }
+
+ /**
+ * Replaces the given article {@code target} in the list with {@code editedArticle}.
+ * {@code target} must exist in the article book.
+ * The article identity of {@code editedArticle} must not be the same as
+ * another existing Article in the Article book.
+ */
+ public void setArticle(Article target, Article editedArticle) {
+ requireNonNull(editedArticle);
+
+ articles.setArticle(target, editedArticle);
+ }
+
+ /**
+ * Removes {@code key} from this {@code ArticleBook}.
+ * {@code key} must exist in the Article book.
+ */
+ public void removeArticle(Article key) {
+ articles.remove(key);
+ }
+
+ /**
+ * Sorts the article book by the attribute represented by the given prefix.
+ */
+ public void sortArticleBook(String prefix) {
+ articles.sortArticles(prefix);
+ }
+
+ //// util methods
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("Articles", articles)
+ .toString();
+ }
+
+ @Override
+ public ObservableList getArticleList() {
+ return articles.asUnmodifiableObservableList();
+ }
+
+ /**
+ * Makes links between articles and persons in the address book.
+ */
+ public void makeLinks(AddressBook addressBook) {
+ articles.makeLinks(addressBook.getPersonList());
+ }
+
+ /**
+ * Makes links between articles and the given person.
+ */
+ public void makeLinkPerson(Person person) {
+ articles.makeLinkPerson(person);
+ }
+
+ /**
+ * Reestablishes links between articles and the edited person.
+ */
+ public void setEditedPerson(Person target, Person editedPerson) {
+ articles.setEditedPerson(target, editedPerson);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof ArticleBook)) {
+ return false;
+ }
+
+ ArticleBook otherArticleBook = (ArticleBook) other;
+ return articles.equals(otherArticleBook.articles);
+ }
+
+ @Override
+ public int hashCode() {
+ return articles.hashCode();
+ }
+}
diff --git a/src/main/java/seedu/address/model/ArticleFilter.java b/src/main/java/seedu/address/model/ArticleFilter.java
new file mode 100644
index 00000000000..ad331e610bb
--- /dev/null
+++ b/src/main/java/seedu/address/model/ArticleFilter.java
@@ -0,0 +1,23 @@
+package seedu.address.model;
+
+import static seedu.address.model.Model.PREDICATE_SHOW_ALL_ARTICLES;
+
+import java.util.function.Predicate;
+
+import seedu.address.model.article.Article;
+
+/**
+ * Use to filter through ArticleBook
+ */
+public class ArticleFilter implements Filter {
+ private Predicate finalPredicate;
+ public ArticleFilter() {
+ finalPredicate = PREDICATE_SHOW_ALL_ARTICLES;
+ }
+ public void updateFilter(Predicate newPredicate) {
+ finalPredicate = newPredicate;
+ }
+ public Predicate getFinalPredicate() {
+ return finalPredicate;
+ }
+}
diff --git a/src/main/java/seedu/address/model/Filter.java b/src/main/java/seedu/address/model/Filter.java
new file mode 100644
index 00000000000..8725e56ba63
--- /dev/null
+++ b/src/main/java/seedu/address/model/Filter.java
@@ -0,0 +1,7 @@
+package seedu.address.model;
+
+/**
+ * Used to sieve through lists.
+ */
+public interface Filter {
+}
diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java
index d54df471c1f..bd59711bf34 100644
--- a/src/main/java/seedu/address/model/Model.java
+++ b/src/main/java/seedu/address/model/Model.java
@@ -5,6 +5,7 @@
import javafx.collections.ObservableList;
import seedu.address.commons.core.GuiSettings;
+import seedu.address.model.article.Article;
import seedu.address.model.person.Person;
/**
@@ -14,6 +15,9 @@ public interface Model {
/** {@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_ARTICLES = unused -> true;
+
/**
* Replaces user prefs data with the data in {@code userPrefs}.
*/
@@ -76,6 +80,11 @@ public interface Model {
*/
void setPerson(Person target, Person editedPerson);
+ /**
+ * Sorts the address book by the attribute represented by the given prefix.
+ */
+ void sortAddressBook(String prefix);
+
/** Returns an unmodifiable view of the filtered person list */
ObservableList getFilteredPersonList();
@@ -84,4 +93,64 @@ public interface Model {
* @throws NullPointerException if {@code predicate} is null.
*/
void updateFilteredPersonList(Predicate predicate);
+
+ //=========== Article ================================================================================
+
+ /**
+ * Replaces article book data with the data in {@code articleBook}.
+ */
+ void setArticleBook(ReadOnlyArticleBook articleBook);
+
+ /** Returns the ArticleBook */
+ ReadOnlyArticleBook getArticleBook();
+
+ /**
+ * Returns true if an article with the same identity as {@code article} exists in the article book.
+ */
+ boolean hasArticle(Article article);
+
+ /**
+ * Adds the given article.
+ * {@code article} must not already exist in the article book.
+ */
+ void addArticle(Article article);
+
+ /** Returns an unmodifiable view of the filtered article list */
+ ObservableList getFilteredArticleList();
+
+ /**
+ * Replaces the given article {@code target} with {@code editedArticle}.
+ * {@code target} must exist in the article book.
+ * The article identity of {@code editedArticle} must not be the same as
+ * another existing article in the article book.
+ */
+ void setArticle(Article target, Article editedArticle);
+
+ /**
+ * Deletes the given article.
+ * The article must exist in the article book.
+ */
+ void deleteArticle(Article target);
+
+ /**
+ * Sorts the article book by the attribute represented by the given prefix.
+ */
+ void sortArticleBook(String prefix);
+
+ /**
+ * Updates the filter of the filtered article list to filter by the given {@code predicate}.
+ * @throws NullPointerException if {@code predicate} is null.
+ */
+ void updateFilteredArticleList(Predicate predicate);
+ ArticleFilter getFilter();
+
+ /**
+ * Updates the filter of the filtered article list to filter for persons within the article.
+ */
+ void lookupArticle(Article articleToLookup);
+
+ /**
+ * Updates the filter of the filtered article list to filter for persons within the article.
+ */
+ void lookupPerson(Person personToLookup);
}
diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java
index 57bc563fde6..4e4bf43b08f 100644
--- a/src/main/java/seedu/address/model/ModelManager.java
+++ b/src/main/java/seedu/address/model/ModelManager.java
@@ -11,6 +11,9 @@
import javafx.collections.transformation.FilteredList;
import seedu.address.commons.core.GuiSettings;
import seedu.address.commons.core.LogsCenter;
+import seedu.address.model.article.Article;
+import seedu.address.model.article.ArticleWithinPersonPredicate;
+import seedu.address.model.person.NameWithinArticlePredicate;
import seedu.address.model.person.Person;
/**
@@ -20,24 +23,32 @@ public class ModelManager implements Model {
private static final Logger logger = LogsCenter.getLogger(ModelManager.class);
private final AddressBook addressBook;
+ private final ArticleBook articleBook;
private final UserPrefs userPrefs;
private final FilteredList filteredPersons;
+ private final FilteredList filteredArticles;
+ private final ArticleFilter filter;
/**
* Initializes a ModelManager with the given addressBook and userPrefs.
*/
- public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs) {
+ public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyArticleBook articleBook, ReadOnlyUserPrefs userPrefs) {
requireAllNonNull(addressBook, userPrefs);
logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs);
this.addressBook = new AddressBook(addressBook);
+ this.articleBook = new ArticleBook(articleBook);
this.userPrefs = new UserPrefs(userPrefs);
filteredPersons = new FilteredList<>(this.addressBook.getPersonList());
+ filteredArticles = new FilteredList<>(this.articleBook.getArticleList());
+ filter = new ArticleFilter();
+
+ this.articleBook.makeLinks(this.addressBook);
}
public ModelManager() {
- this(new AddressBook(), new UserPrefs());
+ this(new AddressBook(), new ArticleBook(), new UserPrefs());
}
//=========== UserPrefs ==================================================================================
@@ -74,11 +85,11 @@ public void setAddressBookFilePath(Path addressBookFilePath) {
requireNonNull(addressBookFilePath);
userPrefs.setAddressBookFilePath(addressBookFilePath);
}
-
//=========== AddressBook ================================================================================
@Override
public void setAddressBook(ReadOnlyAddressBook addressBook) {
+ requireNonNull(addressBook);
this.addressBook.resetData(addressBook);
}
@@ -95,12 +106,15 @@ public boolean hasPerson(Person person) {
@Override
public void deletePerson(Person target) {
+ requireNonNull(target);
addressBook.removePerson(target);
}
@Override
public void addPerson(Person person) {
+ requireNonNull(person);
addressBook.addPerson(person);
+ articleBook.makeLinkPerson(person);
updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS);
}
@@ -109,6 +123,12 @@ public void setPerson(Person target, Person editedPerson) {
requireAllNonNull(target, editedPerson);
addressBook.setPerson(target, editedPerson);
+ articleBook.setEditedPerson(target, editedPerson);
+ }
+
+ @Override
+ public void sortAddressBook(String prefix) {
+ addressBook.sortAddressBook(prefix);
}
//=========== Filtered Person List Accessors =============================================================
@@ -128,6 +148,70 @@ public void updateFilteredPersonList(Predicate predicate) {
filteredPersons.setPredicate(predicate);
}
+ //=========== ArticleBook ================================================================================
+
+ @Override
+ public void setArticleBook(ReadOnlyArticleBook articleBook) {
+ requireNonNull(articleBook);
+ this.articleBook.resetData(articleBook);
+ }
+
+ @Override
+ public ReadOnlyArticleBook getArticleBook() {
+ return articleBook;
+ }
+
+ @Override
+ public boolean hasArticle(Article article) {
+ requireNonNull(article);
+ return articleBook.hasArticle(article);
+ }
+
+ @Override
+ public void deleteArticle(Article target) {
+ requireNonNull(target);
+ articleBook.removeArticle(target);
+ }
+
+ @Override
+ public void addArticle(Article article) {
+ requireNonNull(article);
+ articleBook.addArticle(article);
+ article.makeLinks(addressBook.getPersonList());
+ updateFilteredArticleList(PREDICATE_SHOW_ALL_ARTICLES);
+ }
+
+ @Override
+ public void setArticle(Article target, Article editedArticle) {
+ requireAllNonNull(target, editedArticle);
+
+ editedArticle.setPersons(editedArticle.getMatchingPersonsList(addressBook.getPersonList()));
+ articleBook.setArticle(target, editedArticle);
+ }
+
+ @Override
+ public void sortArticleBook(String prefix) {
+ articleBook.sortArticleBook(prefix);
+ }
+
+ //=========== Filtered Article List Accessors =============================================================
+
+ /**
+ * Returns an unmodifiable view of the list of {@code Article} backed by the internal list of
+ * {@code versionedArticleBook}
+ */
+ @Override
+ public ObservableList getFilteredArticleList() {
+ return filteredArticles;
+ }
+
+ @Override
+ public void updateFilteredArticleList(Predicate predicate) {
+ requireNonNull(predicate);
+ predicate = predicate.and(filter.getFinalPredicate());
+ filteredArticles.setPredicate(predicate);
+ }
+
@Override
public boolean equals(Object other) {
if (other == this) {
@@ -141,8 +225,27 @@ public boolean equals(Object other) {
ModelManager otherModelManager = (ModelManager) other;
return addressBook.equals(otherModelManager.addressBook)
+ && articleBook.equals(otherModelManager.articleBook)
&& userPrefs.equals(otherModelManager.userPrefs)
- && filteredPersons.equals(otherModelManager.filteredPersons);
+ && filteredPersons.equals(otherModelManager.filteredPersons)
+ && filteredArticles.equals(otherModelManager.filteredArticles);
+ }
+
+ public ArticleFilter getFilter() {
+ return filter;
}
+ @Override
+ public void lookupArticle(Article article) {
+ requireNonNull(article);
+ NameWithinArticlePredicate predicate = new NameWithinArticlePredicate(article);
+ updateFilteredPersonList(predicate);
+ }
+
+ @Override
+ public void lookupPerson(Person personToLookup) {
+ requireNonNull(personToLookup);
+ ArticleWithinPersonPredicate predicate = new ArticleWithinPersonPredicate(personToLookup);
+ updateFilteredArticleList(predicate);
+ }
}
diff --git a/src/main/java/seedu/address/model/ReadOnlyArticleBook.java b/src/main/java/seedu/address/model/ReadOnlyArticleBook.java
new file mode 100644
index 00000000000..63f1032301b
--- /dev/null
+++ b/src/main/java/seedu/address/model/ReadOnlyArticleBook.java
@@ -0,0 +1,16 @@
+package seedu.address.model;
+
+import javafx.collections.ObservableList;
+import seedu.address.model.article.Article;
+
+/**
+ * Unmodifiable view of an article book
+ */
+public interface ReadOnlyArticleBook {
+
+ /**
+ * Returns an unmodifiable view of the articles list.
+ * This list will not contain any duplicate articles.
+ */
+ ObservableList getArticleList();
+}
diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/address/model/UserPrefs.java
index 6be655fb4c7..d5e1b67fd1f 100644
--- a/src/main/java/seedu/address/model/UserPrefs.java
+++ b/src/main/java/seedu/address/model/UserPrefs.java
@@ -15,7 +15,7 @@ public class UserPrefs implements ReadOnlyUserPrefs {
private GuiSettings guiSettings = new GuiSettings();
private Path addressBookFilePath = Paths.get("data" , "addressbook.json");
-
+ private Path articleBookFilePath = Paths.get("data", "articlebook.json");
/**
* Creates a {@code UserPrefs} with default values.
*/
@@ -47,6 +47,8 @@ public void setGuiSettings(GuiSettings guiSettings) {
this.guiSettings = guiSettings;
}
+ // ================ AddressBook methods ==============================
+
public Path getAddressBookFilePath() {
return addressBookFilePath;
}
@@ -56,6 +58,12 @@ public void setAddressBookFilePath(Path addressBookFilePath) {
this.addressBookFilePath = addressBookFilePath;
}
+ // ================ ArticleBook methods ==============================
+
+ public Path getArticleBookFilePath() {
+ return articleBookFilePath;
+ }
+
@Override
public boolean equals(Object other) {
if (other == this) {
diff --git a/src/main/java/seedu/address/model/article/Article.java b/src/main/java/seedu/address/model/article/Article.java
new file mode 100644
index 00000000000..6e4288ba1b0
--- /dev/null
+++ b/src/main/java/seedu/address/model/article/Article.java
@@ -0,0 +1,253 @@
+package seedu.address.model.article;
+
+import static seedu.address.commons.util.CollectionUtil.requireAllNonNull;
+import static seedu.address.logic.parser.ParserUtil.parseDateToString;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+import seedu.address.commons.util.ToStringBuilder;
+import seedu.address.model.person.Person;
+import seedu.address.model.tag.Tag;
+
+/**
+ * Represents an article in the address book.
+ */
+public class Article {
+ private final Title title;
+ private final Set outlets = new HashSet<>();
+ private final Set authors = new HashSet<>();
+ private final Set sources = new HashSet<>();
+ private final Set tags = new HashSet<>();
+ private final PublicationDate publicationDate;
+
+ private List persons = new ArrayList<>();
+
+ /**
+ * Enumeration of Status of an article.
+ */
+ public enum Status {
+ DRAFT, PUBLISHED, ARCHIVED
+ }
+
+ private final Status status;
+ private final Link link;
+
+ /**
+ * Constructs an Article object.
+ *
+ * @param title the title of the article.
+ * @param authors the authors of the article.
+ * @param publicationDate the date of publication.
+ * @param sources the people interviewed.
+ * @param tags the subject of the article.
+ * @param status the current status of the article.
+ */
+ public Article(Title title, Set authors, Set sources, Set tags,
+ Set outlets, PublicationDate publicationDate, Status status, Link link) {
+ requireAllNonNull(title, authors, sources, tags, outlets, publicationDate, status);
+ this.title = title;
+ this.authors.addAll(authors);
+ this.sources.addAll(sources);
+ this.tags.addAll(tags);
+ this.outlets.addAll(outlets);
+ this.publicationDate = publicationDate;
+ this.status = status;
+ this.link = link;
+ }
+
+ public Title getTitle() {
+ return title;
+ }
+
+ public Set getAuthors() {
+ return Collections.unmodifiableSet(authors);
+ }
+
+ public PublicationDate getPublicationDate() {
+ return this.publicationDate;
+ }
+
+ public String getPublicationDateAsString() {
+ return parseDateToString(this.publicationDate.date);
+ }
+
+ public Set getOutlets() {
+ return Collections.unmodifiableSet(outlets);
+ }
+
+ public Set getSources() {
+ return Collections.unmodifiableSet(sources);
+ }
+
+ public Set getTags() {
+ return Collections.unmodifiableSet(tags);
+ }
+
+ public Status getStatus() {
+ return this.status;
+ }
+
+ public List getPersons() {
+ return persons;
+ }
+ public Link getLink() {
+ return this.link;
+ }
+
+ /**
+ * Returns true if all the attributes of Article class are identical to the attributes of an existing Article.
+ *
+ * @param otherArticle
+ * @return
+ */
+ public boolean isSameArticle(Article otherArticle) {
+ if (otherArticle == this) {
+ return true;
+
+ /*
+ * If it is not draft and has same title as another article,
+ * consider it as same article
+ */
+ } else if (otherArticle.getStatus() != Status.DRAFT && this.getStatus() != Status.DRAFT
+ && otherArticle.getTitle().equals(this.title)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns a list of persons that match the authors and sources of the article.
+ *
+ * @param persons a list of persons to compare against the article's authors and sources.
+ * @return a list of persons that match the authors and sources of the article.
+ */
+ public List getMatchingPersonsList(List persons) {
+ List matchingPersons = new ArrayList<>();
+ for (Person person : persons) {
+ for (Author author : authors) {
+ if (author.authorName.equals(person.getNameString())) {
+ matchingPersons.add(person);
+ }
+ }
+ for (Source source : sources) {
+ if (source.sourceName.equals(person.getNameString())) {
+ matchingPersons.add(person);
+ }
+ }
+ }
+ return matchingPersons;
+ }
+
+ /**
+ * Sets the persons list of an edited article.
+ *
+ * @param persons
+ */
+ public void setPersons(List persons) {
+ makeLinks(persons);
+ this.persons = persons;
+ }
+
+ /**
+ * Makes links between the article and the persons in the list.
+ *
+ * @param persons
+ */
+ public void makeLinks(List persons) {
+ for (Person person : persons) {
+ makeLink(person);
+ }
+ }
+
+ /**
+ * Makes links between the article and the person.
+ *
+ * @param person
+ */
+ public void makeLink(Person person) {
+ for (Author author : authors) {
+ if (author.authorName.equals(person.getNameString())) {
+ persons.add(person);
+ person.addArticle(this);
+ }
+ }
+ for (Source source : sources) {
+ if (source.sourceName.equals(person.getNameString())) {
+ persons.add(person);
+ person.addArticle(this);
+ }
+ }
+ }
+
+ /**
+ * Updates the names in the article when a person is edited.
+ *
+ * @param from
+ * @param to
+ */
+ public void updateNamesInArticle(Person from, Person to) {
+ for (Author author : authors) {
+ if (author.authorName.equals(from.getNameString())) {
+ authors.remove(author);
+ persons.remove(from);
+ authors.add(new Author(to.getNameString()));
+ persons.add(to);
+ }
+ }
+ for (Source source : sources) {
+ if (source.sourceName.equals(from.getNameString())) {
+ sources.remove(source);
+ persons.remove(from);
+ sources.add(new Source(to.getNameString()));
+ persons.add(to);
+ }
+ }
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof Article)) {
+ return false;
+ }
+
+ Article otherArticle = (Article) other;
+ return title.equals(otherArticle.title)
+ && authors.equals(otherArticle.authors)
+ && sources.equals(otherArticle.sources)
+ && tags.equals(otherArticle.tags)
+ && outlets.equals(otherArticle.outlets)
+ && publicationDate.equals(otherArticle.publicationDate)
+ && status.equals(otherArticle.status)
+ && link.equals(otherArticle.link);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(title, authors, sources, tags, outlets, publicationDate, status, link);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("headline", title)
+ .add("contributors", authors)
+ .add("interviewees", sources)
+ .add("tags", tags)
+ .add("outlets", outlets)
+ .add("date", publicationDate)
+ .add("status", status)
+ .add("link", link)
+ .toString();
+ }
+}
diff --git a/src/main/java/seedu/address/model/article/ArticleMatchesStatusPredicate.java b/src/main/java/seedu/address/model/article/ArticleMatchesStatusPredicate.java
new file mode 100644
index 00000000000..888ca428415
--- /dev/null
+++ b/src/main/java/seedu/address/model/article/ArticleMatchesStatusPredicate.java
@@ -0,0 +1,53 @@
+package seedu.address.model.article;
+
+
+import java.util.function.Predicate;
+
+import seedu.address.logic.parser.ParserUtil;
+import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.article.exceptions.InvalidStatusException;
+
+/**
+ * Ensures that an {@code Article}'s {@code Status} matches the given status
+ */
+public class ArticleMatchesStatusPredicate implements Predicate {
+ private Enum status;
+
+ /**
+ * Constructs an ArticleMatchesStatusPredicate
+ * @param s The string representation of the status
+ * @throws InvalidStatusException thrown when status entered is invalid.
+ */
+ public ArticleMatchesStatusPredicate(String s) throws InvalidStatusException {
+ try {
+ status = ParserUtil.parseStatus(s);
+ } catch (ParseException e) {
+ throw new InvalidStatusException();
+ }
+ }
+
+ @Override
+ public boolean test(Article article) {
+ //Check that article is not null.
+ assert (article instanceof Article);
+ return this.status.equals(article.getStatus());
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof TitleContainsKeywordsPredicate)) {
+ return false;
+ }
+
+ ArticleMatchesStatusPredicate otherArticleMatchesStatusPredicate = (ArticleMatchesStatusPredicate) other;
+ return this.status.equals(otherArticleMatchesStatusPredicate.getStatus());
+ }
+ public Enum getStatus() {
+ return this.status;
+ }
+}
diff --git a/src/main/java/seedu/address/model/article/ArticleMatchesTagPredicate.java b/src/main/java/seedu/address/model/article/ArticleMatchesTagPredicate.java
new file mode 100644
index 00000000000..de94265de56
--- /dev/null
+++ b/src/main/java/seedu/address/model/article/ArticleMatchesTagPredicate.java
@@ -0,0 +1,40 @@
+package seedu.address.model.article;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.Set;
+import java.util.function.Predicate;
+
+import seedu.address.model.tag.Tag;
+
+/**
+ * Checks if article has matching tag
+ */
+public class ArticleMatchesTagPredicate implements Predicate {
+ private Tag tag;
+
+ /**
+ * Constructs a predicate that returns true if tag matches.
+ * @param tag The tag to be checked against
+ */
+ public ArticleMatchesTagPredicate(Tag tag) {
+ //Ensures that tag is not null
+ assert(tag instanceof Tag);
+ this.tag = tag;
+ }
+
+ @Override
+ public boolean test(Article article) {
+ requireNonNull(article);
+ Set others = article.getTags();
+ boolean isMatch = false;
+ requireNonNull(tag);
+ for (Tag other : others) {
+ requireNonNull(other);
+ if (other.equals(tag)) {
+ isMatch = true;
+ }
+ }
+ return isMatch;
+ }
+}
diff --git a/src/main/java/seedu/address/model/article/ArticleMatchesTimePeriodPredicate.java b/src/main/java/seedu/address/model/article/ArticleMatchesTimePeriodPredicate.java
new file mode 100644
index 00000000000..18d150f27eb
--- /dev/null
+++ b/src/main/java/seedu/address/model/article/ArticleMatchesTimePeriodPredicate.java
@@ -0,0 +1,32 @@
+package seedu.address.model.article;
+
+import java.util.function.Predicate;
+
+import seedu.address.model.article.exceptions.InvalidDatesException;
+
+/**
+ * Ensures that an {@code Article}'s {@code PublicationDate} falls within desired period.
+ */
+public class ArticleMatchesTimePeriodPredicate implements Predicate {
+ private PublicationDate start;
+ private PublicationDate end;
+
+ /**
+ * Constructs a predicate that tests if article was published in desired time period.
+ * @param start The earliest date the article could have been published.
+ * @param end The latest date the article could have been published.
+ */
+ public ArticleMatchesTimePeriodPredicate(PublicationDate start, PublicationDate end) throws InvalidDatesException {
+ this.start = start;
+ this.end = end;
+ if (end.date.isBefore(start.date)) {
+ throw new InvalidDatesException();
+ }
+ }
+
+ @Override
+ public boolean test(Article article) {
+ PublicationDate articleDate = article.getPublicationDate();
+ return articleDate.date.isAfter(start.date) && articleDate.date.isBefore(end.date);
+ }
+}
diff --git a/src/main/java/seedu/address/model/article/ArticleWithinPersonPredicate.java b/src/main/java/seedu/address/model/article/ArticleWithinPersonPredicate.java
new file mode 100644
index 00000000000..2241150ffc3
--- /dev/null
+++ b/src/main/java/seedu/address/model/article/ArticleWithinPersonPredicate.java
@@ -0,0 +1,48 @@
+package seedu.address.model.article;
+
+import java.util.function.Predicate;
+
+import seedu.address.commons.util.ToStringBuilder;
+import seedu.address.model.person.Person;
+
+/**
+ * Tests that an {@code Article} is within a {@code Person}'s list of articles.
+ */
+public class ArticleWithinPersonPredicate implements Predicate {
+
+ private final Person person;
+
+ /**
+ * Constructs a {@code ArticleWithinPersonPredicate}.
+ *
+ * @param person The person to test against.
+ */
+ public ArticleWithinPersonPredicate(Person person) {
+ this.person = person;
+ }
+
+ @Override
+ public boolean test(Article article) {
+ return person.getArticles().contains(article);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof ArticleWithinPersonPredicate)) {
+ return false;
+ }
+
+ ArticleWithinPersonPredicate otherArticleWithinPersonPredicate = (ArticleWithinPersonPredicate) other;
+ return person.equals(otherArticleWithinPersonPredicate.person);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this).add("person", person).toString();
+ }
+}
diff --git a/src/main/java/seedu/address/model/article/Author.java b/src/main/java/seedu/address/model/article/Author.java
new file mode 100644
index 00000000000..55e9246165c
--- /dev/null
+++ b/src/main/java/seedu/address/model/article/Author.java
@@ -0,0 +1,59 @@
+package seedu.address.model.article;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.commons.util.AppUtil.checkArgument;
+
+/**
+ * Represents an Author of an Article
+ */
+public class Author {
+ public static final String MESSAGE_CONSTRAINTS = "Contributor names should be alphanumeric";
+ public static final String VALIDATION_REGEX = "[\\p{Alnum} ]+";
+
+ public final String authorName;
+
+ /**
+ * Constructs a {@code authorName}.
+ *
+ * @param authorName A valid author name.
+ */
+ public Author(String authorName) {
+ requireNonNull(authorName);
+ checkArgument(isValidAuthorName(authorName), MESSAGE_CONSTRAINTS);
+ this.authorName = authorName;
+ }
+
+ /**
+ * Returns true if a given string is a valid author name.
+ */
+ public static boolean isValidAuthorName(String test) {
+ return test.matches(VALIDATION_REGEX);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof Author)) {
+ return false;
+ }
+
+ Author otherAuthor = (Author) other;
+ return authorName.equals(otherAuthor.authorName);
+ }
+
+ @Override
+ public int hashCode() {
+ return authorName.hashCode();
+ }
+
+ /**
+ * Format state as text for viewing.
+ */
+ public String toString() {
+ return '[' + authorName + ']';
+ }
+}
diff --git a/src/main/java/seedu/address/model/article/Link.java b/src/main/java/seedu/address/model/article/Link.java
new file mode 100644
index 00000000000..7b41367e7ec
--- /dev/null
+++ b/src/main/java/seedu/address/model/article/Link.java
@@ -0,0 +1,60 @@
+package seedu.address.model.article;
+
+import static seedu.address.commons.util.AppUtil.checkArgument;
+
+/**
+ * Represents an Article's link in the address book.
+ * Guarantees: immutable; is valid as declared in {@link #isValidLink(String)}
+ */
+public class Link {
+ public static final String MESSAGE_CONSTRAINTS =
+ "Link cannot start with a whitespace character.";
+
+ public static final String VALIDATION_REGEX = "^(?!\\s).*";
+
+ public final String link;
+
+ /**
+ * Constructs a {@code Link}.
+ *
+ * @param link A valid link.
+ */
+ public Link(String link) {
+ if (link == null) {
+ link = "";
+ }
+ checkArgument(isValidLink(link), MESSAGE_CONSTRAINTS);
+ this.link = link;
+ }
+
+ /**
+ * Returns true if a given string is a valid link.
+ */
+ public static boolean isValidLink(String test) {
+ return test.matches(VALIDATION_REGEX);
+ }
+
+ @Override
+ public String toString() {
+ return link;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ if (!(other instanceof Link)) {
+ return false;
+ }
+
+ Link otherTitle = (Link) other;
+ return otherTitle.link.equals(this.link);
+ }
+
+ @Override
+ public int hashCode() {
+ return link.hashCode();
+ }
+}
diff --git a/src/main/java/seedu/address/model/article/Outlet.java b/src/main/java/seedu/address/model/article/Outlet.java
new file mode 100644
index 00000000000..57612452b14
--- /dev/null
+++ b/src/main/java/seedu/address/model/article/Outlet.java
@@ -0,0 +1,60 @@
+package seedu.address.model.article;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.commons.util.AppUtil.checkArgument;
+
+/**
+ * Represents the news outlet an Article is published by
+ */
+public class Outlet {
+ public static final String MESSAGE_CONSTRAINTS = "Outlet names should be alphanumeric";
+ public static final String VALIDATION_REGEX = "[\\p{Alnum} ]+";
+
+ public final String outletName;
+
+
+ /**
+ * Constructs a {@code outletName}.
+ *
+ * @param outletName A valid outlet name.
+ */
+ public Outlet(String outletName) {
+ requireNonNull(outletName);
+ checkArgument(isValidOutletName(outletName), MESSAGE_CONSTRAINTS);
+ this.outletName = outletName;
+ }
+
+ /**
+ * Returns true if a given string is a valid outlet name.
+ */
+ public static boolean isValidOutletName(String test) {
+ return test.matches(VALIDATION_REGEX);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof Outlet)) {
+ return false;
+ }
+
+ Outlet otherOutlet = (Outlet) other;
+ return outletName.equals(otherOutlet.outletName);
+ }
+
+ @Override
+ public int hashCode() {
+ return outletName.hashCode();
+ }
+
+ /**
+ * Format state as text for viewing.
+ */
+ public String toString() {
+ return '[' + outletName + ']';
+ }
+}
diff --git a/src/main/java/seedu/address/model/article/PublicationDate.java b/src/main/java/seedu/address/model/article/PublicationDate.java
new file mode 100644
index 00000000000..af416082ee1
--- /dev/null
+++ b/src/main/java/seedu/address/model/article/PublicationDate.java
@@ -0,0 +1,55 @@
+package seedu.address.model.article;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.logic.parser.ParserUtil.parseDateToString;
+
+import java.time.LocalDateTime;
+
+/**
+ * Represents an Article's publication date in the article book.
+ * Guarantees: immutable;
+ */
+public class PublicationDate implements Comparable {
+ public static final String MESSAGE_CONSTRAINTS =
+ "Date should be a valid date in the format of dd-MM-yyyy [HH:mm].";
+ public final LocalDateTime date;
+
+ /**
+ * Constructs a {@code PublicationDate}.
+ *
+ * @param publicationDate A valid publication date.
+ */
+ public PublicationDate(LocalDateTime publicationDate) {
+ requireNonNull(publicationDate);
+ this.date = publicationDate;
+ }
+
+ @Override
+ public String toString() {
+ return parseDateToString(date);
+ }
+
+ @Override
+ public int compareTo(PublicationDate other) {
+ return this.date.compareTo(other.date);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ if (!(other instanceof PublicationDate)) {
+ return false;
+ }
+
+ PublicationDate otherDate = (PublicationDate) other;
+ return otherDate.date.equals(this.date);
+ }
+
+ @Override
+ public int hashCode() {
+ return date.hashCode();
+ }
+}
diff --git a/src/main/java/seedu/address/model/article/Source.java b/src/main/java/seedu/address/model/article/Source.java
new file mode 100644
index 00000000000..ba6f0029280
--- /dev/null
+++ b/src/main/java/seedu/address/model/article/Source.java
@@ -0,0 +1,59 @@
+package seedu.address.model.article;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.commons.util.AppUtil.checkArgument;
+
+/**
+ * Represents a Source (Contributor) to an Article
+ */
+public class Source {
+ public static final String MESSAGE_CONSTRAINTS = "Interviewee names should be alphanumeric";
+ public static final String VALIDATION_REGEX = "[\\p{Alnum} ]+";
+
+ public final String sourceName;
+
+ /**
+ * Constructs a {@code sourceName}.
+ *
+ * @param sourceName A valid source name.
+ */
+ public Source(String sourceName) {
+ requireNonNull(sourceName);
+ checkArgument(isValidSourceName(sourceName), MESSAGE_CONSTRAINTS);
+ this.sourceName = sourceName;
+ }
+
+ /**
+ * Returns true if a given string is a valid source name.
+ */
+ public static boolean isValidSourceName(String test) {
+ return test.matches(VALIDATION_REGEX);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof Source)) {
+ return false;
+ }
+
+ Source otherSource = (Source) other;
+ return sourceName.equals(otherSource.sourceName);
+ }
+
+ @Override
+ public int hashCode() {
+ return sourceName.hashCode();
+ }
+
+ /**
+ * Format state as text for viewing.
+ */
+ public String toString() {
+ return '[' + sourceName + ']';
+ }
+}
diff --git a/src/main/java/seedu/address/model/article/Title.java b/src/main/java/seedu/address/model/article/Title.java
new file mode 100644
index 00000000000..134f3e0cbbb
--- /dev/null
+++ b/src/main/java/seedu/address/model/article/Title.java
@@ -0,0 +1,77 @@
+package seedu.address.model.article;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.commons.util.AppUtil.checkArgument;
+
+/**
+ * Represents an Article's title in the address book.
+ * Guarantees: immutable; is valid as declared in {@link #isValidTitle(String)}
+ */
+public class Title {
+ public static final String MESSAGE_CONSTRAINTS =
+ "Headlines should should not start with a whitespace character.";
+
+ public static final String VALIDATION_REGEX = "^(?!\\s).*";
+
+ public final String fullTitle;
+
+ /**
+ * Constructs a {@code Title}.
+ *
+ * @param title A valid title.
+ */
+ public Title(String title) {
+ requireNonNull(title);
+ checkArgument(isValidTitle(title), MESSAGE_CONSTRAINTS);
+ fullTitle = title;
+ }
+
+ /**
+ * Returns true if a given string is a valid title.
+ */
+ public static boolean isValidTitle(String test) {
+ return test.matches(VALIDATION_REGEX);
+ }
+
+ @Override
+ public String toString() {
+ return fullTitle;
+ }
+
+ private boolean areSameTitles(String[] title, String[] otherTitle) {
+ if (title.length != otherTitle.length) {
+ return false;
+ }
+ for (int i = 0; i < title.length; i++) {
+ if (!title[i].equalsIgnoreCase(otherTitle[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ if (!(other instanceof Title)) {
+ return false;
+ }
+
+ Title otherTitle = (Title) other;
+ String[] splitTitle = fullTitle.split("\\s+");
+ String[] splitOtherTitle = otherTitle.fullTitle.split("\\s+");
+
+ return areSameTitles(splitTitle, splitOtherTitle);
+ }
+
+ // It is safe to implement hashCode() this way because there are already precautions
+ // in place to ensure that no two Title instances can be kept in articles in the Article Book
+ // that are equal and yet have different hashcode values.
+ @Override
+ public int hashCode() {
+ return fullTitle.hashCode();
+ }
+}
diff --git a/src/main/java/seedu/address/model/article/TitleContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/article/TitleContainsKeywordsPredicate.java
new file mode 100644
index 00000000000..cffdf207952
--- /dev/null
+++ b/src/main/java/seedu/address/model/article/TitleContainsKeywordsPredicate.java
@@ -0,0 +1,44 @@
+package seedu.address.model.article;
+
+import java.util.List;
+import java.util.function.Predicate;
+
+import seedu.address.commons.util.StringUtil;
+import seedu.address.commons.util.ToStringBuilder;
+
+/**
+ * Tests that an {@code Article}'s {@code Title} matches any of the keywords given.
+ */
+public class TitleContainsKeywordsPredicate implements Predicate {
+ private final List keywords;
+
+ public TitleContainsKeywordsPredicate(List keywords) {
+ this.keywords = keywords;
+ }
+
+ @Override
+ public boolean test(Article article) {
+ return keywords.stream()
+ .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(article.getTitle().fullTitle, keyword));
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof TitleContainsKeywordsPredicate)) {
+ return false;
+ }
+
+ TitleContainsKeywordsPredicate otherTitleContainsKeywordsPredicate = (TitleContainsKeywordsPredicate) other;
+ return keywords.equals(otherTitleContainsKeywordsPredicate.keywords);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this).add("keywords", keywords).toString();
+ }
+}
diff --git a/src/main/java/seedu/address/model/article/UniqueArticleList.java b/src/main/java/seedu/address/model/article/UniqueArticleList.java
new file mode 100644
index 00000000000..66a90d61ea4
--- /dev/null
+++ b/src/main/java/seedu/address/model/article/UniqueArticleList.java
@@ -0,0 +1,165 @@
+package seedu.address.model.article;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.commons.util.CollectionUtil.requireAllNonNull;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE;
+
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import seedu.address.model.person.Person;
+
+/**
+ * A list of articles that are unique
+ */
+public class UniqueArticleList implements Iterable {
+
+ private final ObservableList internalList = FXCollections.observableArrayList();
+ private final ObservableList internalUnmodifiableList =
+ FXCollections.unmodifiableObservableList(internalList);
+
+ /**
+ * Returns true if the list contains an equivalent article as the given argument.
+ */
+ public boolean contains(Article toCheck) {
+ requireNonNull(toCheck);
+ return internalList.stream().anyMatch(toCheck::isSameArticle);
+ }
+
+ /**
+ * Adds an article to the list.
+ * The article must not already exist in the list.
+ */
+ public void add(Article toAdd) {
+ requireNonNull(toAdd);
+ internalList.add(toAdd);
+ }
+
+ /**
+ * Replaces the article {@code target} in the list with {@code editedArticle}.
+ * {@code target} must exist in the list.
+ */
+ public void setArticle(Article target, Article editedArticle) {
+ requireAllNonNull(target, editedArticle);
+
+ int index = internalList.indexOf(target);
+
+ internalList.set(index, editedArticle);
+ }
+
+ /**
+ * Removes the equivalent article from the list.
+ */
+ public void remove(Article toRemove) {
+ requireNonNull(toRemove);
+ internalList.remove(toRemove);
+ }
+
+ public void setArticles(UniqueArticleList replacement) {
+ requireNonNull(replacement);
+ internalList.setAll(replacement.internalList);
+ }
+
+ /**
+ * Replaces the contents of this list with {@code persons}.
+ * {@code persons} must not contain duplicate articles.
+ */
+ public void setArticles(List articles) {
+ requireAllNonNull(articles);
+ internalList.setAll(articles);
+ }
+
+ /**
+ * Sorts the list of articles by the attribute represented by the given prefix.
+ */
+ public void sortArticles(String prefix) {
+ requireNonNull(prefix);
+ if (PREFIX_DATE.getPrefix().equalsIgnoreCase(prefix)) {
+ // Sort by publication date and display most recent articles first.
+ internalList.sort(Comparator.comparing(Article::getPublicationDate, Comparator.reverseOrder()));
+ } else {
+ throw new IllegalArgumentException("Invalid prefix supplied.");
+ }
+ }
+
+ /**
+ * 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 UniqueArticleList)) {
+ return false;
+ }
+
+ UniqueArticleList otherUniqueArticleList = (UniqueArticleList) other;
+ return internalList.equals(otherUniqueArticleList.internalList);
+ }
+
+ @Override
+ public int hashCode() {
+ return internalList.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return internalList.toString();
+ }
+
+ /**
+ * Returns true if {@code articles} contains only unique persons.
+ */
+ private boolean articlesAreUnique(List articles) {
+ for (int i = 0; i < articles.size() - 1; i++) {
+ for (int j = i + 1; j < articles.size(); j++) {
+ if (articles.get(i).isSameArticle(articles.get(j))) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Makes links between articles and persons in the address book.
+ */
+ public void makeLinks(List uniquePersonList) {
+ for (Article article : internalList) {
+ article.makeLinks(uniquePersonList);
+ }
+ }
+
+ /**
+ * Makes links between articles and the given person.
+ */
+ public void makeLinkPerson(Person person) {
+ for (Article article : internalList) {
+ article.makeLink(person);
+ }
+ }
+
+ /**
+ * Reestablishes links between articles and the edited person.
+ */
+ public void setEditedPerson(Person target, Person editedPerson) {
+ for (Article article : internalList) {
+ article.updateNamesInArticle(target, editedPerson);
+ }
+ }
+}
diff --git a/src/main/java/seedu/address/model/article/exceptions/InvalidDatesException.java b/src/main/java/seedu/address/model/article/exceptions/InvalidDatesException.java
new file mode 100644
index 00000000000..eb2aac4e89e
--- /dev/null
+++ b/src/main/java/seedu/address/model/article/exceptions/InvalidDatesException.java
@@ -0,0 +1,10 @@
+package seedu.address.model.article.exceptions;
+
+/**
+ * Indicates that dates entered are invalid
+ */
+public class InvalidDatesException extends RuntimeException {
+ public InvalidDatesException() {
+ super("The start date should not come after end date");
+ }
+}
diff --git a/src/main/java/seedu/address/model/article/exceptions/InvalidStatusException.java b/src/main/java/seedu/address/model/article/exceptions/InvalidStatusException.java
new file mode 100644
index 00000000000..6717af05b86
--- /dev/null
+++ b/src/main/java/seedu/address/model/article/exceptions/InvalidStatusException.java
@@ -0,0 +1,10 @@
+package seedu.address.model.article.exceptions;
+
+/**
+ * Signals that status entered is not a valid one.
+ */
+public class InvalidStatusException extends RuntimeException {
+ public InvalidStatusException() {
+ super("The status is invalid");
+ }
+}
diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java
index 173f15b9b00..1a0a1ef1520 100644
--- a/src/main/java/seedu/address/model/person/Name.java
+++ b/src/main/java/seedu/address/model/person/Name.java
@@ -7,7 +7,7 @@
* Represents a Person's name in the address book.
* Guarantees: immutable; is valid as declared in {@link #isValidName(String)}
*/
-public class Name {
+public class Name implements Comparable {
public static final String MESSAGE_CONSTRAINTS =
"Names should only contain alphanumeric characters and spaces, and it should not be blank";
@@ -44,6 +44,23 @@ public String toString() {
return fullName;
}
+ @Override
+ public int compareTo(Name other) {
+ return this.fullName.compareTo(other.fullName);
+ }
+
+ private boolean areSameNames(String[] name, String[] otherName) {
+ if (name.length != otherName.length) {
+ return false;
+ }
+ for (int i = 0; i < name.length; i++) {
+ if (!name[i].equalsIgnoreCase(otherName[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
@Override
public boolean equals(Object other) {
if (other == this) {
@@ -56,9 +73,15 @@ public boolean equals(Object other) {
}
Name otherName = (Name) other;
- return fullName.equals(otherName.fullName);
+ String[] splitName = fullName.split("\\s+");
+ String[] splitOtherName = otherName.fullName.split("\\s+");
+
+ return areSameNames(splitName, splitOtherName);
}
+ // It is safe to implement hashCode() this way because there are already precautions
+ // in place to ensure that no two Name instances can be kept in persons in the Address Book
+ // that are equal and yet have different hashcode values.
@Override
public int hashCode() {
return fullName.hashCode();
diff --git a/src/main/java/seedu/address/model/person/NameWithinArticlePredicate.java b/src/main/java/seedu/address/model/person/NameWithinArticlePredicate.java
new file mode 100644
index 00000000000..af60394584a
--- /dev/null
+++ b/src/main/java/seedu/address/model/person/NameWithinArticlePredicate.java
@@ -0,0 +1,47 @@
+package seedu.address.model.person;
+
+import java.util.function.Predicate;
+
+import seedu.address.commons.util.ToStringBuilder;
+import seedu.address.model.article.Article;
+
+/**
+ * Tests that a {@code Person}'s {@code Name} is within the {@code Article}.
+ */
+public class NameWithinArticlePredicate implements Predicate {
+ private final Article article;
+
+ /**
+ * Constructs a {@code NameWithinArticlePredicate}.
+ *
+ * @param article The article to test against.
+ */
+ public NameWithinArticlePredicate(Article article) {
+ this.article = article;
+ }
+
+ @Override
+ public boolean test(Person person) {
+ return article.getPersons().contains(person);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof NameWithinArticlePredicate)) {
+ return false;
+ }
+
+ NameWithinArticlePredicate otherNameWithinArticlePredicate = (NameWithinArticlePredicate) other;
+ return article.equals(otherNameWithinArticlePredicate.article);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this).add("article", article).toString();
+ }
+}
diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java
index abe8c46b535..b711126df0b 100644
--- a/src/main/java/seedu/address/model/person/Person.java
+++ b/src/main/java/seedu/address/model/person/Person.java
@@ -2,12 +2,15 @@
import static seedu.address.commons.util.CollectionUtil.requireAllNonNull;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
+import java.util.List;
import java.util.Objects;
import java.util.Set;
import seedu.address.commons.util.ToStringBuilder;
+import seedu.address.model.article.Article;
import seedu.address.model.tag.Tag;
/**
@@ -25,6 +28,8 @@ public class Person {
private final Address address;
private final Set tags = new HashSet<>();
+ private List articles = new ArrayList<>();
+
/**
* Every field must be present and not null.
*/
@@ -61,6 +66,10 @@ public Set getTags() {
return Collections.unmodifiableSet(tags);
}
+ public List getArticles() {
+ return articles;
+ }
+
/**
* Returns true if both persons have the same name.
* This defines a weaker notion of equality between two persons.
@@ -114,4 +123,15 @@ public String toString() {
.toString();
}
+ public void addArticle(Article article) {
+ articles.add(article);
+ }
+
+ public void setArticles(Person person) {
+ this.articles = person.articles;
+ }
+
+ public String getNameString() {
+ return name.fullName;
+ }
}
diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java
index cc0a68d79f9..ae79cd8f60c 100644
--- a/src/main/java/seedu/address/model/person/UniquePersonList.java
+++ b/src/main/java/seedu/address/model/person/UniquePersonList.java
@@ -2,7 +2,9 @@
import static java.util.Objects.requireNonNull;
import static seedu.address.commons.util.CollectionUtil.requireAllNonNull;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME;
+import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
@@ -97,6 +99,18 @@ public void setPersons(List persons) {
internalList.setAll(persons);
}
+ /**
+ * Sorts the list of persons by the attribute represented by the given prefix.
+ */
+ public void sortPersons(String prefix) {
+ requireNonNull(prefix);
+ if (PREFIX_NAME.getPrefix().equalsIgnoreCase(prefix)) {
+ internalList.sort(Comparator.comparing(Person::getName));
+ } else {
+ throw new IllegalArgumentException("Invalid prefix supplied.");
+ }
+ }
+
/**
* Returns the backing list as an unmodifiable {@code ObservableList}.
*/
diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java
index f1a0d4e233b..ad0ffd35f00 100644
--- a/src/main/java/seedu/address/model/tag/Tag.java
+++ b/src/main/java/seedu/address/model/tag/Tag.java
@@ -10,7 +10,7 @@
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 VALIDATION_REGEX = "[\\p{Alnum} ]+";
public final String tagName;
diff --git a/src/main/java/seedu/address/model/util/SampleArticleDataUtil.java b/src/main/java/seedu/address/model/util/SampleArticleDataUtil.java
new file mode 100644
index 00000000000..42825790191
--- /dev/null
+++ b/src/main/java/seedu/address/model/util/SampleArticleDataUtil.java
@@ -0,0 +1,77 @@
+package seedu.address.model.util;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import seedu.address.model.ArticleBook;
+import seedu.address.model.ReadOnlyArticleBook;
+import seedu.address.model.article.Article;
+import seedu.address.model.article.Article.Status;
+import seedu.address.model.article.Author;
+import seedu.address.model.article.Link;
+import seedu.address.model.article.Outlet;
+import seedu.address.model.article.PublicationDate;
+import seedu.address.model.article.Source;
+import seedu.address.model.article.Title;
+import seedu.address.model.tag.Tag;
+
+/**
+ * Contains utility methods for populating {@code ArticleBook} with sample data.
+ */
+public class SampleArticleDataUtil {
+
+ public static Article[] getSampleArticles() {
+ return new Article[]{
+ new Article(new Title("The epitome of pain and suffering by NUS CS students."),
+ getAuthorSet("Alice", "Bob"),
+ getSourceSet("NUS Computing Club"), getTagSet("Student Life"), getOutletSet("SOC News Bulletin"),
+ new PublicationDate(LocalDateTime.now()), Status.PUBLISHED, new Link("https://www.google.com/"))
+ };
+ }
+
+ public static ReadOnlyArticleBook getSampleArticleBook() {
+ ArticleBook sampleAb = new ArticleBook();
+ for (Article sampleArticle : getSampleArticles()) {
+ sampleAb.addArticle(sampleArticle);
+ }
+ return sampleAb;
+ }
+
+ /**
+ * Returns an author set containing the list of strings given.
+ */
+ public static Set getAuthorSet(String... strings) {
+ return Arrays.stream(strings)
+ .map(Author::new)
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * Returns a source set containing the list of strings given.
+ */
+ public static Set getSourceSet(String... strings) {
+ return Arrays.stream(strings)
+ .map(Source::new)
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * Returns a tag set containing the list of strings given.
+ */
+ public static Set getTagSet(String... strings) {
+ return Arrays.stream(strings)
+ .map(Tag::new)
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * Returns an outlet set containing the list of strings given.
+ */
+ public static Set getOutletSet(String... strings) {
+ return Arrays.stream(strings)
+ .map(Outlet::new)
+ .collect(Collectors.toSet());
+ }
+}
diff --git a/src/main/java/seedu/address/storage/ArticleBookStorage.java b/src/main/java/seedu/address/storage/ArticleBookStorage.java
new file mode 100644
index 00000000000..6e122288ae6
--- /dev/null
+++ b/src/main/java/seedu/address/storage/ArticleBookStorage.java
@@ -0,0 +1,43 @@
+package seedu.address.storage;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Optional;
+
+import seedu.address.commons.exceptions.DataLoadingException;
+import seedu.address.model.ReadOnlyArticleBook;
+
+/**
+ * Represents a storage for {@link seedu.address.model.ArticleBook}.
+ */
+public interface ArticleBookStorage {
+ /**
+ * Returns the file path of the data file.
+ */
+ Path getArticleBookFilePath();
+
+ /**
+ * Returns ArticleBook data as a {@link ReadOnlyArticleBook}.
+ * Returns {@code Optional.empty()} if storage file is not found.
+ *
+ * @throws DataLoadingException if loading the data from storage failed.
+ */
+ Optional readArticleBook() throws DataLoadingException;
+
+ /**
+ * @see #getArticleBookFilePath()
+ */
+ Optional readArticleBook(Path filePath) throws DataLoadingException;
+
+ /**
+ * Saves the given {@link ReadOnlyArticleBook} to the storage.
+ * @param articleBook cannot be null.
+ * @throws IOException if there was any problem writing to the file.
+ */
+ void saveArticleBook(ReadOnlyArticleBook articleBook) throws IOException;
+
+ /**
+ * @see #saveArticleBook(ReadOnlyArticleBook)
+ */
+ void saveArticleBook(ReadOnlyArticleBook articleBook, Path filePath) throws IOException;
+}
diff --git a/src/main/java/seedu/address/storage/JsonAdaptedArticle.java b/src/main/java/seedu/address/storage/JsonAdaptedArticle.java
new file mode 100644
index 00000000000..d695073eb9d
--- /dev/null
+++ b/src/main/java/seedu/address/storage/JsonAdaptedArticle.java
@@ -0,0 +1,144 @@
+package seedu.address.storage;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.logic.parser.ParserUtil.parsePublicationDate;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import seedu.address.commons.exceptions.IllegalValueException;
+import seedu.address.model.article.Article;
+import seedu.address.model.article.Author;
+import seedu.address.model.article.Link;
+import seedu.address.model.article.Outlet;
+import seedu.address.model.article.PublicationDate;
+import seedu.address.model.article.Source;
+import seedu.address.model.article.Title;
+import seedu.address.model.tag.Tag;
+
+/**
+ * Jackson-friendly version of {@link Article}.
+ */
+public class JsonAdaptedArticle {
+ private final String title;
+ private final List authors = new ArrayList<>();
+ //Should be able to be null
+ private final String publicationDate;
+ private final List sources = new ArrayList<>();
+ private final List tags = new ArrayList<>();
+ private final List outlets = new ArrayList<>();
+ private final Article.Status status;
+ private final String link;
+
+ /**
+ * Construct a {@code JsonAdaptedArticle} with the given article details.
+ *
+ * @param title
+ * @param authors
+ * @param sources
+ * @param tags
+ * @param outlets
+ * @param publicationDate
+ * @param status
+ * @param link
+ */
+ @JsonCreator
+ public JsonAdaptedArticle(@JsonProperty("title") String title,
+ @JsonProperty("authors") List authors,
+ @JsonProperty("sources") List sources,
+ @JsonProperty("tags") List tags,
+ @JsonProperty("outlets") List outlets,
+ @JsonProperty("publicationDate") String publicationDate,
+ @JsonProperty("status") Article.Status status,
+ @JsonProperty("link") String link) {
+ this.title = title;
+ if (authors != null) {
+ this.authors.addAll(authors);
+ }
+ if (sources != null) {
+ this.sources.addAll(sources);
+ }
+ if (tags != null) {
+ this.tags.addAll(tags);
+ }
+ if (outlets != null) {
+ this.outlets.addAll(outlets);
+ }
+ this.publicationDate = publicationDate;
+ this.status = status;
+ this.link = link;
+ }
+ /**
+ * Construct a {@code JsonAdaptedArticle} with neccessary details
+ * @param sourceArticle
+ */
+ public JsonAdaptedArticle(Article sourceArticle) {
+ title = sourceArticle.getTitle().fullTitle;
+ authors.addAll(sourceArticle.getAuthors().stream()
+ .map(JsonAdaptedAuthor::new)
+ .collect(Collectors.toList()));
+ sources.addAll(sourceArticle.getSources().stream()
+ .map(JsonAdaptedSource::new)
+ .collect(Collectors.toList()));
+ tags.addAll(sourceArticle.getTags().stream()
+ .map(JsonAdaptedTag::new)
+ .collect(Collectors.toList()));
+ outlets.addAll(sourceArticle.getOutlets().stream()
+ .map(JsonAdaptedOutlet::new)
+ .collect(Collectors.toList()));
+ publicationDate = sourceArticle.getPublicationDate().toString();
+ status = sourceArticle.getStatus();
+ link = sourceArticle.getLink().link;
+ }
+
+ /**
+ * Convert this object into Model's object
+ * @return Model's object
+ * @throws IllegalValueException if data constraints are violated
+ */
+ public Article toModelType() throws IllegalValueException {
+ if (title == null) {
+ throw new IllegalValueException("The title is missing");
+ }
+ final Title modelTitle = new Title(title);
+ if (status == null) {
+ throw new IllegalValueException("The status is missing");
+ }
+
+ final List articleTags = new ArrayList<>();
+ for (JsonAdaptedTag tag : tags) {
+ articleTags.add(tag.toModelType());
+ }
+ final List articleAuthors = new ArrayList<>();
+ for (JsonAdaptedAuthor author : authors) {
+ articleAuthors.add(author.toModelType());
+ }
+ final List articleSources = new ArrayList<>();
+ for (JsonAdaptedSource source : sources) {
+ articleSources.add(source.toModelType());
+ }
+ final List articleOutlets = new ArrayList<>();
+ for (JsonAdaptedOutlet outlet : outlets) {
+ articleOutlets.add(outlet.toModelType());
+ }
+ requireNonNull(this.publicationDate);
+ final PublicationDate modelPublicationDate = parsePublicationDate(this.publicationDate);
+
+ final Set modelAuthors = new HashSet<>(articleAuthors);
+
+ final Set modelSources = new HashSet<>(articleSources);
+
+ final Set modelTags = new HashSet<>(articleTags);
+
+ final Set modelOutlets = new HashSet<>(articleOutlets);
+ final Link modelLink = new Link(link);
+ return new Article(modelTitle, modelAuthors, modelSources, modelTags,
+ modelOutlets, modelPublicationDate, status, modelLink);
+ }
+}
diff --git a/src/main/java/seedu/address/storage/JsonAdaptedAuthor.java b/src/main/java/seedu/address/storage/JsonAdaptedAuthor.java
new file mode 100644
index 00000000000..101c56f7fcd
--- /dev/null
+++ b/src/main/java/seedu/address/storage/JsonAdaptedAuthor.java
@@ -0,0 +1,46 @@
+package seedu.address.storage;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+import seedu.address.commons.exceptions.IllegalValueException;
+import seedu.address.model.article.Author;
+
+/**
+ * Jackson-friendly version of {@link Author}.
+ */
+public class JsonAdaptedAuthor {
+ private final String authorName;
+
+ /**
+ * Constructs a {@code JsonAdaptedAuthor} with the given {@code authorName}.
+ */
+ @JsonCreator
+ public JsonAdaptedAuthor(String authorName) {
+ this.authorName = authorName;
+ }
+
+ /**
+ * Converts a given {@code Author} into this class for Jackson use.
+ */
+ public JsonAdaptedAuthor(Author source) {
+ authorName = source.authorName;
+ }
+
+ @JsonValue
+ public String getAuthorName() {
+ return authorName;
+ }
+
+ /**
+ * Converts this Jackson-friendly adapted author object into the model's {@code Author} object.
+ *
+ * @throws IllegalValueException if there were any data constraints violated in the adapted author.
+ */
+ public Author toModelType() throws IllegalValueException {
+ if (!Author.isValidAuthorName(authorName)) {
+ throw new IllegalValueException(Author.MESSAGE_CONSTRAINTS);
+ }
+ return new Author(authorName);
+ }
+}
diff --git a/src/main/java/seedu/address/storage/JsonAdaptedOutlet.java b/src/main/java/seedu/address/storage/JsonAdaptedOutlet.java
new file mode 100644
index 00000000000..47ed8de72be
--- /dev/null
+++ b/src/main/java/seedu/address/storage/JsonAdaptedOutlet.java
@@ -0,0 +1,48 @@
+package seedu.address.storage;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+import seedu.address.commons.exceptions.IllegalValueException;
+import seedu.address.model.article.Outlet;
+
+/**
+ * Jackson-friendly version of {@link Outlet}.
+ */
+class JsonAdaptedOutlet {
+
+ private final String outletName;
+
+ /**
+ * Constructs a {@code JsonAdaptedTag} with the given {@code tagName}.
+ */
+ @JsonCreator
+ public JsonAdaptedOutlet(String outletName) {
+ this.outletName = outletName;
+ }
+
+ /**
+ * Converts a given {@code Tag} into this class for Jackson use.
+ */
+ public JsonAdaptedOutlet(Outlet source) {
+ outletName = source.outletName;
+ }
+
+ @JsonValue
+ public String getOutletName() {
+ return outletName;
+ }
+
+ /**
+ * Converts this Jackson-friendly adapted tag object into the model's {@code Outlet} object.
+ *
+ * @throws IllegalValueException if there were any data constraints violated in the adapted outlet.
+ */
+ public Outlet toModelType() throws IllegalValueException {
+ if (!Outlet.isValidOutletName(outletName)) {
+ throw new IllegalValueException(Outlet.MESSAGE_CONSTRAINTS);
+ }
+ return new Outlet(outletName);
+ }
+
+}
diff --git a/src/main/java/seedu/address/storage/JsonAdaptedSource.java b/src/main/java/seedu/address/storage/JsonAdaptedSource.java
new file mode 100644
index 00000000000..cc87e36834d
--- /dev/null
+++ b/src/main/java/seedu/address/storage/JsonAdaptedSource.java
@@ -0,0 +1,46 @@
+package seedu.address.storage;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+import seedu.address.commons.exceptions.IllegalValueException;
+import seedu.address.model.article.Source;
+
+/**
+ * Jackson-friendly version of {@link Source}.
+ */
+public class JsonAdaptedSource {
+ private final String sourceName;
+
+ /**
+ * Constructs a {@code JsonAdaptedSource} with the given {@code sourceName}.
+ */
+ @JsonCreator
+ public JsonAdaptedSource(String sourceName) {
+ this.sourceName = sourceName;
+ }
+
+ /**
+ * Converts a given {@code Source} into this class for Jackson use.
+ */
+ public JsonAdaptedSource(Source source) {
+ sourceName = source.sourceName;
+ }
+
+ @JsonValue
+ public String getSourceName() {
+ return sourceName;
+ }
+
+ /**
+ * Converts this Jackson-friendly adapted source object into the model's {@code Source} object.
+ *
+ * @throws IllegalValueException if there were any data constraints violated in the adapted source.
+ */
+ public Source toModelType() throws IllegalValueException {
+ if (!Source.isValidSourceName(sourceName)) {
+ throw new IllegalValueException(Source.MESSAGE_CONSTRAINTS);
+ }
+ return new Source(sourceName);
+ }
+}
diff --git a/src/main/java/seedu/address/storage/JsonArticleBookStorage.java b/src/main/java/seedu/address/storage/JsonArticleBookStorage.java
new file mode 100644
index 00000000000..7daa881e2b7
--- /dev/null
+++ b/src/main/java/seedu/address/storage/JsonArticleBookStorage.java
@@ -0,0 +1,78 @@
+package seedu.address.storage;
+
+import static java.util.Objects.requireNonNull;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.logging.Logger;
+
+import seedu.address.commons.core.LogsCenter;
+import seedu.address.commons.exceptions.DataLoadingException;
+import seedu.address.commons.exceptions.IllegalValueException;
+import seedu.address.commons.util.FileUtil;
+import seedu.address.commons.util.JsonUtil;
+import seedu.address.model.ReadOnlyArticleBook;
+
+/**
+ * A class to access ArticleBook data stored as a json file
+ */
+public class JsonArticleBookStorage implements ArticleBookStorage {
+ private static final Logger logger = LogsCenter.getLogger(JsonArticleBookStorage.class);
+
+ private Path filePath;
+
+ public JsonArticleBookStorage(Path filePath) {
+ this.filePath = filePath;
+ }
+
+ public Path getArticleBookFilePath() {
+ return filePath;
+ }
+
+ @Override
+ public Optional readArticleBook() throws DataLoadingException {
+ return readArticleBook(filePath);
+ }
+
+ /**
+ * Similar to {@link #readArticleBook()}.
+ *
+ * @param filePath location of the data. Cannot be null.
+ * @throws DataLoadingException if loading the data from storage failed.
+ */
+ public Optional readArticleBook(Path filePath) throws DataLoadingException {
+ requireNonNull(filePath);
+
+ Optional jsonArticleBook = JsonUtil.readJsonFile(
+ filePath, JsonSerializableArticleBook.class);
+ if (!jsonArticleBook.isPresent()) {
+ return Optional.empty();
+ }
+
+ try {
+ return Optional.of(jsonArticleBook.get().toModelType());
+ } catch (IllegalValueException ive) {
+ logger.info("Illegal values found in " + filePath + ": " + ive.getMessage());
+ throw new DataLoadingException(ive);
+ }
+ }
+
+ @Override
+ public void saveArticleBook(ReadOnlyArticleBook articleBook) throws IOException {
+ saveArticleBook(articleBook, filePath);
+ }
+
+ /**
+ * Similar to {@link #saveArticleBook(ReadOnlyArticleBook)}.
+ *
+ * @param filePath location of the data. Cannot be null.
+ */
+ public void saveArticleBook(ReadOnlyArticleBook articleBook, Path filePath) throws IOException {
+ requireNonNull(articleBook);
+ requireNonNull(filePath);
+
+ FileUtil.createIfMissing(filePath);
+ JsonUtil.saveJsonFile(new JsonSerializableArticleBook(articleBook), filePath);
+ }
+}
diff --git a/src/main/java/seedu/address/storage/JsonSerializableArticleBook.java b/src/main/java/seedu/address/storage/JsonSerializableArticleBook.java
new file mode 100644
index 00000000000..02e3cf1c26b
--- /dev/null
+++ b/src/main/java/seedu/address/storage/JsonSerializableArticleBook.java
@@ -0,0 +1,60 @@
+package seedu.address.storage;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonRootName;
+
+import seedu.address.commons.exceptions.IllegalValueException;
+import seedu.address.model.ArticleBook;
+import seedu.address.model.ReadOnlyArticleBook;
+import seedu.address.model.article.Article;
+
+/**
+ * An immutable ArticleBook that can be serialized into Json format
+ */
+@JsonRootName(value = "articlebook")
+public class JsonSerializableArticleBook {
+ public static final String MESSAGE_DUPLICATE_ARTICLE = "Articles list contains duplicate article(s).";
+
+ private final List articles = new ArrayList<>();
+
+ /**
+ * Constructs a {@code JsonSerializableArticleBook} with the given articles.
+ */
+ @JsonCreator
+ public JsonSerializableArticleBook(@JsonProperty("articles") List articles) {
+ this.articles.addAll(articles);
+ }
+
+ /**
+ * Converts a given {@code ReadOnlyArticleBook} into this class for Jackson use.
+ *
+ * @param source future changes to this will not affect the created {@code JsonSerializableArticleBook}.
+ */
+ public JsonSerializableArticleBook(ReadOnlyArticleBook source) {
+ articles.addAll(source.getArticleList().stream().map(JsonAdaptedArticle::new).collect(Collectors.toList()));
+ }
+
+
+
+ /**
+ * Converts this article book into the model's {@code ArticleBook} object.
+ *
+ * @throws IllegalValueException if there were any data constraints violated.
+ */
+ public ArticleBook toModelType() throws IllegalValueException {
+ ArticleBook articleBook = new ArticleBook();
+ for (JsonAdaptedArticle jsonAdaptedArticle : articles) {
+ Article article = jsonAdaptedArticle.toModelType();
+ if (articleBook.hasArticle(article)) {
+ throw new IllegalValueException(MESSAGE_DUPLICATE_ARTICLE);
+ }
+ articleBook.addArticle(article);
+ }
+ return articleBook;
+ }
+}
diff --git a/src/main/java/seedu/address/storage/Storage.java b/src/main/java/seedu/address/storage/Storage.java
index 9fba0c7a1d6..4a508cde06d 100644
--- a/src/main/java/seedu/address/storage/Storage.java
+++ b/src/main/java/seedu/address/storage/Storage.java
@@ -6,13 +6,14 @@
import seedu.address.commons.exceptions.DataLoadingException;
import seedu.address.model.ReadOnlyAddressBook;
+import seedu.address.model.ReadOnlyArticleBook;
import seedu.address.model.ReadOnlyUserPrefs;
import seedu.address.model.UserPrefs;
/**
* API of the Storage component
*/
-public interface Storage extends AddressBookStorage, UserPrefsStorage {
+public interface Storage extends AddressBookStorage, ArticleBookStorage, UserPrefsStorage {
@Override
Optional readUserPrefs() throws DataLoadingException;
@@ -20,6 +21,8 @@ public interface Storage extends AddressBookStorage, UserPrefsStorage {
@Override
void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException;
+ // ================ AddressBook methods ==============================
+
@Override
Path getAddressBookFilePath();
@@ -29,4 +32,14 @@ public interface Storage extends AddressBookStorage, UserPrefsStorage {
@Override
void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException;
+ // ================ ArticleBook methods ==============================
+ @Override
+ Path getArticleBookFilePath();
+
+ @Override
+ Optional readArticleBook() throws DataLoadingException;
+
+ @Override
+ void saveArticleBook(ReadOnlyArticleBook articleBook) throws IOException;
+
}
diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/address/storage/StorageManager.java
index 8b84a9024d5..3babcca7b65 100644
--- a/src/main/java/seedu/address/storage/StorageManager.java
+++ b/src/main/java/seedu/address/storage/StorageManager.java
@@ -8,23 +8,28 @@
import seedu.address.commons.core.LogsCenter;
import seedu.address.commons.exceptions.DataLoadingException;
import seedu.address.model.ReadOnlyAddressBook;
+import seedu.address.model.ReadOnlyArticleBook;
import seedu.address.model.ReadOnlyUserPrefs;
import seedu.address.model.UserPrefs;
/**
- * Manages storage of AddressBook data in local storage.
+ * Manages storage of AddressBook and ArticleBook data in local storage.
*/
public class StorageManager implements Storage {
private static final Logger logger = LogsCenter.getLogger(StorageManager.class);
private AddressBookStorage addressBookStorage;
+ private ArticleBookStorage articleBookStorage;
private UserPrefsStorage userPrefsStorage;
/**
- * Creates a {@code StorageManager} with the given {@code AddressBookStorage} and {@code UserPrefStorage}.
+ * Creates a {@code StorageManager} with the given {@code AddressBookStorage} and {@code UserPrefStorage}
+ * and {@code ArticleBookStorage}.
*/
- public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage) {
+ public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage,
+ ArticleBookStorage articleBookStorage) {
this.addressBookStorage = addressBookStorage;
+ this.articleBookStorage = articleBookStorage;
this.userPrefsStorage = userPrefsStorage;
}
@@ -75,4 +80,39 @@ public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) thro
addressBookStorage.saveAddressBook(addressBook, filePath);
}
+ @Override
+ public Path getArticleBookFilePath() {
+ return articleBookStorage.getArticleBookFilePath();
+ }
+ public Optional readArticleBook() throws DataLoadingException {
+ return readArticleBook(articleBookStorage.getArticleBookFilePath());
+ }
+
+ /**
+ * @see #getArticleBookFilePath()
+ */
+ @Override
+ public Optional readArticleBook(Path filePath) throws DataLoadingException {
+ logger.fine("Attempting to read data from file: " + filePath);
+ return articleBookStorage.readArticleBook(filePath);
+ }
+
+ /**
+ * Saves the given {@link ReadOnlyArticleBook} to the storage.
+ * @param articleBook cannot be null.
+ * @throws IOException if there was any problem writing to the file.
+ */
+ @Override
+ public void saveArticleBook(ReadOnlyArticleBook articleBook) throws IOException {
+ saveArticleBook(articleBook, articleBookStorage.getArticleBookFilePath());
+ }
+
+ /**
+ * @see #saveArticleBook(ReadOnlyArticleBook)
+ */
+ @Override
+ public void saveArticleBook(ReadOnlyArticleBook articleBook, Path filePath) throws IOException {
+ logger.fine("Attempting to write to data file: " + filePath);
+ articleBookStorage.saveArticleBook(articleBook, filePath);
+ }
}
diff --git a/src/main/java/seedu/address/ui/ArticleCard.java b/src/main/java/seedu/address/ui/ArticleCard.java
new file mode 100644
index 00000000000..6b939a33aeb
--- /dev/null
+++ b/src/main/java/seedu/address/ui/ArticleCard.java
@@ -0,0 +1,117 @@
+package seedu.address.ui;
+
+
+import java.awt.Desktop;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Comparator;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.Hyperlink;
+import javafx.scene.control.Label;
+import javafx.scene.layout.FlowPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Region;
+import seedu.address.model.article.Article;
+
+/**
+ * A UI component that displays information of an {@code Article}.
+ */
+public class ArticleCard extends UiPart {
+
+ private static final String FXML = "ArticleListCard.fxml";
+
+ /**
+ * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX.
+ * As a consequence, UI elements' variable names cannot be set to such keywords
+ * or an exception will be thrown by JavaFX during runtime.
+ *
+ * @see The issue on AddressBook level 4
+ */
+
+ public final Article article;
+
+ @FXML
+ private HBox cardPane;
+ @FXML
+ private Label title;
+ @FXML
+ private Label id;
+ @FXML
+ private FlowPane authors;
+ @FXML
+ private FlowPane sources;
+ @FXML
+ private FlowPane tags;
+ @FXML
+ private FlowPane outlets;
+ @FXML
+ private Label publicationDate;
+ @FXML
+ private Label status;
+ @FXML
+ private Hyperlink hyperlink;
+
+
+ /**
+ * Creates a {@code ArticleCode} with the given {@code Article} and index to display.
+ */
+ public ArticleCard(Article article, int displayedIndex) {
+ super(FXML);
+ this.article = article;
+ id.setText(displayedIndex + ". ");
+ title.setText(article.getTitle().fullTitle);
+
+ article.getAuthors().stream()
+ .sorted(Comparator.comparing(author -> author.authorName))
+ .forEach(author -> authors.getChildren().add(new Label(author.authorName)));
+ article.getSources().stream()
+ .sorted(Comparator.comparing(source -> source.sourceName))
+ .forEach(source -> sources.getChildren().add(new Label(source.sourceName)));
+ article.getTags().stream()
+ .sorted(Comparator.comparing(tag -> tag.tagName))
+ .forEach(tag -> tags.getChildren().add(new Label(tag.tagName)));
+ article.getOutlets().stream()
+ .sorted(Comparator.comparing(outlet -> outlet.outletName))
+ .forEach(outlet -> outlets.getChildren().add(new Label(outlet.outletName)));
+
+ setPadding(authors);
+ setPadding(sources);
+ setPadding(tags);
+ setPadding(outlets);
+ publicationDate.setText(article.getPublicationDateAsString());
+ status.setText(article.getStatus().toString());
+
+ String link = article.getLink().link;
+ if (!link.isEmpty()) {
+ hyperlink.setText("Link");
+ hyperlink.setOnAction(event -> {
+ openBrowser(link);
+ });
+ } else {
+ hyperlink.setVisible(false);
+ hyperlink.setManaged(false);
+ }
+ }
+
+ private void openBrowser(String link) {
+ if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
+ try {
+ Desktop.getDesktop().browse(new URI(link));
+ } catch (IOException | URISyntaxException e) {
+ e.printStackTrace();
+ }
+ } else {
+ System.out.println("Desktop not supported, cannot open browser.");
+ }
+ }
+
+ private void setPadding(FlowPane flowPane) {
+ if (flowPane.getChildren().isEmpty()) {
+ flowPane.setPadding(new javafx.geometry.Insets(0, 0, 0, 0));
+ } else {
+ flowPane.setPadding(new javafx.geometry.Insets(0, 0, 3, 0));
+ }
+ }
+}
diff --git a/src/main/java/seedu/address/ui/ArticleListPanel.java b/src/main/java/seedu/address/ui/ArticleListPanel.java
new file mode 100644
index 00000000000..067e072787b
--- /dev/null
+++ b/src/main/java/seedu/address/ui/ArticleListPanel.java
@@ -0,0 +1,49 @@
+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.article.Article;
+
+/**
+ * Panel containing the list of articles.
+ */
+public class ArticleListPanel extends UiPart {
+ private static final String FXML = "ArticleListPanel.fxml";
+ private final Logger logger = LogsCenter.getLogger(ArticleListPanel.class);
+
+ @FXML
+ private ListView articleListView;
+
+ /**
+ * Creates a {@code ArticleListPanel} with the given {@code ObservableList}.
+ */
+ public ArticleListPanel(ObservableList articleList) {
+ super(FXML);
+ articleListView.setItems(articleList);
+ articleListView.setCellFactory(listView -> new ArticleListViewCell());
+ }
+
+ /**
+ * Custom {@code ListCell} that displays the graphics of a {@code Article} using a {@code ArticleCard}.
+ */
+ class ArticleListViewCell extends ListCell {
+ @Override
+ protected void updateItem(Article article, boolean empty) {
+ super.updateItem(article, empty);
+
+ if (empty || article == null) {
+ setGraphic(null);
+ setText(null);
+ } else {
+ setGraphic(new ArticleCard(article, getIndex() + 1).getRoot());
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java
index 3f16b2fcf26..cfdd5a0c7d0 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://ay2324s2-cs2103t-f12-2.github.io/tp/UserGuide.html";
public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL;
private static final Logger logger = LogsCenter.getLogger(HelpWindow.class);
diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java
index 79e74ef37c0..0bcda7565ab 100644
--- a/src/main/java/seedu/address/ui/MainWindow.java
+++ b/src/main/java/seedu/address/ui/MainWindow.java
@@ -8,6 +8,7 @@
import javafx.scene.control.TextInputControl;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
+import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import seedu.address.commons.core.GuiSettings;
@@ -31,7 +32,9 @@ public class MainWindow extends UiPart {
private Logic logic;
// Independent Ui parts residing in this Ui container
+ private GridPane listPanelContainer;
private PersonListPanel personListPanel;
+ private ArticleListPanel articleListPanel;
private ResultDisplay resultDisplay;
private HelpWindow helpWindow;
@@ -43,6 +46,8 @@ public class MainWindow extends UiPart {
@FXML
private StackPane personListPanelPlaceholder;
+ @FXML
+ private StackPane articleListPanelPlaceholder;
@FXML
private StackPane resultDisplayPlaceholder;
@@ -113,6 +118,9 @@ void fillInnerParts() {
personListPanel = new PersonListPanel(logic.getFilteredPersonList());
personListPanelPlaceholder.getChildren().add(personListPanel.getRoot());
+ articleListPanel = new ArticleListPanel(logic.getFilteredArticleList());
+ articleListPanelPlaceholder.getChildren().add(articleListPanel.getRoot());
+
resultDisplay = new ResultDisplay();
resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot());
@@ -185,7 +193,21 @@ private CommandResult executeCommand(String commandText) throws CommandException
if (commandResult.isExit()) {
handleExit();
}
+ if (logic.getCommandType(commandText).equals("articleCommand")) {
+ // Initialize articleListPanel if not already initialized
+ if (articleListPanel == null) {
+ articleListPanel = new ArticleListPanel(logic.getFilteredArticleList());
+ articleListPanelPlaceholder.getChildren().add(articleListPanel.getRoot());
+ }
+
+ } else if (logic.getCommandType(commandText).equals("personCommand")) {
+ // Initialize personListPanel if not already initialized
+ if (personListPanel == null) {
+ personListPanel = new PersonListPanel(logic.getFilteredPersonList());
+ personListPanelPlaceholder.getChildren().add(personListPanel.getRoot());
+ }
+ }
return commandResult;
} catch (CommandException | ParseException e) {
logger.info("An error occurred while executing command: " + commandText);
diff --git a/src/main/resources/view/ArticleListCard.fxml b/src/main/resources/view/ArticleListCard.fxml
new file mode 100644
index 00000000000..ce285c40b03
--- /dev/null
+++ b/src/main/resources/view/ArticleListCard.fxml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/ArticleListPanel.fxml b/src/main/resources/view/ArticleListPanel.fxml
new file mode 100644
index 00000000000..a4ffb2ce2b4
--- /dev/null
+++ b/src/main/resources/view/ArticleListPanel.fxml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css
index 36e6b001cd8..e4feabcb263 100644
--- a/src/main/resources/view/DarkTheme.css
+++ b/src/main/resources/view/DarkTheme.css
@@ -350,3 +350,45 @@
-fx-background-radius: 2;
-fx-font-size: 11;
}
+
+#authors {
+ -fx-hgap: 7;
+ -fx-vgap: 3;
+}
+
+#authors .label {
+ -fx-text-fill: white;
+ -fx-background-color: #0B3142;
+ -fx-padding: 1 3 1 3;
+ -fx-border-radius: 2;
+ -fx-background-radius: 2;
+ -fx-font-size: 11;
+}
+
+#sources {
+ -fx-hgap: 7;
+ -fx-vgap: 3;
+}
+
+#sources .label {
+ -fx-text-fill: white;
+ -fx-background-color: #0F5257;
+ -fx-padding: 1 3 1 3;
+ -fx-border-radius: 2;
+ -fx-background-radius: 2;
+ -fx-font-size: 11;
+}
+
+#outlets {
+ -fx-hgap: 7;
+ -fx-vgap: 3;
+}
+
+#outlets .label {
+ -fx-text-fill: white;
+ -fx-background-color: #933d46;
+ -fx-padding: 1 3 1 3;
+ -fx-border-radius: 2;
+ -fx-background-radius: 2;
+ -fx-font-size: 11;
+}
diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml
index 7778f666a0a..f77d4c31bc6 100644
--- a/src/main/resources/view/MainWindow.fxml
+++ b/src/main/resources/view/MainWindow.fxml
@@ -6,13 +6,15 @@
-
+
+
+
+
-
+
@@ -33,26 +35,39 @@
-
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/java/seedu/address/logic/LogicManagerTest.java b/src/test/java/seedu/address/logic/LogicManagerTest.java
index baf8ce336a2..756490652ec 100644
--- a/src/test/java/seedu/address/logic/LogicManagerTest.java
+++ b/src/test/java/seedu/address/logic/LogicManagerTest.java
@@ -23,12 +23,14 @@
import seedu.address.logic.commands.ListCommand;
import seedu.address.logic.commands.exceptions.CommandException;
import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.ArticleBook;
import seedu.address.model.Model;
import seedu.address.model.ModelManager;
import seedu.address.model.ReadOnlyAddressBook;
import seedu.address.model.UserPrefs;
import seedu.address.model.person.Person;
import seedu.address.storage.JsonAddressBookStorage;
+import seedu.address.storage.JsonArticleBookStorage;
import seedu.address.storage.JsonUserPrefsStorage;
import seedu.address.storage.StorageManager;
import seedu.address.testutil.PersonBuilder;
@@ -48,7 +50,9 @@ public void setUp() {
JsonAddressBookStorage addressBookStorage =
new JsonAddressBookStorage(temporaryFolder.resolve("addressBook.json"));
JsonUserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(temporaryFolder.resolve("userPrefs.json"));
- StorageManager storage = new StorageManager(addressBookStorage, userPrefsStorage);
+ JsonArticleBookStorage articleBookStorage =
+ new JsonArticleBookStorage(temporaryFolder.resolve("articleBook.json"));
+ StorageManager storage = new StorageManager(addressBookStorage, userPrefsStorage, articleBookStorage);
logic = new LogicManager(model, storage);
}
@@ -123,7 +127,7 @@ private void assertCommandException(String inputCommand, String expectedMessage)
*/
private void assertCommandFailure(String inputCommand, Class extends Throwable> expectedException,
String expectedMessage) {
- Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs());
+ Model expectedModel = new ModelManager(model.getAddressBook(), new ArticleBook(), new UserPrefs());
assertCommandFailure(inputCommand, expectedException, expectedMessage, expectedModel);
}
@@ -160,7 +164,9 @@ public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath)
JsonUserPrefsStorage userPrefsStorage =
new JsonUserPrefsStorage(temporaryFolder.resolve("ExceptionUserPrefs.json"));
- StorageManager storage = new StorageManager(addressBookStorage, userPrefsStorage);
+ JsonArticleBookStorage articleBookStorage =
+ new JsonArticleBookStorage(temporaryFolder.resolve("articleBook.json"));
+ StorageManager storage = new StorageManager(addressBookStorage, userPrefsStorage, articleBookStorage);
logic = new LogicManager(model, storage);
diff --git a/src/test/java/seedu/address/logic/commands/AddCommandIntegrationTest.java b/src/test/java/seedu/address/logic/commands/AddCommandIntegrationTest.java
index 162a0c86031..a4e20abd2f5 100644
--- a/src/test/java/seedu/address/logic/commands/AddCommandIntegrationTest.java
+++ b/src/test/java/seedu/address/logic/commands/AddCommandIntegrationTest.java
@@ -8,6 +8,7 @@
import org.junit.jupiter.api.Test;
import seedu.address.logic.Messages;
+import seedu.address.model.ArticleBook;
import seedu.address.model.Model;
import seedu.address.model.ModelManager;
import seedu.address.model.UserPrefs;
@@ -23,14 +24,14 @@ public class AddCommandIntegrationTest {
@BeforeEach
public void setUp() {
- model = new ModelManager(getTypicalAddressBook(), new UserPrefs());
+ model = new ModelManager(getTypicalAddressBook(), new ArticleBook(), new UserPrefs());
}
@Test
public void execute_newPerson_success() {
Person validPerson = new PersonBuilder().build();
- Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs());
+ Model expectedModel = new ModelManager(model.getAddressBook(), new ArticleBook(), new UserPrefs());
expectedModel.addPerson(validPerson);
assertCommandSuccess(new AddCommand(validPerson), model,
diff --git a/src/test/java/seedu/address/logic/commands/AddCommandTest.java b/src/test/java/seedu/address/logic/commands/AddCommandTest.java
index 90e8253f48e..5beffdfef72 100644
--- a/src/test/java/seedu/address/logic/commands/AddCommandTest.java
+++ b/src/test/java/seedu/address/logic/commands/AddCommandTest.java
@@ -19,9 +19,12 @@
import seedu.address.logic.Messages;
import seedu.address.logic.commands.exceptions.CommandException;
import seedu.address.model.AddressBook;
+import seedu.address.model.ArticleFilter;
import seedu.address.model.Model;
import seedu.address.model.ReadOnlyAddressBook;
+import seedu.address.model.ReadOnlyArticleBook;
import seedu.address.model.ReadOnlyUserPrefs;
+import seedu.address.model.article.Article;
import seedu.address.model.person.Person;
import seedu.address.testutil.PersonBuilder;
@@ -148,6 +151,11 @@ public void setPerson(Person target, Person editedPerson) {
throw new AssertionError("This method should not be called.");
}
+ @Override
+ public void sortAddressBook(String prefix) {
+ throw new AssertionError("This method should not be called.");
+ }
+
@Override
public ObservableList getFilteredPersonList() {
throw new AssertionError("This method should not be called.");
@@ -157,6 +165,65 @@ public ObservableList getFilteredPersonList() {
public void updateFilteredPersonList(Predicate predicate) {
throw new AssertionError("This method should not be called.");
}
+
+ @Override
+ public void setArticleBook(ReadOnlyArticleBook newData) {
+ throw new AssertionError("This method should not be called.");
+ }
+
+ @Override
+ public ReadOnlyArticleBook getArticleBook() {
+ throw new AssertionError("This method should not be called.");
+ }
+
+ @Override
+ public void updateFilteredArticleList(Predicate predicate) {
+ throw new AssertionError("This method should not be called.");
+ }
+
+ @Override
+ public ObservableList getFilteredArticleList() {
+ throw new AssertionError("This method should not be called.");
+ }
+
+ @Override
+ public void deleteArticle(Article target) {
+ throw new AssertionError("This method should not be called.");
+ }
+
+ @Override
+ public void addArticle(Article article) {
+ throw new AssertionError("This method should not be called.");
+ }
+
+ @Override
+ public boolean hasArticle(Article article) {
+ throw new AssertionError("This method should not be called.");
+ }
+
+ @Override
+ public void setArticle(Article target, Article editedArticle) {
+ throw new AssertionError("This method should not be called.");
+ }
+
+ @Override
+ public void sortArticleBook(String prefix) {
+ throw new AssertionError("This method should not be called.");
+ }
+ @Override
+ public ArticleFilter getFilter() {
+ throw new AssertionError("This method should not be called.");
+ }
+
+ @Override
+ public void lookupArticle(Article articleToLookup) {
+ throw new AssertionError("This method should not be called.");
+ }
+
+ @Override
+ public void lookupPerson(Person personToLookup) {
+ throw new AssertionError("This method should not be called.");
+ }
}
/**
diff --git a/src/test/java/seedu/address/logic/commands/ClearCommandTest.java b/src/test/java/seedu/address/logic/commands/ClearCommandTest.java
index 80d9110c03a..74c67d65dad 100644
--- a/src/test/java/seedu/address/logic/commands/ClearCommandTest.java
+++ b/src/test/java/seedu/address/logic/commands/ClearCommandTest.java
@@ -6,6 +6,7 @@
import org.junit.jupiter.api.Test;
import seedu.address.model.AddressBook;
+import seedu.address.model.ArticleBook;
import seedu.address.model.Model;
import seedu.address.model.ModelManager;
import seedu.address.model.UserPrefs;
@@ -22,8 +23,8 @@ public void execute_emptyAddressBook_success() {
@Test
public void execute_nonEmptyAddressBook_success() {
- Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs());
- Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs());
+ Model model = new ModelManager(getTypicalAddressBook(), new ArticleBook(), new UserPrefs());
+ Model expectedModel = new ModelManager(getTypicalAddressBook(), new ArticleBook(), new UserPrefs());
expectedModel.setAddressBook(new AddressBook());
assertCommandSuccess(new ClearCommand(), model, ClearCommand.MESSAGE_SUCCESS, expectedModel);
diff --git a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java
index 643a1d08069..b9b335e29df 100644
--- a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java
+++ b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java
@@ -22,7 +22,7 @@
import seedu.address.testutil.EditPersonDescriptorBuilder;
/**
- * Contains helper methods for testing commands.
+ * Contains helper methods for testing person commands.
*/
public class CommandTestUtil {
@@ -74,8 +74,8 @@ public class CommandTestUtil {
* - the returned {@link CommandResult} matches {@code expectedCommandResult}
* - the {@code actualModel} matches {@code expectedModel}
*/
- public static void assertCommandSuccess(Command command, Model actualModel, CommandResult expectedCommandResult,
- Model expectedModel) {
+ public static void assertCommandSuccess(PersonCommand command, Model actualModel,
+ CommandResult expectedCommandResult, Model expectedModel) {
try {
CommandResult result = command.execute(actualModel);
assertEquals(expectedCommandResult, result);
@@ -86,10 +86,10 @@ public static void assertCommandSuccess(Command command, Model actualModel, Comm
}
/**
- * Convenience wrapper to {@link #assertCommandSuccess(Command, Model, CommandResult, Model)}
+ * Convenience wrapper to {@link #assertCommandSuccess(PersonCommand, Model, CommandResult, Model)}
* that takes a string {@code expectedMessage}.
*/
- public static void assertCommandSuccess(Command command, Model actualModel, String expectedMessage,
+ public static void assertCommandSuccess(PersonCommand command, Model actualModel, String expectedMessage,
Model expectedModel) {
CommandResult expectedCommandResult = new CommandResult(expectedMessage);
assertCommandSuccess(command, actualModel, expectedCommandResult, expectedModel);
@@ -101,7 +101,7 @@ public static void assertCommandSuccess(Command command, Model actualModel, Stri
* - the CommandException message matches {@code expectedMessage}
* - the address book, filtered person list and selected person in {@code actualModel} remain unchanged
*/
- public static void assertCommandFailure(Command command, Model actualModel, String expectedMessage) {
+ public static void assertCommandFailure(PersonCommand command, Model actualModel, String expectedMessage) {
// we are unable to defensively copy the model for comparison later, so we can
// only do so by copying its components.
AddressBook expectedAddressBook = new AddressBook(actualModel.getAddressBook());
@@ -111,6 +111,7 @@ public static void assertCommandFailure(Command command, Model actualModel, Stri
assertEquals(expectedAddressBook, actualModel.getAddressBook());
assertEquals(expectedFilteredList, actualModel.getFilteredPersonList());
}
+
/**
* Updates {@code model}'s filtered list to show only the person at the given {@code targetIndex} in the
* {@code model}'s address book.
@@ -125,4 +126,26 @@ public static void showPersonAtIndex(Model model, Index targetIndex) {
assertEquals(1, model.getFilteredPersonList().size());
}
+ /**
+ * Updates {@code model}'s filtered list to show only the persons within the range {@code startIndex-endIndex}
+ * in the {@code model}'s address book.
+ */
+ public static void showPersonInRange(Model model, Index startIndex, Index endIndex) {
+ assertTrue(0 <= startIndex.getZeroBased());
+ assertTrue(startIndex.getZeroBased() < model.getFilteredPersonList().size());
+ assertTrue(endIndex.getZeroBased() < model.getFilteredPersonList().size());
+ assertTrue(startIndex.getZeroBased() <= endIndex.getZeroBased());
+
+ ArrayList personFirstNames = new ArrayList<>();
+
+ for (int i = startIndex.getZeroBased(); i <= endIndex.getZeroBased(); i++) {
+ Person person = model.getFilteredPersonList().get(i);
+ final String[] splitName = person.getName().fullName.split("\\s+");
+ personFirstNames.add(splitName[0]);
+ }
+
+ model.updateFilteredPersonList(new NameContainsKeywordsPredicate(personFirstNames));
+
+ assertEquals(endIndex.getOneBased() - startIndex.getZeroBased(), model.getFilteredPersonList().size());
+ }
}
diff --git a/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java
index b6f332eabca..e176c6d51da 100644
--- a/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java
+++ b/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java
@@ -14,6 +14,7 @@
import seedu.address.commons.core.index.Index;
import seedu.address.logic.Messages;
+import seedu.address.model.ArticleBook;
import seedu.address.model.Model;
import seedu.address.model.ModelManager;
import seedu.address.model.UserPrefs;
@@ -25,7 +26,7 @@
*/
public class DeleteCommandTest {
- private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs());
+ private Model model = new ModelManager(getTypicalAddressBook(), new ArticleBook(), new UserPrefs());
@Test
public void execute_validIndexUnfilteredList_success() {
@@ -35,7 +36,7 @@ public void execute_validIndexUnfilteredList_success() {
String expectedMessage = String.format(DeleteCommand.MESSAGE_DELETE_PERSON_SUCCESS,
Messages.format(personToDelete));
- ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs());
+ ModelManager expectedModel = new ModelManager(model.getAddressBook(), new ArticleBook(), new UserPrefs());
expectedModel.deletePerson(personToDelete);
assertCommandSuccess(deleteCommand, model, expectedMessage, expectedModel);
@@ -59,7 +60,7 @@ public void execute_validIndexFilteredList_success() {
String expectedMessage = String.format(DeleteCommand.MESSAGE_DELETE_PERSON_SUCCESS,
Messages.format(personToDelete));
- Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs());
+ Model expectedModel = new ModelManager(model.getAddressBook(), new ArticleBook(), new UserPrefs());
expectedModel.deletePerson(personToDelete);
showNoPerson(expectedModel);
diff --git a/src/test/java/seedu/address/logic/commands/EditArticleCommandTest.java b/src/test/java/seedu/address/logic/commands/EditArticleCommandTest.java
new file mode 100644
index 00000000000..7d9ed141d7f
--- /dev/null
+++ b/src/test/java/seedu/address/logic/commands/EditArticleCommandTest.java
@@ -0,0 +1,153 @@
+package seedu.address.logic.commands;
+
+import static seedu.address.logic.commands.articlecommands.ArticleCommandTestUtil.assertCommandSuccess;
+import static seedu.address.logic.commands.articlecommands.ArticleCommandTestUtil.showArticleAtIndex;
+import static seedu.address.testutil.TypicalArticles.getTypicalArticleBook;
+import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_ARTICLE;
+import static seedu.address.testutil.TypicalIndexes.INDEX_SIXTH_ARTICLE;
+import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.logic.Messages;
+import seedu.address.logic.commands.articlecommands.ArticleCommandTestUtil;
+import seedu.address.logic.commands.articlecommands.EditArticleCommand;
+import seedu.address.logic.commands.articlecommands.EditArticleCommand.EditArticleDescriptor;
+import seedu.address.model.AddressBook;
+import seedu.address.model.ArticleBook;
+import seedu.address.model.Model;
+import seedu.address.model.ModelManager;
+import seedu.address.model.UserPrefs;
+import seedu.address.model.article.Article;
+import seedu.address.testutil.ArticleBuilder;
+import seedu.address.testutil.EditArticleDescriptorBuilder;
+
+/**
+ * Contains integration tests (interaction with the Model) and unit tests for EditArticleCommand.
+ */
+public class EditArticleCommandTest {
+
+ private Model model = new ModelManager(getTypicalAddressBook(), getTypicalArticleBook() , new UserPrefs());
+
+ @Test
+ public void execute_allFieldsSpecifiedUnfilteredList_success() {
+ Article editedArticle = new ArticleBuilder().build();
+ EditArticleDescriptor descriptor = new EditArticleDescriptorBuilder(editedArticle).build();
+ EditArticleCommand editArticleCommand = new EditArticleCommand(INDEX_FIRST_ARTICLE, descriptor);
+
+ String expectedMessage = String.format(EditArticleCommand.MESSAGE_EDIT_ARTICLE_SUCCESS,
+ Messages.format(editedArticle));
+
+ Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()),
+ new ArticleBook(model.getArticleBook()), new UserPrefs());
+ expectedModel.setArticle(model.getFilteredArticleList().get(0), editedArticle);
+
+ assertCommandSuccess(editArticleCommand, model, expectedMessage, expectedModel);
+ }
+
+ @Test
+ public void execute_someFieldsSpecifiedUnfilteredList_success() {
+ Article lastArticle = model.getFilteredArticleList().get(model.getFilteredArticleList().size() - 1);
+
+ ArticleBuilder articleInList = new ArticleBuilder(lastArticle);
+ Article editedArticle = articleInList.withTitle("Seven upon a time").build();
+
+ EditArticleDescriptor descriptor = new EditArticleDescriptorBuilder().withTitle("Seven upon a time").build();
+ EditArticleCommand editArticleCommand = new EditArticleCommand(INDEX_SIXTH_ARTICLE, descriptor);
+
+ String expectedMessage = String.format(EditArticleCommand.MESSAGE_EDIT_ARTICLE_SUCCESS,
+ Messages.format(editedArticle));
+
+ Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()),
+ new ArticleBook(model.getArticleBook()), new UserPrefs());
+ expectedModel.setArticle(lastArticle, editedArticle);
+
+ assertCommandSuccess(editArticleCommand, model, expectedMessage, expectedModel);
+ }
+
+ @Test
+ public void execute_noFieldSpecifiedUnfilteredList_success() {
+ EditArticleCommand editArticleCommand = new EditArticleCommand(INDEX_FIRST_ARTICLE,
+ new EditArticleDescriptor());
+ Article editedArticle = model.getFilteredArticleList().get(INDEX_FIRST_ARTICLE.getZeroBased());
+
+ String expectedMessage = String.format(EditArticleCommand.MESSAGE_EDIT_ARTICLE_SUCCESS,
+ Messages.format(editedArticle));
+
+ Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()),
+ new ArticleBook(model.getArticleBook()), new UserPrefs());
+
+ assertCommandSuccess(editArticleCommand, model, expectedMessage, expectedModel);
+ }
+
+ @Test
+ public void execute_filteredList_success() {
+ showArticleAtIndex(model, INDEX_FIRST_ARTICLE);
+
+ Article articleInFilteredList = model.getFilteredArticleList().get(INDEX_FIRST_ARTICLE.getZeroBased());
+ Article editedArticle = new ArticleBuilder(articleInFilteredList).withTitle("New Title").build();
+ EditArticleCommand editArticleCommand = new EditArticleCommand(INDEX_FIRST_ARTICLE,
+ new EditArticleDescriptorBuilder().withTitle("New Title").build());
+
+ String expectedMessage = String.format(EditArticleCommand.MESSAGE_EDIT_ARTICLE_SUCCESS,
+ Messages.format(editedArticle));
+
+ Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()),
+ new ArticleBook(model.getArticleBook()), new UserPrefs());
+ expectedModel.setArticle(articleInFilteredList, editedArticle);
+
+ assertCommandSuccess(editArticleCommand, model, expectedMessage, expectedModel);
+ }
+
+ @Test
+ public void execute_invalidArticleIndexUnfilteredList_failure() {
+ int outOfBoundIndex = model.getFilteredArticleList().size() + 1;
+ EditArticleDescriptor descriptor = new EditArticleDescriptorBuilder().withTitle("New Title").build();
+ EditArticleCommand editArticleCommand = new EditArticleCommand(Index.fromOneBased(outOfBoundIndex), descriptor);
+
+ ArticleCommandTestUtil.assertCommandFailure(editArticleCommand, model,
+ Messages.MESSAGE_INVALID_ARTICLE_DISPLAYED_INDEX);
+ }
+
+ @Test
+ public void execute_invalidArticleIndexFilteredList_failure() {
+ showArticleAtIndex(model, INDEX_FIRST_ARTICLE);
+
+ Index outOfBoundIndex = Index.fromOneBased(model.getFilteredArticleList().size() + 1);
+ EditArticleDescriptor descriptor = new EditArticleDescriptorBuilder().withTitle("New Title").build();
+ EditArticleCommand editArticleCommand = new EditArticleCommand(outOfBoundIndex, descriptor);
+
+ ArticleCommandTestUtil.assertCommandFailure(editArticleCommand, model,
+ Messages.MESSAGE_INVALID_ARTICLE_DISPLAYED_INDEX);
+ }
+
+ @Test
+ public void equals() {
+ final EditArticleCommand standardCommand = new EditArticleCommand(INDEX_FIRST_ARTICLE,
+ ArticleCommandTestUtil.DESC_NVIDIA);
+
+ // same values -> returns true
+ EditArticleDescriptor copyDescriptor = new EditArticleDescriptor(ArticleCommandTestUtil.DESC_NVIDIA);
+ EditArticleCommand commandWithSameValues = new EditArticleCommand(INDEX_FIRST_ARTICLE, copyDescriptor);
+ assert (standardCommand.equals(commandWithSameValues));
+
+ // same object -> returns true
+ assert (standardCommand.equals(standardCommand));
+
+ // null -> returns false
+ assert (!standardCommand.equals(null));
+
+ // different types -> returns false
+ assert (!standardCommand.equals(new ClearCommand()));
+
+ // different index -> returns false
+ assert (!standardCommand.equals(new EditArticleCommand(INDEX_SIXTH_ARTICLE,
+ ArticleCommandTestUtil.DESC_NVIDIA)));
+
+ // different descriptor -> returns false
+ assert (!standardCommand.equals(new EditArticleCommand(INDEX_FIRST_ARTICLE,
+ ArticleCommandTestUtil.DESC_INTEL)));
+ }
+
+}
diff --git a/src/test/java/seedu/address/logic/commands/EditArticleDescriptorTest.java b/src/test/java/seedu/address/logic/commands/EditArticleDescriptorTest.java
new file mode 100644
index 00000000000..c20397ca0cb
--- /dev/null
+++ b/src/test/java/seedu/address/logic/commands/EditArticleDescriptorTest.java
@@ -0,0 +1,75 @@
+package seedu.address.logic.commands;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static seedu.address.logic.commands.articlecommands.ArticleCommandTestUtil.DESC_INTEL;
+import static seedu.address.logic.commands.articlecommands.ArticleCommandTestUtil.DESC_NVIDIA;
+import static seedu.address.logic.commands.articlecommands.ArticleCommandTestUtil.FIRST_VALID_AUTHOR_INTEL;
+import static seedu.address.logic.commands.articlecommands.ArticleCommandTestUtil.FIRST_VALID_OUTLET_INTEL;
+import static seedu.address.logic.commands.articlecommands.ArticleCommandTestUtil.FIRST_VALID_SOURCE_INTEL;
+import static seedu.address.logic.commands.articlecommands.ArticleCommandTestUtil.FIRST_VALID_TAG_INTEL;
+import static seedu.address.logic.commands.articlecommands.ArticleCommandTestUtil.VALID_LINK_INTEL;
+import static seedu.address.logic.commands.articlecommands.ArticleCommandTestUtil.VALID_PUBLICATION_DATE_INTEL;
+import static seedu.address.logic.commands.articlecommands.ArticleCommandTestUtil.VALID_STATUS_INTEL;
+import static seedu.address.logic.commands.articlecommands.ArticleCommandTestUtil.VALID_TITLE_INTEL;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.logic.commands.articlecommands.EditArticleCommand.EditArticleDescriptor;
+import seedu.address.testutil.EditArticleDescriptorBuilder;
+
+public class EditArticleDescriptorTest {
+
+ @Test
+ public void equals() {
+ // same values -> returns true
+ EditArticleDescriptor descriptorWithSameValues = new EditArticleDescriptor(DESC_NVIDIA);
+ assertTrue(DESC_NVIDIA.equals(descriptorWithSameValues));
+
+ // same object -> returns true
+ assertTrue(DESC_NVIDIA.equals(DESC_NVIDIA));
+
+ // null -> returns false
+ assertFalse(DESC_NVIDIA.equals(null));
+
+ // different types -> returns false
+ assertFalse(DESC_NVIDIA.equals(5));
+
+ // different values -> returns false
+ assertFalse(DESC_NVIDIA.equals(DESC_INTEL));
+
+ // different headline -> returns false
+ EditArticleDescriptor editedArticle = new EditArticleDescriptorBuilder(DESC_NVIDIA).withTitle(VALID_TITLE_INTEL)
+ .build();
+ assertFalse(DESC_NVIDIA.equals(editedArticle));
+
+ // different date -> returns false
+ editedArticle = new EditArticleDescriptorBuilder(DESC_NVIDIA).withPublicationDate(VALID_PUBLICATION_DATE_INTEL)
+ .build();
+ assertFalse(DESC_NVIDIA.equals(editedArticle));
+
+ // different status -> returns false
+ editedArticle = new EditArticleDescriptorBuilder(DESC_NVIDIA).withStatus(VALID_STATUS_INTEL).build();
+ assertFalse(DESC_NVIDIA.equals(editedArticle));
+
+ // different contributors -> returns false
+ editedArticle = new EditArticleDescriptorBuilder(DESC_NVIDIA).withAuthors(FIRST_VALID_AUTHOR_INTEL).build();
+ assertFalse(DESC_NVIDIA.equals(editedArticle));
+
+ // different interviewees -> returns false
+ editedArticle = new EditArticleDescriptorBuilder(DESC_NVIDIA).withSources(FIRST_VALID_SOURCE_INTEL).build();
+ assertFalse(DESC_NVIDIA.equals(editedArticle));
+
+ // different outlet -> returns false
+ editedArticle = new EditArticleDescriptorBuilder(DESC_NVIDIA).withOutlets(FIRST_VALID_OUTLET_INTEL).build();
+ assertFalse(DESC_NVIDIA.equals(editedArticle));
+
+ // different tags -> returns false
+ editedArticle = new EditArticleDescriptorBuilder(DESC_NVIDIA).withTags(FIRST_VALID_TAG_INTEL).build();
+ assertFalse(DESC_NVIDIA.equals(editedArticle));
+
+ // different link -> returns false
+ editedArticle = new EditArticleDescriptorBuilder(DESC_NVIDIA).withLink(VALID_LINK_INTEL).build();
+ assertFalse(DESC_NVIDIA.equals(editedArticle));
+ }
+}
diff --git a/src/test/java/seedu/address/logic/commands/EditCommandTest.java b/src/test/java/seedu/address/logic/commands/EditCommandTest.java
index 469dd97daa7..30a3eece791 100644
--- a/src/test/java/seedu/address/logic/commands/EditCommandTest.java
+++ b/src/test/java/seedu/address/logic/commands/EditCommandTest.java
@@ -21,6 +21,7 @@
import seedu.address.logic.Messages;
import seedu.address.logic.commands.EditCommand.EditPersonDescriptor;
import seedu.address.model.AddressBook;
+import seedu.address.model.ArticleBook;
import seedu.address.model.Model;
import seedu.address.model.ModelManager;
import seedu.address.model.UserPrefs;
@@ -33,7 +34,7 @@
*/
public class EditCommandTest {
- private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs());
+ private Model model = new ModelManager(getTypicalAddressBook(), new ArticleBook(), new UserPrefs());
@Test
public void execute_allFieldsSpecifiedUnfilteredList_success() {
@@ -43,7 +44,8 @@ public void execute_allFieldsSpecifiedUnfilteredList_success() {
String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson));
- Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs());
+ Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()),
+ new ArticleBook(), new UserPrefs());
expectedModel.setPerson(model.getFilteredPersonList().get(0), editedPerson);
assertCommandSuccess(editCommand, model, expectedMessage, expectedModel);
@@ -64,7 +66,8 @@ public void execute_someFieldsSpecifiedUnfilteredList_success() {
String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson));
- Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs());
+ Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()),
+ new ArticleBook(), new UserPrefs());
expectedModel.setPerson(lastPerson, editedPerson);
assertCommandSuccess(editCommand, model, expectedMessage, expectedModel);
@@ -77,7 +80,8 @@ public void execute_noFieldSpecifiedUnfilteredList_success() {
String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson));
- Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs());
+ Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()),
+ new ArticleBook(), new UserPrefs());
assertCommandSuccess(editCommand, model, expectedMessage, expectedModel);
}
@@ -93,7 +97,8 @@ public void execute_filteredList_success() {
String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson));
- Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs());
+ Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()),
+ new ArticleBook(), new UserPrefs());
expectedModel.setPerson(model.getFilteredPersonList().get(0), editedPerson);
assertCommandSuccess(editCommand, model, expectedMessage, expectedModel);
diff --git a/src/test/java/seedu/address/logic/commands/FindCommandTest.java b/src/test/java/seedu/address/logic/commands/FindCommandTest.java
index b8b7dbba91a..4f21eece454 100644
--- a/src/test/java/seedu/address/logic/commands/FindCommandTest.java
+++ b/src/test/java/seedu/address/logic/commands/FindCommandTest.java
@@ -15,6 +15,7 @@
import org.junit.jupiter.api.Test;
+import seedu.address.model.ArticleBook;
import seedu.address.model.Model;
import seedu.address.model.ModelManager;
import seedu.address.model.UserPrefs;
@@ -24,8 +25,8 @@
* Contains integration tests (interaction with the Model) for {@code FindCommand}.
*/
public class FindCommandTest {
- private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs());
- private Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs());
+ private Model model = new ModelManager(getTypicalAddressBook(), new ArticleBook(), new UserPrefs());
+ private Model expectedModel = new ModelManager(getTypicalAddressBook(), new ArticleBook(), new UserPrefs());
@Test
public void equals() {
diff --git a/src/test/java/seedu/address/logic/commands/ListCommandTest.java b/src/test/java/seedu/address/logic/commands/ListCommandTest.java
index 435ff1f7275..a313807e604 100644
--- a/src/test/java/seedu/address/logic/commands/ListCommandTest.java
+++ b/src/test/java/seedu/address/logic/commands/ListCommandTest.java
@@ -8,6 +8,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import seedu.address.model.ArticleBook;
import seedu.address.model.Model;
import seedu.address.model.ModelManager;
import seedu.address.model.UserPrefs;
@@ -22,8 +23,8 @@ public class ListCommandTest {
@BeforeEach
public void setUp() {
- model = new ModelManager(getTypicalAddressBook(), new UserPrefs());
- expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs());
+ model = new ModelManager(getTypicalAddressBook(), new ArticleBook(), new UserPrefs());
+ expectedModel = new ModelManager(model.getAddressBook(), new ArticleBook(), new UserPrefs());
}
@Test
diff --git a/src/test/java/seedu/address/logic/commands/SortCommandTest.java b/src/test/java/seedu/address/logic/commands/SortCommandTest.java
new file mode 100644
index 00000000000..29251bc75d5
--- /dev/null
+++ b/src/test/java/seedu/address/logic/commands/SortCommandTest.java
@@ -0,0 +1,101 @@
+package seedu.address.logic.commands;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure;
+import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess;
+import static seedu.address.logic.commands.CommandTestUtil.showPersonInRange;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME;
+import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON;
+import static seedu.address.testutil.TypicalIndexes.INDEX_THIRD_PERSON;
+import static seedu.address.testutil.TypicalPersons.getTypicalReversedAddressBook;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.logic.Messages;
+import seedu.address.logic.parser.Prefix;
+import seedu.address.model.ArticleBook;
+import seedu.address.model.Model;
+import seedu.address.model.ModelManager;
+import seedu.address.model.UserPrefs;
+
+/**
+ * Contains integration tests (interaction with the Model) and unit tests for SortCommand.
+ */
+class SortCommandTest {
+
+ private Model model = new ModelManager(getTypicalReversedAddressBook(), new ArticleBook(), new UserPrefs());
+ private final Prefix invalidPrefix = new Prefix("z/");
+
+ @Test
+ public void execute_validPrefixUnfilteredList_success() {
+ SortCommand sortCommand = new SortCommand(PREFIX_NAME.getPrefix());
+
+ String expectedMessage = SortCommand.MESSAGE_SUCCESS;
+
+ ModelManager expectedModel = new ModelManager(model.getAddressBook(), new ArticleBook(), new UserPrefs());
+ expectedModel.sortAddressBook(PREFIX_NAME.getPrefix());
+
+ assertCommandSuccess(sortCommand, model, expectedMessage, expectedModel);
+ }
+
+ @Test
+ public void execute_invalidPrefixUnfilteredList_throwsCommandException() {
+ SortCommand sortCommand = new SortCommand(invalidPrefix.getPrefix());
+
+ assertCommandFailure(sortCommand, model, Messages.MESSAGE_INVALID_SORTING_PREFIX);
+ }
+
+ @Test
+ public void execute_validPrefixFilteredList_success() {
+ showPersonInRange(model, INDEX_FIRST_PERSON, INDEX_THIRD_PERSON);
+
+ SortCommand sortCommand = new SortCommand(PREFIX_NAME.getPrefix());
+
+ Model expectedModel = new ModelManager(model.getAddressBook(), new ArticleBook(), new UserPrefs());
+ expectedModel.sortAddressBook(PREFIX_NAME.getPrefix());
+
+ assertCommandSuccess(sortCommand, model, SortCommand.MESSAGE_SUCCESS, model);
+ }
+
+ @Test
+ public void execute_invalidPrefixFilteredList_throwsCommandException() {
+ showPersonInRange(model, INDEX_FIRST_PERSON, INDEX_THIRD_PERSON);
+
+ SortCommand sortCommand = new SortCommand(invalidPrefix.getPrefix());
+
+ assertCommandFailure(sortCommand, model, Messages.MESSAGE_INVALID_SORTING_PREFIX);
+ }
+
+ @Test
+ public void equals() {
+ SortCommand fSortCommand = new SortCommand(PREFIX_NAME.getPrefix());
+ SortCommand sSortCommand = new SortCommand(PREFIX_ADDRESS.getPrefix());
+
+ // same object -> returns true
+ assertTrue(fSortCommand.equals(fSortCommand));
+
+ // same values -> returns true
+ SortCommand sortCommandCopy = new SortCommand(PREFIX_NAME.getPrefix());
+ assertTrue(fSortCommand.equals(sortCommandCopy));
+
+ // different types -> returns false
+ assertFalse(fSortCommand.equals(1));
+
+ // null -> returns false
+ assertFalse(fSortCommand.equals(null));
+
+ // different person -> returns false
+ assertFalse(fSortCommand.equals(sSortCommand));
+ }
+
+ @Test
+ public void toStringMethod() {
+ Prefix namePrefix = PREFIX_NAME;
+ SortCommand sortCommand = new SortCommand(namePrefix.getPrefix());
+ String expected = SortCommand.class.getCanonicalName() + "{prefix=" + namePrefix.getPrefix() + "}";
+ assertEquals(expected, sortCommand.toString());
+ }
+}
diff --git a/src/test/java/seedu/address/logic/commands/articlecommands/ArticleCommandTestUtil.java b/src/test/java/seedu/address/logic/commands/articlecommands/ArticleCommandTestUtil.java
new file mode 100644
index 00000000000..780a7b4a05e
--- /dev/null
+++ b/src/test/java/seedu/address/logic/commands/articlecommands/ArticleCommandTestUtil.java
@@ -0,0 +1,200 @@
+package seedu.address.logic.commands.articlecommands;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_CONTRIBUTOR;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_HEADLINE;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_INTERVIEWEE;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_LINK;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_OUTLET;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_STATUS;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG;
+import static seedu.address.testutil.Assert.assertThrows;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.logic.commands.CommandResult;
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.model.ArticleBook;
+import seedu.address.model.Model;
+import seedu.address.model.article.Article;
+import seedu.address.model.article.TitleContainsKeywordsPredicate;
+import seedu.address.testutil.EditArticleDescriptorBuilder;
+
+/**
+ * Contains helper methods for testing article commands.
+ */
+public class ArticleCommandTestUtil {
+
+ public static final String VALID_TITLE_NVIDIA = "Nvidia's new B200 'Blackwell' chips change the game";
+ public static final String VALID_TITLE_INTEL = "Intel's new 12th Gen Alder Lake CPUs makes gaming so much better";
+ public static final String FIRST_VALID_AUTHOR_NVIDIA = "Alice Wang";
+ public static final String SECOND_VALID_AUTHOR_NVIDIA = "Amy Wong";
+ public static final String FIRST_VALID_AUTHOR_INTEL = "Bob Ross";
+ public static final String SECOND_VALID_AUTHOR_INTEL = "Bobby Fisher";
+ public static final String VALID_PUBLICATION_DATE_NVIDIA = "20-03-2024 12:00";
+ public static final String VALID_PUBLICATION_DATE_INTEL = "22-04-2022 20:00";
+ public static final String FIRST_VALID_SOURCE_NVIDIA = "Jensen Huang";
+ public static final String SECOND_VALID_SOURCE_NVIDIA = "Morris Chang";
+ public static final String FIRST_VALID_SOURCE_INTEL = "Patrick Gelsinger";
+ public static final String SECOND_VALID_SOURCE_INTEL = "Greg Lavender";
+ public static final String FIRST_VALID_OUTLET_NVIDIA = "BBC News";
+ public static final String SECOND_VALID_OUTLET_NVIDIA = "The Economic Times";
+ public static final String FIRST_VALID_OUTLET_INTEL = "CNBC News";
+ public static final String SECOND_VALID_OUTLET_INTEL = "The New York Times";
+ public static final String FIRST_VALID_TAG_NVIDIA = "RND";
+ public static final String SECOND_VALID_TAG_NVIDIA = "Tech";
+ public static final String FIRST_VALID_TAG_INTEL = "Gaming";
+ public static final String SECOND_VALID_TAG_INTEL = "Tech";
+ public static final String VALID_STATUS_NVIDIA = "PUBLISHED";
+ public static final String VALID_STATUS_INTEL = "ARCHIVED";
+ public static final String VALID_LINK_NVIDIA = "https://www.nvidia.com/en-sg/";
+ public static final String VALID_LINK_INTEL = "https://www.intel.com/content/www/us/en/homepage.html";
+
+ public static final String TITLE_DESC_NVIDIA = " " + PREFIX_HEADLINE + VALID_TITLE_NVIDIA;
+ public static final String TITLE_DESC_INTEL = " " + PREFIX_HEADLINE + VALID_TITLE_INTEL;
+ public static final String AUTHOR_DESC_NVIDIA = " " + PREFIX_CONTRIBUTOR + FIRST_VALID_AUTHOR_NVIDIA
+ + " " + PREFIX_CONTRIBUTOR + SECOND_VALID_AUTHOR_NVIDIA;
+ public static final String AUTHOR_DESC_INTEL = " " + PREFIX_CONTRIBUTOR + FIRST_VALID_AUTHOR_INTEL
+ + " " + PREFIX_CONTRIBUTOR + SECOND_VALID_AUTHOR_INTEL;
+ public static final String PUBLICATION_DATE_DESC_NVIDIA = " " + PREFIX_DATE
+ + VALID_PUBLICATION_DATE_NVIDIA;
+ public static final String PUBLICATION_DATE_DESC_INTEL = " " + PREFIX_DATE
+ + VALID_PUBLICATION_DATE_INTEL;
+ public static final String SOURCE_DESC_NVIDIA = " " + PREFIX_INTERVIEWEE + FIRST_VALID_SOURCE_NVIDIA
+ + " " + PREFIX_INTERVIEWEE + SECOND_VALID_SOURCE_NVIDIA;
+ public static final String SOURCE_DESC_INTEL = " " + PREFIX_INTERVIEWEE + FIRST_VALID_SOURCE_INTEL
+ + " " + PREFIX_INTERVIEWEE + SECOND_VALID_SOURCE_INTEL;
+ public static final String OUTLET_DESC_NVIDIA = " " + PREFIX_OUTLET + FIRST_VALID_OUTLET_NVIDIA
+ + " " + PREFIX_OUTLET + SECOND_VALID_OUTLET_NVIDIA;
+ public static final String OUTLET_DESC_INTEL = " " + PREFIX_OUTLET + FIRST_VALID_OUTLET_INTEL
+ + " " + PREFIX_OUTLET + SECOND_VALID_OUTLET_INTEL;
+ public static final String TAG_DESC_NVIDIA = " " + PREFIX_TAG + FIRST_VALID_TAG_NVIDIA
+ + " " + PREFIX_TAG + SECOND_VALID_TAG_NVIDIA;
+ public static final String TAG_DESC_INTEL = " " + PREFIX_TAG + FIRST_VALID_TAG_INTEL
+ + " " + PREFIX_TAG + SECOND_VALID_TAG_INTEL;
+ public static final String STATUS_DESC_NVIDIA = " " + PREFIX_STATUS + VALID_STATUS_NVIDIA;
+ public static final String STATUS_DESC_INTEL = " " + PREFIX_STATUS + VALID_STATUS_INTEL;
+ public static final String LINK_DESC_NVIDIA = " " + PREFIX_LINK + VALID_LINK_NVIDIA;
+ public static final String LINK_DESC_INTEL = " " + PREFIX_LINK + VALID_LINK_INTEL;
+
+ public static final String INVALID_TITLE_DESC = " " + PREFIX_HEADLINE; // empty string not allowed for titles
+ public static final String INVALID_AUTHOR_DESC = " " + PREFIX_CONTRIBUTOR + "Bob&"; // '&' not allowed in authors
+ public static final String INVALID_PUBLICATION_DATE_DESC = " " + PREFIX_DATE
+ + "03-20-2024 12:00"; // Invalid date format
+ public static final String INVALID_PUBLICATION_TIME_DESC = " " + PREFIX_DATE
+ + "20-03-2024 25:00"; // Invalid time format
+ public static final String INVALID_SOURCE_DESC = " " + PREFIX_INTERVIEWEE + "Ryan&"; // '&' not allowed in sources
+ public static final String INVALID_OUTLET_DESC = " " + PREFIX_OUTLET + "BBC News*"; // '*' not allowed in outlets
+ public static final String INVALID_TAG_DESC = " " + PREFIX_TAG + "Tech*"; // '*' not allowed in tags
+ public static final String INVALID_STATUS_DESC = " " + PREFIX_STATUS + "PUBLISHEDD"; // Invalid status
+
+ public static final String PREAMBLE_WHITESPACE = "\t \r \n";
+ public static final String PREAMBLE_NON_EMPTY = "NonEmptyPreamble";
+
+ public static final EditArticleCommand.EditArticleDescriptor DESC_NVIDIA;
+ public static final EditArticleCommand.EditArticleDescriptor DESC_INTEL;
+
+ static {
+ DESC_NVIDIA = new EditArticleDescriptorBuilder().withTitle(VALID_TITLE_NVIDIA)
+ .withAuthors(FIRST_VALID_AUTHOR_NVIDIA, SECOND_VALID_AUTHOR_NVIDIA)
+ .withPublicationDate(VALID_PUBLICATION_DATE_NVIDIA)
+ .withSources(FIRST_VALID_SOURCE_NVIDIA, SECOND_VALID_SOURCE_NVIDIA)
+ .withOutlets(FIRST_VALID_OUTLET_NVIDIA, SECOND_VALID_OUTLET_NVIDIA)
+ .withTags(FIRST_VALID_TAG_NVIDIA, SECOND_VALID_TAG_NVIDIA)
+ .withStatus(VALID_STATUS_NVIDIA)
+ .withLink(VALID_LINK_NVIDIA).build();
+ DESC_INTEL = new EditArticleDescriptorBuilder().withTitle(VALID_TITLE_INTEL)
+ .withAuthors(FIRST_VALID_AUTHOR_INTEL, SECOND_VALID_AUTHOR_INTEL)
+ .withPublicationDate(VALID_PUBLICATION_DATE_INTEL)
+ .withSources(FIRST_VALID_SOURCE_INTEL, SECOND_VALID_SOURCE_INTEL)
+ .withOutlets(FIRST_VALID_OUTLET_INTEL, SECOND_VALID_OUTLET_INTEL)
+ .withTags(FIRST_VALID_TAG_INTEL, SECOND_VALID_TAG_INTEL)
+ .withStatus(VALID_STATUS_INTEL)
+ .withLink(VALID_LINK_INTEL).build();
+ }
+
+ /**
+ * Executes the given {@code command}, confirms that
+ * - the returned {@link CommandResult} matches {@code expectedCommandResult}
+ * - the {@code actualModel} matches {@code expectedModel}
+ */
+ public static void assertCommandSuccess(ArticleCommand command, Model actualModel,
+ CommandResult expectedCommandResult, Model expectedModel) {
+ try {
+ CommandResult result = command.execute(actualModel);
+ assertEquals(expectedCommandResult, result);
+ assertEquals(expectedModel, actualModel);
+ } catch (CommandException ce) {
+ throw new AssertionError("Execution of command should not fail.", ce);
+ }
+ }
+
+ /**
+ * Convenience wrapper to {@link #assertCommandSuccess(ArticleCommand, Model, CommandResult, Model)}
+ * that takes a string {@code expectedMessage}.
+ */
+ public static void assertCommandSuccess(ArticleCommand command, Model actualModel, String expectedMessage,
+ Model expectedModel) {
+ CommandResult expectedCommandResult = new CommandResult(expectedMessage);
+ assertCommandSuccess(command, actualModel, expectedCommandResult, expectedModel);
+ }
+
+ /**
+ * Executes the given {@code command}, confirms that
+ * - a {@code CommandException} is thrown
+ * - the CommandException message matches {@code expectedMessage}
+ * - the article book, filtered article list and selected article in {@code actualModel} remain unchanged
+ */
+ public static void assertCommandFailure(ArticleCommand command, Model actualModel, String expectedMessage) {
+ // we are unable to defensively copy the model for comparison later, so we can
+ // only do so by copying its components.
+ ArticleBook expectedArticleBook = new ArticleBook(actualModel.getArticleBook());
+ List expectedFilteredList = new ArrayList<>(actualModel.getFilteredArticleList());
+
+ assertThrows(CommandException.class, expectedMessage, () -> command.execute(actualModel));
+ assertEquals(expectedArticleBook, actualModel.getArticleBook());
+ assertEquals(expectedFilteredList, actualModel.getFilteredArticleList());
+ }
+
+ /**
+ * Updates {@code model}'s filtered list to show only the article at the given {@code targetIndex} in the
+ * {@code model}'s article book.
+ */
+ public static void showArticleAtIndex(Model model, Index targetIndex) {
+ assertTrue(targetIndex.getZeroBased() < model.getFilteredArticleList().size());
+
+ Article article = model.getFilteredArticleList().get(targetIndex.getZeroBased());
+ final String[] splitTitle = article.getTitle().fullTitle.split("\\s+");
+ model.updateFilteredArticleList(new TitleContainsKeywordsPredicate(Arrays.asList(splitTitle[0])));
+
+ assertEquals(1, model.getFilteredArticleList().size());
+ }
+
+ /**
+ * Updates {@code model}'s filtered list to show only the articles within the range {@code startIndex-endIndex}
+ * in the {@code model}'s article book.
+ */
+ public static void showArticleInRange(Model model, Index startIndex, Index endIndex) {
+ assertTrue(0 <= startIndex.getZeroBased());
+ assertTrue(startIndex.getZeroBased() < model.getFilteredArticleList().size());
+ assertTrue(endIndex.getZeroBased() < model.getFilteredArticleList().size());
+ assertTrue(startIndex.getZeroBased() <= endIndex.getZeroBased());
+
+ ArrayList articleFirstTitleWords = new ArrayList<>();
+
+ for (int i = startIndex.getZeroBased(); i <= endIndex.getZeroBased(); i++) {
+ Article article = model.getFilteredArticleList().get(i);
+ final String[] splitTitle = article.getTitle().fullTitle.split("\\s+");
+ articleFirstTitleWords.add(splitTitle[0]);
+ }
+
+ model.updateFilteredArticleList(new TitleContainsKeywordsPredicate(articleFirstTitleWords));
+
+ assertEquals(endIndex.getOneBased() - startIndex.getZeroBased(), model.getFilteredArticleList().size());
+ }
+}
diff --git a/src/test/java/seedu/address/logic/commands/articlecommands/FilterArticleCommandTest.java b/src/test/java/seedu/address/logic/commands/articlecommands/FilterArticleCommandTest.java
new file mode 100644
index 00000000000..f8923fdf15b
--- /dev/null
+++ b/src/test/java/seedu/address/logic/commands/articlecommands/FilterArticleCommandTest.java
@@ -0,0 +1,67 @@
+package seedu.address.logic.commands.articlecommands;
+
+
+import static seedu.address.logic.commands.articlecommands.ArticleCommandTestUtil.assertCommandSuccess;
+import static seedu.address.testutil.TypicalArticles.getTypicalArticleBook;
+
+import java.time.LocalDateTime;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import seedu.address.logic.parser.ParserUtil;
+import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.AddressBook;
+import seedu.address.model.Model;
+import seedu.address.model.ModelManager;
+import seedu.address.model.UserPrefs;
+import seedu.address.model.article.ArticleMatchesStatusPredicate;
+import seedu.address.model.article.ArticleMatchesTagPredicate;
+import seedu.address.model.article.ArticleMatchesTimePeriodPredicate;
+import seedu.address.model.article.PublicationDate;
+import seedu.address.model.tag.Tag;
+
+public class FilterArticleCommandTest {
+ private static final Tag EMPTY_TAG = null;
+ private static final PublicationDate EMPTY_START = new PublicationDate(LocalDateTime.MIN);
+ private static final PublicationDate EMPTY_END = new PublicationDate(LocalDateTime.MAX);
+ private Model model;
+ private Model expectedModel;
+ @BeforeEach
+ public void setUp() {
+ model = new ModelManager(new AddressBook(), getTypicalArticleBook(), new UserPrefs());
+ expectedModel = new ModelManager(new AddressBook(), model.getArticleBook(), new UserPrefs());
+ }
+ @Test
+ public void execute_zeroParameters_showUnfilteredList() throws ParseException {
+ assertCommandSuccess(new FilterArticleCommand("", EMPTY_TAG, EMPTY_START, EMPTY_END),
+ model, FilterArticleCommand.MESSAGE_SUCCESS, expectedModel);
+ }
+ //EP Only Status
+ @Test
+ public void execute_onlyStatus_showFilteredList() throws ParseException {
+ ArticleMatchesStatusPredicate predicate = new ArticleMatchesStatusPredicate("DRAFT");
+ expectedModel.updateFilteredArticleList(predicate);
+ assertCommandSuccess(new FilterArticleCommand("DRAFT", EMPTY_TAG, EMPTY_START, EMPTY_END),
+ model, FilterArticleCommand.MESSAGE_SUCCESS, expectedModel);
+ }
+ //ep Only tag
+ @Test
+ public void execute_onlyTag_showFilteredList() throws ParseException {
+ Tag tag = new Tag("Science");
+ ArticleMatchesTagPredicate predicate = new ArticleMatchesTagPredicate(tag);
+ expectedModel.updateFilteredArticleList(predicate);
+ assertCommandSuccess(new FilterArticleCommand("", tag, EMPTY_START, EMPTY_END),
+ model, FilterArticleCommand.MESSAGE_SUCCESS, expectedModel);
+ }
+ //ep Only date
+ @Test
+ public void execute_onlyDates_showFilteredList() throws ParseException {
+ PublicationDate start = ParserUtil.parsePublicationDate("01-01-2021");
+ PublicationDate end = ParserUtil.parsePublicationDate("08-07-2021");
+ ArticleMatchesTimePeriodPredicate predicate = new ArticleMatchesTimePeriodPredicate(start, end);
+ expectedModel.updateFilteredArticleList(predicate);
+ assertCommandSuccess(new FilterArticleCommand("", EMPTY_TAG, start, end),
+ model, FilterArticleCommand.MESSAGE_SUCCESS, expectedModel);
+ }
+}
diff --git a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java
index 5a1ab3dbc0c..f0047137522 100644
--- a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java
+++ b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java
@@ -4,6 +4,7 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
import static seedu.address.logic.Messages.MESSAGE_UNKNOWN_COMMAND;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME;
import static seedu.address.testutil.Assert.assertThrows;
import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON;
@@ -22,6 +23,7 @@
import seedu.address.logic.commands.FindCommand;
import seedu.address.logic.commands.HelpCommand;
import seedu.address.logic.commands.ListCommand;
+import seedu.address.logic.commands.SortCommand;
import seedu.address.logic.parser.exceptions.ParseException;
import seedu.address.model.person.NameContainsKeywordsPredicate;
import seedu.address.model.person.Person;
@@ -88,6 +90,13 @@ public void parseCommand_list() throws Exception {
assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD + " 3") instanceof ListCommand);
}
+ @Test
+ public void parseCommand_sort() throws Exception {
+ SortCommand command = (SortCommand) parser.parseCommand(
+ SortCommand.COMMAND_WORD + " " + PREFIX_NAME.getPrefix());
+ assertEquals(new SortCommand(PREFIX_NAME.getPrefix()), command);
+ }
+
@Test
public void parseCommand_unrecognisedInput_throwsParseException() {
assertThrows(ParseException.class, String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE), ()
diff --git a/src/test/java/seedu/address/logic/parser/SortCommandParserTest.java b/src/test/java/seedu/address/logic/parser/SortCommandParserTest.java
new file mode 100644
index 00000000000..39ecf3ccf46
--- /dev/null
+++ b/src/test/java/seedu/address/logic/parser/SortCommandParserTest.java
@@ -0,0 +1,28 @@
+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.CommandParserTestUtil.assertParseFailure;
+import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.logic.commands.SortCommand;
+
+class SortCommandParserTest {
+
+ private final SortCommandParser parser = new SortCommandParser();
+
+ @Test
+ public void parse_validArgs_returnsSortCommand() {
+ assertParseSuccess(parser, " n/", new SortCommand(PREFIX_NAME.getPrefix()));
+ }
+
+ @Test
+ public void parse_invalidArgs_throwsParseException() {
+ assertParseFailure(parser, " z/", String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE));
+ assertParseFailure(parser, "Hello World",
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE));
+ assertParseFailure(parser, "123", String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE));
+ }
+}
diff --git a/src/test/java/seedu/address/model/AddressBookTest.java b/src/test/java/seedu/address/model/AddressBookTest.java
index 68c8c5ba4d5..352362ed415 100644
--- a/src/test/java/seedu/address/model/AddressBookTest.java
+++ b/src/test/java/seedu/address/model/AddressBookTest.java
@@ -5,8 +5,11 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB;
import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME;
import static seedu.address.testutil.Assert.assertThrows;
import static seedu.address.testutil.TypicalPersons.ALICE;
+import static seedu.address.testutil.TypicalPersons.BOB;
+import static seedu.address.testutil.TypicalPersons.CARL;
import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook;
import java.util.Arrays;
@@ -78,6 +81,19 @@ public void hasPerson_personWithSameIdentityFieldsInAddressBook_returnsTrue() {
assertTrue(addressBook.hasPerson(editedAlice));
}
+ @Test
+ public void sortAddressBook_validPrefix_sortsPeopleInAddressBookByAttributeCapturedByPrefix() {
+ addressBook.addPerson(CARL);
+ addressBook.addPerson(BOB);
+ addressBook.addPerson(ALICE);
+ addressBook.sortAddressBook(PREFIX_NAME.getPrefix());
+ AddressBook expected = new AddressBook();
+ expected.addPerson(ALICE);
+ expected.addPerson(BOB);
+ expected.addPerson(CARL);
+ assertEquals(expected, addressBook);
+ }
+
@Test
public void getPersonList_modifyList_throwsUnsupportedOperationException() {
assertThrows(UnsupportedOperationException.class, () -> addressBook.getPersonList().remove(0));
diff --git a/src/test/java/seedu/address/model/ModelManagerTest.java b/src/test/java/seedu/address/model/ModelManagerTest.java
index 2cf1418d116..c85c11fd406 100644
--- a/src/test/java/seedu/address/model/ModelManagerTest.java
+++ b/src/test/java/seedu/address/model/ModelManagerTest.java
@@ -96,12 +96,16 @@ public void getFilteredPersonList_modifyList_throwsUnsupportedOperationException
@Test
public void equals() {
AddressBook addressBook = new AddressBookBuilder().withPerson(ALICE).withPerson(BENSON).build();
+
+ // A dummy articleBook for now before implementing tests for ArticleBooks.
+ ArticleBook articleBook = new ArticleBook();
+
AddressBook differentAddressBook = new AddressBook();
UserPrefs userPrefs = new UserPrefs();
// same values -> returns true
- modelManager = new ModelManager(addressBook, userPrefs);
- ModelManager modelManagerCopy = new ModelManager(addressBook, userPrefs);
+ modelManager = new ModelManager(addressBook, articleBook, userPrefs);
+ ModelManager modelManagerCopy = new ModelManager(addressBook, articleBook, userPrefs);
assertTrue(modelManager.equals(modelManagerCopy));
// same object -> returns true
@@ -114,12 +118,12 @@ public void equals() {
assertFalse(modelManager.equals(5));
// different addressBook -> returns false
- assertFalse(modelManager.equals(new ModelManager(differentAddressBook, userPrefs)));
+ assertFalse(modelManager.equals(new ModelManager(differentAddressBook, articleBook, userPrefs)));
// different filteredList -> returns false
String[] keywords = ALICE.getName().fullName.split("\\s+");
modelManager.updateFilteredPersonList(new NameContainsKeywordsPredicate(Arrays.asList(keywords)));
- assertFalse(modelManager.equals(new ModelManager(addressBook, userPrefs)));
+ assertFalse(modelManager.equals(new ModelManager(addressBook, articleBook, userPrefs)));
// resets modelManager to initial state for upcoming tests
modelManager.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS);
@@ -127,6 +131,6 @@ public void equals() {
// different userPrefs -> returns false
UserPrefs differentUserPrefs = new UserPrefs();
differentUserPrefs.setAddressBookFilePath(Paths.get("differentFilePath"));
- assertFalse(modelManager.equals(new ModelManager(addressBook, differentUserPrefs)));
+ assertFalse(modelManager.equals(new ModelManager(addressBook, articleBook, differentUserPrefs)));
}
}
diff --git a/src/test/java/seedu/address/model/article/ArticleMatchesStatusPredicateTest.java b/src/test/java/seedu/address/model/article/ArticleMatchesStatusPredicateTest.java
new file mode 100644
index 00000000000..87b7687b984
--- /dev/null
+++ b/src/test/java/seedu/address/model/article/ArticleMatchesStatusPredicateTest.java
@@ -0,0 +1,50 @@
+package seedu.address.model.article;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static seedu.address.testutil.TypicalArticles.FIVE;
+import static seedu.address.testutil.TypicalArticles.FOUR;
+import static seedu.address.testutil.TypicalPredicates.DRAFT;
+import static seedu.address.testutil.TypicalPredicates.PUBLISHED;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.model.article.exceptions.InvalidStatusException;
+
+public class ArticleMatchesStatusPredicateTest {
+ //EP valid status in capital letters
+ @Test
+ public void newPredicate_validStatus_noError() {
+ ArticleMatchesStatusPredicate predicate = new ArticleMatchesStatusPredicate("PUBLISHED");
+ //assert(predicate.equals(PUBLISHED));
+ }
+ //EP valid status with no capitalization
+ @Test
+ public void newPredicate_validStatusNoCap_noError() {
+ ArticleMatchesStatusPredicate predicate = new ArticleMatchesStatusPredicate("draft");
+ }
+
+ //EP invalid status null
+ @Test
+ public void newPredicate_invalidStatusNull_error() {
+ assertThrows(NullPointerException.class, () -> new ArticleMatchesStatusPredicate(null));
+ }
+ //EP invalid status non-null
+ @Test
+ public void newPredicate_invalidStatusNonNull_error() {
+ assertThrows(InvalidStatusException.class, () -> new ArticleMatchesStatusPredicate("Qwerty"));
+ }
+ //EP test status match
+ @Test
+ public void test_statusMatch_returnTrue() {
+ ArticleMatchesStatusPredicate predicate = DRAFT;
+ Article article = FIVE;
+ assert(predicate.test(article));
+ }
+ //EP test status do not match
+ @Test
+ public void test_statusMismatch_returnFalse() {
+ ArticleMatchesStatusPredicate predicate = PUBLISHED;
+ Article article = FOUR;
+ assert(!(predicate.test(article)));
+ }
+}
diff --git a/src/test/java/seedu/address/model/article/ArticleMatchesTagPredicateTest.java b/src/test/java/seedu/address/model/article/ArticleMatchesTagPredicateTest.java
new file mode 100644
index 00000000000..045b7cd88d8
--- /dev/null
+++ b/src/test/java/seedu/address/model/article/ArticleMatchesTagPredicateTest.java
@@ -0,0 +1,37 @@
+package seedu.address.model.article;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static seedu.address.testutil.TypicalArticles.FOUR;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.model.tag.Tag;
+
+public class ArticleMatchesTagPredicateTest {
+ //EP valid tag
+ @Test
+ public void newPredicate_validTag_noError() {
+ Tag tag = new Tag("tag");
+ ArticleMatchesTagPredicate predicate = new ArticleMatchesTagPredicate(tag);
+ }
+
+ //EP invalid tag null
+ @Test
+ public void newPredicate_invalidTagNull_assertionError() {
+ assertThrows(AssertionError.class, () -> new ArticleMatchesTagPredicate(null));
+ }
+ //EP test when tags match
+ @Test
+ public void test_tagMatches_returnTrue() {
+ Tag tag = new Tag("Fiction");
+ ArticleMatchesTagPredicate predicate = new ArticleMatchesTagPredicate(tag);
+ assert(predicate.test(FOUR));
+ }
+ //EP test when tags don't match
+ @Test
+ public void test_tagMismatch_returnFalse() {
+ Tag tag = new Tag("Function");
+ ArticleMatchesTagPredicate predicate = new ArticleMatchesTagPredicate(tag);
+ assert(!predicate.test(FOUR));
+ }
+}
diff --git a/src/test/java/seedu/address/model/article/ArticleMatchesTimePeriodPredicateTest.java b/src/test/java/seedu/address/model/article/ArticleMatchesTimePeriodPredicateTest.java
new file mode 100644
index 00000000000..0db23e62517
--- /dev/null
+++ b/src/test/java/seedu/address/model/article/ArticleMatchesTimePeriodPredicateTest.java
@@ -0,0 +1,77 @@
+package seedu.address.model.article;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static seedu.address.testutil.TypicalArticles.FOUR;
+import static seedu.address.testutil.TypicalArticles.OLD;
+import static seedu.address.testutil.TypicalArticles.ONCE;
+import static seedu.address.testutil.TypicalPredicates.END;
+import static seedu.address.testutil.TypicalPredicates.START;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.logic.parser.ParserUtil;
+import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.article.exceptions.InvalidDatesException;
+
+public class ArticleMatchesTimePeriodPredicateTest {
+ @Test
+ //EP Valid Time Period
+ public void newPredicate_validTimePeriod_noError() throws ParseException {
+ PublicationDate start = ParserUtil.parsePublicationDate(START);
+ PublicationDate end = ParserUtil.parsePublicationDate(END);
+ ArticleMatchesTimePeriodPredicate predicate = new ArticleMatchesTimePeriodPredicate(start, end);
+ }
+ //EP Invalid time periods null
+ @Test
+ public void newPredicate_invalidTimePeriodNull_nullPointerError() throws ParseException {
+ PublicationDate start = null;
+ PublicationDate end = null;
+ assertThrows(NullPointerException.class, () -> new ArticleMatchesTimePeriodPredicate(start, end));
+ }
+ //EP Invalid time periods start after end
+ @Test
+ public void newPredicate_invalidTimeStartAfterEnd_invalidDateError() throws ParseException {
+ PublicationDate start = ParserUtil.parsePublicationDate(END);
+ PublicationDate end = ParserUtil.parsePublicationDate(START);
+ assertThrows(InvalidDatesException.class, () -> new ArticleMatchesTimePeriodPredicate(start, end));
+ }
+ //EP Boundary Value Start date = end date
+ @Test
+ public void newPredicate_startDateIsEndDate_noError() throws ParseException {
+ PublicationDate start = ParserUtil.parsePublicationDate(START);
+ PublicationDate end = ParserUtil.parsePublicationDate(START);
+ ArticleMatchesTimePeriodPredicate predicate = new ArticleMatchesTimePeriodPredicate(start, end);
+ }
+ //EP Test falls within predicate
+ @Test
+ public void test_withinPeriod_returnTrue() throws ParseException {
+ PublicationDate start = ParserUtil.parsePublicationDate(START);
+ PublicationDate end = ParserUtil.parsePublicationDate(END);
+ ArticleMatchesTimePeriodPredicate predicate = new ArticleMatchesTimePeriodPredicate(start, end);
+ assert(predicate.test(ONCE));
+ }
+ //EP Test article is after predicate period
+ @Test
+ public void test_afterPeriod_returnFalse() throws ParseException {
+ PublicationDate start = ParserUtil.parsePublicationDate(START);
+ PublicationDate end = ParserUtil.parsePublicationDate(END);
+ ArticleMatchesTimePeriodPredicate predicate = new ArticleMatchesTimePeriodPredicate(start, end);
+ assert(!predicate.test(FOUR));
+ }
+ //EP Test article was published before predicate period
+ @Test
+ public void test_beforePeriod_returnFalse() throws ParseException {
+ PublicationDate start = ParserUtil.parsePublicationDate(START);
+ PublicationDate end = ParserUtil.parsePublicationDate(END);
+ ArticleMatchesTimePeriodPredicate predicate = new ArticleMatchesTimePeriodPredicate(start, end);
+ assert(!predicate.test(OLD));
+ }
+ //EP Boundary Start date = End date = Article Publish date
+ @Test
+ public void test_startEqualsToEndAndPublish_returnFalse() throws ParseException {
+ PublicationDate start = ParserUtil.parsePublicationDate("03-01-2021");
+ PublicationDate end = ParserUtil.parsePublicationDate("03-01-2021");
+ ArticleMatchesTimePeriodPredicate predicate = new ArticleMatchesTimePeriodPredicate(start, end);
+ assert(!predicate.test(ONCE));
+ }
+}
diff --git a/src/test/java/seedu/address/model/article/AuthorTest.java b/src/test/java/seedu/address/model/article/AuthorTest.java
new file mode 100644
index 00000000000..7eb9d92b435
--- /dev/null
+++ b/src/test/java/seedu/address/model/article/AuthorTest.java
@@ -0,0 +1,79 @@
+package seedu.address.model.article;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static seedu.address.testutil.Assert.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+public class AuthorTest {
+
+ @Test
+ public void constructor_null_throwsNullPointerException() {
+ assertThrows(NullPointerException.class, () -> new Author(null));
+ }
+
+ @Test
+ public void constructor_invalidAuthor_throwsIllegalArgumentException() {
+ String invalidAuthor = "";
+ assertThrows(IllegalArgumentException.class, () -> new Author(invalidAuthor));
+ }
+
+ @Test
+ public void isValidAuthor() {
+ // null author name
+ assertThrows(NullPointerException.class, () -> Author.isValidAuthorName(null));
+
+ // blank author name
+ assertFalse(Author.isValidAuthorName("")); // empty string
+
+ // whitespaces only
+ assertTrue(Author.isValidAuthorName(" ")); // space only
+ assertTrue(Author.isValidAuthorName(" ")); // multiple spaces only
+ assertFalse(Author.isValidAuthorName(" ".trim())); // space only with expected parser pre-processing
+ assertFalse(Author.isValidAuthorName(" ".trim())); // multiple spaces with expected parser pre-processing
+
+ // whitespaces in author name
+ assertTrue(Author.isValidAuthorName("myauthor")); // alphabetical input
+ assertTrue(Author.isValidAuthorName("myauthor ")); // trailing space
+ assertTrue(Author.isValidAuthorName("myauthor ")); // multiple trailing spaces
+ assertTrue(Author.isValidAuthorName("my author")); // middle space
+ assertTrue(Author.isValidAuthorName("my author")); // multiple middle spaces
+ assertTrue(Author.isValidAuthorName(" myauthor")); // leading space
+ assertTrue(Author.isValidAuthorName(" myauthor")); // multiple leading spaces
+ assertTrue(Author.isValidAuthorName("myauthor123")); // alphanumeric input
+ assertTrue(Author.isValidAuthorName("myauthor 123")); // alphanumeric with space
+ assertTrue(Author.isValidAuthorName("my author 123")); // alphanumeric with multiple spaces
+ assertTrue(Author.isValidAuthorName("my author 123 ")); // alphanumeric with trailing space
+ assertTrue(Author.isValidAuthorName("my author 123 ")); // alphanumeric with trailing spaces
+ assertTrue(Author.isValidAuthorName(" my author 123")); // alphanumeric with leading space
+ assertTrue(Author.isValidAuthorName(" my author 123")); // alphanumeric with leading spaces
+
+ // invalid author names
+ assertFalse(Author.isValidAuthorName(":")); // special characters
+ assertFalse(Author.isValidAuthorName("my:author")); // special characters with alphabetical input
+ assertFalse(Author.isValidAuthorName("my author.")); // special characters with space
+ assertFalse(Author.isValidAuthorName("myauthor:123")); // special characters with alphanumeric input
+ assertFalse(Author.isValidAuthorName("my author: 123")); // special characters with alphanumeric and space
+ }
+
+ @Test
+ public void equals() {
+ Author author = new Author("my author");
+
+ // same values -> returns true
+ assertTrue(author.equals(new Author("my author")));
+
+ // same object -> returns true
+ assertTrue(author.equals(author));
+
+ // null -> returns false
+ assertFalse(author.equals(null));
+
+ // different types -> returns false
+ assertFalse(author.equals(5.0f));
+
+ // different author name -> returns false
+ assertFalse(author.equals(new Author("not my author")));
+ }
+}
diff --git a/src/test/java/seedu/address/model/article/LinkTest.java b/src/test/java/seedu/address/model/article/LinkTest.java
new file mode 100644
index 00000000000..9766e9f8d72
--- /dev/null
+++ b/src/test/java/seedu/address/model/article/LinkTest.java
@@ -0,0 +1,80 @@
+package seedu.address.model.article;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static seedu.address.testutil.Assert.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+public class LinkTest {
+
+ @Test
+ public void constructor_null_createsEmptyLink() {
+ assertTrue(new Link("").equals(new Link(null)));
+ }
+
+ @Test
+ public void constructor_invalidLink_throwsIllegalArgumentException() {
+ String invalidLink = " ";
+ assertThrows(IllegalArgumentException.class, () -> new Link(invalidLink));
+ }
+
+ @Test
+ public void isValidLink() {
+ //null Link
+ assertThrows(NullPointerException.class, () -> Link.isValidLink(null));
+
+ // blank Link
+ assertTrue(Link.isValidLink("")); // empty string
+
+ // whitespaces only
+ assertFalse(Link.isValidLink(" ")); // space only
+ assertFalse(Link.isValidLink(" ")); // multiple spaces only
+
+ // whitespaces in Link
+ assertTrue(Link.isValidLink("mylink ")); // trailing space
+ assertTrue(Link.isValidLink("mylink ")); // multiple trailing spaces
+ assertTrue(Link.isValidLink("my link")); // middle space
+ assertTrue(Link.isValidLink("my link")); // multiple middle spaces
+
+ // invalid Links
+ assertFalse(Link.isValidLink(" mylink")); // leading space
+ assertFalse(Link.isValidLink(" mylink")); // multiple leading spaces
+
+ // valid Links
+ assertTrue(Link.isValidLink("mylink")); // alphabets only
+ assertTrue(Link.isValidLink("12345")); // numbers only
+ assertTrue(Link.isValidLink("my link")); // alphabets with space
+ assertTrue(Link.isValidLink("123 45")); // numbers with space
+ assertTrue(Link.isValidLink("my link 123")); // alphanumeric with spaces
+ assertTrue(Link.isValidLink("~!@#$%^&*()_+`-={}|[]:,.?/;" + "\"")); // special characters only
+ assertTrue(Link.isValidLink("~!@#$%^&* ()_+`-={}|[]:,.?/;" + " \"")); // special characters with spaces
+ assertTrue(Link.isValidLink("my link: 123")); // alphanumeric with special characters and spaces
+ assertTrue(Link.isValidLink("my link: 123 ")); // alphanumeric with special characters and trailing space
+ assertTrue(Link.isValidLink("试试看")); // non-english unicode characters
+ assertTrue(Link.isValidLink("试试 看")); // non-english unicode characters with space
+ assertTrue(Link.isValidLink("试试看 123")); // non-english unicode characters with numbers and space
+ assertTrue(Link.isValidLink("试试看: #123@")); // unicode characters with special characters, and spaces
+ assertTrue(Link.isValidLink("试试看: #123@ ")); // mixed unicode characters with trailing space
+ }
+
+ @Test
+ public void equals() {
+ Link link = new Link("my link");
+
+ // same values -> returns true
+ assertTrue(link.equals(new Link("my link")));
+
+ // same object -> returns true
+ assertTrue(link.equals(link));
+
+ // null -> returns false
+ assertFalse(link.equals(null));
+
+ // different types -> returns false
+ assertFalse(link.equals(5.0f));
+
+ // different link -> returns false
+ assertFalse(link.equals(new Link("not my link")));
+ }
+}
diff --git a/src/test/java/seedu/address/model/article/OutletTest.java b/src/test/java/seedu/address/model/article/OutletTest.java
new file mode 100644
index 00000000000..a56bbb011ee
--- /dev/null
+++ b/src/test/java/seedu/address/model/article/OutletTest.java
@@ -0,0 +1,78 @@
+package seedu.address.model.article;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static seedu.address.testutil.Assert.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+public class OutletTest {
+ @Test
+ public void constructor_null_throwsNullPointerException() {
+ assertThrows(NullPointerException.class, () -> new Outlet(null));
+ }
+
+ @Test
+ public void constructor_invalidOutlet_throwsIllegalArgumentException() {
+ String invalidOutlet = "";
+ assertThrows(IllegalArgumentException.class, () -> new Outlet(invalidOutlet));
+ }
+
+ @Test
+ public void isValidOutlet() {
+ // null outlet name
+ assertThrows(NullPointerException.class, () -> Outlet.isValidOutletName(null));
+
+ // blank outlet name
+ assertFalse(Outlet.isValidOutletName("")); // empty string
+
+ // whitespaces only
+ assertTrue(Outlet.isValidOutletName(" ")); // space only
+ assertTrue(Outlet.isValidOutletName(" ")); // multiple spaces only
+ assertFalse(Outlet.isValidOutletName(" ".trim())); // space only with expected parser pre-processing
+ assertFalse(Outlet.isValidOutletName(" ".trim())); // multiple spaces with expected parser pre-processing
+
+ // whitespaces in outlet name
+ assertTrue(Outlet.isValidOutletName("myoutlet")); // alphabetical input
+ assertTrue(Outlet.isValidOutletName("myoutlet ")); // trailing space
+ assertTrue(Outlet.isValidOutletName("myoutlet ")); // multiple trailing spaces
+ assertTrue(Outlet.isValidOutletName("my outlet")); // middle space
+ assertTrue(Outlet.isValidOutletName("my outlet")); // multiple middle spaces
+ assertTrue(Outlet.isValidOutletName(" myoutlet")); // leading space
+ assertTrue(Outlet.isValidOutletName(" myoutlet")); // multiple leading spaces
+ assertTrue(Outlet.isValidOutletName("myoutlet123")); // alphanumeric input
+ assertTrue(Outlet.isValidOutletName("myoutlet 123")); // alphanumeric with space
+ assertTrue(Outlet.isValidOutletName("my outlet 123")); // alphanumeric with multiple spaces
+ assertTrue(Outlet.isValidOutletName("my outlet 123 ")); // alphanumeric with trailing space
+ assertTrue(Outlet.isValidOutletName("my outlet 123 ")); // alphanumeric with trailing spaces
+ assertTrue(Outlet.isValidOutletName(" my outlet 123")); // alphanumeric with leading space
+ assertTrue(Outlet.isValidOutletName(" my outlet 123")); // alphanumeric with leading spaces
+
+ // invalid outlet names
+ assertFalse(Outlet.isValidOutletName(":")); // special characters
+ assertFalse(Outlet.isValidOutletName("my:outlet")); // special characters with alphabetical input
+ assertFalse(Outlet.isValidOutletName("my outlet.")); // special characters with space
+ assertFalse(Outlet.isValidOutletName("myoutlet:123")); // special characters with alphanumeric input
+ assertFalse(Outlet.isValidOutletName("my outlet: 123")); // special characters with alphanumeric and space
+ }
+
+ @Test
+ public void equals() {
+ Outlet outlet = new Outlet("my outlet");
+
+ // same values -> returns true
+ assertTrue(outlet.equals(new Outlet("my outlet")));
+
+ // same object -> returns true
+ assertTrue(outlet.equals(outlet));
+
+ // null -> returns false
+ assertFalse(outlet.equals(null));
+
+ // different types -> returns false
+ assertFalse(outlet.equals(5.0f));
+
+ // different outlet name -> returns false
+ assertFalse(outlet.equals(new Outlet("not my outlet")));
+ }
+}
diff --git a/src/test/java/seedu/address/model/article/SourceTest.java b/src/test/java/seedu/address/model/article/SourceTest.java
new file mode 100644
index 00000000000..91747674081
--- /dev/null
+++ b/src/test/java/seedu/address/model/article/SourceTest.java
@@ -0,0 +1,79 @@
+package seedu.address.model.article;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static seedu.address.testutil.Assert.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+public class SourceTest {
+
+ @Test
+ public void constructor_null_throwsNullPointerException() {
+ assertThrows(NullPointerException.class, () -> new Source(null));
+ }
+
+ @Test
+ public void constructor_invalidSource_throwsIllegalArgumentException() {
+ String invalidSource = "";
+ assertThrows(IllegalArgumentException.class, () -> new Source(invalidSource));
+ }
+
+ @Test
+ public void isValidSource() {
+ // null source name
+ assertThrows(NullPointerException.class, () -> Source.isValidSourceName(null));
+
+ // blank source name
+ assertFalse(Source.isValidSourceName("")); // empty string
+
+ // whitespaces only
+ assertTrue(Source.isValidSourceName(" ")); // space only
+ assertTrue(Source.isValidSourceName(" ")); // multiple spaces only
+ assertFalse(Source.isValidSourceName(" ".trim())); // space only with expected parser pre-processing
+ assertFalse(Source.isValidSourceName(" ".trim())); // multiple spaces with expected parser pre-processing
+
+ // whitespaces in source name
+ assertTrue(Source.isValidSourceName("mysource")); // alphabetical input
+ assertTrue(Source.isValidSourceName("mysource ")); // trailing space
+ assertTrue(Source.isValidSourceName("mysource ")); // multiple trailing spaces
+ assertTrue(Source.isValidSourceName("my source")); // middle space
+ assertTrue(Source.isValidSourceName("my source")); // multiple middle spaces
+ assertTrue(Source.isValidSourceName(" mysource")); // leading space
+ assertTrue(Source.isValidSourceName(" mysource")); // multiple leading spaces
+ assertTrue(Source.isValidSourceName("mysource123")); // alphanumeric input
+ assertTrue(Source.isValidSourceName("mysource 123")); // alphanumeric with space
+ assertTrue(Source.isValidSourceName("my source 123")); // alphanumeric with multiple spaces
+ assertTrue(Source.isValidSourceName("my source 123 ")); // alphanumeric with trailing space
+ assertTrue(Source.isValidSourceName("my source 123 ")); // alphanumeric with trailing spaces
+ assertTrue(Source.isValidSourceName(" my source 123")); // alphanumeric with leading space
+ assertTrue(Source.isValidSourceName(" my source 123")); // alphanumeric with leading spaces
+
+ // invalid source names
+ assertFalse(Source.isValidSourceName(":")); // special characters
+ assertFalse(Source.isValidSourceName("my:source")); // special characters with alphabetical input
+ assertFalse(Source.isValidSourceName("my source.")); // special characters with space
+ assertFalse(Source.isValidSourceName("mysource:123")); // special characters with alphanumeric input
+ assertFalse(Source.isValidSourceName("my source: 123")); // special characters with alphanumeric and space
+ }
+
+ @Test
+ public void equals() {
+ Source source = new Source("my source");
+
+ // same values -> returns true
+ assertTrue(source.equals(new Source("my source")));
+
+ // same object -> returns true
+ assertTrue(source.equals(source));
+
+ // null -> returns false
+ assertFalse(source.equals(null));
+
+ // different types -> returns false
+ assertFalse(source.equals(5.0f));
+
+ // different source name -> returns false
+ assertFalse(source.equals(new Source("not my source")));
+ }
+}
diff --git a/src/test/java/seedu/address/model/article/TitleTest.java b/src/test/java/seedu/address/model/article/TitleTest.java
new file mode 100644
index 00000000000..8d4f5219a8f
--- /dev/null
+++ b/src/test/java/seedu/address/model/article/TitleTest.java
@@ -0,0 +1,80 @@
+package seedu.address.model.article;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static seedu.address.testutil.Assert.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+public class TitleTest {
+
+ @Test
+ public void constructor_null_throwsNullPointerException() {
+ assertThrows(NullPointerException.class, () -> new Title(null));
+ }
+
+ @Test
+ public void constructor_invalidTitle_throwsIllegalArgumentException() {
+ String invalidTitle = " ";
+ assertThrows(IllegalArgumentException.class, () -> new Title(invalidTitle));
+ }
+
+ @Test
+ public void isValidTitle() {
+ //null title
+ assertThrows(NullPointerException.class, () -> Title.isValidTitle(null));
+
+ // blank title
+ assertTrue(Title.isValidTitle("")); // empty string
+
+ // whitespaces only
+ assertFalse(Title.isValidTitle(" ")); // space only
+ assertFalse(Title.isValidTitle(" ")); // multiple spaces only
+
+ // whitespaces in title
+ assertTrue(Title.isValidTitle("mytitle ")); // trailing space
+ assertTrue(Title.isValidTitle("mytitle ")); // multiple trailing spaces
+ assertTrue(Title.isValidTitle("my title")); // middle space
+ assertTrue(Title.isValidTitle("my title")); // multiple middle spaces
+
+ // invalid titles
+ assertFalse(Title.isValidTitle(" mytitle")); // leading space
+ assertFalse(Title.isValidTitle(" mytitle")); // multiple leading spaces
+
+ // valid titles
+ assertTrue(Title.isValidTitle("mytitle")); // alphabets only
+ assertTrue(Title.isValidTitle("12345")); // numbers only
+ assertTrue(Title.isValidTitle("my title")); // alphabets with space
+ assertTrue(Title.isValidTitle("123 45")); // numbers with space
+ assertTrue(Title.isValidTitle("my title 123")); // alphanumeric with spaces
+ assertTrue(Title.isValidTitle("~!@#$%^&*()_+`-={}|[]:,.?/;" + "\"")); // special characters only
+ assertTrue(Title.isValidTitle("~!@#$%^&* ()_+`-={}|[]:,.?/;" + " \"")); // special characters with spaces
+ assertTrue(Title.isValidTitle("my title: 123")); // alphanumeric with special characters and spaces
+ assertTrue(Title.isValidTitle("my title: 123 ")); // alphanumeric with special characters and trailing space
+ assertTrue(Title.isValidTitle("试试看")); // non-english unicode characters
+ assertTrue(Title.isValidTitle("试试 看")); // non-english unicode characters with space
+ assertTrue(Title.isValidTitle("试试看 123")); // non-english unicode characters with numbers and space
+ assertTrue(Title.isValidTitle("试试看: #123@")); // unicode characters with special characters, and spaces
+ assertTrue(Title.isValidTitle("试试看: #123@ ")); // mixed unicode characters with trailing space
+ }
+
+ @Test
+ public void equals() {
+ Title title = new Title("my title");
+
+ // same values -> returns true
+ assertTrue(title.equals(new Title("my title")));
+
+ // same object -> returns true
+ assertTrue(title.equals(title));
+
+ // null -> returns false
+ assertFalse(title.equals(null));
+
+ // different types -> returns false
+ assertFalse(title.equals(5.0f));
+
+ // different title -> returns false
+ assertFalse(title.equals(new Title("not my title")));
+ }
+}
diff --git a/src/test/java/seedu/address/model/person/NameTest.java b/src/test/java/seedu/address/model/person/NameTest.java
index 94e3dd726bd..c1ea9fb42f6 100644
--- a/src/test/java/seedu/address/model/person/NameTest.java
+++ b/src/test/java/seedu/address/model/person/NameTest.java
@@ -38,6 +38,41 @@ public void isValidName() {
assertTrue(Name.isValidName("David Roger Jackson Ray Jr 2nd")); // long names
}
+ @Test
+ public void compareTo() {
+ Name fname = new Name("Alice");
+ Name sname = new Name("Bob");
+ Name tname = new Name("Charlie");
+
+
+ // same values -> returns 0
+ assertTrue(fname.compareTo(fname) == 0);
+ assertTrue(sname.compareTo(sname) == 0);
+ assertTrue(tname.compareTo(tname) == 0);
+ assertFalse(fname.compareTo(sname) == 0);
+ assertFalse(sname.compareTo(tname) == 0);
+ assertFalse(tname.compareTo(fname) == 0);
+
+ // Name object where method is invoked upon with lower lexicographical ordering
+ // than the Name object passed in as argument -> returns negative value
+ assertTrue(fname.compareTo(sname) < 0);
+ assertTrue(sname.compareTo(tname) < 0);
+ assertTrue(fname.compareTo(tname) < 0);
+ assertFalse(sname.compareTo(fname) < 0);
+ assertFalse(tname.compareTo(sname) < 0);
+ assertFalse(tname.compareTo(fname) < 0);
+
+ // Name object where method is invoked upon with higher lexicographical ordering
+ // than the Name object passed in as argument -> returns positive value
+ assertTrue(sname.compareTo(fname) > 0);
+ assertTrue(tname.compareTo(sname) > 0);
+ assertTrue(tname.compareTo(fname) > 0);
+ assertFalse(fname.compareTo(sname) > 0);
+ assertFalse(sname.compareTo(tname) > 0);
+ assertFalse(fname.compareTo(tname) > 0);
+
+ }
+
@Test
public void equals() {
Name name = new Name("Valid Name");
diff --git a/src/test/java/seedu/address/model/person/PersonTest.java b/src/test/java/seedu/address/model/person/PersonTest.java
index 31a10d156c9..46e310dcacd 100644
--- a/src/test/java/seedu/address/model/person/PersonTest.java
+++ b/src/test/java/seedu/address/model/person/PersonTest.java
@@ -37,18 +37,18 @@ public void isSamePerson() {
.withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND).build();
assertTrue(ALICE.isSamePerson(editedAlice));
- // different name, all other attributes same -> returns false
- editedAlice = new PersonBuilder(ALICE).withName(VALID_NAME_BOB).build();
- assertFalse(ALICE.isSamePerson(editedAlice));
-
- // name differs in case, all other attributes same -> returns false
+ // name differs in case, all other attributes same -> returns true
Person editedBob = new PersonBuilder(BOB).withName(VALID_NAME_BOB.toLowerCase()).build();
- assertFalse(BOB.isSamePerson(editedBob));
+ assertTrue(BOB.isSamePerson(editedBob));
- // name has trailing spaces, all other attributes same -> returns false
+ // name has trailing spaces, all other attributes same -> returns true
String nameWithTrailingSpaces = VALID_NAME_BOB + " ";
editedBob = new PersonBuilder(BOB).withName(nameWithTrailingSpaces).build();
- assertFalse(BOB.isSamePerson(editedBob));
+ assertTrue(BOB.isSamePerson(editedBob));
+
+ // different name, all other attributes same -> returns false
+ editedAlice = new PersonBuilder(ALICE).withName(VALID_NAME_BOB).build();
+ assertFalse(ALICE.isSamePerson(editedAlice));
}
@Test
diff --git a/src/test/java/seedu/address/model/person/UniquePersonListTest.java b/src/test/java/seedu/address/model/person/UniquePersonListTest.java
index 17ae501df08..c18e0cab8bb 100644
--- a/src/test/java/seedu/address/model/person/UniquePersonListTest.java
+++ b/src/test/java/seedu/address/model/person/UniquePersonListTest.java
@@ -5,9 +5,11 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB;
import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME;
import static seedu.address.testutil.Assert.assertThrows;
import static seedu.address.testutil.TypicalPersons.ALICE;
import static seedu.address.testutil.TypicalPersons.BOB;
+import static seedu.address.testutil.TypicalPersons.CARL;
import java.util.Arrays;
import java.util.Collections;
@@ -162,6 +164,29 @@ public void setPersons_listWithDuplicatePersons_throwsDuplicatePersonException()
assertThrows(DuplicatePersonException.class, () -> uniquePersonList.setPersons(listWithDuplicatePersons));
}
+ @Test
+ void sortPersons_nullPrefix_throwsNullPointerException() {
+ assertThrows(NullPointerException.class, () -> uniquePersonList.sortPersons(null));
+ }
+
+ @Test
+ void sortPersons_prefix_sortsUniquePersonListAccordingToPrefix() {
+ uniquePersonList.add(CARL);
+ uniquePersonList.add(BOB);
+ uniquePersonList.add(ALICE);
+ uniquePersonList.sortPersons(PREFIX_NAME.getPrefix());
+ UniquePersonList expectedUniquePersonList = new UniquePersonList();
+ expectedUniquePersonList.add(ALICE);
+ expectedUniquePersonList.add(BOB);
+ expectedUniquePersonList.add(CARL);
+ assertEquals(expectedUniquePersonList, uniquePersonList);
+ }
+
+ @Test
+ void sortPersons_disallowedPrefix_throwsIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () -> uniquePersonList.sortPersons("t/"));
+ }
+
@Test
public void asUnmodifiableObservableList_modifyList_throwsUnsupportedOperationException() {
assertThrows(UnsupportedOperationException.class, ()
diff --git a/src/test/java/seedu/address/storage/JsonAdaptedArticleTest.java b/src/test/java/seedu/address/storage/JsonAdaptedArticleTest.java
new file mode 100644
index 00000000000..4cddc145918
--- /dev/null
+++ b/src/test/java/seedu/address/storage/JsonAdaptedArticleTest.java
@@ -0,0 +1,59 @@
+package seedu.address.storage;
+
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static seedu.address.testutil.TypicalArticles.ONCE;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.commons.exceptions.IllegalValueException;
+import seedu.address.model.article.Article;
+
+public class JsonAdaptedArticleTest {
+ private static final String VALID_TITLE = ONCE.getTitle().toString();
+ private static final List VALID_AUTHORS = ONCE.getAuthors().stream()
+ .map(JsonAdaptedAuthor::new).collect(Collectors.toList());
+ private static final String VALID_DATE = ONCE.getPublicationDateAsString();
+ private static final List VALID_SOURCES = ONCE.getSources().stream()
+ .map(JsonAdaptedSource::new).collect(Collectors.toList());
+ private static final List VALID_TAGS = ONCE.getTags().stream()
+ .map(JsonAdaptedTag::new).collect(Collectors.toList());
+ private static final List VALID_OUTLETS = ONCE.getOutlets().stream()
+ .map(JsonAdaptedOutlet::new).collect(Collectors.toList());
+ private static final Article.Status VALID_STATUS = ONCE.getStatus();
+ private static final String VALID_LINK = ONCE.getLink().toString();
+ @Test
+ public void toModelType_validArticleDetails_returnArticle() throws IllegalValueException {
+ JsonAdaptedArticle article = new JsonAdaptedArticle(ONCE);
+ assertEquals(ONCE, article.toModelType());
+ }
+ @Test
+ public void toModelType_invalidName_returnException() {
+ JsonAdaptedArticle article = new JsonAdaptedArticle(null, VALID_AUTHORS, VALID_SOURCES, VALID_TAGS,
+ VALID_OUTLETS, VALID_DATE, VALID_STATUS, VALID_LINK);
+ assertThrows(IllegalValueException.class, article::toModelType);
+ }
+ //Test exception throwing when status is null.
+ @Test
+ public void toModelType_invalidStatus_returnException() {
+ JsonAdaptedArticle article = new JsonAdaptedArticle(VALID_TITLE, VALID_AUTHORS, VALID_SOURCES, VALID_TAGS,
+ VALID_OUTLETS, VALID_DATE, null, VALID_LINK);
+ assertThrows(IllegalValueException.class, article::toModelType);
+ }
+ @Test
+ public void toModelType_invalidDate_returnException() {
+ JsonAdaptedArticle article = new JsonAdaptedArticle(VALID_TITLE, VALID_AUTHORS, VALID_SOURCES,
+ VALID_TAGS, VALID_OUTLETS, "December", VALID_STATUS, VALID_LINK);
+ assertThrows(IllegalValueException.class, article::toModelType);
+ }
+ @Test
+ public void toModelType_invalidDateNull_returnException() {
+ JsonAdaptedArticle article = new JsonAdaptedArticle(VALID_TITLE, VALID_AUTHORS, VALID_SOURCES,
+ VALID_TAGS, VALID_OUTLETS, null, VALID_STATUS, VALID_LINK);
+ assertThrows(NullPointerException.class, article::toModelType);
+ }
+}
diff --git a/src/test/java/seedu/address/storage/StorageManagerTest.java b/src/test/java/seedu/address/storage/StorageManagerTest.java
index 99a16548970..3c950ac55ed 100644
--- a/src/test/java/seedu/address/storage/StorageManagerTest.java
+++ b/src/test/java/seedu/address/storage/StorageManagerTest.java
@@ -26,7 +26,8 @@ public class StorageManagerTest {
public void setUp() {
JsonAddressBookStorage addressBookStorage = new JsonAddressBookStorage(getTempFilePath("ab"));
JsonUserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(getTempFilePath("prefs"));
- storageManager = new StorageManager(addressBookStorage, userPrefsStorage);
+ JsonArticleBookStorage articleBookStorage = new JsonArticleBookStorage(getTempFilePath("artb"));
+ storageManager = new StorageManager(addressBookStorage, userPrefsStorage, articleBookStorage);
}
private Path getTempFilePath(String fileName) {
diff --git a/src/test/java/seedu/address/testutil/ArticleBookBuilder.java b/src/test/java/seedu/address/testutil/ArticleBookBuilder.java
new file mode 100644
index 00000000000..7fb2937c0a9
--- /dev/null
+++ b/src/test/java/seedu/address/testutil/ArticleBookBuilder.java
@@ -0,0 +1,34 @@
+package seedu.address.testutil;
+
+import seedu.address.model.ArticleBook;
+import seedu.address.model.article.Article;
+
+/**
+ * A utility class to help with building ArticleBook objects.
+ * Example usage:
+ * {@code ArticleBook ab = new ArticleBookBuilder().withArticle("NVIDIA", "INTEL").build();}
+ */
+public class ArticleBookBuilder {
+
+ private ArticleBook articleBook;
+
+ public ArticleBookBuilder() {
+ articleBook = new ArticleBook();
+ }
+
+ public ArticleBookBuilder(ArticleBook articleBook) {
+ this.articleBook = articleBook;
+ }
+
+ /**
+ * Adds a new {@code Article} to the {@code ArticleBook} that we are building.
+ */
+ public ArticleBookBuilder withArticle(Article article) {
+ articleBook.addArticle(article);
+ return this;
+ }
+
+ public ArticleBook build() {
+ return articleBook;
+ }
+}
diff --git a/src/test/java/seedu/address/testutil/ArticleBuilder.java b/src/test/java/seedu/address/testutil/ArticleBuilder.java
new file mode 100644
index 00000000000..0bd43a04e59
--- /dev/null
+++ b/src/test/java/seedu/address/testutil/ArticleBuilder.java
@@ -0,0 +1,147 @@
+package seedu.address.testutil;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import seedu.address.logic.parser.ParserUtil;
+import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.article.Article;
+import seedu.address.model.article.Article.Status;
+import seedu.address.model.article.Author;
+import seedu.address.model.article.Link;
+import seedu.address.model.article.Outlet;
+import seedu.address.model.article.PublicationDate;
+import seedu.address.model.article.Source;
+import seedu.address.model.article.Title;
+import seedu.address.model.tag.Tag;
+import seedu.address.model.util.SampleArticleDataUtil;
+
+/**
+ * A utility class to help with building Article objects.
+ */
+public class ArticleBuilder {
+
+ public static final String DEFAULT_TITLE = "Once upon a time";
+ public static final String DEFAULT_AUTHOR = "Barney Loo";
+ public static final String DEFAULT_PUBLICATION_DATE = "03-01-2021"; // In dd-MM-yyyy format.
+ public static final String DEFAULT_SOURCE = "Domo Dragto";
+ public static final String DEFAULT_OUTLET = "The Straits Times";
+ public static final String DEFAULT_TAG = "Fantasy";
+ public static final String DEFAULT_STATUS = "DRAFT";
+ public static final String DEFAULT_LINK = "https://www.google.com/";
+
+ private Title title;
+ private Set authors;
+ private PublicationDate publicationDate;
+ private Set sources;
+ private Set outlets;
+ private Set tags;
+ private Status status;
+ private Link link;
+ /**
+ * Creates a {@code ArticleBuilder} with the default details.
+ */
+ public ArticleBuilder() {
+ title = new Title(DEFAULT_TITLE);
+ authors = new HashSet<>();
+ authors.add(new Author(DEFAULT_AUTHOR));
+ try {
+ publicationDate = ParserUtil.parsePublicationDate(DEFAULT_PUBLICATION_DATE);
+ } catch (ParseException e) {
+ assert false : "Default publication date should be valid.";
+ }
+ sources = new HashSet<>();
+ sources.add(new Source(DEFAULT_SOURCE));
+ outlets = new HashSet<>();
+ outlets.add(new Outlet(DEFAULT_OUTLET));
+ tags = new HashSet<>();
+ tags.add(new Tag(DEFAULT_TAG));
+ status = Status.valueOf(DEFAULT_STATUS);
+ link = new Link(DEFAULT_LINK);
+ }
+
+ /**
+ * Initializes the ArticleBuilder with the data of {@code articleToCopy}.
+ */
+ public ArticleBuilder(Article articleToCopy) {
+ title = articleToCopy.getTitle();
+ authors = articleToCopy.getAuthors();
+ publicationDate = articleToCopy.getPublicationDate();
+ sources = articleToCopy.getSources();
+ outlets = articleToCopy.getOutlets();
+ tags = articleToCopy.getTags();
+ status = articleToCopy.getStatus();
+ link = articleToCopy.getLink();
+ }
+
+ /**
+ * Sets the {@code Title} of the {@code Article} that we are building.
+ */
+ public ArticleBuilder withTitle(String title) {
+ this.title = new Title(title);
+ return this;
+ }
+
+ /**
+ * Parses the {@code authors} into a {@code Set} and set it to the {@code Article} that we are building.
+ */
+ public ArticleBuilder withAuthors(String ... authors) {
+ this.authors = SampleArticleDataUtil.getAuthorSet(authors);
+ return this;
+ }
+
+ /**
+ * Sets the {@code PublicationDate} of the {@code Article} that we are building.
+ */
+ public ArticleBuilder withPublicationDate(String publicationDate) {
+ try {
+ this.publicationDate = ParserUtil.parsePublicationDate(publicationDate);
+ } catch (ParseException e) {
+ assert false : "Publication date should be valid.";
+ }
+ return this;
+ }
+
+ /**
+ * Parses the {@code sources} into a {@code Set} and set it to the {@code Article} that we are building.
+ */
+ public ArticleBuilder withSources(String ... sources) {
+ this.sources = SampleArticleDataUtil.getSourceSet(sources);
+ return this;
+ }
+
+ /**
+ * Parses the {@code outlets} into a {@code Set