Skip to content

Latest commit

 

History

History
executable file
·
1700 lines (1175 loc) · 81.5 KB

DeveloperGuide.adoc

File metadata and controls

executable file
·
1700 lines (1175 loc) · 81.5 KB

Know-It-All - Developer Guide

By: CS2103-AY2018/19s2-W10-4 Team Since: Mar 2019 Licence: MIT

1. Setting up

1.1. Prerequisites

  1. 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 JDK 9.
  2. IntelliJ IDE

    ℹ️
    IntelliJ by default has Gradle and JavaFx plugins installed.
    Do not disable them. If you have disabled them, go to File > Settings > Plugins to re-enable them.

1.2. Setting up the project in your computer

  1. Fork this repo, and clone the fork to your computer

  2. Open IntelliJ (if you are not in the welcome screen, click File > Close Project to close the existing project dialog first)

  3. Set up the correct JDK version for Gradle

    1. Click Configure > Project Defaults > Project Structure

    2. Click New…​ and find the directory of the JDK

  4. Click Import Project

  5. Locate the build.gradle file and select it. Click OK

  6. Click Open as Project

  7. Click OK to accept the default settings

  8. Open a console 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.

  9. Open MainWindow.java and check for any code errors

    1. 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

    2. 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

  10. Repeat this for the test folder as well (e.g. check HelpWindowTest.java for code errors, and if so, resolve it the same way)

1.3. Verifying the setup

  1. Run the seedu.knowitall.MainApp and try a few commands

  2. Run the tests to ensure they all pass.

1.4. Configurations to do before writing code

1.4.1. Configuring the coding style

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,

  1. Go to File > Settings…​ (Windows/Linux), or IntelliJ IDEA > Preferences…​ (macOS)

  2. Select Editor > Code Style > Java

  3. Click on the Imports tab to set the order

    • For Class count to use import with '*' and Names count to use static import with '*': Set to 999 to prevent IntelliJ from contracting the import statements

    • For Import Layout: The order is import static all other imports, import java.*, import javax.*, import org.*, import com.*, import all other imports. Add a <blank line> between each import

Optionally, you can follow the UsingCheckstyle.adoc document to configure Intellij to check style-compliance as you write code.

1.4.2. Updating documentation to match your fork

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:

  1. Configure the site-wide documentation settings in build.gradle, such as the site-name, to suit your own project.

  2. Replace the URL in the attribute repoURL in DeveloperGuide.adoc and UserGuide.adoc with the URL of your fork.

1.4.3. Setting up CI

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)

1.4.4. Getting started with coding

When you are ready to start coding, get some sense of the overall design by reading Section 2.1, “Architecture”.

2. Design

2.1. Architecture

Architecture
Figure 1. Architecture Diagram

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.

  • UI: The UI of the App.

  • Logic: The command executor.

  • Model: Holds the data of the App in-memory.

  • Storage: Reads data from, and writes data to, the hard disk.

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.

LogicClassDiagram
Figure 2. Class Diagram of the Logic Component

How the architecture components interact with each other

The Sequence Diagram below shows how the components interact with each other for the scenario where the user issues the command delete 1.

SDforDeleteCard
Figure 3. Component interactions for delete 1 command

The sections below give more details of each component.

2.2. UI component

UiClassDiagram
Figure 4. Structure of the UI 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.

2.3. Logic component

LogicClassDiagram
Figure 5. Structure of the Logic Component

API : Logic.java

  1. Logic uses the CardFolderParser class to parse the user command.

  2. This results in a Command object which is executed by the LogicManager.

  3. The command execution can affect the Model (e.g. adding a card).

  4. The result of the command execution is encapsulated as a CommandResult object which is passed back to the Ui.

  5. In addition, the CommandResult object can also instruct the Ui 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.

DeleteCardSdForLogic
Figure 6. Interactions Inside the Logic Component for the delete 1 Command

2.4. Model component

