diff --git a/.gitignore b/.gitignore index 823d175eb670..645bc02c38b5 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ classes/ /bin/ src/main/resources/docs/ out/ +!_reposense/config.json diff --git a/README.adoc b/README.adoc index 450054624f48..fc90705c510d 100644 --- a/README.adoc +++ b/README.adoc @@ -1,11 +1,10 @@ -= Address Book (Level 4) += CAPTracker ifdef::env-github,env-browser[:relfileprefix: docs/] -https://travis-ci.org/se-edu/addressbook-level4[image:https://travis-ci.org/se-edu/addressbook-level4.svg?branch=master[Build Status]] -https://ci.appveyor.com/project/damithc/addressbook-level4[image:https://ci.appveyor.com/api/projects/status/3boko2x2vr5cc3w2?svg=true[Build status]] -https://coveralls.io/github/se-edu/addressbook-level4?branch=master[image:https://coveralls.io/repos/github/se-edu/addressbook-level4/badge.svg?branch=master[Coverage Status]] -https://www.codacy.com/app/damith/addressbook-level4?utm_source=github.com&utm_medium=referral&utm_content=se-edu/addressbook-level4&utm_campaign=Badge_Grade[image:https://api.codacy.com/project/badge/Grade/fc0b7775cf7f4fdeaf08776f3d8e364a[Codacy Badge]] -https://gitter.im/se-edu/Lobby[image:https://badges.gitter.im/se-edu/Lobby.svg[Gitter chat]] +https://travis-ci.org/CS2103-AY1819S1-T13-4/main[image:https://travis-ci.org/CS2103-AY1819S1-T13-4/main.svg?branch=master[Build Status]] +https://ci.appveyor.com/project/alexkmj/main/branch/master[image:https://ci.appveyor.com/api/projects/status/1sxo4mvlcd5oia7h?svg=true[Build Status]] +https://coveralls.io/github/CS2103-AY1819S1-T13-4/main?branch=master[image:https://coveralls.io/repos/github/CS2103-AY1819S1-T13-4/main/badge.svg?branch=master[Coverage Status]] +image:https://api.codacy.com/project/badge/Grade/28acc5b7c04044519964e6253aeb58fa["Codacy code quality", link="https://www.codacy.com/app/alexkmj/main?utm_source=github.com&utm_medium=referral&utm_content=CS2103-AY1819S1-T13-4/main&utm_campaign=Badge_Grade"] ifdef::env-github[] image::docs/images/Ui.png[width="600"] @@ -15,25 +14,21 @@ ifndef::env-github[] image::images/Ui.png[width="600"] endif::[] -* This is a desktop Address Book application. It has a GUI but most of the user interactions happen using a CLI (Command Line Interface). -* It is a Java sample application intended for students learning Software Engineering while using Java as the main programming language. -* It is *written in OOP fashion*. It provides a *reasonably well-written* code example that is *significantly bigger* (around 6 KLoC)than what students usually write in beginner-level SE modules. -* What's different from https://github.com/se-edu/addressbook-level3[level 3]: -** A more sophisticated GUI that includes a list panel and an in-built Browser. -** More test cases, including automated GUI testing. -** Support for _Build Automation_ using Gradle and for _Continuous Integration_ using Travis CI. +* CAPTracker is for those students who prefer to use a desktop app for calculating and managing their CAP. More importantly CAPTracker is optimized for those who prefer to work with a Command Line Interface (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, CAPTracker is the ideal application to calculate your current CAP, and predict what grades you need in modules you haven’t taken to achieve your target CAP. == Site Map * <> * <> -* <> * <> * <> == Acknowledgements -* Some parts of this sample application were inspired by the excellent http://code.makery.ch/library/javafx-8-tutorial/[Java FX tutorial] by +* Some parts of this application were inspired by https://github.com/nus-cs2103-AY1819S1/ +* Some parts of this application were inspired by https://github.com/se-edu/addressbook-level4 +* Some parts of this application were inspired by https://github.com/se-edu/addressbook-level3 +* Some parts of this application were inspired by the excellent http://code.makery.ch/library/javafx-8-tutorial/[Java FX tutorial] by _Marco Jakob_. * Libraries used: https://github.com/TestFX/TestFX[TextFX], https://bitbucket.org/controlsfx/controlsfx/[ControlsFX], https://github.com/FasterXML/jackson[Jackson], https://github.com/google/guava[Guava], https://github.com/junit-team/junit5[JUnit5] diff --git a/_reposense/config.json b/_reposense/config.json new file mode 100644 index 000000000000..495b824e5ca6 --- /dev/null +++ b/_reposense/config.json @@ -0,0 +1,30 @@ +{ + "authors": + [ + { + "githubId": "alexkmj", + "displayName": "ALE...KOH", + "authorNames": ["alex", "alexkmj", "Alex Koh", "alex koh"] + }, + { + "githubId": "josephambe", + "displayName": "JOSEP...R KIM", + "authorNames": ["josephambe"] + }, + { + "githubId": "jeremiah-ang", + "displayName": "JEREM...NG EN", + "authorNames": ["jeremiah-ang"] + }, + { + "githubId": "jeremyyew", + "displayName": "JEREM...W ERN", + "authorNames": ["jeremyyew"] + }, + { + "githubId": "BugEyedBug", + "displayName": "KONG ...N YIN", + "authorNames": ["BugEyedBug"] + } + ] +} diff --git a/build.gradle b/build.gradle index f8e614f8b49b..cb52dc93a47a 100644 --- a/build.gradle +++ b/build.gradle @@ -82,7 +82,7 @@ dependencies { } shadowJar { - archiveName = 'addressbook.jar' + archiveName = 'captracker.jar' destinationDir = file("${buildDir}/jar/") } @@ -134,7 +134,7 @@ test { testLogging { events TestLogEvent.FAILED, TestLogEvent.SKIPPED - + exceptionFormat "full" // Prints the currently running test's name in the CI's build log, // so that we can check if tests are being silently skipped or // stalling the build. @@ -207,9 +207,8 @@ asciidoctor { idprefix: '', // for compatibility with GitHub preview idseparator: '-', 'site-root': "${sourceDir}", // must be the same as sourceDir, do not modify - 'site-name': 'AddressBook-Level4', - 'site-githuburl': 'https://github.com/se-edu/addressbook-level4', - 'site-seedu': true, // delete this line if your project is not a fork (not a SE-EDU project) + 'site-name': 'CAPTracker', + 'site-githuburl': 'https://github.com/CS2103-AY1819S1-T13-4/main' ] options['template_dirs'].each { diff --git a/collated/functional/alexkmj.md b/collated/functional/alexkmj.md new file mode 100644 index 000000000000..7c8106c1cf1c --- /dev/null +++ b/collated/functional/alexkmj.md @@ -0,0 +1,2023 @@ +# alexkmj +###### /java/seedu/address/logic/LogicManager.java +``` java + public LogicManager(Model model) { + this.model = model; + history = new CommandHistory(); + transcriptParser = new TranscriptParser(); + addressBookParser = new AddressBookParser(); + } + +``` +###### /java/seedu/address/logic/LogicManager.java +``` java + @Override + public CommandResult execute(String commandText) throws CommandException, ParseException { + logger.info("----------------[USER COMMAND][" + commandText + "]"); + try { + if (commandText.trim().startsWith("c_")) { + Command command = transcriptParser.parseCommand(commandText); + return command.execute(model, history); + } + + Command command = addressBookParser.parseCommand(commandText); + return command.execute(model, history); + } finally { + history.add(commandText); + } + } + +``` +###### /java/seedu/address/logic/LogicManager.java +``` java + @Override + public ObservableList getFilteredModuleList() { + return model.getFilteredModuleList(); + } + + @Override + public ObservableList getFilteredPersonList() { + return model.getFilteredPersonList(); + } + + @Override + public ListElementPointer getHistorySnapshot() { + return new ListElementPointer(history.getHistory()); + } +} +``` +###### /java/seedu/address/logic/Logic.java +``` java + /** Returns an unmodifiable view of the filtered list of modules */ + ObservableList getFilteredModuleList(); + + /** Returns an unmodifiable view of the filtered list of persons */ + ObservableList getFilteredPersonList(); + + /** Returns the list of input entered by the user, encapsulated in a {@code ListElementPointer} object */ + ListElementPointer getHistorySnapshot(); +} +``` +###### /java/seedu/address/logic/parser/AddModuleCommandParser.java +``` java +/** + * Parses input arguments and creates a new AddCommand object + */ +public class AddModuleCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddCommand + * and returns an AddCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddModuleCommand parse(String args) throws ParseException { + String[] tokenizedArgs = ParserUtil.tokenize(args); + validateNumOfArgs(tokenizedArgs, 4, 5); + + int index = 0; + + Code code = ParserUtil.parseCode(tokenizedArgs[index++]); + Year year = ParserUtil.parseYear(tokenizedArgs[index++]); + Semester semester = ParserUtil.parseSemester(tokenizedArgs[index++]); + Credit credit = ParserUtil.parseCredit(tokenizedArgs[index++]); + + Module module = null; + + if (tokenizedArgs.length == 4) { + module = new Module(code, year, semester, credit, null, false); + } else { + Grade grade = ParserUtil.parseGrade(tokenizedArgs[index++]); + module = new Module(code, year, semester, credit, grade, true); + } + + return new AddModuleCommand(module); + } +} +``` +###### /java/seedu/address/logic/parser/TranscriptParser.java +``` java +/** + * Parses user input. + */ +public class TranscriptParser { + + /** + * Used for initial separation of command word and args. + */ + private static final Pattern BASIC_COMMAND_FORMAT = Pattern + .compile("(?\\S+)(?.*)"); + + /** + * Parses user input into command for execution. + * + * @param userInput full user input string + * @return the command based on the user input + * @throws ParseException if the user input does not conform the expected format + */ + public Command parseCommand(String userInput) throws ParseException { + final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); + if (!matcher.matches()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); + } + + final String commandWord = matcher.group("commandWord") + .replaceFirst("c_", ""); + final String arguments = matcher.group("arguments"); + switch (commandWord) { + case AddModuleCommand.COMMAND_WORD: + return new AddModuleCommandParser().parse(arguments); + case CapCommand.COMMAND_WORD: + return new CapCommand(); + case GoalCommand.COMMAND_WORD: + return new GoalCommandParser().parse(arguments); + default: + throw new ParseException(MESSAGE_UNKNOWN_COMMAND); + } + } + +} +``` +###### /java/seedu/address/logic/parser/ParserUtil.java +``` java + /** + * Tokenizes args into an array of args. Checks if args is null and trims leading and trailing + * whitespaces. + * + * @param args the target args that would be tokenize + * @return array of args + */ + public static String[] tokenize(String args) { + requireNonNull(args); + String trimmedArgs = args.trim(); + return trimmedArgs.split(" "); + } + +``` +###### /java/seedu/address/logic/parser/ParserUtil.java +``` java + /** + * Validates the number of arguments. If number of arguments is not within the bounds, + * ParseException will be thrown. + * + * @throws ParseException if the number of arguments is invalid + */ + public static void validateNumOfArgs(String[] args, int min, int max) throws ParseException { + if (args.length < min || args.length > max) { + throw new ParseException("Invalid number of arguments!" + + "Number of arguments should be more than or equal to " + min + + " and less than or equal to " + max); + } + } + +``` +###### /java/seedu/address/logic/parser/ParserUtil.java +``` java + /** + * Parses a {@code String code} into a {@code Code}. Leading and trailing whitespaces will be + * trimmed. + * + * @throws ParseException if the given {@code code} is invalid. + */ + public static Code parseCode(String args) throws ParseException { + requireNonNull(args); + String trimmedCode = args.trim(); + if (!Code.isValidCode(trimmedCode)) { + throw new ParseException(Code.MESSAGE_CODE_CONSTRAINTS); + } + return new Code(trimmedCode); + } + +``` +###### /java/seedu/address/logic/parser/ParserUtil.java +``` java + /** + * Parses a {@code String year} into a {@code Year}. Leading and trailing whitespaces will be + * trimmed. + * + * @throws ParseException if the given {@code year} is invalid. + */ + public static Year parseYear(String args) throws ParseException { + requireNonNull(args); + String trimmedYear = args.trim(); + if (!Year.isValidYear(trimmedYear)) { + throw new ParseException(Year.MESSAGE_YEAR_CONSTRAINTS); + } + return new Year(trimmedYear); + } + +``` +###### /java/seedu/address/logic/parser/ParserUtil.java +``` java + /** + * Parses a {@code String semester} into a {@code Semester}. Leading and trailing whitespaces + * will be trimmed. + * + * @throws ParseException if the given {@code semester} is invalid. + */ + public static Semester parseSemester(String args) throws ParseException { + requireNonNull(args); + String trimmedSemester = args.trim(); + if (!Semester.isValidSemester(trimmedSemester)) { + throw new ParseException(Semester.MESSAGE_SEMESTER_CONSTRAINTS); + } + return new Semester(trimmedSemester); + } + +``` +###### /java/seedu/address/logic/parser/ParserUtil.java +``` java + /** + * Parses a {@code String credit} into a {@code Credit}. Leading and trailing whitespaces will + * be trimmed. + * + * @throws ParseException if the given {@code credit} is invalid. + */ + public static Credit parseCredit(String args) throws ParseException { + requireNonNull(args); + String trimmedCredit = args.trim(); + int intCredit = Integer.parseInt(trimmedCredit); + if (!Credit.isValidCredit(intCredit)) { + throw new ParseException(Credit.MESSAGE_CREDIT_CONSTRAINTS); + } + return new Credit(intCredit); + } + +``` +###### /java/seedu/address/logic/parser/ParserUtil.java +``` java + /** + * Parses a {@code String grade} into a {@code Grade}. Leading and trailing whitespaces will be + * trimmed. + * + * @throws ParseException if the given {@code grade} is invalid. + */ + public static Grade parseGrade(String args) throws ParseException { + requireNonNull(args); + String trimmedGrade = args.trim(); + if (!Grade.isValidGrade(trimmedGrade)) { + throw new ParseException(Grade.MESSAGE_GRADE_CONSTRAINTS); + } + return new Grade(trimmedGrade); + } + + /** + * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing + * whitespaces will be trimmed. + * + * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). + */ + public static Index parseIndex(String oneBasedIndex) throws ParseException { + String trimmedIndex = oneBasedIndex.trim(); + if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) { + throw new ParseException(MESSAGE_INVALID_INDEX); + } + return Index.fromOneBased(Integer.parseInt(trimmedIndex)); + } + + /** + * Parses a {@code String name} into a {@code Name}. Leading and trailing whitespaces will be + * trimmed. + * + * @throws ParseException if the given {@code name} is invalid. + */ + public static Name parseName(String name) throws ParseException { + requireNonNull(name); + String trimmedName = name.trim(); + if (!Name.isValidName(trimmedName)) { + throw new ParseException(Name.MESSAGE_NAME_CONSTRAINTS); + } + return new Name(trimmedName); + } + + /** + * Parses a {@code String phone} into a {@code Phone}. Leading and trailing whitespaces will be + * trimmed. + * + * @throws ParseException if the given {@code phone} is invalid. + */ + public static Phone parsePhone(String phone) throws ParseException { + requireNonNull(phone); + String trimmedPhone = phone.trim(); + if (!Phone.isValidPhone(trimmedPhone)) { + throw new ParseException(Phone.MESSAGE_PHONE_CONSTRAINTS); + } + return new Phone(trimmedPhone); + } + + /** + * Parses a {@code String address} into an {@code Address}. Leading and trailing whitespaces + * will be trimmed. + * + * @throws ParseException if the given {@code address} is invalid. + */ + public static Address parseAddress(String address) throws ParseException { + requireNonNull(address); + String trimmedAddress = address.trim(); + if (!Address.isValidAddress(trimmedAddress)) { + throw new ParseException(Address.MESSAGE_ADDRESS_CONSTRAINTS); + } + return new Address(trimmedAddress); + } + + /** + * Parses a {@code String email} into an {@code Email}. Leading and trailing whitespaces will be + * trimmed. + * + * @throws ParseException if the given {@code email} is invalid. + */ + public static Email parseEmail(String email) throws ParseException { + requireNonNull(email); + String trimmedEmail = email.trim(); + if (!Email.isValidEmail(trimmedEmail)) { + throw new ParseException(Email.MESSAGE_EMAIL_CONSTRAINTS); + } + return new Email(trimmedEmail); + } + + /** + * Parses a {@code String tag} into a {@code Tag}. Leading and trailing whitespaces will be + * trimmed. + * + * @throws ParseException if the given {@code tag} is invalid. + */ + public static Tag parseTag(String tag) throws ParseException { + requireNonNull(tag); + String trimmedTag = tag.trim(); + if (!Tag.isValidTagName(trimmedTag)) { + throw new ParseException(Tag.MESSAGE_TAG_CONSTRAINTS); + } + return new Tag(trimmedTag); + } + + /** + * Parses {@code Collection tags} into a {@code Set}. + */ + public static Set parseTags(Collection tags) throws ParseException { + requireNonNull(tags); + final Set tagSet = new HashSet<>(); + for (String tagName : tags) { + tagSet.add(parseTag(tagName)); + } + return tagSet; + } +} +``` +###### /java/seedu/address/model/Model.java +``` java + /** + * {@code Predicate} that always evaluate to true. + */ + Predicate PREDICATE_SHOW_ALL_MODULES = unused -> true; + +``` +###### /java/seedu/address/model/Model.java +``` java + /** + * Clears existing backing model and replaces with the newly provided data. + * @param replacement the replacement. + */ + void resetData(ReadOnlyTranscript replacement); + + /** Clears existing backing model and replaces with the provided new data. TODO: REMOVE*/ + void resetData(ReadOnlyAddressBook newData); + +``` +###### /java/seedu/address/model/Model.java +``` java + /** + * Returns the Transcript. + * + * @return read only version of the transcript + */ + ReadOnlyTranscript getTranscript(); + +``` +###### /java/seedu/address/model/Model.java +``` java + /** + * Returns true if a module with the same identity as {@code module} exists in the transcript. + * + * @param module module to find in the transcript + * @return true if module exists in transcript + */ + boolean hasModule(Module module); + +``` +###### /java/seedu/address/model/Model.java +``` java + /** + * Deletes the given module. + *

+ * The module must exist in the transcript. + * + * @param target module to be deleted from the transcript + */ + void deleteModule(Module target); + +``` +###### /java/seedu/address/model/Model.java +``` java + /** + * Adds the given module. + *

+ * {@code module} must not already exist in the transcript. + * + * @param module module to be added into the transcript + */ + void addModule(Module module); + +``` +###### /java/seedu/address/model/Model.java +``` java + /** + * Replaces the given module {@code target} with {@code editedModule}. + * {@code target} must exist in the transcript. The module identity of {@code editedModule} + * must not be the same as another existing module in the transcript. + * + * @param target module to be updated + * @param editedModule the updated version of the module + */ + void updateModule(Module target, Module editedModule); + +``` +###### /java/seedu/address/model/Model.java +``` java + /** + * Returns an unmodifiable view of the filtered module list. + */ + ObservableList getFilteredModuleList(); + +``` +###### /java/seedu/address/model/Model.java +``` java + /** + * Updates the filter of the filtered module list to filter by the given {@code predicate}. + * @throws NullPointerException if {@code predicate} is null. + */ + void updateFilteredModuleList(Predicate predicate); + +``` +###### /java/seedu/address/model/Model.java +``` java + /** + * Returns true if the model has previous transcript states to restore. + */ + boolean canUndoTranscript(); + +``` +###### /java/seedu/address/model/Model.java +``` java + /** + * Returns true if the model has undone transcript states to restore. + */ + boolean canRedoTranscript(); + +``` +###### /java/seedu/address/model/Model.java +``` java + /** + * Restores the model's transcript to its previous state. + */ + void undoTranscript(); + +``` +###### /java/seedu/address/model/Model.java +``` java + /** + * Restores the model's transcript to its previously undone state. + */ + void redoTranscript(); + +``` +###### /java/seedu/address/model/Model.java +``` java + /** + * Saves the current transcript state for undo/redo. + */ + void commitTranscript(); + + /** + * Get the cap goal of the current transcript + */ + CapGoal getCapGoal(); + + /** + * Set the cap goal of the current transcript + */ + void updateCapGoal(double capGoal); + + /** + * Returns the CAP based on the current Transcript records + */ + double getCap(); + + /** Returns the AddressBook TODO: REMOVE*/ + ReadOnlyAddressBook getAddressBook(); + + /** + * Returns true if a person with the same identity as {@code person} exists in the address book. + * TODO: REMOVE + */ + boolean hasPerson(Person person); + + /** + * Deletes the given person. + * The person must exist in the address book. + * TODO: REMOVE + */ + void deletePerson(Person target); + + /** + * Adds the given person. + * {@code person} must not already exist in the address book. + * TODO: REMOVE + */ + void addPerson(Person person); + + /** + * Replaces the given person {@code target} with {@code editedPerson}. + * {@code target} must exist in the address book. + * The person identity of {@code editedPerson} must not be the same as another existing person + * in the address book. + * TODO: REMOVE + */ + void updatePerson(Person target, Person editedPerson); + + /** Returns an unmodifiable view of the filtered person list TODO: REMOVE */ + ObservableList getFilteredPersonList(); + + /** + * Updates the filter of the filtered person list to filter by the given {@code predicate}. + * @throws NullPointerException if {@code predicate} is null. + * TODO: REMOVE + */ + void updateFilteredPersonList(Predicate predicate); + + /** + * Returns true if the model has previous address book states to restore. + * TODO: REMOVE + */ + boolean canUndoAddressBook(); + + /** + * Returns true if the model has undone address book states to restore. + * TODO: REMOVE + */ + boolean canRedoAddressBook(); + + /** + * Restores the model's address book to its previous state. + * TODO: REMOVE + */ + void undoAddressBook(); + + /** + * Restores the model's address book to its previously undone state. + * TODO: REMOVE + */ + void redoAddressBook(); + + /** + * Saves the current address book state for undo/redo. + * TODO: REMOVE + */ + void commitAddressBook(); +} +``` +###### /java/seedu/address/model/ReadOnlyTranscript.java +``` java +/** + * Unmodifiable view of a Transcript. + */ +@JsonDeserialize(using = JsonTranscriptDeserializer.class) +public interface ReadOnlyTranscript { + /** + * Returns an unmodifiable view of the module list. + * This list will not contain any duplicate modules. + */ + ObservableList getModuleList(); +} +``` +###### /java/seedu/address/model/ModelManager.java +``` java + private final VersionedTranscript versionedTranscript; +``` +###### /java/seedu/address/model/ModelManager.java +``` java + private final FilteredList filteredModules; + +``` +###### /java/seedu/address/model/ModelManager.java +``` java + /** + * Initializes a ModelManager with the given transcript and userPrefs. + */ + public ModelManager(ReadOnlyTranscript transcript, UserPrefs userPrefs) { + super(); + requireAllNonNull(transcript, userPrefs); + + logger.fine("Initializing with transcript: " + transcript + " and user prefs " + userPrefs); + + versionedTranscript = new VersionedTranscript(transcript); + filteredModules = new FilteredList<>(versionedTranscript.getModuleList()); + + //TODO: REMOVE + versionedAddressBook = new VersionedAddressBook(new AddressBook()); + filteredPersons = new FilteredList<>(versionedAddressBook.getPersonList()); + } + + public ModelManager() { + this(new Transcript(), new UserPrefs()); + } + +``` +###### /java/seedu/address/model/ModelManager.java +``` java + /** + * Initializes a ModelManager with the given addressBook and userPrefs. + * TODO: REMOVE + */ + public ModelManager(ReadOnlyAddressBook addressBook, UserPrefs userPrefs) { + super(); + requireAllNonNull(addressBook, userPrefs); + + logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); + + versionedAddressBook = new VersionedAddressBook(addressBook); + filteredPersons = new FilteredList<>(versionedAddressBook.getPersonList()); + versionedTranscript = new VersionedTranscript(new Transcript()); + filteredModules = new FilteredList<>(versionedTranscript.getModuleList()); + } + + @Override + public void resetData(ReadOnlyTranscript newData) { + versionedTranscript.resetData(newData); + indicateTranscriptChanged(); + } + + //TODO: REMOVE + @Override + public void resetData(ReadOnlyAddressBook newData) { + versionedAddressBook.resetData(newData); + indicateAddressBookChanged(); + } + +``` +###### /java/seedu/address/model/ModelManager.java +``` java + @Override + public ReadOnlyTranscript getTranscript() { + return versionedTranscript; + } + +``` +###### /java/seedu/address/model/ModelManager.java +``` java + /** Raises an event to indicate the model has changed */ + private void indicateTranscriptChanged() { + raise(new TranscriptChangedEvent(versionedTranscript)); + } + +``` +###### /java/seedu/address/model/ModelManager.java +``` java + @Override + public boolean hasModule(Module module) { + requireNonNull(module); + return versionedTranscript.hasModule(module); + } + +``` +###### /java/seedu/address/model/ModelManager.java +``` java + @Override + public void deleteModule(Module target) { + versionedTranscript.removeModule(target); + indicateTranscriptChanged(); + } + +``` +###### /java/seedu/address/model/ModelManager.java +``` java + @Override + public void addModule(Module module) { + versionedTranscript.addModule(module); + updateFilteredModuleList(PREDICATE_SHOW_ALL_MODULES); + indicateTranscriptChanged(); + } + +``` +###### /java/seedu/address/model/ModelManager.java +``` java + @Override + public void updateModule(Module target, Module editedModule) { + requireAllNonNull(target, editedModule); + + versionedTranscript.updateModule(target, editedModule); + indicateTranscriptChanged(); + } + + //=========== Filtered Module List Accessors ============================================================= + +``` +###### /java/seedu/address/model/ModelManager.java +``` java + /** + * Returns an unmodifiable view of the list of {@code Module} backed by the internal list of + * {@code versionedTranscript} + */ + @Override + public ObservableList getFilteredModuleList() { + return FXCollections.unmodifiableObservableList(filteredModules); + } + +``` +###### /java/seedu/address/model/ModelManager.java +``` java + @Override + public void updateFilteredModuleList(Predicate predicate) { + requireNonNull(predicate); + filteredModules.setPredicate(predicate); + } + + //=========== Undo/Redo ================================================================================= + +``` +###### /java/seedu/address/model/ModelManager.java +``` java + @Override + public boolean canUndoTranscript() { + return versionedTranscript.canUndo(); + } + +``` +###### /java/seedu/address/model/ModelManager.java +``` java + @Override + public boolean canRedoTranscript() { + return versionedTranscript.canRedo(); + } + +``` +###### /java/seedu/address/model/ModelManager.java +``` java + @Override + public void undoTranscript() { + versionedTranscript.undo(); + indicateTranscriptChanged(); + } + +``` +###### /java/seedu/address/model/ModelManager.java +``` java + @Override + public void redoTranscript() { + versionedTranscript.redo(); + indicateTranscriptChanged(); + } + +``` +###### /java/seedu/address/model/ModelManager.java +``` java + @Override + public void commitTranscript() { + versionedTranscript.commit(); + } + +``` +###### /java/seedu/address/model/ModelManager.java +``` java + @Override + public boolean equals(Object obj) { + // short circuit if same object + if (obj == this) { + return true; + } + + // instanceof handles nulls + if (!(obj instanceof ModelManager)) { + return false; + } + + // state check + ModelManager other = (ModelManager) obj; + return versionedAddressBook.equals(other.versionedAddressBook) + && filteredPersons.equals(other.filteredPersons) // TODO: REMOVE + && filteredModules.equals(other.filteredModules); + } +} +``` +###### /java/seedu/address/model/VersionedTranscript.java +``` java +/** + * {@code Transcript} that keeps track of its own history. + */ +public class VersionedTranscript extends Transcript { + + private final List transcriptStateList; + private int currentStatePointer; + + public VersionedTranscript(ReadOnlyTranscript initialState) { + super(initialState); + + transcriptStateList = new ArrayList<>(); + transcriptStateList.add(new Transcript(initialState)); + currentStatePointer = 0; + } + + /** + * Saves a copy of the current {@code Transcript} state at the end of the state list. + * Undone states are removed from the state list. + */ + public void commit() { + removeStatesAfterCurrentPointer(); + transcriptStateList.add(new Transcript(this)); + currentStatePointer++; + } + + private void removeStatesAfterCurrentPointer() { + transcriptStateList.subList(currentStatePointer + 1, transcriptStateList.size()).clear(); + } + + /** + * Restores the transcript to its previous state. + */ + public void undo() { + if (!canUndo()) { + throw new NoUndoableStateException(); + } + currentStatePointer--; + resetData(transcriptStateList.get(currentStatePointer)); + } + + /** + * Restores the transcript to its previously undone state. + */ + public void redo() { + if (!canRedo()) { + throw new NoRedoableStateException(); + } + currentStatePointer++; + resetData(transcriptStateList.get(currentStatePointer)); + } + + /** + * Returns true if {@code undo()} has transcript states to undo. + */ + public boolean canUndo() { + return currentStatePointer > 0; + } + + /** + * Returns true if {@code redo()} has transcript states to redo. + */ + public boolean canRedo() { + return currentStatePointer < transcriptStateList.size() - 1; + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof VersionedTranscript)) { + return false; + } + + VersionedTranscript otherVersionedTranscript = (VersionedTranscript) other; + + // state check + return super.equals(otherVersionedTranscript) + && transcriptStateList.equals(otherVersionedTranscript.transcriptStateList) + && currentStatePointer == otherVersionedTranscript.currentStatePointer; + } + + /** + * Thrown when trying to {@code undo()} but can't. + */ + public static class NoUndoableStateException extends RuntimeException { + private NoUndoableStateException() { + super("Current state pointer at start of transcriptState list, unable to undo."); + } + } + + /** + * Thrown when trying to {@code redo()} but can't. + */ + public static class NoRedoableStateException extends RuntimeException { + private NoRedoableStateException() { + super("Current state pointer at end of transcriptState list, unable to redo."); + } + } +} +``` +###### /java/seedu/address/model/util/ModuleBuilder.java +``` java +/** + * A utility class to help with building Module objects. + */ +public class ModuleBuilder { + + public static final String DEFAULT_CODE = "CS2103"; + public static final int DEFAULT_YEAR = 1; + public static final String DEFAULT_SEMESTER = Semester.SEMESTER_ONE; + public static final int DEFAULT_CREDIT = 4; + public static final String DEFAULT_GRADE = "A+"; + public static final boolean DEFAULT_COMPLETED = true; + + private Code code; + private Year year; + private Semester semester; + private Credit credit; + private Grade grade; + private boolean completed; + + public ModuleBuilder() { + code = new Code(DEFAULT_CODE); + year = new Year(DEFAULT_YEAR); + semester = new Semester(DEFAULT_SEMESTER); + credit = new Credit(DEFAULT_CREDIT); + grade = new Grade(DEFAULT_GRADE); + completed = DEFAULT_COMPLETED; + } + + /** + * Initializes the ModuleBuilder with the data of {@code personToCopy}. + */ + public ModuleBuilder(Module moduleToCopy) { + code = moduleToCopy.getCode(); + year = moduleToCopy.getYear(); + semester = moduleToCopy.getSemester(); + credit = moduleToCopy.getCredits(); + grade = moduleToCopy.getGrade(); + completed = moduleToCopy.hasCompleted(); + } + + /** + * Sets the {@code Code} of the {@code Module} that we are building. + */ + public ModuleBuilder withCode(String code) { + this.code = new Code(code); + return this; + } + + /** + * Sets the {@code Year} of the {@code Module} that we are building. + */ + public ModuleBuilder withYear(int year) { + this.year = new Year(year); + return this; + } + + /** + * Sets the {@code Semester} of the {@code Module} that we are building. + */ + public ModuleBuilder withSemester(String semester) { + this.semester = new Semester(semester); + return this; + } + + /** + * Sets the {@code Credit} of the {@code Module} that we are building. + */ + public ModuleBuilder withCredit(int credit) { + this.credit = new Credit(credit); + return this; + } + + /** + * Sets the {@code Grade} of the {@code Module} that we are building. + */ + public ModuleBuilder withGrade(String grade) { + this.grade = new Grade(grade); + return this; + } + + /** + * Sets the {@code Grade} of the {@code Module} that we are building to null. + */ + public ModuleBuilder noGrade() { + this.grade = null; + return this; + } + + /** + * Sets the {@code completed} of the {@code Module} that we are building. + */ + public ModuleBuilder withCompleted(boolean completed) { + this.completed = completed; + return this; + } + + public Module build() { + return new Module(code, year, semester, credit, grade, completed); + } +} +``` +###### /java/seedu/address/model/Transcript.java +``` java +/** + * Wraps all data at the transcript level + * Duplicates are not allowed (by .isSameModule comparison) + */ +public class Transcript implements ReadOnlyTranscript { + + private final UniqueModuleList modules; + private CapGoal capGoal; + + /* + * The 'unusual' code block below is an non-static initialization block, sometimes used to avoid duplication + * between constructors. See https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html + * + * Note that non-static init blocks are not recommended to use. There are other ways to avoid duplication + * among constructors. + */ + + { + modules = new UniqueModuleList(); + } + + public Transcript() { + capGoal = new CapGoal(); + } + + /** + * Creates an Transcript using the Modules in the {@code toBeCopied} + */ + public Transcript(ReadOnlyTranscript toBeCopied) { + this(); + resetData(toBeCopied); + } + + //// list overwrite operations + + /** + * Replaces the contents of the module list with {@code modules}. + * {@code modules} must not contain duplicate modules. + */ + public void setModules(List modules) { + this.modules.setModules(modules); + } + + /** + * Resets the existing data of this {@code Transcript} with {@code newData}. + */ + public void resetData(ReadOnlyTranscript newData) { + requireNonNull(newData); + + setModules(newData.getModuleList()); + } + + //// module-level operations + + /** + * Returns true if a module with the same identity as {@code module} exists in the transcript. + */ + public boolean hasModule(Module module) { + requireNonNull(module); + return modules.contains(module); + } + + /** + * Adds a module to the transcript. + * The module must not already exist in the transcript. + */ + public void addModule(Module p) { + modules.add(p); + } + + /** + * Replaces the given module {@code target} in the list with {@code editedModule}. + * {@code target} must exist in the transcript. + * The module identity of {@code editedModule} must not be the same as another existing module in the transcript. + */ + public void updateModule(Module target, Module editedModule) { + requireNonNull(editedModule); + + modules.setModule(target, editedModule); + } + + /** + * Removes {@code key} from this {@code Transcript}. + * {@code key} must exist in the transcript. + */ + public void removeModule(Module key) { + modules.remove(key); + } + +``` +###### /java/seedu/address/model/module/Year.java +``` java +/** + * Represents a Module's year in the transcript. + *

+ * Guarantees: immutable; is valid as declared in {@link #isValidYear(int)} + */ +public class Year { + + public static final String MESSAGE_YEAR_CONSTRAINTS = + "Year must be [1-5]. Example: 1 represents Year 1"; + + /** + * No whitespace allowed. + */ + public static final String YEAR_VALIDATION_REGEX = "[1-5]"; + + /** + * Immutable year value. + */ + public final int value; + + /** + * Constructs an {@code Year}. + * + * @param year A valid year. + */ + public Year(int year) { + checkArgument(isValidYear(year), MESSAGE_YEAR_CONSTRAINTS); + value = year; + } + + /** + * Constructs an {@code Year}. + * + * @param year A valid year. + */ + public Year(String year) { + requireNonNull(year); + checkArgument(isValidYear(year), MESSAGE_YEAR_CONSTRAINTS); + value = Integer.valueOf(year); + } + + /** + * Returns true if a given string is a valid year. + * + * @param year string to be tested for validity + * @return true if given string is a valid year + */ + public static boolean isValidYear(int year) { + return isValidYear(Integer.toString(year)); + } + + /** + * Returns true if a given string is a valid year. + * + * @param year string to be tested for validity + * @return true if given string is a valid year + */ + public static boolean isValidYear(String year) { + return year.matches(YEAR_VALIDATION_REGEX); + } + + /** + * Returns the year the module was taken. + * + * @return year + */ + @Override + public String toString() { + return Integer.toString(value); + } + + /** + * Compares the year value of both Year object. + *

+ * This defines a notion of equality between two Year objects. + * + * @param other Year object compared against this object + * @return true if both are the same object or contains the same value + */ + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof Year + && value == ((Year) other).value); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } +} +``` +###### /java/seedu/address/model/module/Code.java +``` java +/** + * Represents a Module's code in the transcript. + *

+ * Guarantees: immutable; is valid as declared in {@link #isValidCode(String)} + */ +public class Code { + + /** + * Describes the requirements for code value. + */ + public static final String MESSAGE_CODE_CONSTRAINTS = + "Code can take any values except whitespaces"; + + /** + * No whitespace allowed. + */ + public static final String CODE_VALIDATION_REGEX = "^[^\\s]+$"; + + /** + * Immutable code value. + */ + public final String value; + + /** + * Constructs an {@code Code}. + * + * @param code A valid code. + */ + public Code(String code) { + requireNonNull(code); + checkArgument(isValidCode(code), MESSAGE_CODE_CONSTRAINTS); + value = code; + } + + /** + * Returns true if a given string is a valid code. + * + * @param code string to be tested for validity + * @return true if given string is a valid code + */ + public static boolean isValidCode(String code) { + return code.matches(CODE_VALIDATION_REGEX); + } + + /** + * Returns the module code. + * + * @return module code + */ + @Override + public String toString() { + return value; + } + + /** + * Compares the module code value of both Code object. + *

+ * This defines a notion of equality between two code objects. + * + * @param other Code object compared against this object + * @return true if both are the same object or contains the same value + */ + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof Code + && value.equals(((Code) other).value)); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} +``` +###### /java/seedu/address/model/module/Module.java +``` java + /** + * Constant for completed. + */ + public static final boolean MODULE_COMPLETED = true; + +``` +###### /java/seedu/address/model/module/Module.java +``` java + /** + * Constant for not completed. + */ + public static final boolean MODULE_NOT_COMPLETED = false; + +``` +###### /java/seedu/address/model/module/Module.java +``` java + /** + * Code for the module. + */ + private final Code code; + +``` +###### /java/seedu/address/model/module/Module.java +``` java + /** + * Year the module was taken. + */ + private final Year year; + +``` +###### /java/seedu/address/model/module/Module.java +``` java + /** + * Semester the module was taken. + */ + private final Semester semester; + +``` +###### /java/seedu/address/model/module/Module.java +``` java + /** + * Module credits awarded for completion this module. + */ + private final Credit credits; + +``` +###### /java/seedu/address/model/module/Module.java +``` java + /** + * Module grade awarded for completion this module. + */ + private final Grade grade; + + /** + * True if module has been completed. False if module has not been taken yet. + */ + private final boolean completed; + +``` +###### /java/seedu/address/model/module/Module.java +``` java + public Module(Code code, Year year, Semester semester, Credit credit, Grade grade, + boolean completed) { + requireNonNull(code); + requireNonNull(year); + requireNonNull(semester); + requireNonNull(credit); + + this.code = code; + this.year = year; + this.semester = semester; + this.credits = credit; + this.grade = grade; + this.completed = completed; + } + +``` +###### /java/seedu/address/model/module/Module.java +``` java + /** + * Returns the module code. + * + * @return module code + */ + public Code getCode() { + return code; + } + +``` +###### /java/seedu/address/model/module/Module.java +``` java + /** + * Returns the module credits awarded. + * + * @return module credits + */ + public Credit getCredits() { + return credits; + } + +``` +###### /java/seedu/address/model/module/Module.java +``` java + /** + * Returns the year in which the module was taken. + * + * @return year in which module was taken + */ + public Year getYear() { + return year; + } + +``` +###### /java/seedu/address/model/module/Module.java +``` java + /** + * Returns the semester in which the module was taken. + * + * @return semester in which module was taken + */ + public Semester getSemester() { + return semester; + } + +``` +###### /java/seedu/address/model/module/Module.java +``` java + /** + * Returns the module grade awarded. + * + * @return module grade + */ + public Grade getGrade() { + return grade; + } + +``` +###### /java/seedu/address/model/module/Module.java +``` java + /** + * Returns true if module has been completed and false if module has not been taken. + * + * @return true if module has been completed and false if module has not been taken + */ + public boolean hasCompleted() { + return completed; + } + +``` +###### /java/seedu/address/model/module/Module.java +``` java + /** + * Returns true if module code is the same. + * + * @return true if modue code is the same + */ + public boolean isSameModule(Module otherModule) { + if (otherModule == this) { + return true; + } + + return otherModule != null && otherModule.getCode().equals(getCode()); + } + +``` +###### /java/seedu/address/model/module/Module.java +``` java + /** + * Returns true if both modules are of the same object or contains the same set of data fields. + *

+ * This defines a notion of equality between two modules. + * + * @param other other module to be compared with this Module object + * @return true if both objects contains the same data fields + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Module)) { + return false; + } + + Module otherModule = (Module) other; + + if (grade == null && otherModule.grade != null) { + return false; + } + + if (grade != null && otherModule.grade == null) { + return false; + } + + if (grade == null) { + return otherModule.getCode().equals(getCode()) + && otherModule.getYear().equals(getYear()) + && otherModule.getSemester().equals(getSemester()) + && otherModule.getCredits().equals(getCredits()) + && otherModule.hasCompleted() == hasCompleted(); + } + + + return otherModule.getCode().equals(getCode()) + && otherModule.getYear().equals(getYear()) + && otherModule.getSemester().equals(getSemester()) + && otherModule.getCredits().equals(getCredits()) + && otherModule.getGrade().equals(getGrade()) + && otherModule.hasCompleted() == hasCompleted(); + } + +``` +###### /java/seedu/address/model/module/Module.java +``` java + /** + * Returns the code, year, semester, credits, grade, is module completed. + *

+ * Format: Code: CODE Year: YEAR Semester: SEMESTER Credits: CREDITS Grade: GRADE Completed: + * COMPLETED + * + * @return information of this module + */ + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + + return builder.append("Code: ") + .append(getCode()) + .append(" Year: ") + .append(getYear()) + .append(" Semester: ") + .append(getSemester()) + .append(" Credits: ") + .append(getCredits()) + .append(" Grade: ") + .append(getGrade()) + .append(" Completed: ") + .append(hasCompleted()) + .toString(); + } + +``` +###### /java/seedu/address/model/module/Module.java +``` java + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(code, year, semester, credits, grade, completed); + } +} +``` +###### /java/seedu/address/model/module/UniqueModuleList.java +``` java +/** + * A list of modules that enforces uniqueness between its elements and does not allow nulls. + *

+ * A module is considered unique by comparing {@code moduleA.equals(moduleB)}. + *

+ * As such, adding and updating of modules uses {@code moduleA.equals(moduleB)} for equality so as + * to ensure that the module being added or updated is unique in terms of identity in the + * UniqueModuleList. + */ +public class UniqueModuleList implements Iterable { + + /** + * Creates an observable list of module. + * See {@link Module}. + */ + private final ObservableList internalList = FXCollections.observableArrayList(); + + /** + * Returns true if the list contains an equivalent module as the given argument. + * See {@link Module}. + * + * @param toCheck the module that is being checked against + * @return true if list contains equivalent module + */ + public boolean contains(Module toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSameModule); + } + + /** + * Adds a module to the list. + *

+ * The {@link Module} must not have already exist in the list. + * + * @param toAdd the module that would be added into the list + */ + public void add(Module toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateModuleException(); + } + internalList.add(toAdd); + } + + /** + * Replaces the module {@code target} in the list with {@code editedModule}. + *

+ * {@code target} must exist in the list. The {@link Module} identity of {@code editedModule} + * must not be the same as another existing module in the list. + * + * @param target the module to be replaced + * @param editedModule the modue that replaces the old module + */ + public void setModule(Module target, Module editedModule) { + requireAllNonNull(target, editedModule); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new ModuleNotFoundException(); + } + + if (!target.equals(editedModule) && contains(editedModule)) { + throw new DuplicateModuleException(); + } + + internalList.set(index, editedModule); + } + + /** + * Replaces the {@link #internalList} of this UniqueModuleList with the internalList of the + * replacement. + * + * @param replacement the UniqueModuleList object that contains the internalList that is + * replacing the old internalList + */ + public void setModules(UniqueModuleList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code modules}. {@code modules} must not contain + * duplicate modules. + * + * @param modules the list of module that would replace the old list + */ + public void setModules(List modules) { + requireAllNonNull(modules); + if (!modulesAreUnique(modules)) { + throw new DuplicateModuleException(); + } + + internalList.setAll(modules); + } + + /** + * Removes the equivalent module from the list. + *

+ * The {@link Module} must exist in the list. + * + * @param toRemove the module to be removed from the list + */ + public void remove(Module toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new ModuleNotFoundException(); + } + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + * + * @return backing list as an unmodifiable {@code ObservableList} + */ + public ObservableList asUnmodifiableObservableList() { + return FXCollections.unmodifiableObservableList(internalList); + } + + /** + * Returns true if {@code modules} contains only unique modules. + * + * @param modules the module list that is being checked + * @return true if modules are unique and false if modules are not unique + */ + private boolean modulesAreUnique(List modules) { + return modules.size() == modules.parallelStream() + .distinct() + .count(); + } + +``` +###### /java/seedu/address/model/module/Grade.java +``` java + /** + * Describes the requirements for grade value. + */ + public static final String MESSAGE_GRADE_CONSTRAINTS = + "Grade can be A+, A, A-, B+, B, B-, C+, C, D+, D, F, CS, CU"; + + public static final String MESSAGE_POINT_CONSTRAINTS = + "Score must be between [0, 5] with increments of 0.5 and not 0.5"; + +``` +###### /java/seedu/address/model/module/Grade.java +``` java + /** + * No whitespace allowed. + */ + public static final String GRADE_VALIDATION_REGEX = + "A\\+|A\\-|A|B\\+|B\\-|B|C\\+|C|D\\+|D|F|CS|CU"; + + /** + * Static Unchangeable Mapping between Grade and Point + */ + private static final Map MAP_GRADE_POINT; + private static final Map MAP_POINT_GRADE; + static { + Map tempGradePointMap = new HashMap<>(); + Map tempPointGradeMap = new HashMap<>(); + tempGradePointMap.put("A+", 5.0); + tempGradePointMap.put("A", 5.0); + tempGradePointMap.put("A-", 4.5); + tempGradePointMap.put("B+", 4.0); + tempGradePointMap.put("B", 3.5); + tempGradePointMap.put("B-", 3.0); + tempGradePointMap.put("C+", 2.5); + tempGradePointMap.put("C", 2.0); + tempGradePointMap.put("D+", 1.5); + tempGradePointMap.put("D", 1.0); + tempGradePointMap.put("F", 0.0); + + for (Map.Entry entry : tempGradePointMap.entrySet()) { + tempPointGradeMap.put(entry.getValue(), entry.getKey()); + } + tempPointGradeMap.put(5.0, "A"); + + MAP_GRADE_POINT = Collections.unmodifiableMap(tempGradePointMap); + MAP_POINT_GRADE = Collections.unmodifiableMap(tempPointGradeMap); + } + +``` +###### /java/seedu/address/model/module/Grade.java +``` java + /** + * Immutable grade value. + */ + public final String value; + +``` +###### /java/seedu/address/model/module/Grade.java +``` java + /** + * Constructs an {@code Grade}. + * + * @param grade A valid grade. + */ + public Grade(String grade) { + requireNonNull(grade); + checkArgument(isValidGrade(grade), MESSAGE_GRADE_CONSTRAINTS); + value = grade; + } + + /** + * Constructs an {@code Grade} from point + * @param point + */ + public Grade(double point) { + requireNonNull(point); + checkArgument(isValidPoint(point), MESSAGE_POINT_CONSTRAINTS); + value = mapPointToValue(point); + } + + /** + * Returns true if point is within [0, 5] and step by 0.5 and not 0.5 + * @param point + * @return + */ + public static boolean isValidPoint(double point) { + double fraction = point - Math.floor(point); + return point >= 0 && point <= 5 && (fraction == 0 || fraction == 0.5) && point != 0.5; + } + + /** + * Returns the letter grade the point should be mapped to. + * @param point + * @return + */ + private String mapPointToValue(double point) { + return MAP_POINT_GRADE.get(point); + } + +``` +###### /java/seedu/address/model/module/Grade.java +``` java + /** + * Returns true if a given string is a valid grade. + * + * @param grade string to be tested for validity + * @return true if given string is a valid grade + */ + public static boolean isValidGrade(String grade) { + return grade.matches(GRADE_VALIDATION_REGEX); + } + + /** + * Returns true if grade affects cap and false if grade does not affect cap. + * + * @return true if grade affects cap and false if grade does not affect cap. + */ + public boolean affectsCap() { + return !value.contentEquals("CS") && !value.contentEquals("CU"); + } + + /** + * Returns the point equivalent of the grade or 0 if grade is invalid. + * + * @return point equivalent of the grade + */ + public float getPoint() { + if (MAP_GRADE_POINT.containsKey(value)) { + return MAP_GRADE_POINT.get(value).floatValue(); + } + return 0; + } + +``` +###### /java/seedu/address/model/module/Grade.java +``` java + /** + * Returns the grade value. + * + * @return grade + */ + @Override + public String toString() { + return value; + } + +``` +###### /java/seedu/address/model/module/Grade.java +``` java + /** + * Compares the grade value of both Grade object. + *

+ * This defines a notion of equality between two grade objects. + * + * @param other Grade object compared against this object + * @return true if both are the same object or contains the same value + */ + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof Grade + && value.equals(((Grade) other).value)); + } + +``` +###### /java/seedu/address/model/module/Grade.java +``` java + @Override + public int hashCode() { + return value.hashCode(); + } +} +``` +###### /java/seedu/address/model/module/Credit.java +``` java +/** + * Represents a Module's credits in the transcript. + *

+ * Guarantees: immutable; is valid as declared in {@link #isValidCredit(int)} + */ +public class Credit { + + /** + * Describes the requirements for credit value. + */ + public static final String MESSAGE_CREDIT_CONSTRAINTS = "Credits must be a integer"; + + /** + * Immutable credit value. + */ + public final int value; + + /** + * Constructs an {@code Credit}. + * + * @param credits A valid credit. + */ + public Credit(int credits) { + requireNonNull(credits); + checkArgument(isValidCredit(credits), MESSAGE_CREDIT_CONSTRAINTS); + value = credits; + } + + /** + * Returns true if a given string is a valid credit. + *

+ * Credit must be between 1 and 20 + * + * @param credits string to be tested for validity + * @return true if given string is a valid credit + */ + public static boolean isValidCredit(int credits) { + if (credits < 1) { + return false; + } else if (credits > 20) { + return false; + } + + return true; + } + + /** + * Returns the module credits value. + * + * @return module credits + */ + @Override + public String toString() { + return Integer.toString(value); + } + + /** + * Compares the module credit value of both Credit object. + *

+ * This defines a notion of equality between two credit objects. + * + * @param other Credit object compared against this object + * @return true if both are the same object or contains the same value + */ + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof Credit + && value == ((Credit) other).value); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } +} +``` +###### /java/seedu/address/model/module/Semester.java +``` java +/** + * Represents a Module's semester in the transcript. + *

+ * Guarantees: immutable; is valid as declared in {@link #isValidSemester(String)} + *

+ * Legal values: 1, 2, s1, s2. 1 + *

+ * (Semester 1), 2 (Semester 2), s1 (Special Semester 1), s2 (Special Semester 2). + */ +public class Semester { + + /** + * Describes the requirements for semester value. + */ + public static final String MESSAGE_SEMESTER_CONSTRAINTS = "Semester can be 1, 2, s1 or s2"; + + /** + * No whitespace allowed. + */ + public static final String SEMESTER_VALIDATION_REGEX = "1|2|s1|s2"; + + /** + * Constant for semester one. + */ + public static final String SEMESTER_ONE = "1"; + + /** + * Constant for semester two. + */ + public static final String SEMESTER_TWO = "2"; + + /** + * Constant for special semester one. + */ + public static final String SEMESTER_SPECIAL_ONE = "s1"; + + /** + * Constant for special semester two. + */ + public static final String SEMESTER_SPECIAL_TWO = "s2"; + + /** + * Immutable semester value. + */ + public final String value; + + /** + * Constructs an {@code Code}. + * + * @param semester A valid semester. + */ + public Semester(String semester) { + requireNonNull(semester); + checkArgument(isValidSemester(semester), MESSAGE_SEMESTER_CONSTRAINTS); + value = semester; + } + + /** + * Returns true if a given string is a valid semester. + * + * @param semester string to be tested for validity + * @return true if given string is a valid semester + */ + public static boolean isValidSemester(String semester) { + return semester.matches(SEMESTER_VALIDATION_REGEX); + } + + /** + * Returns the semester value. + * + * @return grade + */ + @Override + public String toString() { + return value; + } + + /** + * Compares the semester value of both Semester object. + *

+ * This defines a notion of equality between two semester objects. + * + * @param other Semester object compared against this object + * @return true if both are the same object or contains the same value + */ + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof Semester + && value.equals(((Semester) other).value)); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} +``` diff --git a/collated/functional/jeremiah-ang.md b/collated/functional/jeremiah-ang.md new file mode 100644 index 000000000000..5c0b11c36bcf --- /dev/null +++ b/collated/functional/jeremiah-ang.md @@ -0,0 +1,419 @@ +# jeremiah-ang +###### /java/seedu/address/logic/commands/GoalCommand.java +``` java +/** + * Sets CAP Goal + */ +public class GoalCommand extends Command { + + public static final String COMMAND_WORD = "goal"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Set your CAP goal. " + + "Parameters: " + + "CAP_GOAL " + + "Example: " + COMMAND_WORD + " " + + "4.5"; + + public static final String MESSAGE_SUCCESS = "Your CAP Goal: %1$s"; + + private final double goal; + + /** + * Creates an GoalCommand to set the CAP Goal + */ + public GoalCommand(double goal) { + this.goal = goal; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + model.updateCapGoal(goal); + CapGoal capGoal = model.getCapGoal(); + return new CommandResult(String.format(MESSAGE_SUCCESS, capGoal.toString())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof GoalCommand // instanceof handles nulls + && goal == ((GoalCommand) other).goal); // state check + } +} +``` +###### /java/seedu/address/logic/commands/CapCommand.java +``` java +/** + * Shows CAP based on existing modules. + */ +public class CapCommand extends Command { + public static final String COMMAND_WORD = "cap"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Calculate current CAP with given modules " + + "Parameters: NONE " + + "Example: " + COMMAND_WORD; + public static final String MESSAGE_SUCCESS = "Your Current CAP is: %1$s"; + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + double cap = model.getCap(); + return new CommandResult(String.format(MESSAGE_SUCCESS, cap)); + } +} +``` +###### /java/seedu/address/logic/parser/GoalCommandParser.java +``` java +/** + * Parses User Input + */ +public class GoalCommandParser implements Parser { + @Override + public GoalCommand parse(String userInput) throws ParseException { + final String trimmedArgs = userInput.trim(); + final String format = String.format(MESSAGE_INVALID_COMMAND_FORMAT, GoalCommand.MESSAGE_USAGE); + if (trimmedArgs.isEmpty()) { + throw new ParseException(format); + } + + try { + double newGoal = Double.parseDouble(trimmedArgs); + if (newGoal < 0 || newGoal > 5) { + throw new ParseException(format); + } + return new GoalCommand(newGoal); + } catch (NumberFormatException nfe) { + throw new ParseException(format); + } + } +} +``` +###### /java/seedu/address/model/ModelManager.java +``` java + @Override + public CapGoal getCapGoal() { + return versionedTranscript.getCapGoal(); + } + +``` +###### /java/seedu/address/model/ModelManager.java +``` java + @Override + public void updateCapGoal(double capGoal) { + versionedTranscript.setCapGoal(capGoal); + indicateTranscriptChanged(); + } + + //TODO: REMOVE + @Override + public ReadOnlyAddressBook getAddressBook() { + return versionedAddressBook; + } + + //TODO: REMOVE + /** Raises an event to indicate the model has changed */ + private void indicateAddressBookChanged() { + raise(new AddressBookChangedEvent(versionedAddressBook)); + } + + //TODO: REMOVE + @Override + public boolean hasPerson(Person person) { + requireNonNull(person); + return versionedAddressBook.hasPerson(person); + } + + //TODO: REMOVE + @Override + public void deletePerson(Person target) { + versionedAddressBook.removePerson(target); + indicateAddressBookChanged(); + } + + //TODO: REMOVE + @Override + public void addPerson(Person person) { + versionedAddressBook.addPerson(person); + updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + indicateAddressBookChanged(); + } + + //TODO: REMOVE + @Override + public void updatePerson(Person target, Person editedPerson) { + requireAllNonNull(target, editedPerson); + + versionedAddressBook.updatePerson(target, editedPerson); + indicateAddressBookChanged(); + } + + //=========== Filtered Person List Accessors ============================================================= + + /** + * Returns an unmodifiable view of the list of {@code Person} backed by the internal list of + * {@code versionedAddressBook} + * TODO: REMOVE + */ + @Override + public ObservableList getFilteredPersonList() { + return FXCollections.unmodifiableObservableList(filteredPersons); + } + + //TODO: REMOVE + @Override + public void updateFilteredPersonList(Predicate predicate) { + requireNonNull(predicate); + filteredPersons.setPredicate(predicate); + } + + //=========== Undo/Redo ================================================================================= + + //TODO: REMOVE + @Override + public boolean canUndoAddressBook() { + return versionedAddressBook.canUndo(); + } + + //TODO: REMOVE + @Override + public boolean canRedoAddressBook() { + return versionedAddressBook.canRedo(); + } + + //TODO: REMOVE + @Override + public void undoAddressBook() { + versionedAddressBook.undo(); + indicateAddressBookChanged(); + } + + //TODO: REMOVE + @Override + public void redoAddressBook() { + versionedAddressBook.redo(); + indicateAddressBookChanged(); + } + + //TODO: REMOVE + @Override + public void commitAddressBook() { + versionedAddressBook.commit(); + } + +``` +###### /java/seedu/address/model/ModelManager.java +``` java + @Override + public double getCap() { + return versionedTranscript.getCap(); + } + +``` +###### /java/seedu/address/model/Transcript.java +``` java + /** + * Return the current CAP + * + * @return current cap score + */ + public double getCap() { + return calculateCap(); + } + + /** + * Calculate CAP Score based on modules with scores + * + * @return cap: cap score + */ + private double calculateCap() { + + ObservableList gradedModulesList = getGradedModulesList(); + double totalModulePoint = calculateTotalModulePoint(gradedModulesList); + double totalModuleCredit = calculateTotalModuleCredit(gradedModulesList); + + double cap = 0; + if (totalModuleCredit > 0) { + cap = totalModulePoint / totalModuleCredit; + } + + return cap; + } + + /** + * Calculates the total module point from the list of modules + * @param modules + * @return + */ + private double calculateTotalModulePoint(ObservableList modules) { + double totalPoint = 0; + for (Module module : modules) { + totalPoint += module.getGrade().getPoint() * module.getCredits().value; + } + return totalPoint; + } + + /** + * Calculates the total module credit from the list of modules + * @param modules + * @return + */ + private double calculateTotalModuleCredit(ObservableList modules) { + int totalModuleCredit = 0; + for (Module module : modules) { + totalModuleCredit += module.getCredits().value; + } + return totalModuleCredit; + } + + /** + * Filters for modules that is to be used for CAP calculation + * + * @return list of modules used for CAP calculation + */ + private ObservableList getGradedModulesList() { + return modules.getFilteredModules(this::moduleIsUsedForCapCalculation); + } + + /** + * Filters for modules that have yet been graded + * @return gradedModulesList: a list of modules used for CAP calculation + */ + private ObservableList getNotCompletedModulesList() { + return modules.getFilteredModules(module -> !module.hasCompleted()); + } + + /** + * Check if the given module should be considered for CAP Calculation + * + * @param module + * @return true if yes, false otherwise + */ + private boolean moduleIsUsedForCapCalculation(Module module) { + return module.hasCompleted() && moduleAffectsGrade(module); + } + + /** + * Check if a module affects grade + * + * @param module + * @return true if module affects grade, false otheriwse + */ + private boolean moduleAffectsGrade(Module module) { + return module.getGrade().affectsCap(); + } + + /** + * Calculates target module grade in order to achieve target goal + * @return a list of modules with target grade if possible. null otherwise + */ + public ObservableList getTargetModuleGrade() { + ObservableList gradedModules = getGradedModulesList(); + ObservableList ungradedModules = getNotCompletedModulesList() + .sorted(Comparator.comparingInt(o -> o.getCredits().value)); + List targetModules = new ArrayList<>(); + if (ungradedModules.isEmpty()) { + return FXCollections.observableArrayList(targetModules); + } + + double totalUngradedModuleCredit = calculateTotalModuleCredit(ungradedModules); + double totalMc = calculateTotalModuleCredit(gradedModules) + totalUngradedModuleCredit; + double currentTotalPoint = calculateTotalModulePoint(gradedModules); + + double totalScoreToAchieve = capGoal.getCapGoal() * totalMc - currentTotalPoint; + double unitScoreToAchieve = Math.ceil(totalScoreToAchieve / totalUngradedModuleCredit * 2) / 2.0; + if (unitScoreToAchieve > 5) { + return null; + } + + + Module targetModule; + for (Module ungradedModule : ungradedModules) { + if (unitScoreToAchieve == 0.5) { + unitScoreToAchieve = 1.0; + } + targetModule = new Module(ungradedModule, new Grade(unitScoreToAchieve)); + targetModules.add(targetModule); + totalScoreToAchieve -= targetModule.getCredits().value * unitScoreToAchieve; + totalUngradedModuleCredit -= targetModule.getCredits().value; + unitScoreToAchieve = Math.ceil(totalScoreToAchieve / totalUngradedModuleCredit * 2) / 2.0; + } + + return FXCollections.observableArrayList(targetModules); + } + + public CapGoal getCapGoal() { + return capGoal; + } + + public void setCapGoal(double capGoal) { + this.capGoal = new CapGoal(capGoal); + } + +``` +###### /java/seedu/address/model/capgoal/CapGoal.java +``` java +/** + * Represents Cap Goal + * + * Immutable. Value can be null. + */ +public class CapGoal { + + private static final String MESSAGE_IS_NULL = "NIL"; + + private double capGoal; + private boolean isSet = true; + + public CapGoal() { + + } + + public CapGoal(double capGoal) { + isSet = false; + this.capGoal = capGoal; + } + + /** + * Returns the cap goal + * @return + */ + public double getCapGoal() { + return capGoal; + } + + public boolean isSet() { + return isSet; + } + + @Override + public String toString() { + if (isSet) { + return MESSAGE_IS_NULL; + } + return "" + getCapGoal(); + } +} +``` +###### /java/seedu/address/model/module/Module.java +``` java + /** + * Creates a new Module from an existing module but with a different grade + * @param module + * @param grade + */ + public Module(Module module, Grade grade) { + this(module.code, module.year, module.semester, module.credits, grade, module.completed); + } + +``` +###### /java/seedu/address/model/module/UniqueModuleList.java +``` java + /** + * Returns the list of filtered Module based on the given predicate + * + * @param predicate + * @return filtered list + */ + public ObservableList getFilteredModules(Predicate predicate) { + return internalList.filtered(predicate); + } + +``` diff --git a/collated/functional/jeremyyew.md b/collated/functional/jeremyyew.md new file mode 100644 index 000000000000..176791ddf6e0 --- /dev/null +++ b/collated/functional/jeremyyew.md @@ -0,0 +1,50 @@ +# jeremyyew +###### /java/seedu/address/storage/JsonTranscriptStorage.java +``` java +/** + * A class to access Transcript stored in the hard disk as a json file + */ +public class JsonTranscriptStorage implements TranscriptStorage { + + private final Path filePath; + + public JsonTranscriptStorage(Path filePath) { + this.filePath = filePath; + } + + @Override + public Path getTranscriptFilePath() { + return filePath; + } + + @Override + public Optional readTranscript() throws DataConversionException { + return readTranscript(filePath); + } + + /** + * Similar to {@link #readTranscript()} + * + * @param transcriptFilePath location of the data. Cannot be null. + * @throws DataConversionException if the file format is not as expected. + */ + public Optional readTranscript(Path transcriptFilePath) throws DataConversionException { + return JsonUtil.readJsonFile(transcriptFilePath, ReadOnlyTranscript.class); + } + + @Override + public void saveTranscript(ReadOnlyTranscript transcript) throws IOException { + JsonUtil.saveJsonFile(new Transcript(transcript), filePath); + } + + @Override + public void saveTranscript(ReadOnlyTranscript transcript, Path filePath) throws IOException { + requireNonNull(transcript); + requireNonNull(filePath); + + FileUtil.createIfMissing(filePath); + JsonUtil.saveJsonFile(new Transcript(transcript), filePath); + } + +} +``` diff --git a/collated/test/alexkmj.md b/collated/test/alexkmj.md new file mode 100644 index 000000000000..18344ba20901 --- /dev/null +++ b/collated/test/alexkmj.md @@ -0,0 +1,673 @@ +# alexkmj +###### /java/seedu/address/logic/parser/AddModuleCommandParserTest.java +``` java +public class AddModuleCommandParserTest { + private AddModuleCommandParser parser = new AddModuleCommandParser(); + + @Test + public void parseAllFieldsPresentSuccess() throws Exception { + // leading and trailing whitespace + assertParseSuccess(parser, PREAMBLE_WHITESPACE + + " " + DISCRETE_MATH.getCode().value + + " " + DISCRETE_MATH.getYear().value + + " " + DISCRETE_MATH.getSemester().value + + " " + DISCRETE_MATH.getCredits().value + + " " + DISCRETE_MATH.getGrade().value, new AddModuleCommand(DISCRETE_MATH)); + } + + @Test + public void parseOptionalFieldsMissingSuccess() { + Module expectedModule = new ModuleBuilder(DISCRETE_MATH) + .noGrade() + .withCompleted(false) + .build(); + + // no grade + assertParseSuccess(parser, DISCRETE_MATH.getCode().value + + " " + DISCRETE_MATH.getYear().value + + " " + DISCRETE_MATH.getSemester().value + + " " + DISCRETE_MATH.getCredits().value, new AddModuleCommand(expectedModule)); + } +} +``` +###### /java/seedu/address/logic/parser/TranscriptParserTest.java +``` java +public class TranscriptParserTest { + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private final TranscriptParser parser = new TranscriptParser(); + + @Test + public void parseCommandAddModule() throws Exception { + Module module = new ModuleBuilder().build(); + AddModuleCommand command = (AddModuleCommand) parser.parseCommand(ModuleUtil.getAddModuleCommand(module)); + assertEquals(new AddModuleCommand(module), command); + } +} +``` +###### /java/seedu/address/model/module/GradeTest.java +``` java +public class GradeTest { + + @Test + public void constructorNullThrowsNullPointerException() { + Assert.assertThrows(NullPointerException.class, () -> new Grade(null)); + } + + @Test + public void constructorInvalidGradeThrowsIllegalArgumentException() { + String invalidGrade = ""; + Assert.assertThrows(IllegalArgumentException.class, () -> new Grade(invalidGrade)); + } + + @Test + public void isValidGrade() { + // invalid grade format + assertFalse(Grade.isValidGrade(" A+")); // no leading whitespace + assertFalse(Grade.isValidGrade("A+ ")); // no leading whitespace + assertFalse(Grade.isValidGrade("A +")); // no whitespace in between + assertFalse(Grade.isValidGrade("G")); // First character has to be A, B, C, D, F + + // valid grade + assertTrue(Grade.isValidGrade("A+")); + assertTrue(Grade.isValidGrade("A")); + assertTrue(Grade.isValidGrade("A-")); + assertTrue(Grade.isValidGrade("B+")); + assertTrue(Grade.isValidGrade("B")); + assertTrue(Grade.isValidGrade("B-")); + assertTrue(Grade.isValidGrade("C+")); + assertTrue(Grade.isValidGrade("C")); + assertTrue(Grade.isValidGrade("D+")); + assertTrue(Grade.isValidGrade("D")); + assertTrue(Grade.isValidGrade("F")); + assertTrue(Grade.isValidGrade("CU")); + assertTrue(Grade.isValidGrade("CS")); + } + + @Test + public void affectCapValid() { + assertTrue(new Grade("A+").affectsCap()); + assertTrue(new Grade("A").affectsCap()); + assertTrue(new Grade("A-").affectsCap()); + assertTrue(new Grade("B+").affectsCap()); + assertTrue(new Grade("B").affectsCap()); + assertTrue(new Grade("B-").affectsCap()); + assertTrue(new Grade("C+").affectsCap()); + assertTrue(new Grade("C").affectsCap()); + assertTrue(new Grade("D+").affectsCap()); + assertTrue(new Grade("D").affectsCap()); + assertTrue(new Grade("F").affectsCap()); + assertFalse(new Grade("CS").affectsCap()); + assertFalse(new Grade("CU").affectsCap()); + } + + @Test + public void getPointValid() { + assertTrue(new Grade("A+").getPoint() == 5); + assertTrue(new Grade("A").getPoint() == 5); + assertTrue(new Grade("A-").getPoint() == 4.5); + assertTrue(new Grade("B+").getPoint() == 4.0); + assertTrue(new Grade("B").getPoint() == 3.5); + assertTrue(new Grade("B-").getPoint() == 3.0); + assertTrue(new Grade("C+").getPoint() == 2.5); + assertTrue(new Grade("C").getPoint() == 2); + assertTrue(new Grade("D+").getPoint() == 1.5); + assertTrue(new Grade("D").getPoint() == 1); + assertTrue(new Grade("F").getPoint() == 0); + } + + @Test + public void toStringValid() { + assertTrue(new Grade("A+").toString().contentEquals("A+")); + } + + @Test + public void equalsValid() { + assertTrue(new Grade("A+").equals(new Grade("A+"))); + } + + +``` +###### /java/seedu/address/model/module/SemesterTest.java +``` java +public class SemesterTest { + + @Test + public void constructorInvalidYearThrowsIllegalArgumentException() { + Assert.assertThrows(NullPointerException.class, () -> new Semester(null)); + } + + @Test + public void isValidSemester() { + // invalid semester format + assertFalse(Semester.isValidSemester("s3")); + assertFalse(Semester.isValidSemester("3")); + + // valid semester + assertTrue(Semester.isValidSemester(Semester.SEMESTER_ONE)); + assertTrue(Semester.isValidSemester(Semester.SEMESTER_TWO)); + assertTrue(Semester.isValidSemester(Semester.SEMESTER_SPECIAL_ONE)); + assertTrue(Semester.isValidSemester(Semester.SEMESTER_SPECIAL_TWO)); + } + + @Test + public void toStringValid() { + assertTrue(new Semester(Semester.SEMESTER_ONE).toString().contentEquals(Semester.SEMESTER_ONE)); + } + + @Test + public void equalsValid() { + assertTrue(new Semester(Semester.SEMESTER_ONE).equals(new Semester(Semester.SEMESTER_ONE))); + } +} +``` +###### /java/seedu/address/model/module/YearTest.java +``` java +public class YearTest { + + @Test + public void constructorNullThrowsNullPointerException() { + Assert.assertThrows(NullPointerException.class, () -> new Year(null)); + } + + @Test + public void constructorInvalidYearThrowsIllegalArgumentException() { + Assert.assertThrows(IllegalArgumentException.class, () -> new Year(0)); + Assert.assertThrows(IllegalArgumentException.class, () -> new Year("0")); + } + + @Test + public void isValidYear() { + // invalid year format + assertFalse(Year.isValidYear(0)); // year must be at least 1 + assertFalse(Year.isValidYear(6)); // year must be 5 or below + assertFalse(Year.isValidYear(10)); // only 1 digit allowed + + // valid year format + assertTrue(Year.isValidYear(1)); // year 1 + assertTrue(Year.isValidYear(2)); // year 2 + assertTrue(Year.isValidYear(3)); // year 3 + assertTrue(Year.isValidYear(4)); // year 4 + assertTrue(Year.isValidYear(5)); // year 5 + } + + @Test + public void toStringValid() { + assertTrue(new Year(1).toString().contentEquals("1")); + } + + @Test + public void equalsValid() { + assertTrue(new Year(1).equals(new Year(1))); + } + + @Test + public void hashCodeValid() { + assertTrue(new Year(1).hashCode() == "1".hashCode()); + } +} +``` +###### /java/seedu/address/model/module/UniqueModuleListTest.java +``` java +public class UniqueModuleListTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private final UniqueModuleList uniqueModuleList = new UniqueModuleList(); + + @Test + public void containsNullModuleThrowsNullPointerException() { + thrown.expect(NullPointerException.class); + uniqueModuleList.contains(null); + } + + @Test + public void containsModuleNotInListReturnsFalse() { + assertFalse(uniqueModuleList.contains(DATA_STRUCTURES)); + } + + @Test + public void containsModuleInListReturnsTrue() { + uniqueModuleList.add(DATA_STRUCTURES); + assertTrue(uniqueModuleList.contains(DATA_STRUCTURES)); + } + + @Test + public void addNullModuleThrowsNullPointerException() { + thrown.expect(NullPointerException.class); + uniqueModuleList.add(null); + } + + @Test + public void addDuplicateModuleThrowsDuplicateModuleException() { + uniqueModuleList.add(DATA_STRUCTURES); + thrown.expect(DuplicateModuleException.class); + uniqueModuleList.add(DATA_STRUCTURES); + } + + @Test + public void setModuleNullTargetModuleThrowsNullPointerException() { + thrown.expect(NullPointerException.class); + uniqueModuleList.setModule(null, DATA_STRUCTURES); + } + + @Test + public void setModuleNullEditedModuleThrowsNullPointerException() { + thrown.expect(NullPointerException.class); + uniqueModuleList.setModule(DATA_STRUCTURES, null); + } + + @Test + public void setModuleTargetModuleNotInListThrowsModuleNotFoundException() { + thrown.expect(ModuleNotFoundException.class); + uniqueModuleList.setModule(DATA_STRUCTURES, DATA_STRUCTURES); + } + + @Test + public void setModuleEditedModuleIsSameModuleSuccess() { + uniqueModuleList.add(DATA_STRUCTURES); + uniqueModuleList.setModule(DATA_STRUCTURES, DATA_STRUCTURES); + UniqueModuleList expectedUniqueModuleList = new UniqueModuleList(); + expectedUniqueModuleList.add(DATA_STRUCTURES); + assertEquals(expectedUniqueModuleList, uniqueModuleList); + } + + @Test + public void setModuleEditedModuleHasSameIdentitySuccess() { + uniqueModuleList.add(DATA_STRUCTURES); + Module editedDataStructures = new ModuleBuilder(DATA_STRUCTURES) + .withCode(DISCRETE_MATH.getCode().value) + .build(); + uniqueModuleList.setModule(DATA_STRUCTURES, editedDataStructures); + UniqueModuleList expectedUniqueModuleList = new UniqueModuleList(); + expectedUniqueModuleList.add(editedDataStructures); + assertEquals(expectedUniqueModuleList, uniqueModuleList); + } + + @Test + public void setModuleEditedModuleHasDifferentIdentitySuccess() { + uniqueModuleList.add(DATA_STRUCTURES); + uniqueModuleList.setModule(DATA_STRUCTURES, DISCRETE_MATH); + UniqueModuleList expectedUniqueModuleList = new UniqueModuleList(); + expectedUniqueModuleList.add(DISCRETE_MATH); + assertEquals(expectedUniqueModuleList, uniqueModuleList); + } + + @Test + public void setModuleEditedModuleHasNonUniqueIdentityThrowsDuplicateModuleException() { + uniqueModuleList.add(DATA_STRUCTURES); + uniqueModuleList.add(DISCRETE_MATH); + thrown.expect(DuplicateModuleException.class); + uniqueModuleList.setModule(DATA_STRUCTURES, DISCRETE_MATH); + } + + @Test + public void removeNullModuleThrowsNullPointerException() { + thrown.expect(NullPointerException.class); + uniqueModuleList.remove(null); + } + + @Test + public void removeModuleDoesNotExistThrowsModuleNotFoundException() { + thrown.expect(ModuleNotFoundException.class); + uniqueModuleList.remove(DATA_STRUCTURES); + } + + @Test + public void removeExistingModuleRemovesModule() { + uniqueModuleList.add(DATA_STRUCTURES); + uniqueModuleList.remove(DATA_STRUCTURES); + UniqueModuleList expectedUniqueModuleList = new UniqueModuleList(); + assertEquals(expectedUniqueModuleList, uniqueModuleList); + } + + @Test + public void setModulesNullUniqueModuleListThrowsNullPointerException() { + thrown.expect(NullPointerException.class); + uniqueModuleList.setModules((UniqueModuleList) null); + } + + @Test + public void setModulesUniqueModuleListReplacesOwnListWithProvidedUniqueModuleList() { + uniqueModuleList.add(DATA_STRUCTURES); + UniqueModuleList expectedUniqueModuleList = new UniqueModuleList(); + expectedUniqueModuleList.add(DISCRETE_MATH); + uniqueModuleList.setModules(expectedUniqueModuleList); + assertEquals(expectedUniqueModuleList, uniqueModuleList); + } + + @Test + public void setModulesNullListThrowsNullPointerException() { + thrown.expect(NullPointerException.class); + uniqueModuleList.setModules((List) null); + } + + @Test + public void setModulesListReplacesOwnListWithProvidedList() { + uniqueModuleList.add(DATA_STRUCTURES); + List moduleList = Collections.singletonList(DISCRETE_MATH); + uniqueModuleList.setModules(moduleList); + UniqueModuleList expectedUniqueModuleList = new UniqueModuleList(); + expectedUniqueModuleList.add(DISCRETE_MATH); + assertEquals(expectedUniqueModuleList, uniqueModuleList); + } + + @Test + public void setModulesListWithDuplicateModuleThrowsDuplicateModuleException() { + List listWithDuplicateModules = Arrays.asList(DATA_STRUCTURES, DATA_STRUCTURES); + thrown.expect(DuplicateModuleException.class); + uniqueModuleList.setModules(listWithDuplicateModules); + } + + @Test + public void asUnmodifiableObservableListModifyListThrowsUnsupportedOperationException() { + thrown.expect(UnsupportedOperationException.class); + uniqueModuleList.asUnmodifiableObservableList().remove(0); + } +} +``` +###### /java/seedu/address/model/module/CreditTest.java +``` java +public class CreditTest { + @Test + public void constructorInvalidCreditThrowsIllegalArgumentException() { + int invalidCredit = 0; + Assert.assertThrows(IllegalArgumentException.class, () -> new Credit(invalidCredit)); + } + + @Test + public void isValidCredit() { + // invalid code format + assertFalse(Credit.isValidCredit(0)); // must be greater than or equal to 1 + assertFalse(Credit.isValidCredit(21)); // must be lower than or equal to 20 + + // valid code + assertTrue(Credit.isValidCredit(4)); // credit between 1 and 20 + } + + @Test + public void toStringValid() { + assertTrue(new Credit(4).toString().contentEquals("4")); + } + + @Test + public void equalsValid() { + assertTrue(new Credit(4).equals(new Credit(4))); + } +} +``` +###### /java/seedu/address/model/module/CodeTest.java +``` java +public class CodeTest { + + @Test + public void constructorNullThrowsNullPointerException() { + Assert.assertThrows(NullPointerException.class, () -> new Code(null)); + } + + @Test + public void constructorInvalidCodeThrowsIllegalArgumentException() { + String invalidCode = ""; + Assert.assertThrows(IllegalArgumentException.class, () -> new Code(invalidCode)); + } + + @Test + public void isValidCode() { + // invalid code format + assertFalse(Code.isValidCode("")); // cannot be blank + assertFalse(Code.isValidCode(" CS2103")); // no leading whitespace + assertFalse(Code.isValidCode("CS2103 ")); // no leading whitespace + assertFalse(Code.isValidCode("CS 2103")); // no whitespace in between + + // valid code + assertTrue(Code.isValidCode("CS2103")); // no whitespace + } + + @Test + public void toStringValid() { + assertTrue(new Code("CS2103").toString().contentEquals("CS2103")); + } + + @Test + public void equalsValid() { + assertTrue(new Code("CS2103").equals(new Code("CS2103"))); + } +} +``` +###### /java/seedu/address/model/module/ModuleTest.java +``` java +public class ModuleTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void constructorNullThrowsNullPointerException() { + Assert.assertThrows(NullPointerException.class, () -> + new Module(null, null, null, null, null, false)); + } + + @Test + public void isSameModule() { + // same object -> returns true + assertTrue(DATA_STRUCTURES.isSameModule(DATA_STRUCTURES)); + + // different object -> returns false + assertFalse(DATA_STRUCTURES.isSameModule(DISCRETE_MATH)); + + // different code -> returns false + Module editedDataStructures = new ModuleBuilder(DATA_STRUCTURES) + .withCode(DISCRETE_MATH.getCode().value) + .build(); + assertFalse(DATA_STRUCTURES.isSameModule(editedDataStructures)); + } + + @Test + public void equals() { + // same values -> returns true + Module dataStructuresCopy = new ModuleBuilder(DATA_STRUCTURES).build(); + assertTrue(DATA_STRUCTURES.equals(dataStructuresCopy)); + + // same object -> returns true + assertTrue(DATA_STRUCTURES.equals(DATA_STRUCTURES)); + + // different type -> returns false + assertFalse(DATA_STRUCTURES.equals(5)); + + // different module -> returns false + assertFalse(DATA_STRUCTURES.equals(DISCRETE_MATH)); + + // different code -> returns false + Module editedDataStrucure = new ModuleBuilder(DATA_STRUCTURES) + .withCode(DISCRETE_MATH.getCode().value) + .build(); + assertFalse(DATA_STRUCTURES.equals(editedDataStrucure)); + + // different year -> returns false + editedDataStrucure = new ModuleBuilder(DATA_STRUCTURES) + .withYear(DISCRETE_MATH.getYear().value) + .build(); + assertFalse(DATA_STRUCTURES.equals(editedDataStrucure)); + + // different semester -> returns false + editedDataStrucure = new ModuleBuilder(DATA_STRUCTURES) + .withSemester(DISCRETE_MATH.getSemester().value) + .build(); + assertFalse(DATA_STRUCTURES.equals(editedDataStrucure)); + + // different credit -> returns false + editedDataStrucure = new ModuleBuilder(DATA_STRUCTURES) + .withCredit(DATABASE_SYSTEMS_2MC.getCredits().value) + .build(); + assertFalse(DATA_STRUCTURES.equals(editedDataStrucure)); + + // different grade -> returns false + editedDataStrucure = new ModuleBuilder(DATA_STRUCTURES) + .withGrade(DISCRETE_MATH.getGrade().value) + .build(); + assertFalse(DATA_STRUCTURES.equals(editedDataStrucure)); + + // different completed -> returns false + editedDataStrucure = new ModuleBuilder(DATA_STRUCTURES) + .withCompleted(false) + .build(); + assertFalse(DATA_STRUCTURES.equals(editedDataStrucure)); + } + + @Test + public void toStringValid() { + assertTrue(DATA_STRUCTURES.toString().contentEquals("Code: CS2040 Year: 3 Semester: " + + "s1 Credits: 4 Grade: F Completed: true")); + } +} +``` +###### /java/seedu/address/testutil/ModuleUtil.java +``` java +/** + * A utility class for Module. + */ +public class ModuleUtil { + + /** + * Returns an add command string for adding the {@code module}. + */ + public static String getAddModuleCommand(Module module) { + return AddModuleCommand.COMMAND_WORD + " " + getModuleDetails(module); + } + + /** + * Returns the part of command string for the given {@code person}'s details. + */ + public static String getModuleDetails(Module module) { + StringBuilder sb = new StringBuilder(); + sb.append(module.getCode().value + " "); + sb.append(module.getYear().value + " "); + sb.append(module.getSemester().value + " "); + sb.append(module.getCredits().value + " "); + sb.append(module.getGrade().value + " "); + return sb.toString(); + } +} +``` +###### /java/seedu/address/testutil/TypicalModules.java +``` java +/** + * A utility class containing a list of {@code Module} objects to be used in tests. + */ +public class TypicalModules { + // Manually added + + public static final Double MODULES_WITHOUT_NON_AFFECTING_MODULES_CAP = 3.0; + + public static final Module DISCRETE_MATH = new ModuleBuilder().withCode("CS1231") + .withYear(1) + .withSemester(Semester.SEMESTER_ONE) + .withCredit(4) + .withGrade("A+") + .build(); + + public static final Module PROGRAMMING_METHODOLOGY_TWO = new ModuleBuilder().withCode("CS2030") + .withYear(2) + .withSemester(Semester.SEMESTER_TWO) + .withCredit(4) + .withGrade("B+") + .build(); + + public static final Module DATA_STRUCTURES = new ModuleBuilder().withCode("CS2040") + .withYear(3) + .withSemester(Semester.SEMESTER_SPECIAL_ONE) + .withCredit(4) + .withGrade("F") + .build(); + + public static final Module ASKING_QUESTIONS = new ModuleBuilder().withCode("GEQ1000") + .withYear(1) + .withSemester(Semester.SEMESTER_ONE) + .withCredit(4) + .withGrade("CS") + .build(); + + public static final Module SOFTWARE_ENGINEERING = new ModuleBuilder().withCode("CS2103") + .withYear(3) + .withSemester(Semester.SEMESTER_ONE) + .withCredit(4) + .withGrade("A+") + .build(); + + public static final Module DATABASE_SYSTEMS = new ModuleBuilder().withCode("CS2102") + .withYear(2) + .withSemester(Semester.SEMESTER_ONE) + .withCredit(4) + .withGrade("A+") + .build(); + + public static final Module DATABASE_SYSTEMS_2MC = new ModuleBuilder().withCode("CS2102B") + .withYear(2) + .withSemester(Semester.SEMESTER_ONE) + .withCredit(2) + .withGrade("A+") + .build(); + + /** + * Prevents instantiation + */ + private TypicalModules() { + + } + + /** + * Returns an {@code Transcript} given modules as arguments. + */ + public static Transcript getTranscriptWithModules(Module... modules) { + Transcript tr = new Transcript(); + for (Module module : modules) { + tr.addModule(module); + } + return tr; + } + + /** + * Returns an {@code Transcript} with all the typical persons. + */ + public static Transcript getTypicalTranscript() { + Transcript tr = new Transcript(); + for (Module module : getTypicalModules()) { + tr.addModule(module); + } + return tr; + } + + public static List getTypicalModules() { + return new ArrayList<>(Arrays.asList(DISCRETE_MATH, + PROGRAMMING_METHODOLOGY_TWO, + DATA_STRUCTURES)); + } + + + /** + * A list of modules that affects the cap + * + * @return + */ + public static List getModulesWithoutNonGradeAffectingModules() { + return new ArrayList<>(Arrays.asList(DISCRETE_MATH, + PROGRAMMING_METHODOLOGY_TWO, + DATA_STRUCTURES)); + } + + /** + * A list of modules that might not affect the cap + * + * @return + */ + public static List getModulesWithNonGradeAffectingModules() { + List affectingModules = getModulesWithoutNonGradeAffectingModules(); + List nonAffectingModules = new ArrayList<>(Arrays.asList(ASKING_QUESTIONS)); + affectingModules.addAll(nonAffectingModules); + return affectingModules; + } + // TODO: getTypicalAddressBook() +} +``` diff --git a/collated/test/jeremiah-ang.md b/collated/test/jeremiah-ang.md new file mode 100644 index 000000000000..e9eef0657c19 --- /dev/null +++ b/collated/test/jeremiah-ang.md @@ -0,0 +1,286 @@ +# jeremiah-ang +###### /java/seedu/address/logic/parser/GoalCommandParserTest.java +``` java +public class GoalCommandParserTest { + private GoalCommandParser parser = new GoalCommandParser(); + + @Test + public void parseValidCommandSuccess() { + String userInput = "4.5"; + GoalCommand expectedCommand = new GoalCommand(4.5); + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parseInvalidNumberFormatFailure() { + String userInput = "4.5 3.5"; + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, GoalCommand.MESSAGE_USAGE); + assertParseFailure(parser, userInput, expectedMessage); + } +} +``` +###### /java/seedu/address/model/TranscriptTest.java +``` java +/** + * Test {@code TranscriptTest} Class + */ +public class TranscriptTest { + + private static final Module GRADE_BMINUS_4MC_A = new ModuleBuilder() + .withCode("BMINUSA") + .withCredit(4) + .withGrade("B-") + .build(); + private static final Module GRADE_A_4MC_A = new ModuleBuilder() + .withCode("AA") + .withCredit(4) + .withGrade("A") + .build(); + private static final Module GRADE_A_4MC_B = new ModuleBuilder() + .withCode("AB") + .withCredit(4) + .withGrade("A") + .build(); + private static final Module INCOMPLETE_4MC_A = new ModuleBuilder() + .withCode("INCOMPLETEA") + .withCredit(4) + .withCompleted(false) + .build(); + private static final Module INCOMPLETE_4MC_B = new ModuleBuilder() + .withCode("INCOMPLETEB") + .withCredit(4) + .withCompleted(false) + .build(); + private static final Module INCOMPLETE_4MC_C = new ModuleBuilder() + .withCode("INCOMPLETEC") + .withCredit(4) + .withCompleted(false) + .build(); + private static final Module INCOMPLETE_5MC_A = new ModuleBuilder() + .withCode("INCOMPLETE5A") + .withCredit(5) + .withCompleted(false) + .build(); + + @Test + public void typicalModulesCapScore() { + List modules = getModulesWithoutNonGradeAffectingModules(); + assertCapScoreEquals(modules, MODULES_WITHOUT_NON_AFFECTING_MODULES_CAP); + } + + @Test + public void calculateCapScoreWithSuModule() { + List modules = getModulesWithNonGradeAffectingModules(); + assertCapScoreEquals(modules, MODULES_WITHOUT_NON_AFFECTING_MODULES_CAP); + + } + + @Test + public void calculateTargetGrades() { + List modules = new ArrayList<>(Arrays.asList( + INCOMPLETE_4MC_A, + INCOMPLETE_4MC_B, + INCOMPLETE_4MC_C + )); + double capGoal = 4.0; + List expectedTargetGrades = new ArrayList<>(Arrays.asList( + "B+", + "B+", + "B+" + )); + assertTargetGradesEquals(modules, capGoal, expectedTargetGrades); + + modules = new ArrayList<>(Arrays.asList( + INCOMPLETE_4MC_A, + INCOMPLETE_4MC_B, + INCOMPLETE_5MC_A, + INCOMPLETE_4MC_C, + GRADE_BMINUS_4MC_A + )); + capGoal = 4.5; + expectedTargetGrades = new ArrayList<>(Arrays.asList( + "A", + "A", + "A", + "A-" + )); + assertTargetGradesEquals(modules, capGoal, expectedTargetGrades); + + capGoal = 5.0; + assertTargetGradesEquals(modules, capGoal, null); + + modules = new ArrayList<>(Arrays.asList( + GRADE_BMINUS_4MC_A + )); + assertTargetGradesEquals(modules, capGoal, new ArrayList<>()); + + modules = new ArrayList<>(Arrays.asList( + INCOMPLETE_4MC_A, + INCOMPLETE_4MC_B, + INCOMPLETE_4MC_C, + GRADE_A_4MC_A, + GRADE_A_4MC_B + )); + capGoal = 4.0; + expectedTargetGrades = new ArrayList<>(Arrays.asList( + "B", + "B", + "B-" + )); + assertTargetGradesEquals(modules, capGoal, expectedTargetGrades); + } + + /** + * Assert that the modules will have the CAP score of expectedCapScore + * @param modules + * @param expectedCapScore + */ + private void assertCapScoreEquals(List modules, Double expectedCapScore) { + Transcript transcript = new Transcript(); + transcript.setModules(modules); + double cap = transcript.getCap(); + assertEquals(Double.valueOf(cap), expectedCapScore); + } + + /** + * Assert that the given modules and cap goal will result in expected target grades + * @param modules + * @param capGoal + * @param expectedTargetGrades + */ + private void assertTargetGradesEquals(List modules, Double capGoal, List expectedTargetGrades) { + Transcript transcript = new Transcript(); + transcript.setModules(modules); + transcript.setCapGoal(capGoal); + ObservableList targetModules = transcript.getTargetModuleGrade(); + + if (expectedTargetGrades == null) { + assertEquals(targetModules, null); + return; + } + + List targetGrades = new ArrayList<>(); + targetModules.forEach(module -> targetGrades.add(module.getGrade().value)); + String targetGradesString = String.join(" ", targetGrades); + String expectedTargetGradesString = String.join(" ", expectedTargetGrades); + assertEquals(targetGradesString, expectedTargetGradesString); + } + +} +``` +###### /java/seedu/address/model/module/GradeTest.java +``` java + @Test + public void isValidPoint() { + assertTrue(Grade.isValidPoint(5.0)); + assertTrue(Grade.isValidPoint(4.0)); + assertTrue(Grade.isValidPoint(3.0)); + assertTrue(Grade.isValidPoint(2.0)); + assertTrue(Grade.isValidPoint(1.0)); + assertTrue(Grade.isValidPoint(0)); + assertTrue(Grade.isValidPoint(0.0)); + assertTrue(Grade.isValidPoint(4.5)); + assertTrue(Grade.isValidPoint(3.5)); + assertTrue(Grade.isValidPoint(2.5)); + assertTrue(Grade.isValidPoint(1.5)); + assertFalse(Grade.isValidPoint(6.0)); + assertFalse(Grade.isValidPoint(4.3)); + assertFalse(Grade.isValidPoint(0.5)); + } + @Test + public void getGradeValid() { + assertTrue("A".equals(new Grade(5.0).value)); + assertTrue("A-".equals(new Grade(4.5).value)); + assertTrue("B+".equals(new Grade(4.0).value)); + assertTrue("B".equals(new Grade(3.5).value)); + assertTrue("B-".equals(new Grade(3.0).value)); + assertTrue("C+".equals(new Grade(2.5).value)); + assertTrue("C".equals(new Grade(2.0).value)); + assertTrue("D+".equals(new Grade(1.5).value)); + assertTrue("D".equals(new Grade(1.0).value)); + assertTrue("F".equals(new Grade(0).value)); + } + +} +``` +###### /java/systemtests/GoalCommandSystemTest.java +``` java +/** + * System test for Goal Command + */ +public class GoalCommandSystemTest extends AddressBookSystemTest { + @Test + public void setGoalSuccess() { + /* Case: Set goal with valid value + * -> goal command handled correctly + */ + double newGoal = 4.5; + assertGoalSuccess(newGoal); + + newGoal = 5.0; + assertGoalSuccess(newGoal); + } + + @Test + public void setGoalFailure() { + /* Case: Set goal with valid value + * -> goal command handled correctly + */ + double newGoal = -1; + assertGoalFailure(newGoal); + } + + /** + * Assert that the given goal would result in a failure action. + * @param goal + */ + private void assertGoalFailure(double goal) { + String expectedResultMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, GoalCommand.MESSAGE_USAGE); + assertCommandFailure(getCommandString(goal), getModel(), expectedResultMessage); + } + + /** + * Assert that the given goal would result in a successful action. + * @param goal + */ + public void assertGoalSuccess(double goal) { + String expectedResultMessage = String.format(GoalCommand.MESSAGE_SUCCESS, goal); + assertCommandSuccess(getCommandString(goal), getModel(), expectedResultMessage); + } + + private String getCommandString(double goal) { + return GoalCommand.COMMAND_WORD + " " + goal; + } + + /** + * Assert given command would be successful + * @param command + * @param expectedModel + * @param expectedResultMessage + */ + private void assertCommandSuccess(String command, Model expectedModel, String expectedResultMessage) { + executeCommand(command); + assertApplicationDisplaysExpected("", expectedResultMessage, expectedModel); + } + + private void assertCommandFailure(String command, Model expectedModel, String expectedResultMessage) { + executeCommand(command); + assertApplicationDisplaysExpected(command, expectedResultMessage, expectedModel); + } +} +``` +###### /java/systemtests/CapCommandSystemTest.java +``` java +public class CapCommandSystemTest extends AddressBookSystemTest { + @Test + public void cap() { + + /** + * Empty system should show cap = 0 + */ + executeCommand(CapCommand.COMMAND_WORD); + double cap = 0.0; + assertApplicationDisplaysExpected("", String.format(CapCommand.MESSAGE_SUCCESS, cap), getModel()); + } +} +``` diff --git a/docs/AboutUs.adoc b/docs/AboutUs.adoc index e647ed1e715a..31dc1752cf07 100644 --- a/docs/AboutUs.adoc +++ b/docs/AboutUs.adoc @@ -4,53 +4,65 @@ :imagesDir: images :stylesDir: stylesheets -AddressBook - Level 4 was developed by the https://se-edu.github.io/docs/Team.html[se-edu] team. + -_{The dummy content given below serves as a placeholder to be used by future forks of the project.}_ + -{empty} + +CAPTracker is for those students who prefer to use a desktop app for calculating and managing their CAP. More importantly CAPTracker is optimized for those who prefer to work with a Command Line Interface (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, CAPTracker is the ideal application to calculate your current CAP, and predict what grades you need in modules you haven’t taken to achieve your ideal CAP. + We are a team based in the http://www.comp.nus.edu.sg[School of Computing, National University of Singapore]. == Project Team -=== John Doe -image::damithc.jpg[width="150", align="left"] -{empty}[http://www.comp.nus.edu.sg/~damithch[homepage]] [https://github.com/damithc[github]] [<>] +=== Alex Koh +image::alexkmj.png[width="150", align="left"] +{empty} [https://github.com/alexkmj[github]] [<>] -Role: Project Advisor +Role: Developer + +Responsibilities: +* Tools Expert +* Code quality +* In charge of Logic Component ''' -=== John Roe -image::lejolly.jpg[width="150", align="left"] -{empty}[http://github.com/lejolly[github]] [<>] +=== Amber Joseph +image::josephambe.png[width="150", align="left"] +{empty}[http://github.com/josephambe[github]] [<>] -Role: Team Lead + -Responsibilities: UI +Role: Developer + +Responsibilities: +* Documentation +* Deliverables and deadlines +* In charge of UI Component ''' -=== Johnny Doe -image::yijinl.jpg[width="150", align="left"] -{empty}[http://github.com/yijinl[github]] [<>] +=== Jeremy Yew +image::jeremyyew.png[width="150", align="left"] +{empty}[http://github.com/jeremyyew[github]] [<>] Role: Developer + -Responsibilities: Data +Responsibilities: +* Team Lead +* Code quality +* In charge of Storage Component ''' -=== Johnny Roe -image::m133225.jpg[width="150", align="left"] -{empty}[http://github.com/m133225[github]] [<>] +=== Jeremiah Ang +image::jeremiah-ang.png[width="150", align="left"] +{empty}[http://github.com/jeremiah-ang[github]] [<>] Role: Developer + -Responsibilities: Dev Ops + Threading +Responsibilities: +* Scheduling and tracking +* Integration +* In charge of Model Component ''' -=== Benson Meier -image::yl_coder.jpg[width="150", align="left"] -{empty}[http://github.com/yl-coder[github]] [<>] +=== Kong Jun Yin +image::bugeyedbug.png[width="150", align="left"] +{empty}[http://github.com/BugEyedBug[github]] [<>] Role: Developer + -Responsibilities: UI +Responsibilities: +* Testing ''' diff --git a/docs/ContactUs.adoc b/docs/ContactUs.adoc index 5de5363abffd..342f79460a42 100644 --- a/docs/ContactUs.adoc +++ b/docs/ContactUs.adoc @@ -2,6 +2,5 @@ :site-section: ContactUs :stylesDir: stylesheets -* *Bug reports, Suggestions* : Post in our https://github.com/se-edu/addressbook-level4/issues[issue tracker] if you noticed bugs or have suggestions on how to improve. +* *Bug reports, Suggestions* : Post in our https://github.com/CS2103-AY1819S1-T13-4/main/issues[issue tracker] if you noticed bugs or have suggestions on how to improve. * *Contributing* : We welcome pull requests. Follow the process described https://github.com/oss-generic/process[here] -* *Email us* : You can also reach us at `damith [at] comp.nus.edu.sg` diff --git a/docs/DeveloperGuide.adoc b/docs/DeveloperGuide.adoc index ea58481e4740..8a93051e67ff 100644 --- a/docs/DeveloperGuide.adoc +++ b/docs/DeveloperGuide.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 4 - Developer Guide += CAPTracker - Developer Guide :site-section: DeveloperGuide :toc: :toc-title: @@ -12,15 +12,15 @@ ifdef::env-github[] :note-caption: :information_source: :warning-caption: :warning: endif::[] -:repoURL: https://github.com/se-edu/addressbook-level4/tree/master +:repoURL: https://github.com/CS2103-AY1819S1-T13-4/main/tree/master -By: `Team SE-EDU`      Since: `Jun 2016`      Licence: `MIT` +By: `T13-4`      Since: `Aug 2018`      Licence: `MIT` == Setting up === Prerequisites -. *JDK `9`* or later +. *JDK `10`* or later + [WARNING] JDK `10` on Windows will fail to run tests in <> due to a https://github.com/javafxports/openjdk-jfx/issues/66[JavaFX bug]. @@ -58,7 +58,7 @@ This will generate all resources required by the application and tests. This project follows https://github.com/oss-generic/process/blob/master/docs/CodingStandards.adoc[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), or `IntelliJ IDEA` > `Preferences...` (macOS) +. Go to `File` > `Settings...` (Windows/Linux), or `IntelliJ IDEA` > `Preferences...` (macOS)to add manually . Select `Editor` > `Code Style` > `Java` . Click on the `Imports` tab to set the order @@ -99,7 +99,7 @@ When you are ready to start coding, 2. Take a look at <>. == Design - +tag::architecture[] [[Design-Architecture]] === Architecture @@ -136,29 +136,30 @@ Each of the four components 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. .Class Diagram of the Logic Component -image::LogicClassDiagram.png[width="800"] +image::LogicClassDiagram.png[width="600"] +image::[width="800"] [discrete] ==== Events-Driven nature of the design -The _Sequence Diagram_ below shows how the components interact for the scenario where the user issues the command `delete 1`. +The _Sequence Diagram_ below shows how the components interact for the scenario where the user issues the command `delete -t CS2103 -e 4 -z 2`. -.Component interactions for `delete 1` command (part 1) -image::SDforDeletePerson.png[width="800"] +.Component interactions for `delete -t CS2103 -e 4 -z 2` command (part 1) +image::SDforDeleteModule.png[width="800"] [NOTE] -Note how the `Model` simply raises a `AddressBookChangedEvent` when the Address Book data are changed, instead of asking the `Storage` to save the updates to the hard disk. +Note how the `Model` simply raises a `TranscriptChangedEvent` when the Transcript data are changed, instead of asking the `Storage` to save the updates to the hard disk. -The diagram below shows how the `EventsCenter` reacts to that event, which eventually results in the updates being saved to the hard disk and the status bar of the UI being updated to reflect the 'Last Updated' time. +The diagram below shows how the `EventsCenter` reacts to that event, which eventually results in the updates being saved to the hard disk and the panels of the UI being updated to reflect the latest modules in the system. -.Component interactions for `delete 1` command (part 2) -image::SDforDeletePersonEventHandling.png[width="800"] +.Component interactions for `delete -t CS2103 -e 4 -z 2` command (part 2) +image::SDforDeleteModuleEventHandling.png[width="800"] [NOTE] Note how the event is propagated through the `EventsCenter` to the `Storage` and `UI` without `Model` having to be coupled to either of them. This is an example of how this Event Driven approach helps us reduce direct coupling between components. The sections below give more details of each component. - +end::architecture[] [[Design-Ui]] === UI component @@ -167,16 +168,34 @@ image::UiClassDiagram.png[width="800"] *API* : link:{repoURL}/src/main/java/seedu/address/ui/Ui.java[`Ui.java`] -The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter`, `BrowserPanel` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class. +The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `StatusBarFooter`, `BrowserPanel`, 'ModuleListPanel' 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 link:{repoURL}/src/main/java/seedu/address/ui/MainWindow.java[`MainWindow`] is specified in link:{repoURL}/src/main/resources/view/MainWindow.fxml[`MainWindow.fxml`] -The `UI` component, +The `UI` component uses JavaFX UI 'DarkTheme' to draw different text, sizes, fonts, and colours from. The actual data displayed in the UI is called using a sample transcript which is created through the Module and Transcript classes. The values themselves are abstracted from the '.fxml' files so the UI display can be easily updated. * Executes user commands using the `Logic` component. * Binds itself to some data in the `Model` so that the UI can auto-update when data in the `Model` change. * Responds to events raised from various parts of the App and updates the UI accordingly. +[[Design-Layout]] +=== UI component +* The bottom two thirds of the UI is seperated into 2 panels to clearly identify the different outputs from commands entered by the user. +* The first panel on the left is for Modules that have already been completed; this is shown by the GREEN circles which +surround the grades which indicate this grade is "set" and of no concern to the user anymore. +* The second panel on the right is for Modules that have not yet been completed by the user; this is shown by the RED +circles which surround the grades to indicate that this is a grade the user should be aware of. The red indicates an +urgency towards that module as it's outcome will affect the users predicted CAP goal. +* The top third of the UI is seperated into four distinct rows; +. The first row contains the title and drop down menu's for `File` and `Help` options. +. The second row is the command line and how the user interacts with the application. Notice there is no button for the +user to click when they are ready to enter their command; it is expected the user is familiar with Command Line Interface +and will know to use the `enter` button on their keyboard when ready to submit a command to the app. +. The third row is where replies from the application to the user will be displayed. When the commands become too big +for the box, a scroll down option becomes available for the user to continue reading the message. +. The fourth row displays the summary of the users current CAP goal and their target CAP. + +//tag::designlogic[] [[Design-Logic]] === Logic component @@ -187,36 +206,37 @@ image::LogicClassDiagram.png[width="800"] *API* : link:{repoURL}/src/main/java/seedu/address/logic/Logic.java[`Logic.java`] -. `Logic` uses the `AddressBookParser` class to parse the user command. +. `Logic` uses the `TranscriptParser` class to parse the user command. . This results in a `Command` object which is executed by the `LogicManager`. -. The command execution can affect the `Model` (e.g. adding a person) and/or raise events. +. The command execution can affect the `Model` (e.g. adding a module) and/or raise events. . The result of the command execution is encapsulated as a `CommandResult` object which is passed back to the `Ui`. -Given below is the Sequence Diagram for interactions within the `Logic` component for the `execute("delete 1")` API call. +Given below is the Sequence Diagram for interactions within the `Logic` component for the + + `execute("delete -t CS1231")` API call. -.Interactions Inside the Logic Component for the `delete 1` Command -image::DeletePersonSdForLogic.png[width="800"] +.Interactions Inside the Logic Component for the `delete -t CS1231` Command +image::DeleteModuleForLogic.png[width="800"] +//end::designlogic[] [[Design-Model]] +//tag::designmodel[] === Model component .Structure of the Model Component -image::ModelClassDiagram.png[width="800"] +image::ModelClassDiagram_Transcript.png[width="800"] *API* : link:{repoURL}/src/main/java/seedu/address/model/Model.java[`Model.java`] The `Model`, * stores a `UserPref` object that represents the user's preferences. -* stores the Address Book data. -* exposes an unmodifiable `ObservableList` 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. +* stores the Transcript data. +* exposes an unmodifiable `ObservableList` 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. +* provides filter function to filter `Module` with different kind of `Grade` -[NOTE] -As a more OOP model, we can store a `Tag` list in `Address Book`, which `Person` can reference. This would allow `Address Book` to only require one `Tag` object per unique `Tag`, instead of each `Person` needing their own `Tag` object. An example of how such a model may look like is given below. + - + -image:ModelClassBetterOopDiagram.png[width="800"] - +//end::designmodel[] +//tag::designstorage[] [[Design-Storage]] === Storage component @@ -225,10 +245,14 @@ image::StorageClassDiagram.png[width="800"] *API* : link:{repoURL}/src/main/java/seedu/address/storage/Storage.java[`Storage.java`] -The `Storage` component, +The `Storage` interface defines a component which + +* can save `UserPref` objects in JSON format and read it back. +* can save Transcript data in JSON format and read it back. + +The `StorageManager` implements the `Storage` interface, and updates the transcript JSON file when the `TranscriptChangedEvent` is fired (see Figure 4). It also logs the reading and saving of transcript data. -* can save `UserPref` objects in json format and read it back. -* can save the Address Book data in xml format and read it back. +//end::designstorage[] [[Design-Commons]] === Common classes @@ -239,92 +263,88 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa This section describes some noteworthy details on how certain features are implemented. -// tag::undoredo[] -=== Undo/Redo feature -==== Current Implementation +// tag::captargetcalculation[] +=== CAP & Target Grades Calculation -The undo/redo mechanism is facilitated by `VersionedAddressBook`. -It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. -Additionally, it implements the following operations: +The two calculations are triggered upon an change to the list of modules in `Transcript` _i.e. add/update/delete_. -* `VersionedAddressBook#commit()` -- Saves the current address book state in its history. -* `VersionedAddressBook#undo()` -- Restores the previous address book state from its history. -* `VersionedAddressBook#redo()` -- Restores a previously undone address book state from its history. +.Sequence Diagram of updating modules in Transcript +image::SDTranscriptModulesUpdate.png[width="800"] -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. +[[Implementation-CAP]] +==== CAP Calculation -Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. +The CAP calculation is handled by `Transcript`. -Step 1. The user launches the application for the first time. The `VersionedAddressBook` will be initialized with the initial address book state, and the `currentStatePointer` pointing to that single address book state. +The pseudo-code for CAP is the following: +``` +all_points <- sum(credits(m) * points(m) for all completed modules m) +all_credits <- sum(credits(m) for all completed modules m) -image::UndoRedoStartingStateListDiagram.png[width="800"] +CAP <- all_points/all_credits +``` -Step 2. The user executes `delete 5` command to delete the 5th person in the address book. The `delete` command calls `Model#commitAddressBook()`, causing the modified state of the address book after the `delete 5` command executes to be saved in the `addressBookStateList`, and the `currentStatePointer` is shifted to the newly inserted address book state. +.Sequence Diagram of CAP calculation +image::SDTranscriptCalculateCap.png[width="800"] -image::UndoRedoNewCommand1StateListDiagram.png[width="800"] +CAP Calculation is triggered by: -Step 3. The user executes `add n/David ...` to add a new person. The `add` command also calls `Model#commitAddressBook()`, causing another modified address book state to be saved into the `addressBookStateList`. - -image::UndoRedoNewCommand2StateListDiagram.png[width="800"] - -[NOTE] -If a command fails its execution, it will not call `Model#commitAddressBook()`, so the address book state will not be saved into the `addressBookStateList`. - -Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoAddressBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous address book state, and restores the address book to that state. - -image::UndoRedoExecuteUndoStateListDiagram.png[width="800"] - -[NOTE] -If the `currentStatePointer` is at index 0, pointing to the initial address book state, then there are no previous address book states to restore. The `undo` command uses `Model#canUndoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the undo. +[[Implementation-TargetGrades]] +==== Target Grades Calculation -The following sequence diagram shows how the undo operation works: +The target `Grade` calculation is facilitated by `Transcript`. +The returned list of modules with target `Grade` assures the following properties: -image::UndoRedoSequenceDiagram.png[width="800"] - -The `redo` command does the opposite -- it calls `Model#redoAddressBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the address book to that state. - -[NOTE] -If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone address book states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. - -Step 5. The user then decides to execute the command `list`. Commands that do not modify the address book, such as `list`, will usually not call `Model#commitAddressBook()`, `Model#undoAddressBook()` or `Model#redoAddressBook()`. Thus, the `addressBookStateList` remains unchanged. - -image::UndoRedoNewCommand3StateListDiagram.png[width="800"] +* Reducing the `Grade` of any proposed target will result in the increase of another. +* If `x` is the minimum `Grade` required when assigned to *all* modules to obtain the desired CAP Goal, +none of the proposed target `Grade` will be greater than `x` ++ +i.e. if assigning `B+` to *all* module is the minimal requirement to obtain the desired CAP Goal, +none of the proposed target `Grade` will be `A-` or above. -Step 6. The user executes `clear`, which calls `Model#commitAddressBook()`. Since the `currentStatePointer` is not pointing at the end of the `addressBookStateList`, all address book states after the `currentStatePointer` will be purged. We designed it this way because it no longer makes sense to redo the `add n/David ...` command. This is the behavior that most modern desktop applications follow. +Below is the pseudo-code for Target Grade Calculation: +``` +CG <- CAP goal of user. +TC <- total credit of completed and incomplete modules. +PO <- total points achieved from completed and adjusted modules. +P <- CG * TC - PO // total points needed to achieve from incomplete modules. -image::UndoRedoNewCommand4StateListDiagram.png[width="800"] +mc_remaining <- sum of module credit of all incomplete modules +accumulated_points <- 0 +for every incomplete Module m: + avg_point_per_mc <- (P - accumulated_points) / mc_remaining + target(m) <- ceiling(avg_point_per_mc) + mc_remaining <- mc_remaining - credits(m) + accumulated_points <- accumulated_points + (credits(m) * target(m)) -The following activity diagram summarizes what happens when a user executes a new command: +``` -image::UndoRedoActivityDiagram.png[width="650"] +This sequence diagram shows the interaction of the different classes involved +in the process of creating a new Module with updated target grade -==== Design Considerations +.Sequence Diagram of Target Grade calculation +image::SDTranscriptTargetCalculation.png[width="800"] -===== Aspect: How undo & redo executes +And below the activity diagram to further illustrate several exceptional cases. -* **Alternative 1 (current choice):** Saves the entire address book. -** Pros: Easy to implement. -** Cons: May have performance issues in terms of memory usage. -* **Alternative 2:** Individual command knows how to undo/redo by itself. -** Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). -** Cons: We must ensure that the implementation of each individual command are correct. +.Activity Diagram of High level view of Target Grade Calculation +image::activityDiagramTargetGradeCalculationWhenChanged.png[width="800"] -===== Aspect: Data structure to support the undo/redo commands +.Activity Diagram of Calculate New Target Grade +image::activityDiagramTargetGradeCalculationActualCalculation.png[width="800"] -* **Alternative 1 (current choice):** Use a list to store the history of address book 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 `VersionedAddressBook`. -* **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. -// end::undoredo[] +// end::captargetcalculation[] -// tag::dataencryption[] -=== [Proposed] Data Encryption +//tag::transcriptstorageimplementation[] +=== Transcript Storage: Reading and Writing +Whenever the in-memory `Transcript` object is changed, the transcript data file is updated. When the app initializes, it will look for an existing data file from which to load the transcript.+ +If the file is not found, the app will initialize with an empty Transcript.+ +If the file is found but the data is an incorrect format or there is some problem reading from the file, the app will initialize with an empty Transcript as well.+ -_{Explain here how the data encryption feature will be implemented}_ +When the app initializes, it looks for the file name provided by `data/preferences.json` under the `transcriptFilePath` key - by default this is `data/transcript_demo.json`. This file stores all transcript data including modules data and cap Goal information, such as its value, whether it is set, and whether it is impossible. The `transcriptFilePath` may be changed manually by the user.+ -// end::dataencryption[] +By using the `Jackson` library to store the Transcript data as a JSON file instead of an XML file, we avoid having to write `XMLSerializableTranscript` and `XMLAdaptedModule` classes. Instead, we simply register a custom `JSONTranscriptDeserializer` on the `ReadOnlyTranscript` class so that when we read from the JSON file, Jackson's `ObjectMapper` is able to use the values to reconstruct the saved object. +//end::transcriptstorageimplementation[] === Logging @@ -533,429 +553,466 @@ a. Include those libraries in the repo (this bloats the repo size) + b. Require developers to download those libraries manually (this creates extra work for developers) [[GetStartedProgramming]] -[appendix] -== Suggested Programming Tasks to Get Started -Suggested path for new programmers: - -1. First, add small local-impact (i.e. the impact of the change does not go beyond the component) enhancements to one component at a time. Some suggestions are given in <>. - -2. Next, add a feature that touches multiple components to learn how to implement an end-to-end feature across all components. <> explains how to go about adding such a feature. +//tag::targetUser[] +[appendix] +== Target User Profile +- An NUS Student who has a need to keep track of current CAP, calculate expected CAP, and grades required to achieve desired CAP. +- Prefer desktop apps over other types. +- Can type fast. +- Prefers typing over mouse input. +- Is reasonably comfortable using CLI apps. +//end::targetUser[] + +//tag::value[] +[appendix] +== Value Proposition +Helps students manage their CAP and predict what grades they need to reach their CAP goal. +//end::value[] -[[GetStartedProgramming-EachComponent]] -=== Improving each component +//tag::userStories[] +[appendix] +== User Stories -Each individual exercise in this section is component-based (i.e. you would not need to modify the other components to get it to work). +*Must-Have* + +1. As a user, I can add all the modules I have taken (module code, +*module title*, MCs, grade, semester taken) so that I can calculate +average mark I need to graduate with desired CAP. +2. As a user I can delete modules so if I change my mind or fail a +module I can re-calculate my average. +3. As a user I can delete modules so that if i entered a wrong module +or failed it, I can remove it. +4. As a user I can edit the marks I’ve entered previously so that I can +update my CAP. +5. As a user, I can enter a CAP goal so that I can keep track of the +progress of my course +6. As a user, I can calculate the average mark I need across the +modules I’ve entered so I can see what mark I need for each module to +achieve my CAP goal. + +7. As a user, I can enter in predicted grades for modules so I can see +what CAP I would get if I got these grades in my modules. +8. As a user, I can close the app and return to modify my entries so I +can enter my entries incrementally. + +*Nice-To-Have* + +1. As a user, I can add a module without having to add the code, MCs, or semester taken so that I can quickly calculate CAP without worrying about the modules. +2. As a user, I can still enter the same modules but get warned first so that I would not mistakenly enter the same module again +3. As a user, I can import all modules for this semester with NUSMods link so that my data can be consistent with NUSMods. +4. As a user I can search for a module via keyword or module title, and select it for entering my grade so that I don’t have to remember the module code. +5. As a user, I can adjust what marks I need for each module to achieve my CAP so I can put more emphasis on certain modules instead of expecting the same grade across all modules. +6. As a user, I can view my current semester’s module goals and use a GUI to adjust projected grade for each module, and other modules will automatically adjust to compensate, so that I can see easily modify my data to see what grades I need for other modules. +7. As a user, I can view total current MCs so that i can check if I meet the number of mc I need to graduate +8. As a user I can see if my CAP Goal is possible so I can check whether it is achievable. +9. As a user, I can list modules taken by level as an alternative listing mode, so that it is easier to track graduation requirements. +10. As a user, I can see what the highest possible CAP I can achieve with my current grades is so that I can set an appropriate goal. +11. As a user, I can see what the minimum grades I need per module is to pass the year with my current grades so that I can see what the minimum about of work I need to do is. + + +*Not-Useful* + +1. As a user, I can view the module descriptions so that I know what modules I should take +2. As a user I can enter the MCs for exchange I can keep track of modules that may not be mapped to our database of modules codes or isn’t the default number of MCs. +3. As a user, I can export my timetable so that I can share it with my friend. +//end::userStories[] -[discrete] -==== `Logic` component +[appendix] +== Use Cases -*Scenario:* You are in charge of `logic`. During dog-fooding, your team realize that it is troublesome for the user to type the whole command in order to execute a command. Your team devise some strategies to help cut down the amount of typing necessary, and one of the suggestions was to implement aliases for the command words. Your job is to implement such aliases. +//tag::usecase[] +=== Use case: [UC1] Calculating current CAP -[TIP] -Do take a look at <> before attempting to modify the `Logic` component. +*MSS* -. Add a shorthand equivalent alias for each of the individual commands. For example, besides typing `clear`, the user can also type `c` to remove all persons in the list. +1. User enter modules +2. System recalculates CAP +3. System displays CAP + -**** -* Hints -** Just like we store each individual command word constant `COMMAND_WORD` inside `*Command.java` (e.g. link:{repoURL}/src/main/java/seedu/address/logic/commands/FindCommand.java[`FindCommand#COMMAND_WORD`], link:{repoURL}/src/main/java/seedu/address/logic/commands/DeleteCommand.java[`DeleteCommand#COMMAND_WORD`]), you need a new constant for aliases as well (e.g. `FindCommand#COMMAND_ALIAS`). -** link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser`] is responsible for analyzing command words. -* Solution -** Modify the switch statement in link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser#parseCommand(String)`] such that both the proper command word and alias can be used to execute the same intended command. -** Add new tests for each of the aliases that you have added. -** Update the user guide to document the new aliases. -** See this https://github.com/se-edu/addressbook-level4/pull/785[PR] for the full solution. -**** - -[discrete] -==== `Model` component +Use case ends. -*Scenario:* You are in charge of `model`. One day, the `logic`-in-charge approaches you for help. He wants to implement a command such that the user is able to remove a particular tag from everyone in the address book, but the model API does not support such a functionality at the moment. Your job is to implement an API method, so that your teammate can use your API to implement his command. +*Extensions* -[TIP] -Do take a look at <> before attempting to modify the `Model` component. - -. Add a `removeTag(Tag)` method. The specified tag will be removed from everyone in the address book. -+ -**** -* Hints -** The link:{repoURL}/src/main/java/seedu/address/model/Model.java[`Model`] and the link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`] API need to be updated. -** Think about how you can use SLAP to design the method. Where should we place the main logic of deleting tags? -** Find out which of the existing API methods in link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`] and link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`] classes can be used to implement the tag removal logic. link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`] allows you to update a person, and link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`] allows you to update the tags. -* Solution -** Implement a `removeTag(Tag)` method in link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`]. Loop through each person, and remove the `tag` from each person. -** Add a new API method `deleteTag(Tag)` in link:{repoURL}/src/main/java/seedu/address/model/ModelManager.java[`ModelManager`]. Your link:{repoURL}/src/main/java/seedu/address/model/ModelManager.java[`ModelManager`] should call `AddressBook#removeTag(Tag)`. -** Add new tests for each of the new public methods that you have added. -** See this https://github.com/se-edu/addressbook-level4/pull/790[PR] for the full solution. -**** +* 1a. User enters invalid parameters +** 1a1. System shows an `Invalid entry` error message ++ +Use case ends. -[discrete] -==== `Ui` component +* 1b. User enters duplicate Module +** 1b1. System shows an `Duplicate Module` error message ++ +Use case ends -*Scenario:* You are in charge of `ui`. During a beta testing session, your team is observing how the users use your address book application. You realize that one of the users occasionally tries to delete non-existent tags from a contact, because the tags all look the same visually, and the user got confused. Another user made a typing mistake in his command, but did not realize he had done so because the error message wasn't prominent enough. A third user keeps scrolling down the list, because he keeps forgetting the index of the last person in the list. Your job is to implement improvements to the UI to solve all these problems. +=== Use case: [UC2] View grades needed to achieve CAP goal -[TIP] -Do take a look at <> before attempting to modify the `UI` component. +*MSS* -. Use different colors for different tags inside person cards. For example, `friends` tags can be all in brown, and `colleagues` tags can be all in yellow. +1. User enters completed Modules +2. User enters incomplete Modules + -**Before** +Step 1-2 are repeated until user is satisfied. +3. User enter CAP goal +4. System calculated target grades +5. System displays target grades for ungraded modules + -image::getting-started-ui-tag-before.png[width="300"] -+ -**After** -+ -image::getting-started-ui-tag-after.png[width="300"] -+ -**** -* Hints -** The tag labels are created inside link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[the `PersonCard` constructor] (`new Label(tag.tagName)`). https://docs.oracle.com/javase/8/javafx/api/javafx/scene/control/Label.html[JavaFX's `Label` class] allows you to modify the style of each Label, such as changing its color. -** Use the .css attribute `-fx-background-color` to add a color. -** You may wish to modify link:{repoURL}/src/main/resources/view/DarkTheme.css[`DarkTheme.css`] to include some pre-defined colors using css, especially if you have experience with web-based css. -* Solution -** You can modify the existing test methods for `PersonCard` 's to include testing the tag's color as well. -** See this https://github.com/se-edu/addressbook-level4/pull/798[PR] for the full solution. -*** The PR uses the hash code of the tag names to generate a color. This is deliberately designed to ensure consistent colors each time the application runs. You may wish to expand on this design to include additional features, such as allowing users to set their own tag colors, and directly saving the colors to storage, so that tags retain their colors even if the hash code algorithm changes. -**** +Use case ends. -. Modify link:{repoURL}/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java[`NewResultAvailableEvent`] such that link:{repoURL}/src/main/java/seedu/address/ui/ResultDisplay.java[`ResultDisplay`] can show a different style on error (currently it shows the same regardless of errors). -+ -**Before** -+ -image::getting-started-ui-result-before.png[width="200"] -+ -**After** -+ -image::getting-started-ui-result-after.png[width="200"] -+ -**** -* Hints -** link:{repoURL}/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java[`NewResultAvailableEvent`] is raised by link:{repoURL}/src/main/java/seedu/address/ui/CommandBox.java[`CommandBox`] which also knows whether the result is a success or failure, and is caught by link:{repoURL}/src/main/java/seedu/address/ui/ResultDisplay.java[`ResultDisplay`] which is where we want to change the style to. -** Refer to link:{repoURL}/src/main/java/seedu/address/ui/CommandBox.java[`CommandBox`] for an example on how to display an error. -* Solution -** Modify link:{repoURL}/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java[`NewResultAvailableEvent`] 's constructor so that users of the event can indicate whether an error has occurred. -** Modify link:{repoURL}/src/main/java/seedu/address/ui/ResultDisplay.java[`ResultDisplay#handleNewResultAvailableEvent(NewResultAvailableEvent)`] to react to this event appropriately. -** You can write two different kinds of tests to ensure that the functionality works: -*** The unit tests for `ResultDisplay` can be modified to include verification of the color. -*** The system tests link:{repoURL}/src/test/java/systemtests/AddressBookSystemTest.java[`AddressBookSystemTest#assertCommandBoxShowsDefaultStyle() and AddressBookSystemTest#assertCommandBoxShowsErrorStyle()`] to include verification for `ResultDisplay` as well. -** See this https://github.com/se-edu/addressbook-level4/pull/799[PR] for the full solution. -*** Do read the commits one at a time if you feel overwhelmed. -**** +*Extensions* -. Modify the link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter`] to show the total number of people in the address book. -+ -**Before** -+ -image::getting-started-ui-status-before.png[width="500"] -+ -**After** -+ -image::getting-started-ui-status-after.png[width="500"] +* 3a. CAP goal is invalid +** 3a1. System shows an `Invalid CAP Goal` error message + -**** -* Hints -** link:{repoURL}/src/main/resources/view/StatusBarFooter.fxml[`StatusBarFooter.fxml`] will need a new `StatusBar`. Be sure to set the `GridPane.columnIndex` properly for each `StatusBar` to avoid misalignment! -** link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter`] needs to initialize the status bar on application start, and to update it accordingly whenever the address book is updated. -* Solution -** Modify the constructor of link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter`] to take in the number of persons when the application just started. -** Use link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter#handleAddressBookChangedEvent(AddressBookChangedEvent)`] to update the number of persons whenever there are new changes to the addressbook. -** For tests, modify link:{repoURL}/src/test/java/guitests/guihandles/StatusBarFooterHandle.java[`StatusBarFooterHandle`] by adding a state-saving functionality for the total number of people status, just like what we did for save location and sync status. -** For system tests, modify link:{repoURL}/src/test/java/systemtests/AddressBookSystemTest.java[`AddressBookSystemTest`] to also verify the new total number of persons status bar. -** See this https://github.com/se-edu/addressbook-level4/pull/803[PR] for the full solution. -**** - -[discrete] -==== `Storage` component - -*Scenario:* You are in charge of `storage`. For your next project milestone, your team plans to implement a new feature of saving the address book to the cloud. However, the current implementation of the application constantly saves the address book after the execution of each command, which is not ideal if the user is working on limited internet connection. Your team decided that the application should instead save the changes to a temporary local backup file first, and only upload to the cloud after the user closes the application. Your job is to implement a backup API for the address book storage. - -[TIP] -Do take a look at <> before attempting to modify the `Storage` component. +Use case ends. -. Add a new method `backupAddressBook(ReadOnlyAddressBook)`, so that the address book can be saved in a fixed temporary location. +* 4a. There are no incomplete Modules and current CAP is lesser than CAP Goal +** 4a1. Go to step `5a` + -**** -* Hint -** Add the API method in link:{repoURL}/src/main/java/seedu/address/storage/AddressBookStorage.java[`AddressBookStorage`] interface. -** Implement the logic in link:{repoURL}/src/main/java/seedu/address/storage/StorageManager.java[`StorageManager`] and link:{repoURL}/src/main/java/seedu/address/storage/XmlAddressBookStorage.java[`XmlAddressBookStorage`] class. -* Solution -** See this https://github.com/se-edu/addressbook-level4/pull/594[PR] for the full solution. -**** - -[[GetStartedProgramming-RemarkCommand]] -=== Creating a new command: `remark` - -By creating this command, you will get a chance to learn how to implement a feature end-to-end, touching all major components of the app. - -*Scenario:* You are a software maintainer for `addressbook`, as the former developer team has moved on to new projects. The current users of your application have a list of new feature requests that they hope the software will eventually have. The most popular request is to allow adding additional comments/notes about a particular contact, by providing a flexible `remark` field for each contact, rather than relying on tags alone. After designing the specification for the `remark` command, you are convinced that this feature is worth implementing. Your job is to implement the `remark` command. - -==== Description -Edits the remark for a person specified in the `INDEX`. + -Format: `remark INDEX r/[REMARK]` - -Examples: - -* `remark 1 r/Likes to drink coffee.` + -Edits the remark for the first person to `Likes to drink coffee.` -* `remark 1 r/` + -Removes the remark for the first person. - -==== Step-by-step Instructions - -===== [Step 1] Logic: Teach the app to accept 'remark' which does nothing -Let's start by teaching the application how to parse a `remark` command. We will add the logic of `remark` later. - -**Main:** - -. Add a `RemarkCommand` that extends link:{repoURL}/src/main/java/seedu/address/logic/commands/Command.java[`Command`]. Upon execution, it should just throw an `Exception`. -. Modify link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser`] to accept a `RemarkCommand`. - -**Tests:** - -. Add `RemarkCommandTest` that tests that `execute()` throws an Exception. -. Add new test method to link:{repoURL}/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java[`AddressBookParserTest`], which tests that typing "remark" returns an instance of `RemarkCommand`. - -===== [Step 2] Logic: Teach the app to accept 'remark' arguments -Let's teach the application to parse arguments that our `remark` command will accept. E.g. `1 r/Likes to drink coffee.` - -**Main:** - -. Modify `RemarkCommand` to take in an `Index` and `String` and print those two parameters as the error message. -. Add `RemarkCommandParser` that knows how to parse two arguments, one index and one with prefix 'r/'. -. Modify link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser`] to use the newly implemented `RemarkCommandParser`. - -**Tests:** - -. Modify `RemarkCommandTest` to test the `RemarkCommand#equals()` method. -. Add `RemarkCommandParserTest` that tests different boundary values -for `RemarkCommandParser`. -. Modify link:{repoURL}/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java[`AddressBookParserTest`] to test that the correct command is generated according to the user input. - -===== [Step 3] Ui: Add a placeholder for remark in `PersonCard` -Let's add a placeholder on all our link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`] s to display a remark for each person later. - -**Main:** - -. Add a `Label` with any random text inside link:{repoURL}/src/main/resources/view/PersonListCard.fxml[`PersonListCard.fxml`]. -. Add FXML annotation in link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`] to tie the variable to the actual label. - -**Tests:** - -. Modify link:{repoURL}/src/test/java/guitests/guihandles/PersonCardHandle.java[`PersonCardHandle`] so that future tests can read the contents of the remark label. - -===== [Step 4] Model: Add `Remark` class -We have to properly encapsulate the remark in our link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`] class. Instead of just using a `String`, let's follow the conventional class structure that the codebase already uses by adding a `Remark` class. - -**Main:** - -. Add `Remark` to model component (you can copy from link:{repoURL}/src/main/java/seedu/address/model/person/Address.java[`Address`], remove the regex and change the names accordingly). -. Modify `RemarkCommand` to now take in a `Remark` instead of a `String`. +Use case ends. -**Tests:** +* 5a. CAP goal is not achievable +** 5a1. System inform that it is not achievable ++ +Use case ends. -. Add test for `Remark`, to test the `Remark#equals()` method. -===== [Step 5] Model: Modify `Person` to support a `Remark` field -Now we have the `Remark` class, we need to actually use it inside link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`]. -**Main:** +=== Use case: [UC3] Updating target grades -. Add `getRemark()` in link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`]. -. You may assume that the user will not be able to use the `add` and `edit` commands to modify the remarks field (i.e. the person will be created without a remark). -. Modify link:{repoURL}/src/main/java/seedu/address/model/util/SampleDataUtil.java/[`SampleDataUtil`] to add remarks for the sample data (delete your `addressBook.xml` so that the application will load the sample data when you launch it.) +*Pre-condition:* `[UC2]` completed -===== [Step 6] Storage: Add `Remark` field to `XmlAdaptedPerson` class -We now have `Remark` s for `Person` s, but they will be gone when we exit the application. Let's modify link:{repoURL}/src/main/java/seedu/address/storage/XmlAdaptedPerson.java[`XmlAdaptedPerson`] to include a `Remark` field so that it will be saved. +*MSS* -**Main:** +1. User modify modules entries +2. System recalculates target grades for ungraded modules +3. System displays new target grades for ungraded modules ++ +Use case ends. -. Add a new Xml field for `Remark`. +*Extensions* -**Tests:** +* 2a. CAP goal is not achievable with new set of modules +** 2a1. System inform that it is not achievable ++ +Use case ends. -. Fix `invalidAndValidPersonAddressBook.xml`, `typicalPersonsAddressBook.xml`, `validAddressBook.xml` etc., such that the XML tests will not fail due to a missing `` element. +=== Use case: [UC4] Save entered module -===== [Step 6b] Test: Add withRemark() for `PersonBuilder` -Since `Person` can now have a `Remark`, we should add a helper method to link:{repoURL}/src/test/java/seedu/address/testutil/PersonBuilder.java[`PersonBuilder`], so that users are able to create remarks when building a link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`]. +*MSS* -**Tests:** +1. User enters module +2. System saves the modules ++ +Use case ends. -. Add a new method `withRemark()` for link:{repoURL}/src/test/java/seedu/address/testutil/PersonBuilder.java[`PersonBuilder`]. This method will create a new `Remark` for the person that it is currently building. -. Try and use the method on any sample `Person` in link:{repoURL}/src/test/java/seedu/address/testutil/TypicalPersons.java[`TypicalPersons`]. +=== Use case: [UC5] Loading saved modules -===== [Step 7] Ui: Connect `Remark` field to `PersonCard` -Our remark label in link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`] is still a placeholder. Let's bring it to life by binding it with the actual `remark` field. +*Pre-conditions:* `[UC4]` completed -**Main:** +*MSS* -. Modify link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`]'s constructor to bind the `Remark` field to the `Person` 's remark. +1. User restarts the application +2. User list entered modules +3. System displays saved modules ++ +Use case ends -**Tests:** +=== Use case: [UC6] Adjusting target grades -. Modify link:{repoURL}/src/test/java/seedu/address/ui/testutil/GuiTestAssert.java[`GuiTestAssert#assertCardDisplaysPerson(...)`] so that it will compare the now-functioning remark label. +*Pre-conditions:* -===== [Step 8] Logic: Implement `RemarkCommand#execute()` logic -We now have everything set up... but we still can't modify the remarks. Let's finish it up by adding in actual logic for our `remark` command. +* `[UC2]` completed +* There are targets given to incomplete modules -**Main:** +*MSS* -. Replace the logic in `RemarkCommand#execute()` (that currently just throws an `Exception`), with the actual logic to modify the remarks of a person. +1. User adjust target +2. System recalculates target grades for remaining ungraded modules +3. System displays new target grades for remaining ungraded modules -**Tests:** +*Extensions* -. Update `RemarkCommandTest` to test that the `execute()` logic works. +* 2a. CAP goal is not achievable with new set of modules +** 2a1. System inform that it is not achievable ++ +Use case ends. -==== Full Solution +//end::usecase[] +[appendix] +== Non Functional Requirements -See this https://github.com/se-edu/addressbook-level4/pull/599[PR] for the step-by-step solution. +. Should work on any [mainstream OS](https://github.com/nus-cs2103-AY1819S1/addressbook-level4/blob/master/docs/DeveloperGuide.adoc#mainstream-os) as long as it has Java 10 or higher installed. +. Should be able to hold up to 100 modules 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. +. Should calculate prediction/expected CAP in 1 seconds [appendix] -== Product Scope - -*Target user profile*: +== Glossary -* has a need to manage a significant number of contacts -* prefer desktop apps over other types -* can type fast -* prefers typing over mouse input -* is reasonably comfortable using CLI apps +[[mainstream-os]] Mainstream OS:: +Windows, Linux, Unix, OS-X -*Value proposition*: manage contacts faster than a typical mouse/GUI driven app +[[private-contact-detail]] Private contact detail:: +A contact detail that is not meant to be shared with others [appendix] -== User Stories +== Instructions for Manual Testing -Priorities: High (must have) - `* * \*`, Medium (nice to have) - `* \*`, Low (unlikely to have) - `*` +Given below are instructions to test the app manually. -[width="59%",cols="22%,<23%,<25%,<30%",options="header",] -|======================================================================= -|Priority |As a ... |I want to ... |So that I can... -|`* * *` |new user |see usage instructions |refer to instructions when I forget how to use the App +[NOTE] +These instructions only provide a starting point for testers to work on; testers are expected to do more _exploratory_ testing. -|`* * *` |user |add a new person | +//tag::manualteststorage[] -|`* * *` |user |delete a person |remove entries that I no longer need +=== Launch and Shutdown -|`* * *` |user |find a person by name |locate details of persons without having to go through the entire list +. Initial launch -|`* *` |user |hide <> by default |minimize chance of someone else seeing them by accident +.. 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. -|`*` |user with many persons in the address book |sort persons by name |locate a person easily -|======================================================================= +. Saving window preferences -_{More to be added}_ +.. 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. -[appendix] -== Use Cases +. Saving transcript data -(For all use cases below, the *System* is the `AddressBook` and the *Actor* is the `user`, unless specified otherwise) +.. Add some modules, set a CAP goal, and close the app. +.. Re-launch the app by double-clicking the jar file. + + Expected: The added modules and CAP goal are retained. -[discrete] -=== Use case: Delete person +. Changing the transcript data file path +.. Add some modules, set a CAP goal, and close the app. +.. Change the value of `transcriptFilePath` in the file `data/preferences.json` (found in the same directory as the jar file) to some non-existent file such as `data/transcript_test.json`. +.. Re-launch the app by double-clicking the jar file.+ + Expected: The app will initialize with an empty transcript (no modules or CAP goal). On further change, the app will save the transcript data to the new file `data/transcript_test..json`. -*MSS* +. Importing transcript data +.. Add some modules, set a CAP goal, and close the app. +.. Save another copy of the jar file in another directory and open and close this second app. +.. Replace the `data/transcript.json` of the second app with the `data/transcript.json` from the first app. +.. Relaunch the new app. + + Expected: The app will initialize with the added modules and CAP goal from the first app.+ +//end::manualteststorage[] -1. User requests to list persons -2. AddressBook shows a list of persons -3. User requests to delete a specific person in the list -4. AddressBook deletes the person -+ -Use case ends. +//tag::manualtestcaptargetcalculation[] +=== CAP Calculation -*Extensions* +[NOTE] +To further test the *Correctness* of CAP Calculation you can input your own combination of modules and verify it with +link:https://gradecalc.info/sg/nus/cumulative_gpa_calc.pl[This URL] -[none] -* 2a. The list is empty. +. Initial CAP should be 0 +.. Launch the application +.. Delete any Completed Modules already added + -Use case ends. +|=== +|*Expected*: Current CAP should be 0 +|=== -* 3a. The given index is invalid. -+ -[none] -** 3a1. AddressBook shows an error message. +. CAP should increase/decrease correctly while *Adding* modules +.. CAP score of 1 Module +... Add a module(4MC, Grade B+) with the following command + + `add -m CS1010 -y 1 -s 1 -c 4 -g B+` +... Observe the current CAP + -Use case resumes at step 2. - -_{More to be added}_ - -[appendix] -== Non Functional Requirements - -. Should work on any <> as long as it has Java `9` or higher installed. -. Should be able to hold up to 1000 persons 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. - -_{More to be added}_ - -[appendix] -== Glossary - -[[mainstream-os]] Mainstream OS:: -Windows, Linux, Unix, OS-X - -[[private-contact-detail]] Private contact detail:: -A contact detail that is not meant to be shared with others - -[appendix] -== Product Survey +|=== +|*Expected*: Current CAP should be 4.0 +|=== -*Product Name* +.. CAP score of 4 Module +... Add another module(4MC, Grade A-) with the following command + + `add -m CS1020 -y 1 -s 2 -c 4 -g A-` +... Observe the current CAP ++ +|=== +|*Expected*: Current CAP should be 4.25 +|=== -Author: ... +... Add another module(5MC, Grade C+) with the following command + + `add -m CS2010 -y 2 -s 1 -c 5 -g C+` +... Observe the current CAP ++ +|=== +|*Expected*: Current CAP should be 3.58 +|=== -Pros: +... Add another module(5MC, Grade CS) with the following command + + `add -m CS2020 -y 2 -s 1 -c 5 -g CS` +... Observe the current CAP ++ +|=== +|*Expected*: Current CAP should be 3.58 +|=== -* ... -* ... +. CAP should increase/decrease correctly while *Editing* modules +.. Edit one of the CS module with the following command + + `edit -t CS2020 -g B+` +.. Observe the current CAP ++ +|=== +|*Expected*: Current CAP should be 3.69 +|=== -Cons: +.. Edit one of the 5MC module with the following command + + `edit -t CS2010 -c 4` +.. Observe the current CAP ++ +|=== +|*Expected*: Current CAP should be 3.76 +|=== -* ... -* ... +. CAP should increase/decrease correctly while *Deleting* modules +.. Delete one of the module with the following command + + `delete -t CS2020` +.. Observe the current CAP ++ +|=== +|*Expected*: Current CAP should be 3.67 +|=== -[appendix] -== Instructions for Manual Testing +.. Delete another one of the module with the following command + + `delete -t CS1020` +.. Observe the current CAP ++ +|=== +|*Expected*: Current CAP should be 3.25 +|=== -Given below are instructions to test the app manually. +=== Target Grade Calculation [NOTE] -These instructions only provide a starting point for testers to work on; testers are expected to do more _exploratory_ testing. +_To follow this guide, ensure the there are only the following 2 modules:_ + +Completed Modules: +CS1010 year 1 sem 1 credits 4 grade B+ + + `add -m cs1010 -y 1 -s 1 -c 4 -g B+` + +CS1020 year 2 sem 1 credits 4 grade C+ + + `add -m cs2010 -y 2 -s 1 -c 4 -g C+` -=== Launch and Shutdown +[NOTE] +You can also further verify that the given target grades together with the adjusted grades +and completed grades indeed provide a CAP greater to or equal to your CAP Goal if it is possible +with link:https://gradecalc.info/sg/nus/cumulative_gpa_calc.pl[This URL] + +. Target Grade should not be calculated when there is no CAP Goal +.. Remove any CAP Goal with the following command + + `goal 0` +.. Add 2 incomplete module with the following command (One after another) + + `add -m CS4234 -y 4 -s 1 -c 4` + + `add -m CS4226 -y 4 -s 1 -c 4` ++ +|=== +|*Expected*: Both module should have grade NIL +|=== -. Initial launch +. Target Grade should not update upon entering an impossible CAP Goal +.. Add an impossible CAP Goal with the following command + + `goal 5.0` +.. Observe the CAP Goal field ++ +|=== +|*Expected*: CAP Goal should be `5.0 (Impossible)` and both modules should still have grade NIL +|=== -.. 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. +. Target Grade should update upon entering an achievable CAP Goal +.. Change the CAP Goal to something achievable with the following command + + `goal 4.0` +.. Observe the Target Grades of CS4234 and CS4226 ++ +|=== +|*Expected*: CS4234 -> `A` CS4226 -> `A-` +|=== -. Saving window preferences +. Target Grade should update upon entering an unachievable CAP Goal +.. Change the CAP Goal to something achievable with the following command + + `goal 5.0` +.. Observe the Target Grades of CS4234 and CS4226 ++ +|=== +|*Expected*: both modules should have grade NIL +|=== -.. 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. +. Target Grade should update upon modifying the list of modules +.. Adding another completed module with good grade +... Set a goal to something achievable with the following command + + `goal 4.0` +... Add a module with good grade with the following command: + + `add -m CS2100 -y 1 -s 2 -c 4 -g A` +... Observe that the Target Grades of CS4234 and CS4226 have dropped ++ +|=== +|*Expected*: CS4234 -> `A-` CS4226 -> `B+` +|=== -_{ more test cases ... }_ +.. Adding another completed module with bad grade +... Add a module with bad grade with the following command: + + `add -m CS2105 -y 2 -s 1 -c 4 -g C+` +... Observe that the Target Grades of CS4234 and CS4226 have increased ++ +|=== +|*Expected*: CS4234 -> `A` CS4226 -> `A` +|=== -=== Deleting a person +.. Adding another incomplete +... Add another incomplete module with the following command: + + `add -m CS4231 -y 4 -s 2 -c 4` +... Observe that the Target Grades of CS4234 and CS4226 have dropped ++ +|=== +|*Expected*: CS4234 -> `A` CS4226 -> `A-` CS4231 -> `A-` +|=== -. Deleting a person while all persons are listed +. Adjusting target grade +.. Increasing a target grade should reduce another +... Adjust CS4226 with the following command + + `adjust cs4226 A` +... Observe the Target Grade of CS4234 ++ +|=== +|*Expected*: CS4234 -> `A-` CS4231 -> `A-` +|=== -.. Prerequisites: List all persons using the `list` command. Multiple persons in the list. -.. Test case: `delete 1` + - Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. -.. Test case: `delete 0` + - Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. -.. Other incorrect delete commands to try: `delete`, `delete x` (where x is larger than the list size) _{give more}_ + - Expected: Similar to previous. +.. Decreasing a target grade should reduce another +... Adjust CS4226 with the following command + + `adjust cs4226 A-` +... Observe the Target Grade of CS4234 ++ +|=== +|*Expected*: CS4234 -> `A` CS4231 -> `A-` +|=== -_{ more test cases ... }_ +.. Decreasing a target grade causing goal to be impossible will not recalculate target grade +... Adjust CS4226 with the following command + + `adjust cs4226 C+` +... Observe the CAP Goal field ++ +|=== +|*Expected*: CAP Goal should be `4.0 (Impossible)` CS4234 -> `NIL` CS4231 -> `NIL` +|=== -=== Saving data +. Adjusting multiple modules + +.. Adjusting all modules to grades above what was target will be ok + `adjust cs4226 A` + + `adjust cs4234 A` + + `adjust cs4231 A` +.. Observe the CAP Goal field ++ +|=== +|*Expected*: CAP Goal should still be 4.0 +|=== -. Dealing with missing/corrupted data files +.. Adjusting all modules to grades below what was target will result in impossible CAP Goal + `c_adjust cs4226 A-` + + `c_adjust cs4234 A-` + + `c_adjust cs4231 A-` +.. Observe the CAP Goal field ++ +|=== +|*Expected*: CAP Goal should be `4.0 (Impossible)` +|=== -.. _{explain how to simulate a missing/corrupted file and the expected behavior}_ +//end::manualtestcaptargetcalculation[] -_{ more test cases ... }_ diff --git a/docs/LearningOutcomes.adoc b/docs/LearningOutcomes.adoc deleted file mode 100644 index 83cda0927226..000000000000 --- a/docs/LearningOutcomes.adoc +++ /dev/null @@ -1,266 +0,0 @@ -= Learning Outcomes -:site-section: LearningOutcomes -:toc: macro -:toc-title: -:toclevels: 1 -:sectnums: -:sectnumlevels: 1 -:imagesDir: images -:stylesDir: stylesheets -:repoURL: https://github.com/se-edu/addressbook-level4/tree/master - -After studying this code and completing the corresponding exercises, you should be able to, - -toc::[] - -''' - -== Use High-Level Designs `[LO-HighLevelDesign]` - -Note how the <> describes the high-level design using an _Architecture Diagrams_ and high-level sequence diagrams. - -*Resources* - -* https://se-edu.github.io/se-book/architecture/[se-edu/se-book: Design: Architecture] -* https://se-edu.github.io/se-book/design/introduction/multilevelDesign/[se-edu/se-book: Design: Introduction: Multi-Level Design] - -''' - -== Use Event-Driven Programming `[LO-EventDriven]` - -Note how the <> uses events to communicate with components without needing a direct coupling. Also note how the link:{repoURL}/src/main/java/seedu/address/commons/core/index/EventsCenter.java[`EventsCenter.java`] acts as an event dispatcher to facilitate communication between event creators and event consumers. - -*Resources* - -* https://se-edu.github.io/se-book/architecture/architecturalStyles/eventDriven/[se-edu/se-book: Design: Architecture: Architecture Styles: Event-Driven Architectural Style] - -''' - -== Use API Design `[LO-ApiDesign]` - -Note how components of AddressBook have well-defined APIs. For example, the API of the `Logic` component is given in the link:{repoURL}/src/main/java/seedu/address/logic/Logic.java[`Logic.java`] -image:LogicClassDiagram.png[width="800"] - -*Resources* - -* https://se-edu.github.io/se-book/reuse/apis/[se-edu/se-book: Implementation: Reuse: APIs] - -''' - -== Use Assertions `[LO-Assertions]` - -Note how the AddressBook app uses Java ``assert``s to verify assumptions. - -*Resources* - -* https://se-edu.github.io/se-book/errorHandling/assertions/[se-edu/se-book: Implementation: Error Handling: Assertions] - -=== Exercise: Add more assertions - -* Make sure assertions are enabled in your IDE by forcing an assertion failure (e.g. add `assert false;` somewhere in the code and run the code to ensure the runtime reports an assertion failure). -* Add more assertions to AddressBook as you see fit. - - -''' - -== Use Logging `[LO-Logging]` - -Note <>. - -*Resources* - -* https://se-edu.github.io/se-book/errorHandling/logging/[se-edu/se-book: Implementation: Error Handling: Logging] - -=== Exercise: Add more logging - -Add more logging to AddressBook as you see fit. - - -''' - -== Use Defensive Coding `[LO-DefensiveCoding]` - -Note how AddressBook uses the `ReadOnly*` interfaces to prevent objects being modified by clients who are not supposed to modify them. - -*Resources* - -* https://se-edu.github.io/se-book/errorHandling/defensiveProgramming/[se-edu/se-book: Implementation: Error Handling: Defensive Programming] - -=== Exercise: identify more places for defensive coding - -Analyze the AddressBook code/design to identify, - -* where defensive coding is used -* where the code can be more defensive - -''' - -== Use Build Automation `[LO-BuildAutomation]` - -Note <>. - -*Resources* - -* https://se-edu.github.io/se-book/integration/buildAutomation/what/[se-edu/se-book: Implementation: Integration: Build Automation: What] - -=== Exercise: Use gradle to run tasks - -* Use gradle to do these tasks: Run all tests in headless mode, build the jar file. - -=== Exercise: Use gradle to manage dependencies - -* Note how the build script `build.gradle` file manages third party dependencies such as ControlsFx. Update that file to manage a third-party library dependency. - - -''' - -== Use Continuous Integration `[LO-ContinuousIntegration]` - -Note <>. (https://travis-ci.org/se-edu/addressbook-level4[image:https://travis-ci.org/se-edu/addressbook-level4.svg?branch=master[Build Status]]) - -*Resources* - -* https://se-edu.github.io/se-book/integration/buildAutomation/continuousIntegrationDeployment/[se-edu/se-book: Implementation: Integration: Build Automation: CI & CD] - -=== Exercise: Use Travis in your own project - -* Set up Travis to perform CI on your own fork. - - -''' - -== Use Code Coverage `[LO-CodeCoverage]` - -Note how our CI server <>. (https://coveralls.io/github/se-edu/addressbook-level4?branch=master[image:https://coveralls.io/repos/github/se-edu/addressbook-level4/badge.svg?branch=master[Coverage Status]]) After <> for your project, you can visit Coveralls website to find details about the coverage of code pushed to your repo. https://coveralls.io/github/se-edu/addressbook-level4?branch=master[Here] is an example. - -*Resources* - -* https://se-edu.github.io/se-book/testing/testCoverage/[se-edu/se-book: QA: Testing: Test Coverage] - -=== Exercise: Use the IDE to measure coverage locally - -* Use the IDE to measure code coverage of your tests. - -''' - -== Apply Test Case Design Heuristics `[LO-TestCaseDesignHeuristics]` - -The link:{repoURL}/src/test/java/seedu/address/commons/util/StringUtilTest.java[`StringUtilTest.java`] -class gives some examples of how to use _Equivalence Partitions_, _Boundary Value Analysis_, and _Test Input Combination Heuristics_ to improve the efficiency and effectiveness of test cases testing the link:../src/main/java/seedu/address/commons/util/StringUtil.java[`StringUtil.java`] class. - -*Resources* - -* https://se-edu.github.io/se-book/testCaseDesign/[se-edu/se-book: QA: Test Case Design] - -=== Exercise: Apply Test Case Design Heuristics to other places - -* Use the test case design heuristics mentioned above to improve test cases in other places. - -''' - -== Write Integration Tests `[LO-IntegrationTests]` - -Consider the link:{repoURL}/src/test/java/seedu/address/storage/StorageManagerTest.java[`StorageManagerTest.java`] class. - -* Test methods `prefsReadSave()` and `addressBookReadSave()` are integration tests. Note how they simply test if The `StorageManager` class is correctly wired to its dependencies. -* Test method `handleAddressBookChangedEvent_exceptionThrown_eventRaised()` is a unit test because it uses _dependency injection_ to isolate the SUT `StorageManager#handleAddressBookChangedEvent(...)` from its dependencies. - -Compare the above with link:{repoURL}/src/test/java/seedu/address/logic/LogicManagerTest.java[`LogicManagerTest`]. Some of the tests in that class (e.g. `execute_*` methods) are neither integration nor unit tests. They are _integration + unit_ tests because they not only check if the LogicManager is correctly wired to its dependencies, but also checks the working of its dependencies. For example, the following two lines test the `LogicManager` but also the `Parser`. - -[source,java] ----- -@Test -public void execute_invalidCommandFormat_throwsParseException() { - ... - assertParseException(invalidCommand, MESSAGE_UNKNOWN_COMMAND); - assertHistoryCorrect(invalidCommand); -} ----- - -*Resources* - -* https://se-edu.github.io/se-book/testing/testingTypes/[se-edu/se-book: QA: Testing: Testing Types] - -=== Exercise: Write unit and integration tests for the same method. - -* Write a unit test for a high-level method somewhere in the code base (or a new method you wrote). -* Write an integration test for the same method. - -''' - -== Write System Tests `[LO-SystemTesting]` - -Note how tests below `src/test/java/systemtests` package (e.g link:{repoURL}/src/test/java/systemtests/AddCommandSystemTest.java[`AddCommandSystemTest.java`]) are system tests because they test the entire system end-to-end. - -*Resources* - -* https://se-edu.github.io/se-book/testing/testingTypes/[se-edu/se-book: QA: Testing: Testing Types] - -=== Exercise: Write more system tests - -* Write system tests for the new features you add. - -''' - -== Automate GUI Testing `[LO-AutomateGuiTesting]` - -Note how this project uses TextFX library to automate GUI testing, including <>. - -=== Exercise: Write more automated GUI tests - -* Covered by `[LO-SystemTesting]` - -''' - -== Apply Design Patterns `[LO-DesignPatterns]` - -Here are some example design patterns used in the code base. - -* *Singleton Pattern* : link:{repoURL}/src/main/java/seedu/address/commons/core/EventsCenter.java[`EventsCenter.java`] is Singleton class. Its single instance can be accessed using the `EventsCenter.getInstance()` method. -* *Facade Pattern* : link:{repoURL}/src/main/java/seedu/address/storage/StorageManager.java[`StorageManager.java`] is not only shielding the internals of the Storage component from outsiders, it is mostly redirecting method calls to its internal components (i.e. minimal logic in the class itself). Therefore, `StorageManager` can be considered a Facade class. -* *Command Pattern* : The link:{repoURL}/src/main/java/seedu/address/logic/commands/Command.java[`Command.java`] and its sub classes implement the Command Pattern. -* *Observer Pattern* : The <> used by this code base employs the Observer pattern. For example, objects that are interested in events need to have the `@Subscribe` annotation in the class (this is similar to implementing an `\<>` interface) and register with the `EventsCenter`. When something noteworthy happens, an event is raised and the `EventsCenter` notifies all relevant subscribers. Unlike in the Observer pattern in which the `\<>` class is notifying all `\<>` objects, here the `\<>` classes simply raises an event and the `EventsCenter` takes care of the notifications. -* *MVC Pattern* : -** The 'View' part of the application is mostly in the `.fxml` files in the `src/main/resources/view` folder. -** `Model` component contains the 'Model'. However, note that it is possible to view the `Logic` as the model because it hides the `Model` behind it and the view has to go through the `Logic` to access the `Model`. -** Sub classes of link:{repoURL}/src/main/java/seedu/address/ui/UiPart.java[`UiPart`] (e.g. `PersonListPanel` ) act as 'Controllers', each controlling some part of the UI and communicating with the 'Model' (via the `Logic` component which sits between the 'Controller' and the 'Model'). -* *Abstraction Occurrence Pattern* : Not currently used in the app. - -*Resources* - -* https://se-edu.github.io/se-book/designPatterns/[se-edu/se-book: Design: Design Patterns] - -=== Exercise: Discover other possible applications of the patterns - -* Find other possible applications of the patterns to improve the current design. e.g. where else in the design can you apply the Singleton pattern? -* Discuss pros and cons of applying the pattern in each of the situations you found in the previous step. - -=== Exercise: Find more applicable patterns - -* Learn other _Gang of Four_ Design patterns to see if they are applicable to the app. - -''' - -== Use Static Analysis `[LO-StaticAnalysis]` - -Note how this project uses the http://checkstyle.sourceforge.net/[CheckStyle] static analysis tool to confirm compliance with the coding standard. - -*Resources* - -* https://se-edu.github.io/se-book/qualityAssurance/staticAnalysis/[se-edu/se-book: QA: Static Analysis] - -=== Exercise: Use CheckStyle locally to check style compliance - -* Install the CheckStyle plugin for your IDE and use it to check compliance of your code with our style rules (given in `/config/checkstyle/checkstyle.xml`). - -''' - -== Do Code Reviews `[LO-CodeReview]` - -* Note how some PRs in this project have been reviewed by other developers. Here is an https://github.com/se-edu/addressbook-level4/pull/147[example]. -* Also note how we have used https://www.codacy.com[Codacy] to do automate some part of the code review workload (https://www.codacy.com/app/damith/addressbook-level4?utm_source=github.com&utm_medium=referral&utm_content=se-edu/addressbook-level4&utm_campaign=Badge_Grade[image:https://api.codacy.com/project/badge/Grade/fc0b7775cf7f4fdeaf08776f3d8e364a[Codacy Badge]]) - - -=== Exercise: Review a PR - -* Review PRs created by team members. diff --git a/docs/UserGuide.adoc b/docs/UserGuide.adoc index 7e0070e12f49..8c57f862bcfd 100644 --- a/docs/UserGuide.adoc +++ b/docs/UserGuide.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 4 - User Guide += CAPTracker - User Guide :site-section: UserGuide :toc: :toc-title: @@ -12,249 +12,366 @@ ifdef::env-github[] :tip-caption: :bulb: :note-caption: :information_source: endif::[] -:repoURL: https://github.com/se-edu/addressbook-level4 +:repoURL: https://github.com/CS2103-AY1819S1-T13-4/main/tree/master -By: `Team SE-EDU` Since: `Jun 2016` Licence: `MIT` +By: `T13-4` Since: `Aug 2018` Licence: `MIT` == Introduction -AddressBook Level 4 (AB4) is for those who *prefer to use a desktop app for managing contacts*. More importantly, AB4 is *optimized for those who prefer to work with a Command Line Interface* (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, AB4 can get your contact management tasks done faster than traditional GUI apps. Interested? Jump to the <> to get started. Enjoy! +CAPTracker is a desktop app that allows NUS students to track their current CAP (Cumulative Average Point), set a CAP goal, and calculate target grades needed to achieve their goal.+ +CAPTracker is optimized for users who are familiar with CLI (Command Line Interface) for input control while still having the benefits of a Graphical User Interface (GUI) for displaying information.+ +If you can type fast, CAPTracker is the ideal application for monitoring your current CAP, and predicting grades needed to achieve your ideal CAP for modules that hasn't been taken. Interested? Jump to the <> to get started. Enjoy! == Quick Start -. Ensure you have Java version `9` or later installed in your Computer. -. Download the latest `addressbook.jar` link:{repoURL}/releases[here]. -. Copy the file to the folder you want to use as the home folder for your Address Book. -. Double-click the file to start the app. The GUI should appear in a few seconds. +. Ensure you have Java version `10` or later installed in your Computer. +. Download the latest `captracker.jar` link:{repoURL}/releases[here]. +. Copy the file to the folder you would want to use as the home folder for +your CAP Tracker. +. Double-click the file to start the app. The GUI should appear in a few +seconds. + image::Ui.png[width="790"] + . Type the command in the command box and press kbd:[Enter] to execute it. + -e.g. typing *`help`* and pressing kbd:[Enter] will open the help window. -. Some example commands you can try: +e.g. Typing *`help`* and pressing kbd:[Enter] will open the help window. +. Some commands you can try: -* *`list`* : lists all contacts -* **`add`**`n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : adds a contact named `John Doe` to the Address Book. -* **`delete`**`3` : deletes the 3rd contact shown in the current list -* *`exit`* : exits the app +* `add -m CS2103 -y 2 -s 1 -c 4 -g A+` : Adds a module `CS2103` worth +`4` MCs taken in Semester `1` as a Year `2` student. +* **`goal 4.5`** : Sets a CAP goal of 4.5. +* **`delete -t CS2103 -e 2 -z 1` : Deletes `CS2103` taken in year `2` semester +* `delete -t CS2103 -e 2 -z 1` : Deletes `CS2103` taken in year `2` semester +`1` from CAPTracker. +* *`exit`* : Exits the app . Refer to <> for details of each command. [[Features]] == Features +//tag::featureoverview[] + +=== Feature overview +In the CAPTracker app, you can add modules that you've completed and it will display your current CAP. A module consists of a code, year, semester, number of credits, and an associated grade. + +You may also add incomplete modules (i.e. modules that you are currently enrolled in, or plan to enroll in eventually, but you have not received your grade yet) simply by adding that module and omitting the grade.+ +You may also set a CAP goal, and the app will automatically generate the target grades necessary for your incomplete modules so that you can achieve your goal. The app will also let you know if your goal is too high based on the number of incomplete credits you have and your current CAP. + +Lastly, you can also adjust the target grade of a particular incomplete module (for example, a module you know you can get an A in) and the app will automatically recalculate the required grades for the rest of the modules (so you can afford to score lower on those modules). + +Happy tracking! +//end::featureoverview[] + ==== *Command Format* -* Words in `UPPER_CASE` are the parameters to be supplied by the user e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. -* Items in square brackets are optional e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`. -* Items with `…`​ after them can be used multiple times including zero times e.g. `[t/TAG]...` can be used as `{nbsp}` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. -* Parameters can be in any order e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. +* Words in `UPPER_CASE` are the parameters to be replaced and supplied by the +user. + +e.g. `add -m MODULE_CODE -y YEAR -s SEMESTER -c CREDIT [-g GRADE]`: + +`add -m CS2103 -y 2 -s 1 -c 4 -g A+`. +* Items in square brackets are optional. + +e.g. `add -m MODULE_CODE -y YEAR -s SEMESTER -c CREDIT [-g GRADE]`: + +`add -m CS2103 -y 2 -s 1 -c 4 -g A+` or + +`add -m CS2103 -y 2 -s 1 -c 4` ==== === Viewing help : `help` Format: `help` -=== Adding a person: `add` - -Adds a person to the address book + -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...` - -[TIP] -A person can have any number of tags (including 0) - -Examples: +//tag::add[] -* `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` -* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal` +=== Adding a module: `add` -=== Listing all persons : `list` +Adds a `*particular*` module entry to the _CAPTracker_. -Shows a list of all persons in the address book. + -Format: `list` -=== Editing a person : `edit` +*Case 1:* + +Add `incomplete` (not graded yet) module. -Edits an existing person in the address book. + -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]...` +_Format:_ + +---- + add -m CODE -y YEAR -s SEMESTER -c CREDIT +---- + +*Case 2:* + +Add `completed` (already graded) module. + +_Format:_ + +---- + add -m CODE -y YEAR -s SEMESTER -c CREDIT -g GRADE +---- + +*Examples*: -**** -* Edits the person at the specified `INDEX`. The index refers to the index number shown in the displayed person list. The index *must be a positive integer* 1, 2, 3, ... -* At least one of the optional fields must be provided. -* Existing values will be updated to the input values. -* When editing tags, the existing tags of the person will be removed i.e adding of tags is not cumulative. -* You can remove all the person's tags by typing `t/` without specifying any tags after it. -**** +Command: `add -m MA1521 -y 1 -s 2 -c 4 -g A` + +Adds a module with: + +* Module code `MA1521` +* Taken in year `1` semester `2` +* Worth `4` module credits +* Graded `A` + +Command: `add -m CFG1010 -y 1 -s 1 -c 2 -g CS` + +Adds a module with: + +* Module code `CFG1010` +* Taken in year `1` semester `1` +* Worth `2` module credits +* Graded `CU` + +Command: `add -m CS2103 -y 2 -s 1 -c 4` + +Adds a module with: + +* Module code `CFG1010` +* Taken in year `2` semester `1` +* Worth `4` module credits +* Not completed yet + +IMPORTANT: - Arguments must be in name-value pair format (-name value) + +- Illegal name or value is not allowed + +- `CODE` has to be specified + +- `YEAR` has to be specified + +- `SEMESTER` has to be specified + +- `CREDIT` has to be specified + +- `GRADE` has to be specified if it is completed + +- Module should not exist in CAPTracker +//end::add[] + +//tag::edit[] +=== Editing a module : `edit` + +Edits fields of a `*particular*` module entry in the CAPTracker. + +*Case 1:* + +Only `one` module entry have the specified target module code. + +_Pretty Print Format:_ + +---- +edit -t TARGET_CODE + [-m NEW_CODE ] + [-y NEW_YEAR ] + [-s NEW_SEMESTER] + [-c NEW_CREDIT ] + [-g NEW_GRADE ] +---- + +*Case 2:* + +`Two or more` module entries has the specified target module code. (E.g. Retook +the module) + +_Pretty Print Format:_ + +---- +edit -t TARGET_CODE -e TARGET_YEAR -z TARGET_SEMESTER + [-m NEW_CODE ] + [-y NEW_YEAR ] + [-s NEW_SEMESTER] + [-c NEW_CREDIT ] + [-g NEW_GRADE ] +---- + +*Examples*: + +*Command*: `edit -t MA1521 -g A+` + +Change grade of `MA1521` to `A+`. + +*Command*: `edit -t CFG1010 -m ST2334 -c 4` + +Change module credit to `4` and module code to `ST2334`. +//end::edit[] + +*Command*: `edit -t CFG1020 -e 2 -z 1 -g CS` + +Change the grade of `CFG1020` taken in year `2` and semester `1` to 1. + +In this specific case, `CFG1020` was retaken and there exist multiple entries +of it. + +IMPORTANT: - Arguments must be in name-value pair format (E.g. `-name value`) + +- Illegal name or value is not allowed + +- `TARGET_CODE` has to be specified + +- `TARGET_YEAR` is not specified if and only if `TARGET_SEMESTER` is also not +specified + +- At least one new value has to be specified + +- The targeted module entry should exist in the CAPTracker + +- `TARGET_YEAR` and `TARGET_SEMESTER` must be specified if there exist multiple +entries with the same module `TARGET_CODE` + +- The edit cannot lead to two module entries sharing the same module code, +year, and semester + +//tag::delete[] +=== Deleting a module : `delete` + +Deletes a `*particular*` module entry in the CAPTracker. + +*Case 1:* + +Only `one` module entry have the specified target module code. + +_Format:_ + +---- +delete -t TARGET_CODE +---- + +*Case 2:* + +`Two or more` module entries has the specified target module code. (E.g. Retook +the module) + +_Format:_ + +---- +delete -t TARGET_CODE -e TARGET_YEAR -z TARGET_SEMESTER +---- Examples: -* `edit 1 p/91234567 e/johndoe@example.com` + -Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively. -* `edit 2 n/Betsy Crower t/` + -Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. - -=== Locating persons by name: `find` - -Finds persons whose names contain any of the given keywords. + -Format: `find KEYWORD [MORE_KEYWORDS]` -**** -* The search is case insensitive. e.g `hans` will match `Hans` -* The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans` -* Only the name is searched. -* Only full words will be matched e.g. `Han` will not match `Hans` -* Persons matching at least one keyword will be returned (i.e. `OR` search). e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` -**** +*Command*: `delete -t CS2103` + +Deletes the only `CS2103` module. -Examples: +*Command*: `delete -t CS2103 -e 3 -z 2` + +Deletes `CS2103` taken in year `3` semester `2`. + +In this specific case, `CS2103` was retaken and there exist multiple entries +of it. +//end::delete[] -* `find John` + -Returns `john` and `John Doe` -* `find Betsy Tim John` + -Returns any person having names `Betsy`, `Tim`, or `John` +IMPORTANT: - Arguments must be in name-value pair format (E.g. `-name value`) + +- Illegal name or value is not allowed + +- `TARGET_CODE` has to be specified + +- `TARGET_YEAR` is not specified if and only if `TARGET_SEMESTER` is also not +specified + +- The targeted module entry should exist in the CAPTracker + +- `TARGET_YEAR` and `TARGET_SEMESTER` of the targeted entry must be specified +if there exist multiple entries with the same module `TARGET_CODE`. -=== Deleting a person : `delete` +//tag::commandgoal[] +=== Setting Cap Goal : `goal` -Deletes the specified person from the address book. + -Format: `delete INDEX` +Set the CAP goal you want to achieve. + +Format: `goal CAP_GOAL` **** -* Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. -* The index *must be a positive integer* 1, 2, 3, ... +* Sets and updates the CAP goal. **** Examples: -* `list` + -`delete 2` + -Deletes the 2nd person in the address book. -* `find Betsy` + -`delete 1` + -Deletes the 1st person in the results of the `find` command. - -=== Selecting a person : `select` - -Selects the person identified by the index number used in the displayed person list. + -Format: `select INDEX` - -**** -* Selects the person and loads the Google search page the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. -* The index *must be a positive integer* `1, 2, 3, ...` -**** - -Examples: +* `goal 4.5` + +Update your CAP goal to 4.5 +//end::commandgoal[] +//tag::commandadjust[] -* `list` + -`select 2` + -Selects the 2nd person in the address book. -* `find Betsy` + -`select 1` + -Selects the 1st person in the results of the `find` command. +=== Adjusting target goals: `adjust` -=== Listing entered commands : `history` +[NOTE] +Removal of adjustment will be made available in v1.5. -Lists all the commands that you have entered in reverse chronological order. + -Format: `history` +Adjust the grade of an incomplete module + +Format: -[NOTE] -==== -Pressing the kbd:[↑] and kbd:[↓] arrows will display the previous and next input respectively in the command box. -==== +* *Module code is unique*: `adjust MODULE_CODE GRADE` +* *Otherwise*: `adjust MODULE_CODE YEAR SEM GRADE` -// tag::undoredo[] -=== Undoing previous command : `undo` +Examples: -Restores the address book to the state before the previous _undoable_ command was executed. + -Format: `undo` +* `adjust CS2103 A` + +Adjusts the grade with module code CS2103 to have grade A -[NOTE] -==== -Undoable commands: those commands that modify the address book's content (`add`, `delete`, `edit` and `clear`). -==== +* `adjust CS2103 1 1 A` + +Adjusts the grade with module code CS2103 taken in year 1 sem 1 to have grade A -Examples: +//end::commandadjust[] -* `delete 1` + -`list` + -`undo` (reverses the `delete 1` command) + +=== Exiting the program : `exit` -* `select 1` + -`list` + -`undo` + -The `undo` command fails as there are no undoable commands executed previously. +Exits the program. + +Format: `exit` -* `delete 1` + -`clear` + -`undo` (reverses the `clear` command) + -`undo` (reverses the `delete 1` command) + +//tag::savingdata[] -=== Redoing the previously undone command : `redo` +=== Saving your module data +Your module information and CAP goal are automatically saved on the hard disk as a JSON file (by default `data/transcript.json`) whenever there are changes to the modules or CAP goal. The data is automatically loaded into the app again upon startup.+ +You may manually change the location to store your data by changing the value of `transcriptFilePath` in the file `data/preferences.json` (found in the same directory as the CapTracker jar file). + +You may load your module data in the CAPTracker app on another computer or share it with other users if desired, simply by sharing the module data file generated by your instance of the app. The value of `transcriptFilePath` in the preferences file will have to be changed to the location of your own data file. +Note: Do not modify the JSON module data file manually, or you may lose your data. +//end::savingdata[] -Reverses the most recent `undo` command. + -Format: `redo` +=== Understanding the User Interface +Understanding the User Interface can be tricky - what do all the different colours mean? How do I +know what has been saved or not? Where can I see new modules I've added? -Examples: +* To view new modules you've added, scroll down to the bottom of the pannel that you have categorized your +module under; either the 'Completed Modules' panel on the left, or the 'Incomplete Modules' panel on the +right. Your new entry should be at the bottom of these lists. -* `delete 1` + -`undo` (reverses the `delete 1` command) + -`redo` (reapplies the `delete 1` command) + +* Understanding the colours. The grades of modules in the 'Completed Modules' panel on the left +are circled in GREEN. This indicates that this particular module has already been taken and this is +a grade that the user does not need to worry about; it is in the past. +The grades in the the 'Incomplete Modules' panel on the right are circled in RED. This indicates +that this particular module has not been taken and that the grade displayed in this RED circle is +not certain. It is a grade that the user needs to be aware of as it's outcome will impact the +users overall CAP score. -* `delete 1` + -`redo` + -The `redo` command fails as there are no `undo` commands executed previously. +== FAQ -* `delete 1` + -`clear` + -`undo` (reverses the `clear` command) + -`undo` (reverses the `delete 1` command) + -`redo` (reapplies the `delete 1` command) + -`redo` (reapplies the `clear` command) + -// end::undoredo[] +*Q*: I entered the wrong grade into my module. How do I change it? + +*A*: Use the `edit` command to input the correct information for the module + +`edit -t MODULE_CODE -g ACTUAL_GRADE` +//tag::ambersFAQ[] +*Q*: I entered the year I took my module, 2018, and it doesn't work. Why not? -=== Clearing all entries : `clear` +*A*: The year of the module in CAPTracker doesn't refer to the calander year, but +instead the year that you are studying at. For example, a module taken in your +second year of study would have a year value of 2. -Clears all entries from the address book. + -Format: `clear` -=== Exiting the program : `exit` +*Q*: I entered a new module but can't see it in the app. Where is it? -Exits the program. + -Format: `exit` +*A*: New modules you have added will appear at the bottom of the list in either +the 'Completed Modules' list or the 'Incomplete Modules' list depending on your +specification. Scroll down to the bottom of these lists to find your new module; +it may not appear without you scrolling if there are already a number of mdoules +entered! -=== Saving the data -Address book data are saved in the hard disk automatically after any command that changes the data. + -There is no need to save manually. +*Q*: Why does my CAP goal say 'Impossible'? -// tag::dataencryption[] -=== Encrypting data files `[coming in v2.0]` +*A*: Whether or not the users CAP goal is achievable is based on the grades of +completed modules and the number of incomplete modules the user has entered. If +the completed modules do not have sufficient grades to meet the CAP goal, try to +add some modules you are planning to take; it may be that you need an A+ is four +other modules before your CAP goal can be achieved. It is also based on the +adjustments made by the user. For example, if the user originally enters -_{explain how the user can enable/disable data encryption}_ -// end::dataencryption[] -== FAQ +*Q*: I made the wrong adjustment to a module...how do I remove the adjustment +I made? -*Q*: How do I transfer my data to another Computer? + -*A*: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous Address Book folder. +*A*: To remove an incorrect adjustment, you need to delete that module (see +delete command chapter 3.4) and add it back in with the desired/correct +adjustments. A direct solution will be implemented in v2.0 +//end::ambersFAQ[] + <> +v1.0, 2018-04-11 +:toc: +:imagesdir: assets/images +:homepage: http://asciidoctor.org == Command Summary -* *Add* `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...` + -e.g. `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` -* *Clear* : `clear` -* *Delete* : `delete INDEX` + -e.g. `delete 3` -* *Edit* : `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]...` + -e.g. `edit 2 n/James Lee e/jameslee@example.com` -* *Find* : `find KEYWORD [MORE_KEYWORDS]` + -e.g. `find James Jake` -* *List* : `list` +* *Add* + +`add -m MODULE_CODE -y YEAR -s SEMESTER -c CREDIT [-g GRADE]` + +e.g. `add -m CS2103 -y 2 -s 1 -c 4 -g A+` +* *Edit* : + +`edit -t TARGET_MODULE_CODE [-e TARGET_YEAR -z TARGET_SEMESTER] +[-m MODULE_CODE] +[-y YEAR] +[-s SEMESTER] +[-c CREDIT] +[-g GRADE]` + +e.g. `edit -t CS2103 -g A+` +* *Delete* : + +`delete -t MODULE_CODE [-e TARGET_YEAR -z TARGET_SEMESTER]` + +e.g. `delete -t CS2103` +* *Goal* : `goal CAP_GOAL` + +e.g. `goal 4.5` +* *Adjust* : `adjust MODULE_CODE GRADE` + +e.g. `adjust CS2103 A` + +or + +`adjust MODULE_CODE YEAR SEM GRADE` + +e.g. `adjust CS2103 1 1 A` * *Help* : `help` -* *Select* : `select INDEX` + -e.g.`select 2` -* *History* : `history` -* *Undo* : `undo` -* *Redo* : `redo` +* *Exit* : `exit` diff --git a/docs/diagrams/ArchitectureDiagram.pptx b/docs/diagrams/ArchitectureDiagram.pptx index b0e5a9d0ff55..bf05179d81b9 100644 Binary files a/docs/diagrams/ArchitectureDiagram.pptx and b/docs/diagrams/ArchitectureDiagram.pptx differ diff --git a/docs/diagrams/HighLevelSequenceDiagrams.pptx b/docs/diagrams/HighLevelSequenceDiagrams.pptx index 38332090a79a..14cb5a374ef5 100644 Binary files a/docs/diagrams/HighLevelSequenceDiagrams.pptx and b/docs/diagrams/HighLevelSequenceDiagrams.pptx differ diff --git a/docs/diagrams/LogicComponentClassDiagram.pptx b/docs/diagrams/LogicComponentClassDiagram.pptx index 6fcc1136a5bb..e3d9b2e14417 100644 Binary files a/docs/diagrams/LogicComponentClassDiagram.pptx and b/docs/diagrams/LogicComponentClassDiagram.pptx differ diff --git a/docs/diagrams/LogicComponentSequenceDiagram.pptx b/docs/diagrams/LogicComponentSequenceDiagram.pptx index c5b6d5fad6e3..183e5080a9d9 100644 Binary files a/docs/diagrams/LogicComponentSequenceDiagram.pptx and b/docs/diagrams/LogicComponentSequenceDiagram.pptx differ diff --git a/docs/diagrams/StorageComponentClassDiagram.pptx b/docs/diagrams/StorageComponentClassDiagram.pptx index be29a9de7ca6..84f47d4eede8 100644 Binary files a/docs/diagrams/StorageComponentClassDiagram.pptx and b/docs/diagrams/StorageComponentClassDiagram.pptx differ diff --git a/docs/diagrams/TranscriptCapAndTargetSequenceDiagram.pptx b/docs/diagrams/TranscriptCapAndTargetSequenceDiagram.pptx new file mode 100644 index 000000000000..c4ec2e1077ac Binary files /dev/null and b/docs/diagrams/TranscriptCapAndTargetSequenceDiagram.pptx differ diff --git a/docs/diagrams/UiComponentClassDiagram.pptx b/docs/diagrams/UiComponentClassDiagram.pptx index 384d0a00e6ea..14d182141054 100644 Binary files a/docs/diagrams/UiComponentClassDiagram.pptx and b/docs/diagrams/UiComponentClassDiagram.pptx differ diff --git a/docs/diagrams/activityDiagramTargetGradeCalculation.pptx b/docs/diagrams/activityDiagramTargetGradeCalculation.pptx new file mode 100644 index 000000000000..f01cc20523f2 Binary files /dev/null and b/docs/diagrams/activityDiagramTargetGradeCalculation.pptx differ diff --git a/docs/images/Architecture.png b/docs/images/Architecture.png index bdc789000f77..04f517a3b859 100644 Binary files a/docs/images/Architecture.png and b/docs/images/Architecture.png differ diff --git a/docs/images/DeleteModuleForLogic.png b/docs/images/DeleteModuleForLogic.png new file mode 100644 index 000000000000..a21c94eb300c Binary files /dev/null and b/docs/images/DeleteModuleForLogic.png differ diff --git a/docs/images/LogicClassDiagram.png b/docs/images/LogicClassDiagram.png index f4ecf65b3193..145bca6c7cfe 100644 Binary files a/docs/images/LogicClassDiagram.png and b/docs/images/LogicClassDiagram.png differ diff --git a/docs/images/ModelClassDiagram_Transcript.png b/docs/images/ModelClassDiagram_Transcript.png new file mode 100644 index 000000000000..69546e47843f Binary files /dev/null and b/docs/images/ModelClassDiagram_Transcript.png differ diff --git a/docs/images/SDTranscriptCalculateCap.png b/docs/images/SDTranscriptCalculateCap.png new file mode 100644 index 000000000000..19f3680510cc Binary files /dev/null and b/docs/images/SDTranscriptCalculateCap.png differ diff --git a/docs/images/SDTranscriptModulesUpdate.png b/docs/images/SDTranscriptModulesUpdate.png new file mode 100644 index 000000000000..6594d05c4b40 Binary files /dev/null and b/docs/images/SDTranscriptModulesUpdate.png differ diff --git a/docs/images/SDTranscriptTargetCalculation.png b/docs/images/SDTranscriptTargetCalculation.png new file mode 100644 index 000000000000..02ec6b95439b Binary files /dev/null and b/docs/images/SDTranscriptTargetCalculation.png differ diff --git a/docs/images/SDforDeleteModule.png b/docs/images/SDforDeleteModule.png new file mode 100644 index 000000000000..53a557739e94 Binary files /dev/null and b/docs/images/SDforDeleteModule.png differ diff --git a/docs/images/SDforDeleteModuleEventHandling.png b/docs/images/SDforDeleteModuleEventHandling.png new file mode 100644 index 000000000000..20d212897326 Binary files /dev/null and b/docs/images/SDforDeleteModuleEventHandling.png differ diff --git a/docs/images/SDforDeletePerson.png b/docs/images/SDforDeletePerson.png deleted file mode 100644 index 1e836f10dcd8..000000000000 Binary files a/docs/images/SDforDeletePerson.png and /dev/null differ diff --git a/docs/images/SDforDeletePersonEventHandling.png b/docs/images/SDforDeletePersonEventHandling.png deleted file mode 100644 index ecec0805d32c..000000000000 Binary files a/docs/images/SDforDeletePersonEventHandling.png and /dev/null differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 7a4cd2700cbf..1ccc1413a8cf 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5ec9c527b49c..35f3bed0706a 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/UiClassDiagram.png b/docs/images/UiClassDiagram.png index 369469ef176e..76bf0c7b740c 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/activityDiagramTargetGradeCalculationActualCalculation.png b/docs/images/activityDiagramTargetGradeCalculationActualCalculation.png new file mode 100644 index 000000000000..afa9bdd2536f Binary files /dev/null and b/docs/images/activityDiagramTargetGradeCalculationActualCalculation.png differ diff --git a/docs/images/activityDiagramTargetGradeCalculationWhenChanged.png b/docs/images/activityDiagramTargetGradeCalculationWhenChanged.png new file mode 100644 index 000000000000..b6d2022dd7c6 Binary files /dev/null and b/docs/images/activityDiagramTargetGradeCalculationWhenChanged.png differ diff --git a/docs/images/alexkmj.png b/docs/images/alexkmj.png new file mode 100644 index 000000000000..077fca4252fc Binary files /dev/null and b/docs/images/alexkmj.png differ diff --git a/docs/images/bugeyedbug.png b/docs/images/bugeyedbug.png new file mode 100644 index 000000000000..873ffe55f9e1 Binary files /dev/null and b/docs/images/bugeyedbug.png differ diff --git a/docs/images/damithc.jpg b/docs/images/damithc.jpg deleted file mode 100644 index 127543883893..000000000000 Binary files a/docs/images/damithc.jpg and /dev/null differ diff --git a/docs/images/jeremiah-ang.png b/docs/images/jeremiah-ang.png new file mode 100644 index 000000000000..369ecd1f1265 Binary files /dev/null and b/docs/images/jeremiah-ang.png differ diff --git a/docs/images/jeremyyew.png b/docs/images/jeremyyew.png new file mode 100644 index 000000000000..0da6fe63cbd7 Binary files /dev/null and b/docs/images/jeremyyew.png differ diff --git a/docs/images/josephambe.png b/docs/images/josephambe.png new file mode 100644 index 000000000000..89673820e0b1 Binary files /dev/null and b/docs/images/josephambe.png differ diff --git a/docs/images/lejolly.jpg b/docs/images/lejolly.jpg deleted file mode 100644 index 2d1d94e0cf5d..000000000000 Binary files a/docs/images/lejolly.jpg and /dev/null differ diff --git a/docs/images/m133225.jpg b/docs/images/m133225.jpg deleted file mode 100644 index fd14fb94593a..000000000000 Binary files a/docs/images/m133225.jpg and /dev/null differ diff --git a/docs/images/yijinl.jpg b/docs/images/yijinl.jpg deleted file mode 100644 index adbf62ad9406..000000000000 Binary files a/docs/images/yijinl.jpg and /dev/null differ diff --git a/docs/images/yl_coder.jpg b/docs/images/yl_coder.jpg deleted file mode 100644 index 17b48a732272..000000000000 Binary files a/docs/images/yl_coder.jpg and /dev/null differ diff --git a/docs/team/alexkmj.adoc b/docs/team/alexkmj.adoc new file mode 100644 index 000000000000..a2c7c1deaa8f --- /dev/null +++ b/docs/team/alexkmj.adoc @@ -0,0 +1,58 @@ += Alex Koh - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: CAPTracker + +--- + +== Overview + +_CapTracker_ is a desktop application. The user interacts with it using a CLI, +and it has a GUI created with JavaFX. It is written in Java, and has about 10 +kLoC. + +== Summary of Contributions + +* *Major Enhancements*: added *the ability to add, edit, and delete modules*. +** *What it does*: Allows the user to add, edit, and delete modules in CAPTracker one at a time. name-value paired (-name value) arguments instead of prefix used. +** *Justification*: Adding, editing, and deleting is needed to populate the CAPTracker. The name-value pair is also more intuitive for advance CLI users +** *Highlights*: name-value pair format was used as advance users are more familiar with it. The way the arguments are tokenize has also been changed. + +* *Minor enhancement*: +*** Make improvements to the APIs: `CommandUtil.java`, `ParserUtil.java`, and `UniqueModuleList.java` +* *Code Contributed*: https://nus-cs2103-ay1819s1.github.io/cs2103-dashboard/#=undefined&search=alexkmj[Project Code Dashboard - alexkmj] + +* *Other contributions*: + +** `Morphing`: Morphed the `Logic` and `Model` component including its unit test. Introduced the `Module` package. + +** *Organization setup*: Created and setup GitHub team organisation. + +** *Repository setup:* Setup organisation repository. Enable AppVeyor, Codacy, Coveralls, Travis CI, and GitHub Pages and its badges. Create team PR. + +** *Manage releases* + +** *Community* +*** PRs reviewed: https://goo.gl/QqNNsj[#23], https://goo.gl/6Wy8o4[#62], https://goo.gl/szTgtm[#71], https://goo.gl/SsxRkG[#76], https://goo.gl/rrR4bv[#106] + +== Contributions to the User Guide + +|=== +|_Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users._ +|=== + +include::../UserGuide.adoc[tag=add] + +include::../UserGuide.adoc[tag=edit] + +include::../UserGuide.adoc[tag=delete] + +== Contributions to the Developer Guide + +|=== +|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== + +include::../DeveloperGuide.adoc[tag=designlogic] diff --git a/docs/team/bugeyedbug.adoc b/docs/team/bugeyedbug.adoc new file mode 100644 index 000000000000..2da1006a0655 --- /dev/null +++ b/docs/team/bugeyedbug.adoc @@ -0,0 +1,57 @@ += Kong Jun Yin - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: CAPTracker + +--- + +== Overview + +CapTracker is a desktop application. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +== Summary of contributions + +//* *Major enhancement*: TBC. +//** What it does: TBC. +//** Justification: TBC. +//** Highlights: TBC. +//** Credits: TBC. +// +//* *Minor enhancement*: TBC. +// +//* *Code contributed*: [https://github.com[Functional code]] [https://github.com[Test code]] _{give links to collated code files}_ +// +//* *Other contributions*: +// +//** Project management: +//*** Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub +//** Enhancements to existing features: +//** Documentation: +//** Community: +//*** PRs reviewed (with non-trivial review comments): TBC. +//*** Contributed to forum discussions (examples: TBC.) +//*** Reported bugs and suggestions for other teams in the class (examples: TBC.) +//** Tools: + +== Contributions to the User Guide + + +|=== +|_Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users._ +|=== + +//include::../UserGuide.adoc[tag=undoredo] +// +//include::../UserGuide.adoc[tag=dataencryption] + +== Contributions to the Developer Guide + +|=== +|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== + +//include::../DeveloperGuide.adoc[tag=undoredo] +// +//include::../DeveloperGuide.adoc[tag=dataencryption] diff --git a/docs/team/jeremiah-ang.adoc b/docs/team/jeremiah-ang.adoc new file mode 100644 index 000000000000..a2b8493e8518 --- /dev/null +++ b/docs/team/jeremiah-ang.adoc @@ -0,0 +1,60 @@ += Jeremiah Ang - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: CAPTracker + +--- + +== Overview + +CapTracker is a desktop application. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +== Summary of contributions + +* *Major enhancement*: Implemented the *CAP & Target Grade Calculation* feature in Model. + +** What it does: Calculates the current CAP of the user and also calculate the target grades the user have to achieve +in order to achieve their CAP Goal. + +** Justification: This is one of the *must-have* User Story of the application, +to allow users to verify if they are able to graduate with their desired CAP Goal + +** Highlights: +*** Calculation of target grades are triggered when changes are made to the list of modules +in the transcript and updated on the UI. +*** Users may further adjust the target grade and obtain a updated list of target grades + + +* *Minor enhancement*: +** Added `adjust` and `goal` command in Logic + +* *Code Contributed*: https://nus-cs2103-ay1819s1.github.io/cs2103-dashboard/#=undefined&search=jeremiah-ang + +* *Other contributions*: + +** Project management: +*** Maintained issue tracker, milestones and review/merger of pull requests. +** Documentation: +*** Updated existing content of the Model Section of Developer Guide +*** Updated Use Cases to follow the proper format +*** Added Sequence Diagrams for Target Grade and CAP Calculations +*** Added Activity Diagram for Target Grade Calculations +*** Updated User Guide for `goal` and `adjust` commands +*** Added Manual Testing Guide for Target Grade and CAP Calculations +** Community: +*** PRs reviewed (with non-trivial review comments): #76, #78, #102, #133, #135, #190, #216 + + +== Contributions to the User Guide + +include::../UserGuide.adoc[tag=commandgoal] +include::../UserGuide.adoc[tag=commandadjust] + +== Contributions to the Developer Guide + +include::../DeveloperGuide.adoc[tag=designmodel] +include::../DeveloperGuide.adoc[tag=captargetcalculation] +include::../DeveloperGuide.adoc[tag=usecase] + diff --git a/docs/team/jeremyyew.adoc b/docs/team/jeremyyew.adoc new file mode 100644 index 000000000000..4ab4b81737c1 --- /dev/null +++ b/docs/team/jeremyyew.adoc @@ -0,0 +1,65 @@ += Jeremy Yew - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: CAPTracker + +--- + +== Overview + +CapTracker is a desktop application. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +== Summary of contributions + +* *Major enhancement*: Transcript file storage feature. +** What it does: Automatically saves and updates all of user's transcript data into a file, and loads this file. +** Justification: Users should be able to access previously created modules when opening the app again, otherwise they would have to re-enter all their modules again. They can also load their data in another computer or share it with other users if desired, simply by sharing the file itself. +** Highlights: +*** By using the `Jackson` library to store the Transcript data as a JSON file instead of an XML file, we avoid having to write `XMLSerializableTranscript` and `XMLAdaptedModule` classes. +* *Minor enhancement*: Display and updating of current CAP and CAP Goal. +** What it does: Displays current CAP, immediately updates whenever completed modules are edited. Displays CAP Goal if set, else displays NIL; immediately updates whenever incomplete modules are changed or adjusted. +** Justification: Users should be able to view their current CAP and CAP Goal as they are updated, without having to request for them through a command. + +* *Code contributed*: https://nus-cs2103-ay1819s1.github.io/cs2103-dashboard/#=undefined&search=jeremyyew + +* *Other contributions*: +** Project management: +*** Guided planning of early project timeline and milestone setting. +*** Managed release `v1.1` (1 release) on GitHub. +** Enhancements to existing features: +*** Enabled app to load initial sample module data (See PR #27). +*** Set up initial GUI layout (See PR #52). +** Documentation: +*** Updated existing content of the Storage Section of Developer Guide. +*** Added feature overview and updated "Saving your module data" section of User Guide. + +** Community: +*** PRs reviewed (with non-trivial review comments): #23, #37, #121 + +== Contributions to the User Guide +Note: Since my main feature is non-interactive, there are less things I could write for the user guide. + +|=== +|_Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users._ +|=== + +include::../UserGuide.adoc[tag=featureoverview] +include::../UserGuide.adoc[tag=savingdata] + +== Contributions to the Developer Guide + +|=== +|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== + +include::../DeveloperGuide.adoc[tag=transcriptstorageimplementation] + +include::../DeveloperGuide.adoc[tag=designstorage] + +=== Instructions for Manual Testing + +include::../DeveloperGuide.adoc[tag=manualteststorage] + + diff --git a/docs/team/johndoe.adoc b/docs/team/johndoe.adoc deleted file mode 100644 index 453c2152ab9d..000000000000 --- a/docs/team/johndoe.adoc +++ /dev/null @@ -1,72 +0,0 @@ -= John Doe - Project Portfolio -:site-section: AboutUs -:imagesDir: ../images -:stylesDir: ../stylesheets - -== PROJECT: AddressBook - Level 4 - ---- - -== Overview - -AddressBook - Level 4 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. - -== Summary of contributions - -* *Major enhancement*: added *the ability to undo/redo previous commands* -** What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. -** Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. -** Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. -** Credits: _{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}_ - -* *Minor enhancement*: added a history command that allows the user to navigate to previous commands using up/down keys. - -* *Code contributed*: [https://github.com[Functional code]] [https://github.com[Test code]] _{give links to collated code files}_ - -* *Other contributions*: - -** Project management: -*** Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub -** Enhancements to existing features: -*** Updated the GUI color scheme (Pull requests https://github.com[#33], https://github.com[#34]) -*** Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests https://github.com[#36], https://github.com[#38]) -** Documentation: -*** Did cosmetic tweaks to existing contents of the User Guide: https://github.com[#14] -** Community: -*** PRs reviewed (with non-trivial review comments): https://github.com[#12], https://github.com[#32], https://github.com[#19], https://github.com[#42] -*** Contributed to forum discussions (examples: https://github.com[1], https://github.com[2], https://github.com[3], https://github.com[4]) -*** Reported bugs and suggestions for other teams in the class (examples: https://github.com[1], https://github.com[2], https://github.com[3]) -*** Some parts of the history feature I added was adopted by several other class mates (https://github.com[1], https://github.com[2]) -** Tools: -*** Integrated a third party library (Natty) to the project (https://github.com[#42]) -*** Integrated a new Github plugin (CircleCI) to the team repo - -_{you can add/remove categories in the list above}_ - -== Contributions to the User Guide - - -|=== -|_Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users._ -|=== - -include::../UserGuide.adoc[tag=undoredo] - -include::../UserGuide.adoc[tag=dataencryption] - -== Contributions to the Developer Guide - -|=== -|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ -|=== - -include::../DeveloperGuide.adoc[tag=undoredo] - -include::../DeveloperGuide.adoc[tag=dataencryption] - - -== PROJECT: PowerPointLabs - ---- - -_{Optionally, you may include other projects in your portfolio.}_ diff --git a/docs/team/josephambe.adoc b/docs/team/josephambe.adoc new file mode 100644 index 000000000000..ecfa89efa56c --- /dev/null +++ b/docs/team/josephambe.adoc @@ -0,0 +1,151 @@ += Amber Joseph - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: CAPTracker + +--- + +== Overview + +CAPTracker is for students who wants to use a desktop app for calculating and managing their +CAP (Cumulative Average Point), allowing users to calculate their current CAP, and predict +grades needed to achieve their ideal CAP in modules that haven’t been taken. + +CAPTracker is optimized for those who prefer to work with a CLI (Command Line Interface) +while still having the benefits of a Graphical User Interface (GUI) created with JavaFX. + +CAPTracker is written in Java and has about 10kLoC. + +== Summary of contributions + +* *Major enhancement*: added the ability to view, display, and interact with CAPTracker. +** What it does: allows users to understand and intuitively use the app with minimum +additional instructions or hassel. +** Justification: this feature improves CAPTracker significantly as it is the + base at which the user interacts with the product. It not only increases the + applications usability but also the seamless completion of tasks that + CAPTracker allows. +** Highlights: This enhancement affects the way the user uses the product +and how vital information such as CAP calculations and predictions are +displayed. It requires an understanding of user interaction and behaviour, +as well as skills in design, colour complentation, and visual communication. +The implementation was challenging in that it forced me to think like the +user and have a good understanding of how the whole project works together +as I was combining results from various aspects of the software project. + +* *Minor enhancement*: Added a help section that gives the user hints on +what commands they can use to navigate the application depending on what +command they were trying to execute. + +* *Code contributed*: https://nus-cs2103-ay1819s1.github.io/cs2103-dashboard/#=undefined&search=josephambe +* *Other contributions*: +** Project management: +*** Managed the initial setup of the project including delegating and +organising tasks such as use case development, prioritizing user stories, +identifying our value proposition, completing product surveys, +separating non-functional requirements, defining project direction, +target user profile, and problem addressed. +** Enhancements to existing features: +*** Enhanced the automated replies from the application when the user enters +an incorrect command. Specialised it to respond to specific commands entered +and provide useful information that prevents the user from constantly having +to refer back to the User Guide. +** Documentation: +*** Updated the Developer Guide #2.2, #2.3, +*** Updated the User Guide on #3.11, 4.FAQ. +** Community: +*** PR’s reviewed with non-trivial comments, and suggestions. +*** Contributed to discussions on the Slack Channel +*** Reported bugs and suggestions for other team members in class +*** The UI features I created was adopted by Yingnan in Group T13 +//** Tools: + +== Contributions to the User Guide + + +|=== +|_Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users._ +|=== + + +*Initial Quick Start Image*: + +image::Ui.png[width="790"] + +=== Understanding the User Interface +Understanding the User Interface can be tricky - what do all the different colours mean? How do I +know what has been saved or not? Where can I see new modules I've added? + +* To view new modules you've added, scroll down to the bottom of the pannel that you have categorized your +module under; either the 'Completed Modules' panel on the left, or the 'Incomplete Modules' panel on the +right. Your new entry should be at the bottom of these lists. + +* Understanding the colours. The grades of modules in the 'Completed Modules' panel on the left +are circled in GREEN. This indicates that this particular module has already been taken and this is +a grade that the user does not need to worry about; it is in the past. +The grades in the the 'Incomplete Modules' panel on the right are circled in RED. This indicates +that this particular module has not been taken and that the grade displayed in this RED circle is +not certain. It is a grade that the user needs to be aware of as it's outcome will impact the +users overall CAP score. + +*PLEASE NOTE*: +As I am focused on the UI design of the product, there is a limited amount of information I can contribute to the User Guide. This is because if I need to explain the interface in too much detail, it means the user can’t intuitively use the product, which in turn, means I haven’t designed a very good interface. + +Part of my User Guide, which reminds users the format in which to type out commands, is integrated in the UI. This means that the user doesn’t have to keep switching back and forth between the User Guide and the product; they can receive the same information in real time when they need it. + +== FAQ +include::../UserGuide.adoc[tag=ambersFAQ] + +== Contributions to the Developer Guide + +|=== +|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== + +include::../DeveloperGuide.adoc[tag=architecture] + +[[Design-Ui]] +=== UI component + +.Structure of the UI Component +image::UiClassDiagram.png[width="800"] + +*API* : link:{repoURL}/src/main/java/seedu/address/ui/Ui.java[`Ui.java`] + +The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `StatusBarFooter`, `BrowserPanel`, 'ModuleListPanel' 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 link:{repoURL}/src/main/java/seedu/address/ui/MainWindow.java[`MainWindow`] is specified in link:{repoURL}/src/main/resources/view/MainWindow.fxml[`MainWindow.fxml`] + +The `UI` component uses JavaFX UI 'DarkTheme' to draw different text, sizes, fonts, and colours from. The actual data displayed in the UI is called using a sample transcript which is created through the Module and Transcript classes. The values themselves are abstracted from the '.fxml' files so the UI display can be easily updated. + +* Executes user commands using the `Logic` component. +* Binds itself to some data in the `Model` so that the UI can auto-update when data in the `Model` change. +* Responds to events raised from various parts of the App and updates the UI accordingly. + +[[Design-Layout]] +=== UI component +* The bottom two thirds of the UI is seperated into 2 panels to clearly identify the different outputs from commands entered by the user. +* The first panel on the left is for Modules that have already been completed; this is shown by the GREEN circles which +surround the grades which indicate this grade is "set" and of no concern to the user anymore. +* The second panel on the right is for Modules that have not yet been completed by the user; this is shown by the RED +circles which surround the grades to indicate that this is a grade the user should be aware of. The red indicates an +urgency towards that module as it's outcome will affect the users predicted CAP goal. +* The top third of the UI is seperated into four distinct rows; +. The first row contains the title and drop down menu's for `File` and `Help` options. +. The second row is the command line and how the user interacts with the application. Notice there is no button for the +user to click when they are ready to enter their command; it is expected the user is familiar with Command Line Interface +and will know to use the `enter` button on their keyboard when ready to submit a command to the app. +. The third row is where replies from the application to the user will be displayed. When the commands become too big +for the box, a scroll down option becomes available for the user to continue reading the message. +. The fourth row displays the summary of the users current CAP goal and their target CAP. + +The following was not pushed to Git by me, but I contributed to the final outcome +through team discussions during meetings and by sending my own User Stories, Value + Proposition, and Target User Profile via Slack and email to members of the team. + +include::../DeveloperGuide.adoc[tag=value] +include::../DeveloperGuide.adoc[tag=targetUser] +include::../DeveloperGuide.adoc[tag=userStories] + diff --git a/docs/templates/_header.html.slim b/docs/templates/_header.html.slim index 1995d26a1615..c596ecea643f 100644 --- a/docs/templates/_header.html.slim +++ b/docs/templates/_header.html.slim @@ -1,26 +1,4 @@ / NOTE: You must restart the gradle daemon after modifying any template file for the changes to take effect. -- if !(attr? 'no-site-header') && (attr? 'site-seedu') - #seedu-header - nav.navbar.navbar-lg.navbar-light.bg-lighter - .container - a.navbar-brand href='https://se-edu.github.io/' - img src=(site_url 'images/SeEduLogo.png') alt='SE-EDU' - ul.navbar-nav - li.nav-item - a.nav-link href='https://se-edu.github.io/addressbook-level1' AB-1 - li.nav-item - a.nav-link href='https://se-edu.github.io/addressbook-level2' AB-2 - li.nav-item - a.nav-link href='https://se-edu.github.io/addressbook-level3' AB-3 - li.nav-item - a.nav-link.active href=(site_url 'index.html') AB-4 - li.nav-item - a.nav-link href='https://se-edu.github.io/collate' Collate - li.nav-item - a.nav-link href='https://se-edu.github.io/se-book' Book - li.nav-item - a.nav-link href='https://se-edu.github.io/learningresources' Resources - - if !(attr? 'no-site-header') #site-header nav.navbar.navbar-light.bg-light @@ -32,9 +10,6 @@ =nav_link('UserGuide', 'UserGuide.html', 'User Guide') li.nav-item =nav_link('DeveloperGuide', 'DeveloperGuide.html', 'Developer Guide') - - if attr? 'site-seedu' - li.nav-item - =nav_link('LearningOutcomes', 'LearningOutcomes.html', 'LOs') li.nav-item =nav_link('AboutUs', 'AboutUs.html', 'About Us') li.nav-item diff --git a/parseCommandEditModuleSuccess b/parseCommandEditModuleSuccess new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index ecdd043a4f81..3474921b7ca0 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -20,16 +20,19 @@ import seedu.address.commons.util.StringUtil; import seedu.address.logic.Logic; import seedu.address.logic.LogicManager; -import seedu.address.model.AddressBook; import seedu.address.model.Model; import seedu.address.model.ModelManager; -import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyTranscript; +import seedu.address.model.Transcript; import seedu.address.model.UserPrefs; import seedu.address.model.util.SampleDataUtil; + import seedu.address.storage.AddressBookStorage; +import seedu.address.storage.JsonTranscriptStorage; import seedu.address.storage.JsonUserPrefsStorage; import seedu.address.storage.Storage; import seedu.address.storage.StorageManager; +import seedu.address.storage.TranscriptStorage; import seedu.address.storage.UserPrefsStorage; import seedu.address.storage.XmlAddressBookStorage; import seedu.address.ui.Ui; @@ -54,7 +57,7 @@ public class MainApp extends Application { @Override public void init() throws Exception { - logger.info("=============================[ Initializing AddressBook ]==========================="); + logger.info("=============================[ Initializing Transcript ]==========================="); super.init(); AppParameters appParameters = AppParameters.parse(getParameters()); @@ -63,7 +66,8 @@ public void init() throws Exception { UserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(config.getUserPrefsFilePath()); userPrefs = initPrefs(userPrefsStorage); AddressBookStorage addressBookStorage = new XmlAddressBookStorage(userPrefs.getAddressBookFilePath()); - storage = new StorageManager(addressBookStorage, userPrefsStorage); + TranscriptStorage transcriptStorage = new JsonTranscriptStorage(userPrefs.getTranscriptFilePath()); + storage = new StorageManager(addressBookStorage, userPrefsStorage, transcriptStorage); initLogging(config); @@ -82,23 +86,24 @@ public void init() throws Exception { * or an empty address book will be used instead if errors occur when reading {@code storage}'s address book. */ private Model initModelManager(Storage storage, UserPrefs userPrefs) { - Optional addressBookOptional; - ReadOnlyAddressBook initialData; + + Optional transcriptOptional; + ReadOnlyTranscript initialTranscriptData; + try { - addressBookOptional = storage.readAddressBook(); - if (!addressBookOptional.isPresent()) { - logger.info("Data file not found. Will be starting with a sample AddressBook"); + transcriptOptional = storage.readTranscript(); + if (!transcriptOptional.isPresent()) { + logger.info("Data file not found. Will be starting with a sample transcript"); } - initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); + initialTranscriptData = transcriptOptional.orElseGet(SampleDataUtil::getSampleTranscript); } catch (DataConversionException e) { - logger.warning("Data file not in the correct format. Will be starting with an empty AddressBook"); - initialData = new AddressBook(); + logger.warning("Data file not in the correct format. Will be starting with an empty Transcript"); + initialTranscriptData = new Transcript(); } catch (IOException e) { - logger.warning("Problem while reading from the file. Will be starting with an empty AddressBook"); - initialData = new AddressBook(); + logger.warning("Problem while reading from the file. Will be starting with an empty Transcript"); + initialTranscriptData = new Transcript(); } - - return new ModelManager(initialData, userPrefs); + return new ModelManager(initialTranscriptData, userPrefs); } private void initLogging(Config config) { @@ -201,8 +206,4 @@ public void handleExitAppRequestEvent(ExitAppRequestEvent event) { logger.info(LogsCenter.getEventHandlingLogMessage(event)); stop(); } - - public static void main(String[] args) { - launch(args); - } } diff --git a/src/main/java/seedu/address/commons/core/Messages.java b/src/main/java/seedu/address/commons/core/Messages.java index 1deb3a1e4695..41cd52f8dd45 100644 --- a/src/main/java/seedu/address/commons/core/Messages.java +++ b/src/main/java/seedu/address/commons/core/Messages.java @@ -9,5 +9,4 @@ public class Messages { public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; - } diff --git a/src/main/java/seedu/address/commons/events/model/TranscriptChangedEvent.java b/src/main/java/seedu/address/commons/events/model/TranscriptChangedEvent.java new file mode 100644 index 000000000000..c53ab3afa748 --- /dev/null +++ b/src/main/java/seedu/address/commons/events/model/TranscriptChangedEvent.java @@ -0,0 +1,19 @@ +package seedu.address.commons.events.model; + +import seedu.address.commons.events.BaseEvent; +import seedu.address.model.ReadOnlyTranscript; + +/** Indicates the Transcript in the model has changed*/ +public class TranscriptChangedEvent extends BaseEvent { + + public final ReadOnlyTranscript data; + + public TranscriptChangedEvent(ReadOnlyTranscript data) { + this.data = data; + } + + @Override + public String toString() { + return "number of transcripts " + data.getModuleList().size(); + } +} diff --git a/src/main/java/seedu/address/commons/events/ui/ModulePanelSelectionChangedEvent.java b/src/main/java/seedu/address/commons/events/ui/ModulePanelSelectionChangedEvent.java new file mode 100644 index 000000000000..391ba168851f --- /dev/null +++ b/src/main/java/seedu/address/commons/events/ui/ModulePanelSelectionChangedEvent.java @@ -0,0 +1,26 @@ +package seedu.address.commons.events.ui; + +import seedu.address.commons.events.BaseEvent; +import seedu.address.model.module.Module; + +/** + * Represents a selection change in the Person List Panel + */ +public class ModulePanelSelectionChangedEvent extends BaseEvent { + + + private final Module newSelection; + + public ModulePanelSelectionChangedEvent(Module newSelection) { + this.newSelection = newSelection; + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + + public Module getNewSelection() { + return newSelection; + } +} diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 8b34b862039a..f82aa027e8c8 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -4,6 +4,8 @@ import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.ReadOnlyTranscript; +import seedu.address.model.module.Module; import seedu.address.model.person.Person; /** @@ -19,9 +21,23 @@ public interface Logic { */ CommandResult execute(String commandText) throws CommandException, ParseException; + //@@author alexkmj + /** Returns an unmodifiable view of the filtered list of modules */ + ObservableList getFilteredModuleList(); + + /** Returns the transcript. */ + ReadOnlyTranscript getTranscript(); + /** Returns an unmodifiable view of the filtered list of persons */ ObservableList getFilteredPersonList(); /** Returns the list of input entered by the user, encapsulated in a {@code ListElementPointer} object */ ListElementPointer getHistorySnapshot(); + + //@@author jeremiah-ang + /** Returns an unmodifiable view of the list of completed Modules */ + ObservableList getCompletedModuleList(); + + /** Returns an unmodifiable view of the list of yet to complete Modules */ + ObservableList getIncompleteModuleList(); } diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 9aff86fc33dc..662a4a4b5aaa 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -8,32 +8,36 @@ import seedu.address.logic.commands.Command; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.AddressBookParser; +import seedu.address.logic.parser.TranscriptParser; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; +import seedu.address.model.ReadOnlyTranscript; +import seedu.address.model.module.Module; import seedu.address.model.person.Person; /** - * The main LogicManager of the app. + * The main {@code LogicManager} of the app. */ public class LogicManager extends ComponentManager implements Logic { private final Logger logger = LogsCenter.getLogger(LogicManager.class); private final Model model; private final CommandHistory history; - private final AddressBookParser addressBookParser; + private final TranscriptParser transcriptParser; + //@@author alexkmj public LogicManager(Model model) { this.model = model; history = new CommandHistory(); - addressBookParser = new AddressBookParser(); + transcriptParser = new TranscriptParser(); } + //@@author alexkmj @Override public CommandResult execute(String commandText) throws CommandException, ParseException { logger.info("----------------[USER COMMAND][" + commandText + "]"); try { - Command command = addressBookParser.parseCommand(commandText); + Command command = transcriptParser.parseCommand(commandText); return command.execute(model, history); } finally { history.add(commandText); @@ -41,12 +45,33 @@ public CommandResult execute(String commandText) throws CommandException, ParseE } @Override - public ObservableList getFilteredPersonList() { - return model.getFilteredPersonList(); + public ObservableList getFilteredModuleList() { + return model.getFilteredModuleList(); + } + + @Override + public ReadOnlyTranscript getTranscript() { + return model.getTranscript(); } @Override public ListElementPointer getHistorySnapshot() { return new ListElementPointer(history.getHistory()); } + + @Override + public ObservableList getCompletedModuleList() { + return model.getCompletedModuleList(); + } + + @Override + public ObservableList getIncompleteModuleList() { + return model.getIncompleteModuleList(); + } + + //TODO: REMOVE LEGACY CODE + @Override + public ObservableList getFilteredPersonList() { + return model.getFilteredPersonList(); + } } diff --git a/src/main/java/seedu/address/logic/commands/AddModuleCommand.java b/src/main/java/seedu/address/logic/commands/AddModuleCommand.java new file mode 100644 index 000000000000..8c6327ee90a6 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddModuleCommand.java @@ -0,0 +1,123 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.Objects; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.module.Module; + +/** + * Adds a module to the transcript. + */ +public class AddModuleCommand extends Command { + /** + * Command word for {@code AddModuleCommand}. + */ + public static final String COMMAND_WORD = "add"; + + /** + * Usage of add. + *

+ * Provides the description and syntax of add. + */ + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Adds a module to the transcript.\n" + + "Parameters: -m CODE -y YEAR -s SEMESTER -c CREDIT -g [GRADE]\n" + + "Example: " + COMMAND_WORD + " -m CS2103 -y 2 -s 1 -c 4 -g A+\n" + + "If you haven't taken the module before, you can omit the grade:"; + + /** + * Message that informs that new module is added successfully. + */ + public static final String MESSAGE_ADD_SUCCESS = "New module add: %1$s"; + + /** + * Message that informs that new module already exist. + */ + public static final String MESSAGE_MODULE_ALREADY_EXIST = "New module" + + " already exist."; + + /** + * Module to be added. + */ + private final Module toAdd; + + /** + * Constructor that instantiates {@code AddModuleCommand}. + * @param module + */ + public AddModuleCommand(Module module) { + requireNonNull(module); + toAdd = module; + } + + /** + * Adds a module into the module list of transcript. + *

+ * Throws {@code CommandException} when module to be added already exist. + * + * @param model {@code Model} that the command operates on. + * @param history {@code CommandHistory} that the command operates on. + * @return result of the command + * @throws CommandException thrown when command cannot be executed + * successfully + */ + @Override + public CommandResult execute(Model model, CommandHistory history) + throws CommandException { + // Model cannot be null. + requireNonNull(model); + + // Throws CommandException if module already exists. + editedModuleExist(model, toAdd); + + // Add module and commit. + model.addModule(toAdd); + model.commitTranscript(); + + // Return success message. + String successMsg = String.format(MESSAGE_ADD_SUCCESS, toAdd); + return new CommandResult(successMsg); + } + + /** + * Throws {@code CommandException} if {@code toAdd} already exist. + * + * @param model {@code Model} that the command operates on. + * @param toAdd module to be added + * @throws CommandException thrown if {@code toAdd} already exist + */ + private void editedModuleExist(Model model, Module toAdd) + throws CommandException { + + if (model.hasModule(toAdd)) { + throw new CommandException(MESSAGE_MODULE_ALREADY_EXIST); + } + } + + /** + * Returns true if all {@code toAdd} matches. + * + * @param other the other object compared against + * @return true if all field matches + */ + @Override + public boolean equals(Object other) { + // Short circuit if same object. + if (other == this) { + return true; + } + + // instanceof handles nulls. + if (!(other instanceof AddModuleCommand)) { + return false; + } + + // State check. + AddModuleCommand e = (AddModuleCommand) other; + return Objects.equals(toAdd, e.toAdd); + } +} diff --git a/src/main/java/seedu/address/logic/commands/AdjustCommand.java b/src/main/java/seedu/address/logic/commands/AdjustCommand.java new file mode 100644 index 000000000000..637bc2d36a0d --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AdjustCommand.java @@ -0,0 +1,85 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.module.Code; +import seedu.address.model.module.Grade; +import seedu.address.model.module.Module; +import seedu.address.model.module.Semester; +import seedu.address.model.module.Year; +import seedu.address.model.module.exceptions.ModuleCompletedException; +import seedu.address.model.module.exceptions.ModuleNotFoundException; +import seedu.address.model.module.exceptions.MultipleModuleEntryFoundException; + +/** + * Adjusts target grade of a Module + */ +public class AdjustCommand extends Command { + public static final String COMMAND_WORD = "adjust"; + public static final String MESSAGE_COMMAND_CODE_ONLY = COMMAND_WORD + " CODE"; + public static final String MESSAGE_COMMAND_CODE_YEAR_SEM = COMMAND_WORD + " CODE YEAR SEM"; + public static final String MESSAGE_USAGE = MESSAGE_COMMAND_CODE_ONLY + + "\n" + + MESSAGE_COMMAND_CODE_YEAR_SEM + + "\n" + + "Adjust target grade of an incomplete module \n" + + "Parameters: CODE [YEAR SEM] GRADE \n" + + "Example: " + COMMAND_WORD + " CS2103 1 1 A+"; + public static final String MESSAGE_MULTIPLE_INSTANCE = + "Multiple Instance of Module, please include Year and Semester\n" + + MESSAGE_COMMAND_CODE_YEAR_SEM; + public static final String MESSAGE_SUCCESS = "Module Adjusted: %1$s"; + public static final String MESSAGE_MODULE_NOT_FOUND = "Module not found"; + public static final String MESSAGE_MODULE_COMPLETED = "Module already Completed!\n" + MESSAGE_USAGE; + + private final Code code; + private final Year year; + private final Semester sem; + private final Grade grade; + + public AdjustCommand(Code code, Year year, Semester sem, Grade grade) { + requireNonNull(code); + requireNonNull(grade); + this.code = code; + this.year = year; + this.sem = sem; + this.grade = grade; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) + throws CommandException { + requireNonNull(model); + + Module targetModule; + + try { + targetModule = model.getOnlyOneModule(code, year, sem); + } catch (ModuleNotFoundException mnfe) { + throw new CommandException(MESSAGE_MODULE_NOT_FOUND); + } catch (MultipleModuleEntryFoundException mmefe) { + throw new CommandException(MESSAGE_MULTIPLE_INSTANCE); + } + + try { + Module adjustedModule = model.adjustModule(targetModule, grade); + model.commitTranscript(); + return new CommandResult(String.format(MESSAGE_SUCCESS, adjustedModule)); + } catch (ModuleCompletedException mce) { + throw new CommandException(MESSAGE_MODULE_COMPLETED); + } + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AdjustCommand // instanceof handles nulls + && grade.equals(((AdjustCommand) other).grade) + && code.equals(((AdjustCommand) other).code) + && (year == null || year.equals(((AdjustCommand) other).year)) + && (sem == null || sem.equals(((AdjustCommand) other).sem))); + } +} diff --git a/src/main/java/seedu/address/logic/commands/CommandUtil.java b/src/main/java/seedu/address/logic/commands/CommandUtil.java new file mode 100644 index 000000000000..1ddcc6c09e69 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/CommandUtil.java @@ -0,0 +1,53 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.module.Code; +import seedu.address.model.module.Module; +import seedu.address.model.module.Semester; +import seedu.address.model.module.Year; +import seedu.address.model.module.exceptions.ModuleNotFoundException; +import seedu.address.model.module.exceptions.MultipleModuleEntryFoundException; + +//@@author alexkmj +/** + * Contains utility methods used for executing commands in the various + * Command classes. + */ +public class CommandUtil { + public static final String MESSAGE_NO_SUCH_MODULE = "No such module."; + public static final String MESSAGE_MULTIPLE_MODULE_ENTRIES = "There exists" + + " more than one module that matches the target code, and target" + + " year and target semester is not specified."; + + /** + * Returns the targeted module. + *

+ * Checks if module specified by {@code targetCode} exist. If multiple + * module entries matches {@code targetCode}, check if {@code targetYear} + * and {@code targetSemester} has been specified. If all check passes, the + * targeted module is returned. + * + * @param model {@code model} containing the transcript + * @return targeted module + * @throws CommandException thrown when specified module does not exist or + * there are multiple module entries matching the {@code targetCode} but + * {@code targetYear} or {@code targetSemester} was not specified + */ + public static Module getUniqueTargetModule(Model model, Code targetCode, + Year targetYear, Semester targetSemester) throws CommandException { + requireNonNull(targetCode); + + try { + return model.getOnlyOneModule(targetCode, + targetYear, + targetSemester); + } catch (ModuleNotFoundException moduleNotFoundException) { + throw new CommandException(MESSAGE_NO_SUCH_MODULE); + } catch (MultipleModuleEntryFoundException multipleFoundException) { + throw new CommandException(MESSAGE_MULTIPLE_MODULE_ENTRIES); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteModuleCommand.java b/src/main/java/seedu/address/logic/commands/DeleteModuleCommand.java new file mode 100644 index 000000000000..a4bf290a670c --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteModuleCommand.java @@ -0,0 +1,131 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.EnumMap; +import java.util.Objects; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.arguments.DeleteArgument; +import seedu.address.model.Model; +import seedu.address.model.module.Code; +import seedu.address.model.module.Module; +import seedu.address.model.module.Semester; +import seedu.address.model.module.Year; + +//@@author alexkmj +/** + * Deletes a person identified using it's displayed index from the address book. + */ +public class DeleteModuleCommand extends Command { + + public static final String COMMAND_WORD = "delete"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the module identified by the module code, year, and" + + " semester." + + " \nParameters: -t MODULE_CODE [-e YEAR -z SEMESTER]" + + " \nOR c_delete -t TARGET_CODE -e TARGET_YEAR -z TARGET_SEMESTER" + + " \nExample 1: " + COMMAND_WORD + " -t CS2103 -e 4 -z 2"; + + public static final String MESSAGE_DELETE_SUCCESS = "Deleted Module: %1$s"; + + private final Code targetCode; + private final Year targetYear; + private final Semester targetSemester; + + /** + * Constructor that instantiates {@code DeleteModuleCommand}. + *

+ * Sets target field used to find and delete the module. + *

+ * Assumes that: + *

    + *
  • {@code targetCode} is not null.
  • + *
  • + * {@code targetYear} is null if and only if {@code targetSemester} + * is null + *
  • + *
+ * + * @param argMap Contains the name-value pair mapping of the arguments + */ + public DeleteModuleCommand(EnumMap argMap) { + requireNonNull(argMap); + + // Instantiate target fields. + targetCode = (Code) argMap.get(DeleteArgument.TARGET_CODE); + targetYear = (Year) argMap.get(DeleteArgument.TARGET_YEAR); + targetSemester = (Semester) argMap.get(DeleteArgument.TARGET_SEMESTER); + + // Already handled by DeleteModuleCommandParser: + // 1) Target code cannot be null. + // 2) Target year is null if and only if target semester is null. + requireNonNull(targetCode); + assert !(targetYear == null ^ targetSemester == null); + } + + /** + * Deletes the targeted module in the module list of transcript. + *

+ * Throws {@code CommandException} when: + *

    + *
  • There is no module that matches the target code.
  • + *
  • There exists more than one matching module.
  • + *
+ * + * @param model {@code Model} that the command operates on. + * @param history {@code CommandHistory} that the command operates on. + * @return result of the command + * @throws CommandException thrown when command cannot be executed + * successfully + */ + @Override + public CommandResult execute(Model model, CommandHistory history) + throws CommandException { + // Model cannot be null. + requireNonNull(model); + + // Get target module. + // Throws CommandException if module does not exists. + // Throws CommandException if there is more than one matching module. + Module targetModule = CommandUtil.getUniqueTargetModule(model, + targetCode, + targetYear, + targetSemester); + + // Delete module and commit. + model.deleteModule(targetModule); + model.commitTranscript(); + + // Return success message. + String successMsg = String.format(MESSAGE_DELETE_SUCCESS, targetModule); + return new CommandResult(successMsg); + } + + /** + * Returns true if all field matches. + * + * @param other the other object compared against + * @return true if all field matches + */ + @Override + public boolean equals(Object other) { + // Short circuit if same object. + if (other == this) { + return true; + } + + // instanceof handles nulls. + if (!(other instanceof DeleteModuleCommand)) { + return false; + } + + // State check. + DeleteModuleCommand e = (DeleteModuleCommand) other; + return Objects.equals(targetCode, e.targetCode) + && Objects.equals(targetYear, e.targetYear) + && Objects.equals(targetSemester, e.targetSemester); + } +} diff --git a/src/main/java/seedu/address/logic/commands/EditModuleCommand.java b/src/main/java/seedu/address/logic/commands/EditModuleCommand.java new file mode 100644 index 000000000000..d3b7224b1896 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/EditModuleCommand.java @@ -0,0 +1,274 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.EnumMap; +import java.util.Objects; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.arguments.EditArgument; +import seedu.address.model.Model; +import seedu.address.model.module.Code; +import seedu.address.model.module.Credit; +import seedu.address.model.module.Grade; +import seedu.address.model.module.Module; +import seedu.address.model.module.Semester; +import seedu.address.model.module.Year; +import seedu.address.model.util.ModuleBuilder; + +/** + * {@code EditModuleCommand} edit fields of existing module. + */ +public class EditModuleCommand extends Command { + /** + * Command word for {@code EditModuleCommand}. + */ + public static final String COMMAND_WORD = "edit"; + + /** + * Usage of edit. + *

+ * Provides the description and syntax of edit. + */ + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Edits the details of the module specified by the module code." + + " Existing values will be overwritten by the input values." + + " \nParameters:" + + " \n-t TARGET_MODULE_CODE" + + " \n[-e TARGET_MODULE_YEAR -z TARGET_MODULE_SEMESTER]" + + " \n[-m NEW_MODULE_CODE]" + + " \n[-y NEW_YEAR]" + + " \n[-s NEW_SEMESTER]" + + " \n[-c NEW_CREDIT]" + + " \n[-g NEW_GRADE]" + + " \nExample 1: c_edit -t CS2103 -g A+ " + + " \nExample 2: c_edit -t CS2103 -e 3 -z 2 -s 1"; + + // Constants for CommandException. + public static final String MESSAGE_EDIT_SUCCESS = "Edited module: %1$s"; + public static final String MESSAGE_INCOMPLETE_MODULE_GRADE_CHANGE = "Cannot" + + " change grade of incomplete modules. Use adjust to change grade" + + " of incomplete modules."; + public static final String MESSAGE_MODULE_ALREADY_EXIST = "Edited module" + + "already exist."; + + // Target fields. + private final Code targetCode; + private final Year targetYear; + private final Semester targetSemester; + + // New fields. + private final Code newCode; + private final Year newYear; + private final Semester newSemester; + private final Credit newCredit; + private final Grade newGrade; + + /** + * Constructor that instantiates {@code EditModuleCommand}. + *

+ * Sets target field and new field used to find and editing the targeted + * module. + *

+ * Assumes that: + *

    + *
  • {@code targetCode} is not null.
  • + *
  • + * {@code targetYear} is null if and only if {@code targetSemester} + * is null + *
  • + *
  • + * One of {@code newCode}, {@code newYear}, {@code newSemester}, + * {@code newCredit}, or {@code newGrade} is not null. + *
  • + *
+ * + * @param argMap Contains the name-value pair mapping of the arguments + */ + public EditModuleCommand(EnumMap argMap) { + requireNonNull(argMap); + + // Instantiate target fields. + targetCode = (Code) argMap.get(EditArgument.TARGET_CODE); + targetYear = (Year) argMap.get(EditArgument.TARGET_YEAR); + targetSemester = (Semester) argMap.get(EditArgument.TARGET_SEMESTER); + + // Instantiate new fields. + newCode = (Code) argMap.get(EditArgument.NEW_CODE); + newYear = (Year) argMap.get(EditArgument.NEW_YEAR); + newSemester = (Semester) argMap.get(EditArgument.NEW_SEMESTER); + newCredit = (Credit) argMap.get(EditArgument.NEW_CREDIT); + newGrade = (Grade) argMap.get(EditArgument.NEW_GRADE); + + // Already handled by EditModuleCommandParser: + // 1) Target code cannot be null. + // 2) Target year is null if and only if target semester is null. + // 3) One of new field is not null. + requireNonNull(targetCode); + assert !(targetYear == null ^ targetSemester == null); + assert newCode != null + || newYear != null + || newSemester != null + || newCredit != null + || newGrade != null; + } + + /** + * Edits the targeted module in the module list of transcript. + *

+ * Throws {@code CommandException} when: + *

    + *
  • Target module does not exist
  • + *
  • Target module is incomplete and edited module has new grade
  • + *
  • + * Another module in transcript already have the same module code, + * year, and semester of the edited module. + *
  • + *
+ * + * @param model {@code Model} that the command operates on. + * @param history {@code CommandHistory} that the command operates on. + * @return result of the command + * @throws CommandException thrown when command cannot be executed + * successfully + */ + @Override + public CommandResult execute(Model model, CommandHistory history) + throws CommandException { + requireNonNull(model); + + // Get target module. + // Throws CommandException if module does not exists. + // Throws CommandException if module is incomplete and grade changed. + Module targetModule = CommandUtil.getUniqueTargetModule(model, + targetCode, + targetYear, + targetSemester); + moduleCompletedIfGradeChange(targetModule); + + // Get edited module. + // Throws CommandException if edited module already exist. + Module editedModule = createEditedModule(targetModule); + editedModuleExist(model, editedModule); + + if (targetModule.equals(editedModule)) { + throw new CommandException("No changes"); + } + + // Update module and commit the transcript. + model.updateModule(targetModule, editedModule); + model.commitTranscript(); + + String successMsg = String.format(MESSAGE_EDIT_SUCCESS, editedModule); + return new CommandResult(successMsg); + } + + /** + * Returns the edited version of the target module. + * + * @param target the module to be edited + * @return the edited version of {@code target} + */ + private Module createEditedModule(Module target) { + ModuleBuilder moduleBuilder = new ModuleBuilder(target); + + if (newCode != null) { + moduleBuilder = moduleBuilder.withCode(newCode); + } + + if (newYear != null) { + moduleBuilder = moduleBuilder.withYear(newYear); + } + + if (newSemester != null) { + moduleBuilder = moduleBuilder.withSemester(newSemester); + } + + if (newCredit != null) { + moduleBuilder = moduleBuilder.withCredit(newCredit); + } + + if (newGrade != null) { + moduleBuilder = moduleBuilder.withGrade(newGrade); + } + + return moduleBuilder.build(); + } + + /** + * Throws {@code CommandException} if target is an incomplete module and + * grade has been changed. + * + * @param target targeted module to be updated + * @throws CommandException thrown when target is an incomplete module and + * grade has been changed + */ + private void moduleCompletedIfGradeChange(Module target) + throws CommandException { + boolean targetIncomplete = !target.getGrade().isComplete(); + boolean newGradeNotNull = newGrade != null; + + if (targetIncomplete && newGradeNotNull) { + throw new CommandException(MESSAGE_INCOMPLETE_MODULE_GRADE_CHANGE); + } + } + + /** + * Throws {@code CommandException} if code, year, or semester has been + * changed, and there exist a module in module list of transcript that + * shares the same module code, year, and semester as the + * {@code editedModule}. + * + * @param model {@code Model} that the command operates on. + * @param editedModule module with updated fields + * @throws CommandException thrown if current module list already contain a + * module sharing the same module code, year, and semester as the + * {@code editedModule} + */ + private void editedModuleExist(Model model, Module editedModule) + throws CommandException { + boolean identifierNotChanged = newCode == null + && newYear == null + && newSemester == null; + + // No conflicts since identifier hasn't changed. + if (identifierNotChanged) { + return; + } + + // Throw CommandException if module with same identifier already exist. + if (model.hasModule(editedModule)) { + throw new CommandException(MESSAGE_MODULE_ALREADY_EXIST); + } + } + + /** + * Returns true if all field matches. + * + * @param other the other object compared against + * @return true if all field matches + */ + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditModuleCommand)) { + return false; + } + + // state check + EditModuleCommand e = (EditModuleCommand) other; + return targetCode.equals(e.targetCode) + && Objects.equals(targetYear, e.targetYear) + && Objects.equals(targetSemester, e.targetSemester) + && Objects.equals(newYear, e.newYear) + && Objects.equals(newSemester, e.newSemester) + && Objects.equals(newCredit, e.newCredit) + && Objects.equals(newGrade, e.newGrade); + } +} diff --git a/src/main/java/seedu/address/logic/commands/GoalCommand.java b/src/main/java/seedu/address/logic/commands/GoalCommand.java new file mode 100644 index 000000000000..742276da4f48 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/GoalCommand.java @@ -0,0 +1,47 @@ +package seedu.address.logic.commands; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.capgoal.CapGoal; + +//@@author jeremiah-ang +/** + * Sets CAP Goal + */ +public class GoalCommand extends Command { + + public static final String COMMAND_WORD = "goal"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Set your CAP goal. " + + "Parameters: " + + "CAP_GOAL " + + "\nExample: " + COMMAND_WORD + " " + + "4.5"; + + public static final String MESSAGE_SUCCESS = "Your CAP Goal: %1$s"; + + private final double goal; + + /** + * Creates an GoalCommand to set the CAP Goal + */ + public GoalCommand(double goal) { + this.goal = goal; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + model.updateCapGoal(goal); + CapGoal capGoal = model.getCapGoal(); + model.commitTranscript(); + return new CommandResult(String.format(MESSAGE_SUCCESS, capGoal.toString())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof GoalCommand // instanceof handles nulls + && goal == ((GoalCommand) other).goal); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/RedoCommand.java b/src/main/java/seedu/address/logic/commands/RedoCommand.java index 227771a4eef6..965e3e7227d2 100644 --- a/src/main/java/seedu/address/logic/commands/RedoCommand.java +++ b/src/main/java/seedu/address/logic/commands/RedoCommand.java @@ -10,6 +10,7 @@ /** * Reverts the {@code model}'s address book to its previously undone state. */ +//TODO: Remove public class RedoCommand extends Command { public static final String COMMAND_WORD = "redo"; diff --git a/src/main/java/seedu/address/logic/commands/RedoModuleCommand.java b/src/main/java/seedu/address/logic/commands/RedoModuleCommand.java new file mode 100644 index 000000000000..9a128c6169c7 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/RedoModuleCommand.java @@ -0,0 +1,31 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_MODULES; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Reverts the {@code model}'s transcript to its previously undone state. + */ +public class RedoModuleCommand extends Command { + + public static final String COMMAND_WORD = "redo"; + public static final String MESSAGE_SUCCESS = "Redo success!"; + public static final String MESSAGE_FAILURE = "No more commands to redo!"; + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + if (!model.canRedoTranscript()) { + throw new CommandException(MESSAGE_FAILURE); + } + + model.redoTranscript(); + model.updateFilteredModuleList(PREDICATE_SHOW_ALL_MODULES); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/UndoCommand.java b/src/main/java/seedu/address/logic/commands/UndoCommand.java index 40441264f346..be55b3733ed2 100644 --- a/src/main/java/seedu/address/logic/commands/UndoCommand.java +++ b/src/main/java/seedu/address/logic/commands/UndoCommand.java @@ -10,6 +10,8 @@ /** * Reverts the {@code model}'s address book to its previous state. */ +//TODO: Remove + public class UndoCommand extends Command { public static final String COMMAND_WORD = "undo"; diff --git a/src/main/java/seedu/address/logic/commands/UndoModuleCommand.java b/src/main/java/seedu/address/logic/commands/UndoModuleCommand.java new file mode 100644 index 000000000000..d4ad52aa60e2 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/UndoModuleCommand.java @@ -0,0 +1,31 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_MODULES; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Reverts the {@code model}'s transcript to its previous state. + */ +public class UndoModuleCommand extends Command { + + public static final String COMMAND_WORD = "undo"; + public static final String MESSAGE_SUCCESS = "Undo success!"; + public static final String MESSAGE_FAILURE = "No more commands to undo!"; + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + if (!model.canUndoTranscript()) { + throw new CommandException(MESSAGE_FAILURE); + } + + model.undoTranscript(); + model.updateFilteredModuleList(PREDICATE_SHOW_ALL_MODULES); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddModuleCommandParser.java b/src/main/java/seedu/address/logic/parser/AddModuleCommandParser.java new file mode 100644 index 000000000000..39ef1e78e40c --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddModuleCommandParser.java @@ -0,0 +1,179 @@ +package seedu.address.logic.parser; + +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toSet; +import static seedu.address.logic.parser.ParserUtil.argsAreNameValuePair; +import static seedu.address.logic.parser.ParserUtil.argsWithBounds; +import static seedu.address.logic.parser.ParserUtil.validateName; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.IntStream; + +import com.google.common.collect.ImmutableSet; + +import seedu.address.logic.commands.AddModuleCommand; +import seedu.address.logic.parser.arguments.AddArgument; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.module.Code; +import seedu.address.model.module.Credit; +import seedu.address.model.module.Grade; +import seedu.address.model.module.Module; +import seedu.address.model.module.Semester; +import seedu.address.model.module.Year; + +//@@author alexkmj +/** + * Parses input arguments and creates a new AddModuleCommand object + */ +public class AddModuleCommandParser implements Parser { + /** + * Message that informs that the command is in a wrong format and + * prints the usage for delete command. + */ + public static final String MESSAGE_INVALID_FORMAT = + ParserUtil.MESSAGE_INVALID_FORMAT + + "\n" + + AddModuleCommand.MESSAGE_USAGE; + + /** + * Immutable map that maps string argument to edit argument enum. + */ + private static final Map NAME_TO_ARGUMENT_MAP; + + /** + * Immutable set containing the allowable size of arguments. + */ + private static final Set ALLOWED_ARG_SIZE; + + static { + Map map = new HashMap<>(); + for (AddArgument instance : AddArgument.values()) { + map.put(instance.getShortName(), instance); + map.put(instance.getLongName(), instance); + } + NAME_TO_ARGUMENT_MAP = Collections.unmodifiableMap(map); + } + + /** + * Populate {@code ALLOWED_ARG_SIZE} with a set of numbers representing the + * allowed argument sizes. + */ + static { + ALLOWED_ARG_SIZE = IntStream.range(8, 11) + .filter(index -> index % 2 == 0) + .boxed() + .collect(collectingAndThen(toSet(), ImmutableSet::copyOf)); + } + + /** + * Parses {@code args} in the context of {@code DeleteModuleCommand} and + * returns {@code DeleteModuleCommand} for execution. + *

+ * Throws {@code ParseException} when: + *

    + *
  • Number of argument is not between 2 and 6.
  • + *
  • Number of argument is not even.
  • + *
  • Argument is not in name-value pair format
  • + *
  • Argument contains illegal name
  • + *
  • Same name appeared more than once
  • + *
  • Target code is not provided.
  • + *
  • Target year is provided but target semester is not provided.
  • + *
  • Target semester is provided but target year is not provided.
  • + *
+ * + * @param argsInString String that contains all the argument + * @return {@code EditModuleCommand} object for execution + * @throws ParseException thrown when user input does not conform to the + * expected format + */ + + public AddModuleCommand parse(String argsInString) + throws ParseException { + // Converts argument string to tokenize argument array. + String[] args = ParserUtil.tokenize(argsInString); + + + // Size of argument should be 8 or 10. + // Arguments should be in name-value pair. + // Name should be legal. + // No duplicate name. + argsWithBounds(args, ALLOWED_ARG_SIZE); + argsAreNameValuePair(args, MESSAGE_INVALID_FORMAT); + validateName(args, NAME_TO_ARGUMENT_MAP, MESSAGE_INVALID_FORMAT); + + // Map the object of the parsed value to {@code AddArgument} + // instance. + // Code, Year, Semeter, and Credit should not be null. + EnumMap argMap = parseValues(args); + onlyGradeCanBeEmpty(argMap); + Module newModule = getModuleWithAddArgMap(argMap); + + // Return delete module command for execution. + return new AddModuleCommand(newModule); + } + + /** + * Parse the value into its relevant object and put it in {@code argMap}. + * + * @param args array of name-value pair arguments + * @throws ParseException thrown when the value cannot be parsed + */ + private EnumMap parseValues(String[] args) + throws ParseException { + // Initialise argument map. + EnumMap argMap = + new EnumMap<>(AddArgument.class); + + for (int index = 0; index < args.length; index = index + 2) { + AddArgument name = NAME_TO_ARGUMENT_MAP.get(args[index]); + Object value = name.getValue(args[index + 1]); + argMap.put(name, value); + } + + return argMap; + } + + /** + * Only grade argument can be empty. + * + * @param argMap add argument map used to create module instance + * @throws ParseException thrown when code, year, semester, or credit is + * null + */ + private static void onlyGradeCanBeEmpty(EnumMap argMap) + throws ParseException { + boolean codeIsNull = argMap.get(AddArgument.NEW_CODE) == null; + boolean yearIsNull = argMap.get(AddArgument.NEW_YEAR) == null; + boolean semesterIsNull = argMap.get(AddArgument.NEW_SEMESTER) == null; + boolean creditIsNull = argMap.get(AddArgument.NEW_CREDIT) == null; + + if (codeIsNull || yearIsNull || semesterIsNull || creditIsNull) { + throw new ParseException(MESSAGE_INVALID_FORMAT); + } + } + + /** + * Creates and returns a module instance based on {@code argMap}. + * + * @param argMap add argument map used to create module instance + * @return module instance based on {@code argMap} + */ + private static Module getModuleWithAddArgMap( + EnumMap argMap) { + Code code = (Code) argMap.get(AddArgument.NEW_CODE); + Year year = (Year) argMap.get(AddArgument.NEW_YEAR); + Semester semester = (Semester) argMap.get(AddArgument.NEW_SEMESTER); + Credit credit = (Credit) argMap.get(AddArgument.NEW_CREDIT); + Grade grade = (Grade) argMap.get(AddArgument.NEW_GRADE); + + if (grade == null) { + return new Module(code, year, semester, credit, null, false); + } + + return new Module(code, year, semester, credit, grade, true); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AdjustCommandParser.java b/src/main/java/seedu/address/logic/parser/AdjustCommandParser.java new file mode 100644 index 000000000000..87c3303ed415 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AdjustCommandParser.java @@ -0,0 +1,43 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.parser.ParserUtil.argsWithBounds; + +import java.util.Set; + +import seedu.address.logic.commands.AdjustCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.module.Code; +import seedu.address.model.module.Grade; +import seedu.address.model.module.Semester; +import seedu.address.model.module.Year; + +//@@author jeremiah-ang +/** + * Parses input arguments and creates a new AdjustCommand object + */ +public class AdjustCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddCommand + * and returns an AddCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AdjustCommand parse(String args) throws ParseException { + String[] tokenizedArgs = ParserUtil.tokenize(args); + argsWithBounds(tokenizedArgs, Set.of(2, 4), AdjustCommand.MESSAGE_USAGE); + + int index = 0; + + Year year = null; + Semester sem = null; + Code code; + Grade grade; + code = ParserUtil.parseCode(tokenizedArgs[index++]); + if (tokenizedArgs.length == 4) { + year = ParserUtil.parseYear(tokenizedArgs[index++]); + sem = ParserUtil.parseSemester(tokenizedArgs[index++]); + } + grade = ParserUtil.parseGrade(tokenizedArgs[index++]); + return new AdjustCommand(code, year, sem, grade); + } +} diff --git a/src/main/java/seedu/address/logic/parser/DeleteModuleCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteModuleCommandParser.java new file mode 100644 index 000000000000..9dc91c5868b5 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteModuleCommandParser.java @@ -0,0 +1,158 @@ +package seedu.address.logic.parser; + +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toSet; +import static seedu.address.logic.parser.ParserUtil.argsAreNameValuePair; +import static seedu.address.logic.parser.ParserUtil.argsWithBounds; +import static seedu.address.logic.parser.ParserUtil.targetCodeNotNull; +import static seedu.address.logic.parser.ParserUtil.targetYearNullIffTargetSemesterNull; +import static seedu.address.logic.parser.ParserUtil.validateName; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.IntStream; + +import com.google.common.collect.ImmutableSet; + +import seedu.address.logic.commands.DeleteModuleCommand; +import seedu.address.logic.parser.arguments.DeleteArgument; +import seedu.address.logic.parser.exceptions.ParseException; + +//@@author alexkmj +/** + * {@code DeleteModuleCommandParser} parses input arguments for + * {@code DeleteModuleCommand}. + */ +public class DeleteModuleCommandParser implements Parser { + /** + * Message that informs that the target code is required and prints the + * usage for delete command. + */ + public static final String MESSAGE_TARGET_CODE_REQUIRED = + ParserUtil.MESSAGE_TARGET_CODE_REQUIRED + + "\n" + + DeleteModuleCommand.MESSAGE_USAGE; + + /** + * Message that informs that target year has to be specified if and only if + * semester is specified, and prints the usage for delete command. + */ + public static final String MESSAGE_YEAR_AND_SEMESTER_XOR_NULL = + ParserUtil.MESSAGE_YEAR_AND_SEMESTER_XOR_NULL + + "\n" + + DeleteModuleCommand.MESSAGE_USAGE; + + /** + * Message that informs that the command is in a wrong format and + * prints the usage for delete command. + */ + public static final String MESSAGE_INVALID_FORMAT = + ParserUtil.MESSAGE_INVALID_FORMAT + + "\n" + + DeleteModuleCommand.MESSAGE_USAGE; + + /** + * Immutable map that maps string argument to edit argument enum. + */ + private static final Map NAME_TO_ARGUMENT_MAP; + + /** + * Immutable set containing the allowable size of arguments. + */ + private static final Set ALLOWED_ARG_SIZE; + + static { + Map map = new HashMap<>(); + for (DeleteArgument instance : DeleteArgument.values()) { + map.put(instance.getShortName(), instance); + map.put(instance.getLongName(), instance); + } + NAME_TO_ARGUMENT_MAP = Collections.unmodifiableMap(map); + } + + /** + * Populate {@code ALLOWED_ARG_SIZE} with a set of numbers representing the + * allowed argument sizes. + */ + static { + ALLOWED_ARG_SIZE = IntStream.range(2, 7) + .filter(index -> index % 2 == 0) + .boxed() + .collect(collectingAndThen(toSet(), ImmutableSet::copyOf)); + } + + /** + * Parses {@code args} in the context of {@code DeleteModuleCommand} and + * returns {@code DeleteModuleCommand} for execution. + *

+ * Throws {@code ParseException} when: + *

    + *
  • Number of argument is not between 2 and 6.
  • + *
  • Number of argument is not even.
  • + *
  • Argument is not in name-value pair format
  • + *
  • Argument contains illegal name
  • + *
  • Same name appeared more than once
  • + *
  • Target code is not provided.
  • + *
  • Target year is provided but target semester is not provided.
  • + *
  • Target semester is provided but target year is not provided.
  • + *
+ * + * @param argsInString String that contains all the argument + * @return {@code EditModuleCommand} object for execution + * @throws ParseException thrown when user input does not conform to the + * expected format + */ + public DeleteModuleCommand parse(String argsInString) + throws ParseException { + // Converts argument string to tokenize argument array. + String[] args = ParserUtil.tokenize(argsInString); + + // Size of argument should be between 2 to 6. + // Size of argument should be even. + // Arguments should be in name-value pair. + // Name should be legal. + // No duplicate name. + argsWithBounds(args, ALLOWED_ARG_SIZE, DeleteModuleCommand.MESSAGE_USAGE); + argsAreNameValuePair(args, MESSAGE_INVALID_FORMAT); + validateName(args, NAME_TO_ARGUMENT_MAP, MESSAGE_INVALID_FORMAT); + + // Map the object of the parsed value to {@code DeleteArgument} + // instance. + // Target code should not be null. + // Target year is null if and only if target semester is null. + EnumMap argMap = parseValues(args); + targetCodeNotNull(argMap.get(DeleteArgument.TARGET_CODE), + MESSAGE_TARGET_CODE_REQUIRED); + targetYearNullIffTargetSemesterNull( + argMap.get(DeleteArgument.TARGET_YEAR), + argMap.get(DeleteArgument.TARGET_SEMESTER), + MESSAGE_YEAR_AND_SEMESTER_XOR_NULL); + + // Return delete module command for execution. + return new DeleteModuleCommand(argMap); + } + + /** + * Parse the value into its relevant object and put it in {@code argMap}. + * + * @param args array of name-value pair arguments + * @throws ParseException thrown when the value cannot be parsed + */ + private EnumMap parseValues(String[] args) + throws ParseException { + // Initialise argument map. + EnumMap argMap = + new EnumMap<>(DeleteArgument.class); + + for (int index = 0; index < args.length; index = index + 2) { + DeleteArgument name = NAME_TO_ARGUMENT_MAP.get(args[index]); + Object value = name.getValue(args[index + 1]); + argMap.put(name, value); + } + + return argMap; + } +} diff --git a/src/main/java/seedu/address/logic/parser/EditModuleCommandParser.java b/src/main/java/seedu/address/logic/parser/EditModuleCommandParser.java new file mode 100644 index 000000000000..af9993ab04eb --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/EditModuleCommandParser.java @@ -0,0 +1,197 @@ +package seedu.address.logic.parser; + +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toSet; +import static seedu.address.logic.parser.ParserUtil.argsAreNameValuePair; +import static seedu.address.logic.parser.ParserUtil.argsWithBounds; +import static seedu.address.logic.parser.ParserUtil.parseException; +import static seedu.address.logic.parser.ParserUtil.targetCodeNotNull; +import static seedu.address.logic.parser.ParserUtil.targetYearNullIffTargetSemesterNull; +import static seedu.address.logic.parser.ParserUtil.tokenize; +import static seedu.address.logic.parser.ParserUtil.validateName; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.IntStream; + +import com.google.common.collect.ImmutableSet; + +import seedu.address.logic.commands.EditModuleCommand; +import seedu.address.logic.parser.arguments.EditArgument; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * {@code EditModuleCommandParser} parses input arguments for + * {@code EditModuleCommand}. + */ +public class EditModuleCommandParser implements Parser { + + /** + * Message that informs that the command is in a wrong format and + * prints the usage for edit command. + */ + public static final String MESSAGE_INVALID_FORMAT = + ParserUtil.MESSAGE_INVALID_FORMAT + + "\n" + + EditModuleCommand.MESSAGE_USAGE; + + /** + * Message that informs that the command does not lead to any changes. + */ + public static final String MESSAGE_NO_NEW_VALUE = "No new value provided.\n" + + EditModuleCommand.MESSAGE_USAGE; + + /** + * Message that informs that the target code is required and prints the + * usage for edit command. + */ + public static final String MESSAGE_TARGET_CODE_REQUIRED = + ParserUtil.MESSAGE_TARGET_CODE_REQUIRED + + "\n" + + EditModuleCommand.MESSAGE_USAGE; + + /** + * Message that informs that target year has to be specified if and only if + * semester is specified, and prints the usage for edit command. + */ + public static final String MESSAGE_YEAR_AND_SEMESTER_XOR_NULL = + ParserUtil.MESSAGE_YEAR_AND_SEMESTER_XOR_NULL + + "\n" + + EditModuleCommand.MESSAGE_USAGE; + + /** + * Immutable map that maps string argument to edit argument enum. + */ + private static final Map NAME_TO_ARGUMENT_MAP; + + /** + * Immutable set containing the allowable size of arguments. + */ + private static final Set ALLOWED_ARG_SIZE; + + /** + * Populate {@code NAME_TO_ARGUMENT_MAP} with short name and long name as + * key and the respective {@code EditArgument} instance as value. + */ + static { + Map map = new HashMap<>(); + for (EditArgument instance : EditArgument.values()) { + map.put(instance.getShortName(), instance); + map.put(instance.getLongName(), instance); + } + NAME_TO_ARGUMENT_MAP = Collections.unmodifiableMap(map); + } + + /** + * Populate {@code ALLOWED_ARG_SIZE} with a set of numbers representing the + * allowed argument sizes. + */ + static { + ALLOWED_ARG_SIZE = IntStream.range(4, 17) + .filter(index -> index % 2 == 0) + .boxed() + .collect(collectingAndThen(toSet(), ImmutableSet::copyOf)); + } + + /** + * Parses {@code argsInString} in the context of {@code EditModuleCommand} + * and returns {@code EditModuleCommand} for execution. + *

+ * Throws {@code ParseException} when: + *

    + *
  • Number of argument is not between 4 and 16.
  • + *
  • Number of argument is not even.
  • + *
  • Argument is not in name-value pair format
  • + *
  • Argument contains illegal name
  • + *
  • Same name appeared more than once
  • + *
  • Target code is not provided.
  • + *
  • Target year is provided but target semester is not provided.
  • + *
  • Target semester is provided but target year is not provided.
  • + *
  • No new value provided.
  • + *
  • Unable to parse any field.
  • + *
+ * + * @param argsInString String that contains all the argument + * @return {@code EditModuleCommand} object for execution + * @throws ParseException thrown when user input does not conform to the + * expected format + */ + public EditModuleCommand parse(String argsInString) throws ParseException { + // Converts argument string to tokenize argument array. + String[] args = tokenize(argsInString); + + // Size of argument should be between 4 to 16. + // Size of argument should be even. + // Arguments should be in name-value pair. + // Name should be legal. + // No duplicate name. + argsWithBounds(args, ALLOWED_ARG_SIZE, EditModuleCommand.MESSAGE_USAGE); + argsAreNameValuePair(args, MESSAGE_INVALID_FORMAT); + validateName(args, NAME_TO_ARGUMENT_MAP, MESSAGE_INVALID_FORMAT); + + // Parse values. + parseValues(args); + + // Map the object of the parsed value to {@code EditArgument} instance. + // Target code should not be null. + // Target year is null if and only if target semester is null. + // At least one new value should be specified. + EnumMap argMap = parseValues(args); + targetCodeNotNull( + argMap.get(EditArgument.TARGET_CODE), + MESSAGE_TARGET_CODE_REQUIRED); + targetYearNullIffTargetSemesterNull( + argMap.get(EditArgument.TARGET_YEAR), + argMap.get(EditArgument.TARGET_SEMESTER), + MESSAGE_YEAR_AND_SEMESTER_XOR_NULL); + atLeastOneNewValueSpecified(argMap); + + // Return edit module command for execution. + return new EditModuleCommand(argMap); + } + + /** + * Parses the value into its relevant object. + * + * @param args array of name-value pair arguments + * @throws ParseException thrown when the value cannot be parsed + */ + private EnumMap parseValues(String[] args) + throws ParseException { + // Initialise argument map + EnumMap argMap = + new EnumMap<>(EditArgument.class); + + for (int index = 0; index < args.length; index = index + 2) { + EditArgument name = NAME_TO_ARGUMENT_MAP.get(args[index]); + Object value = name.getValue(args[index + 1]); + argMap.put(name, value); + } + + return argMap; + } + + /** + * Checks that one of code, year, semester, credit, or grade should have a + * new value. + * + * @throws ParseException Thrown when code, year, semester, credit, and + * grade are all null + */ + private void atLeastOneNewValueSpecified(EnumMap argMap) throws ParseException { + boolean allNewValueIsNull = argMap.entrySet() + .stream() + .filter(entry -> entry.getKey().name().startsWith("NEW")) + .map(entry -> entry.getValue()) + .allMatch(Objects::isNull); + + if (allNewValueIsNull) { + throw parseException(MESSAGE_NO_NEW_VALUE); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/GoalCommandParser.java b/src/main/java/seedu/address/logic/parser/GoalCommandParser.java new file mode 100644 index 000000000000..d288bdaec6d3 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/GoalCommandParser.java @@ -0,0 +1,32 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.logic.commands.GoalCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +//@@author jeremiah-ang +/** + * Parses User Input + */ +public class GoalCommandParser implements Parser { + public static final String MESSAGE_OUT_OF_RANGE = "CAP Goal out of range! Should be between 0 and 5 inclusive."; + @Override + public GoalCommand parse(String userInput) throws ParseException { + final String trimmedArgs = userInput.trim(); + final String format = String.format(MESSAGE_INVALID_COMMAND_FORMAT, GoalCommand.MESSAGE_USAGE); + if (trimmedArgs.isEmpty()) { + throw new ParseException(format); + } + + try { + double newGoal = Double.parseDouble(trimmedArgs); + if (newGoal < 0 || newGoal > 5) { + throw new ParseException(MESSAGE_OUT_OF_RANGE); + } + return new GoalCommand(newGoal); + } catch (NumberFormatException nfe) { + throw new ParseException(format); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index 76daf40807e2..3a5674ddff84 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -4,28 +4,355 @@ import java.util.Collection; import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.StringUtil; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.module.Code; +import seedu.address.model.module.Credit; +import seedu.address.model.module.Grade; +import seedu.address.model.module.Semester; +import seedu.address.model.module.Year; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; +//@@author alexkmj /** - * Contains utility methods used for parsing strings in the various *Parser classes. + * Contains utility methods used for parsing strings in the various + * Parser classes. */ public class ParserUtil { + /** + * Message that informs that the command is in a wrong format. + */ + public static final String MESSAGE_INVALID_FORMAT = "Invalid format"; + + /** + * TODO: Remove legacy code. + */ + public static final String MESSAGE_INVALID_INDEX = "Invalid index"; + + /** + * Message that informs that the target code is required. + */ + public static final String MESSAGE_TARGET_CODE_REQUIRED = "Target code" + + " required."; + + /** + * Message that informs that target year has to be specified if and only if + * semester is specified. + */ + public static final String MESSAGE_YEAR_AND_SEMESTER_XOR_NULL = "Year can" + + " only be specified if and only if semester is also specifed."; + + /** + * Prefix used for short name. + */ + public static final String NAME_PREFIX_SHORT = "-"; + + /** + * Prefix used for long name. + */ + public static final String NAME_PREFIX_LONG = "--"; + + /** + * Tokenize arguments in a string into an argument array. + * + * @param args non-null string that contains the arguments + * @return tokenized argument array + */ + public static String[] tokenize(String args) { + requireNonNull(args); + String trimmedArgs = args.trim(); + return trimmedArgs.split("\\s+"); + } + + /** + * Size of {@code args} equals to the size of {@code size}. + *

+ * Throws {@code ParseException} when size of {@code args} is not equal to + * {@code size}. + * + * @param args argument array to validate + * @param size the size that the argument array should have + * @throws ParseException when the number of arguments is not equal to + * {@code size}. + */ + public static void argsWithBounds(Object[] args, Set size) + throws ParseException { + requireNonNull(args); + argsWithBounds(args, size, ""); + } + + /** + * Size of {@code args} is between {@code min} and {@code max}. + *

+ * Throws {@code ParseException} when size of {@code args} is not between + * {@code min} and {@code max}. + * + * @param args argument array to validate + * @param min the minimum size allowed for {@code args} + * @param max the maximum size allowed for {@code args} + * @throws ParseException when the number of arguments is not equal between + * {@code min} and {@code max}. + */ + public static void argsWithBounds(Object[] args, int min, int max, String inputSuggestion) + throws ParseException { + requireNonNull(args); + + if (args.length < min || args.length > max) { + throw new ParseException("Invalid number of arguments!" + + " Number of arguments should be more than or equal to " + + min + + " and less than or equal to " + + max + + "\n" + + inputSuggestion); + } + } + + /** + * Size of {@code args} is in {@code allowedSize}. + *

+ * Throws {@code ParseException} when size of {@code args} is not in + * {@code allowedSize}. + * + * @param args argument array to validate + * @param allowedSize set containing size that {@code args} is allowed to + * have + * @throws ParseException when the number of arguments is not in + * {@code allowedSize} + */ + public static void argsWithBounds(Object[] args, + Set allowedSize, String inputSuggestion) throws ParseException { + requireNonNull(args); + + if (!allowedSize.contains(args.length)) { + String allowedNumOfArgs = allowedSize.stream() + .sorted() + .map(Objects::toString) + .collect(Collectors.joining(", ")); + + throw parseException("Invalid number of arguments! " + + "Number of arguments should be " + + allowedNumOfArgs + + "\n" + + inputSuggestion); + } + } + + /** + * All arguments in {@code args} conforms to the name-value pair format. + *

+ * For all of the arguments in the argument array, odd arguments must be a + * name and even arguments must be a value. Throws {@code ParseException} + * when argument is not in name value pair format. + *

+ * Valid: -name1 value1 -name2 value2 + *

+ * Invalid: -name1 -name2 value2 + *

+ * Invalid: -name1 value1 value2 + * + * @param args argument array that contains the name-value pair + * @param errorMsg error message shown when ParseException is thrown + * @throws ParseException thrown when argument array does not conform to + * name-value pair format + */ + public static void argsAreNameValuePair(String[] args, String errorMsg) + throws ParseException { + boolean invalidFormat = IntStream.range(0, args.length) + .mapToObj(index -> { + boolean isEven = index % 2 == 0; + boolean isName = isName(args[index]); + return isEven == isName; + }) + .anyMatch(booleanValue -> !booleanValue); + + if (invalidFormat) { + throw parseException(errorMsg); + } + } + + /** + * Returns true if argument is a name. + *

+ * {@code argument} is a name if it starts with {@code NAME_PREFIX_SHORT} + * or {@code NAME_PREFIX_LONG}. + * + * @param argument argument to be checked + * @return true if argument is a name. + */ + private static boolean isName(String argument) { + return argument.startsWith(NAME_PREFIX_SHORT) + || argument.startsWith(NAME_PREFIX_LONG); + } + + /** + * Argument array does not contain the same name twice and all names are + * legal. + * + * @param args array of name-value pair arguments + * @param nameToArgMap map that maps {@code T} to string which is the name + * @param errorMsg message shown when {@code ParseException} is + * thrown + * @param the argument enum + * @throws ParseException thrown when there are duplicate or illegal name. + */ + public static void validateName(String[] args, + Map nameToArgMap, String errorMsg) + throws ParseException { + List nameArray = IntStream.range(0, args.length) + .filter(index -> index % 2 == 0) + .mapToObj(index -> nameToArgMap.get(args[index])) + .collect(Collectors.toList()); + + boolean illegalNameExist = nameArray.stream() + .anyMatch(Objects::isNull); + + if (illegalNameExist) { + throw parseException(errorMsg); + } + + Set nameSet = new HashSet<>(nameArray); + + if (nameArray.size() != nameSet.size()) { + throw parseException(errorMsg); + } + } + + /** + * Target code is not null. + * + * @param targetCode {@code Code} that identifies the target {@code Module} + * @param errorMsg message shown when {@code ParseException} is thrown + * @throws ParseException thrown when target code is null + */ + public static void targetCodeNotNull(Object targetCode, String errorMsg) + throws ParseException { + if (targetCode == null) { + throw parseException(errorMsg); + } + } + + /** + * Target year and target semester cannot be exclusively null. + * + * @throws ParseException thrown when target year and target semester is + * exclusively null + */ + public static void targetYearNullIffTargetSemesterNull(Object targetYear, + Object targetSemester, String errorMsg) throws ParseException { + if (targetYear == null ^ targetSemester == null) { + throw parseException(errorMsg); + } + } + + /** + * Creates parse exception with the error message. + * + * @param errorMsg error messge for the exception + * @return {@code ParseException} with {@code errorMsg} as the message + */ + public static ParseException parseException(String errorMsg) { + String messageError = String.format(errorMsg); + return new ParseException(messageError); + } + + /** + * Parses a {@code String code} into a {@code Code}. Leading and trailing + * whitespaces will be trimmed. + * + * @throws ParseException thrown when the given {@code code} is invalid. + */ + public static Code parseCode(String args) throws ParseException { + requireNonNull(args); + String trimmedCode = args.trim(); + trimmedCode = trimmedCode.toUpperCase(); + + if (!Code.isValidCode(trimmedCode)) { + throw new ParseException(Code.MESSAGE_CODE_CONSTRAINTS); + } + return new Code(trimmedCode); + } + + /** + * Parses a {@code String year} into a {@code Year}. Leading and trailing + * whitespaces will be trimmed. + * + * @throws ParseException thrown when the given {@code year} is invalid. + */ + public static Year parseYear(String args) throws ParseException { + requireNonNull(args); + String trimmedYear = args.trim(); + if (!Year.isValidYear(trimmedYear)) { + throw new ParseException(Year.MESSAGE_YEAR_CONSTRAINTS); + } + return new Year(trimmedYear); + } + + /** + * Parses a {@code String semester} into a {@code Semester}. Leading and + * trailing whitespaces will be trimmed. + * + * @throws ParseException thrown when the given {@code semester} is invalid. + */ + public static Semester parseSemester(String args) throws ParseException { + requireNonNull(args); + String trimmedSemester = args.trim(); + if (!Semester.isValidSemester(trimmedSemester)) { + throw new ParseException(Semester.MESSAGE_SEMESTER_CONSTRAINTS); + } + return new Semester(trimmedSemester); + } + + /** + * Parses a {@code String credit} into a {@code Credit}. Leading and + * trailing whitespaces will be trimmed. + * + * @throws ParseException thrown when the given {@code credit} is invalid. + */ + public static Credit parseCredit(String args) throws ParseException { + requireNonNull(args); + String trimmedCredit = args.trim(); + int intCredit = Integer.parseInt(trimmedCredit); + if (!Credit.isValidCredit(intCredit)) { + throw new ParseException(Credit.MESSAGE_CREDIT_CONSTRAINTS); + } + return new Credit(intCredit); + } - public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; + /** + * Parses a {@code String grade} into a {@code Grade}. Leading and trailing + * whitespaces will be trimmed. + * + * @throws ParseException thrown when the given {@code grade} is invalid. + */ + public static Grade parseGrade(String args) throws ParseException { + requireNonNull(args); + String trimmedGrade = args.trim(); + if (!Grade.isValidGrade(trimmedGrade)) { + throw new ParseException(Grade.MESSAGE_GRADE_CONSTRAINTS); + } + return new Grade(trimmedGrade); + } /** - * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be - * trimmed. - * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). + * TODO: Remove legacy code. + * Parses {@code oneBasedIndex} into an {@code Index} and returns it. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException thrown when the specified index is invalid + * (not non-zero unsigned integer). */ public static Index parseIndex(String oneBasedIndex) throws ParseException { String trimmedIndex = oneBasedIndex.trim(); @@ -36,8 +363,9 @@ public static Index parseIndex(String oneBasedIndex) throws ParseException { } /** - * Parses a {@code String name} into a {@code Name}. - * Leading and trailing whitespaces will be trimmed. + * TODO: Remove legacy code. + * Parses a {@code String name} into a {@code Name}. Leading and trailing + * whitespaces will be trimmed. * * @throws ParseException if the given {@code name} is invalid. */ @@ -51,8 +379,9 @@ public static Name parseName(String name) throws ParseException { } /** - * Parses a {@code String phone} into a {@code Phone}. - * Leading and trailing whitespaces will be trimmed. + * TODO: Remove legacy code. + * Parses a {@code String phone} into a {@code Phone}. Leading and trailing + * whitespaces will be trimmed. * * @throws ParseException if the given {@code phone} is invalid. */ @@ -66,8 +395,9 @@ public static Phone parsePhone(String phone) throws ParseException { } /** - * Parses a {@code String address} into an {@code Address}. - * Leading and trailing whitespaces will be trimmed. + * TODO: Remove legacy code. + * Parses a {@code String address} into an {@code Address}. Leading and + * trailing whitespaces will be trimmed. * * @throws ParseException if the given {@code address} is invalid. */ @@ -81,8 +411,9 @@ public static Address parseAddress(String address) throws ParseException { } /** - * Parses a {@code String email} into an {@code Email}. - * Leading and trailing whitespaces will be trimmed. + * TODO: Remove legacy code. + * Parses a {@code String email} into an {@code Email}. Leading and trailing + * whitespaces will be trimmed. * * @throws ParseException if the given {@code email} is invalid. */ @@ -96,10 +427,7 @@ public static Email parseEmail(String email) throws ParseException { } /** - * Parses a {@code String tag} into a {@code Tag}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code tag} is invalid. + * TODO: Remove legacy code. */ public static Tag parseTag(String tag) throws ParseException { requireNonNull(tag); @@ -111,9 +439,11 @@ public static Tag parseTag(String tag) throws ParseException { } /** + * TODO: Remove legacy code. * Parses {@code Collection tags} into a {@code Set}. */ - public static Set parseTags(Collection tags) throws ParseException { + public static Set parseTags(Collection tags) + throws ParseException { requireNonNull(tags); final Set tagSet = new HashSet<>(); for (String tagName : tags) { diff --git a/src/main/java/seedu/address/logic/parser/TranscriptParser.java b/src/main/java/seedu/address/logic/parser/TranscriptParser.java new file mode 100644 index 000000000000..0059086a6ea4 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/TranscriptParser.java @@ -0,0 +1,87 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import seedu.address.logic.commands.AddModuleCommand; +import seedu.address.logic.commands.AdjustCommand; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.DeleteModuleCommand; +import seedu.address.logic.commands.EditModuleCommand; +import seedu.address.logic.commands.ExitCommand; +import seedu.address.logic.commands.GoalCommand; +import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.HistoryCommand; +import seedu.address.logic.commands.RedoModuleCommand; +import seedu.address.logic.commands.UndoModuleCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +//@@author alexkmj +/** + * Parses user input. + */ +public class TranscriptParser { + + /** + * Used for initial separation of command word and args. + */ + private static final Pattern BASIC_COMMAND_FORMAT = Pattern + .compile("(?\\S+)(?.*)"); + + /** + * Parses user input into command for execution. + * + * @param userInput full user input string + * @return the command based on the user input + * @throws ParseException if the user input does not conform the expected format + */ + public Command parseCommand(String userInput) throws ParseException { + final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); + if (!matcher.matches()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); + } + + final String commandWord = matcher.group("commandWord").replaceFirst("c_", ""); + final String arguments = matcher.group("arguments"); + + switch (commandWord) { + case AddModuleCommand.COMMAND_WORD: + return new AddModuleCommandParser().parse(arguments); + + case AdjustCommand.COMMAND_WORD: + return new AdjustCommandParser().parse(arguments); + + case DeleteModuleCommand.COMMAND_WORD: + return new DeleteModuleCommandParser().parse(arguments); + + case EditModuleCommand.COMMAND_WORD: + return new EditModuleCommandParser().parse(arguments); + + case GoalCommand.COMMAND_WORD: + return new GoalCommandParser().parse(arguments); + + case RedoModuleCommand.COMMAND_WORD: + return new RedoModuleCommand(); + + case UndoModuleCommand.COMMAND_WORD: + return new UndoModuleCommand(); + + case HistoryCommand.COMMAND_WORD: + return new HistoryCommand(); + + case ExitCommand.COMMAND_WORD: + return new ExitCommand(); + + case HelpCommand.COMMAND_WORD: + return new HelpCommand(); + + default: + throw new ParseException(MESSAGE_UNKNOWN_COMMAND); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/arguments/AddArgument.java b/src/main/java/seedu/address/logic/parser/arguments/AddArgument.java new file mode 100644 index 000000000000..02a33d74cd4f --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/arguments/AddArgument.java @@ -0,0 +1,126 @@ +package seedu.address.logic.parser.arguments; + +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Basic enum for AddArgument. + */ +public enum AddArgument { + NEW_CODE('m', "code") { + /** + * Parses value into code. + * + * @param value new code in string + * @return {@code Code} cast into {@code Object} + * @throws ParseException Thrown when code cannot be parsed + */ + @Override + public Object getValue(String value) throws ParseException { + return ParserUtil.parseCode(value); + } + }, + NEW_YEAR('y', "year") { + /** + * Parses value into year. + * + * @param value new year in string + * @return {@code Year} cast into {@code Object} + * @throws ParseException Thrown when year cannot be parsed + */ + @Override + public Object getValue(String value) throws ParseException { + return ParserUtil.parseYear(value); + } + }, + NEW_SEMESTER('s', "semester") { + /** + * Parses value into semester. + * + * @param value new semester in string + * @return {@code Semester} cast into {@code Object} + * @throws ParseException Thrown when semester cannot be parsed + */ + @Override + public Object getValue(String value) throws ParseException { + return ParserUtil.parseSemester(value); + } + }, + NEW_CREDIT('c', "credit") { + /** + * Parses value into credit. + * + * @param value new credit in string + * @return {@code Credit} cast into {@code Object} + * @throws ParseException Thrown when credit cannot be parsed + */ + @Override + public Object getValue(String value) throws ParseException { + return ParserUtil.parseCredit(value); + } + }, + NEW_GRADE('g', "grade") { + /** + * Parses value into grade. + * + * @param value new grade in string + * @return {@code Grade} cast into {@code Object} + * @throws ParseException Thrown when grade cannot be parsed + */ + @Override + public Object getValue(String value) throws ParseException { + return ParserUtil.parseGrade(value); + } + }; + + /** + * A character that represents the name for the particular name-value pair. + */ + private char shortName; + + /** + * A string that represents the name for a the particular name-value pair. + */ + private String longName; + + /** + * Constructor that takes in the short name and long name. + * + * @param shortName short name of the name of a name-value pair + * @param longName long name of the name of a name-value pair + */ + AddArgument(char shortName, String longName) { + this.shortName = shortName; + this.longName = longName; + } + + /** + * Returns the short name of a name-value pair. It is a prefix and a + * character representing the name. + * + * @return short name of the name-value pair + */ + public String getShortName() { + return ParserUtil.NAME_PREFIX_SHORT + shortName; + } + + /** + * Returns the long name of a name-value pair. It is a long prefix and a + * string representing the name. + * + * @return long name of the name-value pair + */ + public String getLongName() { + return ParserUtil.NAME_PREFIX_LONG + longName; + } + + /** + * Returns null. + *

+ * Overwritten by the specific variable that will return the object + * associated to it. + */ + public Object getValue(String value) throws ParseException { + return null; + } +} diff --git a/src/main/java/seedu/address/logic/parser/arguments/DeleteArgument.java b/src/main/java/seedu/address/logic/parser/arguments/DeleteArgument.java new file mode 100644 index 000000000000..45c3269c5064 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/arguments/DeleteArgument.java @@ -0,0 +1,101 @@ +package seedu.address.logic.parser.arguments; + +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Basic enum for DeleteArgument. + */ +public enum DeleteArgument { + TARGET_CODE('t', "targetCode") { + /** + * Parses value into code. + * + * @param value target code in string + * @return {@code Code} of the targeted module cast into a generic + * {@code Object} + * @throws ParseException Thrown when code cannot be parsed + */ + @Override + public Object getValue(String value) throws ParseException { + return ParserUtil.parseCode(value); + } + }, + TARGET_YEAR('e', "targetYear") { + /** + * Parses value into year. + * + * @param value target year in string + * @return {@code Year} cast into {@code Object} + * @throws ParseException Thrown when year cannot be parsed + */ + @Override + public Object getValue(String value) throws ParseException { + return ParserUtil.parseYear(value); + } + }, + TARGET_SEMESTER('z', "targetSemester") { + /** + * Parses value into semester. + * + * @param value target semester in string + * @return {@code Semester} cast into {@code Object} + * @throws ParseException Thrown when semester cannot be parsed + */ + @Override + public Object getValue(String value) throws ParseException { + return ParserUtil.parseSemester(value); + } + }; + + /** + * A character that represents the name for the particular name-value pair. + */ + private char shortName; + + /** + * A string that represents the name for a the particular name-value pair. + */ + private String longName; + + /** + * Constructor that takes in the short name and long name. + * + * @param shortName short name of the name of a name-value pair + * @param longName long name of the name of a name-value pair + */ + DeleteArgument(char shortName, String longName) { + this.shortName = shortName; + this.longName = longName; + } + + /** + * Returns the short name of a name-value pair. It is a prefix and a + * character representing the name. + * + * @return short name of the name-value pair + */ + public String getShortName() { + return ParserUtil.NAME_PREFIX_SHORT + shortName; + } + + /** + * Returns the long name of a name-value pair. It is a long prefix and a + * string representing the name. + * + * @return long name of the name-value pair + */ + public String getLongName() { + return ParserUtil.NAME_PREFIX_LONG + longName; + } + + /** + * Returns null. + *

+ * Overwritten by the specific variable that will return the object + * associated to it. + */ + public Object getValue(String value) throws ParseException { + return null; + } +} diff --git a/src/main/java/seedu/address/logic/parser/arguments/EditArgument.java b/src/main/java/seedu/address/logic/parser/arguments/EditArgument.java new file mode 100644 index 000000000000..60102e102d0b --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/arguments/EditArgument.java @@ -0,0 +1,166 @@ +package seedu.address.logic.parser.arguments; + +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Basic enum for EditArgument. + */ +public enum EditArgument { + TARGET_CODE('t', "targetCode") { + /** + * Parses value into code. + * + * @param value target code in string + * @return {@code Code} of the targeted module cast into a generic + * {@code Object} + * @throws ParseException Thrown when code cannot be parsed + */ + @Override + public Object getValue(String value) throws ParseException { + return ParserUtil.parseCode(value); + } + }, + TARGET_YEAR('e', "targetYear") { + /** + * Parses value into year. + * + * @param value target year in string + * @return {@code Year} cast into {@code Object} + * @throws ParseException Thrown when year cannot be parsed + */ + @Override + public Object getValue(String value) throws ParseException { + return ParserUtil.parseYear(value); + } + }, + TARGET_SEMESTER('z', "targetSemester") { + /** + * Parses value into semester. + * + * @param value target semester in string + * @return {@code Semester} cast into {@code Object} + * @throws ParseException Thrown when semester cannot be parsed + */ + @Override + public Object getValue(String value) throws ParseException { + return ParserUtil.parseSemester(value); + } + }, + NEW_CODE('m', "code") { + /** + * Parses value into code. + * + * @param value new code in string + * @return {@code Code} cast into {@code Object} + * @throws ParseException Thrown when code cannot be parsed + */ + @Override + public Object getValue(String value) throws ParseException { + return ParserUtil.parseCode(value); + } + }, + NEW_YEAR('y', "year") { + /** + * Parses value into year. + * + * @param value new year in string + * @return {@code Year} cast into {@code Object} + * @throws ParseException Thrown when year cannot be parsed + */ + @Override + public Object getValue(String value) throws ParseException { + return ParserUtil.parseYear(value); + } + }, + NEW_SEMESTER('s', "semester") { + /** + * Parses value into semester. + * + * @param value new semester in string + * @return {@code Semester} cast into {@code Object} + * @throws ParseException Thrown when semester cannot be parsed + */ + @Override + public Object getValue(String value) throws ParseException { + return ParserUtil.parseSemester(value); + } + }, + NEW_CREDIT('c', "credit") { + /** + * Parses value into credit. + * + * @param value new credit in string + * @return {@code Credit} cast into {@code Object} + * @throws ParseException Thrown when credit cannot be parsed + */ + @Override + public Object getValue(String value) throws ParseException { + return ParserUtil.parseCredit(value); + } + }, + NEW_GRADE('g', "grade") { + /** + * Parses value into grade. + * + * @param value new grade in string + * @return {@code Grade} cast into {@code Object} + * @throws ParseException Thrown when grade cannot be parsed + */ + @Override + public Object getValue(String value) throws ParseException { + return ParserUtil.parseGrade(value); + } + }; + + /** + * A character that represents the name for the particular name-value pair. + */ + private char shortName; + + /** + * A string that represents the name for a the particular name-value pair. + */ + private String longName; + + /** + * Constructor that takes in the short name and long name. + * + * @param shortName short name of the name of a name-value pair + * @param longName long name of the name of a name-value pair + */ + EditArgument(char shortName, String longName) { + this.shortName = shortName; + this.longName = longName; + } + + /** + * Returns the short name of a name-value pair. It is a prefix and a + * character representing the name. + * + * @return short name of the name-value pair + */ + public String getShortName() { + return ParserUtil.NAME_PREFIX_SHORT + shortName; + } + + /** + * Returns the long name of a name-value pair. It is a long prefix and a + * string representing the name. + * + * @return long name of the name-value pair + */ + public String getLongName() { + return ParserUtil.NAME_PREFIX_LONG + longName; + } + + /** + * Returns null. + *

+ * Overwritten by the specific variable that will return the object + * associated to it. + */ + public Object getValue(String value) throws ParseException { + return null; + } +} diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index ac4521f33199..213ca6a10b0b 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -3,76 +3,210 @@ import java.util.function.Predicate; import javafx.collections.ObservableList; + +import seedu.address.model.capgoal.CapGoal; +import seedu.address.model.module.Code; +import seedu.address.model.module.Grade; +import seedu.address.model.module.Module; +import seedu.address.model.module.Semester; +import seedu.address.model.module.Year; +import seedu.address.model.module.exceptions.ModuleNotFoundException; +import seedu.address.model.module.exceptions.MultipleModuleEntryFoundException; import seedu.address.model.person.Person; /** * The API of the Model component. */ public interface Model { - /** {@code Predicate} that always evaluate to true */ + // TODO: REMOVE LEGACY CODE Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; - /** Clears existing backing model and replaces with the provided new data. */ + //@@author alexkmj + /** + * {@code Predicate} that always evaluate to true. + */ + Predicate PREDICATE_SHOW_ALL_MODULES = unused -> true; + + /** + * Clears existing backing model and replaces with the newly provided data. + * + * @param replacement the replacement. + */ + void resetData(ReadOnlyTranscript replacement); + + // TODO: REMOVE LEGACY CODE void resetData(ReadOnlyAddressBook newData); - /** Returns the AddressBook */ - ReadOnlyAddressBook getAddressBook(); + /** + * Returns the Transcript. + * + * @return read only version of the transcript + */ + ReadOnlyTranscript getTranscript(); /** - * Returns true if a person with the same identity as {@code person} exists in the address book. + * Returns true if a module with the same identity as {@code module} exists + * in the transcript. + * + * @param module module to find in the transcript + * @return true if module exists in transcript */ - boolean hasPerson(Person person); + boolean hasModule(Module module); /** - * Deletes the given person. - * The person must exist in the address book. + * Returns the matching module entry. + *

+ * Finds the module with {@code targetCode}, {@code targetYear} if + * {@code targetYear} is not null, and {@code targetSemester} if + * {@code targetSemester} is not null. + * + * @param targetCode code to match + * @param targetYear year to match if not null + * @param targetSemester semester to match if not null + * @return the matching module + * @throws ModuleNotFoundException thrown when no entries match the + * parameters. + * @throws MultipleModuleEntryFoundException thrown when multiple entries + * match the parameters. */ - void deletePerson(Person target); + Module getOnlyOneModule(Code targetCode, Year targetYear, + Semester targetSemester) + throws ModuleNotFoundException, MultipleModuleEntryFoundException; /** - * Adds the given person. - * {@code person} must not already exist in the address book. + * Adds the given module. + *

+ * {@code module} must not already exist in the transcript. + * + * @param module module to be added into the transcript */ - void addPerson(Person person); + void addModule(Module module); /** - * Replaces the given person {@code target} with {@code editedPerson}. - * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. + * Deletes the given module. + *

+ * The module must exist in the transcript. + * + * @param target module to be deleted from the transcript */ - void updatePerson(Person target, Person editedPerson); + void deleteModule(Module target); - /** Returns an unmodifiable view of the filtered person list */ - ObservableList getFilteredPersonList(); + /** + * Replaces the given module {@code target} with {@code editedModule}. + * {@code target} must exist in the transcript. The module identity of + * {@code editedModule} must not be the same as another existing module in + * the transcript. + * + * @param target module to be updated + * @param editedModule the updated version of the module + */ + void updateModule(Module target, Module editedModule); /** - * Updates the filter of the filtered person list to filter by the given {@code predicate}. - * @throws NullPointerException if {@code predicate} is null. + * Returns an unmodifiable view of the list of {@code Module} backed by the + * internal list of {@code versionedTranscript} */ - void updateFilteredPersonList(Predicate predicate); + ObservableList getFilteredModuleList(); /** - * Returns true if the model has previous address book states to restore. + * Updates the filter of the filtered module list to filter by the given + * {@code predicate}. */ - boolean canUndoAddressBook(); + void updateFilteredModuleList(Predicate predicate); /** - * Returns true if the model has undone address book states to restore. + * Returns true if the model has previous transcript states to restore. + * + * @return true if transcript can be undone */ - boolean canRedoAddressBook(); + boolean canUndoTranscript(); /** - * Restores the model's address book to its previous state. + * Returns true if the model has undone transcript states to restore. + * + * @return true if transcript can be redone */ - void undoAddressBook(); + boolean canRedoTranscript(); /** - * Restores the model's address book to its previously undone state. + * Restores the model's transcript to its previous state. */ - void redoAddressBook(); + void undoTranscript(); + + /** + * Restores the model's transcript to its previously undone state. + */ + void redoTranscript(); + + /** + * Saves the current transcript state for undo/redo. + */ + void commitTranscript(); + //@@author + + //@@author jeremiah-ang + /** + * Get the cap goal of the current transcript. + */ + CapGoal getCapGoal(); + + /** + * Set the cap goal of the current transcript. + */ + void updateCapGoal(double capGoal); /** - * Saves the current address book state for undo/redo. + * Returns the CAP based on the current Transcript records. */ + double getCap(); + + /** + * Returns an unmodifiable view of list of modules that have completed. + * + * @return completed module list + */ + ObservableList getCompletedModuleList(); + + /** + * Returns an unmodifiable view of list of modules that have yet been + * completed. + * + * @return incomplete module list + */ + ObservableList getIncompleteModuleList(); + + /** + * Adjust the target Module to the desired Grade + * @param targetModule + * @param adjustGrade + * @return adjusted Module + */ + Module adjustModule(Module targetModule, Grade adjustGrade); + //@@author + + // TODO: REMOVE LEGACY CODE + + ReadOnlyAddressBook getAddressBook(); + + boolean hasPerson(Person person); + + void deletePerson(Person target); + + void addPerson(Person person); + + void updatePerson(Person target, Person editedPerson); + + ObservableList getFilteredPersonList(); + + void updateFilteredPersonList(Predicate predicate); + + boolean canUndoAddressBook(); + + boolean canRedoAddressBook(); + + void undoAddressBook(); + + void redoAddressBook(); + void commitAddressBook(); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index a664602ef5b1..e84146986235 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -9,43 +9,326 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; + import seedu.address.commons.core.ComponentManager; import seedu.address.commons.core.LogsCenter; import seedu.address.commons.events.model.AddressBookChangedEvent; +import seedu.address.commons.events.model.TranscriptChangedEvent; +import seedu.address.model.capgoal.CapGoal; +import seedu.address.model.module.Code; +import seedu.address.model.module.Grade; +import seedu.address.model.module.Module; +import seedu.address.model.module.Semester; +import seedu.address.model.module.Year; +import seedu.address.model.module.exceptions.ModuleNotFoundException; +import seedu.address.model.module.exceptions.MultipleModuleEntryFoundException; import seedu.address.model.person.Person; /** * Represents the in-memory model of the address book data. */ public class ModelManager extends ComponentManager implements Model { - private static final Logger logger = LogsCenter.getLogger(ModelManager.class); + private static final Logger logger = + LogsCenter.getLogger(ModelManager.class); + //TODO: REMOVE LEGACY CODE private final VersionedAddressBook versionedAddressBook; private final FilteredList filteredPersons; + //@@author alexkmj + private final VersionedTranscript versionedTranscript; + private final FilteredList filteredModules; + + /** + * Initializes a ModelManager with the given transcript and userPrefs. + */ + public ModelManager(ReadOnlyTranscript transcript, UserPrefs userPrefs) { + super(); + requireAllNonNull(transcript, userPrefs); + + logger.fine("Initializing with transcript: " + + transcript + + " and user prefs " + + userPrefs); + + versionedTranscript = new VersionedTranscript(transcript); + filteredModules = new FilteredList<>( + versionedTranscript.getModuleList()); + + //TODO: REMOVE LEGACY CODE + versionedAddressBook = new VersionedAddressBook(new AddressBook()); + filteredPersons = new FilteredList<>( + versionedAddressBook.getPersonList()); + } + + public ModelManager() { + this(new Transcript(), new UserPrefs()); + } + /** * Initializes a ModelManager with the given addressBook and userPrefs. + * TODO: REMOVE LEGACY CODE */ public ModelManager(ReadOnlyAddressBook addressBook, UserPrefs userPrefs) { super(); requireAllNonNull(addressBook, userPrefs); - logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); + logger.fine("Initializing with address book: " + addressBook + + " and user prefs " + userPrefs); versionedAddressBook = new VersionedAddressBook(addressBook); - filteredPersons = new FilteredList<>(versionedAddressBook.getPersonList()); + filteredPersons = new FilteredList<>( + versionedAddressBook.getPersonList()); + versionedTranscript = new VersionedTranscript(new Transcript()); + filteredModules = new FilteredList<>( + versionedTranscript.getModuleList()); } - public ModelManager() { - this(new AddressBook(), new UserPrefs()); + /** + * Clears existing backing model and replaces with the newly provided data. + * + * @param replacement the replacement. + */ + @Override + public void resetData(ReadOnlyTranscript replacement) { + versionedTranscript.resetData(replacement); + indicateTranscriptChanged(); } + //TODO: REMOVE LEGACY CODE @Override public void resetData(ReadOnlyAddressBook newData) { versionedAddressBook.resetData(newData); indicateAddressBookChanged(); } + /** + * Returns the Transcript. + * + * @return read only version of the transcript + */ + @Override + public ReadOnlyTranscript getTranscript() { + return versionedTranscript; + } + + /** + * Raises an event to indicate the model has changed. + */ + private void indicateTranscriptChanged() { + raise(new TranscriptChangedEvent(versionedTranscript)); + } + + /** + * Returns true if a module with the same identity as {@code module} exists + * in the transcript. + * + * @param module module to find in the transcript + * @return true if module exists in transcript + */ + @Override + public boolean hasModule(Module module) { + requireNonNull(module); + return versionedTranscript.hasModule(module); + } + + /** + * Returns the matching module entry. + *

+ * Finds the module with {@code targetCode}, {@code targetYear} if + * {@code targetYear} is not null, and {@code targetSemester} if + * {@code targetSemester} is not null. + * + * @param targetCode code to match + * @param targetYear year to match if not null + * @param targetSemester semester to match if not null + * @return the matching module + * @throws ModuleNotFoundException thrown when no entries match the + * parameters. + * @throws MultipleModuleEntryFoundException thrown when multiple entries + * match the parameters. + */ + @Override + public Module getOnlyOneModule(Code targetCode, Year targetYear, + Semester targetSemester) + throws ModuleNotFoundException, MultipleModuleEntryFoundException { + return versionedTranscript.getOnlyOneModule(targetCode, + targetYear, + targetSemester); + } + + /** + * Adds the given module. + *

+ * {@code module} must not already exist in the transcript. + * + * @param module module to be added into the transcript + */ + @Override + public void addModule(Module module) { + versionedTranscript.addModule(module); + updateFilteredModuleList(PREDICATE_SHOW_ALL_MODULES); + indicateTranscriptChanged(); + } + + /** + * Deletes the given module. + *

+ * The module must exist in the transcript. + * + * @param target module to be deleted from the transcript + */ + @Override + public void deleteModule(Module target) { + versionedTranscript.removeModule(target); + indicateTranscriptChanged(); + } + + /** + * Replaces the given module {@code target} with {@code editedModule}. + * {@code target} must exist in the transcript. The module identity of + * {@code editedModule} must not be the same as another existing module in + * the transcript. + * + * @param target module to be updated + * @param editedModule the updated version of the module + */ + @Override + public void updateModule(Module target, Module editedModule) { + requireAllNonNull(target, editedModule); + + versionedTranscript.updateModule(target, editedModule); + indicateTranscriptChanged(); + } + + //=========== Filtered Module List Accessors =============================== + + /** + * Returns an unmodifiable view of the list of {@code Module} backed by the + * internal list of {@code versionedTranscript} + */ + @Override + public ObservableList getFilteredModuleList() { + return FXCollections.unmodifiableObservableList(filteredModules); + } + + /** + * Updates the filter of the filtered module list to filter by the given + * {@code predicate}. + */ + @Override + public void updateFilteredModuleList(Predicate predicate) { + requireNonNull(predicate); + filteredModules.setPredicate(predicate); + } + + //=========== Undo/Redo ==================================================== + + /** + * Returns true if the model has previous transcript states to restore. + * + * @return true if transcript can be undone + */ + @Override + public boolean canUndoTranscript() { + return versionedTranscript.canUndo(); + } + + /** + * Returns true if the model has undone transcript states to restore. + * + * @return true if transcript can be redone + */ + @Override + public boolean canRedoTranscript() { + return versionedTranscript.canRedo(); + } + + /** + * Restores the model's transcript to its previous state. + */ + @Override + public void undoTranscript() { + versionedTranscript.undo(); + indicateTranscriptChanged(); + } + + /** + * Restores the model's transcript to its previously undone state. + */ + @Override + public void redoTranscript() { + versionedTranscript.redo(); + indicateTranscriptChanged(); + } + + /** + * Saves the current transcript state for undo/redo. + */ + @Override + public void commitTranscript() { + versionedTranscript.commit(); + } + //@@author + + //@@author jeremiah-ang + @Override + public CapGoal getCapGoal() { + return versionedTranscript.getCapGoal(); + } + + @Override + public void updateCapGoal(double capGoal) { + versionedTranscript.setCapGoal(capGoal); + indicateTranscriptChanged(); + } + + @Override + public double getCap() { + return versionedTranscript.getCurrentCap(); + } + + @Override + public ObservableList getCompletedModuleList() { + return versionedTranscript.getCompletedModuleList(); + } + + @Override + public ObservableList getIncompleteModuleList() { + return versionedTranscript.getIncompleteModuleList(); + } + + @Override + public Module adjustModule(Module targetModule, Grade adjustGrade) { + Module adjustedModule = versionedTranscript.adjustModule(targetModule, adjustGrade); + indicateTranscriptChanged(); + return adjustedModule; + } + //@@author + + //@@author alexkmj + @Override + public boolean equals(Object obj) { + // short circuit if same object + if (obj == this) { + return true; + } + + // instanceof handles nulls + if (!(obj instanceof ModelManager)) { + return false; + } + + // state check + ModelManager other = (ModelManager) obj; + return versionedAddressBook.equals(other.versionedAddressBook) + && filteredPersons.equals(other.filteredPersons) // TODO: REMOVE + && filteredModules.equals(other.filteredModules); + } + //@@author + + //TODO: REMOVE LEGACY CODES BELOW + @Override public ReadOnlyAddressBook getAddressBook() { return versionedAddressBook; @@ -83,12 +366,8 @@ public void updatePerson(Person target, Person editedPerson) { indicateAddressBookChanged(); } - //=========== Filtered Person List Accessors ============================================================= + //=========== Filtered Person List Accessors =============================== - /** - * Returns an unmodifiable view of the list of {@code Person} backed by the internal list of - * {@code versionedAddressBook} - */ @Override public ObservableList getFilteredPersonList() { return FXCollections.unmodifiableObservableList(filteredPersons); @@ -100,7 +379,7 @@ public void updateFilteredPersonList(Predicate predicate) { filteredPersons.setPredicate(predicate); } - //=========== Undo/Redo ================================================================================= + //=========== Undo/Redo ==================================================== @Override public boolean canUndoAddressBook() { @@ -128,23 +407,4 @@ public void redoAddressBook() { public void commitAddressBook() { versionedAddressBook.commit(); } - - @Override - public boolean equals(Object obj) { - // short circuit if same object - if (obj == this) { - return true; - } - - // instanceof handles nulls - if (!(obj instanceof ModelManager)) { - return false; - } - - // state check - ModelManager other = (ModelManager) obj; - return versionedAddressBook.equals(other.versionedAddressBook) - && filteredPersons.equals(other.filteredPersons); - } - } diff --git a/src/main/java/seedu/address/model/ReadOnlyTranscript.java b/src/main/java/seedu/address/model/ReadOnlyTranscript.java new file mode 100644 index 000000000000..fa2b7ebcd810 --- /dev/null +++ b/src/main/java/seedu/address/model/ReadOnlyTranscript.java @@ -0,0 +1,44 @@ +package seedu.address.model; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import javafx.collections.ObservableList; + +import seedu.address.model.capgoal.CapGoal; +import seedu.address.model.module.Module; +import seedu.address.storage.JsonTranscriptDeserializer; + +//@@author jeremiah-ang +/** + * Unmodifiable view of a Transcript. + */ +@JsonDeserialize(using = JsonTranscriptDeserializer.class) +public interface ReadOnlyTranscript { + /** + * Returns an unmodifiable view of the module list. + * This list will not contain any duplicate modules. + */ + ObservableList getModuleList(); + + /** + * Returns the CapGoal of this transcript + */ + CapGoal getCapGoal(); + + /** + * Returns the current CAP of this transcript + */ + double getCurrentCap(); + + /** + * Returns an unmodifiable view of list of modules that have completed + * @return completed module list + */ + ObservableList getCompletedModuleList(); + + /** + * Returns an unmodifiable view of list of modules that have yet been completed + * @return incomplete module list + */ + ObservableList getIncompleteModuleList(); +} diff --git a/src/main/java/seedu/address/model/Transcript.java b/src/main/java/seedu/address/model/Transcript.java new file mode 100644 index 000000000000..8856d266ec40 --- /dev/null +++ b/src/main/java/seedu/address/model/Transcript.java @@ -0,0 +1,526 @@ +package seedu.address.model; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import java.util.logging.Logger; +import java.util.stream.Stream; + +import javafx.collections.ObservableList; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.capgoal.CapGoal; +import seedu.address.model.exceptions.CapGoalIsImpossibleException; +import seedu.address.model.exceptions.NoTargetableModulesException; +import seedu.address.model.module.Code; +import seedu.address.model.module.Grade; +import seedu.address.model.module.Module; +import seedu.address.model.module.Semester; +import seedu.address.model.module.UniqueModuleList; +import seedu.address.model.module.Year; +import seedu.address.model.module.exceptions.ModuleCompletedException; +import seedu.address.model.module.exceptions.ModuleNotFoundException; +import seedu.address.model.module.exceptions.MultipleModuleEntryFoundException; + +//@@author alexkmj +/** + * Wraps all data at the transcript level + * Duplicates are not allowed (by .isSameModule comparison) + */ +public class Transcript implements ReadOnlyTranscript { + private static final Logger logger = LogsCenter.getLogger(Transcript.class); + + private final UniqueModuleList modules; + private CapGoal capGoal; + private double currentCap; + + /* + * The 'unusual' code block below is an non-static initialization block, + * sometimes used to avoid duplication between constructors. + * See https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html + * + * Note that non-static init blocks are not recommended to use. There are + * other ways to avoid duplication among constructors. + */ + { + modules = new UniqueModuleList(); + } + + public Transcript() { + capGoal = new CapGoal(); + currentCap = 0; + } + + /** + * Creates an Transcript using the Modules in the {@code toBeCopied} + */ + public Transcript(ReadOnlyTranscript toBeCopied) { + this(); + resetData(toBeCopied); + } + + //// list overwrite operations + + /** + * Replaces the contents of the module list with {@code modules}. + * {@code modules} must not contain duplicate modules. + */ + public void setModules(List modules) { + this.modules.setModules(modules); + modulesUpdated(); + } + + /** + * Resets the existing data of this {@code Transcript} with {@code newData}. + */ + public void resetData(ReadOnlyTranscript newData) { + requireNonNull(newData); + + setModules(newData.getModuleList()); + setCapGoal(newData.getCapGoal()); + } + + //// module-level operations + + /** + * Returns true if a module with the same identity as {@code module} exists + * in the transcript. + */ + public boolean hasModule(Module module) { + requireNonNull(module); + return modules.contains(module); + } + + /** + * Adds a module to the transcript. + * The module must not already exist in the transcript. + */ + public void addModule(Module p) { + modules.add(p); + modulesUpdated(); + } + + /** + * Replaces the given module {@code target} in the list with + * {@code editedModule}. {@code target} must exist in the transcript. + * The module identity of {@code editedModule} must not be the same as + * another existing module in the transcript. + */ + public void updateModule(Module target, Module editedModule) { + requireNonNull(editedModule); + modules.setModule(target, editedModule); + modulesUpdated(); + } + + /** + * Removes {@code key} from this {@code Transcript}. + * {@code key} must exist in the transcript. + */ + public void removeModule(Module key) { + modules.remove(key); + modulesUpdated(); + } + + //@@author jeremiah-ang + @Override + public double getCurrentCap() { + return currentCap; + } + + @Override + public ObservableList getCompletedModuleList() { + return getModuleList().filtered(Module::hasCompleted); + } + + @Override + public ObservableList getIncompleteModuleList() { + return getModuleList().filtered(module -> !module.hasCompleted()); + } + + private void updateCurrentCap() { + logger.info("Updating Current CAP"); + currentCap = calculateCap(); + } + + /** + * Calculate CAP Score based on modules with scores + * + * @return cap: cap score + */ + private double calculateCap() { + ObservableList gradedModulesList = getGradedModulesList(); + double totalModulePoint = calculateTotalModulePoint(gradedModulesList); + double totalModuleCredit = calculateTotalModuleCredit(gradedModulesList); + return calculateCap(totalModulePoint, totalModuleCredit); + } + + /** + * Calculate CAP given total points and credits gained. + * @param totalModulePoint + * @param totalModuleCredit + * @return CAP + */ + private double calculateCap(double totalModulePoint, double totalModuleCredit) { + double cap = 0; + if (totalModuleCredit > 0) { + cap = totalModulePoint / totalModuleCredit; + } + return cap; + } + + + /** + * Calculates the total module point from the list of list of modules + * @param listOfModules + * @return total module point + */ + @SafeVarargs + private double calculateTotalModulePoint(List... listOfModules) { + return Stream.of(listOfModules).mapToDouble(this::calculateTotalModulePoint).sum(); + } + + /** + * Calculates the total module point from the list of modules + * @param modules + * @return total points from list of modules + */ + private double calculateTotalModulePoint(List modules) { + double totalPoint = 0; + for (Module module : modules) { + totalPoint += module.getGrade().getPoint() * module.getCredits().value; + } + return totalPoint; + } + + /** + * Calculates the total module credit from the list of list of modules + * @param listOfModules + * @return total module credit + */ + @SafeVarargs + private double calculateTotalModuleCredit(List... listOfModules) { + return Stream.of(listOfModules).mapToDouble(this::calculateTotalModuleCredit).sum(); + } + + /** + * Calculates the total module credit from the list of modules + * @param modules + * @return total module credit from the list of modules + */ + private double calculateTotalModuleCredit(List modules) { + int totalModuleCredit = 0; + for (Module module : modules) { + totalModuleCredit += module.getCredits().value; + } + return totalModuleCredit; + } + + /** + * Filters for modules that is to be used for CAP calculation + * + * @return list of modules used for CAP calculation + */ + private ObservableList getGradedModulesList() { + return modules.getFilteredModules(this::moduleIsUsedForCapCalculation); + } + + /** + * Filters for modules that is to be assigned a target grade + * @return gradedModulesList: a list of modules used for CAP calculation + */ + private ObservableList getTargetableModulesList() { + return modules.getFilteredModules(Module::isTargetable); + } + + /** + * Filters for modules that have target grades + * @return gradedModulesList: a list of modules used for CAP calculation + */ + protected ObservableList getTargetedModulesList() { + return modules.getFilteredModules(Module::isTargetted); + } + + /** + * Check if the given module should be considered for CAP Calculation + * + * @param module + * @return true if yes, false otherwise + */ + private boolean moduleIsUsedForCapCalculation(Module module) { + return module.hasCompleted() && module.isAffectCap(); + } + + /** + * Calls relevant methods when the modules list is updated + */ + private void modulesUpdated() { + logger.info("Modules Updated... Updating Target Grades and Current CAP"); + updateTargetModuleGrades(); + updateCurrentCap(); + } + + /** + * Replaces targetable module with an updated target grade + */ + private void updateTargetModuleGrades() { + logger.info("Updating Target Grades..."); + boolean shouldSkip = !capGoal.isSet(); + if (shouldSkip) { + logger.info("No CAP Goal set, stopping target grades calculation."); + return; + } + ObservableList targetableModules = getTargetableModulesList().sorted( + Comparator.comparingInt(Module::getCreditsValue)); + + try { + List newTargetModules = getNewTargetModuleGrade(targetableModules); + makeCapGoalPossible(); + replaceTargetModules(targetableModules, newTargetModules); + } catch (CapGoalIsImpossibleException cgiie) { + logger.info("CAP Goal is impossible to achieve."); + makeCapGoalImpossible(); + } catch (NoTargetableModulesException ntme) { + logger.info("No targetable modules."); + makeCapGoalPossible(); + } + } + + /** + * Replaces Modules used to calculate target grade with new Modules with those target grades + * @param targetableModules + * @param newTargetModules + */ + private void replaceTargetModules( + List targetableModules, List newTargetModules) { + if (targetableModules.isEmpty()) { + return; + } + modules.removeAll(targetableModules); + modules.addAll(newTargetModules); + } + + + /** + * Calculates target module grade in order to achieve target goal + * @return a list of modules with target grade if possible. null otherwise + */ + private List getNewTargetModuleGrade(List sortedTargetableModules) + throws CapGoalIsImpossibleException, NoTargetableModulesException { + List gradedModules = getGradedModulesList(); + List adjustedModules = getGradedAdjustedModulesList(); + + double totalUngradedModuleCredit = calculateTotalModuleCredit(sortedTargetableModules); + double totalMc = calculateTotalModuleCredit( + gradedModules, adjustedModules, sortedTargetableModules); + double currentTotalPoint = calculateTotalModulePoint( + gradedModules, adjustedModules); + + if (totalUngradedModuleCredit == 0) { + if (totalMc == 0 || capGoal.getValue() > currentTotalPoint / totalMc) { + throw new CapGoalIsImpossibleException(); + } + throw new NoTargetableModulesException(); + } + + return createNewTargetModuleGrade( + sortedTargetableModules, + totalUngradedModuleCredit, totalMc, currentTotalPoint); + } + + /** + * Creates the new list of modules with target grade + * @param sortedTargetableModules + * @param totalUngradedModuleCredit + * @param totalMc + * @param currentTotalPoint + * @return the new list of modules with target grade + * @throws IllegalArgumentException if totalUngradedModuleCredit is zero or negative + */ + private List createNewTargetModuleGrade( + List sortedTargetableModules, + double totalUngradedModuleCredit, double totalMc, double currentTotalPoint) + throws CapGoalIsImpossibleException { + + if (totalUngradedModuleCredit <= 0) { + throw new IllegalArgumentException("totalUngradedModuleCredit cannot be zero or negative"); + } + double totalScoreToAchieve = capGoal.getValue() * totalMc - currentTotalPoint; + return calculateAndCreateNewTargetModuleGrade( + sortedTargetableModules, + totalUngradedModuleCredit, totalScoreToAchieve); + } + + /** + * Calculates and creates the new list of modules with target grade + * @param sortedTargetableModules + * @param givenTotalUngradedModuleCredit + * @param givenTotalScoreToAchieve + * @return the new list of modules with target grade + * @throws IllegalArgumentException if totalUngradedModuleCredit is zero or negative + */ + private List calculateAndCreateNewTargetModuleGrade( + List sortedTargetableModules, + double givenTotalUngradedModuleCredit, double givenTotalScoreToAchieve) + throws CapGoalIsImpossibleException { + + double totalUngradedModuleCredit = givenTotalUngradedModuleCredit; + double totalScoreToAchieve = givenTotalScoreToAchieve; + double unitScoreToAchieve; + + List targetModules = new ArrayList<>(); + Module newTargetModule; + for (Module targetedModule : sortedTargetableModules) { + unitScoreToAchieve = getUnitScoreToAchieve(totalUngradedModuleCredit, totalScoreToAchieve); + newTargetModule = targetedModule.updateTargetGrade(unitScoreToAchieve); + targetModules.add(newTargetModule); + totalScoreToAchieve -= newTargetModule.getCreditsValue() * unitScoreToAchieve; + totalUngradedModuleCredit -= newTargetModule.getCreditsValue(); + } + + return targetModules; + } + + private double getUnitScoreToAchieve(double totalUngradedModuleCredit, double totalScoreToAchieve) + throws CapGoalIsImpossibleException { + if (totalUngradedModuleCredit <= 0) { + logger.warning("Total Amount of ungraded Module Credit is 0 or lesser."); + throw new IllegalArgumentException("totalUngradedModuleCredit cannot be zero or negative."); + } + double unitScoreToAchieve = Math.ceil(totalScoreToAchieve / totalUngradedModuleCredit * 2) / 2.0; + if (unitScoreToAchieve > 5) { + throw new CapGoalIsImpossibleException(); + } + if (unitScoreToAchieve <= 0.5) { + logger.info("Unit score to achieve is the minimum"); + return 1.0; + } + return unitScoreToAchieve; + } + + private ObservableList getGradedAdjustedModulesList() { + return modules.getFilteredModules(module -> module.isAdjusted() && module.isAffectCap()); + } + + @Override + public CapGoal getCapGoal() { + return capGoal; + } + + private void setCapGoal(CapGoal capGoal) { + this.capGoal = capGoal; + } + + public void setCapGoal(double capGoal) { + this.capGoal = new CapGoal(capGoal); + updateTargetModuleGrades(); + } + + /** + * Sets the capGoal impossible + */ + private void makeCapGoalImpossible() { + capGoal = capGoal.makeIsImpossible(); + removeTargetFromTargetedModules(); + } + + /** + * Removes the given target grades to incomplete modules + */ + private void removeTargetFromTargetedModules() { + List targetedModules = getTargetedModulesList(); + List targetRemovedModules = new ArrayList<>(); + for (Module targetedModule : targetedModules) { + targetRemovedModules.add(new Module(targetedModule, new Grade())); + } + modules.removeAll(targetedModules); + modules.addAll(targetRemovedModules); + } + + /** + * Sets the capGoal as possible + */ + private void makeCapGoalPossible() { + assert capGoal.getValue() > 0; + capGoal = new CapGoal(capGoal.getValue()); + } + + /** + * Tells if the value is no longer possible + * @return true if yes, false otherwise + */ + public boolean isCapGoalImpossible() { + return capGoal.isImpossible(); + } + + /** + * Returns the matching module entry. + *

+ * Finds the module with {@code targetCode}, {@code targetYear} if + * {@code targetYear} is not null, and {@code targetSemester} if + * {@code targetSemester} is not null. + * + * @param targetCode code to match + * @param targetYear year to match if not null + * @param targetSemester semester to match if not null + * @return the matching module + * @throws ModuleNotFoundException thrown when no entries match the + * parameters. + * @throws MultipleModuleEntryFoundException thrown when multiple entries + * match the parameters. + */ + public Module getOnlyOneModule(Code targetCode, Year targetYear, + Semester targetSemester) + throws ModuleNotFoundException, MultipleModuleEntryFoundException { + return modules.getOnlyOneModule(targetCode, targetYear, targetSemester); + } + + /** + * Adjust the target Module to the desired Grade + * @param targetModule + * @param adjustGrade + * @return adjusted Module + */ + public Module adjustModule(Module targetModule, Grade adjustGrade) { + requireNonNull(targetModule); + requireNonNull(adjustGrade); + if (targetModule.hasCompleted()) { + throw new ModuleCompletedException(); + } + + Module adjustedModule = targetModule.adjustGrade(adjustGrade); + //TODO: Use updateModule when fixed + modules.remove(targetModule); + modules.add(adjustedModule); + modulesUpdated(); + return adjustedModule; + } + + //@@author + //// util methods + + @Override + public String toString() { + return modules.asUnmodifiableObservableList().size() + " modules"; + // TODO: refine later + } + + @Override + public ObservableList getModuleList() { + return modules.asUnmodifiableObservableList(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Transcript // instanceof handles nulls + && modules.equals(((Transcript) other).modules)); + } + + @Override + public int hashCode() { + return modules.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/address/model/UserPrefs.java index 980b2b388852..22adca8a7f8b 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/seedu/address/model/UserPrefs.java @@ -12,7 +12,9 @@ public class UserPrefs { private GuiSettings guiSettings; - private Path addressBookFilePath = Paths.get("data" , "addressbook.xml"); + //TODO: REMOVE + private Path addressBookFilePath = Paths.get("data", "addressbook.xml"); + private Path transcriptFilePath = Paths.get("data", "transcript.json"); public UserPrefs() { setGuiSettings(500, 500, 0, 0); @@ -30,10 +32,20 @@ public void setGuiSettings(double width, double height, int x, int y) { guiSettings = new GuiSettings(width, height, x, y); } + public Path getTranscriptFilePath() { + return transcriptFilePath; + } + + public void setTranscriptFilePath(Path transcriptFilePath) { + this.transcriptFilePath = transcriptFilePath; + } + + // TODO: REMOVE public Path getAddressBookFilePath() { return addressBookFilePath; } + // TODO: REMOVE public void setAddressBookFilePath(Path addressBookFilePath) { this.addressBookFilePath = addressBookFilePath; } @@ -50,20 +62,20 @@ public boolean equals(Object other) { UserPrefs o = (UserPrefs) other; return Objects.equals(guiSettings, o.guiSettings) - && Objects.equals(addressBookFilePath, o.addressBookFilePath); + && Objects.equals(addressBookFilePath, o.addressBookFilePath) //TODO: REMOVE + && Objects.equals(transcriptFilePath, o.transcriptFilePath); } @Override public int hashCode() { - return Objects.hash(guiSettings, addressBookFilePath); + return Objects.hash(guiSettings, transcriptFilePath); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Gui Settings : " + guiSettings.toString()); - sb.append("\nLocal data file location : " + addressBookFilePath); + sb.append("\nLocal data file location : " + transcriptFilePath); return sb.toString(); } - } diff --git a/src/main/java/seedu/address/model/VersionedTranscript.java b/src/main/java/seedu/address/model/VersionedTranscript.java new file mode 100644 index 000000000000..2cd51abc8095 --- /dev/null +++ b/src/main/java/seedu/address/model/VersionedTranscript.java @@ -0,0 +1,114 @@ +package seedu.address.model; + +import java.util.ArrayList; +import java.util.List; + +//@@author alexkmj +/** + * {@code Transcript} that keeps track of its own history. + */ +public class VersionedTranscript extends Transcript { + + private final List transcriptStateList; + private int currentStatePointer; + + public VersionedTranscript(ReadOnlyTranscript initialState) { + super(initialState); + + transcriptStateList = new ArrayList<>(); + transcriptStateList.add(new Transcript(initialState)); + currentStatePointer = 0; + } + + /** + * Saves a copy of the current {@code Transcript} state at the end of the + * state list. + *

+ * Undone states are removed from the state list. + */ + public void commit() { + removeStatesAfterCurrentPointer(); + transcriptStateList.add(new Transcript(this)); + currentStatePointer++; + } + + private void removeStatesAfterCurrentPointer() { + int fromIndex = currentStatePointer + 1; + int toIndex = transcriptStateList.size(); + transcriptStateList.subList(fromIndex, toIndex).clear(); + } + + /** + * Restores the transcript to its previous state. + */ + public void undo() { + if (!canUndo()) { + throw new NoUndoableStateException(); + } + currentStatePointer--; + resetData(transcriptStateList.get(currentStatePointer)); + } + + /** + * Restores the transcript to its previously undone state. + */ + public void redo() { + if (!canRedo()) { + throw new NoRedoableStateException(); + } + currentStatePointer++; + resetData(transcriptStateList.get(currentStatePointer)); + } + + /** + * Returns true if {@code undo()} has transcript states to undo. + */ + public boolean canUndo() { + return currentStatePointer > 0; + } + + /** + * Returns true if {@code redo()} has transcript states to redo. + */ + public boolean canRedo() { + return currentStatePointer < transcriptStateList.size() - 1; + } + + /** + * Thrown when trying to {@code undo()} but can't. + */ + public static class NoUndoableStateException extends RuntimeException { + private NoUndoableStateException() { + super("Current state pointer at start of transcriptState list, unable to undo."); + } + } + + /** + * Thrown when trying to {@code redo()} but can't. + */ + public static class NoRedoableStateException extends RuntimeException { + private NoRedoableStateException() { + super("Current state pointer at end of transcriptState list, unable to redo."); + } + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof VersionedTranscript)) { + return false; + } + + VersionedTranscript otherVersionedTranscript = (VersionedTranscript) other; + + // state check + return super.equals(otherVersionedTranscript) + && transcriptStateList.equals(otherVersionedTranscript.transcriptStateList) + && currentStatePointer == otherVersionedTranscript.currentStatePointer; + } +} diff --git a/src/main/java/seedu/address/model/capgoal/CapGoal.java b/src/main/java/seedu/address/model/capgoal/CapGoal.java new file mode 100644 index 000000000000..8e8b5267cf58 --- /dev/null +++ b/src/main/java/seedu/address/model/capgoal/CapGoal.java @@ -0,0 +1,51 @@ +package seedu.address.model.capgoal; + +//@@author jeremiah-ang +/** + * Represents Cap Goal + * + * Immutable. Value can be null. + */ +public class CapGoal { + + private final double value; + private final boolean isSet; + private final boolean isImpossible; + + public CapGoal() { + value = 0; + isSet = false; + isImpossible = false; + } + + public CapGoal(double value) { + this(value, false); + } + + public CapGoal(double value, boolean isImpossible) { + isSet = (value > 0); + this.value = value; + this.isImpossible = isImpossible; + } + + public double getValue() { + return value; + } + + public boolean isSet() { + return isSet; + } + + public boolean isImpossible() { + return isImpossible; + } + + public CapGoal makeIsImpossible() { + return new CapGoal(value, true); + } + + @Override + public String toString() { + return "" + getValue(); + } +} diff --git a/src/main/java/seedu/address/model/exceptions/CapGoalIsImpossibleException.java b/src/main/java/seedu/address/model/exceptions/CapGoalIsImpossibleException.java new file mode 100644 index 000000000000..ed3cbe32f0dc --- /dev/null +++ b/src/main/java/seedu/address/model/exceptions/CapGoalIsImpossibleException.java @@ -0,0 +1,10 @@ +package seedu.address.model.exceptions; + +/** + * Flags that the CAP Goal is impossible to achieve. + */ +public class CapGoalIsImpossibleException extends RuntimeException { + public CapGoalIsImpossibleException() { + super("CAP Goal is not achievable!"); + } +} diff --git a/src/main/java/seedu/address/model/exceptions/NoTargetableModulesException.java b/src/main/java/seedu/address/model/exceptions/NoTargetableModulesException.java new file mode 100644 index 000000000000..7dc52f016cf7 --- /dev/null +++ b/src/main/java/seedu/address/model/exceptions/NoTargetableModulesException.java @@ -0,0 +1,10 @@ +package seedu.address.model.exceptions; + +/** + * Flags that there are no targetable modules. + */ +public class NoTargetableModulesException extends RuntimeException { + public NoTargetableModulesException() { + super("No Modules Are Targetable!"); + } +} diff --git a/src/main/java/seedu/address/model/module/Code.java b/src/main/java/seedu/address/model/module/Code.java new file mode 100644 index 000000000000..d5f6996c8e29 --- /dev/null +++ b/src/main/java/seedu/address/model/module/Code.java @@ -0,0 +1,80 @@ +package seedu.address.model.module; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +//@@author alexkmj +/** + * Represents a Module's code in the transcript. + *

+ * Guarantees: immutable; is valid as declared in {@link #isValidCode(String)} + */ +public class Code { + + /** + * Describes the requirements for code value. + */ + public static final String MESSAGE_CODE_CONSTRAINTS = + "Code can take any values except whitespaces"; + + /** + * No whitespace allowed. + */ + public static final String CODE_VALIDATION_REGEX = "^[^\\s]+$"; + + /** + * Immutable code value. + */ + public final String value; + + /** + * Constructs an {@code Code}. + * + * @param code A valid code. + */ + public Code(String code) { + requireNonNull(code); + checkArgument(isValidCode(code), MESSAGE_CODE_CONSTRAINTS); + value = code.toUpperCase(); + } + + /** + * Returns true if a given string is a valid code. + * + * @param code string to be tested for validity + * @return true if given string is a valid code + */ + public static boolean isValidCode(String code) { + return code.matches(CODE_VALIDATION_REGEX); + } + + /** + * Returns the module code. + * + * @return module code + */ + @Override + public String toString() { + return value; + } + + /** + * Compares the module code value of both Code object. + *

+ * This defines a notion of equality between two code objects. + * + * @param other Code object compared against this object + * @return true if both are the same object or contains the same value + */ + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof Code + && value.equals(((Code) other).value)); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/module/Credit.java b/src/main/java/seedu/address/model/module/Credit.java new file mode 100644 index 000000000000..dcb2f0337011 --- /dev/null +++ b/src/main/java/seedu/address/model/module/Credit.java @@ -0,0 +1,82 @@ +package seedu.address.model.module; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +//@@author alexkmj +/** + * Represents a Module's credits in the transcript. + *

+ * Guarantees: immutable; is valid as declared in {@link #isValidCredit(int)} + */ +public class Credit { + + /** + * Describes the requirements for credit value. + */ + public static final String MESSAGE_CREDIT_CONSTRAINTS = "Credits must be a integer"; + + /** + * Immutable credit value. + */ + public final int value; + + /** + * Constructs an {@code Credit}. + * + * @param credits A valid credit. + */ + public Credit(int credits) { + requireNonNull(credits); + checkArgument(isValidCredit(credits), MESSAGE_CREDIT_CONSTRAINTS); + value = credits; + } + + /** + * Returns true if a given string is a valid credit. + *

+ * Credit must be between 1 and 20 + * + * @param credits string to be tested for validity + * @return true if given string is a valid credit + */ + public static boolean isValidCredit(int credits) { + if (credits < 1) { + return false; + } else if (credits > 20) { + return false; + } + + return true; + } + + /** + * Returns the module credits value. + * + * @return module credits + */ + @Override + public String toString() { + return Integer.toString(value); + } + + /** + * Compares the module credit value of both Credit object. + *

+ * This defines a notion of equality between two credit objects. + * + * @param other Credit object compared against this object + * @return true if both are the same object or contains the same value + */ + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof Credit + && value == ((Credit) other).value); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/module/Grade.java b/src/main/java/seedu/address/model/module/Grade.java new file mode 100644 index 000000000000..e2093365ffc2 --- /dev/null +++ b/src/main/java/seedu/address/model/module/Grade.java @@ -0,0 +1,297 @@ +package seedu.address.model.module; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Represents a Module's grade in the transcript. + *

+ * Guarantees: immutable; is valid as declared in {@link #isValidGrade(String)} + */ +public class Grade { + + //@@author alexkmj + /** + * Describes the requirements for grade value. + */ + public static final String MESSAGE_GRADE_CONSTRAINTS = + "Grade can be A+, A, A-, B+, B, B-, C+, C, D+, D, F, CS, CU"; + + public static final String MESSAGE_POINT_CONSTRAINTS = + "Score must be between [0, 5] with increments of 0.5 and not 0.5"; + + //@@author alexkmj + /** + * Default value's value + */ + public static final String EMPTY_VALUE = "NIL"; + + /** + * No whitespace allowed. + */ + public static final String GRADE_VALIDATION_REGEX = "A\\+|a\\+|A\\-|a\\-" + + "|A|a|B\\+|b\\+|B\\-|b\\-|B|b|C\\+|c\\+|C|c|D\\+|d\\+|D|d|F|f|" + + "CS|cs|CU|cu|" + + EMPTY_VALUE; + + //@@author jeremiah-ang + /** + * Static Unchangeable Mapping between Grade and Point + */ + private static final Map MAP_GRADE_POINT; + private static final Map MAP_POINT_GRADE; + + static { + Map tempGradePointMap = new HashMap<>(); + Map tempPointGradeMap = new HashMap<>(); + tempGradePointMap.put("A+", 5.0); + tempGradePointMap.put("A", 5.0); + tempGradePointMap.put("A-", 4.5); + tempGradePointMap.put("B+", 4.0); + tempGradePointMap.put("B", 3.5); + tempGradePointMap.put("B-", 3.0); + tempGradePointMap.put("C+", 2.5); + tempGradePointMap.put("C", 2.0); + tempGradePointMap.put("D+", 1.5); + tempGradePointMap.put("D", 1.0); + tempGradePointMap.put("F", 0.0); + + for (Map.Entry entry : tempGradePointMap.entrySet()) { + tempPointGradeMap.put(entry.getValue(), entry.getKey()); + } + tempPointGradeMap.put(5.0, "A"); + + MAP_GRADE_POINT = Collections.unmodifiableMap(tempGradePointMap); + MAP_POINT_GRADE = Collections.unmodifiableMap(tempPointGradeMap); + } + + //@@author alexkmj + /** + * Immutable grade value. + */ + public final String value; + + //@@author alexkmj + /** + * State of the grade + */ + public final State state; + + /** + * Creates a new {@code Grade} object with State INCOMPLETE + */ + public Grade() { + this(EMPTY_VALUE, State.INCOMPLETE); + } + + /** + * Creates a new {@code Grade} object with value grade and State COMPLETE + */ + public Grade(String grade) { + this(grade.toUpperCase(), (EMPTY_VALUE.equals(grade)) ? State.INCOMPLETE : State.COMPLETE); + } + + /** + * Constructs an {@code Grade} with letter grade and state of it. + * @param grade + * @param state + */ + private Grade(String grade, State state) { + requireNonNull(grade); + checkArgument(isValidGrade(grade), MESSAGE_GRADE_CONSTRAINTS); + value = grade.toUpperCase(); + this.state = state; + } + + /** + * Constructs an {@code Grade} from point with state COMPLETE + * @param point + */ + public Grade(double point) { + this(point, State.COMPLETE); + } + + /** + * Constructs an {@code Grade} from point and given state + * @param point + * @param state + */ + private Grade(double point, State state) { + requireNonNull(point); + checkArgument(isValidPoint(point), MESSAGE_POINT_CONSTRAINTS); + value = mapPointToValue(point); + this.state = state; + } + + /** + * Constructs an {@code Grade} from String values of value and state + * @param value + * @param state + */ + public Grade(String value, String state) { + this(value, State.valueOf(state)); + } + + /** + * Returns true if point is within [0, 5] and step by 0.5 and not 0.5 + * @param point + * @return + */ + public static boolean isValidPoint(double point) { + double fraction = point - Math.floor(point); + return point >= 0 && point <= 5 && (fraction == 0 || fraction == 0.5) && point != 0.5; + } + + /** + * Returns the letter grade the point should be mapped to. + * @param point + * @return + */ + private String mapPointToValue(double point) { + return MAP_POINT_GRADE.get(point); + } + + //@@author alexkmj + /** + * Returns true if a given string is a valid grade. + * + * @param grade string to be tested for validity + * @return true if given string is a valid grade + */ + public static boolean isValidGrade(String grade) { + return grade.matches(GRADE_VALIDATION_REGEX); + } + + /** + * Returns true if grade affects cap and false if grade does not affect cap. + * + * @return true if grade affects cap and false if grade does not affect cap. + */ + public boolean affectsCap() { + return !EMPTY_VALUE.equals(value) && !value.contentEquals("CS") && !value.contentEquals("CU"); + } + + /** + * Returns the point equivalent of the grade or 0 if grade is invalid. + * + * @return point equivalent of the grade + */ + public float getPoint() { + if (MAP_GRADE_POINT.containsKey(value)) { + return MAP_GRADE_POINT.get(value).floatValue(); + } + return 0; + } + + //@@author alexkmj + /** + * @return true if grade is complete + */ + public boolean isComplete() { + return State.COMPLETE.equals(state); + } + + /** + * @return true if grade is incomplete + */ + public boolean isIncomplete() { + return State.INCOMPLETE.equals(state); + } + + /** + * @return true if grade is adjusted + */ + public boolean isAdjust() { + return State.ADJUST.equals(state); + } + + /** + * @return true if is target grades + */ + public boolean isTarget() { + return State.TARGET.equals(state); + } + + /** + * Creates a new Grade that is adjusted + * @param grade + * @return new Grade object + */ + public Grade adjustGrade(String grade) { + return new Grade(grade, State.ADJUST); + } + + /** + * Creates a new Grade that is adjusted + * @param point + * @return new Grade object + */ + public Grade adjustGrade(double point) { + return new Grade(point, State.ADJUST); + } + + /** + * Creates a new Grade that is targeted + * @param grade + * @return new Grade object + */ + public Grade targetGrade(String grade) { + return new Grade(grade, State.TARGET); + } + + /** + * Creates a new Grade that is targeted + * @param point + * @return new Grade object + */ + public Grade targetGrade(double point) { + return new Grade(point, State.TARGET); + } + + /** + * Returns the grade value. + * + * @return grade + */ + @Override + public String toString() { + return value; + } + + //@@author alexkmj + /** + * Compares the grade value of both Grade object. + *

+ * This defines a notion of equality between two grade objects. + * + * @param other Grade object compared against this object + * @return true if both are the same object or contains the same value + */ + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof Grade + && value.equals(((Grade) other).value)) + && state.equals(((Grade) other).state); + } + + //@@author alexkmj + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Different states of a grade + */ + private enum State { + COMPLETE, + INCOMPLETE, + TARGET, + ADJUST + } +} diff --git a/src/main/java/seedu/address/model/module/Module.java b/src/main/java/seedu/address/model/module/Module.java new file mode 100644 index 000000000000..98b9fe085daf --- /dev/null +++ b/src/main/java/seedu/address/model/module/Module.java @@ -0,0 +1,286 @@ +package seedu.address.model.module; + +import static java.util.Objects.requireNonNull; + +import java.util.Objects; + +/** + * Represents a Module in the transcript. + *

+ * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Module { + + //@@author alexkmj + /** + * Constant for completed. + */ + public static final boolean MODULE_COMPLETED = true; + + //@@author alexkmj + /** + * Constant for not completed. + */ + public static final boolean MODULE_NOT_COMPLETED = false; + + //@@author alexkmj + /** + * Code for the module. + */ + private final Code code; + + //@@author alexkmj + /** + * Year the module was taken. + */ + private final Year year; + + //@@author alexkmj + /** + * Semester the module was taken. + */ + private final Semester semester; + + //@@author alexkmj + /** + * Module credits awarded for completion this module. + */ + private final Credit credits; + + //@@author alexkmj + /** + * Module grade awarded for completion this module. + */ + private final Grade grade; + + /** + * True if module has been completed. False if module has not been taken yet. + */ + private final boolean completed; + + //@@author alexkmj + public Module(Code code, Year year, Semester semester, Credit credit, Grade grade, + boolean completed) { + requireNonNull(code); + requireNonNull(year); + requireNonNull(semester); + requireNonNull(credit); + + this.code = code; + this.year = year; + this.semester = semester; + this.credits = credit; + this.completed = completed; + + //TODO require grade Non-null + this.grade = (grade == null) ? new Grade() : grade; + } + + public Module(Code code, Year year, Semester semester, Credit credit, Grade grade) { + //TODO remove completed + this(code, year, semester, credit, grade, false); + } + + //@@author jeremiah-ang + /** + * Creates a new Module from an existing module but with a different grade + * @param module + * @param grade + */ + public Module(Module module, Grade grade) { + this(module.code, module.year, module.semester, module.credits, grade); + } + + /** + * Tells if this module can be used for target grade calculation + * @return true if yes false otherwise. + */ + public boolean isTargetable() { + return getGrade().isTarget() || getGrade().isIncomplete(); + } + + /** + * Tells if this module will affect the calculation of CAP + * @return true if yes false otherwise. + */ + public boolean isAffectCap() { + return getGrade().affectsCap(); + } + + /** + * returns the value of the Credit + * @return value of Credit + */ + public int getCreditsValue() { + return getCredits().value; + } + + /** + * Clones current Module into one with a new Grade with state TARGET + * @param point + * @return a new Module with Grade with state TARGET + */ + public Module updateTargetGrade(double point) { + return new Module(this, grade.targetGrade(point)); + } + + /** + * Tells if the Module has a Grade with state TARGET + * @return true if yes, false otherwise + */ + public boolean isTargetted() { + return getGrade().isTarget(); + } + + public boolean isAdjusted() { + return getGrade().isAdjust(); + } + + public Module adjustGrade(Grade grade) { + return new Module(this, grade.adjustGrade(grade.value)); + } + + //@@author alexkmj + /** + * Returns the module code. + * + * @return module code + */ + public Code getCode() { + return code; + } + + //@@author alexkmj + /** + * Returns the module credits awarded. + * + * @return module credits + */ + public Credit getCredits() { + return credits; + } + + //@@author alexkmj + /** + * Returns the year in which the module was taken. + * + * @return year in which module was taken + */ + public Year getYear() { + return year; + } + + //@@author alexkmj + /** + * Returns the semester in which the module was taken. + * + * @return semester in which module was taken + */ + public Semester getSemester() { + return semester; + } + + //@@author alexkmj + /** + * Returns the module grade awarded. + * + * @return module grade + */ + public Grade getGrade() { + return grade; + } + + //@@author alexkmj + /** + * Returns true if module has been completed and false if module has not been taken. + * + * @return true if module has been completed and false if module has not been taken + */ + public boolean hasCompleted() { + return (grade == null) || grade.isComplete(); + } + + //@@author alexkmj + /** + * Returns true if module code, year, and semester are the same. + * + * @return true if modue code, year, and semester is the same + */ + public boolean isSameModule(Module otherModule) { + if (otherModule == this) { + return true; + } + + return otherModule != null + && otherModule.getCode().equals(getCode()) + && otherModule.getYear().equals(getYear()) + && otherModule.getSemester().equals(getSemester()); + } + + /** + * Returns true if both modules are of the same object or contains the same set of data fields. + *

+ * This defines a notion of equality between two modules. + * + * @param other other module to be compared with this Module object + * @return true if both objects contains the same data fields + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Module)) { + return false; + } + + Module otherModule = (Module) other; + return otherModule.getCode().equals(getCode()) + && otherModule.getGrade().equals(getGrade()) + && otherModule.getYear().equals(getYear()) + && otherModule.getSemester().equals(getSemester()) + && otherModule.getCredits().equals(getCredits()) + && otherModule.getGrade().equals(getGrade()) + && otherModule.hasCompleted() == hasCompleted(); + } + + //@@author alexkmj + /** + * Returns the code, year, semester, credits, grade, is module completed. + *

+ * Format: Code: CODE Year: YEAR Semester: SEMESTER Credits: CREDITS Grade: GRADE Completed: + * COMPLETED + * + * @return information of this module + */ + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + + return builder.append("Code: ") + .append(getCode()) + .append(" Year: ") + .append(getYear()) + .append(" Semester: ") + .append(getSemester()) + .append(" Credits: ") + .append(getCredits()) + .append(" Grade: ") + .append(getGrade()) + .append(" Grade State: ") + .append(getGrade().state) + .append(" Completed: ") + .append(hasCompleted()) + .toString(); + } + + //@@author alexkmj + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(code, year, semester, credits, grade, completed); + } + + +} diff --git a/src/main/java/seedu/address/model/module/Semester.java b/src/main/java/seedu/address/model/module/Semester.java new file mode 100644 index 000000000000..cea507b92b8c --- /dev/null +++ b/src/main/java/seedu/address/model/module/Semester.java @@ -0,0 +1,103 @@ +package seedu.address.model.module; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +//@@author alexkmj +/** + * Represents a Module's semester in the transcript. + *

+ * Guarantees: immutable; is valid as declared in {@link #isValidSemester(String)} + *

+ * Legal values: 1, 2, s1, s2. 1 + *

+ * (Semester 1), 2 (Semester 2), s1 (Special Semester 1), s2 (Special Semester 2). + */ +public class Semester { + + /** + * Describes the requirements for semester value. + */ + public static final String MESSAGE_SEMESTER_CONSTRAINTS = "Semester can be 1, 2, s1 or s2"; + + /** + * No whitespace allowed. + */ + public static final String SEMESTER_VALIDATION_REGEX = "1|2|s1|s2"; + + /** + * Constant for semester one. + */ + public static final String SEMESTER_ONE = "1"; + + /** + * Constant for semester two. + */ + public static final String SEMESTER_TWO = "2"; + + /** + * Constant for special semester one. + */ + public static final String SEMESTER_SPECIAL_ONE = "s1"; + + /** + * Constant for special semester two. + */ + public static final String SEMESTER_SPECIAL_TWO = "s2"; + + /** + * Immutable semester value. + */ + public final String value; + + /** + * Constructs an {@code Code}. + * + * @param semester A valid semester. + */ + public Semester(String semester) { + requireNonNull(semester); + checkArgument(isValidSemester(semester), MESSAGE_SEMESTER_CONSTRAINTS); + value = semester; + } + + /** + * Returns true if a given string is a valid semester. + * + * @param semester string to be tested for validity + * @return true if given string is a valid semester + */ + public static boolean isValidSemester(String semester) { + return semester.matches(SEMESTER_VALIDATION_REGEX); + } + + /** + * Returns the semester value. + * + * @return grade + */ + @Override + public String toString() { + return value; + } + + /** + * Compares the semester value of both Semester object. + *

+ * This defines a notion of equality between two semester objects. + * + * @param other Semester object compared against this object + * @return true if both are the same object or contains the same value + */ + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof Semester + && value.equals(((Semester) other).value)); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/module/UniqueModuleList.java b/src/main/java/seedu/address/model/module/UniqueModuleList.java new file mode 100644 index 000000000000..1bdd87c44a21 --- /dev/null +++ b/src/main/java/seedu/address/model/module/UniqueModuleList.java @@ -0,0 +1,307 @@ +package seedu.address.model.module; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.function.Predicate; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import seedu.address.model.module.exceptions.DuplicateModuleException; +import seedu.address.model.module.exceptions.ModuleNotFoundException; +import seedu.address.model.module.exceptions.MultipleModuleEntryFoundException; + +/** + * A list of modules that enforces uniqueness between its elements and does not + * allow nulls. + *

+ * A module is considered unique by comparing + * {@code moduleA.isSameModule(moduleB)}. + *

+ * As such, adding and updating of modules uses {@code moduleA.equals(moduleB)} + * for equality so as to ensure that the module being added or updated is unique + * in terms of identity in the {@code UniqueModuleList}. + */ +public class UniqueModuleList implements Iterable { + //@@author alexkmj + /** + * Creates an observable list of module. + * See {@link Module}. + */ + private final ObservableList internalList = + FXCollections.observableArrayList(); + + /** + * Returns true if the list contains an equivalent module as the given + * argument. See {@link Module}. + * + * @param toCheck the module that is being checked against + * @return true if list contains equivalent module + */ + public boolean contains(Module toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSameModule); + } + + /** + * Adds a module to the list. + *

+ * The {@link Module} should not exist in the list. + * + * @param toAdd the module that would be added into the list + */ + public void add(Module toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateModuleException(); + } + internalList.add(toAdd); + } + //@@author + + //@@author jeremiah-ang + /** + * Adds all module in a list to the list. + * + * @param modules collection of modules to add + * @return true if this list changed as a result of the call + */ + public boolean addAll(Collection modules) { + return internalList.addAll(modules); + } + //@@author + + //@@author alexkmj + /** + * Replaces the module {@code target} in the list with {@code editedModule}. + *

+ * {@code target} must exist in the list. The {@link Module} identity of + * {@code editedModule} must not be the same as another existing module in + * the list. + * + * @param target the module to be replaced + * @param editedModule the modue that replaces the old module + */ + public void setModule(Module target, Module editedModule) { + requireAllNonNull(target, editedModule); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new ModuleNotFoundException(); + } + + if (!target.isSameModule(editedModule) && contains(editedModule)) { + throw new DuplicateModuleException(); + } + + internalList.set(index, editedModule); + } + + /** + * Replaces the {@link #internalList} of this {@code UniqueModuleList} with + * the {@code internalList} of the replacement. + * + * @param replacement the {@code UniqueModuleList} object that contains the + * {@code internalList} that is replacing the old {@code internalList} + */ + public void setModules(UniqueModuleList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code modules}. + *

+ * {@code modules} must not contain duplicate modules. + * + * @param modules the list of module that would replace the old list + */ + public void setModules(List modules) { + requireAllNonNull(modules); + if (!modulesAreUnique(modules)) { + throw new DuplicateModuleException(); + } + + internalList.setAll(modules); + } + + /** + * Removes the equivalent module from the list. + *

+ * The {@link Module} must exist in the list. + * + * @param module the code that the module to be removed contains + */ + public void remove(Module module) { + requireNonNull(module); + + if (!internalList.remove(module)) { + throw new ModuleNotFoundException(); + } + } + + /** + * Removes the equivalent module from the list. + *

+ * The {@link Module} must exist in the list. + * + * @param filter the predicate used to filter the modules to be removed + */ + public void remove(Predicate filter) { + boolean successful = internalList.removeIf(filter); + + if (!successful) { + throw new ModuleNotFoundException(); + } + } + //@@author + + //@@author jeremiah-ang + /** + * Removes all module in a list from the list + */ + public boolean removeAll(Collection modules) { + return internalList.removeAll(modules); + } + //@@author + + //@@author alexkmj + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + * + * @return backing list as an unmodifiable {@code ObservableList} + */ + public ObservableList asUnmodifiableObservableList() { + return FXCollections.unmodifiableObservableList(internalList); + } + //@@author + + /** + * Returns true if {@code modules} contains only unique modules. + * + * @param modules the module list that is being checked + * @return true if modules are unique and false if modules are not unique + */ + private boolean modulesAreUnique(List modules) { + return modules.size() == modules.parallelStream() + .distinct() + .count(); + } + //@@author + + //@@author jeremiah-ang + /** + * Returns the list of filtered Module based on the given predicate + * + * @param predicate + * @return filtered list + */ + public ObservableList getFilteredModules(Predicate predicate) { + return internalList.filtered(predicate); + } + + /** + * Finds Module that isSameModule as moduleToFind + * @param moduleToFind + * @return the Module that matches; null if not matched + */ + public Module find(Module moduleToFind) throws ModuleNotFoundException { + return internalList.stream() + .filter(index -> index.isSameModule(moduleToFind)) + .findAny() + .orElseThrow(() -> new ModuleNotFoundException()); + } + //@@author + + //@@author alexkmj + /** + * Returns the matching module entry. + *

+ * Finds the module with {@code targetCode}, {@code targetYear} if + * {@code targetYear} is not null, and {@code targetSemester} if + * {@code targetSemester} is not null. + * + * @param targetCode code to match + * @param targetYear year to match if not null + * @param targetSemester semester to match if not null + * @return the matching module + * @throws ModuleNotFoundException thrown when no entries match the + * parameters. + * @throws MultipleModuleEntryFoundException thrown when multiple entries + * match the parameters. + */ + public Module getOnlyOneModule(Code targetCode, Year targetYear, + Semester targetSemester) + throws ModuleNotFoundException, MultipleModuleEntryFoundException { + requireNonNull(targetCode); + + Predicate moduleMatches = index -> { + boolean codeMatch = targetCode == null + || index.getCode().equals(targetCode); + + boolean yearMatch = targetYear == null + || index.getYear().equals(targetYear); + + boolean semesterMatch = targetSemester == null + || index.getSemester().equals(targetSemester); + + return codeMatch && yearMatch && semesterMatch; + }; + + Module[] moduleArray = internalList.stream() + .filter(moduleMatches) + .toArray(Module[]::new); + + if (moduleArray.length == 0) { + throw new ModuleNotFoundException(); + } + + if (moduleArray.length > 1) { + throw new MultipleModuleEntryFoundException(); + } + + return moduleArray[0]; + } + + /** + * Returns the iterator of the internal list. + * + * @return iterator of the internal list + */ + @Override + public Iterator iterator() { + return internalList.iterator(); + } + + /** + * Compares the internal list of both UniqueModuleList object. + *

+ * This defines a notion of equality between two UniqueModuleList objects. + * + * @param other Code object compared against this object + * @return true if both are the same object or contains the same value + */ + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (!(other instanceof UniqueModuleList)) { + return false; + } + + UniqueModuleList e = (UniqueModuleList) other; + return internalList.equals(e.internalList); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + //@@author +} diff --git a/src/main/java/seedu/address/model/module/Year.java b/src/main/java/seedu/address/model/module/Year.java new file mode 100644 index 000000000000..702d89081a25 --- /dev/null +++ b/src/main/java/seedu/address/model/module/Year.java @@ -0,0 +1,97 @@ +package seedu.address.model.module; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +//@@author alexkmj +/** + * Represents a Module's year in the transcript. + *

+ * Guarantees: immutable; is valid as declared in {@link #isValidYear(int)} + */ +public class Year { + + public static final String MESSAGE_YEAR_CONSTRAINTS = + "Year must be [1-5]. Example: 1 represents Year 1"; + + /** + * No whitespace allowed. + */ + public static final String YEAR_VALIDATION_REGEX = "[1-5]"; + + /** + * Immutable year value. + */ + public final int value; + + /** + * Constructs an {@code Year}. + * + * @param year A valid year. + */ + public Year(int year) { + checkArgument(isValidYear(year), MESSAGE_YEAR_CONSTRAINTS); + value = year; + } + + /** + * Constructs an {@code Year}. + * + * @param year A valid year. + */ + public Year(String year) { + requireNonNull(year); + checkArgument(isValidYear(year), MESSAGE_YEAR_CONSTRAINTS); + value = Integer.valueOf(year); + } + + /** + * Returns true if a given string is a valid year. + * + * @param year string to be tested for validity + * @return true if given string is a valid year + */ + public static boolean isValidYear(int year) { + return isValidYear(Integer.toString(year)); + } + + /** + * Returns true if a given string is a valid year. + * + * @param year string to be tested for validity + * @return true if given string is a valid year + */ + public static boolean isValidYear(String year) { + return year.matches(YEAR_VALIDATION_REGEX); + } + + /** + * Returns the year the module was taken. + * + * @return year + */ + @Override + public String toString() { + return Integer.toString(value); + } + + /** + * Compares the year value of both Year object. + *

+ * This defines a notion of equality between two Year objects. + * + * @param other Year object compared against this object + * @return true if both are the same object or contains the same value + */ + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof Year + && value == ((Year) other).value); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/module/exceptions/DuplicateModuleException.java b/src/main/java/seedu/address/model/module/exceptions/DuplicateModuleException.java new file mode 100644 index 000000000000..1be15356ead8 --- /dev/null +++ b/src/main/java/seedu/address/model/module/exceptions/DuplicateModuleException.java @@ -0,0 +1,10 @@ +package seedu.address.model.module.exceptions; + +/** + * Signals that the operation will result in duplicate {@link Module} objects. + */ +public class DuplicateModuleException extends RuntimeException { + public DuplicateModuleException() { + super("Operation would result in duplicate modules"); + } +} diff --git a/src/main/java/seedu/address/model/module/exceptions/ModuleCompletedException.java b/src/main/java/seedu/address/model/module/exceptions/ModuleCompletedException.java new file mode 100644 index 000000000000..b62e4b44aa5c --- /dev/null +++ b/src/main/java/seedu/address/model/module/exceptions/ModuleCompletedException.java @@ -0,0 +1,7 @@ +package seedu.address.model.module.exceptions; + +/** + * Signals that a Module in question is completed + */ +public class ModuleCompletedException extends RuntimeException { +} diff --git a/src/main/java/seedu/address/model/module/exceptions/ModuleNotFoundException.java b/src/main/java/seedu/address/model/module/exceptions/ModuleNotFoundException.java new file mode 100644 index 000000000000..05257576c742 --- /dev/null +++ b/src/main/java/seedu/address/model/module/exceptions/ModuleNotFoundException.java @@ -0,0 +1,8 @@ +package seedu.address.model.module.exceptions; + +/** + * Signals that the operation is unable to find the specified {@link Module}. + */ +public class ModuleNotFoundException extends RuntimeException { + +} diff --git a/src/main/java/seedu/address/model/module/exceptions/MultipleModuleEntryFoundException.java b/src/main/java/seedu/address/model/module/exceptions/MultipleModuleEntryFoundException.java new file mode 100644 index 000000000000..7fd23b2bb9cc --- /dev/null +++ b/src/main/java/seedu/address/model/module/exceptions/MultipleModuleEntryFoundException.java @@ -0,0 +1,8 @@ +package seedu.address.model.module.exceptions; + +/** + * Signals that more than one {@link Module} found. + */ +public class MultipleModuleEntryFoundException extends RuntimeException { + +} diff --git a/src/main/java/seedu/address/model/util/ModuleBuilder.java b/src/main/java/seedu/address/model/util/ModuleBuilder.java new file mode 100644 index 000000000000..ad0d5e4b2d6e --- /dev/null +++ b/src/main/java/seedu/address/model/util/ModuleBuilder.java @@ -0,0 +1,172 @@ +package seedu.address.model.util; + +import static java.util.Objects.requireNonNull; + +import seedu.address.model.module.Code; +import seedu.address.model.module.Credit; +import seedu.address.model.module.Grade; +import seedu.address.model.module.Module; +import seedu.address.model.module.Semester; +import seedu.address.model.module.Year; + +//@@author alexkmj +/** + * A utility class to help with building Module objects. + */ +public class ModuleBuilder { + + public static final String DEFAULT_CODE = "CS2103"; + public static final int DEFAULT_YEAR = 1; + public static final String DEFAULT_SEMESTER = Semester.SEMESTER_ONE; + public static final int DEFAULT_CREDIT = 4; + public static final String DEFAULT_GRADE = "A+"; + public static final boolean DEFAULT_COMPLETED = true; + + private Code code; + private Year year; + private Semester semester; + private Credit credit; + private Grade grade; + private boolean completed; + + public ModuleBuilder() { + code = new Code(DEFAULT_CODE); + year = new Year(DEFAULT_YEAR); + semester = new Semester(DEFAULT_SEMESTER); + credit = new Credit(DEFAULT_CREDIT); + grade = new Grade(DEFAULT_GRADE); + completed = DEFAULT_COMPLETED; + } + + /** + * Initializes the ModuleBuilder with the data of {@code personToCopy}. + */ + public ModuleBuilder(Module moduleToCopy) { + code = moduleToCopy.getCode(); + year = moduleToCopy.getYear(); + semester = moduleToCopy.getSemester(); + credit = moduleToCopy.getCredits(); + grade = moduleToCopy.getGrade(); + completed = moduleToCopy.hasCompleted(); + } + + /** + * Sets the {@code Code} of the {@code Module} that we are building. + */ + public ModuleBuilder withCode(String code) { + requireNonNull(code); + + this.code = new Code(code); + return withCode(this.code); + } + + /** + * Sets the {@code Code} of the {@code Module} that we are building. + */ + public ModuleBuilder withCode(Code code) { + requireNonNull(code); + + this.code = code; + return this; + } + + /** + * Sets the {@code Year} of the {@code Module} that we are building. + */ + public ModuleBuilder withYear(int year) { + requireNonNull(year); + + this.year = new Year(year); + return withYear(this.year); + } + + /** + * Sets the {@code Year} of the {@code Module} that we are building. + */ + public ModuleBuilder withYear(Year year) { + requireNonNull(year); + + this.year = year; + return this; + } + + /** + * Sets the {@code Semester} of the {@code Module} that we are building. + */ + public ModuleBuilder withSemester(String semester) { + requireNonNull(semester); + + this.semester = new Semester(semester); + return withSemester(this.semester); + } + + /** + * Sets the {@code Semester} of the {@code Module} that we are building. + */ + public ModuleBuilder withSemester(Semester semester) { + requireNonNull(semester); + + this.semester = semester; + return this; + } + + /** + * Sets the {@code Credit} of the {@code Module} that we are building. + */ + public ModuleBuilder withCredit(int credit) { + requireNonNull(credit); + + this.credit = new Credit(credit); + return withCredit(this.credit); + } + + /** + * Sets the {@code Credit} of the {@code Module} that we are building. + */ + public ModuleBuilder withCredit(Credit credit) { + requireNonNull(credit); + + this.credit = credit; + return this; + } + + /** + * Sets the {@code Grade} of the {@code Module} that we are building. + */ + public ModuleBuilder withGrade(String grade) { + this.grade = new Grade(grade); + return this; + } + + /** + * Sets the {@code Grade} of the {@code Module} that we are building. + */ + public ModuleBuilder withGrade(Grade grade) { + this.grade = grade; + return this; + } + + /** + * Sets the {@code Grade} of the {@code Module} that we are building to null. + */ + public ModuleBuilder noGrade() { + this.grade = null; + return this; + } + + /** + * Sets the {@code completed} of the {@code Module} that we are building. + */ + public ModuleBuilder withCompleted(boolean completed) { + if (completed) { + withGrade((grade == null) ? new Grade(DEFAULT_GRADE) : new Grade(grade.value)); + } else { + noGrade(); + } + return this; + } + + public Module build() { + return new Module(code, year, semester, credit, grade, completed); + } +} diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facfa..f8304e5e7946 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -6,6 +6,10 @@ import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyTranscript; +import seedu.address.model.Transcript; +import seedu.address.model.module.Module; +import seedu.address.model.module.Semester; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; @@ -17,10 +21,64 @@ * Contains utility methods for populating {@code AddressBook} with sample data. */ public class SampleDataUtil { + + + public static final Module DISCRETE_MATH = new ModuleBuilder().withCode("CS1231") + .withYear(1) + .withSemester(Semester.SEMESTER_ONE) + .withCredit(4) + .withGrade("A+") + .build(); + + public static final Module PROGRAMMING_METHODOLOGY_TWO = new ModuleBuilder().withCode("CS2030") + .withYear(2) + .withSemester(Semester.SEMESTER_TWO) + .withCredit(4) + .withGrade("B+") + .build(); + + public static final Module DATA_STRUCTURES = new ModuleBuilder().withCode("CS2040") + .withYear(3) + .withSemester(Semester.SEMESTER_SPECIAL_ONE) + .withCredit(4) + .withGrade("F") + .build(); + + public static final Module ASKING_QUESTIONS = new ModuleBuilder().withCode("GEQ1000") + .withYear(1) + .withSemester(Semester.SEMESTER_ONE) + .withCredit(4) + .withGrade("CS") + .build(); + + public static final Double MODULES_WITHOUT_NON_AFFECTING_MODULES_CAP = 3.0; + + public static final Module SOFTWARE_ENGINEERING = new ModuleBuilder().withCode("CS2103") + .withYear(3) + .withSemester(Semester.SEMESTER_ONE) + .withCredit(4) + .withGrade("A+") + .build(); + + public static final Module DATABASE_SYSTEMS = new ModuleBuilder().withCode("CS2102") + .withYear(2) + .withSemester(Semester.SEMESTER_ONE) + .withCredit(4) + .withGrade("A+") + .build(); + + public static final Module DATABASE_SYSTEMS_2MC = new ModuleBuilder().withCode("CS2102B") + .withYear(2) + .withSemester(Semester.SEMESTER_ONE) + .withCredit(2) + .withGrade("A+") + .build(); + + public static Person[] getSamplePersons() { - return new Person[] { + return new Person[]{ new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), - new Address("Blk 30 Geylang Street 29, #06-40"), + new Address("Blk 30 GePersonylang Street 29, #06-40"), getTagSet("friends")), new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), @@ -48,13 +106,43 @@ public static ReadOnlyAddressBook getSampleAddressBook() { return sampleAb; } + /** + * Returns an {@code Transcript} given modules as arguments. + */ + public static ReadOnlyTranscript getTranscriptWithModules(Module... modules) { + Transcript tr = new Transcript(); + for (Module module : modules) { + tr.addModule(module); + } + return tr; + } + + /** + * Returns an {@code Transcript} given modules as arguments. + */ + public static Module[] getSampleModules(Module... modules) { + return modules; + } + + public static ReadOnlyTranscript getSampleTranscript() { + return getTranscriptWithModules( + // DISCRETE_MATH, + // PROGRAMMING_METHODOLOGY_TWO, + // DATA_STRUCTURES, + // ASKING_QUESTIONS, + // SOFTWARE_ENGINEERING, + // DATABASE_SYSTEMS, + // DATABASE_SYSTEMS_2MC + ); + } + /** * Returns a tag set containing the list of strings given. */ public static Set getTagSet(String... strings) { return Arrays.stream(strings) - .map(Tag::new) - .collect(Collectors.toSet()); + .map(Tag::new) + .collect(Collectors.toSet()); } } diff --git a/src/main/java/seedu/address/storage/JsonTranscriptDeserializer.java b/src/main/java/seedu/address/storage/JsonTranscriptDeserializer.java new file mode 100644 index 000000000000..221969d62194 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonTranscriptDeserializer.java @@ -0,0 +1,62 @@ +package seedu.address.storage; + +import java.io.IOException; +import java.util.Iterator; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +import seedu.address.model.ReadOnlyTranscript; +import seedu.address.model.Transcript; +import seedu.address.model.module.Code; +import seedu.address.model.module.Credit; +import seedu.address.model.module.Grade; +import seedu.address.model.module.Module; +import seedu.address.model.module.Semester; +import seedu.address.model.module.Year; + +/** + * Deserializer for {@link seedu.address.model.ReadOnlyTranscript}. + */ +public class JsonTranscriptDeserializer extends StdDeserializer { + + public JsonTranscriptDeserializer(Class vc) { + super(vc); + } + + public JsonTranscriptDeserializer() { + super(ReadOnlyTranscript.class); + } + + @Override + public ReadOnlyTranscript deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + + Transcript transcript = new Transcript(); + JsonNode node = jp.getCodec().readTree(jp); + try { + Iterator elements = node.get("modules").get("internalList").elements(); + while (elements.hasNext()) { + JsonNode element = elements.next(); + Code code = new Code(element.path("code").path("value").textValue()); + Year year = new Year(element.path("year").path("value").intValue()); + Semester semester = new Semester(element.path("semester").path("value").textValue()); + Credit credits = new Credit(element.path("credits").path("value").intValue()); + Grade grade = new Grade(element.path("grade").path("value").textValue(), + element.path("grade").path("state").textValue()); + boolean completed = element.path("completed").booleanValue(); + Module module = new Module(code, year, semester, credits, grade, completed); + transcript.addModule(module); + } + + JsonNode capGoal = node.get("capGoal"); + if (!capGoal.isMissingNode()) { + transcript.setCapGoal(capGoal.path("value").doubleValue()); + } + return transcript; + } catch (NullPointerException e) { + throw new IOException(e); + } + } +} diff --git a/src/main/java/seedu/address/storage/JsonTranscriptStorage.java b/src/main/java/seedu/address/storage/JsonTranscriptStorage.java new file mode 100644 index 000000000000..22482a9cdfd2 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonTranscriptStorage.java @@ -0,0 +1,61 @@ +package seedu.address.storage; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.commons.util.FileUtil; +import seedu.address.commons.util.JsonUtil; +import seedu.address.model.ReadOnlyTranscript; +import seedu.address.model.Transcript; + +//@@author jeremyyew +/** + * A class to access Transcript stored in the hard disk as a json file + */ +public class JsonTranscriptStorage implements TranscriptStorage { + + private final Path filePath; + + public JsonTranscriptStorage(Path filePath) { + this.filePath = filePath; + } + + @Override + public Path getTranscriptFilePath() { + return filePath; + } + + @Override + public Optional readTranscript() throws DataConversionException { + return readTranscript(filePath); + } + + /** + * Similar to {@link #readTranscript()} + * + * @param transcriptFilePath location of the data. Cannot be null. + * @throws DataConversionException if the file format is not as expected. + */ + public Optional readTranscript(Path transcriptFilePath) throws DataConversionException { + return JsonUtil.readJsonFile(transcriptFilePath, ReadOnlyTranscript.class); + } + + @Override + public void saveTranscript(ReadOnlyTranscript transcript) throws IOException { + JsonUtil.saveJsonFile(new Transcript(transcript), filePath); + } + + @Override + public void saveTranscript(ReadOnlyTranscript transcript, Path filePath) throws IOException { + requireNonNull(transcript); + requireNonNull(filePath); + + FileUtil.createIfMissing(filePath); + JsonUtil.saveJsonFile(new Transcript(transcript), filePath); + } + +} diff --git a/src/main/java/seedu/address/storage/Storage.java b/src/main/java/seedu/address/storage/Storage.java index 28791127999b..6756e072ce59 100644 --- a/src/main/java/seedu/address/storage/Storage.java +++ b/src/main/java/seedu/address/storage/Storage.java @@ -5,6 +5,7 @@ import java.util.Optional; import seedu.address.commons.events.model.AddressBookChangedEvent; +import seedu.address.commons.events.model.TranscriptChangedEvent; import seedu.address.commons.events.storage.DataSavingExceptionEvent; import seedu.address.commons.exceptions.DataConversionException; import seedu.address.model.ReadOnlyAddressBook; @@ -13,7 +14,7 @@ /** * API of the Storage component */ -public interface Storage extends AddressBookStorage, UserPrefsStorage { +public interface Storage extends AddressBookStorage, UserPrefsStorage, TranscriptStorage { @Override Optional readUserPrefs() throws DataConversionException, IOException; @@ -32,8 +33,15 @@ public interface Storage extends AddressBookStorage, UserPrefsStorage { /** * Saves the current version of the Address Book to the hard disk. - * Creates the data file if it is missing. + * Creates the data file if it is missing. * Raises {@link DataSavingExceptionEvent} if there was an error during saving. */ void handleAddressBookChangedEvent(AddressBookChangedEvent abce); + + /** + * Saves the current version of the Transcript to the hard disk. + * Creates the data file if it is missing. + * Raises {@link DataSavingExceptionEvent} if there was an error during saving. + */ + void handleTranscriptChangedEvent(TranscriptChangedEvent tce); } diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/address/storage/StorageManager.java index b0df908a76a7..a1408e94fe5e 100644 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ b/src/main/java/seedu/address/storage/StorageManager.java @@ -10,25 +10,30 @@ import seedu.address.commons.core.ComponentManager; import seedu.address.commons.core.LogsCenter; import seedu.address.commons.events.model.AddressBookChangedEvent; +import seedu.address.commons.events.model.TranscriptChangedEvent; import seedu.address.commons.events.storage.DataSavingExceptionEvent; import seedu.address.commons.exceptions.DataConversionException; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyTranscript; import seedu.address.model.UserPrefs; /** - * Manages storage of AddressBook data in local storage. + * Manages storage of Transcript data in local storage. */ public class StorageManager extends ComponentManager implements Storage { private static final Logger logger = LogsCenter.getLogger(StorageManager.class); private AddressBookStorage addressBookStorage; private UserPrefsStorage userPrefsStorage; + private TranscriptStorage transcriptStorage; - - public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage) { + public StorageManager(AddressBookStorage addressBookStorage, + UserPrefsStorage userPrefsStorage, + TranscriptStorage transcriptStorage) { super(); this.addressBookStorage = addressBookStorage; this.userPrefsStorage = userPrefsStorage; + this.transcriptStorage = transcriptStorage; } // ================ UserPrefs methods ============================== @@ -90,4 +95,44 @@ public void handleAddressBookChangedEvent(AddressBookChangedEvent event) { } } + // ================ Transcript methods ============================== + + @Override + public Path getTranscriptFilePath() { + return transcriptStorage.getTranscriptFilePath(); + } + + @Override + public Optional readTranscript() throws DataConversionException, IOException { + return readTranscript(transcriptStorage.getTranscriptFilePath()); + } + + @Override + public Optional readTranscript(Path filePath) throws DataConversionException, IOException { + logger.fine("Attempting to read data from file: " + filePath); + return transcriptStorage.readTranscript(filePath); + } + + @Override + public void saveTranscript(ReadOnlyTranscript transcript) throws IOException { + saveTranscript(transcript, transcriptStorage.getTranscriptFilePath()); + } + + @Override + public void saveTranscript(ReadOnlyTranscript transcript, Path filePath) throws IOException { + logger.fine("Attempting to write to data file: " + filePath); + transcriptStorage.saveTranscript(transcript, filePath); + } + + @Override + @Subscribe + public void handleTranscriptChangedEvent(TranscriptChangedEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event, "Local transcript data changed, saving to file")); + try { + saveTranscript(event.data); + } catch (IOException e) { + raise(new DataSavingExceptionEvent(e)); + } + } + } diff --git a/src/main/java/seedu/address/storage/TranscriptStorage.java b/src/main/java/seedu/address/storage/TranscriptStorage.java new file mode 100644 index 000000000000..18a7cc5f3463 --- /dev/null +++ b/src/main/java/seedu/address/storage/TranscriptStorage.java @@ -0,0 +1,48 @@ +package seedu.address.storage; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import seedu.address.commons.exceptions.DataConversionException; + +import seedu.address.model.ReadOnlyTranscript; + +/** + * Represents a storage for {@link seedu.address.model.Transcript}. + */ +public interface TranscriptStorage { + + /** + * Returns the file path of the data file. + */ + Path getTranscriptFilePath(); + + /** + * Returns Transcript data as a {@link ReadOnlyTranscript}. + * Returns {@code Optional.empty()} if storage file is not found. + * + * @throws DataConversionException if the data in storage is not in the expected format. + * @throws IOException if there was any problem when reading from the storage. + */ + Optional readTranscript() throws DataConversionException, IOException; + + /** + * @see #getTranscriptFilePath() + */ + Optional readTranscript(Path filePath) throws DataConversionException, IOException; + + /** + * Saves the given {@link ReadOnlyTranscript} to the storage. + * + * @param transcript cannot be null. + * @throws IOException if there was any problem writing to the file. + */ + void saveTranscript(ReadOnlyTranscript transcript) throws IOException; + + /** + * @see #saveTranscript(ReadOnlyTranscript) + */ + void saveTranscript(ReadOnlyTranscript transcript, Path filePath) throws IOException; + +} diff --git a/src/main/java/seedu/address/ui/CapPanel.java b/src/main/java/seedu/address/ui/CapPanel.java new file mode 100644 index 000000000000..41eb313c0d93 --- /dev/null +++ b/src/main/java/seedu/address/ui/CapPanel.java @@ -0,0 +1,66 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import com.google.common.eventbus.Subscribe; + +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.fxml.FXML; +import javafx.scene.layout.Region; +import javafx.scene.text.Text; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.events.model.TranscriptChangedEvent; +import seedu.address.model.ReadOnlyTranscript; +import seedu.address.model.capgoal.CapGoal; +//@@author jeremyyew +/** + * A ui for the status bar that is displayed at the header of the application. + */ +public class CapPanel extends UiPart { + + private static final Logger logger = LogsCenter.getLogger(CapPanel.class); + private static final String FXML = "CapPanel.fxml"; + + private final DoubleProperty currentCapDouble = new SimpleDoubleProperty(0); + private final StringProperty capGoalString = new SimpleStringProperty("NIL"); + + @FXML + private Text currentCapValue; + @FXML + private Text capGoalValue; + + public CapPanel(ReadOnlyTranscript transcript) { + super(FXML); + + currentCapValue.textProperty().bind(Bindings.convert(currentCapDouble)); + capGoalValue.textProperty().bind(capGoalString); + + Platform.runLater(() -> currentCapDouble.setValue(round(transcript.getCurrentCap(), 2))); + CapGoal goal = transcript.getCapGoal(); + Platform.runLater(() -> capGoalString.setValue(goal.isSet() ? String.valueOf(goal.getValue()) : "NIL")); + registerAsAnEventHandler(this); + } + + private static double round (double value, int precision) { + int scale = (int) Math.pow(10, precision); + return (double) Math.round(value * scale) / scale; + } + + @Subscribe + public void handleTranscriptChangedEvent(TranscriptChangedEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event, + "Local transcript data changed, obtaining new cap and cap goal")); + ReadOnlyTranscript transcript = event.data; + Platform.runLater(() -> currentCapDouble.setValue(round(transcript.getCurrentCap(), 2))); + CapGoal goal = transcript.getCapGoal(); + String goalValue = (goal.isSet()) ? goal.getValue() + "" : "NIL"; + String goalIsImpossible = (goal.isSet() && goal.isImpossible()) ? " (Impossible)" : ""; + Platform.runLater(() -> capGoalString.setValue(String.format("%s%s", goalValue, goalIsImpossible))); + } + +} diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 0e361a4d7baf..e91df1ecb182 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -34,15 +34,11 @@ public class MainWindow extends UiPart { private Logic logic; // Independent Ui parts residing in this Ui container - private BrowserPanel browserPanel; - private PersonListPanel personListPanel; + private ModuleListPanel moduleListPanel; private Config config; private UserPrefs prefs; private HelpWindow helpWindow; - @FXML - private StackPane browserPlaceholder; - @FXML private StackPane commandBoxPlaceholder; @@ -50,7 +46,13 @@ public class MainWindow extends UiPart { private MenuItem helpMenuItem; @FXML - private StackPane personListPanelPlaceholder; + private StackPane capPanelPlaceholder; + + @FXML + private StackPane moduleListPanelPlaceholder; + + @FXML + private StackPane moduleListPanelPlaceholderTwo; @FXML private StackPane resultDisplayPlaceholder; @@ -119,16 +121,19 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { * Fills up all the placeholders of this window. */ void fillInnerParts() { - browserPanel = new BrowserPanel(); - browserPlaceholder.getChildren().add(browserPanel.getRoot()); + moduleListPanel = new ModuleListPanel(logic.getCompletedModuleList()); + moduleListPanelPlaceholder.getChildren().add(moduleListPanel.getRoot()); + + ModuleListPanel2 moduleListPanelTwo = new ModuleListPanel2(logic.getIncompleteModuleList()); + moduleListPanelPlaceholderTwo.getChildren().add(moduleListPanelTwo.getRoot()); - personListPanel = new PersonListPanel(logic.getFilteredPersonList()); - personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + CapPanel capPanel = new CapPanel(logic.getTranscript()); + capPanelPlaceholder.getChildren().add(capPanel.getRoot()); ResultDisplay resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); - StatusBarFooter statusBarFooter = new StatusBarFooter(prefs.getAddressBookFilePath()); + StatusBarFooter statusBarFooter = new StatusBarFooter(prefs.getTranscriptFilePath()); statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); CommandBox commandBox = new CommandBox(logic); @@ -187,12 +192,15 @@ private void handleExit() { raise(new ExitAppRequestEvent()); } - public PersonListPanel getPersonListPanel() { - return personListPanel; + + + + public ModuleListPanel getModuleListPanel() { + return moduleListPanel; } void releaseResources() { - browserPanel.freeResources(); + // browserPanel.freeResources(); } @Subscribe diff --git a/src/main/java/seedu/address/ui/ModuleCard.java b/src/main/java/seedu/address/ui/ModuleCard.java new file mode 100644 index 000000000000..7d29507943b2 --- /dev/null +++ b/src/main/java/seedu/address/ui/ModuleCard.java @@ -0,0 +1,67 @@ +package seedu.address.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; +import seedu.address.model.module.Module; + +/** + * An UI component that displays information of a {@code Module}. + */ +public class ModuleCard extends UiPart { + + private static final String FXML = "ModuleListCard.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + public final Module module; + + @FXML + private Label code; + @FXML + private Label id; + @FXML + private Label credits; + @FXML + private Label semester; + @FXML + private Label year; + @FXML + private Label grade; + + public ModuleCard(Module module) { + super(FXML); + this.module = module; + id.setText(""); // may be used to index module cards in the UI. + code.setText(module.getCode().value); + credits.setText(module.getCredits().value + ""); + semester.setText(module.getSemester().value); + year.setText(module.getYear().value + ""); + grade.setText(module.getGrade().value); + //module.getTags().forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ModuleCard)) { + return false; + } + + // state check + ModuleCard card = (ModuleCard) other; + return id.getText().equals(card.id.getText()) + && module.equals(card.module); + } +} diff --git a/src/main/java/seedu/address/ui/ModuleCard2.java b/src/main/java/seedu/address/ui/ModuleCard2.java new file mode 100644 index 000000000000..da8e7b3d9f59 --- /dev/null +++ b/src/main/java/seedu/address/ui/ModuleCard2.java @@ -0,0 +1,67 @@ +package seedu.address.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; +import seedu.address.model.module.Module; + +/** + * An UI component that displays information of a {@code Module}. + */ +public class ModuleCard2 extends UiPart { + + private static final String FXML = "ModuleListCard2.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + public final Module module; + + @FXML + private Label code; + @FXML + private Label id; + @FXML + private Label credits; + @FXML + private Label semester; + @FXML + private Label year; + @FXML + private Label grade; + + public ModuleCard2(Module module) { + super(FXML); + this.module = module; + id.setText(""); // may be used to index module cards in the UI. + code.setText(module.getCode().value); + credits.setText(module.getCredits().value + ""); + semester.setText(module.getSemester().value); + year.setText(module.getYear().value + ""); + grade.setText(module.getGrade().value); + //module.getTags().forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ModuleCard2)) { + return false; + } + + // state check + ModuleCard2 card = (ModuleCard2) other; + return id.getText().equals(card.id.getText()) + && module.equals(card.module); + } +} diff --git a/src/main/java/seedu/address/ui/ModuleListPanel.java b/src/main/java/seedu/address/ui/ModuleListPanel.java new file mode 100644 index 000000000000..f2e4872f350a --- /dev/null +++ b/src/main/java/seedu/address/ui/ModuleListPanel.java @@ -0,0 +1,81 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.events.ui.ModulePanelSelectionChangedEvent; +import seedu.address.model.module.Module; + +/** + * Describes the UI design of first panel containing module list display. + */ +public class ModuleListPanel extends UiPart { + private static final String FXML = "ModuleListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(ModuleListPanel.class); + + @FXML + private ListView moduleListView; + + public ModuleListPanel(ObservableList moduleList) { + super(FXML); + setConnections(moduleList); + registerAsAnEventHandler(this); + } + + private void setConnections(ObservableList moduleList) { + moduleListView.setItems(moduleList); + moduleListView.setCellFactory(listView -> new ModuleListViewCell()); + setEventHandlerForSelectionChangeEvent(); + } + + private void setEventHandlerForSelectionChangeEvent() { + moduleListView.getSelectionModel().selectedItemProperty() + .addListener((observable, oldValue, newValue) -> { + if (newValue != null) { + logger.fine("Selection in module list panel changed to : '" + newValue + "'"); + raise(new ModulePanelSelectionChangedEvent(newValue)); + } + }); + } + + /** + * Scrolls to the {@code ModuleCard} at the {@code index} and selects it. + */ + //private void scrollTo(int index) { + // Platform.runLater(() -> { + // moduleListView.scrollTo(index); + // moduleListView.getSelectionModel().clearAndSelect(index); + // }); + //} + + + //@Subscribe + //private void handleJumpToListRequestEvent(JumpToListRequestEvent event) { + //logger.info(LogsCenter.getEventHandlingLogMessage(event)); + //scrollTo(event.targetIndex); + //} + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Module} using a {@code ModuleCard}. + */ + class ModuleListViewCell extends ListCell { + @Override + protected void updateItem(Module module, boolean empty) { + super.updateItem(module, empty); + + if (empty || module == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new ModuleCard(module).getRoot()); + } + } + } + + +} diff --git a/src/main/java/seedu/address/ui/ModuleListPanel2.java b/src/main/java/seedu/address/ui/ModuleListPanel2.java new file mode 100644 index 000000000000..0ea6e3cde324 --- /dev/null +++ b/src/main/java/seedu/address/ui/ModuleListPanel2.java @@ -0,0 +1,82 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.events.ui.ModulePanelSelectionChangedEvent; +import seedu.address.model.module.Module; + + +/** + * Describes the design of second UI panel containing module list display. + */ +public class ModuleListPanel2 extends UiPart { + private static final String FXML = "ModuleListPanel2.fxml"; + private final Logger logger = LogsCenter.getLogger(ModuleListPanel2.class); + + @FXML + private ListView moduleListView; + + public ModuleListPanel2(ObservableList moduleList) { + super(FXML); + setConnections(moduleList); + registerAsAnEventHandler(this); + } + + private void setConnections(ObservableList moduleList) { + moduleListView.setItems(moduleList); + moduleListView.setCellFactory(listView -> new ModuleListViewCell()); + setEventHandlerForSelectionChangeEvent(); + } + + private void setEventHandlerForSelectionChangeEvent() { + moduleListView.getSelectionModel().selectedItemProperty() + .addListener((observable, oldValue, newValue) -> { + if (newValue != null) { + logger.fine("Selection in module list panel changed to : '" + newValue + "'"); + raise(new ModulePanelSelectionChangedEvent(newValue)); + } + }); + } + + /** + * Scrolls to the {@code ModuleCard} at the {@code index} and selects it. + */ + //private void scrollTo(int index) { + //Platform.runLater(() -> { + //moduleListView.scrollTo(index); + //moduleListView.getSelectionModel().clearAndSelect(index); + //}); + //} + + + //@Subscribe + //private void handleJumpToListRequestEvent(JumpToListRequestEvent event) { + //logger.info(LogsCenter.getEventHandlingLogMessage(event)); + //scrollTo(event.targetIndex); + //} + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Module} using a {@code ModuleCard}. + */ + class ModuleListViewCell extends ListCell { + @Override + protected void updateItem(Module module, boolean empty) { + super.updateItem(module, empty); + + if (empty || module == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new ModuleCard2(module).getRoot()); + } + } + } + + +} diff --git a/src/main/java/seedu/address/ui/StatusBarFooter.java b/src/main/java/seedu/address/ui/StatusBarFooter.java index f6ba29502422..5a27877e8810 100644 --- a/src/main/java/seedu/address/ui/StatusBarFooter.java +++ b/src/main/java/seedu/address/ui/StatusBarFooter.java @@ -14,7 +14,7 @@ import javafx.fxml.FXML; import javafx.scene.layout.Region; import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.events.model.AddressBookChangedEvent; +import seedu.address.commons.events.model.TranscriptChangedEvent; /** * A ui for the status bar that is displayed at the footer of the application. @@ -73,11 +73,12 @@ private void setSyncStatus(String status) { Platform.runLater(() -> syncStatus.setText(status)); } + //@@author jeremyyew @Subscribe - public void handleAddressBookChangedEvent(AddressBookChangedEvent abce) { + public void handleTranscriptChangedEvent(TranscriptChangedEvent tce) { long now = clock.millis(); String lastUpdated = new Date(now).toString(); - logger.info(LogsCenter.getEventHandlingLogMessage(abce, "Setting last updated status to " + lastUpdated)); + logger.info(LogsCenter.getEventHandlingLogMessage(tce, "Setting last updated status to " + lastUpdated)); setSyncStatus(String.format(SYNC_STATUS_UPDATED, lastUpdated)); } } diff --git a/src/main/resources/view/CapPanel.fxml b/src/main/resources/view/CapPanel.fxml new file mode 100644 index 000000000000..4c742ad78082 --- /dev/null +++ b/src/main/resources/view/CapPanel.fxml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index c8941ea18263..279155dc6a06 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -13,14 +13,14 @@ .label-bright { -fx-font-size: 11pt; -fx-font-family: "Segoe UI Semibold"; - -fx-text-fill: white; + -fx-text-fill: green; -fx-opacity: 1; } .label-header { -fx-font-size: 32pt; -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; + -fx-text-fill: green; -fx-opacity: 1; } @@ -123,15 +123,76 @@ .cell_big_label { -fx-font-family: "Segoe UI Semibold"; -fx-font-size: 16px; - -fx-text-fill: #010504; + -fx-text-fill: white; } .cell_small_label { -fx-font-family: "Segoe UI"; -fx-font-size: 13px; - -fx-text-fill: #010504; + -fx-text-fill: white; +} + + +/*@author josephambe */ +.cell_title_label { + -fx-font-family: "Arial Black"; + -fx-font-weight: bold; + -fx-font-size: 24px; + -fx-text-fill: white; + /*-fx-stroke: white;*/ +} + + +/*@author josephambe */ +.cell_list_label { + -fx-font-family: "Arial Black"; + -fx-font-weight: bold; + -fx-font-size: 18px; + -fx-text-fill: lightblue; + /*-fx-stroke: white;*/ +} + +/*@author josephambe */ +.cell_grade_label { + -fx-font-family: "Arial Black"; + -fx-font-weight: bold; + -fx-font-size: 18px; + -fx-text-fill: red; + /*-fx-stroke: red;*/ +} + + +/*author Amber Joseph*/ +.circle_color { + -fx-stroke: "greenyellow"; + -fx-stroke-width: 5; + /*-fx-position-shape: true;*/ +} + +/*author Amber Joseph*/ +.circle_color2 { + -fx-stroke: "red"; + -fx-stroke-width: 5; + /*-fx-position-shape: true;*/ } +/*author josephambe */ +.cell_appName_label { + -fx-font-family: "Arial Black"; + -fx-font-weight: bold; + -fx-font-size: 18px; + -fx-text-fill: goldenrod; +} + +/*author josephambe */ +.cell_capPanel_title { + -fx-font-family: "Segoe UI Semibold"; + -fx-font-weight: bold; + -fx-font-size: 18px; + -fx-text-fill: goldenrod; +} + + .stack-pane { -fx-background-color: derive(#1d1d1d, 20%); } @@ -144,20 +205,40 @@ .status-bar { -fx-background-color: derive(#1d1d1d, 20%); - -fx-text-fill: black; + -fx-text-fill: white; } .result-display { -fx-background-color: transparent; -fx-font-family: "Segoe UI Light"; -fx-font-size: 13pt; - -fx-text-fill: white; + -fx-text-fill: orange; + } .result-display .label { -fx-text-fill: black !important; } +.cap-panel { + -fx-background-color: #515658; +} + +.cap-text { + -fx-font-family: "Segoe UI Semibold"; + -fx-font-weight: bold; + -fx-font-size: 14px; + -fx-fill: goldenrod; +} + +.cap-value { + -fx-font-family: "Arial Black"; + -fx-font-weight: bold; + -fx-font-size: 30px; + -fx-fill: goldenrod; +} + + .status-bar .label { -fx-font-family: "Segoe UI Light"; -fx-text-fill: white; @@ -198,8 +279,9 @@ .menu-bar .label { -fx-font-size: 14pt; -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; -fx-opacity: 0.9; + -fx-text-fill: lightblue; + } .menu .left-container { @@ -349,3 +431,14 @@ -fx-background-radius: 2; -fx-font-size: 11; } + + +#modulesRow { + -fx-background-color: derive(#1d1d1d, 20%); +} + + +#modulesScrollPane { + -fx-background-color: derive(#1d1d1d, 20%); +} + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index daf386d8f5b8..638681252b8c 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -7,62 +7,78 @@ + + + + - - - - - - - - - + + + + + + + + + + + + + +

+ + + + + + +