By: Team T10-1
Since: Aug 2019
Licence: MIT
Refer to the guide here.
The Architecture Diagram given above explains the high-level design of the App. Given below is a quick overview of each component.
-
At app launch: Initializes the components in the correct sequence, and connects them up with each other.
-
At shut down: Shuts down the components and invokes cleanup method where necessary.
Commons
represents a collection of classes used byx
multiple other components. The following class plays an important role at the
architecture level:
-
LogsCenter
: Used by many classes to write log messages to the App’s log file.
The rest of the App consists of four components.
Each of the four components
-
Defines its API in an
interface
with the same name as the Component. -
Exposes its functionality using a
{Component Name}Manager
class.
For example, the Logic
component (see the class diagram given below) defines
it’s API in the Logic.java
interface and exposes its functionality using
the LogicManager.java
class.
The Sequence Diagram below shows how the components interact with each
other for the scenario where the user issues the command delete 1
.
The sections below give more details of each component.
API : 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.
The UI
component uses JavaFx UI framework. The layout of these UI parts are
defined in matching .fxml
files that are in the src/main/resources/view
folder. For example, the layout of the
MainWindow
is specified in
MainWindow.fxml
The UI
component,
-
Executes user commands using the
Logic
component. -
Listens for changes to
Model
data so that the UI can be updated with the modified data.
API :
Logic.java
-
Logic
uses theJarvisParser
class to parse the user command. -
This results in a
Command
object which is executed by theLogicManager
. -
The command execution can affect the
Model
(e.g. adding a person). -
The result of the command execution is encapsulated as a
CommandResult
object which is passed back to theUi
. -
In addition, the
CommandResult
object can also instruct theUi
to perform certain actions, such as displaying help to the user.
Given below is the Sequence Diagram for interactions within the Logic
component for the execute("delete 1")
API call.
ℹ️
|
The lifeline for DeleteAddressCommandParser should end at the destroy
marker (X) but due to a limitation of PlantUML, the lifeline reaches the end
of diagram.
|
API : Model.java
The Model
,
-
stores a
UserPref
object that represents the user’s preferences. -
stores the Address Book data.
-
Stores the History Manager data.
-
Stores the Finance Tracker data
-
Stores the Cca Tracker Data
-
Stores the Course Planner Data
-
Stores the Planner data
-
Does not depend on any of the other three components.
API : Storage.java
The Storage
component,
-
can save
UserPref
objects in json format and read it back. -
can save the Address Book, History Manager, Finance Tracker, Cca Tracker, Course Planner and Planner data in json format and read it back.
This section describes some noteworthy details on how certain features are implemented.
The application should be able to undo and redo changes made by commands to give the user more flexibility in their inputs. Undo and redo operations should also be undo or redo multiple commands in a command. In the event that a undo/redo command that comprises of multiple undo/redo operations fails at any point, all changes made by the command should be rolled back. This is reflected in the Activity Diagram below:
Therefore there is a need to remember commands that change the state of the
Model
. Commands that just render a view without actually changing the
application should not be stored as it does not make sense to undo or redo
them. We will distinguish these types of commands into two categories,
invertible commands and non-invertible commands.
-
Invertible commands — commands that mutate the state of the
Model
and should be stored for undo/redo functions. -
Non-invertible commands — commands that do not mutate the state of the
Model
and should not be stored for undo/redo functions.
ℹ️
|
Undo and redo commands will be considered non-invertible commands even though
they technically change the state of the Model . The reason is that they are
commands facilitating the undo and redo operation, thus they should not be
stored.
|
The following activity diagram illustrates how commands are remembered when a user types in a command:
The undo/redo feature mechanism is facilitated by HistoryManager
.
HistoryManager
remembers invertible commands. These commands are stored
internally in two CommandDeque
objects, executedCommands
and
inverselyExecutedCommands
. CommandDeque
serve as custom Deque
data
structure, which stores the latest added command to the top.
An undo operation would comprise of taking the latest executed command from
executedCommands
, inversely executing it, and adding it to
inverselyExecutedCommands
. A redo operation would comprise of a taking the
latest inversely executed command from inverselyExecutedCommands
, executing
it, and adding it to executedCommands
.
Model
supports operations to facilitate undo and redo capabilities by
extending the HistoryModel
which has the following operations:
-
Model#getHistoryManager()
— Gets theHistoryManager
instance. -
Model#setHistoryManager(HistoryManager)
— Resets theHistoryManager
data to the givenHistoryManager
in the argument. -
Model#getAvailableNumberOfExecutedCommands()
— Gets the maximum available number of commands that can be undone. -
Model#getAvailableNumberOfInverselyExecutedCommands()
— Gets the maximum available number of commands that can be redone. -
Model#canRollback()
— Checks if it is possible to undo a command at the given state. -
Model#canCommit()
— Checks if it is possible to redo a command at the given state. -
Model#rememberExecutedCommand(Command)
— Remembers the givenCommand
and stores it inexecutedCommands
to facilitate undo capability for this command. -
Model#rememberInverselyExecutedCommand(Command)
— Remembers the givenCommand
and stores it ininverselyExecutedCommands
to facilitate redo capability for this command. -
Model#rollback()
— Inversely executes the latest command stored inexecutedCommands
to revert the changes of the latest executed command made ontoModel
. -
Model#commit()
— Executes the latest undone command stored ininverselyExecutedCommands
to reapply the changes that were made ontoModel
by the latest undone command.
Commands support the given operations to mutate the state of the Model
and
to check if they should be stored for undo/redo function:
-
Command#hasInverseExecution()
— Checks if the command’s execution mutates the state of theModel
, which is used to determine if the command should be remembered byHistoryManager
. -
Command#execute(Model)
— Executes the command on the givenModel
. -
Command#executeInverse(Model)
— Executes on the givenModel
such that it will undo whatever changes were made whenCommand#execute(Model)
was called.
Below is a class diagram between Model
, ModelManager
, HistoryManager
,
CommandDeque
and Command
.
Undo and redo operations are executed with UndoCommand
and RedoCommand
These commands store an integer value referencing the number of commands to
undo or redo, represented by UndoCommand#numberOfTimes
and
RedoCommand#numberOfTimes
. The Class Diagram below shows details about
UndoCommand
and RedoCommand
.
Below is a Sequence Diagram of how an UndoCommand
executes in the program.
RedoCommand
follows a similar process.
Given below is an example usage scenario of how undo/redo mechanism behaves.
Step 1. The user launches the application for the first time. The
HistoryManager
is initialized. HistoryManager#executedCommands
and
HistoryManager#inverselyExecutedCommands
are empty.
Step 2. The user executes delete 5
command to delete the 5th person in the
address book. A DeleteAddressCommand
is created and executed in
LogicManager#execute(String)
. Since DeleteCommand
is an invertible
command, HistoryManager
remembers the command, adding it to
HistoryManager#executedCommands
.
Step 3. The user executes add n/David …
to add a new person.
A AddAddressCommand
is created and executed in
LogicManager#execute(String)
. Since AddAddressCommand
is an invertible
command, HistoryManager
remembers the command, adding it to
HistoryManager#executedCommands
.
ℹ️
|
If a invertible command execution fails, HistoryManager will not remember
it, therefore it will not be stored for undo/redo capabilities.
|
Step 4. The user now decides that the last two commands entered was a mistake,
and decides to undo those commands by executing the undo
command by typing
in the command undo r/2
. An UndoCommand
is created and executed in
LogicManager#execute(String)
to undo the latest two commands. The command
will call Model#rollback()
two times. During each Model#rollback()
call,
the Model
will call HistoryManager
to take the latest command from
HistoryManager#executedCommands
and call Command#executeInverse(Model)
on
the Model
, undoing the changes made to Model
by the command, before adding
it to HistoryManager#inverselyExecutedCommands
. After the undo
command
execution is complete, the Model
state is reverted to what it was before the
two undone commands were executed.
ℹ️
|
undo /redo commands can undo/redo one or more commands. To undo/redo one
command, entering undo /redo is equivalent to entering undo 1 /redo 1 .
|
ℹ️
|
If an undo /redo command is given to undo/redo more commands than
available, the operation will fail and no undo /redo is applied at all.
This check is enforced by Model#getAvailableNumberOfExecutedCommands() ,
Model#getAvailableNumberOfInverselyExecutedCommands() , Model#canRollback()
and Model#canCommit() .
|
Step 5. The user then decides to execute the command list
. list
command
is a non-invertible command. Therefore, it will not be stored by
HistoryManager
after its execution.
Step 6. The user decides to redo the last command that was undone by executing
a redo
command by typing in the command redo
. A RedoCommand
is created
and executed in LogicManager#execute(String)
to redo the latest undo. The
command will call Model#commit()
once. Model
will call HistoryManager
to take the latest command from HistoryManager#inverselyExecutedCommands
and call Command#execute(Model)
on the Model
, reapplying the changes that
were made by the command, before adding it to
HistoryManager#executedCommands
. After the redo
command execution is
complete, the Model
has the changes made by the latest the command that was
redone.
Step 7. The user executes add n/John …
to add a new person.
A AddAddressCommand
is created and executed in
LogicManager#execute(String)
. The HistoryManager
clears all commands
stored in HistoryManager#inverselyExecutedCommands
. Similar to Step 3
,
HistoryManager
remembers this command.
ℹ️
|
Whenever a new invertible command is executed that is not currently in
HistoryManager , it will clear all the commands that are stored in
HistoryManager#inverselyExecutedCommands . This means that all potential
redo actions are cleared.
|
-
Alternative 1: Saves the entire
Model
.-
Pros: Easy to implement.
-
Cons: May have performance issues in terms of memory usage.
-
-
Alternative 2: Individual command knows how to undo/redo by itself.
-
Pros: Will use less memory (e.g. for
delete
, just save the person being deleted). -
Cons: We must ensure that the implementation of each individual command are correct.
-
-
Alternative 1: Use a
List
to store the history ofModel
states. Maintain a pointer to point to the current version ofModel
, and shift the pointer along the list to facilitate undo/redo operations.-
Pros: Simple implementation.
-
Cons: Expensive on storage as multiple copies of
Model
is stored.
-
-
Alternative 2: Implement invertible commands whereby they support their inverse execution. Use two
Deque
data structures to store the history of commands, to represent executed commands and inversely executed commands. Move commands from one deque to another and executing/inversely executing them to facilitate undo/redo operations.-
Pros: Storage efficient, as application only needs to keep track of invertible commands, and do not need to store multiple copies of
Model
. -
Cons: Implementation is more complex.
-
The Course Planner feature allows the user to track what courses they
-
Have taken
-
Are taking, and
-
Want to take
The feature offers updated information on courses offered by NUS, along with convenient add, delete and check operations on the user’s course list.
The Course Planner feature closely follows the extendable OOP solution
implemented within Jarvis. Within model
, the CoursePlanner
class
manages all aspects related to this feature.
The list of courses of the user is stored internally using a UniqueCourseList
object, providing an abstraction with add
, delete
and getCourseList
operations that are called by CoursePlanner
.
The String
that is displayed to the user within a the UI text box is
abstracted within a class called CourseText
. This is a simple class that
abstracts some operations of operations on a String
, such as setting,
getting, printing to a displayable form, etc.
-
Model#getCoursePlanner()
- Gets theCoursePlanner
instance -
Model#lookUpCourse(Course)
- Looks up a course’s information -
Model#addCourse(Course)
- Adds a course to the user’s list -
Model#addCourse(Index, Course)
- Adds a course to the user’s list at the specified index -
Model#deleteCourse(Course)
- Deletes the given Course from the user’s list -
Model#hasCourse(Course)
- Checks if the user has this course in their list -
Model#getUnfilteredCourseList()
- Returns anObservableList
containing the user’s list of courses -
Model#getCourseText()
- Returns aCourseText
object -
Model#setCourseText(String)
- Sets theString
displayed by theCourseText
object.
Course data-sets are taken directly from the NUSMods API. These
data-sets are stored using the .json
file format on NUSMod’s API. Since
Jarvis already heavily uses the Jackson JSON API, we have opted to store
all course data within Jarvis in their original form. Therefore, all data
is read directly from .json
files.
ℹ️
|
NUSMods is a popular website officially affiliated with NUS, where students are able to look up information about courses and plan their school timetable. This makes its data-set a reliable source of course information. |
Each course, and their data, are given its own file. These files are laid out
in /modinfo
within /resources
to be easily accessible by the program.
A sample, valid AB1234.json
is given below for a fictional course AB1234
.
{ "courseCode": "AB1234", "courseCredit": "4", "description": "Course description for AB1234.", "faculty": "A Faculty in NUS", "fulfillRequirements": [ "AB2234" ], "preclusion": "AB1231, AB1232", "prereqTree": { "and": [ { "or": [ "CD1111", "XY2222" ] }, "EF3333" ] }, "title": "Course AB1234's title" }
The current codebase requires that every course datafile must have the following attributes:
-
courseCode
-
courseCredit
-
title
-
faculty
-
description
These attributes are non-nullable, this is as from the 11000+ course
datafiles downloaded from NUSMods, all at least have these attributes. The
other three: fulfillRequirements
, preclusion
and prereqTree
are
optional, nullable attributes.
The AndOrTree<R>
is a tree data structure served by the util/andor
package that provides an abstraction for processing the prerequisite tree.
The prerequisite tree (henceforth referred to as prereqTree
) is an attribute
of a Course
that is available in the NUSMod’s course data-set, the data
comes in the form of a String
and will be covered shortly.
The following are public
methods in AndOrTree
.
-
buildTree(String, Function<String, ? extends R>)
Builds a tree from the given jsonString.
Function
is a mapper that processes aString
and returns a value of typeR
, whereR
is the type of data stored by each node in the tree. -
fulfills(Collection<R>)
Checks if the given
Collection
of typeR
fulfills the condition specified by this tree.AndOrNode
has its own correspondingfulfill
that checks its children or data againstCollection
.
Due to the arbitrary ordering of the tree, insert()
and delete()
operations commonly found in implementations of ordered trees are
difficult to implement. Instead, the tree is fully created upon the call to
buildTree()
and is then enforced to be immutable once built. This is
reflected in the class' lack of mutator methods.
Each node in the tree of type R
is represented by an AndOrNode<R>
. Every
node can exist as either of these types:
-
AND
Any subset of elements in a
Collection<R>
must match all children of this node. -
OR
Any element in a
Collection<R>
must match at least one of the children of this node -
DATA
Any element in a
Collection<R>
must match the data stored in this node
The following class diagram demonstrates the structure of the abstract class
AndOrNode
class and its sub-classes.
Using this format, AndOrNode#createNode(T,String)
is able to construct
all instances of its sub-class, thus the caller will not need to know the
different types of nodes there are.
As mentioned above, we use the prereqTree
attribute in order to build
the tree. An example of a processable json string is as such:
"prereqTree": { "and": [ { "or": [ "CD1111", "XY2222" ] }, "EF3333" ] }
This can be read as:
To take AB1234, you require... | └ all of ├── one of | ├─ "CD1111" | └─ "XY2222" └─ "EF3333"
This means that to take the fictional course AB1234
, a user would have to
complete EF3333
, and either CD1111
or XY2222
.
The buildTree()
method takes in the json
string as an input. The
Jackson API uses this string to create a root JsonNode
object, and the tree
is built recursively from the root. The sequence diagram of the tree building
process is shown below:
The class looks at each node - checks if its is an Object
, Array
or
a String
, and does the appropriate actions and function calls.
Other ways of building the tree can be easily extended by overloading the
buildTree
method. However, this will not override the immutable properties
of the tree.
-
Alternative 1: Storing every course in a single, large JSON file
-
Pros:
Easier to manage. Every course can be found in a single file. The code need not deal with
FileNotFoundException
orIOException
as the single file is guaranteed to exist. -
Cons:
A large file will be difficult to view for a developer. It will also have slow performance as the entire file would have to be processed to look up one course.
A developer may also:
-
Store the whole file in a buffer for faster lookup, but this may be time-consuming and troublesome to implement.
-
Process the whole file and create all
Course
objects upon start-up. However, due to the large number of course files (11000+), this may have significant memory overhead.
-
-
-
Current Choice: Storing each course as its own file
-
Pros:
Fast lookup as the contents of 11000+ files worth of data do not need to be scanned directly. Fast,
String
concatenation of file paths directly to the file is used instead. -
Cons:
Difficult to manage. If we want to modify the data-sets in any way, a script will have to be written to process every file in the dataset, unless a developer wants to manually change all files.
-
We did not go with alternative 1 as once the files were downloaded and processed, there was no need to modify them any further. Processing, or loading inside a buffer, of very large text files will likely significantly hamper performance for little benefit. Manual lookup information about a specific course during development is also much easier with such a method.
-
Alternative 1:
AndOrTree
dependent onCourse
(or any fixed datatype)-
Pros:
There is no need to pass any mapper function into the
buildTree()
method as the class will already know how to map eachString
to a typeR
. This makes handling exceptions easier as they can be handled directly by the class instead of by the caller. -
Cons:
This increases coupling between the tree and the fixed data-type used by the tree, resulting the correctness of the
AndOrTree
class being dependent on the fixed data-type, as there will be no way to stub it. The tree will also only be locked to a specific data-type and is non-extendable.
-
-
Current choice:
AndOrTree
with generics-
Pros:
This makes the tree reusable in the future. The tree will also be able to store any data-type which allows for easier unit testing, since it won’t be dependent on the correctness of the fixed data-type (
Course
in this instance). Instead well-tested libraries such as Java’sString
API can be used to test the class instead. -
Cons:
Due to how the tree is built (i.e from a json string), a mapper function must be passed into the
buildTree()
method to process the string in each node to the generic type of the tree. The function should be of a typeFunction<String, ? extends R>
, for a tree of typeR
.
-
Due to its benefits far outweighing its disadvantages, we picked out
current choice. While extendability and resusability of the class is a nice
bonus, the decrease in coupling and increase in testability was the deciding
factor in choosing between these two approaches. Furthermore, behavior of
the building of the tree can be easily extended by either inheritance, or
overloading of the buildTree()
method.
The Finance Tracker feature allows the user to track what purchases they have made for the month and the list of installments that they have subscribed to. Such installments would be added to their purchases once a month. The user can store the information of the description and the amount of both purchases and installments. Furthermore, the user can set a monthly limit to keep track of his expenses for the month.
The Finance Tracker feature closely follows the extendable OOP solution already
implemented within AB3. In the Finance Tracker, the Installment
objects
(containing InstallmentDescription
and InstallmentMoneyPaid
objects) and
the Purchase
objects (containing PurchaseDescription
and
PurchaseMoneySpent
objects) manage most aspects related to this feature.
The finance tracker mechanism is facilitated by FinanceTrackerModel
.
-
Model#getFinanceTracker()
- Gets theFinanceTracker
instance -
Model#getPurchase(index)
- Retrieves the purchase at that index -
Model#updateFilteredPurchaseList(Predicate)
- Updates the filter of the purchase list with the predicate -
Model#getFilteredPurchaseList()
- Retrieves the list of purchases with current predicate applied -
Model#addPurchase(Purchase)
- Adds a single use payment -
Model#deletePurchase(index)
- Deletes single use payment at that index -
Model#getInstallment(index)
- Retrieves the installment at that index -
Model#updateFilteredInstallmentList(Predicate)
- Updates the filter of the installment list with the predicate -
Model#getFilteredInstallmentList()
- Retrieves the list of installments with current predicate applied -
Model#addInstallment(Installment)
- Adds an installment -
Model#deleteInstallment(index)
- Deletes installment at that index -
Model#hasInstallment(Installment)
- Checks for the existence of the same installment in the finance tracker -
Model#setInstallment(Installment, Installment)
- Replaces an existing installment with a new installment for editing -
Model#setMonthlyLimit(MonthlyLimit)
- Sets the monthly limit for spending -
Model#getMonthlyLimit()
- Retrieves the monthly limit if it has been set by user
Installments are added by the user to the FinanceTracker and are stored in an
InstallmentList
. The current codebase requires that all installments
must have the following attributes:
-
InstallmentDescription
-
InstallmentMoneyPaid
These attributes are non-nullable*.
Purchases are added by the user to the FinanceTracker and are stored in a
PurchaseList
. The current codebase requires that all purchases must have
the following attributes:
-
PurchaseDescription
-
PurchaseMoneySpent
These attributes are non-nullable*.
For brevity’s sake, we will illustrate only 1 specific command and its
execution on model. The following activity diagram illustrates how an
Installment
is edited when a user types in a edit-install
command:
Step 1. The user launches the application for the first time. The
FinanceTracker
is initialized. Assume that a valid Installment
has already
been added to the InstallmentList
in FinanceTracker
.
Step 2. The user executes
edit-install 1 d/student-price Spotify subscription a/7.50
command to edit both the description and money spent on the existing
Installment in the FinanceTracker. An EditInstallmentCommandParser
object is
created and its #parse
method is called. The parse method returns a new
EditInstallmentCommand
object.
Step 3. The EditInstallmentCommand
object is executed on the model. The
EditInstallmentCommand#execute
method is called, and this will create a new
Installment
object from the existing installment but with all the edited
fields changed. In this method,
Model#setInstallment(Installment, Installment)
method is called.
ℹ️
|
The EditInstallmentCommand#execute method first checks for whether the
index is within the size of InstallmentList.
|
Step 4. As mentioned in section 2, the methods in Model
merely mirrors the
methods in the FinanceTracker
class. As such, the
FinanceTracker#setInstallment(Installment, Installment)
method is called.
This in turns calls the
#InstallmentList#setInstallment(Installment, Installment)
method.
Step 5. This #InstallmentList#setInstallment(Installment, Installment)
method first finds the Installment
based on its corresponding index. Then,
it sets the edited installment at the index found earlier.
ℹ️
|
TLDR: The calling of the #setInstallment method at the FinanceTracker
level triggers a cascading series of #setInstallment method which culminates
in target installment being edited with the corresponding fields.
|
-
Alternative 1 (Current choice): Encapsulate fields into separate classes.
-
Pros:
-
Increases OOP to allow better manipulation of objects and makes objects more extensible.
-
Cons:
More code needed.
-
Alternative 2: Use primitive Java data types for fields in
Installment
andPurchase
objects.-
Pros:
-
Less code needed.
-
Cons:
Less extensible as all data contained is primitive.
We ultimately went with alternative #1 as it allowed us to better practice OOP, providing a clear modular structure for abstracting data types in which implementation details are hidden. Furthermore, it would make the codebase easier to maintain. The objects created can also be reused across the application.
and PurchaseList
.
-
Alternative 1 (Current choice): Implement them as
ObservableList
.-
Pros:
-
Easier to manipulate for JavaFx.
-
Cons:
Potentially complicated nesting when passing arguments to it
-
Alternative 2: Implement them as normal
List`s e.g. `ArrayList
.-
Pros:
-
Does not require predicates to be passed in.
-
Cons:
Might be more complicated when rendering in Javafx.
We ultimately went with alternative #1 as it provided better support for rendering when implementing the Ui of JARVIS.
The planner feature in JARVIS enables users to easily organise and manage their different tasks in school. Users will be able to keep track of tasks they have done, and tasks they have yet to do.
There are three types of tasks in the planner:
-
Todo
: Tasks with a description only -
Event
: Tasks with a start and end date -
Deadline
: Tasks with a due date
Users can Tag
these tasks to sort them into different categories, as well
as add Priority
and Frequency
levels to them.
The Planner
contains a TaskList
, which in turn, contains a number of tasks
a user has. A simple outline of the Planner
can be seen below.
JARVIS' Model
extends PlannerModel
which facilitates all operations
necessary to carry out commands by the user.
-
Model#getPlanner()
— Returns an instance of aPlanner
. -
Model#addTask(int zeroBasedIndex, Task task
— Adds aTask
to the planner at the specifiedIndex
. -
Model#addTask(Task t)
— Adds aTask
to thePlanner
. Since noIndex
is specified, theTask
is appended to the end of theTaskList
. -
Model#deleteTask(Index index)
— Deletes theTask
at the specifiedIndex
from thePlanner
. -
Model#deleteTask(Task t)
— Deletes the specifiedTask
from thePlanner
. -
Model#size()
— Returns the total number ofTask
objects in thePlanner
. -
Model#hasTask(Task t)
— Checks if a givenTask
is already in thePlanner
. -
Model#getTasks()
— Returns theTaskList
in thePlanner
.
-
Alternative 1 (Current choice): As a string attribute in
Task
-
Pros: Intuitive, easy to implement, less code required
-
Cons: Provides a lower level of abstraction, especially when
edit-task
command is implemented
-
-
Alternative 2: Building a separate
TaskDescription
class-
Pros: Higher level of abstraction
-
Cons: More code, will take time to replace current methods that deal with String
TaskDes
directly
-
The application is able to track Ccas. Each user can have multiple Ccas and each Cca can have multiple equipments needed. In addition, the application is able to track the progress of each person in their Ccas. Hence, there is a need to represent the CcaTracker as a list of Ccas on which the application can perform create, read, update and delete operations on each Cca.
The CcaTracker
mechanism is facilitated by CcaTrackerModel
.
Model
supports operations to facilitate
cca tracking capabilities by extending the CcaTrackerModel
which has the
following operations:
-
Model#containsCca(Cca cca)
— Checks if theCcaTracker
contains the given cca. -
Model#addCca(Cca cca)
— Adds aCca
to theCcaTracker
. -
Model#removeCca(Cca cca)
— Removes aCca
from theCcaTracker
. -
Model#updateCca(Cca toBeUpdatedCca, Cca updatedCca)
— Updates aCca
in theCcaTracker
. -
Model#getCcaTracker()
— Gets theCcaTracker
instance. -
Model#getNumberOfCcas()
— Returns the number ofCcas
currently in theCcaTracker
. -
Model#getCca(Index index)
— Gets theCca
instance by its index in theCcaTracker
. -
Model#updateFilteredCcaList(Predicate<Cca> predicate)
— Updates theFilteredCcaList
by passing it a predicate. -
Model#getFilteredCcaList()
— Returns an instance of theFilteredCcaList
-
Model#addProgress(Cca targetCca, CcaProgressList toAddCcaProgressList)
- AddsCcaProgressList
to the targetCca
. -
Model#increaseProgress(Index index)
— Increases the progress of theCca
CcaTracker has 7 specific commands that support the given operations to mutate
the state of the Model
. Each command is represented as seperate class:
-
AddCcaCommand
— Adds aCca
to theCcaTracker
. -
DeleteCcaCommand
— Deletes aCca
from theCcaTracker
. -
EditCcaCommand
— Edits the selectedCca
in theCcaTracker
. -
FindCcaCommand
— Finds aCca
from theCcaTracker
based on the keywords specified . -
ListCcaCommand
— Lists all theCca
from theCcaTracker
. -
AddProgressCommand
— Adds a progress tracker to a cca. -
IncreaseProgressCommand
— Increments the progress level of a cca.
For brevity’s sake, we will illustrate only 1 specific command and its
execution on model. The following activity diagram illustrates how a Cca’s
progress is incremented when a user types in a `increase-progress
command:
Given below is an example usage scenario of how increase-progress mechanism behaves.
Step 1. The user launches the application for the first time. The CcaTracker
is initialized. Assume that a Cca
has already been added to the Cca and that
a progress tracker has already been set for that Cca
.
Step 2. The user executes increase-progress 1
command to increment the
progress of the 1st Cca
in the CcaTracker. A IncreaseProgressCommandParser
object is created and its #parse
method is called. The parse method returns
a new IncreaseProgressCommand
object.
Step 3. The IncreaseProgressCommand
object is then executed on model. The
IncreaseProgressCommand#execute
method is called and in this method, the
Model#increaseProgress
method is called.
ℹ️
|
The IncreaseProgressCommand#execute method first checks for whether the
index is within the size of CcaList.
|
Step 4. As mentioned in section 2, the methods in Model
merely mirrors the
methods in the CcaTracker
class. As such, the CcaTracker#increaseProgress
method is called. This in turn calls the CcaList#increaseProgress
method.
This method first finds the Cca
based on its corresponding index. Then, it
calls the Cca#increaseProgress
method.
Step 5. This in turn calls the CcaProgress#increaseProgress
method that
calls CcaCurrentProgress#increaseProgress
method. At long last, the final
#increaseProgress
method in the CcaCurrentProgress
instance is called and
the currentProgress
counter is incremented by 1.
ℹ️
|
TLDR: The calling of the #increaseProgress method at the CcaTracker level
triggers a cascading series of #increaseProgress methods which culminates in
the currentProgress variable being incremented by 1.
|
-
Alternative 1 (Current choice): Instantiate a generic
CcaProgress
for eachCca
.-
Pros: Less code needed.
-
Cons: Less extensible as CcaProgress is now limited to a list of strings.
-
-
Alternative 2: Implement
CcaProgress
as a parent class. Create classes such as SportProgress/PerformingArtsProgress that extend from CcaProgress for each type ofCca
-
Pros: Easier to extend functionality for each type of cca.
-
Cons: Does not significantly extend functionality for this version of Jarvis.
-
-
Alternative 1 (Current choice): Implement CcaProgressList as an
ObservableList
.-
Pros: Easier to manipulate for JavaFx.
-
Cons: Potentially complicated nesting when passing arguments to it as CcaProgressList is nested several classes within
Cca
.
-
-
Alternative 2: Implement CcaProgressList as a normal
List
e.g.ArrayList
.-
Pros: Does not require predicates to be passed in.
-
Cons: Might be more complicated when rendering in Javafx.
-
We are using java.util.logging
package for logging. The LogsCenter
class
is used to manage the logging levels and logging destinations.
-
The logging level can be controlled using the
logLevel
setting in the configuration file (See Section 3.7, “Configuration”) -
The
Logger
for a class can be obtained usingLogsCenter.getLogger(Class)
which will log messages according to the specified logging level -
Currently log messages are output through:
Console
and to a.log
file.
Logging Levels
-
SEVERE
: Critical problem detected which may possibly cause the termination of the application -
WARNING
: Can continue, but with caution -
INFO
: Information showing the noteworthy actions by the App -
FINE
: Details that is not usually noteworthy but may be useful in debugging e.g. print the actual list instead of just its size
Refer to the guide here.
Refer to the guide here.
Refer to the guide here.
Target user profile:
-
NUS student
-
plans his own modules
-
prefers typing over mouse input
-
can type fast
-
is reasonably comfortable using CLI apps
-
has to manage a significant number of tasks
-
has a tight budget
Value proposition: optimised for NUS students who have busy schedules and a tight budget
Priorities: High (must have) - * * *
, Medium (nice to have) - * *
, Low
(unlikely to have) - *
Priority | As a(n) … | I want to … | So that I can… |
---|---|---|---|
|
social student |
keep track of who owes me money & how much |
not have anyone owe me any money. |
|
busy student |
keep track of all the tasks I have done |
work on tasks that I have yet to do. |
|
indecisive student |
roll back and forth changes that I have done |
track my ever-changing schedule. |
|
NUS student |
view all the prerequisites for a specified module |
plan my academic roadmap accordingly. |
|
busy student |
be reminded when I am nearing a deadline |
be on top of all my assignments |
|
student |
calculate my CAP easily |
keep track of my progress in university. |
(For all use cases below, the System is the JARVIS
and the Actor is the user
, unless specified otherwise)
MSS
-
User inputs amount paid and the names of people who he paid for
-
JARVIS calculates equal tab for all names including user
-
JARVIS stores individual tabs for names input
-
JARVIS prompts user that tabs have been added
-
User requests to see list of debts owed to him
-
JARVIS shows list of debts
Use case ends.
MSS
-
User requests to list tasks in planner
-
JARVIS shows lists of tasks in planner
-
User requests to mark a certain task as done
-
JARVIS finds task and marks it as done
Use case ends.
Extensions
-
3a. The given index is invalid.
-
3a1. AddressBook shows an error message.
Use case resumes at step 2.
-
MSS
-
User adds a project meeting into planner
-
JARVIS adds meeting into planner
-
User requests to undo project meeting
-
JARVIS rolls backs back the command
Use case ends.
MSS
-
User requests to for the prerequisite tree of a certain module
-
JARVIS shows the prerequisite tree
Use case ends.
Extensions
-
2a. The given module code is invalid
-
2a1. AddressBook shows an error message.
Use case resumes at step 1.
-
-
JARVIS should work on any mainstream OS as long as it has Java 11 or above installed.
-
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.
-
JARVIS should respond within two seconds.
-
JARVIS should be usable by a novice who has never used a command line interface.
-
JARVIS should be able to work without any internet connection.
Given below are instructions to test the app manually.
ℹ️
|
These instructions only provide a starting point for testers to work on; testers are expected to do more exploratory testing. |
-
Initial launch
-
Download the jar file and copy into an empty folder
-
Double-click the jar file
Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum.
-
-
Saving window preferences
-
Resize the window to an optimum size. Move the window to a different location. Close the window.
-
Re-launch the app by double-clicking the jar file.
Expected: The most recent window size and location is retained.
-
{ more test cases … }
-
Deleting a person while all persons are listed
-
Prerequisites: List all persons using the
list
command. Multiple persons in the list. -
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. -
Test case:
delete 0
Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. -
Other incorrect delete commands to try:
delete
,delete x
(where x is larger than the list size) {give more}
Expected: Similar to previous.
-