ModelClassDiagram
Figure 7. Structure of the Model Component

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> and FilteredList<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.

ℹ️
As a more OOP model, we can change the Card implementation to be that of a parent class, from which 2 subclasses, SingleAnswerCard and McqCard can inherit from. This would eliminate the need for the Card class to maintain a Set of MCQ Options even if it is a Single-answer card. An example of how such a model may look like is given below.

ModelClassBetterOopDiagram

2.5. Storage component

StorageClassDiagram
Figure 8. Structure of the Storage Component

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.

2.6. Common classes

Classes used by multiple components are in the seedu.knowitall.commons package.

3. Implementation

This section describes some noteworthy details on how certain features are implemented.

3.1. Cards

3.1.1. Current Implementation

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.

Model

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 active VersionedCardFolder 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

Logic

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.

Example Usage

The following steps detail the underlying logic executed when a user does a card-related operation, say an add card operation.

  1. 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 typing add q/What is the largest organ? a/Skin.

    CardImplementationAddExample

  2. The command parser reads the string input (as entered by the user) and returns the corresponding Command, an AddCommand object in this instance.

  3. Upon execution, the AddCommand checks if the card to be added is already present in the current folder. If so, an exception is thrown.

  4. The AddCommand then calls the ModelManager#addCard(Card card) method.

  5. The new card will then be added to the active VersionedCardFolder.

  6. If the user is not inside a folder, or if the card to add already exists inside the current folder, the addCommand will throw a CommandException.

The following sequence diagram demonstrates how AddCommand works.

AddCommandSequenceDiagram

3.1.2. Design Considerations

Aspect: How to represent options for MCQ cards
  • Alternative 1 (current choice): Maintain a set of Option objects to represent incorrect options, separate from the Answer field of each Card.

    • 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.

3.2. Score feature

3.2.1. Current Implementation

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.

3.2.2. Design Considerations

Aspect: How to represent score
  • 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.

Aspect: How to read/write score from file
  • 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 to String.

    • 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.

3.3. Report feature

3.3.1. Current Implementation

The report feature is meant to provide the user with the ability to look back and compare their folder score from previous test sessions.

3.3.2. Design Considerations

Aspect: How to display folder scores
  • 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.

Aspect: Where to display folder scores
  • 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.

3.4. State

3.4.1. Current Implementation

Model

Previously, Model did not have a concept of state as there was only one screen where the user could be. But as Know-It-All grew, there are more screens that a user can be in and more commands that can only be executed in certain screens. Thus there is a need to manage the state in Model.

3.4.2. Design Considerations

Aspect: How to manage state
  • 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.

3.5. Import/Export feature

Current Implementation

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.

Logic

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.

Model

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.

csvmanager package:

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.

Example Usage
  1. User wants to export folders Human Anatomy and Nervous System. Suppose that these two folders index as shown on the main application screen is 1 and 3 respectively. The user types export 1 3

  2. The Export command parser extracts the indices, parses the indices into a List<Integer> and ExportCommand object, which keeps track of the indices.

  3. The Export command executes, calling Model#exportCardFolders method.

  4. 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 throws CardFolderNotFoundException if card folder index is not found in list.

    import export model list1
  5. 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.e Human Anatomy.csv and Nervous System.csv

  6. User wants to import Human Anatomy.csv file. Human Anatomy.csv file contains flashcards represented in csv file format. User types import Blood.csv command

  7. Import command parser extracts file name, wraps file into a CsvFile object and parses the CsvFile into an Import Command object.

  8. Logic unit executes the import command. The execute method makes a call to Model#importCardFolder method.

  9. 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 the filteredFoldersList 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

import export sequenceDiagram

3.5.1. CSV file structure

Example of a common cardfolder csv file

Blood.csv

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.

3.5.2. Design Considerations

Aspect: Which component responsible for import/export logic
  • 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.

        1. Model class can focus on the representation of the in-memory card folders Storage

        2. 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

Aspect: csv file design structure
  • 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.

