By: CS2103JAN2018-W13-B3
Since: Mar 2018
Licence: MIT
CLIndar is a desktop application that helps university computing students to manage their schedules. CLIndar is Common-Line Interface (CLI) based and uses Linux-style commands, which computing students are familiar with.
This guide provides information on the core functionalities of CLIndar and their implementation, so that a developer can get started customising and contributing to the application.
Below is an overview of this guide:
This section describes the steps needed to set up your computer before you can start developing this application further with your own code.
The following must be installed in your computer before you can begin using the application:
-
JDK
1.8.0_60
or laterℹ️Having any Java 8 version is not enough.
This app will not work with earlier versions of Java 8. -
IntelliJ IDE
ℹ️IntelliJ by default has Gradle and JavaFx plugins installed.
Do not disable them. If you have disabled them, go toFile
>Settings
>Plugins
to re-enable them.
Do the following steps to set up the project in your computer.
Step 1: Fork this repo, and clone the fork to your computer.
To fork the repo, click the button circled below.
Then, open Git Bash. Change the current working directory to the location where you want the cloned directory to be made. Type the following:
git clone https://github.com/YOUR-USERNAME/main.git
If the clone was successful, a new sub-directory titled 'main' appears on your local drive. This directory contains the files and metadata that Git requires to maintain the changes you make to the source files.
The following should appear if git clone https://github.com/YOUR-USERNAME/main.git
at C:/Users/temp folder is executed.
Step 2: Open IntelliJ. If you are not in the welcome screen, click File
> Close Project
to close the existing project dialog first, as shown in the picture below.
Step 3: Set up the correct JDK version for Gradle using the following steps, as illustrated by the pictures.
Step 3a: Click Configure
> Project Defaults
> Project Structure
.
Step 3b: Go to the Project
tab and under Project SDK
section, click New…
. Find the directory of the JDK, and then click OK.
Step 4: Click Import Project
.
Step 5: Locate the build.gradle
file in the main
folder that you have cloned and select it. Click OK
.
Step 6: In the Import Project from Gradle
that appears, click OK
again.
Step 7: Open the Terminal
and run the command gradlew processResources
(Mac/Linux: ./gradlew processResources
). It should finish with the BUILD SUCCESSFUL
message.
This will generate all resources required by the application and tests.
Do the following steps to verify that the project has been successfully set up in your computer:
-
Run the
seedu.address.MainApp
and try a few commands. -
Run the tests to ensure that they all pass.
After you set up the project in your own computer, you still need to do the the configurations described in the following sections before you can start developing this application further with your own code.
This project follows oss-generic coding standards. IntelliJ’s default style is mostly compliant with ours but it uses a different import order from ours. To rectify, do the following steps:
-
Go to
File
>Settings…
(Windows/Linux), orIntelliJ IDEA
>Preferences…
(macOS). -
Select
Editor
>Code Style
>Java
. -
Click on the
Imports
tab.-
For
Class count to use import with '*'
andNames count to use static import with '*'
: Set to999
to prevent IntelliJ from contracting the import statements. -
For
Import Layout
: Set the order to beimport static all other imports
,import java.*
,import javax.*
,import org.*
,import com.*
,import all other imports
. Add a<blank line>
between eachimport
.
-
After you do the steps above, the Settings window should appear as shown in the picture below.
Optionally, you can follow the UsingCheckstyle.adoc document to configure Intellij to check style-compliance as you write the code.
After forking the repo, links in the documentation will still point to the CS2103JAN2018-W13-B3/main
repo. If you plan to develop this as a separate product, you should replace the URL in the variable repoURL
in DeveloperGuide.adoc
and UserGuide.adoc
with the URL of your fork.
Travis needs to be set up to perform Continuous Integration (CI) for your fork. See UsingTravis.adoc to learn how to set it up.
After setting up Travis, you can optionally set up coverage reporting for your team fork (see UsingCoveralls.adoc).
ℹ️
|
Coverage reporting could be useful for a team repository that hosts the final version but it is not that useful for your personal fork. |
Optionally, you can set up AppVeyor as a second CI (see UsingAppVeyor.adoc).
ℹ️
|
Having both Travis and AppVeyor ensures your App works on both Unix-based platforms and Windows-based platforms (Travis is Unix-based and AppVeyor is Windows-based). |
Follow the steps below when you are ready to start coding:
-
Get some sense of the overall design by reading Section 3.1, “Architecture”.
-
Take a look at Appendix A, Suggested Programming Tasks to Get Started.
This section describes how the features in this application are implemented internally.
The high-level design of this application is illustrated in Figure 9 below.
Given below is a quick overview of each component.
💡
|
The .pptx files used to create diagrams in this document can be found in the diagrams folder. To update a diagram, modify the diagram in the pptx file, select the objects of the diagram, and choose Save as picture .
|
Main
has only one class called MainApp
. It is responsible for:
-
at app launch: initializing other components in the correct sequence and connecting them up with each other; and
-
at shut down: shutting down other components and invoking cleanup method where necessary.
Commons
represents a collection of classes used by multiple other components. Two of those classes play important roles at the architecture level:
-
EventsCenter
: This class (written using Google’s Event Bus library) is used by components to communicate with other components using events (i.e. a form of Event Driven design). -
LogsCenter
: This class is used by many classes to write log messages to the App’s log file.
The rest of the application consists of four components:
Each of the four components:
-
defines its API in an
interface
with the same name as the component; and -
exposes its functionality using a
{Component Name}Manager
class.
For example, the Logic
component defines its API in the Logic.java
interface and exposes its functionality using the LogicManager.java
class. Figure 10 below is an overview of the Logic
component.
Figure 11 below shows how the components interact in a scenario where the user issues the command delete 1
.
ℹ️
|
Note how the Model simply raises a DeskBoardChangedEvent when the Desk Board data are changed, instead of asking the Storage to save the updates into the hard disk.
|
Figure 12 below shows how the EventsCenter
reacts to that event, which eventually results in the updates being saved to the hard disk and the status bar of the UI being updated to reflect the 'Last Updated' time.
ℹ️
|
Note how the event is propagated through the EventsCenter to the Storage and UI without Model having to be coupled to either of them. This is an example of how this Event Driven approach helps us reduce direct coupling between components.
|
The sections below give more details of each component.
Figure 13 below shows the architecture diagram of the UI
component.
API : Ui.java
The UI
consists of a MainWindow
that is made up of parts e.g.CommandBox
, ResultDisplay
, TaskListPanel
, EventListPanel
, StatusBarFooter
, BrowserPanel
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; -
binds itself to some data in the
Model
so that the UI can auto-update when data in theModel
change; and -
responds to events raised from various parts of the App and updates the UI accordingly.
Figure 14 below shows the architecture diagram of the Logic
component.
Finer details concerning XYZCommand
and Command
in Figure 14, “Structure of the Logic
Component” in Figure 15 below.
API :
Logic.java
The following describes the sequence of events executed by the Logic component:
-
Logic
uses theDeskBoardParser
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 activity) and/or raise events. -
The result of the command execution is encapsulated as a
CommandResult
object which is passed back to theUi
.
Given below is the Sequence Diagram for interactions within the Logic
component for the execute("rm task 1")
API call.
Figure 17 below shows the architecture diagram of the Model
component.
API : Model.java
The Model
component:
-
stores a
UserPref
object that represents the user’s preferences, -
stores the Desk Board data;
-
uses two classes to store information about
Event
andTask
separately; -
exposes an unmodifiable
ObservableList<Activity>
that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change; and -
does not depend on any of the other three components.
Figure 18 below shows the structure of the Storage
component.
API : Storage.java
The Storage
component saves and reads back:
-
UserPref
objects in json format, and -
desk board data in xml format.
This section describes some noteworthy details on how certain features are implemented.
Activity
resides in the Model
component of the application.
It is an important class in this application as it represents each entry in the desk board.
In CLIndar, an entry in the desk board can be either a Task
or an Event
.
Figure 19 below shows the relationship between Activity
and its relevant components.
As shown above, Activity
is a superclass for Task
and Event
.
Activity
is made abstract so that it cannot be instantiated.
This is because an entry in the desk board must be either a Task
or an Event
and cannot simply be an Activity
.
Task
is one of the two types of entries in the desk board in CLIndar.
Task
represents a piece of work to be done by a certain deadline.
Once the Task
has been done by user, it can be mark as completed in the application.
Figure 20 below shows how Task
is represented in the application.
A brief description of each of the attributes of Task
is given below:
-
name
: name of the task. This attribute is compulsory. -
dueDateTime
: deadline of the task which includes date and time. This attribute is compulsory. -
remark
: a short description or any other comments pertaining to the task. This attribute is optional. -
tags
: groupings for the task. This attribute is optional and aTask
object can have multipletags
.
A Task
object can be added to the desk board through the use of the TaskCommand
.
A TaskCommand
is associated with a Task
object to be added.
When a TaskCommand
object calls the method executeUndoableCommand()
,
the Task
object is added into UniqueActivityList
in the DeskBoard
.
Note that the Task
object will not be added if an equivalent Task
is found in the desk board or the name
or dueDateTime
field is left empty.
The following is a snippet of code for TaskCommand
's executeUndoableCommand()
method.
public CommandResult executeUndoableCommand() throws CommandException {
requireNonNull(model);
try {
model.addActivity(toAdd);
return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd));
} catch (DuplicateActivityException e) {
throw new CommandException(MESSAGE_DUPLICATE_TASK);
}
}
Once the task is successfully added, it will be reflected in the CLIndar GUI itself.
Event
is the second and last type of entries in the desk board in CLIndar.
Event
represents an activity with fixed start and end times, such as a test, doctor appointment etc.
Figure 19 below shows how Event
is implemented in the application.
A brief description of each of the attributes of Event
is given below:
-
name
: name of the event. This attribute is compulsory. -
startDateTime
: start date and time of the event. This attribute is compulsory. -
endDateTime
: end date and time of the event. This attribute is compulsory. -
location
: location of the event. This attribute is optional. -
remark
: a short description or any other comments pertaining to the event. This attribute is optional. -
tags
: groupings for the event. This attribute is optional and anEvent
object can have multipletags
.
An Event
object can be added to the desk board through the use of the EventCommand
.
The way Event
objects are added to UniqueActivityList
is the same as Task
objects.
Note that the Event
object will not be added if an equivalent Event
is found in the desk board or the name
, startDateTime
or endDateTime
field is left empty.
Once the Event
is successfully added, it will be reflected in the CLIndar GUI itself.
In the GUI, the Task
and Event
objects will always be shown sorted in terms of their dueDateTime
and startDateTime
respectively.
This is achieved through the UniqueActivityList
which contains a list of unique activities which is either a Task
or an Event
stored in an internalList
.
Each time an Activity
is added to or edited in the UniqueActivityList
,
internalList
will be sorted according to its dateTime
attribute.
Note dateTime
refers to dueDateTime
for Task
and startDateTime
for Event
.
Following is a snippet which illustrates the function of UniqueActivityList
:
private final ObservableList<Activity> internalList = FXCollections.observableArrayList();
/...some code.../
public void add(Activity toAdd) throws DuplicateActivityException {
requireNonNull(toAdd);
if (contains(toAdd)) {
throw new DuplicateActivityException();
}
internalList.add(toAdd);
Collections.sort(internalList, dateTimeComparator);
}
Task
and Event
objects can be removed through the use of the RemoveCommand
by specifying the Task
or Event
and the index of the task/event reflected in the GUI.
The following snippet shows the executeUndoableCommand()
of the RemoveCommand
class.
UniqueActivityList
resides in the Model
will be updated by having activityToDelete
deleted from it.
If the index of an activity is not found, an ActivityNotFoundException
will be thrown.
public CommandResult executeUndoableCommand() {
requireNonNull(activityToDelete);
try {
model.deleteActivity(activityToDelete);
} catch (ActivityNotFoundException pnfe) {
throw new AssertionError("The target activity cannot be missing");
}
/...some code.../
}
If RemoveCommand
is successful, the relevant Task
or Event
at the specified index will be removed which
will be reflected in the GUI.
Task
objects can be completed through the use of the CompleteCommand
by specifying the Task’s index reflected
in the GUI. Note that `CompleteCommand
can only be applied to Task
objects.
Below is a snippet of how the executeUndoableCommand()
of CompleteCommand
updates the respective task
to completed.
public CommandResult executeUndoableCommand() {
requireNonNull(activityToComplete);
try {
Activity completedActivity = activityToComplete.getCompletedCopy();
model.updateActivity(activityToComplete, completedActivity);
} catch (ActivityNotFoundException pnfe) {
throw new AssertionError("The target activity cannot be missing");
} catch (DuplicateActivityException dae) {
throw new AssertionError("The completed activity cannot be duplicated");
}
return new CommandResult(String.format(MESSAGE_COMPLETE_TASK_SUCCESS, activityToComplete));
}
If CompleteCommand
is successful, 'Uncompleted' will be replaced by 'Completed' in the GUI.
There are two ways to represent tasks and events in the application. The following shows the pros and cons for each alternative:
-
Alternative 1 (Current Choice): Use a single class
Activity
as a superclass forTask
andEvent
.-
Pros: As both
Task
andEvent
can be treated equally in most contexts other than their creation, this can simplify the code significantly. -
Cons: It might be a harder to retrieve information as
Event
contains extra attributes thatTask
does not have. Extra accessor methods will also be required forEvent
.
-
-
Alternative 2 : Simply have two different class
Task
andEvent
.-
Pros: There is a clear distinction between the two classes.
-
Cons: We will need to write codes for classes and methods related to
Task
andEvent
separately, when these could have been simplified by having classes and methods related to the superclass only.
-
This section describes implementation of features related to the GUI.
There are two types of commands related to the GUI: list
and overdue
. The following sections will describe the implementation for each comman.
A single Task
or Event
is displayed using a TaskListCard
or EventListCard
respectively. The list of all the Task
objects added by the user
is then displayed using the TaskListPanel
, while Event
objects added by user are displayed using EventListPanel
. Below are examples of a TaskListCard
and an EventListCard
.
ListCommand
allows the user to only view Task
only, Event
only or both in the GUI.
The following snippet shows how execute()
of ListCommand
displays Task
and/or Event
objects in the GUI.
If an invalid request for ListCommand
happens, a CommandException
will be thrown.
public CommandResult execute() throws CommandException {
switch(commandRequest) {
case "task":
EventsCenter.getInstance().post(new ShowTaskOnlyRequestEvent());
return new CommandResult(MESSAGE_SUCCESS_TASK);
case "event":
EventsCenter.getInstance().post(new ShowEventOnlyRequestEvent());
return new CommandResult(MESSAGE_SUCCESS_EVENT);
/..some code../
default:
throw new CommandException(String.format(Messages.MESSAGE_INVALID_LIST_REQUEST, commandRequest));
}
}
If ListCommand
is successful:
-
list
will show onlyTask
objects; -
list event
will show onlyEvent
objects; and -
list
will show bothTask
andEvent
objects.
Overdue tasks can be viewed through the use of the OverdueCommand
. Note that the OverdueCommand
only
lists down overdue tasks; overdue events are marked as completed automatically.
OverdueCommand
makes use of the LocalDateTime
class in Java to check if an ongoing task becomes overdue
or not.
There will be a class in the Model
component, which has access to the UniqueActivityList
class in that
component. The class will go through the UniqueActivityList
and obtain the dueDateTime
of Task
, and the end
DateTime
of Event
. It will then perform a check of the DateTime
with LocalDateTime.now()
. If a Task
is overdue, it will be marked so in the GUI.
If the current date and time is past an Event
s `endDateTime
, the Event
will be marked as completed.
This section describes various design considerations related to the GUI features. For each aspect, we will discuss the 2 alternatives considered and the pros and cons for each alternative.
-
Alternative 1: Have 1 panel which will display the 2 classes.
-
Pros: The coding required for this approach is much lesser.
-
Cons: The UI will be messier and less appealing.
-
-
Alternative 2 (current choice): Have 2 panels which will display the 2 classes separately.
-
Pros: The backend coding will be neater as the 2 classes do not have the same number of compulsory information tags. The design of the UI will be much more intuitive too.
-
Cons: A lot more coding is required to create the 2 separate panel.
-
We preferred the second alternative as it adheres more closely with fundamental design principles. The GUI
has to be made as intuitive as possible to bring convenience and comfort to the user. As such, the second alternative is
definitely the preferred approach here.
-
Alternative 1: Put the checking method in the
UniqueActivityList
class.-
Pros: There will be one fewer classes and it will be easier for the developer to understand the code.
-
Cons: This violates Single Responsibility Principle (SRP). The
UniqueAcitivtyList
class should not perform the checking.
-
-
Alternative 2: Use Google Maps API to obtain current time.
-
Pros:
LocalDateTime.now()
is reliant on system clock. Thus, if the system clock is in error, tasks that are overdue will not be marked correctly. -
Cons: Reliant on Google Maps API, and might be difficult for the developer to understand.
-
The current implementation and design considerations for the undo/redo feature are explained in this section.
The undo/redo mechanism is facilitated by an UndoRedoStack
, which resides inside LogicManager
. It supports undoing and redoing of commands that modifies the state of the desk board (e.g. add
, edit
). Such commands will inherit from UndoableCommand
.
UndoRedoStack
only deals with UndoableCommands
. Commands that cannot be undone will inherit from Command
instead. The following diagram shows the inheritance diagram for commands:
As you can see from the diagram, UndoableCommand
adds an extra layer between the abstract Command
class and concrete commands that can be undone, such as the DeleteCommand
. Note that extra tasks need to be done when executing a command in an undoable way, such as saving the state of the desk board before execution. UndoableCommand
contains the high-level algorithm for those extra tasks while the child classes implements the details of how to execute the specific command. Note that this technique of putting the high-level algorithm in the parent class and lower-level steps of the algorithm in child classes is also known as the template pattern.
Commands that are not undoable are implemented this way:
public class ListCommand extends Command {
@Override
public CommandResult execute() {
// ... list logic ...
}
}
With the extra layer, the commands that are undoable are implemented this way:
public abstract class UndoableCommand extends Command {
@Override
public CommandResult execute() {
// ... undo logic ...
executeUndoableCommand();
}
}
public class DeleteCommand extends UndoableCommand {
@Override
public CommandResult executeUndoableCommand() {
// ... delete logic ...
}
}
Suppose that the user has just launched the application. The UndoRedoStack
will be empty at the beginning.
The user executes a new UndoableCommand
, delete 5
, to delete the 5th activity in the desk board. The current state of the desk board is saved before the delete 5
command executes. The delete 5
command will then be pushed onto the undoStack
(the current state is saved together with the command).
As the user continues to use the program, more commands are added into the undoStack
. For example, the user may execute add n/David …
to add a new activity.
ℹ️
|
If a command fails its execution, it will not be pushed to the UndoRedoStack at all.
|
The user now decides that adding the activity was a mistake, and decides to undo that action using undo
.
We will pop the most recent command out of the undoStack
and push it back to the redoStack
. We will restore the desk board to the state before the add
command executed.
ℹ️
|
If the undoStack is empty, then there are no other commands left to be undone, and an Exception will be thrown when popping the undoStack .
|
The following sequence diagram shows how the undo operation works:
The redo command does the exact opposite (pops from redoStack
, pushes to undoStack
, and restores the desk board to the state after the command is executed).
ℹ️
|
If the redoStack is empty, then there are no other commands left to be redone, and an Exception will be thrown when popping the redoStack .
|
The user now decides to execute a new command, clear
. As before, clear
will be pushed into the undoStack
. This time the redoStack
is no longer empty. It will be purged as it no longer make sense to redo the add n/David
command (this is the behavior that most modern desktop applications follow).
Commands that are not undoable are not added into the undoStack
. For example, list
, which inherits from Command
rather than UndoableCommand
, will not be added after execution:
The following activity diagram summarize what happens inside the UndoRedoStack
when a user executes a new command:
This section describes various design considerations related to the implementation of undo/redo feature. For each aspect, we will discuss the 2 alternatives considered and the pros and cons for each alternative.
-
Alternative 1 (current choice): Add a new abstract method
executeUndoableCommand()
.-
Pros: We will not lose any undone/redone functionality as it is now part of the default behaviour. Classes that deal with
Command
do not have to know thatexecuteUndoableCommand()
exist. -
Cons: It will be hard for new developers to understand the template pattern.
-
-
Alternative 2: Just override
execute()
.-
Pros: This does not involve the template pattern, easier for new developers to understand.
-
Cons: Classes that inherit from
UndoableCommand
must remember to callsuper.execute()
, or lose the ability to undo/redo.
-
-
Alternative 1 (current choice): Save the entire desk board.
-
Pros: This is easy to implement.
-
Cons: There may be performance issues in terms of memory usage.
-
-
Alternative 2: Make individual command know how to undo/redo itself.
-
Pros: This will use less memory (e.g. for
delete
, just save the activity being deleted). -
Cons: We must ensure that the implementation of each individual command are correct.
-
-
Alternative 1 (current choice): Only include commands that modifies the desk board (
add
,clear
,edit
).-
Pros: We only revert changes that are hard to change back (the view can easily be re-modified as no data are lost).
-
Cons: User might think that undo also applies when the list is modified (undoing filtering for example), only to realize that it does not do that, after executing
undo
.
-
-
Alternative 2: Include all commands.
-
Pros: This might be more intuitive to the user.
-
Cons: User has no way of skipping such commands if he or she just want to reset the state of the desk board and not the view.
-
Additional Info: See our discussion here.
-
Alternative 1 (current choice): Use separate stack for undo and redo.
-
Pros: This is easy for new Computer Science student undergraduates to understand, who are likely to be the new incoming developers of our project.
-
Cons: Logic is duplicated twice. For example, when a new command is executed, we must remember to update both
HistoryManager
andUndoRedoStack
.
-
-
Alternative 2: Use
HistoryManager
for undo/redo.-
Pros: We do not need to maintain a separate stack, and just reuse what is already in the codebase.
-
Cons: This requires dealing with commands that have already been undone: We must remember to skip these commands. This violates SRP and Separation of Concerns as
HistoryManager
now needs to do two different things.
-
This section describes the implementation and design considerations for the storage of Task and Event.
Storing of Task
and Event
data is managed by StorageManager
within the Storage
component.
Task
and Event
is converted to XmlAdaptedTask
and XmlAdaptedEvent
respectively to be stored in a .xml file.
The application deals with 2 main classes: Task and Event. Hence, when the data is stored, there is a need for differentiation between these 2 classes. There are 2 possible implementations to do this:
-
Alternative 1: Use 1 list to store objects of both classes.
-
Pros: Minimal coding is required as only 1 list is required to store the 2 different class objects.
-
Cons: Whenever we want to perform an operation on an object stored, we need to check its class.
-
-
Alternative 2 (current choice): Use 2 lists to store objects of the 2 classes separately.
-
Pros: When we want to perform an operation on all the objects stored in a list, each object can be treated the same as they are from the same class.
-
Cons: More coding is required to create 2 separate lists.
-
While both alternatives have advantages and disadvantages, we feel that the second alternative’s
advantages outweigh its disadvantages in the long run. It is easier to maintain the 2 separate lists of
objects, whereby each list contains objects of the same class, especially as we make the 2 classes more
specialized. The inconvenience of creating 2 separate lists will be counterbalanced by the convenience in
the long run.
We are using java.util.logging
package for logging. The LogsCenter
class is used to manage the logging levels and logging destinations as follows:
-
The logging level can be controlled using the
logLevel
setting in the configuration file (See Section 4.6, “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.
The following are the four logging levels:
-
SEVERE
means critical problem was detected which may possibly cause the termination of the application. -
WARNING
means application can continue, but with caution. -
INFO
shows noteworthy actions by the application. -
FINE
gives details that are not usually noteworthy but may be useful in debugging e.g. printing the actual list instead of just its size.
We use asciidoc for writing documentation.
ℹ️
|
We choose asciidoc over Markdown because asciidoc, although a bit more complex than Markdown, provides more flexibility in formatting. |
See UsingGradle.adoc to learn how to render .adoc
files locally to preview the end result of your edits.
Alternatively, you can download the AsciiDoc plugin for IntelliJ, which allows you to preview the changes you have made to your .adoc
files in real-time.
See UsingTravis.adoc to learn how to deploy GitHub Pages using Travis.
We use Google Chrome for converting documentation to PDF format, as Chrome’s PDF engine preserves hyperlinks used in webpages.
Here are the steps to convert the project documentation files to PDF format:
-
Follow the instructions in UsingGradle.adoc to convert the AsciiDoc files in the
docs/
directory to HTML format. -
Go to your generated HTML files in the
build/docs
folder, right click on them and selectOpen with
→Google Chrome
. -
Within Chrome, click on the
Print
option in Chrome’s menu. -
Set the destination to
Save as PDF
, then clickSave
to save a copy of the file in PDF format. For best results, use the settings indicated in the screenshot below.
This sections describes the different ways to do testing on the application.
There are three ways to run tests.
💡
|
The most reliable way to run tests is the 3rd one. The first two methods might fail some GUI tests due to platform/resolution-specific idiosyncrasies. |
Method 1: Using IntelliJ JUnit test runner
You can choose to run all tests or only a subset of tests:
-
To run all tests, right-click on the
src/test/java
folder and chooseRun 'All Tests'
. -
To run a subset of tests, right-click on a test package, test class, or a test and choose
Run 'ABC'
.
Method 2: Using Gradle
To run tests using gradle, open a console and run the command gradlew clean allTests
(Mac/Linux: ./gradlew clean allTests
).
ℹ️
|
See UsingGradle.adoc for more info on how to run tests using Gradle. |
Method 3: Using Gradle (headless)
Thanks to the TestFX library we use, our GUI tests can be run in the headless mode. In the headless mode, GUI tests do not show up on the screen. That means the developer can do other things on the Computer while the tests are running.
To run tests in headless mode, open a console and run the command gradlew clean headless allTests
(Mac/Linux: ./gradlew clean headless allTests
).
We have two types of tests, based on whether the tests involve the GUI:
-
GUI tests which include:
-
System tests
These test the entire App by simulating user actions on the GUI. System tests are in thesystemtests
package. -
Unit tests
These test the individual components. They can be found inseedu.address.ui
package.
-
-
Non-GUI tests which include:
-
Unit tests
These target the lowest level methods/classes.
Example:seedu.adress.commons.StringUtilTest
. -
Integration tests
These check the integration of multiple code units which are assumed to be working.
Example:seedu.address.storage.StorageManagerTest
. -
Hybrids of unit and integration tests
These tests check multiple code units as well as how the are connected together.
Example:seedu.address.logic.LogicManagerTest
.
-
This section features softwares which can be used to track the progress of the application development.
We use Travis CI and AppVeyor to perform Continuous Integration on our projects. See UsingTravis.adoc and UsingAppVeyor.adoc for more details.
We use Coveralls to track the code coverage of our projects. See UsingCoveralls.adoc for more details.
When a pull request has changes to asciidoc files, you can use Netlify to see a preview of how the HTML version of those asciidoc files will look like when the pull request is merged. See UsingNetlify.adoc for more details.
Here are the steps to create a new release:
-
Update the version number in
MainApp.java
. -
Generate a JAR file using Gradle.
-
Tag the repo with the version number e.g.
v0.1
. -
Create a new release using GitHub and upload the JAR file you created.
A project often depends on third-party libraries. For example, this application depends on the Jackson library for XML parsing. Managing these dependencies can be automated using Gradle. For example, Gradle can download the dependencies automatically, which is better than these alternatives:
a. including those libraries in the repo (this bloats the repo size); and
b. requiring developers to download those libraries manually (this creates extra work for developers)
Suggested path for new programmers:
-
First, add small local-impact (i.e. the impact of the change does not go beyond the component) enhancements to one component at a time. Some suggestions are given in Section A.1, “Improving each component”.
-
Next, add a feature that touches multiple components to learn how to implement an end-to-end feature across all components. Section A.2, “Creating a new command:
remark
” explains how to go about adding such a feature.
Each individual exercise in this section is component-based (i.e. you would not need to modify the other components to get it to work).
Scenario: You are in charge of logic
. During dog-fooding, your team realize that it is troublesome for the user to type the whole command in order to execute a command. Your team devise some strategies to help cut down the amount of typing necessary, and one of the suggestions was to implement aliases for the command words. Your job is to implement such aliases.
💡
|
Do take a look at Section 3.3, “Logic Component” before attempting to modify the Logic component.
|
-
Add a shorthand equivalent alias for each of the individual commands. For example, besides typing
clear
, the user can also typec
to remove all activities in the list.-
Hints
-
Just like we store each individual command word constant
COMMAND_WORD
inside*Command.java
(e.g.FindCommand#COMMAND_WORD
,DeleteCommand#COMMAND_WORD
), you need a new constant for aliases as well (e.g.FindCommand#COMMAND_ALIAS
). -
AddressBookParser
is responsible for analyzing command words.
-
-
Solution
-
Modify the switch statement in
AddressBookParser#parseCommand(String)
such that both the proper command word and alias can be used to execute the same intended command. -
Add new tests for each of the aliases that you have added.
-
Update the user guide to document the new aliases.
-
See this PR for the full solution.
-
-
Scenario: You are in charge of model
. One day, the logic
-in-charge approaches you for help. He wants to implement a command such that the user is able to remove a particular tag from everyone in the desk board, but the model API does not support such a functionality at the moment. Your job is to implement an API method, so that your teammate can use your API to implement his command.
💡
|
Do take a look at Section 3.4, “Model Component” before attempting to modify the Model component.
|
-
Add a
removeTag(Tag)
method. The specified tag will be removed from everyone in the desk board.-
Hints
-
The
Model
and theAddressBook
API need to be updated. -
Think about how you can use SLAP to design the method. Where should we place the main logic of deleting tags?
-
Find out which of the existing API methods in
AddressBook
andPerson
classes can be used to implement the tag removal logic.AddressBook
allows you to update a activity, andPerson
allows you to update the tags.
-
-
Solution
-
Implement a
removeTag(Tag)
method inAddressBook
. Loop through each activity, and remove thetag
from each activity. -
Add a new API method
deleteTag(Tag)
inModelManager
. YourModelManager
should callAddressBook#removeTag(Tag)
. -
Add new tests for each of the new public methods that you have added.
-
See this PR for the full solution.
-
The current codebase has a flaw in tags management. Tags no longer in use by anyone may still exist on the
AddressBook
. This may cause some tests to fail. See issue#753
for more information about this flaw. -
The solution PR has a temporary fix for the flaw mentioned above in its first commit.
-
-
-
Scenario: You are in charge of ui
. During a beta testing session, your team is observing how the users use your desk board application. You realize that one of the users occasionally tries to delete non-existent tags from a contact, because the tags all look the same visually, and the user got confused. Another user made a typing mistake in his command, but did not realize he had done so because the error message wasn’t prominent enough. A third user keeps scrolling down the list, because he keeps forgetting the index of the last activity in the list. Your job is to implement improvements to the UI to solve all these problems.
💡
|
Do take a look at Section 3.2, “UI Component” before attempting to modify the UI component.
|
-
Use different colors for different tags inside activity cards. For example,
friends
tags can be all in brown, andcolleagues
tags can be all in yellow.Before
After
-
Hints
-
The tag labels are created inside the
PersonCard
constructor (new Label(tag.tagName)
). JavaFX’sLabel
class allows you to modify the style of each Label, such as changing its color. -
Use the .css attribute
-fx-background-color
to add a color. -
You may wish to modify
DarkTheme.css
to include some pre-defined colors using css, especially if you have experience with web-based css.
-
-
Solution
-
You can modify the existing test methods for
PersonCard
's to include testing the tag’s color as well. -
See this PR for the full solution.
-
The PR uses the hash code of the tag names to generate a color. This is deliberately designed to ensure consistent colors each time the application runs. You may wish to expand on this design to include additional features, such as allowing users to set their own tag colors, and directly saving the colors to storage, so that tags retain their colors even if the hash code algorithm changes.
-
-
-
-
Modify
NewResultAvailableEvent
such thatResultDisplay
can show a different style on error (currently it shows the same regardless of errors).Before
After
-
Hints
-
NewResultAvailableEvent
is raised byCommandBox
which also knows whether the result is a success or failure, and is caught byResultDisplay
which is where we want to change the style to. -
Refer to
CommandBox
for an example on how to display an error.
-
-
Solution
-
Modify
NewResultAvailableEvent
's constructor so that users of the event can indicate whether an error has occurred. -
Modify
ResultDisplay#handleNewResultAvailableEvent(NewResultAvailableEvent)
to react to this event appropriately. -
You can write two different kinds of tests to ensure that the functionality works:
-
The unit tests for
ResultDisplay
can be modified to include verification of the color. -
The system tests
AddressBookSystemTest#assertCommandBoxShowsDefaultStyle() and AddressBookSystemTest#assertCommandBoxShowsErrorStyle()
to include verification forResultDisplay
as well.
-
-
See this PR for the full solution.
-
Do read the commits one at a time if you feel overwhelmed.
-
-
-
-
Modify the
StatusBarFooter
to show the total number of people in the desk board.Before
After
-
Hints
-
StatusBarFooter.fxml
will need a newStatusBar
. Be sure to set theGridPane.columnIndex
properly for eachStatusBar
to avoid misalignment! -
StatusBarFooter
needs to initialize the status bar on application start, and to update it accordingly whenever the desk board is updated.
-
-
Solution
-
Modify the constructor of
StatusBarFooter
to take in the number of activities when the application just started. -
Use
StatusBarFooter#handleAddressBookChangedEvent(AddressBookChangedEvent)
to update the number of activities whenever there are new changes to the addressbook. -
For tests, modify
StatusBarFooterHandle
by adding a state-saving functionality for the total number of people status, just like what we did for save location and sync status. -
For system tests, modify
AddressBookSystemTest
to also verify the new total number of activities status bar. -
See this PR for the full solution.
-
-
Scenario: You are in charge of storage
. For your next project milestone, your team plans to implement a new feature of saving the desk board to the cloud. However, the current implementation of the application constantly saves the desk board after the execution of each command, which is not ideal if the user is working on limited internet connection. Your team decided that the application should instead save the changes to a temporary local backup file first, and only upload to the cloud after the user closes the application. Your job is to implement a backup API for the desk board storage.
💡
|
Do take a look at Section 3.5, “Storage Component” before attempting to modify the Storage component.
|
-
Add a new method
backupAddressBook(ReadOnlyAddressBook)
, so that the desk board can be saved in a fixed temporary location.-
Hint
-
Add the API method in
AddressBookStorage
interface. -
Implement the logic in
StorageManager
andXmlAddressBookStorage
class.
-
-
Solution
-
See this PR for the full solution.
-
-
By creating this command, you will get a chance to learn how to implement a feature end-to-end, touching all major components of the app.
Scenario: You are a software maintainer for addressbook
, as the former developer team has moved on to new projects. The current users of your application have a list of new feature requests that they hope the software will eventually have. The most popular request is to allow adding additional comments/notes about a particular contact, by providing a flexible remark
field for each contact, rather than relying on tags alone. After designing the specification for the remark
command, you are convinced that this feature is worth implementing. Your job is to implement the remark
command.
Edits the remark for a activity specified in the INDEX
.
Format: remark INDEX r/[REMARK]
Examples:
-
remark 1 r/Likes to drink coffee.
Edits the remark for the first activity toLikes to drink coffee.
-
remark 1 r/
Removes the remark for the first activity.
Let’s start by teaching the application how to parse a remark
command. We will add the logic of remark
later.
Main:
-
Add a
RemarkCommand
that extendsUndoableCommand
. Upon execution, it should just throw anException
. -
Modify
AddressBookParser
to accept aRemarkCommand
.
Tests:
-
Add
RemarkCommandTest
that tests thatexecuteUndoableCommand()
throws an Exception. -
Add new test method to
AddressBookParserTest
, which tests that typing "remark" returns an instance ofRemarkCommand
.
Let’s teach the application to parse arguments that our remark
command will accept. E.g. 1 r/Likes to drink coffee.
Main:
-
Modify
RemarkCommand
to take in anIndex
andString
and print those two parameters as the error message. -
Add
RemarkCommandParser
that knows how to parse two arguments, one index and one with prefix 'r/'. -
Modify
AddressBookParser
to use the newly implementedRemarkCommandParser
.
Tests:
-
Modify
RemarkCommandTest
to test theRemarkCommand#equals()
method. -
Add
RemarkCommandParserTest
that tests different boundary values forRemarkCommandParser
. -
Modify
AddressBookParserTest
to test that the correct command is generated according to the user input.
Let’s add a placeholder on all our PersonCard
s to display a remark for each activity later.
Main:
-
Add a
Label
with any random text insidePersonListCard.fxml
. -
Add FXML annotation in
PersonCard
to tie the variable to the actual label.
Tests:
-
Modify
PersonCardHandle
so that future tests can read the contents of the remark label.
We have to properly encapsulate the remark in our Person
class. Instead of just using a String
, let’s follow the conventional class structure that the codebase already uses by adding a Remark
class.
Main:
-
Add
Remark
to model component (you can copy fromAddress
, remove the regex and change the names accordingly). -
Modify
RemarkCommand
to now take in aRemark
instead of aString
.
Tests:
-
Add test for
Remark
, to test theRemark#equals()
method.
Now we have the Remark
class, we need to actually use it inside Person
.
Main:
-
Add
getRemark()
inPerson
. -
You may assume that the user will not be able to use the
add
andedit
commands to modify the remarks field (i.e. the activity will be created without a remark). -
Modify
SampleDataUtil
to add remarks for the sample data (delete yourdeskBoard.xml
so that the application will load the sample data when you launch it.)
We now have Remark
s for Person
s, but they will be gone when we exit the application. Let’s modify XmlAdaptedPerson
to include a Remark
field so that it will be saved.
Main:
-
Add a new Xml field for
Remark
.
Tests:
-
Fix
invalidAndValidPersonAddressBook.xml
,typicalPersonsAddressBook.xml
,validAddressBook.xml
etc., such that the XML tests will not fail due to a missing<remark>
element.
Since Person
can now have a Remark
, we should add a helper method to PersonBuilder
, so that users are able to create remarks when building a Person
.
Tests:
-
Add a new method
withRemark()
forPersonBuilder
. This method will create a newRemark
for the activity that it is currently building. -
Try and use the method on any sample
Person
inTypicalPersons
.
Our remark label in PersonCard
is still a placeholder. Let’s bring it to life by binding it with the actual remark
field.
Main:
-
Modify
PersonCard
's constructor to bind theRemark
field to thePerson
's remark.
Tests:
-
Modify
GuiTestAssert#assertCardDisplaysTask(…)
so that it will compare the now-functioning remark label.
We now have everything set up… but we still can’t modify the remarks. Let’s finish it up by adding in actual logic for our remark
command.
Main:
-
Replace the logic in
RemarkCommand#execute()
(that currently just throws anException
), with the actual logic to modify the remarks of a activity.
Tests:
-
Update
RemarkCommandTest
to test that theexecute()
logic works.
See this PR for the step-by-step solution.
Target user: University Computing students
Target user profile: Our target users are expected to have the following characteristics:
-
have a need to manage a significant number of tasks and events
-
prefer desktop apps over other types
-
can type fast
-
prefer typing over mouse input
-
are reasonably comfortable using Linux-style commands and CLI apps
Value proposition: The user will have all their tasks and events in one app. This creates convenience for the user. The CLI also makes it faster than a typical mouse/GUI-driven app
The following section describes the major and minor enhancements that each of our members contributes to the development of our product.
Major enhancement: Creating 'overdue' command. This command shows a list of overdue tasks.
Minor enhancement: Creating 'task' and 'event' commands which add Task and Event objects respectively.
Major enhancement: Modifying the GUI to fit our task/event manager application. This includes but is not limited to allowing the UI to display 2 panes for showing the Task and Event lists separately.
Minor enhancement: Modifying the 'help' command. In our application, 'help' can be followed by a command word (e.g. 'help task') so that instructions for only the command desired are shown. Moreover, 'man' command — an alias for 'help' — is also added for users who are familiar with Linux commands.
Major enhancement: Modifying the Storage component to allow storage of Tasks and Events in an xml file.
Minor enhancement: Creating 'ls' command. This command shows the Event and Task objects added to the Desk Board in 2 separate lists.
Major enhancement: Modifying the Model component. This includes creation of Event and Task classes, as well as their superclass, Activity. Other classes relevant to tasks and events are also created, such as DateTime.
Minor enhancement: Creating 'complete' command. This command marks a Task object as completed.
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 |
figure out how to use the application |
|
student |
add a new task |
record a new task to remind myself |
|
student |
add a new event |
record a new event to remind myself |
|
student |
add the location of an event |
be in the right location |
|
student |
add items to bring for an event |
attend an event with all the required items |
|
student |
view upcoming tasks (in order of earliest to latest) |
manage my time and finish my tasks on time |
|
student |
view upcoming events (in order of earliest to latest) |
attend my events |
|
student |
view free time slots |
arrange for other events |
|
student |
change the deadline of a task |
adjust my schedule when a deadline changes |
|
student |
change the time of an event |
see the updated schedule in case there are changes |
|
student |
change the location of an event |
be in the right location when the location changes |
|
student |
delete a task |
remove tasks I no longer need to complete |
|
student |
delete an event |
remove events I no longer need to attend |
|
student |
find a task by name |
locate details of a task without having to go through the entire list |
|
student |
find an event by name |
locate details of an event without having to go through the entire list |
|
student |
mark a task as complete |
tell which tasks are completed and which are not |
|
student |
view completed tasks |
be assured I have completed a task |
|
student |
import entries from another file |
easily add entries previously saved in another file |
|
forgetful student |
view overdue tasks |
be assured I did not miss any deadline |
|
forgetful student |
get notification for upcoming tasks |
never forget to complete my tasks |
|
forgetful student |
get notification for upcoming events |
never forget to attend my events |
|
busy student |
sort tasks based on priority |
allocate sufficient time for each task |
|
busy student |
filter tasks based on time period |
view only tasks with deadlines within a certain time period |
|
busy student |
filter events based on time period |
view only events within a certain time period |
|
busy student |
filter tasks based on tags |
view only certain tasks when there are too many tasks to view |
|
busy student |
filter events based on tags |
view only certain events when there are too many events to view |
|
procrastinating student |
add estimated time needed to complete a task |
estimate when to start on a task to complete it on time |
|
procrastinating student |
view contact details of person in-charge for a deadline extension |
have enough time to complete my assignments despite my tight schedule |
|
busy student |
view free time slots before a deadline |
know how much time I have when I’m actually free to finish my tasks |
|
student |
view common free time slots among my group mates |
arrange a group meeting |
|
organised student |
add multiple deadlines for a task |
keep track of project progress |
|
NUS student |
import timetable from NUSMods |
avoid the trouble of keying in my lecture and tutorial schedules manually |
|
student |
be rewarded for completing a task |
feel a sense of achievement |
|
student |
hide private events |
minimize chance of someone else seeing them by accident [appendix] == Use Cases (For all use cases below, the System is the [discrete] === Use case: Add task MSS 1. User adds a task into CLIndar by typing a command 2. CLIndar parses command typed and adds in the task + Use case ends. Extensions [none] 2a. CLIndar detects that the format of the command typed is invalid + [none] 3a1. CLIndar shows an error message + Use case ends. === Use case: Add events MSS 1. User adds an event into CLIndar by typing a command 2. CLIndar parses command typed and adds in the event + Use case ends. Extensions [none] 3a. CLIndar detects that the format of the command typed is invalid + [none] 3a1. CLIndar shows an error message + Use case ends. === Use case: Complete task MSS 1. User requests to mark a task as completed by typing a command 2. CLIndar shows a list of uncompleted tasks 3. User selects the task to be marked as completed by the index 4. CLIndar marks the selected task as completed + Use case ends. Extensions [none] * 2a. The list is empty + Use case ends. * 3a. The given index is invalid. + [none] 3a1. CLIndar shows an error message + Use case resumes at step 2. === Use case: Show urgent task MSS 1. User requests to show all tasks before a certain date by typing a command 2. CLIndar shows a list of uncompleted tasks with deadline before the provided date + Use case ends. Extensions [none] * 1a. The given date is invalid + [none] 1a1. CLIndar shows an error message + Use case resumes at step 1. * 2a. The list is empty. + Use case ends. === Use case: Show help for command MSS 1. User requests to show help for command requested 2. CLIndar shows the help message for the requested command + Use case ends. Extensions [none] * 1a. The command requested is invalid + [none] * 1a1. CLIndar shows an error message + Use case resumes at step 1. Use case ends. {More to be added} [appendix] == Non Functional Requirements . Should work on any mainstream OS as long as it has Java {More to be added} [appendix] == Glossary Tasks:: A particular work assigned by a professor. Completed:: A task or event that has finished. Deadline:: The date and time by which an event will happen or a task is due. Duration:: The amount of time left to complete a task or prepare for an event. Events:: A thing that will occur in a certain place at a particular interval of time. Location:: The place where the event will occur. Mainstream OS:: Windows, Linux, Unix, OS-X Private contact detail:: A contact detail that is not meant to be shared with others. Overdue:: A task that has not been completed past the deadline. Things to bring:: Things to be brought for an event that is going to happen. Urgent:: A task due or event that will happen within the next 24 hours. [appendix] == Product Survey *Product Name Author: … Pros: * … * … Cons: * … * … [appendix] == Instructions for Manual Testing Given below are instructions to test the app manually. [NOTE] These instructions only provide a starting point for testers to work on; testers are expected to do more exploratory testing. === Launch and Shutdown . Initial launch .. Download the jar file and copy into an empty folder
.. Double-click the jar file . 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. { more test cases … } === Deleting a activity . Deleting a activity while all activities are listed .. Prerequisites: List all activities using the { more test cases … } === Saving data . Dealing with missing/corrupted data files .. {explain how to simulate a missing/corrupted file and the expected behavior} { more test cases … } |