By: CS2103-AY2018/19s2-W10-4 Team
Since: Mar 2019
Licence: MIT
- 1. Setting up
- 2. Design
- 3. Implementation
- 4. Documentation
- 5. Testing
- 6. Dev Ops
- Appendix A: Product Scope
- Appendix B: User Stories
- Appendix C: Use Cases
- Appendix D: Non Functional Requirements
- Appendix E: Glossary
- Appendix F: Instructions for Manual Testing
-
JDK
9
or later⚠️ JDK 10
on Windows will fail to run tests in headless mode due to a JavaFX bug. Windows developers are highly recommended to use JDK9
. -
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.
-
Fork this repo, and clone the fork to your computer
-
Open IntelliJ (if you are not in the welcome screen, click
File
>Close Project
to close the existing project dialog first) -
Set up the correct JDK version for Gradle
-
Click
Configure
>Project Defaults
>Project Structure
-
Click
New…
and find the directory of the JDK
-
-
Click
Import Project
-
Locate the
build.gradle
file and select it. ClickOK
-
Click
Open as Project
-
Click
OK
to accept the default settings -
Open a console and run the command
gradlew processResources
(Mac/Linux:./gradlew processResources
). It should finish with theBUILD SUCCESSFUL
message.
This will generate all resources required by the application and tests. -
Open
MainWindow.java
and check for any code errors-
Due to an ongoing issue with some of the newer versions of IntelliJ, code errors may be detected even if the project can be built and run successfully
-
To resolve this, place your cursor over any of the code section highlighted in red. Press ALT+ENTER, and select
Add '--add-modules=…' to module compiler options
for each error
-
-
Repeat this for the test folder as well (e.g. check
HelpWindowTest.java
for code errors, and if so, resolve it the same way)
-
Run the
seedu.knowitall.MainApp
and try a few commands -
Run the tests to ensure they all pass.
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,
-
Go to
File
>Settings…
(Windows/Linux), orIntelliJ IDEA
>Preferences…
(macOS) -
Select
Editor
>Code Style
>Java
-
Click on the
Imports
tab to set the order-
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
: The order isimport static all other imports
,import java.*
,import javax.*
,import org.*
,import com.*
,import all other imports
. Add a<blank line>
between eachimport
-
Optionally, you can follow the UsingCheckstyle.adoc document to configure Intellij to check style-compliance as you write code.
After forking the repo, the documentation will still have the SE-EDU branding and refer to the
cs2103-ay1819s2-w10-4/main
repo.
If you plan to develop this fork as a separate product (i.e. instead of contributing to cs2103-ay1819s2-w10-4/main
)
, you should do the following:
-
Configure the site-wide documentation settings in
build.gradle
, such as thesite-name
, to suit your own project. -
Replace the URL in the attribute
repoURL
inDeveloperGuide.adoc
andUserGuide.adoc
with the URL of your fork.
Set up Travis 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) |
When you are ready to start coding, get some sense of the overall design by reading Section 2.1, “Architecture”.
The Architecture Diagram given above explains the high-level design of the App. 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: 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 by 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
, CardListPanel
, 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. -
Listens for changes to
Model
data so that the UI can be updated with the modified data.
API :
Logic.java
-
Logic
uses theCardFolderParser
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 card). -
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.
API : Model.java
The Model
,
-
stores a
UserPref
object that represents the user’s preferences. -
stores a list of
VersionedCardFolders
representing the folders that the user has. -
exposes unmodifiable instances of
FilteredList<Card>
andFilteredList<VersionedCardFolder>
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. -
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
CardFolder
data in json format and read it back.
This section describes some noteworthy details on how certain features are implemented.
The Card
is one of the core aspects of the application. Cards are the result of morphing the Person
class from the original AddressBook model. This implementation incorporates the Model
and Logic
components.
To allow users to manage Cards, the following methods are available via the Model
component:
-
ModelManager#addCard(Card card)
- Adds a new card to the currently activeVersionedCardFolder
folder -
ModelManager#setCard(Card target, Card editedCard)
- Edits the information of a target card in the currently active folder -
ModelManager#deleteCard(Card target)
- Deletes the target card from the currently active folder -
ModelManager#hasCard(Card card)
- Checks if a card is already present in the currently active folder
As with all other commands, the LogicManager#execute(String commandText)
method of the Logic
component parses the user’s input, say a command to add a new card, and executes the corresponding Command
.
The following steps detail the underlying logic executed when a user does a card-related operation, say an add card operation.
-
User is in the
Organs
folder and wants to add a new card, with question 'What is the largest organ?' and answer 'Skin'. This is done by typingadd q/What is the largest organ? a/Skin
. -
The command parser reads the string input (as entered by the user) and returns the corresponding
Command
, anAddCommand
object in this instance. -
Upon execution, the
AddCommand
checks if the card to be added is already present in the current folder. If so, an exception is thrown. -
The
AddCommand
then calls theModelManager#addCard(Card card)
method. -
The new card will then be added to the active
VersionedCardFolder
. -
If the user is not inside a folder, or if the card to add already exists inside the current folder, the
addCommand
will throw aCommandException
.
The following sequence diagram demonstrates how AddCommand
works.
-
Alternative 1 (current choice): Maintain a set of
Option
objects to represent incorrect options, separate from theAnswer
field of eachCard
.-
Pros: Simple to implement, easy to convert the card type between MCQ and Single-answer, requires the least amount of implementation changes to
Card
. -
Cons: Single-answer cards still have to maintain an empty
Option
set.
-
-
Alternative 2: Maintain 2 separate subclasses of
Card
, one for Single-answer and another for MCQ.-
Pros: More object-oriented implementation.
-
Cons: Harder to implement.
-
To implement tracking of the number of correct and incorrect attempts, a new attribute Score
was added to Card
.
Score keeps track of both numbers.
This attribute is encapsulated in the Score
class.
-
Alternative 1 (current choice): Track total number of correct attempts and total number of attempts
-
Pros: Simple to implement. Most semantically correct.
-
Cons: Score will be rolling average. If the question is answered wrongly even once, the score can never be 100%.
-
-
Alternative 2: Track only last X attempts.
-
Pros: An improvement in performance will be more obvious.
-
Cons: Uses more memory. Have to delete the X+1th score every time a new score is added.
-
-
Alternative 1 (current choice): Read/write as
String
.-
Pros: `String`s are easier to read/write to file.
-
Cons: There must be strict checks when instantiating score from strings as they are prone to many kinds of formatting errors.
-
-
Alternative 2: Read/write as a double.
-
Pros: A
double
can represent both numbers with just one, which then can be converted toString
. -
Cons: When instantiating score from double, it might be simplified. For example, 2 correct attempts and 4 total attempts becomes 0.5. When instantiating from double, it is interpreted as 1 correct attempt and 2 total attempts.
-
-
Alternative 3: Read and write both numbers as integers.
-
Pros: Most correct implementation.
-
Cons: Reading/writing to file now needs to take into account this fact. The
toString()
method cannot be used to write to file.
-
The report feature is meant to provide the user with the ability to look back and compare their folder score from previous test sessions.
-
Alternative 1 (current choice): Display previous folder scores in a graph and lowest scoring cards
-
Pros: Has benefits of seeing the graph as well as knowing which cards the user needs help in.
-
Cons: More performance and memory intensive. Screen may get messy if there are too many items.
-
-
Alternative 2: Display previous folder scores in a graph
-
Pros: More visual, easier to see change in folder score.
-
Cons: More performance and memory intensive as a graph needs to be rendered. Remedy: Display only last ten.
-
-
Alternative 3: List the previous folder scores
-
Pros: User can see more accurate numbers. They can also see the individual card scores, so that they can tell where they need help.
-
Cons: Hard to see change from test session to test session.
-
-
Alternative 1 (current choice): Display in full screen, entered from card folder
-
Pros: Works because the folder scores are scored by card folders.
-
Cons: Need to implement a new state for commands, because it should not be possible to do some e.g. add card while in fullscreen report.
-
-
Alternative 2: Display in right panel, with cards on the left
-
Pros: User can see all their cards at the same time.
-
Cons: Less space to render report details such as graph.
-
-
Alternative 1 (current choice): Use enum of States
-
Pros: Fixes the states that Model can be in. By design, only one state can be true at any point in time, if state is set to type State which is a enum.
-
Cons: Need to add new states to enum.
-
-
Alternative 2: Use Strings
-
Pros: No need to define new states. Trivial to change into new state: Simply set state to "folder", or "homeDir". Easy to check as well, e.g. to Check state == "folder".
-
Cons: Becomes very unsafe as even a typo would mean entering a state that other parts of the application would not understand. e.g. Setting state to "Folder" instead of "folder".
-
-
Alternative 2: Use boolean flags
-
Pros: Easy to manipulate. Clear when there are only two states.
-
Cons: Becomes very messy when there are more states, since there is a need to ensure that only one boolean flag is true at any point in time. E.g., only one of inFolder, inHomeDir, inTest… can be true.
-
The Import and Export feature is facilitated by the Logic and model components, together with the addition of 2 new classes inside the csvmanager package defined under the storage package.
Similar to how the current commands are implemented, where each specific command inherits from the abstract class command, the Import and Export command likewise inherit from the command class.
The model component supports the addition of a new object attribute CsvManager
, that carries out the read and
write logic of cards to external files.
Additionally, model implements two new methods, Model#importCardFolders
and Model#exportCardFolders
.
These two methods further call the CsvManager API to read and write card folders.
Model also implements Model#setTestCsvPath
and the Model#getDefaultPath
which are only used during testing.
The csvmanager package contains all classes dealing with the reading and writing of cardfolders into and out to the model.
The classes include:
-
CsvManager - Main class responsible for the writing and reading of .csv files during the import and export of files
-
CsvFile - Wrapper class for file name input by user
-
CsvCommands interface - API for import/export method in CsvManager
The main logic for the import / export command is carried out inside the CsvManager class specified by it’s
API CsvManager#writeFoldersToCsv
and CsvManager#readFoldersToCsv
.
-
User wants to export folders
Human Anatomy
andNervous System
. Suppose that these two folders index as shown on the main application screen is 1 and 3 respectively. The user typesexport 1 3
-
The Export command parser extracts the indices, parses the indices into a List<Integer> and
ExportCommand
object, which keeps track of the indices. -
The Export command executes, calling
Model#exportCardFolders
method. -
Model#exportCardFolders method checks that all card folders specified by user exists inside the list of card folders. With reference to the diagram below, we can see that the indices specified by the user corresponds to each card folder.
The method throwsCardFolderNotFoundException
if card folder index is not found in list. -
CsvManager
is called to carry out the main logic of writing the cardfolders to file. File exported will be created in the project root directory.
Names of the files created will correspond to the names of their corresponding cardfolders.
i.eHuman Anatomy.csv
andNervous System.csv
-
User wants to import
Human Anatomy.csv
file.Human Anatomy.csv
file contains flashcards represented in csv file format. User typesimport Blood.csv
command -
Import command parser extracts file name, wraps file into a
CsvFile
object and parses the CsvFile into an Import Command object. -
Logic unit executes the import command. The execute method makes a call to
Model#importCardFolder
method. -
CsvManager is called to carry out the main logic of reading cardfolders to file. File imported will be serialized into a
CardFolder
object and added to thefilteredFoldersList
variable.
ℹ️
|
Both Imported and Exported files have to be placed in the project root directory. |
The below diagram shows the sequence diagram for the Import/Export commands
Example of a common cardfolder csv file
Blood.csv
-
The first line of any file contains the headers for each card. Headers have to be present in the csv file for import.
-
Each row of the csv file represents a single flashcard.
-
Options header can take more than one value or none.
-
Hints header can take either 0 or 1 value.
-
Alternative 1: Implement read and write card folders in
StorageManager
class-
Pros:
-
The most intuitive solution, since Storage involves read and write logic
-
-
Cons:
-
Model and Storage are now more coupled together.
-
Storage has more than one responsibility now. Instead of managing the internal data of card folder, it now has to manage the read and write to csv files.
-
-
-
Alternative 2: Implement read and write card folders in
csvmanager
package class. (Current)-
Pros:
-
Separate responsibilities of both Storage and Model.
-
Model class can focus on the representation of the in-memory card folders Storage
-
Storage class can focus on managing the internal card folder data (.json files)
-
-
-
Cons:
-
More code to write. Storage class could possibly call the relevant API’s to convert .json file into .csv file
-
-
-
Alternative 1: Export multiple card folders into a single file.
-
Pros:
-
Saves user trouble of calling multiple import for files. Each card folder is separated by a new line.
-
-
Cons:
-
Not a .csv file anymore. First line header would now specify cardfolder name before card headers, leading to unequal rows and columns
-
Users will be unable to select specific card folders to import if multiple card folders have been exported into a single file.
-
-
-
Alternative 2: Export each card folder into a single file (Current)
-
Pros:
-
More flexibility for users to import desired card folders, since 1 cardfolder = 1 csv file.
-
Files are now correctly formatted as .csv file
-
-
Cons:
-
Slightly more work needed to import multiple card folders.
-
-
A folder is another layer of abstraction over a CardFolder
. Where we dealt with a single CardFolder
in previous iterations, we now have multiple CardFolders
that each have their own set of Cards
. Users are able to manage each CardFolder
independently.
Folders in the application are achieved via enhancements from the AddressBook implementation. The changes span across all four components (UI, Logic, Model and Storage).
Previously, an instance of ModelManager
contains only a single VersionedCardFolder
, holding the current and previous state of the CardFolder
. To support multiple folders, ModelManager
now holds an ObservableList
of CardFolders
. The change is illustrated in the figure below, with the original implementation on the left and new implementation on the right.
To allow users to operate on multiple CardFolders
, the following notable methods were also introduced:
-
ModelManager#addFolder(CardFolder cardfolder)
- Adds a specified cardfolder to theModelManager
’s list -
ModelManager#deleteFolder(int index)
- Deletes theCardFolder
at the specified index in theModelManager’s
list -
ModelManager#getActiveCardFolderIndex()
- Gets the index of the current activeCardFolder
-
ModelManager#enterFolder(int index)
- Specifies the activeCardFolder
for operations to be performed on via the index inModelManager
’s list and sets the booleaninFolder
totrue
to denote that user is inside a folder. -
ModelManager#exitFolderToHome()
- Sets the booleaninFolder
tofalse
to indicate that the user is at the home directory. -
ModelManager#renameFolder(int index, String newName)
- Renames the folder at the specified index in theModelManager’s
list to the new name. -
ModelManager#isInFolder()
- Returns true if theModelManager
is currently inside a folder.
Similarly, the StorageManager
needs to represent each CardFolder
separately. In the same manner as in the Model component, we introduce a list of JsonCardFolderStorages
. The change is illustrated in the figure below, with the original implementation on the left and new implementation on the right.
Notable new methods:
-
StorageManager#readCardFolders()
- Reads in allCardFolders
from allCardFolderStorage
objects in the list. -
StorageManager#saveCardFolders(List<ReadOnlyCardFolder> cardFolders)
- Saves allCardFolders
provided in the argument to the user’s data directory.
The existing implementation of the Logic component propagates changes in a Model’s CardFolder
to the Storage component. With listeners, it is informed when a CardFolder
is modified (e.g. a new card is added) so that it can invoke the appropriate Storage methods.
The same principle was applied to propagate changes regarding CardFolders
themselves (and not their stored cards) to Storage: e.g. adding a new folder. Model is now an Observable
, and changes to a Model’s CardFolders
will inform the LogicManager
, which in turn invokes StorageManager#saveCardFolders(List<ReadOnlyCardFolder> cardFolders)
.
To illustrate how the Model, Storage and Logic components interact, below is a walkthrough of a typical usage scenario of the addfolder
command. Figure 9, “Component interactions for an addfolder
command” is a sequence diagram that summarises the example:
-
Step 1. The
addfolder
command is executed. For example,addfolder f
. -
Step 2. As with every command, the command parser reads the input and generates the relevant
Command
object, in this case anAddFolderCommand
. The object is returned to theLogicManager
.
ℹ️
|
If the input is invalid (e.g. user did not provide a folder name), Step 2 would not proceed and an error message is displayed. The Model and Storage components will not be modified. |
-
Step 3. The
LogicManager
executes theAddFolderCommand
, storing the result and then transferring control to the Model component with theModelManager#addFolder()
method. -
Step 4. The
ModelManager
creates aVersionedCardFolder
to represent the newly created folder, storing a reference to its currently empty list of cards. Before returning control to the Logic component,ModelManager#indicateModified()
is invoked to notify listeners in theLogicManager
that the list ofCardFolders
have changed. -
Step 5. The Logic component takes over control and checks if the
ModelManager
is modified. In the case ofaddfolder
the object is indeed modified (as a result of Step 4) and thus the component proceeds to save the Model’sCardFolders
to Storage. -
Step 6. Before handing over control to Storage, the
LogicManager
obtains the information to save and the path to save to withModelManager#getCardFolders()
andModelManager#getCardFoldersFilesPath()
respectively. It then passes these objects as parameters when it callsStorageManager#saveCardFolders()
. -
Step 7. The Storage component receives control, with the
StorageManager
clearing the directory at the specified path and creatingJsonCardFolderStorage
objects with path names equivalent to the names of the folders it has received. It then proceeds to invokeJsonCardFolderStorage#saveCardFolder()
on all theJsonCardFolderStorage
to save all the folders before returning to theLogicManager
.
ℹ️
|
If the path provided by the Model Component is invalid, the Storage component throws an exception and an error message is displayed. The changes made to Model are not saved and the command does not execute successfully. |
-
Step 8. The
LogicManager
terminates and returns the result of the command to the calling method.
As folders are a layer of abstraction over the cards, there is a need for the GUI to represent this abstraction for greater clarity and ease of use for the user. This is done by introducing the FolderListPanel
class, which displays a list of all folders that the user has.
The fullScreenPlaceholder:StackPane
object houses the content in the main window of our application. Depending on whether the user is in the home directory or within a folder, different UI objects are placed within fullScreenPlaceholder
.
-
When the user is in the home directory,
fullScreenPlaceholder
holds aFolderListPanel
to display all the folders in a list inside the main window. -
When the user is within a folder,
fullScreenPlaceholder
holds aCardMainScreen
object, which is composed of aCardListPanel
andBrowserPanel
. These represent the list of cards on the scrolling sidebar, as well as the card viewer on the right. The content within theCardMainScreen
depends on the particular folder the user has navigated into, as different folders hold different cards.
To better understand how the UI is updated, below is a walkthrough of what happens when the user enters a folder. Refer to the sequence diagram in Figure 10, “UI behaviour when user enters folder” for a visual representation:
-
Step 1. The Logic component informs the UI component that the user has entered a folder. The UI component responds by invoking
MainWindow#handleEnterFolder()
. -
Step 2. UI retrieves the list of cards belonging to the entered folder from the
LogicManager
. -
Step 3. A new
CardListPanel
is created with the information obtained in Step 2. -
Step 4. The new
CardListPanel
from Step 3, together with the existingBrowserPanel
, are used to create a newCardMainScreen
object. -
Step 5. The content held by
fullScreenPlaceholder
is replaced with the newly generatedCardMainScreen
.
-
Alternative 1 (current choice): List of structures representing individual folders
-
Pros: Scalable and better follows OOP principles.
-
Cons: Hard to implement, alters fundamental architecture of components.
-
-
Alternative 2: A single structure containing Cards with information on their folder membership (folder operations performed by iterating over all cards)
-
Pros: Easy to implement.
-
Cons: Not scalable, will be computationally expensive to perform folder operations when there are many cards and/or folders.
-
-
Alternative 1: Use a unique folder name
-
Pros: Easier to implement.
-
Cons: The undo/redo feature would not be compatible with this approach, as checking equality between different versions of a folder across time necessarily requires the comparison of cards.
-
-
Alternative 2: Identify a folder by its cards
-
Pros: There can be no folders with identical cards, preventing redundancy.
-
Cons: Two folders could have identical names as long as the cards are different, which is potentially confusing.
-
-
Alternative 3 (current choice): Mixed approach, use Alternative 1 for comparing different folders and Alternative 2 for comparing the same folder across time
-
Pros: Reaps the benefits of both approaches without the disadvantages.
-
Cons: Difficult to implement and for future developers to grasp the difference between the two types of comparisons.
-
-
Alternative 1: Let folder name be the file name of the storage file
-
Pros: Less ambiguity as to how file name is related to folder name, able to find storage file path with folder name.
-
Cons: Harder to retrieve folder name from the file as it requires parsing the path, more prone to data corruption as file name could be modified when application is running.
-
-
Alternative 2 (current choice): Let file name be independent of folder name, which is stored inside the storage file itself
-
Pros: Easier to implement and avoids dependency on existing storage files after application starts.
-
Cons: When saving folders from Model, it is difficult to match folders with existing storage files. Hence, rather than saving the modified folder, it is more feasible to clear the directory and save all folders. This is computationally expensive and may not be scalable beyond a certain size.
-
-
Alternative 1: Display a sample folder
-
Pros: Easy to implement, guaranteed that application will not be empty with no folders displayed.
-
Cons: Non-corrupted folders will not be displayed and will potentially be overwritten.
-
-
Alternative 2: Display non-corrupted folders
-
Pros: Non-corrupted data is preserved.
-
Cons: If all data is corrupted, an empty application is presented to the user.
-
-
Alternative 3 (current choice): Mixed approach, display all non-corrupted folders unless all data is corrupted, in which case display sample folder
-
Pros: Has the advantages but not the disadvantages of Alternatives 1 and 2.
-
Cons: Challenging to implement.
-
The state of the application with regard to navigation (i.e. whether a user is inside of a folder or at the home directory) affects the types of commands available to the user.
-
The commands that affect cards (e.g. adding a card, editing a card) are executed within folders and are known as Card Operations.
-
Commands that affect folders (e.g. adding a folder, deleting a folder) are only executable at the home directory and are known as Folder Operations.
Please refer to the User Guide for the full list of commands under both categories.
To keep track of navigation state, a boolean inFolder
is maintained by the ModelManager
. Other components may retrieve the current state with ModelManager#isInFolder()
. This is also how the Command
objects determines whether the command is executable in the present navigation state.
Folder navigation is achieved by the user through the use of the change
command. As navigating folders do not actually modify folders and their cards, folder navigation does not involve the Storage Component.
The change command has the following formats:
-
change ..
- Returns the user to the home directory. This command can only be executed when the user is inside a folder. -
change FOLDER_INDEX
- Enters the folder specified byFOLDER_INDEX
. This command can only be executed from the home directory, when the user is not in any folder.
When a change
command is executed, the Logic component parses the command and creates a ChangeCommand
object. If the command is of the first format, ChangeCommand()
is invoked without any arguments and the boolean isExitingFolder
is set to true
. If the command is of the second format, the overloaded constructor ChangeCommand(FOLDER_INDEX)
is instead called and isExitingFolder
is set to false
.
ChangeCommand#execute()
is then invoked. The value of isExitingFolder
will determine the corresponding methods in ModelManager
that are called (exitFoldersToHome()
or enterFolder()
). The sequence diagram in Figure 11, “Component interactions for change
command” illustrates this conditional choice and the interactions involved with each option.
-
Alternative 1 (current choice): Use variations of the same command (e.g.
change ..
andchange INDEX
)-
Pros: More intuitive and akin to other Command Line applications.
-
Cons: Harder to implement as the logic for parsing the command is different from that of existing commands.
-
-
Alternative 2: Use distinct commands (e.g.
home
andenter INDEX
)-
Pros: Commands would function similar to other commands in the application.
-
Cons: Harder for the user to get acquainted with as there are two separate commands with logically similar functionality; introduces redundancy.
-
This big feature mainly involves UI
, Logic
and Model
components.
There are 4 main variables in ModelManager
introduced to keep track of the current state in a test
session.
-
currentTestedCardFolder
-
The current card folder (stored as an
ObservableList
of cards) that the user is running a test session on. -
Set to null if user is not inside a test session
-
-
currentTestedCard
-
The current card the user is seeing in the test session, obtained from
currentTestedCardFolder
usingcurrentTestedCardIndex
. -
Set to null if user is not inside a test session
-
Related methods:
-
ModelManager#setCurrentTestedCard(Card card)
- setcurrentTestedCard
to the card specified. -
ModelManager#getCurrentTestedCard()
- returns thecurrentTestedCard
.
-
-
-
inTestSession
-
A boolean variable to indicate if user is running a test session.
-
Related methods:
-
ModelManager#isInTestSession()
- returns true if user is in a test session and false otherwise.
-
-
-
cardAlreadyAnswered
-
A boolean variable to indicate if the user has already execute a valid answer command for the current card.
-
Related methods:
-
ModelManager#setCardAsAnswered()
- setcardAlreadyAnswered
to true. -
ModelManager#setCardAsNotAnswered()
- setcardAlreadyAnswered
to false. -
ModelManager#isCardAlreadyAnswered()
- returns true if the current card has already been answered and false otherwise.
-
-
-
Alternative 1 (current choice): Introduction of another variable,
currentTestedCard
, to store the card.-
Pros: More reader friendly. Save time from accessing the list to get card at that index.
-
Cons: Extra space used.
-
-
Alternative 2: No introduction of
currentTestedCard
as using thecurrentTestedCardIndex
suffices. Every time a card is needed, can reference it usingcurrentTestedCardFolder.getIndex(currentTestedCardIndex)
.-
Pros: No need to store an extra variable, use less space.
-
Cons: Not so reader friendly. Need to keep accessing the list at that index. Potentially lead to possible violation of the Law of Demeter where an object should only interact with objects that are closely related to it.
-
Evaluation of the 2 alternatives:
We went with alternative 1 since not a large amount of memory is taken up with just an extra variable. As there will
be a number of references to the currentTestedCard
, it will be better to store them somewhere. Abiding by the Law
of Demeter, currentTestedCard
object will not be interacting with currentTestedCardFolder
, limiting its
knowledge of that object.
The main logic for test
and end
command is carried out in ModelManager
with the following methods:
-
ModelManager#testCardFolder()
- begins a test session on the current card folder user is in and implicitly sorts the cards in it. -
ModelManager#endTestSession()
- ends the current test session.
To update the change in the UI
to reflect that the user is a test session (app goes to full screen with question of
the current card presented), the following methods are introduced.
-
MainWindow#handleStartTestSession(Card card)
- creates a newTestSession
page with the card specified and bring the page forward in front of the currentCardMainScreen
page. -
MainWindow#handleEndTestSession()
- deletes the currentTestSession
page and theCardMainScreen
page at the back is now presented to the user.
To illustrate how the UI
, Model
and Logic
components interact, below is a walkthrough of a typical usage scenario of
the test command.
Figure 12, “Component interactions for a test
command” is a sequence diagram that summarises Model
and Logic
interactions, namely steps 1 to 7.
Step 1. User is inside a folder and wants to begin a test session on the current folder by executing the command
test
.
Step 2. As with every command, the command parser reads the input and generates the relevant Command
object, in
this case a TestCommand
. The object is returned to the LogicManager
.
Step 3. The LogicManager
executes the TestCommand
, storing the result and then transferring control to the
Model component, with ModelManager#isInFolder()
and ModelManager#isInTestSession()
to check that user is inside a
folder and is not already in a test session. (This is omitted from Figure 12, “Component interactions for a test
command” for simplicity.)
ℹ️
|
If the user is not inside a folder or is already in a test session, this test command would be rendered
invalid. Step 4 would not proceed and an error message is displayed.
|
Step 4. After both checks have passed, ModelManager#testCardFolder()
method is invoked, which does the following:
-
currentTestedCardFolder
is set to the current folder the user is in, by invokinggetCardList()
from the activeVersionedCardFolder
infolders
.ℹ️If this folder is empty such that there is no card to present to the user, an EmptyCardFolderException
is thrown, to be caught inTestCommand
, which then throws aCommandException
to display an error message. -
The cards in this folder is sorted in ascending scores by invoking
sortFilteredCard(comparator)
, so that lowest score cards will be presented first to the user in a test session. -
ModelManager#setCurrentTestedCard(currentTestedCardIndex)
is then invoked to setcurrentTestedCard
to the first card in the folder ascurrentTestedCardIndex
is set to 0. -
inTestSession
is set to true. -
No change to
cardAlreadyAnswered
as it is by default false.
Step 5. For TestCommand
to obtain the first card to present in the test session, ModelManager#getCurrentTestedCard
is
invoked and the Card
, c
, is returned.
Step 6. With control now transferred back to the logic unit, TestCommand
creates a CommandResult
object, r
with the type START_TEST_SESSION
, and
set testSessionCard
in CommandResult
to c
obtained in Step 5.
Step 7. r
is returned to LogicManager
which then terminates and returns r
to the calling method.
Step 8. The caller method here is MainWindow
. Control is now transferred to the UI component.
Step 9. MainWindow
sees that CommandResult
object r
has the type START_TEST_SESSION
. It invokes
MainWindow#handleStartTestSession(currentTestedCard)
to display the currentTestedCard
question and hints to the
user.
Step 1. User is currently in a test session and executes the command end
.
Step 2. An EndCommand
object is created and LogicManager
executes the EndCommand
, storing the result and then
transferring control to the Model component, with ModelManager#isInTestSession()
to
check that user is indeed in a test session.
ℹ️
|
If the user is not in a test session, Step 3 would not proceed. |
Step 3. ModelManager#endTestSession()
method is invoked, which does the following:
-
currentTestedCardFolder
is set to null. -
ModelManager#setCurrentTestedCard(null)
is invoked to setcurrentTestedCard
to null. -
inTestSession
is set to false. -
ModelManager#setCardAsNotAnswered()
is invoked.
Step 4. As control is now transferred back to the logic unit, EndCommand
creates a CommandResult
object, r
with
the type END_TEST_SESSION
.
Step 5. r
is returned to LogicManager
which then terminates and returns r
to the calling method, MainWindow.
Control is now transferred to the UI component.
Step 6. MainWindow
sees that CommandResult
object r
has the type END_TEST_SESSION
. It invokes
MainWindow#handleEndTestSession()
to delete the current testSession
page, presenting cardMainScreen
page at the
back (the screen the user was seeing before entering the test session) to the user.
-
Alternative 1 (current choice):
test
is executed when inside a folder. The user does not have to specify the folder index andtest
would just immediately display the first card in the this current folder.-
Pros: The most logical way of carrying out a test session is where user is in the folder that he or she wants to be tested on. Lesser dependency on entering and exiting folder methods.
-
Cons: Requires the extra step of entering the folder before it can actually test the folder. User may actually see the questions before the test session.
-
-
Alternative 2:
test
is executed when outside a folder, in the home directory.test
command would require a folder index, e.gtest 1
to test the first folder. Implementation of getting the card from the folder would rely on enter folder command as well.-
Pros: Fast way to enter test session from home directory
-
Cons: Not logical for test to be called from home directory which should only allow folder operations. Test will then have to implicitly enter the folder to get access to the cards in it in order to display them, creating a dependency between test and enter folder command. Similar issue will arise for the end test session command where user will need to implicitly exit the folder.
-
Evaluation of the two alternatives: Overall, alternative 1 is a better choice following the Single Level of Abstraction Principle(SLAP) where a function should not mix different levels of abstractions. We can then better achieve higher cohesion and lower coupling. Also, user being able to see the questions before the test session is not a big issue since the answer will not be shown unless user selects the card.
To facilitate the marking of attempted answer, we introduce the following method in ModelManager
.
-
ModelManager#markAttemptedAnswer(Answer attemptedAnswer)
- returns true if attemptedAnswer is correct and false if attemptedAnswer is wrong. It compares the attempted answer and the correct answer obtained from the current card.
ℹ️
|
Comparison is not case-sensitive |
To facilitate the update of score after marking the card, we introduce the following method in AnswerCommand
.
-
AnswerCommand#createScoredCard(Card cardToMark, boolean markCorrect)
- creates a new card with the updated score.
To update the change in the UI
to show the user the result of the marked answer, whether it is correct or wrong, the following methods are introduced.
-
MainWindow#handleCorrectAnswer()
- invokesTestSession#handleCorrectAnswer()
-
MainWindow#handleWrongAnswer()
- invokesTestSession#handleWrongAnswer()
-
TestSession#handleCorrectAnswer()
- updates currentTestSession
page to green colour background with correct answer and correct answer description -
TestSession#handleWrongAnswer()
- updates currentTestSession
page to red colour background with correct answer and wrong answer description
To illustrate how the components interact, below is a walkthrough of a typical usage scenario of the answer command.
Figure 13, “Component interactions for a answer command” is a sequence diagram that summarises Model
and Logic
interactions, namely steps 1
to 8.
Step 1. User is in a test session and wants to answer the question on the card currently presented by executing a ans
four
.
Step 2. An AnswerCommand
object is created and LogicManager
executes the AnswerCommand
, storing the result and
then transferring control to the Model component, with ModelManager#isInTestSession()
and
ModelManager#isCardAlreadyAnswered()
to check that user is indeed in a test session and has not attempted an answer
already. (This is omitted from Figure 13, “Component interactions for a answer command” for simplicity.)
ℹ️
|
If the user is not in a test session or already attempted an answer for the current card, this ans command would be
rendered invalid. Step 3 would not proceed and an error message is displayed.
|
Step 3. After both checks have passed, ModelManager#setCardAsAnswered()
is invoked.
Step 4. ModelManager now marks the attempted answer by invoking ModelManager#markAttemptedAnswer(attemptedAnswer)
.
Step 5. After knowing the result of the attempt, the Logic
unit creates a new card exactly the same as the current
card but with the updated score and replaced the current one by invoking AnswerCommand#createScoredCard(Card
cardToMark, boolean markCorrect)
followed by model#setCard(cardToMark, scoredCard)
.
Step 6. To complete the update in the change in score, model#updateFilteredCard(PREDICATE_SHOW_ALL_CARDS) and model#commitActiveCardFolder() are invoked.
Step 7. AnswerCommand
now creates a CommandResult
object, r
with either type ANSWER_CORRECT
or ANSWER_WRONG
depending on the outcome of the attempt.
Step 8. r
is returned to LogicManager
which then terminates and returns r
to the calling method.
Step 9. The caller method here is MainWindow
. Control is now transferred to the UI component.
Step 10. If MainWindow
sees that CommandResult
object r
has the type ANSWER_CORRECT
, it invokes
MainWindow#handleCorrectAnswer()
to display the correct attempt TestSession
page to the user.
If MainWindow
sees that CommandResult
object r
has the type ANSWER_WRONG
, it invokes
MainWindow#handleWrongAnswer()
to display the wrong attempt TestSession
page to the user.
The following method is introduced in ModelManager
to display to the user the next card in the test session.
ModelManager#testNextCard()
- returns true if it successfully finds a next card to present in the test session and
false otherwise (if there is no more cards left to test in the folder).
To display the next card in the test session, the method below is introduced in MainWindow
.
-
MainWindow#handleNextCardTestSession(Card card)
- deletes the currentTestSession
page and adds a newTestSession
page with this next card specified.
To illustrate how the UI
, Model
and Logic
components interact, below is a walkthrough of a typical usage scenario of
the next command.
Figure 14, “Component interactions for a next
command” is a sequence diagram that summarises Model
and Logic
interactions, namely steps 1
to 6.
Step 1. User is in a test session and wants to move on to the next card by executing a next
.
Step 2. A NextCommand
object is created and LogicManager
executes the NextCommand
, storing the result and
then transferring control to the Model component, with ModelManager#isInTestSession()
and
ModelManager#isCardAlreadyAnswered()
to check that user is indeed in a test session and has attempted an
answer already. (This is omitted from Figure 14, “Component interactions for a next
command” for simplicity.)
ℹ️
|
If the user is not in a test session or has not attempted an answer for the current card, this next command would be
rendered invalid. Step 3 would not proceed and an error message is displayed.
|
Step 3. After both checks have passed, ModelManager#testNextCard()
method is invoked, which does the following:
-
currentTestedCardIndex
incremented by 1. -
currentTestedCardIndex
is then checked if it equals to the size ofcurrentTestedCardFolder
.-
Case 1: This check returns true.
This meanscurrentTestedCardIndex
is invalid and there is no more next card to be presented to the user. This method immediately returns false. -
Case 2: This check returns true.
This meanscurrentTestedCardIndex
is valid and will be used to get the next card fromcurrentTestedCardFolder
. This card is set as thecurrentTestedCard
via theModelManager#setCurrentTestedCard (cardToTest)
.ModelManager#setCardAsNotAnswered
is then invoked to reset the value ofcardAlreadyAnswered
. This method returns true.
-
Step 4. From the result of ModelManager#testNextCard()
method earlier:
-
Case 1: Method returns false.
Anext
command will be equivalent to anend
command.ModelManager#endTestSession()
is invoked. Step 5 does not proceed. Instead, step 3 and onwards of the Example Usage of end command takes over. -
Case 2: Method returns true. For
NextCommand
to obtain the next card to present in the test session,ModelManager#getCurrentTestedCard
is invoked and theCard
,c
, is returned.
Step 5. With control now transferred back to the logic unit, NextCommand
creates a CommandResult
object, r
with
the type SHOW_NEXT_CARD
, and set testSessionCard
in CommandResult
to c
.
Step 6. r
is returned to LogicManager
which then terminates and returns r
to the calling method.
Step 7. The caller method here is MainWindow
. Control is now transferred to the UI component.
Step 8. MainWindow
sees that CommandResult
object r
has the type SHOW_NEXT_CARD
. It invokes
MainWindow#handleNextCardTestSession(currentTestedCard)
to display this new currentTestedCard
question and hints
to the user.
-
Alternative 1 (current choice): A
next
command will be equivalent to anend
command-
Pros: More convenient and user-friendly. It is common sense to end the test session for the user.
-
Cons: By right, it is not correct since next command is just to show the next card.
-
-
Alternative 2: A
next
command will throw an exception-
Pros: Most correct way to do it since there is no next card to display.
-
Cons: User may not understand. It is not user-friendly as user has to keep track of which card it is at to prevent the exception thrown.
-
Evaluation of the two alternatives: With our target audience in mind, alternative 1 is the more user friendly and intuitive way to handle this scenario.
ℹ️
|
The following section details a feature implemented in the earlier iteration of the application. As such, the diagrams still refer to AddressBook , which has since replaced with CardFolder . The outdated diagrams here will be updated by v2.0 .
|
The undo/redo mechanism is facilitated by VersionedCardFolder
.
It extends CardFolder
with an undo/redo history, stored internally as an cardFolderStateList
and currentStatePointer
.
Additionally, it implements the following operations:
-
VersionedCardFolder#commit()
— Saves the current card folder state in its history. -
VersionedCardFolder#undo()
— Restores the previous card folder state from its history. -
VersionedCardFolder#redo()
— Restores a previously undone card folder state from its history.
These operations are exposed in the Model
interface as Model#commitCardFolder()
, Model#undoCardFolder()
and Model#redoCardFolder()
respectively.
Given below is an example usage scenario and how the undo/redo mechanism behaves at each step.
Step 1. The user launches the application for the first time. The VersionedCardFolder
will be initialized with the initial card folder state, and the currentStatePointer
pointing to that single card folder state.
Step 2. The user executes delete 5
command to delete the 5th card in the card folder. The delete
command calls Model#commitCardFolder()
, causing the modified state of the card folder after the delete 5
command executes to be saved in the cardFolderStateList
, and the currentStatePointer
is shifted to the newly inserted card folder state.
Step 3. The user executes add q/Some question …
to add a new card. The add
command also calls Model#commitCardFolder()
, causing another modified card folder state to be saved into the cardFolderStateList
.
ℹ️
|
If a command fails its execution, it will not call Model#commitCardFolder() , so the card folder state will not be saved into the cardFolderStateList .
|
Step 4. The user now decides that adding the card was a mistake, and decides to undo that action by executing the undo
command. The undo
command will call Model#undoCardFolder()
, which will shift the currentStatePointer
once to the left, pointing it to the previous card folder state, and restores the card folder to that state.
ℹ️
|
If the currentStatePointer is at index 0, pointing to the initial card folder state, then there are no previous card folder states to restore. The undo command uses Model#canUndoCardFolder() to check if this is the case. If so, it will return an error to the user rather than attempting to perform the undo.
|
The following sequence diagram shows how the undo operation works:
The redo
command does the opposite — it calls Model#redoCardFolder()
, which shifts the currentStatePointer
once to the right, pointing to the previously undone state, and restores the card folder to that state.
ℹ️
|
If the currentStatePointer is at index cardFolderStateList.size() - 1 , pointing to the latest card folder state, then there are no undone card folder states to restore. The redo command uses Model#canRedoCardFolder() to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo.
|
Step 5. The user then decides to execute the command list
. Commands that do not modify the card folder, such as list
, will usually not call Model#commitCardFolder()
, Model#undoCardFolder()
or Model#redoCardFolder()
. Thus, the cardFolderStateList
remains unchanged.
Step 6. The user executes clear
, which calls Model#commitCardFolder()
. Since the currentStatePointer
is not pointing at the end of the cardFolderStateList
, all card folder states after the currentStatePointer
will be purged. We designed it this way because it no longer makes sense to redo the add q/Some question …
command. This is the behavior that most modern desktop applications follow.
The following activity diagram summarizes what happens when a user executes a new command:
-
Alternative 1 (current choice): Saves the entire card folder.
-
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 card being deleted). -
Cons: We must ensure that the implementation of each individual command are correct.
-
-
Alternative 1 (current choice): Use a list to store the history of card folder states.
-
Pros: 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
andVersionedCardFolder
.
-
-
Alternative 2: Use
HistoryManager
for undo/redo-
Pros: We do not need to maintain a separate list, and just reuse what is already in the codebase.
-
Cons: Requires dealing with commands that have already been undone: We must remember to skip these commands. Violates Single Responsibility Principle and Separation of Concerns as
HistoryManager
now needs to do two different things.
-
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.11, “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
We use asciidoc for writing documentation.
ℹ️
|
We chose 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.
The build.gradle
file specifies some project-specific asciidoc attributes which affects how all documentation files within this project are rendered.
💡
|
Attributes left unset in the build.gradle file will use their default value, if any.
|
Attribute question | Description | Default value |
---|---|---|
|
The question of the website. If set, the question will be displayed near the top of the page. |
not set |
|
URL to the site’s repository on GitHub. Setting this will add a "View on GitHub" link in the navigation bar. |
not set |
|
Define this attribute if the project is an official SE-EDU project. This will render the SE-EDU navigation bar at the top of the page, and add some SE-EDU-specific navigation items. |
not set |
Each .adoc
file may also specify some file-specific asciidoc attributes which affects how the file is rendered.
Asciidoctor’s built-in attributes may be specified and used as well.
💡
|
Attributes left unset in .adoc files will use their default value, if any.
|
Attribute question | Description | Default value |
---|---|---|
|
Site section that the document belongs to.
This will cause the associated item in the navigation bar to be highlighted.
One of: * Official SE-EDU projects only |
not set |
|
Set this attribute to remove the site navigation bar. |
not set |
The files in docs/stylesheets
are the CSS stylesheets of the site.
You can modify them to change some properties of the site’s design.
The files in docs/templates
controls the rendering of .adoc
files into HTML5.
These template files are written in a mixture of Ruby and Slim.
|
Modifying the template files in |
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
-
To run all tests, right-click on the
src/test/java
folder and chooseRun 'All Tests'
-
To run a subset of tests, you can right-click on a test package, test class, or a test and choose
Run 'ABC'
Method 2: 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:
-
GUI Tests - These are tests involving the GUI. They include,
-
System Tests that test the entire App by simulating user actions on the GUI. These are in the
systemtests
package. -
Unit tests that test the individual components. These are in
seedu.knowitall.ui
package.
-
-
Non-GUI Tests - These are tests not involving the GUI. They include,
-
Unit tests targeting the lowest level methods/classes.
e.g.seedu.knowitall.commons.StringUtilTest
-
Integration tests that are checking the integration of multiple code units (those code units are assumed to be working).
e.g.seedu.knowitall.storage.StorageManagerTest
-
Hybrids of unit and integration tests. These test are checking multiple code units as well as how the are connected together.
e.g.seedu.knowitall.logic.LogicManagerTest
-
See UsingGradle.adoc to learn how to use Gradle for build automation.
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, card folder depends on the Jackson library for JSON parsing. Managing these dependencies can be automated using Gradle. For example, Gradle can download the dependencies automatically, which is better than these alternatives:
-
Include those libraries in the repo (this bloats the repo size)
-
Require developers to download those libraries manually (this creates extra work for developers)
Target user profile:
-
medicine students who need to rote memorisation of information
-
finds carrying physical flashcards around troublesome and prefers an application to help them store and organize their learning material
-
prefer desktop apps over other types
-
can type fast
-
prefers typing over mouse input
-
is reasonably comfortable using CLI apps
Value proposition: flashcards that are able to test the user instead of simply having them recall the answer. The user experience is more engaging and scoring is more accurate as it is based on actual performance rather than reported performance.
Priorities: High (must have) - * * *
, Medium (nice to have) - * *
, Low (unlikely to have) - *
Priority | As a … | I want to … | So that I can… |
---|---|---|---|
|
student |
have flashcards with questions and answers |
have an easier time memorising content |
|
student |
create and delete my own flashcards |
|
|
student |
edit the content of my flashcards |
add on more content or correct any errors |
|
student |
have folders to store flashcards |
logically group flashcards of the same topic |
|
student |
navigate in and out of folders |
see one folder’s cards at each point of time |
|
student |
test myself on each flashcard folder |
better learn the content |
|
student |
attempt keying in answers before flashcards reveal them |
have a more engaging experience |
|
student |
view the answers of questions directly |
proceed even when I do not remember the answer |
|
student |
know how well I’ve been performing on each flashcard |
know my overall progress |
|
student |
view a progress report by folder |
know my performance for each topic |
|
student |
sort flashcards by score |
know which questions i have more trouble answering |
|
student |
import and export flashcards |
share content |
|
student |
search flash cards in a folder |
save time looking for a particular card |
|
student |
search folders |
save time looking for a particular folder |
|
student |
move flashcards from one folder to another |
better manage my flashcards |
|
student |
add hints that I can toggle on/off |
get help with more difficult cards |
|
student |
add pictures to certain flashcards |
better represent topics that heavily feature topics and diagrams |
|
student |
have a question that expects more than one answer |
test myself more complex questions |
|
student |
different template designs for my flashcards |
have a personalised experience while revising |
(For all use cases below, the System is Know-It-All
and the Actor is the Student
, unless specified otherwise)
MSS
-
Student begins a test session.
-
System presents the question on the lowest-performing flashcard first.
-
Student inputs his/her answer.
-
System indicates whether student’s answer is correct or wrong and shows the answer of the flashcard.
-
Student navigates to next flashcard.
-
Repeat steps 2-4 until all the flashcards in the folder are tested.
Use case ends
Extensions
-
3a. Student doesn’t know the answer and wants to see the answer without attempting.
-
3a1. Student uses the reveal command.
-
3a2. Answer is displayed to the student.
-
MSS
-
Student navigates to a folder that he wants to add a flashcard to.
-
Student inputs question and answer to be stored as flashcard.
-
System stores the details as a flashcard under the current folder.
Use case ends
Extensions
-
2a. Student only inputs a question but no answer.
-
2a1. System displays an error message informing the user that the command format is invalid.
Use case resumes from step 2.
-
MSS
-
Student navigates to the folder that contains the flashcard to be edited.
-
Student indicates the card to be edited, as well as the new question.
-
System stores the updated details for the edited card.
Use case ends
Extensions
-
2a. Student enters a blank as the desired question.
-
2a1. System displays an error message informing the user that the question cannot be a blank.
Use case resumes from step 2.
-
-
2b. Student enters a card index that does not exist.
-
2b1. System displays an error message prompting the user to choose a valid card index.
Use case resumes from step 2.
-
Guarantees
-
A folder of the desired name is created.
MSS
-
Student navigates to home directory.
-
Student inputs the name of the folder he wants to create.
-
System creates a folder of the desired name and shows it on the home directory.
Use case ends
Extensions
-
2a. Student inputs a name that already exists.
-
2a1. System displays an error message prompting the user to use a folder name that is not taken.
Use case resumes from step 2.
-
Guarantees
-
A particular folder as selected by the student is renamed to the desired name.
MSS
-
Student navigates to home directory.
-
Student indicates the folder he wants to rename, as well as the new name.
-
System renames the folder to the new name and shows it on the home directory.
Use case ends
Extensions
-
2a. Student inputs a name that already exists.
-
2a1. System displays an error message prompting the user to use a folder name that is not taken.
Use case resumes from step 2.
-
-
2b. Student chooses a folder that does not exist.
-
2b1. System displays an error message prompting the user to choose a valid folder.
Use case resumes from step 2.
-
-
2c. Student enters a blank as the desired new folder name.
-
2c1. System displays an error message informing that the folder name cannot be a blank.
Use case resumes from step 2.
-
MSS
-
Student indicates the folder he wants to enter.
-
System enters the folder and displays the folder content.
Use case ends
Extensions
-
1a. Student chooses a folder that does not exist.
-
1a1. System displays an error message prompting the user to choose a valid folder.
Use case resumes from step 1.
-
-
1b. Student is already inside a folder.
-
1b1. System displays an error message informing that the user can only navigate into the folder when he is at the home directory.
-
1b2. Student navigates back to home directory.
Use case resumes from step 1.
-
MSS
-
Student enters the a folder.
-
Student indicates that they want to see the report for this folder.
-
System displays a full-screen report.
Use case ends
-
Should work on any mainstream OS as long as it has Java
9
or higher installed. -
Should be able to hold up to 1000 cards without a noticeable sluggishness in performance for typical usage.
-
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.
-
The user interface should be intuitive enough even for medical students to use the app.
{More to be added}
Card Score: The number of correct answers divided by the number of attempts for a single card. When the user is tested on a card, this number is automatically calculated and recorded.
Flashcard/Card: An object containing a single question and answer, and optionally, hints. There are 2 types of cards, Single-answer and MCQ. MCQ cards feature incorrect options in addition to the card answer, while Single-answer cards do not.
Folder Score: The average of all card scores in a folder after a test session. This number is automatically recorded after each test session.
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 to be added … }
-
Deleting a card while all cards are listed
-
Prerequisites: List all cards using the
list
command. Multiple cards 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 card 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.
-
{ more test cases to be added … }