3.6. Folders

3.6.1. Current Implementation

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).

Model

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.

ModelEnhancementDiagram

To allow users to operate on multiple CardFolders, the following notable methods were also introduced:

  • ModelManager#addFolder(CardFolder cardfolder) - Adds a specified cardfolder to the ModelManager’s list

  • ModelManager#deleteFolder(int index) - Deletes the CardFolder at the specified index in the ModelManager’s list

  • ModelManager#getActiveCardFolderIndex() - Gets the index of the current active CardFolder

  • ModelManager#enterFolder(int index) - Specifies the active CardFolder for operations to be performed on via the index in ModelManager’s list and sets the boolean inFolder to true to denote that user is inside a folder.

  • ModelManager#exitFolderToHome() - Sets the boolean inFolder to false to indicate that the user is at the home directory.

  • ModelManager#renameFolder(int index, String newName) - Renames the folder at the specified index in the ModelManager’s list to the new name.

  • ModelManager#isInFolder() - Returns true if the ModelManager is currently inside a folder.

Storage

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.

StorageEnhancementDiagram

Notable new methods:

  • StorageManager#readCardFolders() - Reads in all CardFolders from all CardFolderStorage objects in the list.

  • StorageManager#saveCardFolders(List<ReadOnlyCardFolder> cardFolders) - Saves all CardFolders provided in the argument to the user’s data directory.

Logic

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 an AddFolderCommand. The object is returned to the LogicManager.

ℹ️
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 the AddFolderCommand, storing the result and then transferring control to the Model component with the ModelManager#addFolder() method.

  • Step 4. The ModelManager creates a VersionedCardFolder 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 the LogicManager that the list of CardFolders have changed.

  • Step 5. The Logic component takes over control and checks if the ModelManager is modified. In the case of addfolder the object is indeed modified (as a result of Step 4) and thus the component proceeds to save the Model’s CardFolders to Storage.

  • Step 6. Before handing over control to Storage, the LogicManager obtains the information to save and the path to save to with ModelManager#getCardFolders() and ModelManager#getCardFoldersFilesPath() respectively. It then passes these objects as parameters when it calls StorageManager#saveCardFolders().

  • Step 7. The Storage component receives control, with the StorageManager clearing the directory at the specified path and creating JsonCardFolderStorage objects with path names equivalent to the names of the folders it has received. It then proceeds to invoke JsonCardFolderStorage#saveCardFolder() on all the JsonCardFolderStorage to save all the folders before returning to the LogicManager.

ℹ️
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.

AddFolderSequenceDiagram
Figure 9. Component interactions for an addfolder command
UI

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 a FolderListPanel to display all the folders in a list inside the main window.

  • When the user is within a folder, fullScreenPlaceholder holds a CardMainScreen object, which is composed of a CardListPanel and BrowserPanel. These represent the list of cards on the scrolling sidebar, as well as the card viewer on the right. The content within the CardMainScreen 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 existing BrowserPanel, are used to create a new CardMainScreen object.

  • Step 5. The content held by fullScreenPlaceholder is replaced with the newly generated CardMainScreen.

EnterFolderGUISequenceDiagram
Figure 10. UI behaviour when user enters folder

3.6.2. Design Considerations

Aspect: How multiple folders are represented in Model
  • 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.

Aspect: Folder identification
  • 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.

Aspect: Storage file name and folder name
  • 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.

Aspect: What folders to generate in the event corrupted storage files are encountered
  • 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.

3.7. Navigating folders

3.7.1. Current Implementation

Navigation State

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.

Change Command

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:

  1. change .. - Returns the user to the home directory. This command can only be executed when the user is inside a folder.

  2. change FOLDER_INDEX - Enters the folder specified by FOLDER_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.

ChangeCommandSequenceDiagram
Figure 11. Component interactions for change command

3.7.2. Design Considerations

Aspect: Command format to enter and exit folders
  • Alternative 1 (current choice): Use variations of the same command (e.g. change .. and change 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 and enter 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.

3.8. Test Session feature

3.8.1. Overall Current Implementation

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 using currentTestedCardIndex.

    • Set to null if user is not inside a test session

    • Related methods:

      • ModelManager#setCurrentTestedCard(Card card) - set currentTestedCard to the card specified.

      • ModelManager#getCurrentTestedCard() - returns the currentTestedCard.

  • 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() - set cardAlreadyAnswered to true.

      • ModelManager#setCardAsNotAnswered() - set cardAlreadyAnswered to false.

      • ModelManager#isCardAlreadyAnswered() - returns true if the current card has already been answered and false otherwise.

3.8.2. Overall Design Considerations

Aspect: Usage of an extra card variable to keep track of the current card in test session
  • 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 the currentTestedCardIndex suffices. Every time a card is needed, can reference it using currentTestedCardFolder.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.

3.8.3. Test / End Command

Current Implementation
Model

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.

UI

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 new TestSession page with the card specified and bring the page forward in front of the current CardMainScreen page.

  • MainWindow#handleEndTestSession() - deletes the current TestSession page and the CardMainScreen page at the back is now presented to the user.

Example Usage of test command

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:

  1. currentTestedCardFolder is set to the current folder the user is in, by invoking getCardList() from the active VersionedCardFolder in folders.

    ℹ️
    If this folder is empty such that there is no card to present to the user, an EmptyCardFolderException is thrown, to be caught in TestCommand, which then throws a CommandException to display an error message.
  2. 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.

  3. ModelManager#setCurrentTestedCard(currentTestedCardIndex) is then invoked to set currentTestedCard to the first card in the folder as currentTestedCardIndex is set to 0.

  4. inTestSession is set to true.

  5. 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.

TestCommandSequenceDiagram
Figure 12. Component interactions for a test command


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.

Example Usage of end command

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:

  1. currentTestedCardFolder is set to null.

  2. ModelManager#setCurrentTestedCard(null) is invoked to set currentTestedCard to null.

  3. inTestSession is set to false.

  4. 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.

Design Considerations
Aspect: Way to execute a test/end command
  • Alternative 1 (current choice): test is executed when inside a folder. The user does not have to specify the folder index and test 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.g test 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.

3.8.4. Answer Command

Current Implementation
Model

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
Logic

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.

UI

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() - invokes TestSession#handleCorrectAnswer()

  • MainWindow#handleWrongAnswer() - invokes TestSession#handleWrongAnswer()

  • TestSession#handleCorrectAnswer() - updates current TestSession page to green colour background with correct answer and correct answer description

  • TestSession#handleWrongAnswer() - updates current TestSession page to red colour background with correct answer and wrong answer description

Example Usage of answer command

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.

AnswerCommandSequenceDiagram
Figure 13. Component interactions for a answer command


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.

3.8.5. Next Command

Current Implementation
Model

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).

UI

To display the next card in the test session, the method below is introduced in MainWindow.

  • MainWindow#handleNextCardTestSession(Card card) - deletes the current TestSession page and adds a new TestSession page with this next card specified.

Example Usage of next command

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:

  1. currentTestedCardIndex incremented by 1.

  2. currentTestedCardIndex is then checked if it equals to the size of currentTestedCardFolder.

    • Case 1: This check returns true.
      This means currentTestedCardIndex 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 means currentTestedCardIndex is valid and will be used to get the next card from currentTestedCardFolder. This card is set as the currentTestedCard via the ModelManager#setCurrentTestedCard (cardToTest). ModelManager#setCardAsNotAnswered is then invoked to reset the value of cardAlreadyAnswered. This method returns true.

Step 4. From the result of ModelManager#testNextCard() method earlier:

  • Case 1: Method returns false.
    A next command will be equivalent to an end 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 the Card, 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.

NextCommandSequenceDiagram
Figure 14. Component interactions for a next command
Design Considerations
Aspect: Behavior of next command executed on the last card
  • Alternative 1 (current choice): A next command will be equivalent to an end 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.

3.9. Undo/Redo feature

ℹ️
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.

3.9.1. Current Implementation

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.

UndoRedoStartingStateListDiagram

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.

UndoRedoNewCommand1StateListDiagram

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.

UndoRedoNewCommand2StateListDiagram
ℹ️
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.

UndoRedoExecuteUndoStateListDiagram
ℹ️
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:

UndoRedoSequenceDiagram

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.

UndoRedoNewCommand3StateListDiagram

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.

UndoRedoNewCommand4StateListDiagram

The following activity diagram summarizes what happens when a user executes a new command:

UndoRedoActivityDiagram

3.9.2. Design Considerations

Aspect: How undo & redo executes
  • 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.

Aspect: Data structure to support the undo/redo commands
  • 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 and VersionedCardFolder.

  • 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.

3.10. Logging

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 using LogsCenter.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

3.11. Configuration

Certain properties of the application can be controlled (e.g user prefs file location, logging level) through the configuration file (default: config.json).

4. Documentation

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.

4.1. Editing Documentation

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.

4.2. Publishing Documentation

See UsingTravis.adoc to learn how to deploy GitHub Pages using Travis.

4.3. Converting Documentation to PDF format

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.

  1. Follow the instructions in UsingGradle.adoc to convert the AsciiDoc files in the docs/ directory to HTML format.

  2. Go to your generated HTML files in the build/docs folder, right click on them and select Open withGoogle Chrome.

  3. Within Chrome, click on the Print option in Chrome’s menu.

  4. Set the destination to Save as PDF, then click Save to save a copy of the file in PDF format. For best results, use the settings indicated in the screenshot below.

chrome save as pdf
Figure 15. Saving documentation as PDF files in Chrome

4.4. Site-wide Documentation Settings

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.
Table 1. List of site-wide attributes
Attribute question Description Default value

site-name

The question of the website. If set, the question will be displayed near the top of the page.

not set

site-githuburl

URL to the site’s repository on GitHub. Setting this will add a "View on GitHub" link in the navigation bar.

not set

site-seedu

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

4.5. Per-file Documentation Settings

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.
Table 2. List of per-file attributes, excluding Asciidoctor’s built-in attributes
Attribute question Description Default value

site-section

Site section that the document belongs to. This will cause the associated item in the navigation bar to be highlighted. One of: UserGuide, DeveloperGuide, LearningOutcomes*, AboutUs, ContactUs

* Official SE-EDU projects only

not set

no-site-header

Set this attribute to remove the site navigation bar.

not set

4.6. Site Template

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 docs/templates requires some knowledge and experience with Ruby and Asciidoctor’s API. You should only modify them if you need greater control over the site’s layout than what stylesheets can provide. The SE-EDU team does not provide support for modified template files.

5. Testing

5.1. Running Tests

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 choose Run '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)

5.2. Types of tests

We have two types of tests:

  1. GUI Tests - These are tests involving the GUI. They include,

    1. System Tests that test the entire App by simulating user actions on the GUI. These are in the systemtests package.

    2. Unit tests that test the individual components. These are in seedu.knowitall.ui package.

  2. Non-GUI Tests - These are tests not involving the GUI. They include,

    1. Unit tests targeting the lowest level methods/classes.
      e.g. seedu.knowitall.commons.StringUtilTest

    2. 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

    3. 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

5.3. Troubleshooting Testing

Problem: HelpWindowTest fails with a NullPointerException.

  • Reason: One of its dependencies, HelpWindow.html in src/main/resources/docs is missing.

  • Solution: Execute Gradle task processResources.

6. Dev Ops

6.1. Build Automation

See UsingGradle.adoc to learn how to use Gradle for build automation.

6.2. Continuous Integration

We use Travis CI and AppVeyor to perform Continuous Integration on our projects. See UsingTravis.adoc and UsingAppVeyor.adoc for more details.

6.3. Coverage Reporting

We use Coveralls to track the code coverage of our projects. See UsingCoveralls.adoc for more details.

6.4. Documentation Previews

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.

6.5. Making a Release

Here are the steps to create a new release.

  1. Update the version number in MainApp.java.

  2. Generate a JAR file using Gradle.

  3. Tag the repo with the version number. e.g. v0.1

  4. Create a new release using GitHub and upload the JAR file you created.

6.6. Managing Dependencies

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:

  1. Include those libraries in the repo (this bloats the repo size)

  2. Require developers to download those libraries manually (this creates extra work for developers)

Appendix A: Product Scope

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.

Appendix B: User Stories

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

Appendix C: Use Cases

(For all use cases below, the System is Know-It-All and the Actor is the Student, unless specified otherwise)

UC01 Test flashcards

MSS

  1. Student begins a test session.

  2. System presents the question on the lowest-performing flashcard first.

  3. Student inputs his/her answer.

  4. System indicates whether student’s answer is correct or wrong and shows the answer of the flashcard.

  5. Student navigates to next flashcard.

  6. 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.

UC02 Add flashcards

MSS

  1. Student navigates to a folder that he wants to add a flashcard to.

  2. Student inputs question and answer to be stored as flashcard.

  3. 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.

UC03 Edit flashcard question

MSS

  1. Student navigates to the folder that contains the flashcard to be edited.

  2. Student indicates the card to be edited, as well as the new question.

  3. 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.

UC04 Add folder

Guarantees

  • A folder of the desired name is created.

MSS

  1. Student navigates to home directory.

  2. Student inputs the name of the folder he wants to create.

  3. 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.

UC05 Edit folder name

Guarantees

  • A particular folder as selected by the student is renamed to the desired name.

MSS

  1. Student navigates to home directory.

  2. Student indicates the folder he wants to rename, as well as the new name.

  3. 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.

UC06 Navigating into folders

MSS

  1. Student indicates the folder he wants to enter.

  2. 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.

UC06 Display report for a folder

MSS

  1. Student enters the a folder.

  2. Student indicates that they want to see the report for this folder.

  3. System displays a full-screen report.

    Use case ends

Appendix D: Non Functional Requirements

  1. Should work on any mainstream OS as long as it has Java 9 or higher installed.

  2. Should be able to hold up to 1000 cards without a noticeable sluggishness in performance for typical usage.

  3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse.

  4. The user interface should be intuitive enough even for medical students to use the app.

{More to be added}

Appendix E: Glossary

Card Answer: The correct answer of a card.

Card Hint: The optional hint of a card.

Card Option: An incorrect option for an MCQ card.

Card Question: The question of a card.

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: A collections of flashcards, grouped topically. There are no sub-folders.

Folder Score: The average of all card scores in a folder after a test session. This number is automatically recorded after each test session.

Home Directory: The home page where all the folders are listed. From here, users can enter folders to view cards.

Mainstream OS: Windows, Linux, Unix, OS-X

Test Session: A session where all flashcards in a folder are queued to have their questions displayed. The user is required to key in an answer for each question.

Appendix F: Instructions for Manual Testing

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.

F.1. Launch and Shutdown

  1. Initial launch

    1. Download the jar file and copy into an empty folder

    2. Double-click the jar file
      Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum.

  2. Saving window preferences

    1. Resize the window to an optimum size. Move the window to a different location. Close the window.

    2. 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 …​ }

F.2. Deleting a card

  1. Deleting a card while all cards are listed

    1. Prerequisites: List all cards using the list command. Multiple cards in the list.

    2. 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.

    3. Test case: delete 0
      Expected: No card is deleted. Error details shown in the status message. Status bar remains the same.

    4. 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 …​ }

F.3. Saving data

  1. Dealing with missing/corrupted data files

    1. {explain how to simulate a missing/corrupted file and the expected behavior}

{ more test cases to be added …​ }